mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-27 13:49:10 +02: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>(
|
function transaction<T>(
|
||||||
scope: (trx: KnexTransaction) => void | Promise<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 knex.transaction(scope);
|
||||||
}
|
}
|
||||||
return transaction;
|
return transaction;
|
||||||
|
@ -21,7 +21,6 @@ import { IAuthRequest } from '../../routes/unleash-types';
|
|||||||
import { InvalidOperationError } from '../../error';
|
import { InvalidOperationError } from '../../error';
|
||||||
import { DependentFeaturesService } from './dependent-features-service';
|
import { DependentFeaturesService } from './dependent-features-service';
|
||||||
import { TransactionCreator, UnleashTransaction } from '../../db/transaction';
|
import { TransactionCreator, UnleashTransaction } from '../../db/transaction';
|
||||||
import { extractUsernameFromUser } from '../../util';
|
|
||||||
|
|
||||||
interface ProjectParams {
|
interface ProjectParams {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -91,7 +90,7 @@ export default class DependentFeaturesController extends Controller {
|
|||||||
permission: UPDATE_FEATURE_DEPENDENCY,
|
permission: UPDATE_FEATURE_DEPENDENCY,
|
||||||
middleware: [
|
middleware: [
|
||||||
openApiService.validPath({
|
openApiService.validPath({
|
||||||
tags: ['Features'],
|
tags: ['Dependencies'],
|
||||||
summary: 'Add a feature dependency.',
|
summary: 'Add a feature dependency.',
|
||||||
description:
|
description:
|
||||||
'Add a dependency to a parent feature. Each environment will resolve corresponding dependency independently.',
|
'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,
|
acceptAnyContentType: true,
|
||||||
middleware: [
|
middleware: [
|
||||||
openApiService.validPath({
|
openApiService.validPath({
|
||||||
tags: ['Features'],
|
tags: ['Dependencies'],
|
||||||
summary: 'Deletes a feature dependency.',
|
summary: 'Deletes a feature dependency.',
|
||||||
description: 'Remove a dependency to a parent feature.',
|
description: 'Remove a dependency to a parent feature.',
|
||||||
operationId: 'deleteFeatureDependency',
|
operationId: 'deleteFeatureDependency',
|
||||||
@ -135,7 +134,7 @@ export default class DependentFeaturesController extends Controller {
|
|||||||
acceptAnyContentType: true,
|
acceptAnyContentType: true,
|
||||||
middleware: [
|
middleware: [
|
||||||
openApiService.validPath({
|
openApiService.validPath({
|
||||||
tags: ['Features'],
|
tags: ['Dependencies'],
|
||||||
summary: 'Deletes feature dependencies.',
|
summary: 'Deletes feature dependencies.',
|
||||||
description: 'Remove dependencies to all parent features.',
|
description: 'Remove dependencies to all parent features.',
|
||||||
operationId: 'deleteFeatureDependencies',
|
operationId: 'deleteFeatureDependencies',
|
||||||
@ -154,7 +153,7 @@ export default class DependentFeaturesController extends Controller {
|
|||||||
permission: NONE,
|
permission: NONE,
|
||||||
middleware: [
|
middleware: [
|
||||||
openApiService.validPath({
|
openApiService.validPath({
|
||||||
tags: ['Features'],
|
tags: ['Dependencies'],
|
||||||
summary: 'List parent options.',
|
summary: 'List parent options.',
|
||||||
description:
|
description:
|
||||||
'List available parents who have no transitive dependencies.',
|
'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(
|
async unprotectedArchiveToggles(
|
||||||
featureNames: string[],
|
featureNames: string[],
|
||||||
createdBy: string,
|
createdBy: string,
|
||||||
|
@ -39,6 +39,10 @@ const OPENAPI_TAGS = [
|
|||||||
description:
|
description:
|
||||||
'Create, update, and delete [context fields](https://docs.getunleash.io/reference/unleash-context) that Unleash is aware of.',
|
'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: 'Edge', description: 'Endpoints related to Unleash on the Edge.' },
|
||||||
{
|
{
|
||||||
name: 'Environments',
|
name: 'Environments',
|
||||||
|
@ -16,7 +16,11 @@ import {
|
|||||||
emptyResponse,
|
emptyResponse,
|
||||||
getStandardResponses,
|
getStandardResponses,
|
||||||
} from '../../../openapi/util/standard-responses';
|
} from '../../../openapi/util/standard-responses';
|
||||||
import { BatchFeaturesSchema, createRequestSchema } from '../../../openapi';
|
import {
|
||||||
|
BatchFeaturesSchema,
|
||||||
|
createRequestSchema,
|
||||||
|
createResponseSchema,
|
||||||
|
} from '../../../openapi';
|
||||||
import Controller from '../../controller';
|
import Controller from '../../controller';
|
||||||
import {
|
import {
|
||||||
TransactionCreator,
|
TransactionCreator,
|
||||||
@ -25,6 +29,7 @@ import {
|
|||||||
|
|
||||||
const PATH = '/:projectId';
|
const PATH = '/:projectId';
|
||||||
const PATH_ARCHIVE = `${PATH}/archive`;
|
const PATH_ARCHIVE = `${PATH}/archive`;
|
||||||
|
const PATH_VALIDATE_ARCHIVE = `${PATH}/archive/validate`;
|
||||||
const PATH_DELETE = `${PATH}/delete`;
|
const PATH_DELETE = `${PATH}/delete`;
|
||||||
const PATH_REVIVE = `${PATH}/revive`;
|
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({
|
this.route({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: PATH_ARCHIVE,
|
path: PATH_ARCHIVE,
|
||||||
@ -169,6 +195,18 @@ export default class ProjectArchiveController extends Controller {
|
|||||||
await this.featureService.archiveToggles(features, req.user, projectId);
|
await this.featureService.archiveToggles(features, req.user, projectId);
|
||||||
res.status(202).end();
|
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;
|
module.exports = ProjectArchiveController;
|
||||||
|
@ -17,6 +17,7 @@ beforeAll(async () => {
|
|||||||
experimental: {
|
experimental: {
|
||||||
flags: {
|
flags: {
|
||||||
strictSchemaValidation: true,
|
strictSchemaValidation: true,
|
||||||
|
dependentFeatures: true,
|
||||||
disableEnvsOnRevive: true,
|
disableEnvsOnRevive: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -249,3 +250,48 @@ test('Should be able to bulk archive features', async () => {
|
|||||||
);
|
);
|
||||||
expect(archivedFeatures).toHaveLength(2);
|
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