0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-12-16 21:56:25 -05:00

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 <juanpicado19@gmail.com>
This commit is contained in:
Jian-Chen Chen (jesse) 2020-06-20 04:54:03 +08:00 committed by GitHub
parent 67c31b69ca
commit 80ade97801
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 127 additions and 9 deletions

View file

@ -171,7 +171,7 @@ Verdaccio aims to support all features of a standard npm client that make sense
- Unpublishing packages (npm unpublish) - **supported** - Unpublishing packages (npm unpublish) - **supported**
- Tagging (npm tag) - **supported** - Tagging (npm tag) - **supported**
- Deprecation (npm deprecate) - not supported - *PR-welcome* - Deprecation (npm deprecate) - supported
### User management ### User management

View file

@ -3,7 +3,7 @@ import Path from 'path';
import mime from 'mime'; import mime from 'mime';
import { API_MESSAGE, HEADERS, DIST_TAGS, API_ERROR, HTTP_STATUS } from '../../../lib/constants'; 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 { media, expectJson, allow } from '../../middleware';
import { notify } from '../../../lib/notify'; import { notify } from '../../../lib/notify';
import star from './star'; import star from './star';
@ -144,8 +144,9 @@ export function publishPackage(storage: IStorageHandler, config: Config, auth: I
const { _attachments, versions } = metadataCopy; const { _attachments, versions } = metadataCopy;
// if the is no attachments, it is change, it is a new package. // `npm star` wouldn't have attachments
if (_.isNil(_attachments)) { // and `npm deprecate` would have attachments as a empty object, i.e {}
if (_.isNil(_attachments) || JSON.stringify(_attachments) === '{}') {
if (error) { if (error) {
return next(error); return next(error);
} }
@ -214,7 +215,8 @@ export function publishPackage(storage: IStorageHandler, config: Config, auth: I
try { try {
const metadata = validateMetadata(req.body, packageName); 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}`); logger.debug({packageName} , `updating a new version for @{packageName}`);
// we check unpublish permissions, an update is basically remove versions // we check unpublish permissions, an update is basically remove versions
const remote = req.remote_user; const remote = req.remote_user;

View file

@ -320,7 +320,7 @@ class LocalStorage implements IStorage {
/** /**
* Update the package metadata, tags and attachments (tarballs). * Update the package metadata, tags and attachments (tarballs).
* Note: Currently supports unpublishing only. * Note: Currently supports unpublishing and deprecation.
* @param {*} name * @param {*} name
* @param {*} incomingPkg * @param {*} incomingPkg
* @param {*} revision * @param {*} revision
@ -338,7 +338,8 @@ class LocalStorage implements IStorage {
name, name,
(localData: Package, cb: CallbackAction): void => { (localData: Package, cb: CallbackAction): void => {
for (const version in localData.versions) { 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}'); this.logger.info({ name: name, version: version }, 'unpublishing @{name}@@{version}');
// FIXME: I prefer return a new object rather mutate the metadata // FIXME: I prefer return a new object rather mutate the metadata
@ -350,6 +351,18 @@ class LocalStorage implements IStorage {
delete localData._attachments[file].version; 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();
}
} }
} }

View file

@ -635,3 +635,13 @@ export function isVersionValid(packageMeta, packageVersion): boolean {
const hasMatchVersion = Object.keys(packageMeta.versions).includes(packageVersion); const hasMatchVersion = Object.keys(packageMeta.versions).includes(packageVersion);
return hasMatchVersion; 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;
}

View file

@ -68,7 +68,8 @@ export function getPackage(
return new Promise((resolve) => { return new Promise((resolve) => {
let getRequest = request.get(`/${pkgName}`); 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)); getRequest.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token));
} }

View file

@ -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;
}

View file

@ -20,10 +20,17 @@ import {DOMAIN_SERVERS} from '../../../functional/config.functional';
import {buildToken, encodeScopedUri} from '../../../../src/lib/utils'; import {buildToken, encodeScopedUri} from '../../../../src/lib/utils';
import { import {
getNewToken, getNewToken,
getPackage,
putPackage, putPackage,
verifyPackageVersionDoesExist, generateUnPublishURI verifyPackageVersionDoesExist, generateUnPublishURI
} from '../../__helper/api'; } from '../../__helper/api';
import {generatePackageMetadata, generatePackageUnpublish, generateStarMedatada} from '../../__helper/utils'; import {
generatePackageMetadata,
generatePackageUnpublish,
generateStarMedatada,
generateDeprecateMetadata,
generateVersion,
} from '../../__helper/utils';
require('../../../../src/lib/logger').setup([ require('../../../../src/lib/logger').setup([
{ type: 'stdout', format: 'pretty', level: 'warn' } { 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()
})
});
}); });
}); });

View file

@ -7,6 +7,14 @@ packages:
access: $anonymous jota_unpublish access: $anonymous jota_unpublish
publish: $anonymous jota_unpublish publish: $anonymous jota_unpublish
unpublish: $anonymous jota_unpublish unpublish: $anonymous jota_unpublish
'@scope/deprecate':
access: $all
publish:
- jota_deprecate
- only_publish
unpublish:
- jota_deprecate
- only_unpublish
'@scope/starPackage': '@scope/starPackage':
access: $all access: $all
publish: jota_star publish: jota_star