1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01: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 React, { FC, useState } from 'react';
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 { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi';
@ -45,7 +45,7 @@ export const AddDependency: FC<IAddDependencyProps> = ({
onChange={e => setParent(trim(e.target.value))}
/>
<PermissionButton
permission={CREATE_FEATURE}
permission={UPDATE_FEATURE}
projectId={projectId}
onClick={() => {
addDependency(featureId, { feature: parent });

View File

@ -2,10 +2,10 @@ import { Response } from 'express';
import Controller from '../../routes/controller';
import { OpenApiService } from '../../services';
import {
CREATE_FEATURE,
IFlagResolver,
IUnleashConfig,
IUnleashServices,
UPDATE_FEATURE,
} from '../../types';
import { Logger } from '../../logger';
import {
@ -20,16 +20,24 @@ import { DependentFeaturesService } from './dependent-features-service';
import { TransactionCreator, UnleashTransaction } from '../../db/transaction';
interface FeatureParams {
featureName: string;
child: string;
}
interface DeleteDependencyParams {
child: string;
parent: string;
}
const PATH = '/:projectId/features';
const PATH_FEATURE = `${PATH}/:featureName`;
const PATH_FEATURE = `${PATH}/:child`;
const PATH_DEPENDENCIES = `${PATH_FEATURE}/dependencies`;
const PATH_DEPENDENCY = `${PATH_FEATURE}/dependencies/:parent`;
type DependentFeaturesServices = Pick<
IUnleashServices,
'transactionalDependentFeaturesService' | 'openApiService'
| 'transactionalDependentFeaturesService'
| 'dependentFeaturesService'
| 'openApiService'
>;
export default class DependentFeaturesController extends Controller {
@ -37,6 +45,8 @@ export default class DependentFeaturesController extends Controller {
db: UnleashTransaction,
) => DependentFeaturesService;
private dependentFeaturesService: DependentFeaturesService;
private readonly startTransaction: TransactionCreator<UnleashTransaction>;
private openApiService: OpenApiService;
@ -49,6 +59,7 @@ export default class DependentFeaturesController extends Controller {
config: IUnleashConfig,
{
transactionalDependentFeaturesService,
dependentFeaturesService,
openApiService,
}: DependentFeaturesServices,
startTransaction: TransactionCreator<UnleashTransaction>,
@ -56,6 +67,7 @@ export default class DependentFeaturesController extends Controller {
super(config);
this.transactionalDependentFeaturesService =
transactionalDependentFeaturesService;
this.dependentFeaturesService = dependentFeaturesService;
this.openApiService = openApiService;
this.flagResolver = config.flagResolver;
this.startTransaction = startTransaction;
@ -67,7 +79,7 @@ export default class DependentFeaturesController extends Controller {
method: 'post',
path: PATH_DEPENDENCIES,
handler: this.addFeatureDependency,
permission: CREATE_FEATURE,
permission: UPDATE_FEATURE,
middleware: [
openApiService.validPath({
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(
req: IAuthRequest<FeatureParams, any, CreateDependentFeatureSchema>,
res: Response,
): Promise<void> {
const { featureName } = req.params;
const { child } = req.params;
const { variants, enabled, feature } = req.body;
if (this.config.flagResolver.isEnabled('dependentFeatures')) {
await this.startTransaction(async (tx) =>
this.transactionalDependentFeaturesService(
tx,
).upsertFeatureDependency(featureName, {
).upsertFeatureDependency(child, {
variants,
enabled,
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 { CreateDependentFeatureSchema } from '../../openapi';
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 {
private dependentFeaturesStore: IDependentFeaturesStore;
@ -45,4 +38,10 @@ export class DependentFeaturesService {
};
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 {
upsert(featureDependency: FeatureDependency): Promise<void>;
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 { IDependentFeaturesStore } from './dependent-features-store-type';
import { FeatureDependency, FeatureDependencyId } from './dependent-features';
type SerializableFeatureDependency = Omit<FeatureDependency, 'variants'> & {
variants?: string;
};
export class DependentFeaturesStore implements IDependentFeaturesStore {
private db: Db;
@ -38,4 +37,11 @@ export class DependentFeaturesStore implements IDependentFeaturesStore {
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);
};
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 () => {
const parent = uuidv4();
const child = uuidv4();
@ -60,6 +72,8 @@ test('should add feature dependency', async () => {
feature: parent,
variants: ['variantB'],
});
await deleteFeatureDependency(child, parent);
});
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[]> {
return Promise.resolve([]);
}
delete(): Promise<void> {
return Promise.resolve();
}
}