1
0
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:
Mateusz Kwasniewski 2023-10-13 12:09:46 +02:00 committed by GitHub
parent 36ae842248
commit 3eeafba5f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 102 additions and 6 deletions

View File

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

View File

@ -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.',

View File

@ -1533,6 +1533,10 @@ class FeatureToggleService {
);
}
async validateArchiveToggles(featureNames: string[]): Promise<string[]> {
return this.dependentFeaturesReadModel.getOrphanParents(featureNames);
}
async unprotectedArchiveToggles(
featureNames: string[],
createdBy: string,

View File

@ -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',

View File

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

View File

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