mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-31 00:16:47 +01:00
feat: prevent adding dependency to archived or removed parent (#4987)
This commit is contained in:
parent
7ea7c08654
commit
30e9fb87e9
@ -14,6 +14,8 @@ import {
|
|||||||
createChangeRequestAccessReadModel,
|
createChangeRequestAccessReadModel,
|
||||||
createFakeChangeRequestAccessService,
|
createFakeChangeRequestAccessService,
|
||||||
} from '../change-request-access-service/createChangeRequestAccessReadModel';
|
} from '../change-request-access-service/createChangeRequestAccessReadModel';
|
||||||
|
import { FeaturesReadModel } from '../feature-toggle/features-read-model';
|
||||||
|
import { FakeFeaturesReadModel } from '../feature-toggle/fake-features-read-model';
|
||||||
|
|
||||||
export const createDependentFeaturesService = (
|
export const createDependentFeaturesService = (
|
||||||
db: Db,
|
db: Db,
|
||||||
@ -35,12 +37,14 @@ export const createDependentFeaturesService = (
|
|||||||
db,
|
db,
|
||||||
config,
|
config,
|
||||||
);
|
);
|
||||||
return new DependentFeaturesService(
|
const featuresReadModel = new FeaturesReadModel(db);
|
||||||
|
return new DependentFeaturesService({
|
||||||
dependentFeaturesStore,
|
dependentFeaturesStore,
|
||||||
dependentFeaturesReadModel,
|
dependentFeaturesReadModel,
|
||||||
changeRequestAccessReadModel,
|
changeRequestAccessReadModel,
|
||||||
|
featuresReadModel,
|
||||||
eventService,
|
eventService,
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createFakeDependentFeaturesService = (
|
export const createFakeDependentFeaturesService = (
|
||||||
@ -58,11 +62,13 @@ export const createFakeDependentFeaturesService = (
|
|||||||
const dependentFeaturesStore = new FakeDependentFeaturesStore();
|
const dependentFeaturesStore = new FakeDependentFeaturesStore();
|
||||||
const dependentFeaturesReadModel = new FakeDependentFeaturesReadModel();
|
const dependentFeaturesReadModel = new FakeDependentFeaturesReadModel();
|
||||||
const changeRequestAccessReadModel = createFakeChangeRequestAccessService();
|
const changeRequestAccessReadModel = createFakeChangeRequestAccessService();
|
||||||
|
const featuresReadModel = new FakeFeaturesReadModel();
|
||||||
|
|
||||||
return new DependentFeaturesService(
|
return new DependentFeaturesService({
|
||||||
dependentFeaturesStore,
|
dependentFeaturesStore,
|
||||||
dependentFeaturesReadModel,
|
dependentFeaturesReadModel,
|
||||||
changeRequestAccessReadModel,
|
changeRequestAccessReadModel,
|
||||||
|
featuresReadModel,
|
||||||
eventService,
|
eventService,
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
@ -8,6 +8,15 @@ import { User } from '../../server-impl';
|
|||||||
import { SKIP_CHANGE_REQUEST } from '../../types';
|
import { SKIP_CHANGE_REQUEST } from '../../types';
|
||||||
import { IChangeRequestAccessReadModel } from '../change-request-access-service/change-request-access-read-model';
|
import { IChangeRequestAccessReadModel } from '../change-request-access-service/change-request-access-read-model';
|
||||||
import { extractUsernameFromUser } from '../../util';
|
import { extractUsernameFromUser } from '../../util';
|
||||||
|
import { IFeaturesReadModel } from '../feature-toggle/features-read-model-type';
|
||||||
|
|
||||||
|
interface IDependentFeaturesServiceDeps {
|
||||||
|
dependentFeaturesStore: IDependentFeaturesStore;
|
||||||
|
dependentFeaturesReadModel: IDependentFeaturesReadModel;
|
||||||
|
changeRequestAccessReadModel: IChangeRequestAccessReadModel;
|
||||||
|
featuresReadModel: IFeaturesReadModel;
|
||||||
|
eventService: EventService;
|
||||||
|
}
|
||||||
|
|
||||||
export class DependentFeaturesService {
|
export class DependentFeaturesService {
|
||||||
private dependentFeaturesStore: IDependentFeaturesStore;
|
private dependentFeaturesStore: IDependentFeaturesStore;
|
||||||
@ -16,17 +25,21 @@ export class DependentFeaturesService {
|
|||||||
|
|
||||||
private changeRequestAccessReadModel: IChangeRequestAccessReadModel;
|
private changeRequestAccessReadModel: IChangeRequestAccessReadModel;
|
||||||
|
|
||||||
|
private featuresReadModel: IFeaturesReadModel;
|
||||||
|
|
||||||
private eventService: EventService;
|
private eventService: EventService;
|
||||||
|
|
||||||
constructor(
|
constructor({
|
||||||
dependentFeaturesStore: IDependentFeaturesStore,
|
featuresReadModel,
|
||||||
dependentFeaturesReadModel: IDependentFeaturesReadModel,
|
dependentFeaturesReadModel,
|
||||||
changeRequestAccessReadModel: IChangeRequestAccessReadModel,
|
dependentFeaturesStore,
|
||||||
eventService: EventService,
|
eventService,
|
||||||
) {
|
changeRequestAccessReadModel,
|
||||||
|
}: IDependentFeaturesServiceDeps) {
|
||||||
this.dependentFeaturesStore = dependentFeaturesStore;
|
this.dependentFeaturesStore = dependentFeaturesStore;
|
||||||
this.dependentFeaturesReadModel = dependentFeaturesReadModel;
|
this.dependentFeaturesReadModel = dependentFeaturesReadModel;
|
||||||
this.changeRequestAccessReadModel = changeRequestAccessReadModel;
|
this.changeRequestAccessReadModel = changeRequestAccessReadModel;
|
||||||
|
this.featuresReadModel = featuresReadModel;
|
||||||
this.eventService = eventService;
|
this.eventService = eventService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,15 +90,23 @@ export class DependentFeaturesService {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { enabled, feature: parent, variants } = dependentFeature;
|
const { enabled, feature: parent, variants } = dependentFeature;
|
||||||
|
|
||||||
const children = await this.dependentFeaturesReadModel.getChildren([
|
const [children, parentExists] = await Promise.all([
|
||||||
child,
|
this.dependentFeaturesReadModel.getChildren([child]),
|
||||||
|
this.featuresReadModel.featureExists(parent),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (children.length > 0) {
|
if (children.length > 0) {
|
||||||
throw new InvalidOperationError(
|
throw new InvalidOperationError(
|
||||||
'Transitive dependency detected. Cannot add a dependency to the feature that other features depend on.',
|
'Transitive dependency detected. Cannot add a dependency to the feature that other features depend on.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!parentExists) {
|
||||||
|
throw new InvalidOperationError(
|
||||||
|
`No active feature ${parent} exists`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const featureDependency: FeatureDependency =
|
const featureDependency: FeatureDependency =
|
||||||
enabled === false
|
enabled === false
|
||||||
? {
|
? {
|
||||||
|
@ -136,3 +136,34 @@ test('should not allow to add a parent dependency to a feature that already has
|
|||||||
403,
|
403,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should not allow to add non-existent parent dependency', async () => {
|
||||||
|
const grandparent = uuidv4();
|
||||||
|
const parent = uuidv4();
|
||||||
|
const child = uuidv4();
|
||||||
|
await app.createFeature(child);
|
||||||
|
|
||||||
|
await addFeatureDependency(
|
||||||
|
child,
|
||||||
|
{
|
||||||
|
feature: parent,
|
||||||
|
},
|
||||||
|
403,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not allow to add archived parent dependency', async () => {
|
||||||
|
const parent = uuidv4();
|
||||||
|
const child = uuidv4();
|
||||||
|
await app.createFeature(child);
|
||||||
|
await app.createFeature(parent);
|
||||||
|
await app.archiveFeature(parent);
|
||||||
|
|
||||||
|
await addFeatureDependency(
|
||||||
|
child,
|
||||||
|
{
|
||||||
|
feature: parent,
|
||||||
|
},
|
||||||
|
403,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
import { IFeaturesReadModel } from './features-read-model-type';
|
||||||
|
|
||||||
|
export class FakeFeaturesReadModel implements IFeaturesReadModel {
|
||||||
|
featureExists(): Promise<boolean> {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
export interface IFeaturesReadModel {
|
||||||
|
featureExists(parent: string): Promise<boolean>;
|
||||||
|
}
|
19
src/lib/features/feature-toggle/features-read-model.ts
Normal file
19
src/lib/features/feature-toggle/features-read-model.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Db } from '../../db/db';
|
||||||
|
import { IFeaturesReadModel } from './features-read-model-type';
|
||||||
|
|
||||||
|
export class FeaturesReadModel implements IFeaturesReadModel {
|
||||||
|
private db: Db;
|
||||||
|
|
||||||
|
constructor(db: Db) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
async featureExists(parent: string): Promise<boolean> {
|
||||||
|
const rows = await this.db('features')
|
||||||
|
.where('name', parent)
|
||||||
|
.andWhere('archived_at', null)
|
||||||
|
.select('name');
|
||||||
|
|
||||||
|
return rows.length > 0;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user