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:
parent
0938b2e545
commit
e030b67a19
@ -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',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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[]>;
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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';
|
||||||
|
16
src/lib/openapi/spec/parent-feature-options-schema.ts
Normal file
16
src/lib/openapi/spec/parent-feature-options-schema.ts
Normal 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
|
||||||
|
>;
|
Loading…
Reference in New Issue
Block a user