mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-17 01:17:29 +02:00
feat: enforce no transitive parents (#4818)
This commit is contained in:
parent
eb259a3783
commit
06ea70ef00
@ -0,0 +1,10 @@
|
||||
import { Db } from '../../db/db';
|
||||
import { DependentFeaturesService } from './dependent-features-service';
|
||||
import { DependentFeaturesStore } from './dependent-features-store';
|
||||
|
||||
export const createDependentFeaturesService = (
|
||||
db: Db,
|
||||
): DependentFeaturesService => {
|
||||
const dependentFeaturesStore = new DependentFeaturesStore(db);
|
||||
return new DependentFeaturesService(dependentFeaturesStore);
|
||||
};
|
@ -17,6 +17,7 @@ import {
|
||||
import { IAuthRequest } from '../../routes/unleash-types';
|
||||
import { InvalidOperationError } from '../../error';
|
||||
import { DependentFeaturesService } from './dependent-features-service';
|
||||
import { TransactionCreator, UnleashTransaction } from '../../db/transaction';
|
||||
|
||||
interface FeatureParams {
|
||||
featureName: string;
|
||||
@ -28,11 +29,15 @@ const PATH_DEPENDENCIES = `${PATH_FEATURE}/dependencies`;
|
||||
|
||||
type DependentFeaturesServices = Pick<
|
||||
IUnleashServices,
|
||||
'dependentFeaturesService' | 'openApiService'
|
||||
'transactionalDependentFeaturesService' | 'openApiService'
|
||||
>;
|
||||
|
||||
export default class DependentFeaturesController extends Controller {
|
||||
private dependentFeaturesService: DependentFeaturesService;
|
||||
private transactionalDependentFeaturesService: (
|
||||
db: UnleashTransaction,
|
||||
) => DependentFeaturesService;
|
||||
|
||||
private readonly startTransaction: TransactionCreator<UnleashTransaction>;
|
||||
|
||||
private openApiService: OpenApiService;
|
||||
|
||||
@ -42,12 +47,18 @@ export default class DependentFeaturesController extends Controller {
|
||||
|
||||
constructor(
|
||||
config: IUnleashConfig,
|
||||
{ dependentFeaturesService, openApiService }: DependentFeaturesServices,
|
||||
{
|
||||
transactionalDependentFeaturesService,
|
||||
openApiService,
|
||||
}: DependentFeaturesServices,
|
||||
startTransaction: TransactionCreator<UnleashTransaction>,
|
||||
) {
|
||||
super(config);
|
||||
this.dependentFeaturesService = dependentFeaturesService;
|
||||
this.transactionalDependentFeaturesService =
|
||||
transactionalDependentFeaturesService;
|
||||
this.openApiService = openApiService;
|
||||
this.flagResolver = config.flagResolver;
|
||||
this.startTransaction = startTransaction;
|
||||
this.logger = config.getLogger(
|
||||
'/dependent-features/dependent-feature-service.ts',
|
||||
);
|
||||
@ -84,13 +95,14 @@ export default class DependentFeaturesController extends Controller {
|
||||
const { variants, enabled, feature } = req.body;
|
||||
|
||||
if (this.config.flagResolver.isEnabled('dependentFeatures')) {
|
||||
await this.dependentFeaturesService.upsertFeatureDependency(
|
||||
featureName,
|
||||
{
|
||||
await this.startTransaction(async (tx) =>
|
||||
this.transactionalDependentFeaturesService(
|
||||
tx,
|
||||
).upsertFeatureDependency(featureName, {
|
||||
variants,
|
||||
enabled,
|
||||
feature,
|
||||
},
|
||||
}),
|
||||
);
|
||||
res.status(200).end();
|
||||
} else {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { InvalidOperationError } from '../../error';
|
||||
import { CreateDependentFeatureSchema } from '../../openapi';
|
||||
import { IDependentFeaturesStore } from './dependent-features-store-type';
|
||||
|
||||
@ -17,20 +18,28 @@ export class DependentFeaturesService {
|
||||
}
|
||||
|
||||
async upsertFeatureDependency(
|
||||
childFeature: string,
|
||||
child: string,
|
||||
dependentFeature: CreateDependentFeatureSchema,
|
||||
): Promise<void> {
|
||||
const { enabled, feature, variants } = dependentFeature;
|
||||
const { enabled, feature: parent, variants } = dependentFeature;
|
||||
|
||||
const children = await this.dependentFeaturesStore.getChildren(child);
|
||||
if (children.length > 0) {
|
||||
throw new InvalidOperationError(
|
||||
'Transitive dependency detected. Cannot add a dependency to the feature that other features depend on.',
|
||||
);
|
||||
}
|
||||
|
||||
const featureDependency: FeatureDependency =
|
||||
enabled === false
|
||||
? {
|
||||
parent: feature,
|
||||
child: childFeature,
|
||||
parent,
|
||||
child,
|
||||
enabled,
|
||||
}
|
||||
: {
|
||||
parent: feature,
|
||||
child: childFeature,
|
||||
parent,
|
||||
child,
|
||||
enabled: true,
|
||||
variants,
|
||||
};
|
||||
|
@ -2,4 +2,5 @@ import { FeatureDependency } from './dependent-features-service';
|
||||
|
||||
export interface IDependentFeaturesStore {
|
||||
upsert(featureDependency: FeatureDependency): Promise<void>;
|
||||
getChildren(parent: string): Promise<string[]>;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { IDependentFeaturesStore } from './dependent-features-store-type';
|
||||
type SerializableFeatureDependency = Omit<FeatureDependency, 'variants'> & {
|
||||
variants?: string;
|
||||
};
|
||||
|
||||
export class DependentFeaturesStore implements IDependentFeaturesStore {
|
||||
private db: Db;
|
||||
|
||||
@ -28,4 +29,13 @@ export class DependentFeaturesStore implements IDependentFeaturesStore {
|
||||
.onConflict(['parent', 'child'])
|
||||
.merge();
|
||||
}
|
||||
|
||||
async getChildren(parent: string): Promise<string[]> {
|
||||
const rows = await this.db('dependent_features').where(
|
||||
'parent',
|
||||
parent,
|
||||
);
|
||||
|
||||
return rows.map((row) => row.child);
|
||||
}
|
||||
}
|
||||
|
@ -61,3 +61,23 @@ test('should add feature dependency', async () => {
|
||||
variants: ['variantB'],
|
||||
});
|
||||
});
|
||||
|
||||
test('should not allow to add a parent dependency to a feature that already has children', async () => {
|
||||
const grandparent = uuidv4();
|
||||
const parent = uuidv4();
|
||||
const child = uuidv4();
|
||||
await app.createFeature(grandparent);
|
||||
await app.createFeature(parent);
|
||||
await app.createFeature(child);
|
||||
|
||||
await addFeatureDependency(child, {
|
||||
feature: parent,
|
||||
});
|
||||
await addFeatureDependency(
|
||||
parent,
|
||||
{
|
||||
feature: grandparent,
|
||||
},
|
||||
403,
|
||||
);
|
||||
});
|
||||
|
@ -4,4 +4,8 @@ export class FakeDependentFeaturesStore implements IDependentFeaturesStore {
|
||||
async upsert(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getChildren(): Promise<string[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
|
@ -113,7 +113,14 @@ export default class ProjectApi extends Controller {
|
||||
createKnexTransactionStarter(db),
|
||||
).router,
|
||||
);
|
||||
this.use('/', new DependentFeaturesController(config, services).router);
|
||||
this.use(
|
||||
'/',
|
||||
new DependentFeaturesController(
|
||||
config,
|
||||
services,
|
||||
createKnexTransactionStarter(db),
|
||||
).router,
|
||||
);
|
||||
this.use('/', new EnvironmentsController(config, services).router);
|
||||
this.use('/', new ProjectHealthReport(config, services).router);
|
||||
this.use('/', new VariantsController(config, services).router);
|
||||
|
@ -69,6 +69,7 @@ import {
|
||||
createGetActiveUsers,
|
||||
} from '../features/instance-stats/getActiveUsers';
|
||||
import { DependentFeaturesService } from '../features/dependent-features/dependent-features-service';
|
||||
import { createDependentFeaturesService } from '../features/dependent-features/createDependentFeaturesService';
|
||||
|
||||
// TODO: will be moved to scheduler feature directory
|
||||
export const scheduleServices = async (
|
||||
@ -299,6 +300,8 @@ export const createServices = (
|
||||
const dependentFeaturesService = new DependentFeaturesService(
|
||||
stores.dependentFeaturesStore,
|
||||
);
|
||||
const transactionalDependentFeaturesService = (txDb: Knex.Transaction) =>
|
||||
createDependentFeaturesService(txDb);
|
||||
|
||||
return {
|
||||
accessService,
|
||||
@ -351,6 +354,7 @@ export const createServices = (
|
||||
transactionalGroupService,
|
||||
privateProjectChecker,
|
||||
dependentFeaturesService,
|
||||
transactionalDependentFeaturesService,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -101,4 +101,7 @@ export interface IUnleashServices {
|
||||
transactionalGroupService: (db: Knex.Transaction) => GroupService;
|
||||
privateProjectChecker: IPrivateProjectChecker;
|
||||
dependentFeaturesService: DependentFeaturesService;
|
||||
transactionalDependentFeaturesService: (
|
||||
db: Knex.Transaction,
|
||||
) => DependentFeaturesService;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user