1
0
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:
Mateusz Kwasniewski 2023-09-25 10:12:32 +02:00 committed by GitHub
parent eb259a3783
commit 06ea70ef00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 95 additions and 15 deletions

View File

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

View File

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

View File

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

View File

@ -2,4 +2,5 @@ import { FeatureDependency } from './dependent-features-service';
export interface IDependentFeaturesStore {
upsert(featureDependency: FeatureDependency): Promise<void>;
getChildren(parent: string): Promise<string[]>;
}

View File

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

View File

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

View File

@ -4,4 +4,8 @@ export class FakeDependentFeaturesStore implements IDependentFeaturesStore {
async upsert(): Promise<void> {
return Promise.resolve();
}
getChildren(): Promise<string[]> {
return Promise.resolve([]);
}
}

View File

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

View File

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

View File

@ -101,4 +101,7 @@ export interface IUnleashServices {
transactionalGroupService: (db: Knex.Transaction) => GroupService;
privateProjectChecker: IPrivateProjectChecker;
dependentFeaturesService: DependentFeaturesService;
transactionalDependentFeaturesService: (
db: Knex.Transaction,
) => DependentFeaturesService;
}