From 80ade9780195d023e61ff5c5e006b6d51122eaad Mon Sep 17 00:00:00 2001 From: "Jian-Chen Chen (jesse)" Date: Sat, 20 Jun 2020 04:54:03 +0800 Subject: [PATCH] feat: npm deprecation support (#1842) * support deprecation * test case for deprecation * fix format * testing for multiple packages deprecation * update README Co-authored-by: Juan Picado --- README.md | 2 +- src/api/endpoint/api/publish.ts | 10 ++- src/lib/local-storage.ts | 17 ++++- src/lib/utils.ts | 10 +++ test/unit/__helper/api.ts | 3 +- test/unit/__helper/utils.ts | 9 +++ test/unit/modules/api/api.spec.ts | 77 +++++++++++++++++++- test/unit/partials/config/yaml/api.spec.yaml | 8 ++ 8 files changed, 127 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 06a37a01b..a4be9ce44 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ Verdaccio aims to support all features of a standard npm client that make sense - Unpublishing packages (npm unpublish) - **supported** - Tagging (npm tag) - **supported** -- Deprecation (npm deprecate) - not supported - *PR-welcome* +- Deprecation (npm deprecate) - supported ### User management diff --git a/src/api/endpoint/api/publish.ts b/src/api/endpoint/api/publish.ts index 22195a8ea..de9446b3f 100644 --- a/src/api/endpoint/api/publish.ts +++ b/src/api/endpoint/api/publish.ts @@ -3,7 +3,7 @@ import Path from 'path'; import mime from 'mime'; import { API_MESSAGE, HEADERS, DIST_TAGS, API_ERROR, HTTP_STATUS } from '../../../lib/constants'; -import {validateMetadata, isObject, ErrorCode, hasDiffOneKey} from '../../../lib/utils'; +import {validateMetadata, isObject, ErrorCode, hasDiffOneKey, isRelatedToDeprecation} from '../../../lib/utils'; import { media, expectJson, allow } from '../../middleware'; import { notify } from '../../../lib/notify'; import star from './star'; @@ -144,8 +144,9 @@ export function publishPackage(storage: IStorageHandler, config: Config, auth: I const { _attachments, versions } = metadataCopy; - // if the is no attachments, it is change, it is a new package. - if (_.isNil(_attachments)) { + // `npm star` wouldn't have attachments + // and `npm deprecate` would have attachments as a empty object, i.e {} + if (_.isNil(_attachments) || JSON.stringify(_attachments) === '{}') { if (error) { return next(error); } @@ -214,7 +215,8 @@ export function publishPackage(storage: IStorageHandler, config: Config, auth: I try { const metadata = validateMetadata(req.body, packageName); - if (req.params._rev) { + // treating deprecation as updating a package + if (req.params._rev || isRelatedToDeprecation(req.body)) { logger.debug({packageName} , `updating a new version for @{packageName}`); // we check unpublish permissions, an update is basically remove versions const remote = req.remote_user; diff --git a/src/lib/local-storage.ts b/src/lib/local-storage.ts index 71e94b385..11d5a2dd7 100644 --- a/src/lib/local-storage.ts +++ b/src/lib/local-storage.ts @@ -320,7 +320,7 @@ class LocalStorage implements IStorage { /** * Update the package metadata, tags and attachments (tarballs). - * Note: Currently supports unpublishing only. + * Note: Currently supports unpublishing and deprecation. * @param {*} name * @param {*} incomingPkg * @param {*} revision @@ -338,7 +338,8 @@ class LocalStorage implements IStorage { name, (localData: Package, cb: CallbackAction): void => { for (const version in localData.versions) { - if (_.isNil(incomingPkg.versions[version])) { + const incomingVersion = incomingPkg.versions[version]; + if (_.isNil(incomingVersion)) { this.logger.info({ name: name, version: version }, 'unpublishing @{name}@@{version}'); // FIXME: I prefer return a new object rather mutate the metadata @@ -350,6 +351,18 @@ class LocalStorage implements IStorage { delete localData._attachments[file].version; } } + } else if (Object.prototype.hasOwnProperty.call(incomingVersion, 'deprecated')) { + const incomingDeprecated = incomingVersion.deprecated; + if (incomingDeprecated != localData.versions[version].deprecated) { + if (!incomingDeprecated) { + this.logger.info({ name: name, version: version }, 'undeprecating @{name}@@{version}'); + delete localData.versions[version].deprecated; + } else { + this.logger.info({ name: name, version: version }, 'deprecating @{name}@@{version}'); + localData.versions[version].deprecated = incomingDeprecated; + } + localData.time!.modified = new Date().toISOString(); + } } } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 32ab7c5aa..47e9398d6 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -635,3 +635,13 @@ export function isVersionValid(packageMeta, packageVersion): boolean { const hasMatchVersion = Object.keys(packageMeta.versions).includes(packageVersion); return hasMatchVersion; } + +export function isRelatedToDeprecation(pkgInfo: Package): boolean { + const { versions } = pkgInfo; + for (const version in versions) { + if (Object.prototype.hasOwnProperty.call(versions[version], 'deprecated')) { + return true; + } + } + return false; +} diff --git a/test/unit/__helper/api.ts b/test/unit/__helper/api.ts index c3d41c9d8..3598e8d92 100644 --- a/test/unit/__helper/api.ts +++ b/test/unit/__helper/api.ts @@ -68,7 +68,8 @@ export function getPackage( return new Promise((resolve) => { let getRequest = request.get(`/${pkgName}`); - if (_.isNil(token) === false || _.isEmpty(token) === false) { + // token is a string + if (token !== '') { getRequest.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token)); } diff --git a/test/unit/__helper/utils.ts b/test/unit/__helper/utils.ts index 9968671c4..5d567b60a 100644 --- a/test/unit/__helper/utils.ts +++ b/test/unit/__helper/utils.ts @@ -158,3 +158,12 @@ export function generatePackageMetadata(pkgName: string, version = '1.0.0'): Pac } } } + +export function generateDeprecateMetadata(pkgName: string, version = '1.0.0', deprecated:string = ''): Package { + const res = { + ...generatePackageMetadata(pkgName, version), + _attachments: {}, + }; + res.versions[version].deprecated = deprecated; + return res; +} diff --git a/test/unit/modules/api/api.spec.ts b/test/unit/modules/api/api.spec.ts index 037a82147..78a0db270 100644 --- a/test/unit/modules/api/api.spec.ts +++ b/test/unit/modules/api/api.spec.ts @@ -20,10 +20,17 @@ import {DOMAIN_SERVERS} from '../../../functional/config.functional'; import {buildToken, encodeScopedUri} from '../../../../src/lib/utils'; import { getNewToken, + getPackage, putPackage, verifyPackageVersionDoesExist, generateUnPublishURI } from '../../__helper/api'; -import {generatePackageMetadata, generatePackageUnpublish, generateStarMedatada} from '../../__helper/utils'; +import { + generatePackageMetadata, + generatePackageUnpublish, + generateStarMedatada, + generateDeprecateMetadata, + generateVersion, +} from '../../__helper/utils'; require('../../../../src/lib/logger').setup([ { type: 'stdout', format: 'pretty', level: 'warn' } @@ -903,5 +910,73 @@ describe('endpoint unit test', () => { }); }); }); + + describe('should test (un)deprecate api', () => { + const pkgName = '@scope/deprecate'; + const credentials = { name: 'jota_deprecate', password: 'secretPass' }; + const version = '1.0.0' + let token = ''; + beforeAll(async (done) =>{ + token = await getNewToken(request(app), credentials); + await putPackage(request(app), `/${pkgName}`, generatePackageMetadata(pkgName, version), token); + done(); + }); + + test('should deprecate a package', async (done) => { + const pkg = generateDeprecateMetadata(pkgName, version, 'get deprecated'); + const [err] = await putPackage(request(app), `/${encodeScopedUri(pkgName)}`, pkg, token); + if (err) { + expect(err).toBeNull(); + return done(err); + } + const [,res] = await getPackage(request(app), '', pkgName); + expect(res.body.versions[version].deprecated).toEqual('get deprecated'); + done(); + }); + + test('should undeprecate a package', async (done) => { + let pkg = generateDeprecateMetadata(pkgName, version, 'get deprecated'); + await putPackage(request(app), `/${encodeScopedUri(pkgName)}`, pkg, token); + pkg = generateDeprecateMetadata(pkgName, version, ''); + const [err] = await putPackage(request(app), `/${encodeScopedUri(pkgName)}`, pkg, token); + if (err) { + expect(err).toBeNull(); + return done(err); + } + const [,res] = await getPackage(request(app), '', pkgName); + expect(res.body.versions[version].deprecated).not.toBeDefined(); + done(); + }); + + test('should require both publish and unpublish access to (un)deprecate a package', async () => { + let credentials = { name: 'only_publish', password: 'secretPass' }; + let token = await getNewToken(request(app), credentials); + const pkg = generateDeprecateMetadata(pkgName, version, 'get deprecated'); + const [err, res] = await putPackage(request(app), `/${encodeScopedUri(pkgName)}`, pkg, token); + expect(err).not.toBeNull(); + expect(res.body.error).toBeDefined(); + expect(res.body.error).toMatch(/user only_publish is not allowed to unpublish package @scope\/deprecate/); + credentials = { name: 'only_unpublish', password: 'secretPass' }; + token = await getNewToken(request(app), credentials); + const [err2, res2] = await putPackage(request(app), `/${encodeScopedUri(pkgName)}`, pkg, token); + expect(err2).not.toBeNull(); + expect(res2.body.error).toBeDefined(); + expect(res2.body.error).toMatch(/user only_unpublish is not allowed to publish package @scope\/deprecate/); + }) + + test('should deprecate multiple packages', async (done) => { + await putPackage(request(app), `/${pkgName}`, generatePackageMetadata(pkgName, '1.0.1'), token); + const pkg = generateDeprecateMetadata(pkgName, version, 'get deprecated'); + pkg.versions['1.0.1'] = { + ...generateVersion(pkgName, '1.0.1'), + deprecated: 'get deprecated', + }; + await putPackage(request(app), `/${encodeScopedUri(pkgName)}`, pkg, token); + const [,res] = await getPackage(request(app), '', pkgName); + expect(res.body.versions[version].deprecated).toEqual('get deprecated'); + expect(res.body.versions['1.0.1'].deprecated).toEqual('get deprecated'); + done() + }) + }); }); }); diff --git a/test/unit/partials/config/yaml/api.spec.yaml b/test/unit/partials/config/yaml/api.spec.yaml index be28a2fd6..432e38e55 100644 --- a/test/unit/partials/config/yaml/api.spec.yaml +++ b/test/unit/partials/config/yaml/api.spec.yaml @@ -7,6 +7,14 @@ packages: access: $anonymous jota_unpublish publish: $anonymous jota_unpublish unpublish: $anonymous jota_unpublish + '@scope/deprecate': + access: $all + publish: + - jota_deprecate + - only_publish + unpublish: + - jota_deprecate + - only_unpublish '@scope/starPackage': access: $all publish: jota_star