mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-23 00:22:19 +01:00
feat: validate archive dependent features (#5019)
This commit is contained in:
parent
36ae842248
commit
3eeafba5f9
@ -16,6 +16,11 @@ export const createKnexTransactionStarter = (
|
||||
function transaction<T>(
|
||||
scope: (trx: KnexTransaction) => void | Promise<T>,
|
||||
) {
|
||||
if (!knex) {
|
||||
console.warn(
|
||||
'It looks like your DB is not provided. Very often it is a test setup problem in setupAppWithCustomConfig',
|
||||
);
|
||||
}
|
||||
return knex.transaction(scope);
|
||||
}
|
||||
return transaction;
|
||||
|
@ -21,7 +21,6 @@ import { IAuthRequest } from '../../routes/unleash-types';
|
||||
import { InvalidOperationError } from '../../error';
|
||||
import { DependentFeaturesService } from './dependent-features-service';
|
||||
import { TransactionCreator, UnleashTransaction } from '../../db/transaction';
|
||||
import { extractUsernameFromUser } from '../../util';
|
||||
|
||||
interface ProjectParams {
|
||||
projectId: string;
|
||||
@ -91,7 +90,7 @@ export default class DependentFeaturesController extends Controller {
|
||||
permission: UPDATE_FEATURE_DEPENDENCY,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Features'],
|
||||
tags: ['Dependencies'],
|
||||
summary: 'Add a feature dependency.',
|
||||
description:
|
||||
'Add a dependency to a parent feature. Each environment will resolve corresponding dependency independently.',
|
||||
@ -115,7 +114,7 @@ export default class DependentFeaturesController extends Controller {
|
||||
acceptAnyContentType: true,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Features'],
|
||||
tags: ['Dependencies'],
|
||||
summary: 'Deletes a feature dependency.',
|
||||
description: 'Remove a dependency to a parent feature.',
|
||||
operationId: 'deleteFeatureDependency',
|
||||
@ -135,7 +134,7 @@ export default class DependentFeaturesController extends Controller {
|
||||
acceptAnyContentType: true,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Features'],
|
||||
tags: ['Dependencies'],
|
||||
summary: 'Deletes feature dependencies.',
|
||||
description: 'Remove dependencies to all parent features.',
|
||||
operationId: 'deleteFeatureDependencies',
|
||||
@ -154,7 +153,7 @@ export default class DependentFeaturesController extends Controller {
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Features'],
|
||||
tags: ['Dependencies'],
|
||||
summary: 'List parent options.',
|
||||
description:
|
||||
'List available parents who have no transitive dependencies.',
|
||||
|
@ -1533,6 +1533,10 @@ class FeatureToggleService {
|
||||
);
|
||||
}
|
||||
|
||||
async validateArchiveToggles(featureNames: string[]): Promise<string[]> {
|
||||
return this.dependentFeaturesReadModel.getOrphanParents(featureNames);
|
||||
}
|
||||
|
||||
async unprotectedArchiveToggles(
|
||||
featureNames: string[],
|
||||
createdBy: string,
|
||||
|
@ -39,6 +39,10 @@ const OPENAPI_TAGS = [
|
||||
description:
|
||||
'Create, update, and delete [context fields](https://docs.getunleash.io/reference/unleash-context) that Unleash is aware of.',
|
||||
},
|
||||
{
|
||||
name: 'Dependencies',
|
||||
description: 'Manage feature dependencies.',
|
||||
},
|
||||
{ name: 'Edge', description: 'Endpoints related to Unleash on the Edge.' },
|
||||
{
|
||||
name: 'Environments',
|
||||
|
@ -16,7 +16,11 @@ import {
|
||||
emptyResponse,
|
||||
getStandardResponses,
|
||||
} from '../../../openapi/util/standard-responses';
|
||||
import { BatchFeaturesSchema, createRequestSchema } from '../../../openapi';
|
||||
import {
|
||||
BatchFeaturesSchema,
|
||||
createRequestSchema,
|
||||
createResponseSchema,
|
||||
} from '../../../openapi';
|
||||
import Controller from '../../controller';
|
||||
import {
|
||||
TransactionCreator,
|
||||
@ -25,6 +29,7 @@ import {
|
||||
|
||||
const PATH = '/:projectId';
|
||||
const PATH_ARCHIVE = `${PATH}/archive`;
|
||||
const PATH_VALIDATE_ARCHIVE = `${PATH}/archive/validate`;
|
||||
const PATH_DELETE = `${PATH}/delete`;
|
||||
const PATH_REVIVE = `${PATH}/revive`;
|
||||
|
||||
@ -109,6 +114,27 @@ export default class ProjectArchiveController extends Controller {
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: PATH_VALIDATE_ARCHIVE,
|
||||
handler: this.validateArchiveFeatures,
|
||||
permission: DELETE_FEATURE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Features'],
|
||||
operationId: 'validateArchiveFeatures',
|
||||
description:
|
||||
'This endpoint validated if a list of features can be archived. Returns a list of parent features that would orphan some child features. If archive can process then empty list is returned.',
|
||||
summary: 'Validates if a list of features can be archived',
|
||||
requestBody: createRequestSchema('batchFeaturesSchema'),
|
||||
responses: {
|
||||
200: createResponseSchema('batchFeaturesSchema'),
|
||||
...getStandardResponses(400, 401, 403, 415),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: PATH_ARCHIVE,
|
||||
@ -169,6 +195,18 @@ export default class ProjectArchiveController extends Controller {
|
||||
await this.featureService.archiveToggles(features, req.user, projectId);
|
||||
res.status(202).end();
|
||||
}
|
||||
|
||||
async validateArchiveFeatures(
|
||||
req: IAuthRequest<IProjectParam, void, BatchFeaturesSchema>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { features } = req.body;
|
||||
|
||||
const offendingParents =
|
||||
await this.featureService.validateArchiveToggles(features);
|
||||
|
||||
res.send(offendingParents);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ProjectArchiveController;
|
||||
|
@ -17,6 +17,7 @@ beforeAll(async () => {
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
dependentFeatures: true,
|
||||
disableEnvsOnRevive: true,
|
||||
},
|
||||
},
|
||||
@ -249,3 +250,48 @@ test('Should be able to bulk archive features', async () => {
|
||||
);
|
||||
expect(archivedFeatures).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('Should validate if a list of features with dependencies can be archived', async () => {
|
||||
const child1 = 'child1Feature';
|
||||
const child2 = 'child2Feature';
|
||||
const parent = 'parentFeature';
|
||||
|
||||
await app.createFeature(child1);
|
||||
await app.createFeature(child2);
|
||||
await app.createFeature(parent);
|
||||
await app.addDependency(child1, parent);
|
||||
await app.addDependency(child2, parent);
|
||||
|
||||
const { body: allChildrenAndParent } = await app.request
|
||||
.post(`/api/admin/projects/${DEFAULT_PROJECT}/archive/validate`)
|
||||
.send({
|
||||
features: [child1, child2, parent],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body: allChildren } = await app.request
|
||||
.post(`/api/admin/projects/${DEFAULT_PROJECT}/archive/validate`)
|
||||
.send({
|
||||
features: [child1, child2],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body: onlyParent } = await app.request
|
||||
.post(`/api/admin/projects/${DEFAULT_PROJECT}/archive/validate`)
|
||||
.send({
|
||||
features: [parent],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body: oneChildAndParent } = await app.request
|
||||
.post(`/api/admin/projects/${DEFAULT_PROJECT}/archive/validate`)
|
||||
.send({
|
||||
features: [child1, parent],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(allChildrenAndParent).toEqual([]);
|
||||
expect(allChildren).toEqual([]);
|
||||
expect(onlyParent).toEqual([parent]);
|
||||
expect(oneChildAndParent).toEqual([parent]);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user