diff --git a/services/npm/npm-base.js b/services/npm/npm-base.js index cf1afeb894..033e3b0709 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 e68a424285..5f3ded018b 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,28 +17,31 @@ 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}': { - get: { - summary: 'NPM Last Update', - parameters: [ - pathParam({ - name: 'packageName', - example: 'verdaccio', - packageNameDescription, - }), - queryParam({ - name: 'registry_uri', - example: 'https://registry.npmjs.com', - }), - ], - }, - }, '/npm/last-update/{packageName}/{tag}': { get: { summary: 'NPM Last Update (with dist tag)', @@ -61,15 +64,6 @@ 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) @@ -78,25 +72,63 @@ export class NpmLastUpdate extends NpmBase { registryUrl, scope, packageName, - schema: updateResponseSchema, + schema: fullSchema, }) - let date + const tagVersion = packageData['dist-tags'][tag] - 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]) + 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', + parameters: [ + pathParam({ + name: 'packageName', + example: 'verdaccio', + packageNameDescription, + }), + queryParam({ + name: 'registry_uri', + example: 'https://registry.npmjs.com', + }), + ], + }, + }, + } + + async handle(namedParams, queryParams) { + const { scope, packageName, registryUrl } = this.constructor.unpackParams( + namedParams, + queryParams, + ) + + const packageData = await this.fetch({ + registryUrl, + scope, + packageName, + schema: abbreviatedSchema, + abbreviated: true, + }) + + 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 8a0efa6452..7fcead5b5f 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', + })