mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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