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:
parent
9704f4a04d
commit
85c7f84f8d
@ -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;
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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> {
|
||||||
|
@ -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')
|
||||||
|
Loading…
Reference in New Issue
Block a user