Skip to content

Commit 98006f2

Browse files
committed
built-in ConfigurationFeatureProvider supports both object and map
1 parent 8e0c11c commit 98006f2

File tree

8 files changed

+101
-62
lines changed

8 files changed

+101
-62
lines changed

package-lock.json

Lines changed: 14 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,8 @@
3737
"rollup-plugin-dts": "^6.1.0",
3838
"tslib": "^2.6.2",
3939
"typescript": "^5.3.3"
40+
},
41+
"dependencies": {
42+
"chai-as-promised": "^7.1.1"
4043
}
4144
}

src/featureManager.ts

Lines changed: 12 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { TimewindowFilter } from "./filter/TimeWindowFilter";
22
import { IFeatureFilter } from "./filter/FeatureFilter";
3-
import { FEATURE_FLAGS_KEY, FEATURE_MANAGEMENT_KEY, FeatureDefinition, FeatureManagement, RequirementType } from "./model";
3+
import { FeatureDefinition, RequirementType } from "./model";
4+
import { IFeatureProvider } from "./featureProvider";
45

56
export class FeatureManager {
6-
#provider: IFeatureDefinitionProvider;
7+
#provider: IFeatureProvider;
78
#featureFilters: Map<string, IFeatureFilter> = new Map();
89

9-
constructor(provider: IFeatureDefinitionProvider, options?: FeatureManagerOptions) {
10+
constructor(provider: IFeatureProvider, options?: FeatureManagerOptions) {
1011
this.#provider = provider;
1112

1213
const defaultFilters = [new TimewindowFilter()];
@@ -15,14 +16,16 @@ export class FeatureManager {
1516
}
1617
}
1718

18-
listFeatureNames(): string[] {
19-
const featureNameSet = new Set(this.#features.map((feature) => feature.id));
19+
async listFeatureNames(): Promise<string[]> {
20+
const features = await this.#features();
21+
const featureNameSet = new Set(features.map((feature) => feature.id));
2022
return Array.from(featureNameSet);
2123
}
2224

2325
// If multiple feature flags are found, the first one takes precedence.
2426
async isEnabled(featureId: string, context?: unknown): Promise<boolean> {
25-
const featureFlag = this.#features.find((flag) => flag.id === featureId);
27+
const features = await this.#features();
28+
const featureFlag = features.find((flag) => flag.id === featureId);
2629
if (featureFlag === undefined) {
2730
// If the feature is not found, then it is disabled.
2831
return false;
@@ -61,46 +64,13 @@ export class FeatureManager {
6164
}
6265
}
6366

64-
get #features(): FeatureDefinition[] {
65-
return this.#provider.getFeatureDefinitions();
67+
async #features(): Promise<FeatureDefinition[]> {
68+
const features = await this.#provider.getFeatureFlags();
69+
return features;
6670
}
6771

6872
}
6973

70-
export interface IFeatureDefinitionProvider {
71-
getFeatureDefinitions(): FeatureDefinition[];
72-
}
73-
74-
export class MapBasedFeatureDefinitionProvider implements IFeatureDefinitionProvider {
75-
#map: Map<string, FeatureManagement>;
76-
77-
constructor(map: Map<string, FeatureManagement>) {
78-
this.#map = map;
79-
}
80-
81-
getFeatureDefinitions(): FeatureDefinition[] {
82-
return this.#map.get(FEATURE_MANAGEMENT_KEY)?.[FEATURE_FLAGS_KEY] ?? [];
83-
}
84-
}
85-
86-
export class JsonBasedFeatureDefinitionProvider implements IFeatureDefinitionProvider {
87-
#featureFlags: FeatureDefinition[];
88-
89-
constructor(private json: string) {
90-
const featureManagement = JSON.parse(this.json) as FeatureManagement;
91-
92-
if (featureManagement?.[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] === undefined) {
93-
throw new Error("Invalid input data");
94-
}
95-
96-
this.#featureFlags = featureManagement[FEATURE_MANAGEMENT_KEY][FEATURE_FLAGS_KEY];
97-
}
98-
99-
getFeatureDefinitions(): FeatureDefinition[] {
100-
return this.#featureFlags;
101-
}
102-
}
103-
10474
interface FeatureManagerOptions {
10575
customFilters?: IFeatureFilter[];
10676
}

src/featureProvider.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { IGettable, isGettable } from "./gettable";
2+
import { FeatureDefinition, FeatureConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from "./model";
3+
4+
export interface IFeatureProvider {
5+
getFeatureFlags(): Promise<FeatureDefinition[]>;
6+
}
7+
8+
export class ConfigurationFeatureProvider implements IFeatureProvider {
9+
#configuration: IGettable | Record<string, unknown>;
10+
11+
constructor(configuration: Record<string, unknown> | IGettable) {
12+
if (typeof configuration !== "object") {
13+
throw new Error("Configuration must be an object.");
14+
}
15+
this.#configuration = configuration;
16+
}
17+
18+
async getFeatureFlags(): Promise<FeatureDefinition[]> {
19+
if (isGettable(this.#configuration)) {
20+
const featureConfig = this.#configuration.get<FeatureConfiguration>(FEATURE_MANAGEMENT_KEY);
21+
return featureConfig?.[FEATURE_FLAGS_KEY] ?? [];
22+
} else {
23+
return this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? [];
24+
}
25+
}
26+
}

src/gettable.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface IGettable {
2+
get<T>(key: string): T | undefined;
3+
}
4+
5+
export function isGettable(object: unknown): object is IGettable {
6+
return typeof object === "object" && object !== null && typeof (object as IGettable).get === "function";
7+
}

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
export { FeatureManager } from "./featureManager";
4+
export { FeatureManager } from "./featureManager";
5+
export { ConfigurationFeatureProvider, IFeatureProvider } from "./featureProvider";

src/model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,6 @@ export interface ClientFilter {
6161
// Feature Management Section fed into feature manager.
6262
export const FEATURE_MANAGEMENT_KEY = "FeatureManagement"
6363
export const FEATURE_FLAGS_KEY = "FeatureFlags"
64-
export interface FeatureManagement {
64+
export interface FeatureConfiguration {
6565
[FEATURE_FLAGS_KEY]: FeatureDefinition[]
6666
}

test/featureManager.test.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,25 @@
22
// Licensed under the MIT license.
33

44
import * as chai from "chai";
5+
import * as chaiAsPromised from "chai-as-promised";
6+
chai.use(chaiAsPromised);
57
const expect = chai.expect;
68

7-
import { FeatureManager } from "./exportedApi";
9+
import { FeatureManager, ConfigurationFeatureProvider } from "./exportedApi";
810

911
describe("feature manager", () => {
1012
it("should load from json string", () => {
11-
const json = `{
13+
const jsonObject = {
1214
"FeatureManagement": {
1315
"FeatureFlags": [
1416
{ "id": "Alpha", "description": "", "enabled": true}
1517
]
1618
}
17-
}`;
19+
};
1820

19-
const featureManager = new FeatureManager(json);
20-
expect(featureManager.isEnabled("Alpha")).eq(true);
21+
const provider = new ConfigurationFeatureProvider(jsonObject);
22+
const featureManager = new FeatureManager(provider);
23+
return expect(featureManager.isEnabled("Alpha")).eventually.eq(true);
2124
});
2225

2326
it("should load from map", () => {
@@ -28,8 +31,28 @@ describe("feature manager", () => {
2831
],
2932
});
3033

31-
const featureManager = new FeatureManager(dataSource);
32-
expect(featureManager.isEnabled("Alpha")).eq(true);
34+
const provider = new ConfigurationFeatureProvider(dataSource);
35+
const featureManager = new FeatureManager(provider);
36+
return expect(featureManager.isEnabled("Alpha")).eventually.eq(true);
37+
});
38+
39+
it("should load latest data if source is updated after initialization", () => {
40+
const dataSource = new Map();
41+
dataSource.set("FeatureManagement", {
42+
FeatureFlags: [
43+
{ id: "Alpha", enabled: true }
44+
],
45+
});
46+
47+
const provider = new ConfigurationFeatureProvider(dataSource);
48+
const featureManager = new FeatureManager(provider);
49+
dataSource.set("FeatureManagement", {
50+
FeatureFlags: [
51+
{ id: "Alpha", enabled: false }
52+
],
53+
});
54+
55+
return expect(featureManager.isEnabled("Alpha")).eventually.eq(false);
3356
});
3457

3558
it("shoud evaluate features without conditions", () => {
@@ -41,9 +64,12 @@ describe("feature manager", () => {
4164
],
4265
});
4366

44-
const featureManager = new FeatureManager(dataSource);
45-
expect(featureManager.isEnabled("Alpha")).eq(true);
46-
expect(featureManager.isEnabled("Beta")).eq(false);
67+
const provider = new ConfigurationFeatureProvider(dataSource);
68+
const featureManager = new FeatureManager(provider);
69+
return Promise.all([
70+
expect(featureManager.isEnabled("Alpha")).eventually.eq(true),
71+
expect(featureManager.isEnabled("Beta")).eventually.eq(false)
72+
]);
4773
});
4874

4975
it("shoud evaluate features with conditions");

0 commit comments

Comments
 (0)