Skip to content

Commit ac57b92

Browse files
Eskibearlinglingye001
authored andcommitted
resovle conflicts
1 parent d4541e9 commit ac57b92

File tree

5 files changed

+200
-12
lines changed

5 files changed

+200
-12
lines changed

src/featureManager.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33

44
import { TimeWindowFilter } from "./filter/TimeWindowFilter.js";
55
import { IFeatureFilter } from "./filter/FeatureFilter.js";
6-
import { RequirementType } from "./model.js";
6+
import { RequirementType } from "./schema/model.ts";
77
import { IFeatureFlagProvider } from "./featureProvider.js";
88
import { TargetingFilter } from "./filter/TargetingFilter.js";
9+
import { validateFeatureFlag } from "./schema/validator.ts";
910

1011
export class FeatureManager {
1112
#provider: IFeatureFlagProvider;
@@ -30,15 +31,12 @@ export class FeatureManager {
3031

3132
// If multiple feature flags are found, the first one takes precedence.
3233
async isEnabled(featureName: string, context?: unknown): Promise<boolean> {
33-
const featureFlag = await this.#provider.getFeatureFlag(featureName);
34+
const featureFlag = await this.#getFeatureFlag(featureName);
3435
if (featureFlag === undefined) {
3536
// If the feature is not found, then it is disabled.
3637
return false;
3738
}
3839

39-
// Ensure that the feature flag is in the correct format. Feature providers should validate the feature flags, but we do it here as a safeguard.
40-
validateFeatureFlagFormat(featureFlag);
41-
4240
if (featureFlag.enabled !== true) {
4341
// If the feature is not explicitly enabled, then it is disabled by default.
4442
return false;
@@ -75,14 +73,15 @@ export class FeatureManager {
7573
return !shortCircuitEvaluationResult;
7674
}
7775

76+
async #getFeatureFlag(featureName: string): Promise<any> {
77+
const featureFlag = await this.#provider.getFeatureFlag(featureName);
78+
validateFeatureFlag(featureFlag);
79+
return featureFlag;
80+
}
81+
7882
}
7983

8084
interface FeatureManagerOptions {
8185
customFilters?: IFeatureFilter[];
8286
}
8387

84-
function validateFeatureFlagFormat(featureFlag: any): void {
85-
if (featureFlag.enabled !== undefined && typeof featureFlag.enabled !== "boolean") {
86-
throw new Error(`Feature flag ${featureFlag.id} has an invalid 'enabled' value.`);
87-
}
88-
}

src/featureProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT license.
33

44
import { IGettable } from "./gettable.js";
5-
import { FeatureFlag, FeatureManagementConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from "./model.js";
5+
import { FeatureFlag, FeatureManagementConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from "./schema/model.js";
66

77
export interface IFeatureFlagProvider {
88
/**
File renamed without changes.

src/schema/validator.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
/**
5+
* Validates a feature flag object, checking if it conforms to the schema.
6+
* @param featureFlag The feature flag object to validate.
7+
*/
8+
export function validateFeatureFlag(featureFlag: any): void {
9+
if (featureFlag === undefined) {
10+
return; // no-op if feature flag is undefined, indicating that the feature flag is not found
11+
}
12+
if (featureFlag === null || typeof featureFlag !== "object") { // Note: typeof null = "object"
13+
throw new TypeError("Feature flag must be an object.");
14+
}
15+
if (typeof featureFlag.id !== "string") {
16+
throw new TypeError("Feature flag ID must be a string.");
17+
}
18+
if (featureFlag.description !== undefined && typeof featureFlag.description !== "string") {
19+
throw new TypeError("Feature flag description must be a string.");
20+
}
21+
if (featureFlag.display_name !== undefined && typeof featureFlag.display_name !== "string") {
22+
throw new TypeError("Feature flag display name must be a string.");
23+
}
24+
if (featureFlag.enabled !== undefined && typeof featureFlag.enabled !== "boolean") {
25+
throw new TypeError("Feature flag enabled must be a boolean.");
26+
}
27+
if (featureFlag.conditions !== undefined) {
28+
validateFeatureEnablementConditions(featureFlag.conditions);
29+
}
30+
// variants, allocation, and telemetry
31+
if (featureFlag.variants !== undefined) {
32+
validateVariants(featureFlag.variants);
33+
}
34+
if (featureFlag.allocation !== undefined) {
35+
validateVariantAllocation(featureFlag.allocation);
36+
}
37+
if (featureFlag.telemetry !== undefined) {
38+
validateTelemetryOptions(featureFlag.telemetry);
39+
}
40+
}
41+
42+
function validateFeatureEnablementConditions(conditions: any) {
43+
if (conditions.requirement_type !== undefined && conditions.requirement_type !== "Any" && conditions.requirement_type !== "All") {
44+
throw new TypeError("Feature enablement conditions requirement type must be 'Any' or 'All'.");
45+
}
46+
if (conditions.client_filters !== undefined) {
47+
validateClientFilters(conditions.client_filters);
48+
}
49+
}
50+
51+
function validateClientFilters(client_filters: any) {
52+
if (!Array.isArray(client_filters)) {
53+
throw new TypeError("Client filters must be an array.");
54+
}
55+
56+
for (const filter of client_filters) {
57+
if (typeof filter.name !== "string") {
58+
throw new TypeError("Client filter name must be a string.");
59+
}
60+
if (filter.parameters !== undefined && typeof filter.parameters !== "object") {
61+
throw new TypeError("Client filter parameters must be an object.");
62+
}
63+
// validate parameters here if we have schema of specific filters in the future
64+
}
65+
}
66+
67+
function validateVariants(variants: any) {
68+
if (!Array.isArray(variants)) {
69+
throw new TypeError("Variants must be an array.");
70+
}
71+
72+
for (const variant of variants) {
73+
if (typeof variant.name !== "string") {
74+
throw new TypeError("Variant name must be a string.");
75+
}
76+
77+
// skip configuration_value validation as it accepts any type
78+
79+
// Although configuration_reference is not supported in the current implementation, we validate it here for future compatibility.
80+
if (variant.configuration_reference !== undefined && typeof variant.configuration_reference !== "string") {
81+
throw new TypeError("Variant configuration reference must be a string.");
82+
}
83+
84+
if (variant.status_override !== undefined && variant.status_override !== "None" && variant.status_override !== "Enabled" && variant.status_override !== "Disabled") {
85+
throw new TypeError("Variant status override must be 'None', 'Enabled', or 'Disabled'.");
86+
}
87+
}
88+
}
89+
90+
// #region Allocation
91+
function validateVariantAllocation(allocation: any) {
92+
if (typeof allocation !== "object") {
93+
throw new TypeError("Variant allocation must be an object.");
94+
}
95+
96+
if (allocation.default_when_disabled !== undefined && typeof allocation.default_when_disabled !== "string") {
97+
throw new TypeError("Variant allocation default_when_disabled must be a string.");
98+
}
99+
if (allocation.default_when_enabled !== undefined && typeof allocation.default_when_enabled !== "string") {
100+
throw new TypeError("Variant allocation default_when_enabled must be a string.");
101+
}
102+
if (allocation.user !== undefined) {
103+
validateUserVariantAllocation(allocation.user);
104+
}
105+
if (allocation.group !== undefined) {
106+
validateGroupVariantAllocation(allocation.group);
107+
}
108+
if (allocation.percentile !== undefined) {
109+
validatePercentileVariantAllocation(allocation.percentile);
110+
}
111+
if (allocation.seed !== undefined && typeof allocation.seed !== "string") {
112+
throw new TypeError("Variant allocation seed must be a string.");
113+
}
114+
}
115+
116+
function validateUserVariantAllocation(UserAllocations: any) {
117+
if (!Array.isArray(UserAllocations)) {
118+
throw new TypeError("User allocations must be an array.");
119+
}
120+
121+
for (const allocation of UserAllocations) {
122+
if (typeof allocation.variant !== "string") {
123+
throw new TypeError("User allocation variant must be a string.");
124+
}
125+
if (!Array.isArray(allocation.users)) {
126+
throw new TypeError("User allocation users must be an array.");
127+
}
128+
for (const user of allocation.users) {
129+
if (typeof user !== "string") {
130+
throw new TypeError("Elements in User allocation users must be strings.");
131+
}
132+
}
133+
}
134+
}
135+
136+
function validateGroupVariantAllocation(groupAllocations: any) {
137+
if (!Array.isArray(groupAllocations)) {
138+
throw new TypeError("Group allocations must be an array.");
139+
}
140+
141+
for (const allocation of groupAllocations) {
142+
if (typeof allocation.variant !== "string") {
143+
throw new TypeError("Group allocation variant must be a string.");
144+
}
145+
if (!Array.isArray(allocation.groups)) {
146+
throw new TypeError("Group allocation groups must be an array.");
147+
}
148+
for (const group of allocation.groups) {
149+
if (typeof group !== "string") {
150+
throw new TypeError("Elements in Group allocation groups must be strings.");
151+
}
152+
}
153+
}
154+
}
155+
156+
function validatePercentileVariantAllocation(percentileAllocations: any) {
157+
if (!Array.isArray(percentileAllocations)) {
158+
throw new TypeError("Percentile allocations must be an array.");
159+
}
160+
161+
for (const allocation of percentileAllocations) {
162+
if (typeof allocation.variant !== "string") {
163+
throw new TypeError("Percentile allocation variant must be a string.");
164+
}
165+
if (typeof allocation.from !== "number") {
166+
throw new TypeError("Percentile allocation from must be a number.");
167+
}
168+
if (typeof allocation.to !== "number") {
169+
throw new TypeError("Percentile allocation to must be a number.");
170+
}
171+
}
172+
}
173+
// #endregion
174+
175+
// #region Telemetry
176+
function validateTelemetryOptions(telemetry: any) {
177+
if (typeof telemetry !== "object") {
178+
throw new TypeError("Telemetry options must be an object.");
179+
}
180+
181+
if (telemetry.enabled !== undefined && typeof telemetry.enabled !== "boolean") {
182+
throw new TypeError("Telemetry enabled must be a boolean.");
183+
}
184+
if (telemetry.metadata !== undefined && typeof telemetry.metadata !== "object") {
185+
throw new TypeError("Telemetry metadata must be an object.");
186+
}
187+
// TODO: validate metadata keys (string) and values (string), not sure if we need to do this
188+
}
189+
// #endregion

test/noFilters.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ describe("feature flags with no filters", () => {
6161
return Promise.all([
6262
expect(featureManager.isEnabled("BooleanTrue")).eventually.eq(true),
6363
expect(featureManager.isEnabled("BooleanFalse")).eventually.eq(false),
64-
expect(featureManager.isEnabled("InvalidEnabled")).eventually.rejectedWith("Feature flag InvalidEnabled has an invalid 'enabled' value."),
64+
expect(featureManager.isEnabled("InvalidEnabled")).eventually.rejectedWith("Feature flag enabled must be a boolean."),
6565
expect(featureManager.isEnabled("Minimal")).eventually.eq(true),
6666
expect(featureManager.isEnabled("NoEnabled")).eventually.eq(false),
6767
expect(featureManager.isEnabled("EmptyConditions")).eventually.eq(true)

0 commit comments

Comments
 (0)