1
0
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:
Mateusz Kwasniewski 2024-03-28 16:53:30 +01:00 committed by GitHub
parent 664ceaea09
commit 42355b0c89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 205 additions and 16 deletions

View File

@ -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(

View File

@ -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>;
}

View File

@ -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)

View File

@ -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> {

View File

@ -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 () => {

View File

@ -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([]);
}

View File

@ -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';

View 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
>;