Skip to content

Commit 992f432

Browse files
add ParentFeatureFilter
1 parent edee87f commit 992f432

File tree

3 files changed

+117
-1
lines changed

3 files changed

+117
-1
lines changed

src/feature-management/src/featureManager.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { IFeatureFilter } from "./filter/featureFilter.js";
66
import { FeatureFlag, RequirementType, VariantDefinition } from "./schema/model.js";
77
import { IFeatureFlagProvider, IFeatureManager } from "./model.js";
88
import { TargetingFilter } from "./filter/targetingFilter.js";
9+
import { ParentFeatureFilter } from "./filter/parentFeatureFilter.js";
910
import { Variant } from "./variant/variant.js";
1011
import { ITargetingContext, ITargetingContextAccessor } from "./common/targetingContext.js";
1112
import { isTargetedGroup, isTargetedPercentile, isTargetedUser } from "./common/targetingEvaluator.js";
@@ -21,7 +22,10 @@ export class FeatureManager implements IFeatureManager {
2122
this.#onFeatureEvaluated = options?.onFeatureEvaluated;
2223
this.#targetingContextAccessor = options?.targetingContextAccessor;
2324

24-
const builtinFilters = [new TimeWindowFilter(), new TargetingFilter(options?.targetingContextAccessor)];
25+
const builtinFilters = [
26+
new TimeWindowFilter(),
27+
new TargetingFilter(options?.targetingContextAccessor),
28+
new ParentFeatureFilter(this)];
2529
// If a custom filter shares a name with an existing filter, the custom filter overrides the existing one.
2630
for (const filter of [...builtinFilters, ...(options?.customFilters ?? [])]) {
2731
this.#featureFilters.set(filter.name, filter);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { IFeatureFilter } from "./featureFilter.js";
5+
import { IFeatureManager } from "../model.js";
6+
7+
type ParentFeatureFilterEvaluationContext = {
8+
featureName: string;
9+
parameters: ParentFeatureFilterParameters;
10+
};
11+
12+
type ParentFeatureFilterParameters = {
13+
Name: string
14+
};
15+
16+
export class ParentFeatureFilter implements IFeatureFilter {
17+
readonly name: string = "Microsoft.ParentFeature";
18+
readonly #featureManager: IFeatureManager;
19+
20+
constructor(featureManager: IFeatureManager) {
21+
this.#featureManager = featureManager;
22+
}
23+
24+
async evaluate(context: ParentFeatureFilterEvaluationContext): Promise<boolean> {
25+
const {featureName, parameters} = context;
26+
27+
if (!parameters || !parameters.Name) {
28+
throw new Error(`ParentFeatureFilter: Missing required parameter 'Name' for feature '${featureName}'.`);
29+
}
30+
return await this.#featureManager.isEnabled(parameters.Name);
31+
}
32+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import * as chai from "chai";
5+
import chaiAsPromised from "chai-as-promised";
6+
import { FeatureManager, ConfigurationMapFeatureFlagProvider } from "../src/index.js";
7+
chai.use(chaiAsPromised);
8+
const expect = chai.expect;
9+
10+
describe("parent feature filter", () => {
11+
it("should evaluate feature with parent feature filter enabled", async () => {
12+
const config = {
13+
"feature_management": {
14+
"feature_flags": [
15+
{
16+
"id": "FeatureA",
17+
"enabled": true,
18+
"conditions": {
19+
"client_filters": [
20+
{
21+
"name": "Microsoft.ParentFeature",
22+
"parameters": {
23+
"Name": "FeatureB"
24+
}
25+
}
26+
]
27+
}
28+
},
29+
{
30+
"id": "FeatureB",
31+
"enabled": true
32+
}
33+
]
34+
}
35+
};
36+
37+
const dataSource = new Map();
38+
dataSource.set("feature_management", config.feature_management);
39+
const provider = new ConfigurationMapFeatureFlagProvider(dataSource);
40+
const featureManager = new FeatureManager(provider);
41+
42+
const result = await featureManager.isEnabled("FeatureA");
43+
expect(result).to.equal(true);
44+
});
45+
46+
it("should evaluate feature with parent feature filter disabled", async () => {
47+
const config = {
48+
"feature_management": {
49+
"feature_flags": [
50+
{
51+
"id": "FeatureA",
52+
"enabled": true,
53+
"conditions": {
54+
"client_filters": [
55+
{
56+
"name": "ParentFeature",
57+
"parameters": {
58+
"Name": "FeatureB"
59+
}
60+
}
61+
]
62+
}
63+
},
64+
{
65+
"id": "FeatureB",
66+
"enabled": false
67+
}
68+
]
69+
}
70+
};
71+
72+
const dataSource = new Map();
73+
dataSource.set("feature_management", config.feature_management);
74+
const provider = new ConfigurationMapFeatureFlagProvider(dataSource);
75+
const featureManager = new FeatureManager(provider);
76+
77+
const result = await featureManager.isEnabled("FeatureA");
78+
expect(result).to.equal(false);
79+
});
80+
});

0 commit comments

Comments
 (0)