1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-31 00:16:47 +01:00

feat: Api to list available parent options (#4833)

This commit is contained in:
Mateusz Kwasniewski 2023-09-26 14:03:24 +02:00 committed by GitHub
parent 0938b2e545
commit e030b67a19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 95 additions and 0 deletions

View File

@ -5,14 +5,17 @@ import {
IFlagResolver, IFlagResolver,
IUnleashConfig, IUnleashConfig,
IUnleashServices, IUnleashServices,
NONE,
UPDATE_FEATURE, UPDATE_FEATURE,
} from '../../types'; } from '../../types';
import { Logger } from '../../logger'; import { Logger } from '../../logger';
import { import {
CreateDependentFeatureSchema, CreateDependentFeatureSchema,
createRequestSchema, createRequestSchema,
createResponseSchema,
emptyResponse, emptyResponse,
getStandardResponses, getStandardResponses,
ParentFeatureOptionsSchema,
} from '../../openapi'; } from '../../openapi';
import { IAuthRequest } from '../../routes/unleash-types'; import { IAuthRequest } from '../../routes/unleash-types';
import { InvalidOperationError } from '../../error'; import { InvalidOperationError } from '../../error';
@ -31,6 +34,7 @@ interface DeleteDependencyParams {
const PATH = '/:projectId/features'; const PATH = '/:projectId/features';
const PATH_FEATURE = `${PATH}/:child`; const PATH_FEATURE = `${PATH}/:child`;
const PATH_DEPENDENCIES = `${PATH_FEATURE}/dependencies`; const PATH_DEPENDENCIES = `${PATH_FEATURE}/dependencies`;
const PATH_PARENTS = `${PATH_FEATURE}/parents`;
const PATH_DEPENDENCY = `${PATH_FEATURE}/dependencies/:parent`; const PATH_DEPENDENCY = `${PATH_FEATURE}/dependencies/:parent`;
type DependentFeaturesServices = Pick< type DependentFeaturesServices = Pick<
@ -137,6 +141,26 @@ export default class DependentFeaturesController extends Controller {
}), }),
], ],
}); });
this.route({
method: 'get',
path: PATH_PARENTS,
handler: this.getParentOptions,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['Features'],
summary: 'List parent options.',
description:
'List available parents who have no transitive dependencies.',
operationId: 'listParentOptions',
responses: {
200: createResponseSchema('parentFeatureOptionsSchema'),
...getStandardResponses(401, 403, 404),
},
}),
],
});
} }
async addFeatureDependency( async addFeatureDependency(
@ -200,4 +224,21 @@ export default class DependentFeaturesController extends Controller {
); );
} }
} }
async getParentOptions(
req: IAuthRequest<FeatureParams, any, any>,
res: Response<ParentFeatureOptionsSchema>,
): Promise<void> {
const { child } = req.params;
if (this.config.flagResolver.isEnabled('dependentFeatures')) {
const parentOptions =
await this.dependentFeaturesService.getParentOptions(child);
res.send(parentOptions);
} else {
throw new InvalidOperationError(
'Dependent features are not enabled',
);
}
}
} }

View File

@ -48,4 +48,8 @@ export class DependentFeaturesService {
async deleteFeatureDependencies(feature: string): Promise<void> { async deleteFeatureDependencies(feature: string): Promise<void> {
await this.dependentFeaturesStore.deleteAll(feature); await this.dependentFeaturesStore.deleteAll(feature);
} }
async getParentOptions(feature: string): Promise<string[]> {
return this.dependentFeaturesStore.getParentOptions(feature);
}
} }

View File

@ -5,4 +5,5 @@ export interface IDependentFeaturesStore {
getChildren(parent: string): Promise<string[]>; getChildren(parent: string): Promise<string[]>;
delete(dependency: FeatureDependencyId): Promise<void>; delete(dependency: FeatureDependencyId): Promise<void>;
deleteAll(child: string): Promise<void>; deleteAll(child: string): Promise<void>;
getParentOptions(child: string): Promise<string[]>;
} }

View File

@ -38,6 +38,23 @@ export class DependentFeaturesStore implements IDependentFeaturesStore {
return rows.map((row) => row.child); return rows.map((row) => row.child);
} }
async getParentOptions(child: string): Promise<string[]> {
const result = await this.db('features as f')
.where('f.name', child)
.select('f.project');
if (result.length === 0) {
return [];
}
const rows = await this.db('features as f')
.leftJoin('dependent_features as df', 'f.name', 'df.child')
.where('f.project', result[0].project)
.andWhere('f.name', '!=', child)
.andWhere('df.child', null)
.select('f.name');
return rows.map((item) => item.name);
}
async delete(dependency: FeatureDependencyId): Promise<void> { async delete(dependency: FeatureDependencyId): Promise<void> {
await this.db('dependent_features') await this.db('dependent_features')
.where('parent', dependency.parent) .where('parent', dependency.parent)

View File

@ -67,12 +67,21 @@ const deleteFeatureDependencies = async (
.expect(expectedCode); .expect(expectedCode);
}; };
const getParentOptions = async (childFeature: string, expectedCode = 200) => {
return app.request
.get(`/api/admin/projects/default/features/${childFeature}/parents`)
.expect(expectedCode);
};
test('should add and delete feature dependencies', async () => { test('should add and delete feature dependencies', async () => {
const parent = uuidv4(); const parent = uuidv4();
const child = uuidv4(); const child = uuidv4();
await app.createFeature(parent); await app.createFeature(parent);
await app.createFeature(child); await app.createFeature(child);
const { body: parentOptions } = await getParentOptions(child);
expect(parentOptions).toStrictEqual([parent]);
// save explicit enabled and variants // save explicit enabled and variants
await addFeatureDependency(child, { await addFeatureDependency(child, {
feature: parent, feature: parent,

View File

@ -9,6 +9,10 @@ export class FakeDependentFeaturesStore implements IDependentFeaturesStore {
return Promise.resolve([]); return Promise.resolve([]);
} }
getParentOptions(): Promise<string[]> {
return Promise.resolve([]);
}
delete(): Promise<void> { delete(): Promise<void> {
return Promise.resolve(); return Promise.resolve();
} }

View File

@ -163,6 +163,7 @@ import {
updateFeatureStrategySegmentsSchema, updateFeatureStrategySegmentsSchema,
dependentFeatureSchema, dependentFeatureSchema,
createDependentFeatureSchema, createDependentFeatureSchema,
parentFeatureOptionsSchema,
} from './spec'; } from './spec';
import { IServerOption } from '../types'; import { IServerOption } from '../types';
import { mapValues, omitKeys } from '../util'; import { mapValues, omitKeys } from '../util';
@ -387,6 +388,7 @@ export const schemas: UnleashSchemas = {
updateFeatureStrategySegmentsSchema, updateFeatureStrategySegmentsSchema,
dependentFeatureSchema, dependentFeatureSchema,
createDependentFeatureSchema, createDependentFeatureSchema,
parentFeatureOptionsSchema,
}; };
// Remove JSONSchema keys that would result in an invalid OpenAPI spec. // Remove JSONSchema keys that would result in an invalid OpenAPI spec.

View File

@ -162,3 +162,4 @@ export * from './segments-schema';
export * from './update-feature-strategy-segments-schema'; export * from './update-feature-strategy-segments-schema';
export * from './dependent-feature-schema'; export * from './dependent-feature-schema';
export * from './create-dependent-feature-schema'; export * from './create-dependent-feature-schema';
export * from './parent-feature-options-schema';

View File

@ -0,0 +1,16 @@
import { FromSchema } from 'json-schema-to-ts';
export const parentFeatureOptionsSchema = {
$id: '#/components/schemas/parentFeatureOptionsSchema',
type: 'array',
description:
'A list of parent feature names available for a given child feature. Features that have their own parents are excluded.',
items: {
type: 'string',
},
components: {},
} as const;
export type ParentFeatureOptionsSchema = FromSchema<
typeof parentFeatureOptionsSchema
>;