1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-12 13:48:35 +02:00

feat: Delete dependency api (#4824)

This commit is contained in:
Mateusz Kwasniewski 2023-09-25 15:50:05 +02:00 committed by GitHub
parent 6a49089d6f
commit a9805b312b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 105 additions and 20 deletions

View File

@ -2,7 +2,7 @@ import { Box, styled } from '@mui/material';
import { trim } from '../../common/util'; import { trim } from '../../common/util';
import React, { FC, useState } from 'react'; import React, { FC, useState } from 'react';
import Input from '../../common/Input/Input'; import Input from '../../common/Input/Input';
import { CREATE_FEATURE } from '../../providers/AccessProvider/permissions'; import { UPDATE_FEATURE } from '../../providers/AccessProvider/permissions';
import PermissionButton from '../../common/PermissionButton/PermissionButton'; import PermissionButton from '../../common/PermissionButton/PermissionButton';
import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi'; import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi';
@ -45,7 +45,7 @@ export const AddDependency: FC<IAddDependencyProps> = ({
onChange={e => setParent(trim(e.target.value))} onChange={e => setParent(trim(e.target.value))}
/> />
<PermissionButton <PermissionButton
permission={CREATE_FEATURE} permission={UPDATE_FEATURE}
projectId={projectId} projectId={projectId}
onClick={() => { onClick={() => {
addDependency(featureId, { feature: parent }); addDependency(featureId, { feature: parent });

View File

@ -2,10 +2,10 @@ import { Response } from 'express';
import Controller from '../../routes/controller'; import Controller from '../../routes/controller';
import { OpenApiService } from '../../services'; import { OpenApiService } from '../../services';
import { import {
CREATE_FEATURE,
IFlagResolver, IFlagResolver,
IUnleashConfig, IUnleashConfig,
IUnleashServices, IUnleashServices,
UPDATE_FEATURE,
} from '../../types'; } from '../../types';
import { Logger } from '../../logger'; import { Logger } from '../../logger';
import { import {
@ -20,16 +20,24 @@ import { DependentFeaturesService } from './dependent-features-service';
import { TransactionCreator, UnleashTransaction } from '../../db/transaction'; import { TransactionCreator, UnleashTransaction } from '../../db/transaction';
interface FeatureParams { interface FeatureParams {
featureName: string; child: string;
}
interface DeleteDependencyParams {
child: string;
parent: string;
} }
const PATH = '/:projectId/features'; const PATH = '/:projectId/features';
const PATH_FEATURE = `${PATH}/:featureName`; const PATH_FEATURE = `${PATH}/:child`;
const PATH_DEPENDENCIES = `${PATH_FEATURE}/dependencies`; const PATH_DEPENDENCIES = `${PATH_FEATURE}/dependencies`;
const PATH_DEPENDENCY = `${PATH_FEATURE}/dependencies/:parent`;
type DependentFeaturesServices = Pick< type DependentFeaturesServices = Pick<
IUnleashServices, IUnleashServices,
'transactionalDependentFeaturesService' | 'openApiService' | 'transactionalDependentFeaturesService'
| 'dependentFeaturesService'
| 'openApiService'
>; >;
export default class DependentFeaturesController extends Controller { export default class DependentFeaturesController extends Controller {
@ -37,6 +45,8 @@ export default class DependentFeaturesController extends Controller {
db: UnleashTransaction, db: UnleashTransaction,
) => DependentFeaturesService; ) => DependentFeaturesService;
private dependentFeaturesService: DependentFeaturesService;
private readonly startTransaction: TransactionCreator<UnleashTransaction>; private readonly startTransaction: TransactionCreator<UnleashTransaction>;
private openApiService: OpenApiService; private openApiService: OpenApiService;
@ -49,6 +59,7 @@ export default class DependentFeaturesController extends Controller {
config: IUnleashConfig, config: IUnleashConfig,
{ {
transactionalDependentFeaturesService, transactionalDependentFeaturesService,
dependentFeaturesService,
openApiService, openApiService,
}: DependentFeaturesServices, }: DependentFeaturesServices,
startTransaction: TransactionCreator<UnleashTransaction>, startTransaction: TransactionCreator<UnleashTransaction>,
@ -56,6 +67,7 @@ export default class DependentFeaturesController extends Controller {
super(config); super(config);
this.transactionalDependentFeaturesService = this.transactionalDependentFeaturesService =
transactionalDependentFeaturesService; transactionalDependentFeaturesService;
this.dependentFeaturesService = dependentFeaturesService;
this.openApiService = openApiService; this.openApiService = openApiService;
this.flagResolver = config.flagResolver; this.flagResolver = config.flagResolver;
this.startTransaction = startTransaction; this.startTransaction = startTransaction;
@ -67,7 +79,7 @@ export default class DependentFeaturesController extends Controller {
method: 'post', method: 'post',
path: PATH_DEPENDENCIES, path: PATH_DEPENDENCIES,
handler: this.addFeatureDependency, handler: this.addFeatureDependency,
permission: CREATE_FEATURE, permission: UPDATE_FEATURE,
middleware: [ middleware: [
openApiService.validPath({ openApiService.validPath({
tags: ['Features'], tags: ['Features'],
@ -85,20 +97,40 @@ export default class DependentFeaturesController extends Controller {
}), }),
], ],
}); });
this.route({
method: 'delete',
path: PATH_DEPENDENCY,
handler: this.deleteFeatureDependency,
permission: UPDATE_FEATURE,
acceptAnyContentType: true,
middleware: [
openApiService.validPath({
tags: ['Features'],
summary: 'Deletes a feature dependency.',
description: 'Remove a dependency to a parent feature.',
operationId: 'deleteFeatureDependency',
responses: {
200: emptyResponse,
...getStandardResponses(401, 403, 404),
},
}),
],
});
} }
async addFeatureDependency( async addFeatureDependency(
req: IAuthRequest<FeatureParams, any, CreateDependentFeatureSchema>, req: IAuthRequest<FeatureParams, any, CreateDependentFeatureSchema>,
res: Response, res: Response,
): Promise<void> { ): Promise<void> {
const { featureName } = req.params; const { child } = req.params;
const { variants, enabled, feature } = req.body; const { variants, enabled, feature } = req.body;
if (this.config.flagResolver.isEnabled('dependentFeatures')) { if (this.config.flagResolver.isEnabled('dependentFeatures')) {
await this.startTransaction(async (tx) => await this.startTransaction(async (tx) =>
this.transactionalDependentFeaturesService( this.transactionalDependentFeaturesService(
tx, tx,
).upsertFeatureDependency(featureName, { ).upsertFeatureDependency(child, {
variants, variants,
enabled, enabled,
feature, feature,
@ -111,4 +143,23 @@ export default class DependentFeaturesController extends Controller {
); );
} }
} }
async deleteFeatureDependency(
req: IAuthRequest<DeleteDependencyParams, any, any>,
res: Response,
): Promise<void> {
const { child, parent } = req.params;
if (this.config.flagResolver.isEnabled('dependentFeatures')) {
await this.dependentFeaturesService.deleteFeatureDependency({
parent,
child,
});
res.status(200).end();
} else {
throw new InvalidOperationError(
'Dependent features are not enabled',
);
}
}
} }

View File

@ -1,15 +1,8 @@
import { InvalidOperationError } from '../../error'; import { InvalidOperationError } from '../../error';
import { CreateDependentFeatureSchema } from '../../openapi'; import { CreateDependentFeatureSchema } from '../../openapi';
import { IDependentFeaturesStore } from './dependent-features-store-type'; import { IDependentFeaturesStore } from './dependent-features-store-type';
import { FeatureDependency, FeatureDependencyId } from './dependent-features';
export type FeatureDependency =
| {
parent: string;
child: string;
enabled: true;
variants?: string[];
}
| { parent: string; child: string; enabled: false };
export class DependentFeaturesService { export class DependentFeaturesService {
private dependentFeaturesStore: IDependentFeaturesStore; private dependentFeaturesStore: IDependentFeaturesStore;
@ -45,4 +38,10 @@ export class DependentFeaturesService {
}; };
await this.dependentFeaturesStore.upsert(featureDependency); await this.dependentFeaturesStore.upsert(featureDependency);
} }
async deleteFeatureDependency(
dependency: FeatureDependencyId,
): Promise<void> {
await this.dependentFeaturesStore.delete(dependency);
}
} }

View File

@ -1,6 +1,7 @@
import { FeatureDependency } from './dependent-features-service'; import { FeatureDependency, FeatureDependencyId } from './dependent-features';
export interface IDependentFeaturesStore { export interface IDependentFeaturesStore {
upsert(featureDependency: FeatureDependency): Promise<void>; upsert(featureDependency: FeatureDependency): Promise<void>;
getChildren(parent: string): Promise<string[]>; getChildren(parent: string): Promise<string[]>;
delete(dependency: FeatureDependencyId): Promise<void>;
} }

View File

@ -1,11 +1,10 @@
import { FeatureDependency } from './dependent-features-service';
import { Db } from '../../db/db'; import { Db } from '../../db/db';
import { IDependentFeaturesStore } from './dependent-features-store-type'; import { IDependentFeaturesStore } from './dependent-features-store-type';
import { FeatureDependency, FeatureDependencyId } from './dependent-features';
type SerializableFeatureDependency = Omit<FeatureDependency, 'variants'> & { type SerializableFeatureDependency = Omit<FeatureDependency, 'variants'> & {
variants?: string; variants?: string;
}; };
export class DependentFeaturesStore implements IDependentFeaturesStore { export class DependentFeaturesStore implements IDependentFeaturesStore {
private db: Db; private db: Db;
@ -38,4 +37,11 @@ export class DependentFeaturesStore implements IDependentFeaturesStore {
return rows.map((row) => row.child); return rows.map((row) => row.child);
} }
async delete(dependency: FeatureDependencyId): Promise<void> {
await this.db('dependent_features')
.where('parent', dependency.parent)
.andWhere('child', dependency.child)
.del();
}
} }

View File

@ -0,0 +1,10 @@
export type FeatureDependencyId = { parent: string; child: string };
export type FeatureDependency =
| {
parent: string;
child: string;
enabled: true;
variants?: string[];
}
| { parent: string; child: string; enabled: false };

View File

@ -44,6 +44,18 @@ const addFeatureDependency = async (
.expect(expectedCode); .expect(expectedCode);
}; };
const deleteFeatureDependency = async (
childFeature: string,
parentFeature: string,
expectedCode = 200,
) => {
return app.request
.delete(
`/api/admin/projects/default/features/${childFeature}/dependencies/${parentFeature}`,
)
.expect(expectedCode);
};
test('should add feature dependency', async () => { test('should add feature dependency', async () => {
const parent = uuidv4(); const parent = uuidv4();
const child = uuidv4(); const child = uuidv4();
@ -60,6 +72,8 @@ test('should add feature dependency', async () => {
feature: parent, feature: parent,
variants: ['variantB'], variants: ['variantB'],
}); });
await deleteFeatureDependency(child, parent);
}); });
test('should not allow to add a parent dependency to a feature that already has children', async () => { test('should not allow to add a parent dependency to a feature that already has children', async () => {

View File

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