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:
parent
67c31b69ca
commit
80ade97801
8 changed files with 127 additions and 9 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
})
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue