mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-23 00:16:25 +01:00
feat: List possible parent variants (#6733)
This commit is contained in:
parent
664ceaea09
commit
42355b0c89
@ -15,7 +15,10 @@ import {
|
||||
createResponseSchema,
|
||||
emptyResponse,
|
||||
getStandardResponses,
|
||||
parentFeatureOptionsSchema,
|
||||
type ParentFeatureOptionsSchema,
|
||||
type ParentVariantOptionsSchema,
|
||||
parentVariantOptionsSchema,
|
||||
} from '../../openapi';
|
||||
import type { IAuthRequest } from '../../routes/unleash-types';
|
||||
import type { DependentFeaturesService } from './dependent-features-service';
|
||||
@ -29,6 +32,10 @@ interface FeatureParams extends ProjectParams {
|
||||
child: string;
|
||||
}
|
||||
|
||||
interface ParentVariantsParams extends ProjectParams {
|
||||
parent: string;
|
||||
}
|
||||
|
||||
interface DeleteDependencyParams extends ProjectParams {
|
||||
child: string;
|
||||
parent: string;
|
||||
@ -39,6 +46,7 @@ const PATH_FEATURE = `${PATH}/:child`;
|
||||
const PATH_DEPENDENCIES = `${PATH_FEATURE}/dependencies`;
|
||||
const PATH_DEPENDENCIES_CHECK = `/:projectId/dependencies`;
|
||||
const PATH_PARENTS = `${PATH_FEATURE}/parents`;
|
||||
const PATH_PARENT_VARIANTS = `${PATH}/:parent/parent-variants`;
|
||||
const PATH_DEPENDENCY = `${PATH_FEATURE}/dependencies/:parent`;
|
||||
|
||||
type DependentFeaturesServices = Pick<
|
||||
@ -136,7 +144,7 @@ export default class DependentFeaturesController extends Controller {
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: PATH_PARENTS,
|
||||
handler: this.getParentOptions,
|
||||
handler: this.getPossibleParentFeatures,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
@ -153,6 +161,26 @@ export default class DependentFeaturesController extends Controller {
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: PATH_PARENT_VARIANTS,
|
||||
handler: this.getPossibleParentVariants,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Dependencies'],
|
||||
summary: 'List parent feature variants.',
|
||||
description:
|
||||
'List available parent variants across all strategy variants and feature environment variants.',
|
||||
operationId: 'listParentVariantOptions',
|
||||
responses: {
|
||||
200: createResponseSchema('parentVariantOptionsSchema'),
|
||||
...getStandardResponses(401, 403, 404),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: PATH_DEPENDENCIES_CHECK,
|
||||
@ -227,15 +255,42 @@ export default class DependentFeaturesController extends Controller {
|
||||
res.status(200).end();
|
||||
}
|
||||
|
||||
async getParentOptions(
|
||||
async getPossibleParentFeatures(
|
||||
req: IAuthRequest<FeatureParams, any, any>,
|
||||
res: Response<ParentFeatureOptionsSchema>,
|
||||
): Promise<void> {
|
||||
const { child } = req.params;
|
||||
|
||||
const parentOptions =
|
||||
await this.dependentFeaturesService.getParentOptions(child);
|
||||
res.send(parentOptions);
|
||||
const options =
|
||||
await this.dependentFeaturesService.getPossibleParentFeatures(
|
||||
child,
|
||||
);
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
parentFeatureOptionsSchema.$id,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
async getPossibleParentVariants(
|
||||
req: IAuthRequest<ParentVariantsParams, any, any>,
|
||||
res: Response<ParentVariantOptionsSchema>,
|
||||
): Promise<void> {
|
||||
const { parent } = req.params;
|
||||
|
||||
const options =
|
||||
await this.dependentFeaturesService.getPossibleParentVariants(
|
||||
parent,
|
||||
);
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
parentVariantOptionsSchema.$id,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
async checkDependenciesExist(
|
||||
|
@ -7,7 +7,8 @@ export interface IDependentFeaturesReadModel {
|
||||
getOrphanParents(parentsAndChildren: string[]): Promise<string[]>;
|
||||
getParents(child: string): Promise<IDependency[]>;
|
||||
getDependencies(children: string[]): Promise<IFeatureDependency[]>;
|
||||
getParentOptions(child: string): Promise<string[]>;
|
||||
getPossibleParentFeatures(child: string): Promise<string[]>;
|
||||
getPossibleParentVariants(parent: string): Promise<string[]>;
|
||||
haveDependencies(features: string[]): Promise<boolean>;
|
||||
hasAnyDependencies(): Promise<boolean>;
|
||||
}
|
||||
|
@ -2,6 +2,10 @@ import type { Db } from '../../db/db';
|
||||
import type { IDependentFeaturesReadModel } from './dependent-features-read-model-type';
|
||||
import type { IDependency, IFeatureDependency } from '../../types';
|
||||
|
||||
interface IVariantName {
|
||||
variant_name: string;
|
||||
}
|
||||
|
||||
export class DependentFeaturesReadModel implements IDependentFeaturesReadModel {
|
||||
private db: Db;
|
||||
|
||||
@ -59,7 +63,7 @@ export class DependentFeaturesReadModel implements IDependentFeaturesReadModel {
|
||||
}));
|
||||
}
|
||||
|
||||
async getParentOptions(child: string): Promise<string[]> {
|
||||
async getPossibleParentFeatures(child: string): Promise<string[]> {
|
||||
const result = await this.db('features')
|
||||
.where('features.name', child)
|
||||
.select('features.project');
|
||||
@ -82,6 +86,35 @@ export class DependentFeaturesReadModel implements IDependentFeaturesReadModel {
|
||||
return rows.map((item) => item.name);
|
||||
}
|
||||
|
||||
async getPossibleParentVariants(parent: string): Promise<string[]> {
|
||||
const strategyVariantsQuery = this.db('feature_strategies')
|
||||
.select(
|
||||
this.db.raw(
|
||||
"jsonb_array_elements(variants)->>'name' as variant_name",
|
||||
),
|
||||
)
|
||||
.where('feature_name', parent);
|
||||
|
||||
const featureEnvironmentVariantsQuery = this.db('feature_environments')
|
||||
.select(
|
||||
this.db.raw(
|
||||
"jsonb_array_elements(variants)->>'name' as variant_name",
|
||||
),
|
||||
)
|
||||
.where('feature_name', parent);
|
||||
|
||||
const results = await Promise.all([
|
||||
strategyVariantsQuery,
|
||||
featureEnvironmentVariantsQuery,
|
||||
]);
|
||||
const flatResults = results
|
||||
.flat()
|
||||
.map((item) => (item as unknown as IVariantName).variant_name);
|
||||
const uniqueResults = [...new Set(flatResults)];
|
||||
|
||||
return uniqueResults.sort();
|
||||
}
|
||||
|
||||
async haveDependencies(features: string[]): Promise<boolean> {
|
||||
const parents = await this.db('dependent_features')
|
||||
.whereIn('parent', features)
|
||||
|
@ -227,8 +227,16 @@ export class DependentFeaturesService {
|
||||
);
|
||||
}
|
||||
|
||||
async getParentOptions(feature: string): Promise<string[]> {
|
||||
return this.dependentFeaturesReadModel.getParentOptions(feature);
|
||||
async getPossibleParentFeatures(feature: string): Promise<string[]> {
|
||||
return this.dependentFeaturesReadModel.getPossibleParentFeatures(
|
||||
feature,
|
||||
);
|
||||
}
|
||||
|
||||
async getPossibleParentVariants(parentFeature: string): Promise<string[]> {
|
||||
return this.dependentFeaturesReadModel.getPossibleParentVariants(
|
||||
parentFeature,
|
||||
);
|
||||
}
|
||||
|
||||
async checkDependenciesExist(): Promise<boolean> {
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
FEATURE_DEPENDENCY_REMOVED,
|
||||
type IEventStore,
|
||||
} from '../../types';
|
||||
import { DEFAULT_ENV } from '../../util';
|
||||
|
||||
let app: IUnleashTest;
|
||||
let db: ITestDb;
|
||||
@ -55,6 +56,7 @@ afterAll(async () => {
|
||||
beforeEach(async () => {
|
||||
await db.stores.dependentFeaturesStore.deleteAll();
|
||||
await db.stores.featureToggleStore.deleteAll();
|
||||
await db.stores.featureEnvironmentStore.deleteAll();
|
||||
});
|
||||
|
||||
const addFeatureDependency = async (
|
||||
@ -93,12 +95,69 @@ const deleteFeatureDependencies = async (
|
||||
.expect(expectedCode);
|
||||
};
|
||||
|
||||
const getParentOptions = async (childFeature: string, expectedCode = 200) => {
|
||||
const getPossibleParentFeatures = async (
|
||||
childFeature: string,
|
||||
expectedCode = 200,
|
||||
) => {
|
||||
return app.request
|
||||
.get(`/api/admin/projects/default/features/${childFeature}/parents`)
|
||||
.expect(expectedCode);
|
||||
};
|
||||
|
||||
const getPossibleParentVariants = async (
|
||||
parentFeature: string,
|
||||
expectedCode = 200,
|
||||
) => {
|
||||
return app.request
|
||||
.get(
|
||||
`/api/admin/projects/default/features/${parentFeature}/parent-variants`,
|
||||
)
|
||||
.expect(expectedCode);
|
||||
};
|
||||
|
||||
const addStrategyVariants = async (parent: string, variants: string[]) => {
|
||||
await app.addStrategyToFeatureEnv(
|
||||
{
|
||||
name: 'flexibleRollout',
|
||||
constraints: [],
|
||||
parameters: { rollout: '100', stickiness: 'default' },
|
||||
variants: variants.map((name) => ({
|
||||
name,
|
||||
weight: 1000,
|
||||
weightType: 'variable',
|
||||
stickiness: 'default',
|
||||
})),
|
||||
},
|
||||
DEFAULT_ENV,
|
||||
parent,
|
||||
);
|
||||
};
|
||||
|
||||
const addFeatureEnvironmentVariant = async (
|
||||
parent: string,
|
||||
variant: string,
|
||||
) => {
|
||||
await app.request
|
||||
.patch(
|
||||
`/api/admin/projects/default/features/${parent}/environments/${DEFAULT_ENV}/variants`,
|
||||
)
|
||||
.set('Content-Type', 'application/json')
|
||||
.send([
|
||||
{
|
||||
op: 'add',
|
||||
path: '/0',
|
||||
value: {
|
||||
name: variant,
|
||||
weightType: 'variable',
|
||||
weight: 1000,
|
||||
overrides: [],
|
||||
stickiness: 'default',
|
||||
},
|
||||
},
|
||||
])
|
||||
.expect(200);
|
||||
};
|
||||
|
||||
const checkDependenciesExist = async (expectedCode = 200) => {
|
||||
return app.request
|
||||
.get(`/api/admin/projects/default/dependencies`)
|
||||
@ -111,8 +170,8 @@ test('should add and delete feature dependencies', async () => {
|
||||
await app.createFeature(parent);
|
||||
await app.createFeature(child);
|
||||
|
||||
const { body: parentOptions } = await getParentOptions(child);
|
||||
expect(parentOptions).toStrictEqual([parent]);
|
||||
const { body: options } = await getPossibleParentFeatures(child);
|
||||
expect(options).toStrictEqual([parent]);
|
||||
|
||||
// save explicit enabled and variants
|
||||
await addFeatureDependency(child, {
|
||||
@ -136,7 +195,7 @@ test('should add and delete feature dependencies', async () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('should sort parent options alphabetically', async () => {
|
||||
test('should sort potential parent features alphabetically', async () => {
|
||||
const parent1 = `a${uuidv4()}`;
|
||||
const parent2 = `c${uuidv4()}`;
|
||||
const parent3 = `b${uuidv4()}`;
|
||||
@ -146,8 +205,20 @@ test('should sort parent options alphabetically', async () => {
|
||||
await app.createFeature(parent3);
|
||||
await app.createFeature(child);
|
||||
|
||||
const { body: parentOptions } = await getParentOptions(child);
|
||||
expect(parentOptions).toStrictEqual([parent1, parent3, parent2]);
|
||||
const { body: options } = await getPossibleParentFeatures(child);
|
||||
expect(options).toStrictEqual([parent1, parent3, parent2]);
|
||||
});
|
||||
|
||||
test('should sort potential parent variants', async () => {
|
||||
const parent = uuidv4();
|
||||
await app.createFeature(parent);
|
||||
await addFeatureEnvironmentVariant(parent, 'e');
|
||||
await addStrategyVariants(parent, ['c', 'a', 'd']);
|
||||
await addStrategyVariants(parent, ['b', 'd']);
|
||||
|
||||
const { body: variants } = await getPossibleParentVariants(parent);
|
||||
|
||||
expect(variants).toStrictEqual(['a', 'b', 'c', 'd', 'e']);
|
||||
});
|
||||
|
||||
test('should not allow to add grandparent', async () => {
|
||||
|
@ -15,7 +15,11 @@ export class FakeDependentFeaturesReadModel
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
getParentOptions(): Promise<string[]> {
|
||||
getPossibleParentFeatures(): Promise<string[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
getPossibleParentVariants(): Promise<string[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
|
@ -117,6 +117,7 @@ export * from './outdated-sdks-schema';
|
||||
export * from './override-schema';
|
||||
export * from './parameters-schema';
|
||||
export * from './parent-feature-options-schema';
|
||||
export * from './parent-variant-options-schema';
|
||||
export * from './password-schema';
|
||||
export * from './pat-schema';
|
||||
export * from './patch-schema';
|
||||
|
16
src/lib/openapi/spec/parent-variant-options-schema.ts
Normal file
16
src/lib/openapi/spec/parent-variant-options-schema.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const parentVariantOptionsSchema = {
|
||||
$id: '#/components/schemas/parentVariantOptionsSchema',
|
||||
type: 'array',
|
||||
description:
|
||||
'A list of parent variant names available for a given parent feature. This list includes strategy variants and feature environment variants.',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type ParentVariantOptionsSchema = FromSchema<
|
||||
typeof parentVariantOptionsSchema
|
||||
>;
|
Loading…
Reference in New Issue
Block a user