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 environment = featureQuery?.environment || DEFAULT_ENV;
const stopTimer = this.timer('getFeatureAdmin');
const dependentFeaturesEnabled =
this.flagResolver.isEnabled('dependentFeatures');
let selectColumns = [
'features.name as name',
@ -91,6 +93,9 @@ export default class FeatureToggleClientStore
'fs.variants as strategy_variants',
'segments.id as segment_id',
'segments.constraints as segment_constraints',
'df.parent as parent',
'df.variants as parent_variants',
'df.enabled as parent_enabled',
] as (string | Raw<any>)[];
let query = this.db('features')
@ -122,7 +127,8 @@ export default class FeatureToggleClientStore
`fss.feature_strategy_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) {
query = query.leftJoin(
@ -195,6 +201,16 @@ export default class FeatureToggleClientStore
) {
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.enabled = !!r.enabled;
feature.name = r.name;

View File

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

View File

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

View File

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

View File

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

View File

@ -76,6 +76,7 @@ export interface IFeatureToggleClient {
variants: IVariant[];
enabled: boolean;
strategies: Omit<IStrategyConfig, 'disabled'>[];
dependencies?: IDependency[];
impressionData?: boolean;
lastSeenAt?: 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 interface IEnvironment {

View File

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

View File

@ -10,7 +10,9 @@ let app: IUnleashTest;
let db: ITestDb;
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, {
experimental: {
flags: {
@ -36,6 +38,7 @@ beforeAll(async () => {
},
'test',
);
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',
{
@ -52,6 +55,16 @@ beforeAll(async () => {
},
'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(
'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 () => {
return app.request
.get('/api/client/features')