mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-10 01:16:39 +02:00
feat: List possible parent variants (#6733)
This commit is contained in:
parent
664ceaea09
commit
42355b0c89
@ -15,7 +15,10 @@ import {
|
|||||||
createResponseSchema,
|
createResponseSchema,
|
||||||
emptyResponse,
|
emptyResponse,
|
||||||
getStandardResponses,
|
getStandardResponses,
|
||||||
|
parentFeatureOptionsSchema,
|
||||||
type ParentFeatureOptionsSchema,
|
type ParentFeatureOptionsSchema,
|
||||||
|
type ParentVariantOptionsSchema,
|
||||||
|
parentVariantOptionsSchema,
|
||||||
} from '../../openapi';
|
} from '../../openapi';
|
||||||
import type { IAuthRequest } from '../../routes/unleash-types';
|
import type { IAuthRequest } from '../../routes/unleash-types';
|
||||||
import type { DependentFeaturesService } from './dependent-features-service';
|
import type { DependentFeaturesService } from './dependent-features-service';
|
||||||
@ -29,6 +32,10 @@ interface FeatureParams extends ProjectParams {
|
|||||||
child: string;
|
child: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ParentVariantsParams extends ProjectParams {
|
||||||
|
parent: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface DeleteDependencyParams extends ProjectParams {
|
interface DeleteDependencyParams extends ProjectParams {
|
||||||
child: string;
|
child: string;
|
||||||
parent: string;
|
parent: string;
|
||||||
@ -39,6 +46,7 @@ const PATH_FEATURE = `${PATH}/:child`;
|
|||||||
const PATH_DEPENDENCIES = `${PATH_FEATURE}/dependencies`;
|
const PATH_DEPENDENCIES = `${PATH_FEATURE}/dependencies`;
|
||||||
const PATH_DEPENDENCIES_CHECK = `/:projectId/dependencies`;
|
const PATH_DEPENDENCIES_CHECK = `/:projectId/dependencies`;
|
||||||
const PATH_PARENTS = `${PATH_FEATURE}/parents`;
|
const PATH_PARENTS = `${PATH_FEATURE}/parents`;
|
||||||
|
const PATH_PARENT_VARIANTS = `${PATH}/:parent/parent-variants`;
|
||||||
const PATH_DEPENDENCY = `${PATH_FEATURE}/dependencies/:parent`;
|
const PATH_DEPENDENCY = `${PATH_FEATURE}/dependencies/:parent`;
|
||||||
|
|
||||||
type DependentFeaturesServices = Pick<
|
type DependentFeaturesServices = Pick<
|
||||||
@ -136,7 +144,7 @@ export default class DependentFeaturesController extends Controller {
|
|||||||
this.route({
|
this.route({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: PATH_PARENTS,
|
path: PATH_PARENTS,
|
||||||
handler: this.getParentOptions,
|
handler: this.getPossibleParentFeatures,
|
||||||
permission: NONE,
|
permission: NONE,
|
||||||
middleware: [
|
middleware: [
|
||||||
openApiService.validPath({
|
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({
|
this.route({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: PATH_DEPENDENCIES_CHECK,
|
path: PATH_DEPENDENCIES_CHECK,
|
||||||
@ -227,15 +255,42 @@ export default class DependentFeaturesController extends Controller {
|
|||||||
res.status(200).end();
|
res.status(200).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getParentOptions(
|
async getPossibleParentFeatures(
|
||||||
req: IAuthRequest<FeatureParams, any, any>,
|
req: IAuthRequest<FeatureParams, any, any>,
|
||||||
res: Response<ParentFeatureOptionsSchema>,
|
res: Response<ParentFeatureOptionsSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { child } = req.params;
|
const { child } = req.params;
|
||||||
|
|
||||||
const parentOptions =
|
const options =
|
||||||
await this.dependentFeaturesService.getParentOptions(child);
|
await this.dependentFeaturesService.getPossibleParentFeatures(
|
||||||
res.send(parentOptions);
|
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(
|
async checkDependenciesExist(
|
||||||
|
@ -7,7 +7,8 @@ export interface IDependentFeaturesReadModel {
|
|||||||
getOrphanParents(parentsAndChildren: string[]): Promise<string[]>;
|
getOrphanParents(parentsAndChildren: string[]): Promise<string[]>;
|
||||||
getParents(child: string): Promise<IDependency[]>;
|
getParents(child: string): Promise<IDependency[]>;
|
||||||
getDependencies(children: string[]): Promise<IFeatureDependency[]>;
|
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>;
|
haveDependencies(features: string[]): Promise<boolean>;
|
||||||
hasAnyDependencies(): 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 { IDependentFeaturesReadModel } from './dependent-features-read-model-type';
|
||||||
import type { IDependency, IFeatureDependency } from '../../types';
|
import type { IDependency, IFeatureDependency } from '../../types';
|
||||||
|
|
||||||
|
interface IVariantName {
|
||||||
|
variant_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class DependentFeaturesReadModel implements IDependentFeaturesReadModel {
|
export class DependentFeaturesReadModel implements IDependentFeaturesReadModel {
|
||||||
private db: Db;
|
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')
|
const result = await this.db('features')
|
||||||
.where('features.name', child)
|
.where('features.name', child)
|
||||||
.select('features.project');
|
.select('features.project');
|
||||||
@ -82,6 +86,35 @@ export class DependentFeaturesReadModel implements IDependentFeaturesReadModel {
|
|||||||
return rows.map((item) => item.name);
|
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> {
|
async haveDependencies(features: string[]): Promise<boolean> {
|
||||||
const parents = await this.db('dependent_features')
|
const parents = await this.db('dependent_features')
|
||||||
.whereIn('parent', features)
|
.whereIn('parent', features)
|
||||||
|
@ -227,8 +227,16 @@ export class DependentFeaturesService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getParentOptions(feature: string): Promise<string[]> {
|
async getPossibleParentFeatures(feature: string): Promise<string[]> {
|
||||||
return this.dependentFeaturesReadModel.getParentOptions(feature);
|
return this.dependentFeaturesReadModel.getPossibleParentFeatures(
|
||||||
|
feature,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPossibleParentVariants(parentFeature: string): Promise<string[]> {
|
||||||
|
return this.dependentFeaturesReadModel.getPossibleParentVariants(
|
||||||
|
parentFeature,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkDependenciesExist(): Promise<boolean> {
|
async checkDependenciesExist(): Promise<boolean> {
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
FEATURE_DEPENDENCY_REMOVED,
|
FEATURE_DEPENDENCY_REMOVED,
|
||||||
type IEventStore,
|
type IEventStore,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
import { DEFAULT_ENV } from '../../util';
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
@ -55,6 +56,7 @@ afterAll(async () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await db.stores.dependentFeaturesStore.deleteAll();
|
await db.stores.dependentFeaturesStore.deleteAll();
|
||||||
await db.stores.featureToggleStore.deleteAll();
|
await db.stores.featureToggleStore.deleteAll();
|
||||||
|
await db.stores.featureEnvironmentStore.deleteAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
const addFeatureDependency = async (
|
const addFeatureDependency = async (
|
||||||
@ -93,12 +95,69 @@ const deleteFeatureDependencies = async (
|
|||||||
.expect(expectedCode);
|
.expect(expectedCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getParentOptions = async (childFeature: string, expectedCode = 200) => {
|
const getPossibleParentFeatures = async (
|
||||||
|
childFeature: string,
|
||||||
|
expectedCode = 200,
|
||||||
|
) => {
|
||||||
return app.request
|
return app.request
|
||||||
.get(`/api/admin/projects/default/features/${childFeature}/parents`)
|
.get(`/api/admin/projects/default/features/${childFeature}/parents`)
|
||||||
.expect(expectedCode);
|
.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) => {
|
const checkDependenciesExist = async (expectedCode = 200) => {
|
||||||
return app.request
|
return app.request
|
||||||
.get(`/api/admin/projects/default/dependencies`)
|
.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(parent);
|
||||||
await app.createFeature(child);
|
await app.createFeature(child);
|
||||||
|
|
||||||
const { body: parentOptions } = await getParentOptions(child);
|
const { body: options } = await getPossibleParentFeatures(child);
|
||||||
expect(parentOptions).toStrictEqual([parent]);
|
expect(options).toStrictEqual([parent]);
|
||||||
|
|
||||||
// save explicit enabled and variants
|
// save explicit enabled and variants
|
||||||
await addFeatureDependency(child, {
|
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 parent1 = `a${uuidv4()}`;
|
||||||
const parent2 = `c${uuidv4()}`;
|
const parent2 = `c${uuidv4()}`;
|
||||||
const parent3 = `b${uuidv4()}`;
|
const parent3 = `b${uuidv4()}`;
|
||||||
@ -146,8 +205,20 @@ test('should sort parent options alphabetically', async () => {
|
|||||||
await app.createFeature(parent3);
|
await app.createFeature(parent3);
|
||||||
await app.createFeature(child);
|
await app.createFeature(child);
|
||||||
|
|
||||||
const { body: parentOptions } = await getParentOptions(child);
|
const { body: options } = await getPossibleParentFeatures(child);
|
||||||
expect(parentOptions).toStrictEqual([parent1, parent3, parent2]);
|
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 () => {
|
test('should not allow to add grandparent', async () => {
|
||||||
|
@ -15,7 +15,11 @@ export class FakeDependentFeaturesReadModel
|
|||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
getParentOptions(): Promise<string[]> {
|
getPossibleParentFeatures(): Promise<string[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPossibleParentVariants(): Promise<string[]> {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,6 +117,7 @@ export * from './outdated-sdks-schema';
|
|||||||
export * from './override-schema';
|
export * from './override-schema';
|
||||||
export * from './parameters-schema';
|
export * from './parameters-schema';
|
||||||
export * from './parent-feature-options-schema';
|
export * from './parent-feature-options-schema';
|
||||||
|
export * from './parent-variant-options-schema';
|
||||||
export * from './password-schema';
|
export * from './password-schema';
|
||||||
export * from './pat-schema';
|
export * from './pat-schema';
|
||||||
export * from './patch-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