Skip to content

Commit

Permalink
feat: Client api dependent features (#4778)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew committed Sep 20, 2023
1 parent 9704f4a commit 85c7f84
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 7 deletions.
18 changes: 17 additions & 1 deletion src/lib/db/feature-toggle-client-store.ts
Expand Up @@ -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',
Expand All @@ -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')
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;
Expand Down
Expand Up @@ -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,
};
Expand Down
9 changes: 9 additions & 0 deletions src/lib/openapi/spec/client-feature-schema.ts
Expand Up @@ -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',
Expand Down Expand Up @@ -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: {
Expand All @@ -82,6 +90,7 @@ export const clientFeatureSchema = {
strategyVariantSchema,
variantSchema,
overrideSchema,
dependentFeatureSchema,
},
},
} as const;
Expand Down
2 changes: 2 additions & 0 deletions src/lib/openapi/spec/client-features-schema.ts
Expand Up @@ -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',
Expand Down Expand Up @@ -57,6 +58,7 @@ export const clientFeaturesSchema = {
featureStrategySchema,
strategyVariantSchema,
variantSchema,
dependentFeatureSchema,
},
},
} as const;
Expand Down
2 changes: 2 additions & 0 deletions src/lib/services/feature-toggle-service.ts
Expand Up @@ -986,6 +986,7 @@ class FeatureToggleService {
variants,
description,
impressionData,
dependencies,
}) => ({
name,
type,
Expand All @@ -996,6 +997,7 @@ class FeatureToggleService {
variants,
description,
impressionData,
dependencies,
}),
);
}
Expand Down
7 changes: 7 additions & 0 deletions src/lib/types/model.ts
Expand Up @@ -76,6 +76,7 @@ export interface IFeatureToggleClient {
variants: IVariant[];
enabled: boolean;
strategies: Omit<IStrategyConfig, 'disabled'>[];
dependencies?: IDependency[];
impressionData?: boolean;
lastSeenAt?: Date;
createdAt?: Date;
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/types/stores/feature-strategies-store.ts
@@ -1,5 +1,6 @@
import {
FeatureToggleWithEnvironment,
IDependency,
IFeatureOverview,
IFeatureStrategy,
IStrategyConfig,
Expand All @@ -16,6 +17,7 @@ export interface FeatureConfigurationClient {
stale: boolean;
strategies: IStrategyConfig[];
variants: IVariant[];
dependencies?: IDependency[];
}
export interface IFeatureStrategiesStore
extends Store<IFeatureStrategy, string> {
Expand Down
39 changes: 38 additions & 1 deletion src/test/e2e/api/client/feature.e2e.test.ts
Expand Up @@ -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: {
Expand All @@ -36,6 +38,7 @@ beforeAll(async () => {
},
'test',
);

await app.services.featureToggleServiceV2.createFeatureToggle(
'default',
{
Expand All @@ -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',
Expand Down Expand Up @@ -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')
Expand Down

0 comments on commit 85c7f84

Please sign in to comment.