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:
parent
6a49089d6f
commit
a9805b312b
@ -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 });
|
||||||
|
@ -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',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
10
src/lib/features/dependent-features/dependent-features.ts
Normal file
10
src/lib/features/dependent-features/dependent-features.ts
Normal 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 };
|
@ -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 () => {
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user