From cbb7ab5e8b2b833ec8284c07439327a5f5889138 Mon Sep 17 00:00:00 2001 From: chris48s Date: Wed, 13 Nov 2024 19:02:48 +0000 Subject: [PATCH] reduce overhead of NPM Last Update badge; test [npm] (#10666) * reduce overhead of [NpmLastUpdate] badge * use buildRoute for version without tag --- services/npm/npm-base.js | 26 +++++- services/npm/npm-last-update.service.js | 118 +++++++++++++++--------- services/npm/npm-last-update.tester.js | 42 +++++++-- 3 files changed, 131 insertions(+), 55 deletions(-) diff --git a/services/npm/npm-base.js b/services/npm/npm-base.js index cf1afeb89415c..033e3b0709a0b 100644 --- a/services/npm/npm-base.js +++ b/services/npm/npm-base.js @@ -81,8 +81,11 @@ export default class NpmBase extends BaseJsonService { } async _requestJson(data) { - return super._requestJson( - this.authHelper.withBearerAuthHeader({ + let payload + if (data?.options?.headers?.Accept) { + payload = data + } else { + payload = { ...data, options: { headers: { @@ -91,8 +94,9 @@ export default class NpmBase extends BaseJsonService { Accept: '*/*', }, }, - }), - ) + } + } + return super._requestJson(this.authHelper.withBearerAuthHeader(payload)) } async fetchPackageData({ registryUrl, scope, packageName, tag }) { @@ -144,7 +148,13 @@ export default class NpmBase extends BaseJsonService { return this.constructor._validate(packageData, packageDataSchema) } - async fetch({ registryUrl, scope, packageName, schema }) { + async fetch({ + registryUrl, + scope, + packageName, + schema, + abbreviated = false, + }) { registryUrl = registryUrl || this.constructor.defaultRegistryUrl let url @@ -158,9 +168,15 @@ export default class NpmBase extends BaseJsonService { url = `${registryUrl}/${scoped}` } + // https://github.com/npm/registry/blob/main/docs/responses/package-metadata.md + const options = abbreviated + ? { headers: { Accept: 'application/vnd.npm.install-v1+json' } } + : {} + return this._requestJson({ url, schema, + options, httpErrors: { 404: 'package not found' }, }) } diff --git a/services/npm/npm-last-update.service.js b/services/npm/npm-last-update.service.js index e68a424285030..5f3ded018bea0 100644 --- a/services/npm/npm-last-update.service.js +++ b/services/npm/npm-last-update.service.js @@ -3,13 +3,13 @@ import dayjs from 'dayjs' import { InvalidResponse, NotFound, pathParam, queryParam } from '../index.js' import { formatDate } from '../text-formatters.js' import { age as ageColor } from '../color-formatters.js' -import NpmBase, { packageNameDescription } from './npm-base.js' +import NpmBase, { + packageNameDescription, + queryParamSchema, +} from './npm-base.js' -const updateResponseSchema = Joi.object({ - time: Joi.object({ - created: Joi.string().required(), - modified: Joi.string().required(), - }) +const fullSchema = Joi.object({ + time: Joi.object() .pattern(Joi.string().required(), Joi.string().required()) .required(), 'dist-tags': Joi.object() @@ -17,21 +17,44 @@ const updateResponseSchema = Joi.object({ .required(), }).required() -export class NpmLastUpdate extends NpmBase { +const abbreviatedSchema = Joi.object({ + modified: Joi.string().required(), +}).required() + +class NpmLastUpdateBase extends NpmBase { static category = 'activity' - static route = this.buildRoute('npm/last-update', { withTag: true }) + static defaultBadgeData = { label: 'last updated' } + + static render({ date }) { + return { + message: formatDate(date), + color: ageColor(date), + } + } +} + +export class NpmLastUpdateWithTag extends NpmLastUpdateBase { + static route = { + base: 'npm/last-update', + pattern: ':scope(@[^/]+)?/:packageName/:tag', + queryParamSchema, + } static openApi = { - '/npm/last-update/{packageName}': { + '/npm/last-update/{packageName}/{tag}': { get: { - summary: 'NPM Last Update', + summary: 'NPM Last Update (with dist tag)', parameters: [ pathParam({ name: 'packageName', example: 'verdaccio', packageNameDescription, }), + pathParam({ + name: 'tag', + example: 'next-8', + }), queryParam({ name: 'registry_uri', example: 'https://registry.npmjs.com', @@ -39,19 +62,48 @@ export class NpmLastUpdate extends NpmBase { ], }, }, - '/npm/last-update/{packageName}/{tag}': { + } + + async handle(namedParams, queryParams) { + const { scope, packageName, tag, registryUrl } = + this.constructor.unpackParams(namedParams, queryParams) + + const packageData = await this.fetch({ + registryUrl, + scope, + packageName, + schema: fullSchema, + }) + + const tagVersion = packageData['dist-tags'][tag] + + if (!tagVersion) { + throw new NotFound({ prettyMessage: 'tag not found' }) + } + + const date = dayjs(packageData.time[tagVersion]) + + if (!date.isValid) { + throw new InvalidResponse({ prettyMessage: 'invalid date' }) + } + + return this.constructor.render({ date }) + } +} + +export class NpmLastUpdate extends NpmLastUpdateBase { + static route = this.buildRoute('npm/last-update', { withTag: false }) + + static openApi = { + '/npm/last-update/{packageName}': { get: { - summary: 'NPM Last Update (with dist tag)', + summary: 'NPM Last Update', parameters: [ pathParam({ name: 'packageName', example: 'verdaccio', packageNameDescription, }), - pathParam({ - name: 'tag', - example: 'next-8', - }), queryParam({ name: 'registry_uri', example: 'https://registry.npmjs.com', @@ -61,41 +113,21 @@ export class NpmLastUpdate extends NpmBase { }, } - static defaultBadgeData = { label: 'last updated' } - - static render({ date }) { - return { - message: formatDate(date), - color: ageColor(date), - } - } - async handle(namedParams, queryParams) { - const { scope, packageName, tag, registryUrl } = - this.constructor.unpackParams(namedParams, queryParams) + const { scope, packageName, registryUrl } = this.constructor.unpackParams( + namedParams, + queryParams, + ) const packageData = await this.fetch({ registryUrl, scope, packageName, - schema: updateResponseSchema, + schema: abbreviatedSchema, + abbreviated: true, }) - let date - - if (tag) { - const tagVersion = packageData['dist-tags'][tag] - - if (!tagVersion) { - throw new NotFound({ prettyMessage: 'tag not found' }) - } - - date = dayjs(packageData.time[tagVersion]) - } else { - const timeKey = packageData.time.modified ? 'modified' : 'created' - - date = dayjs(packageData.time[timeKey]) - } + const date = dayjs(packageData.modified) if (!date.isValid) { throw new InvalidResponse({ prettyMessage: 'invalid date' }) diff --git a/services/npm/npm-last-update.tester.js b/services/npm/npm-last-update.tester.js index 8a0efa64528ba..7fcead5b5f534 100644 --- a/services/npm/npm-last-update.tester.js +++ b/services/npm/npm-last-update.tester.js @@ -3,51 +3,79 @@ import { createServiceTester } from '../tester.js' export const t = await createServiceTester() -t.create('last updated date (valid package)') +t.create('last updated date, no tag, valid package') .get('/verdaccio.json') .expectBadge({ label: 'last updated', message: isFormattedDate, }) -t.create('last updated date (invalid package)') +t.create('last updated date, no tag, invalid package') .get('/not-a-package.json') .expectBadge({ label: 'last updated', message: 'package not found', }) -t.create('last update from custom repository (valid scenario)') +t.create('last updated date, no tag, custom repository, valid package') .get('/verdaccio.json?registry_uri=https://registry.npmjs.com') .expectBadge({ label: 'last updated', message: isFormattedDate, }) -t.create('last update scoped package (valid scenario)') +t.create('last updated date, no tag, valid package with scope') .get('/@npm/types.json') .expectBadge({ label: 'last updated', message: isFormattedDate, }) -t.create('last update scoped package (invalid scenario)') +t.create('last updated date, no tag, invalid package with scope') .get('/@not-a-scoped-package/not-a-valid-package.json') .expectBadge({ label: 'last updated', message: 'package not found', }) -t.create('last updated date with tag (valid scenario)') +t.create('last updated date, with tag, valid package') .get('/verdaccio/latest.json') .expectBadge({ label: 'last updated', message: isFormattedDate, }) -t.create('last updated date (invalid tag)') +t.create('last updated date, with tag, invalid package') + .get('/not-a-package/doesnt-matter.json') + .expectBadge({ + label: 'last updated', + message: 'package not found', + }) + +t.create('last updated date, with tag, invalid tag') .get('/verdaccio/not-a-valid-tag.json') .expectBadge({ label: 'last updated', message: 'tag not found', }) + +t.create('last updated date, with tag, custom repository, valid package') + .get('/verdaccio/latest.json?registry_uri=https://registry.npmjs.com') + .expectBadge({ + label: 'last updated', + message: isFormattedDate, + }) + +t.create('last updated date, with tag, valid package with scope') + .get('/@npm/types/latest.json') + .expectBadge({ + label: 'last updated', + message: isFormattedDate, + }) + +t.create('last updated date, with tag, invalid package with scope') + .get('/@not-a-scoped-package/not-a-valid-package/doesnt-matter.json') + .expectBadge({ + label: 'last updated', + message: 'package not found', + })