1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: Client api dependent features (#4778)

This commit is contained in:
Mateusz Kwasniewski 2023-09-20 11:53:43 +02:00 committed by GitHub
parent 9704f4a04d
commit 85c7f84f8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 82 additions and 7 deletions

View File

@ -67,6 +67,8 @@ export default class FeatureToggleClientStore
const isPlayground = requestType === 'playground'; const isPlayground = requestType === 'playground';
const environment = featureQuery?.environment || DEFAULT_ENV; const environment = featureQuery?.environment || DEFAULT_ENV;
const stopTimer = this.timer('getFeatureAdmin'); const stopTimer = this.timer('getFeatureAdmin');
const dependentFeaturesEnabled =
this.flagResolver.isEnabled('dependentFeatures');
let selectColumns = [ let selectColumns = [
'features.name as name', 'features.name as name',
@ -91,6 +93,9 @@ export default class FeatureToggleClientStore
'fs.variants as strategy_variants', 'fs.variants as strategy_variants',
'segments.id as segment_id', 'segments.id as segment_id',
'segments.constraints as segment_constraints', 'segments.constraints as segment_constraints',
'df.parent as parent',
'df.variants as parent_variants',
'df.enabled as parent_enabled',
] as (string | Raw<any>)[]; ] as (string | Raw<any>)[];
let query = this.db('features') let query = this.db('features')
@ -122,7 +127,8 @@ export default class FeatureToggleClientStore
`fss.feature_strategy_id`, `fss.feature_strategy_id`,
`fs.id`, `fs.id`,
) )
.leftJoin('segments', `segments.id`, `fss.segment_id`); .leftJoin('segments', `segments.id`, `fss.segment_id`)
.leftJoin('dependent_features as df', 'df.child', 'features.name');
if (isAdmin) { if (isAdmin) {
query = query.leftJoin( query = query.leftJoin(
@ -195,6 +201,16 @@ export default class FeatureToggleClientStore
) { ) {
this.addSegmentIdsToStrategy(feature, r); this.addSegmentIdsToStrategy(feature, r);
} }
if (r.parent && !isAdmin && dependentFeaturesEnabled) {
feature.dependencies = feature.dependencies || [];
feature.dependencies.push({
feature: r.parent,
enabled: r.parent_enabled,
...(r.parent_enabled
? { variants: r.parent_variants }
: {}),
});
}
feature.impressionData = r.impression_data; feature.impressionData = r.impression_data;
feature.enabled = !!r.enabled; feature.enabled = !!r.enabled;
feature.name = r.name; feature.name = r.name;

View File

@ -17,20 +17,20 @@ export class DependentFeaturesService {
} }
async upsertFeatureDependency( async upsertFeatureDependency(
parentFeature: string, childFeature: string,
dependentFeature: CreateDependentFeatureSchema, dependentFeature: CreateDependentFeatureSchema,
): Promise<void> { ): Promise<void> {
const { enabled, feature, variants } = dependentFeature; const { enabled, feature, variants } = dependentFeature;
const featureDependency: FeatureDependency = const featureDependency: FeatureDependency =
enabled === false enabled === false
? { ? {
parent: parentFeature, parent: feature,
child: feature, child: childFeature,
enabled, enabled,
} }
: { : {
parent: parentFeature, parent: feature,
child: feature, child: childFeature,
enabled: true, enabled: true,
variants, variants,
}; };

View File

@ -5,6 +5,7 @@ import { featureStrategySchema } from './feature-strategy-schema';
import { variantSchema } from './variant-schema'; import { variantSchema } from './variant-schema';
import { overrideSchema } from './override-schema'; import { overrideSchema } from './override-schema';
import { strategyVariantSchema } from './strategy-variant-schema'; import { strategyVariantSchema } from './strategy-variant-schema';
import { dependentFeatureSchema } from './dependent-feature-schema';
export const clientFeatureSchema = { export const clientFeatureSchema = {
$id: '#/components/schemas/clientFeatureSchema', $id: '#/components/schemas/clientFeatureSchema',
@ -73,6 +74,13 @@ export const clientFeatureSchema = {
}, },
nullable: true, nullable: true,
}, },
dependencies: {
type: 'array',
description: 'Feature dependencies for this toggle',
items: {
$ref: '#/components/schemas/dependentFeatureSchema',
},
},
}, },
components: { components: {
schemas: { schemas: {
@ -82,6 +90,7 @@ export const clientFeatureSchema = {
strategyVariantSchema, strategyVariantSchema,
variantSchema, variantSchema,
overrideSchema, overrideSchema,
dependentFeatureSchema,
}, },
}, },
} as const; } as const;

View File

@ -9,6 +9,7 @@ import { featureStrategySchema } from './feature-strategy-schema';
import { clientFeatureSchema } from './client-feature-schema'; import { clientFeatureSchema } from './client-feature-schema';
import { variantSchema } from './variant-schema'; import { variantSchema } from './variant-schema';
import { strategyVariantSchema } from './strategy-variant-schema'; import { strategyVariantSchema } from './strategy-variant-schema';
import { dependentFeatureSchema } from './dependent-feature-schema';
export const clientFeaturesSchema = { export const clientFeaturesSchema = {
$id: '#/components/schemas/clientFeaturesSchema', $id: '#/components/schemas/clientFeaturesSchema',
@ -57,6 +58,7 @@ export const clientFeaturesSchema = {
featureStrategySchema, featureStrategySchema,
strategyVariantSchema, strategyVariantSchema,
variantSchema, variantSchema,
dependentFeatureSchema,
}, },
}, },
} as const; } as const;

View File

@ -986,6 +986,7 @@ class FeatureToggleService {
variants, variants,
description, description,
impressionData, impressionData,
dependencies,
}) => ({ }) => ({
name, name,
type, type,
@ -996,6 +997,7 @@ class FeatureToggleService {
variants, variants,
description, description,
impressionData, impressionData,
dependencies,
}), }),
); );
} }

View File

@ -76,6 +76,7 @@ export interface IFeatureToggleClient {
variants: IVariant[]; variants: IVariant[];
enabled: boolean; enabled: boolean;
strategies: Omit<IStrategyConfig, 'disabled'>[]; strategies: Omit<IStrategyConfig, 'disabled'>[];
dependencies?: IDependency[];
impressionData?: boolean; impressionData?: boolean;
lastSeenAt?: Date; lastSeenAt?: Date;
createdAt?: Date; createdAt?: Date;
@ -133,6 +134,12 @@ export interface IVariant {
}[]; }[];
} }
export interface IDependency {
feature: string;
variants?: string[];
enabled?: boolean;
}
export type IStrategyVariant = Omit<IVariant, 'overrides'>; export type IStrategyVariant = Omit<IVariant, 'overrides'>;
export interface IEnvironment { export interface IEnvironment {

View File

@ -1,5 +1,6 @@
import { import {
FeatureToggleWithEnvironment, FeatureToggleWithEnvironment,
IDependency,
IFeatureOverview, IFeatureOverview,
IFeatureStrategy, IFeatureStrategy,
IStrategyConfig, IStrategyConfig,
@ -16,6 +17,7 @@ export interface FeatureConfigurationClient {
stale: boolean; stale: boolean;
strategies: IStrategyConfig[]; strategies: IStrategyConfig[];
variants: IVariant[]; variants: IVariant[];
dependencies?: IDependency[];
} }
export interface IFeatureStrategiesStore export interface IFeatureStrategiesStore
extends Store<IFeatureStrategy, string> { extends Store<IFeatureStrategy, string> {

View File

@ -10,7 +10,9 @@ let app: IUnleashTest;
let db: ITestDb; let db: ITestDb;
beforeAll(async () => { beforeAll(async () => {
db = await dbInit('feature_api_client', getLogger); db = await dbInit('feature_api_client', getLogger, {
experimental: { flags: { dependentFeatures: true } },
});
app = await setupAppWithCustomConfig(db.stores, { app = await setupAppWithCustomConfig(db.stores, {
experimental: { experimental: {
flags: { flags: {
@ -36,6 +38,7 @@ beforeAll(async () => {
}, },
'test', 'test',
); );
await app.services.featureToggleServiceV2.createFeatureToggle( await app.services.featureToggleServiceV2.createFeatureToggle(
'default', 'default',
{ {
@ -52,6 +55,16 @@ beforeAll(async () => {
}, },
'test', 'test',
); );
// depend on enabled feature with variant
await app.services.dependentFeaturesService.upsertFeatureDependency(
'featureY',
{ feature: 'featureX', variants: ['featureXVariant'] },
);
// depend on parent being disabled
await app.services.dependentFeaturesService.upsertFeatureDependency(
'featureY',
{ feature: 'featureZ', enabled: false },
);
await app.services.featureToggleServiceV2.archiveToggle( await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedX', 'featureArchivedX',
@ -127,6 +140,30 @@ test('returns four feature toggles', async () => {
}); });
}); });
test('returns dependencies', async () => {
return app.request
.get('/api/client/features')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
expect(res.body.features[0]).toMatchObject({
name: 'featureY',
dependencies: [
{
feature: 'featureX',
enabled: true,
variants: ['featureXVariant'],
},
{
feature: 'featureZ',
enabled: false,
},
],
});
expect(res.body.features[1].dependencies).toBe(undefined);
});
});
test('returns four feature toggles without createdAt', async () => { test('returns four feature toggles without createdAt', async () => {
return app.request return app.request
.get('/api/client/features') .get('/api/client/features')