diff --git a/services/npm-stat/npm-stat-downloads.service.js b/services/npm-stat/npm-stat-downloads.service.js new file mode 100644 index 0000000000000000000000000000000000000000..99e4a28bdd137837222e0e6a021d0e68a48400bf --- /dev/null +++ b/services/npm-stat/npm-stat-downloads.service.js @@ -0,0 +1,71 @@ +import Joi from 'joi' +import dayjs from 'dayjs' +import { nonNegativeInteger } from '../validators.js' +import { BaseJsonService } from '../index.js' +import { renderDownloadsBadge } from '../downloads.js' + +const schema = Joi.object() + .pattern(Joi.string(), Joi.object().pattern(Joi.string(), nonNegativeInteger)) + .required() + +const intervalMap = { + dw: { interval: 'week' }, + dm: { interval: 'month' }, + dy: { interval: 'year' }, +} + +export default class NpmStatDownloads extends BaseJsonService { + static category = 'downloads' + + static route = { + base: 'npm-stat', + pattern: ':interval(dw|dm|dy)/:author', + } + + static examples = [ + { + title: 'npm (by author)', + documentation: + 'The total number of downloads of npm packages published by the specified author from [npm-stat](https://npm-stat.com).', + namedParams: { interval: 'dy', author: 'dukeluo' }, + staticPreview: this.render({ interval: 'dy', downloadCount: 30000 }), + keywords: ['node'], + }, + ] + + static _cacheLength = 21600 + + static defaultBadgeData = { label: 'downloads' } + + static getTotalDownloads(data) { + const add = (x, y) => x + y + const sum = nums => nums.reduce(add, 0) + + return Object.values(data).reduce( + (count, packageDownloads) => count + sum(Object.values(packageDownloads)), + 0, + ) + } + + static render({ interval, downloads }) { + return renderDownloadsBadge({ + downloads, + interval: intervalMap[interval].interval, + colorOverride: downloads > 0 ? 'brightgreen' : 'red', + }) + } + + async handle({ interval, author }) { + const unit = intervalMap[interval].interval + const today = dayjs() + const until = today.format('YYYY-MM-DD') + const from = today.subtract(1, unit).format('YYYY-MM-DD') + const data = await this._requestJson({ + url: `https://npm-stat.com/api/download-counts?author=${author}&from=${from}&until=${until}`, + schema, + }) + const downloads = this.constructor.getTotalDownloads(data) + + return this.constructor.render({ interval, downloads }) + } +} diff --git a/services/npm-stat/npm-stat-downloads.spec.js b/services/npm-stat/npm-stat-downloads.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..1932b9b650a7a9af3caac9dd196ad73aa01097fe --- /dev/null +++ b/services/npm-stat/npm-stat-downloads.spec.js @@ -0,0 +1,25 @@ +import { test, given } from 'sazerac' +import NpmStatDownloads from './npm-stat-downloads.service.js' + +describe('NpmStatDownloads helpers', function () { + test(NpmStatDownloads.getTotalDownloads, () => { + given({ + 'hexo-theme-candelas': { + '2022-12-01': 1, + '2022-12-02': 2, + '2022-12-03': 3, + }, + '@dukeluo/fanjs': { + '2022-12-01': 10, + '2022-12-02': 20, + '2022-12-03': 30, + }, + 'eslint-plugin-check-file': { + '2022-12-01': 100, + '2022-12-02': 200, + '2022-12-03': 300, + }, + }).expect(666) + given({}).expect(0) + }) +}) diff --git a/services/npm-stat/npm-stat-downloads.tester.js b/services/npm-stat/npm-stat-downloads.tester.js new file mode 100644 index 0000000000000000000000000000000000000000..830a4208f204cfb6de6af844674a6b1a8c8971e3 --- /dev/null +++ b/services/npm-stat/npm-stat-downloads.tester.js @@ -0,0 +1,35 @@ +import { isMetricOverTimePeriod } from '../test-validators.js' +import { createServiceTester } from '../tester.js' +export const t = await createServiceTester() + +t.create('weekly downloads of npm author dukeluo') + .get('/dw/dukeluo.json') + .expectBadge({ + label: 'downloads', + message: isMetricOverTimePeriod, + color: 'brightgreen', + }) + +t.create('monthly downloads of npm author dukeluo') + .get('/dm/dukeluo.json') + .expectBadge({ + label: 'downloads', + message: isMetricOverTimePeriod, + color: 'brightgreen', + }) + +t.create('yearly downloads of npm author dukeluo') + .get('/dy/dukeluo.json') + .expectBadge({ + label: 'downloads', + message: isMetricOverTimePeriod, + color: 'brightgreen', + }) + +t.create('downloads of unknown npm package author') + .get('/dy/npm-api-does-not-have-this-package-author.json') + .expectBadge({ + label: 'downloads', + message: '0/year', + color: 'red', + })