mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-14 00:19:16 +01:00
feat: allow to delete dependencies when no orphans (#4952)
This commit is contained in:
parent
52fa872fe6
commit
8b0cf8b11d
@ -2,6 +2,9 @@ import { IDependency } from '../../types';
|
|||||||
|
|
||||||
export interface IDependentFeaturesReadModel {
|
export interface IDependentFeaturesReadModel {
|
||||||
getChildren(parents: string[]): Promise<string[]>;
|
getChildren(parents: string[]): Promise<string[]>;
|
||||||
|
// given a list of parents and children verifies if some children would be orphaned after deletion
|
||||||
|
// we're interested in the list of parents, not orphans
|
||||||
|
getOrphanParents(parentsAndChildren: string[]): Promise<string[]>;
|
||||||
getParents(child: string): Promise<IDependency[]>;
|
getParents(child: string): Promise<IDependency[]>;
|
||||||
getParentOptions(child: string): Promise<string[]>;
|
getParentOptions(child: string): Promise<string[]>;
|
||||||
hasDependencies(feature: string): Promise<boolean>;
|
hasDependencies(feature: string): Promise<boolean>;
|
||||||
|
@ -9,6 +9,21 @@ export class DependentFeaturesReadModel implements IDependentFeaturesReadModel {
|
|||||||
this.db = db;
|
this.db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOrphanParents(parentsAndChildren: string[]): Promise<string[]> {
|
||||||
|
const rows = await this.db('dependent_features')
|
||||||
|
.distinct('parent')
|
||||||
|
.whereIn('parent', parentsAndChildren)
|
||||||
|
.andWhere(function () {
|
||||||
|
this.whereIn('parent', function () {
|
||||||
|
this.select('parent')
|
||||||
|
.from('dependent_features')
|
||||||
|
.whereNotIn('child', parentsAndChildren);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows.map((row) => row.parent);
|
||||||
|
}
|
||||||
|
|
||||||
async getChildren(parents: string[]): Promise<string[]> {
|
async getChildren(parents: string[]): Promise<string[]> {
|
||||||
const rows = await this.db('dependent_features').whereIn(
|
const rows = await this.db('dependent_features').whereIn(
|
||||||
'parent',
|
'parent',
|
||||||
|
@ -19,4 +19,8 @@ export class FakeDependentFeaturesReadModel
|
|||||||
hasDependencies(): Promise<boolean> {
|
hasDependencies(): Promise<boolean> {
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOrphanParents(parentsAndChildren: string[]): Promise<string[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,3 +4,4 @@ export * from './feature-toggle/createFeatureToggleService';
|
|||||||
export * from './project/createProjectService';
|
export * from './project/createProjectService';
|
||||||
export * from './change-request-access-service/createChangeRequestAccessReadModel';
|
export * from './change-request-access-service/createChangeRequestAccessReadModel';
|
||||||
export * from './segment/createSegmentService';
|
export * from './segment/createSegmentService';
|
||||||
|
export * from './dependent-features/createDependentFeaturesService';
|
||||||
|
@ -256,13 +256,27 @@ class FeatureToggleService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateNoChildren(featureNames: string[]): Promise<void> {
|
async validateNoChildren(featureName: string): Promise<void> {
|
||||||
|
if (this.flagResolver.isEnabled('dependentFeatures')) {
|
||||||
|
const children = await this.dependentFeaturesReadModel.getChildren([
|
||||||
|
featureName,
|
||||||
|
]);
|
||||||
|
if (children.length > 0) {
|
||||||
|
throw new InvalidOperationError(
|
||||||
|
'You can not archive/delete this feature since other features depend on it.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateNoOrphanParents(featureNames: string[]): Promise<void> {
|
||||||
if (this.flagResolver.isEnabled('dependentFeatures')) {
|
if (this.flagResolver.isEnabled('dependentFeatures')) {
|
||||||
if (featureNames.length === 0) return;
|
if (featureNames.length === 0) return;
|
||||||
const children = await this.dependentFeaturesReadModel.getChildren(
|
const parents =
|
||||||
|
await this.dependentFeaturesReadModel.getOrphanParents(
|
||||||
featureNames,
|
featureNames,
|
||||||
);
|
);
|
||||||
if (children.length > 0) {
|
if (parents.length > 0) {
|
||||||
throw new InvalidOperationError(
|
throw new InvalidOperationError(
|
||||||
featureNames.length > 1
|
featureNames.length > 1
|
||||||
? `You can not archive/delete those features since other features depend on them.`
|
? `You can not archive/delete those features since other features depend on them.`
|
||||||
@ -1460,7 +1474,7 @@ class FeatureToggleService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.validateNoChildren([featureName]);
|
await this.validateNoChildren(featureName);
|
||||||
|
|
||||||
await this.featureToggleStore.archive(featureName);
|
await this.featureToggleStore.archive(featureName);
|
||||||
|
|
||||||
@ -1479,7 +1493,7 @@ class FeatureToggleService {
|
|||||||
projectId: string,
|
projectId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.validateFeaturesContext(featureNames, projectId);
|
await this.validateFeaturesContext(featureNames, projectId);
|
||||||
await this.validateNoChildren(featureNames);
|
await this.validateNoOrphanParents(featureNames);
|
||||||
|
|
||||||
const features = await this.featureToggleStore.getAllByNames(
|
const features = await this.featureToggleStore.getAllByNames(
|
||||||
featureNames,
|
featureNames,
|
||||||
@ -1780,7 +1794,7 @@ class FeatureToggleService {
|
|||||||
|
|
||||||
// TODO: add project id.
|
// TODO: add project id.
|
||||||
async deleteFeature(featureName: string, createdBy: string): Promise<void> {
|
async deleteFeature(featureName: string, createdBy: string): Promise<void> {
|
||||||
await this.validateNoChildren([featureName]);
|
await this.validateNoChildren(featureName);
|
||||||
const toggle = await this.featureToggleStore.get(featureName);
|
const toggle = await this.featureToggleStore.get(featureName);
|
||||||
const tags = await this.tagStore.getAllTagsForFeature(featureName);
|
const tags = await this.tagStore.getAllTagsForFeature(featureName);
|
||||||
await this.featureToggleStore.delete(featureName);
|
await this.featureToggleStore.delete(featureName);
|
||||||
@ -1802,7 +1816,7 @@ class FeatureToggleService {
|
|||||||
createdBy: string,
|
createdBy: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.validateFeaturesContext(featureNames, projectId);
|
await this.validateFeaturesContext(featureNames, projectId);
|
||||||
await this.validateNoChildren(featureNames);
|
await this.validateNoOrphanParents(featureNames);
|
||||||
|
|
||||||
const features = await this.featureToggleStore.getAllByNames(
|
const features = await this.featureToggleStore.getAllByNames(
|
||||||
featureNames,
|
featureNames,
|
||||||
|
@ -295,6 +295,41 @@ test('Should not allow to archive/delete feature with children', async () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Should allow to archive/delete feature with children if no orphans are left', async () => {
|
||||||
|
const parent = uuidv4();
|
||||||
|
const child = uuidv4();
|
||||||
|
await app.createFeature(parent, 'default');
|
||||||
|
await app.createFeature(child, 'default');
|
||||||
|
await app.addDependency(child, parent);
|
||||||
|
|
||||||
|
const { body: deleteBody } = await app.request
|
||||||
|
.post(`/api/admin/projects/default/delete`)
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send({ features: [parent, child] })
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should not allow to archive/delete feature when orphans are left', async () => {
|
||||||
|
const parent = uuidv4();
|
||||||
|
const child = uuidv4();
|
||||||
|
const orphan = uuidv4();
|
||||||
|
await app.createFeature(parent, 'default');
|
||||||
|
await app.createFeature(child, 'default');
|
||||||
|
await app.createFeature(orphan, 'default');
|
||||||
|
await app.addDependency(child, parent);
|
||||||
|
await app.addDependency(orphan, parent);
|
||||||
|
|
||||||
|
const { body: deleteBody } = await app.request
|
||||||
|
.post(`/api/admin/projects/default/delete`)
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send({ features: [parent, child] })
|
||||||
|
.expect(403);
|
||||||
|
|
||||||
|
expect(deleteBody.message).toBe(
|
||||||
|
'You can not archive/delete those features since other features depend on them.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('should clone feature with parent dependencies', async () => {
|
test('should clone feature with parent dependencies', async () => {
|
||||||
const parent = uuidv4();
|
const parent = uuidv4();
|
||||||
const child = uuidv4();
|
const child = uuidv4();
|
||||||
|
Loading…
Reference in New Issue
Block a user