From cbc60e1331db06e5f8e0fb72d4f62c0140bab0b0 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Wed, 6 Nov 2024 15:04:03 +0800 Subject: [PATCH 1/3] validate feature flag --- package-lock.json | 2 +- src/featureManager.ts | 18 +- src/featureProvider.ts | 21 ++- src/{ => schema}/model.ts | 362 +++++++++++++++++------------------- src/schema/validator.ts | 186 ++++++++++++++++++ test/featureManager.test.ts | 39 +++- test/noFilters.test.ts | 4 +- 7 files changed, 424 insertions(+), 208 deletions(-) rename src/{ => schema}/model.ts (91%) create mode 100644 src/schema/validator.ts diff --git a/package-lock.json b/package-lock.json index 0bc8262..73b3276 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "eslint": "^8.56.0", "mocha": "^10.2.0", "rimraf": "^5.0.5", - "rollup": "^4.9.4", + "rollup": "^4.22.4", "rollup-plugin-dts": "^6.1.0", "tslib": "^2.6.2", "typescript": "^5.3.3" diff --git a/src/featureManager.ts b/src/featureManager.ts index 402510f..d9b5d0d 100644 --- a/src/featureManager.ts +++ b/src/featureManager.ts @@ -3,7 +3,7 @@ import { TimeWindowFilter } from "./filter/TimeWindowFilter.js"; import { IFeatureFilter } from "./filter/FeatureFilter.js"; -import { RequirementType } from "./model.js"; +import { RequirementType } from "./schema/model.js"; import { IFeatureFlagProvider } from "./featureProvider.js"; import { TargetingFilter } from "./filter/TargetingFilter.js"; @@ -30,15 +30,12 @@ export class FeatureManager { // If multiple feature flags are found, the first one takes precedence. async isEnabled(featureName: string, context?: unknown): Promise { - const featureFlag = await this.#provider.getFeatureFlag(featureName); + const featureFlag = await this.#getFeatureFlag(featureName); if (featureFlag === undefined) { // If the feature is not found, then it is disabled. return false; } - // 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. - validateFeatureFlagFormat(featureFlag); - if (featureFlag.enabled !== true) { // If the feature is not explicitly enabled, then it is disabled by default. return false; @@ -75,14 +72,13 @@ export class FeatureManager { return !shortCircuitEvaluationResult; } + async #getFeatureFlag(featureName: string): Promise { + const featureFlag = await this.#provider.getFeatureFlag(featureName); + return featureFlag; + } + } interface FeatureManagerOptions { customFilters?: IFeatureFilter[]; } - -function validateFeatureFlagFormat(featureFlag: any): void { - if (featureFlag.enabled !== undefined && typeof featureFlag.enabled !== "boolean") { - throw new Error(`Feature flag ${featureFlag.id} has an invalid 'enabled' value.`); - } -} diff --git a/src/featureProvider.ts b/src/featureProvider.ts index c5a1aac..5d91144 100644 --- a/src/featureProvider.ts +++ b/src/featureProvider.ts @@ -2,7 +2,8 @@ // Licensed under the MIT license. import { IGettable } from "./gettable.js"; -import { FeatureFlag, FeatureManagementConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from "./model.js"; +import { FeatureFlag, FeatureManagementConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from "./schema/model.js"; +import { validateFeatureFlag } from "./schema/validator.js"; export interface IFeatureFlagProvider { /** @@ -28,12 +29,16 @@ export class ConfigurationMapFeatureFlagProvider implements IFeatureFlagProvider } async getFeatureFlag(featureName: string): Promise { const featureConfig = this.#configuration.get(FEATURE_MANAGEMENT_KEY); - return featureConfig?.[FEATURE_FLAGS_KEY]?.findLast((feature) => feature.id === featureName); + const featureFlag = featureConfig?.[FEATURE_FLAGS_KEY]?.findLast((feature) => feature.id === featureName); + validateFeatureFlag(featureFlag); + return featureFlag; } async getFeatureFlags(): Promise { const featureConfig = this.#configuration.get(FEATURE_MANAGEMENT_KEY); - return featureConfig?.[FEATURE_FLAGS_KEY] ?? []; + const featureFlag = featureConfig?.[FEATURE_FLAGS_KEY] ?? []; + validateFeatureFlag(featureFlag); + return featureFlag; } } @@ -49,10 +54,14 @@ export class ConfigurationObjectFeatureFlagProvider implements IFeatureFlagProvi async getFeatureFlag(featureName: string): Promise { const featureFlags = this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY]; - return featureFlags?.findLast((feature: FeatureFlag) => feature.id === featureName); + const featureFlag = featureFlags?.findLast((feature: FeatureFlag) => feature.id === featureName); + validateFeatureFlag(featureFlag); + return featureFlag; } async getFeatureFlags(): Promise { - return this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? []; + const featureFlag = this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? []; + validateFeatureFlag(featureFlag); + return featureFlag; } -} +} \ No newline at end of file diff --git a/src/model.ts b/src/schema/model.ts similarity index 91% rename from src/model.ts rename to src/schema/model.ts index 7ee9cc4..cf15e5e 100644 --- a/src/model.ts +++ b/src/schema/model.ts @@ -1,187 +1,175 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -// Converted from: -// https://github.com/Azure/AppConfiguration/blob/6e544296a5607f922a423df165f60801717c7800/docs/FeatureManagement/FeatureFlag.v2.0.0.schema.json - -/** - * A feature flag is a named property that can be toggled to enable or disable some feature of an application. - */ -export interface FeatureFlag { - /** - * An ID used to uniquely identify and reference the feature. - */ - id: string; - /** - * A description of the feature. - */ - description?: string; - /** - * A display name for the feature to use for display rather than the ID. - */ - display_name?: string; - /** - * A feature is OFF if enabled is false. If enabled is true, then the feature is ON if there are no conditions (null or empty) or if the conditions are satisfied. - */ - enabled?: boolean; - /** - * The declaration of conditions used to dynamically enable the feature. - */ - conditions?: FeatureEnablementConditions; - /** - * The list of variants defined for this feature. A variant represents a configuration value of a feature flag that can be a string, a number, a boolean, or a JSON object. - */ - variants?: Variant[]; - /** - * Determines how variants should be allocated for the feature to various users. - */ - allocation?: VariantAllocation; - /** - * The declaration of options used to configure telemetry for this feature. - */ - telemetry?: TelemetryOptions -} - -/** -* The declaration of conditions used to dynamically enable the feature -*/ -interface FeatureEnablementConditions { - /** - * Determines whether any or all registered client filters must be evaluated as true for the feature to be considered enabled. - */ - requirement_type?: RequirementType; - /** - * Filters that must run on the client and be evaluated as true for the feature to be considered enabled. - */ - client_filters?: ClientFilter[]; -} - -export type RequirementType = "Any" | "All"; - -interface ClientFilter { - /** - * The name used to refer to a client filter. - */ - name: string; - /** - * Parameters for a given client filter. A client filter can require any set of parameters of any type. - */ - parameters?: Record; -} - -interface Variant { - /** - * The name used to refer to a feature variant. - */ - name: string; - /** - * The configuration value for this feature variant. - */ - configuration_value?: unknown; - /** - * The path to a configuration section used as the configuration value for this feature variant. - */ - configuration_reference?: string; - /** - * Overrides the enabled state of the feature if the given variant is assigned. Does not override the state if value is None. - */ - status_override?: "None" | "Enabled" | "Disabled"; -} - -/** -* Determines how variants should be allocated for the feature to various users. -*/ -interface VariantAllocation { - /** - * Specifies which variant should be used when the feature is considered disabled. - */ - default_when_disabled?: string; - /** - * Specifies which variant should be used when the feature is considered enabled and no other allocation rules are applicable. - */ - default_when_enabled?: string; - /** - * A list of objects, each containing a variant name and list of users for whom that variant should be used. - */ - user?: UserAllocation[]; - /** - * A list of objects, each containing a variant name and list of groups for which that variant should be used. - */ - group?: GroupAllocation[]; - /** - * A list of objects, each containing a variant name and percentage range for which that variant should be used. - */ - percentile?: PercentileAllocation[] - /** - * The value percentile calculations are based on. The calculated percentile is consistent across features for a given user if the same nonempty seed is used. - */ - seed?: string; -} - -interface UserAllocation { - /** - * The name of the variant to use if the user allocation matches the current user. - */ - variant: string; - /** - * Collection of users where if any match the current user, the variant specified in the user allocation is used. - */ - users: string[]; -} - -interface GroupAllocation { - /** - * The name of the variant to use if the group allocation matches a group the current user is in. - */ - variant: string; - /** - * Collection of groups where if the current user is in any of these groups, the variant specified in the group allocation is used. - */ - groups: string[]; -} - -interface PercentileAllocation { - /** - * The name of the variant to use if the calculated percentile for the current user falls in the provided range. - */ - variant: string; - /** - * The lower end of the percentage range for which this variant will be used. - */ - from: number; - /** - * The upper end of the percentage range for which this variant will be used. - */ - to: number; -} - -/** -* The declaration of options used to configure telemetry for this feature. -*/ -interface TelemetryOptions { - /** - * Indicates if telemetry is enabled. - */ - enabled?: boolean; - /** - * A container for metadata that should be bundled with flag telemetry. - */ - metadata?: Record; -} - -// Feature Management Section fed into feature manager. -// Converted from https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureManagement.v1.0.0.schema.json - -export const FEATURE_MANAGEMENT_KEY = "feature_management"; -export const FEATURE_FLAGS_KEY = "feature_flags"; - -export interface FeatureManagementConfiguration { - feature_management: FeatureManagement -} - -/** - * Declares feature management configuration. - */ -export interface FeatureManagement { - feature_flags: FeatureFlag[]; -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Converted from: +// https://github.com/Azure/AppConfiguration/blob/6e544296a5607f922a423df165f60801717c7800/docs/FeatureManagement/FeatureFlag.v2.0.0.schema.json + +/** + * A feature flag is a named property that can be toggled to enable or disable some feature of an application. + */ +export interface FeatureFlag { + /** + * An ID used to uniquely identify and reference the feature. + */ + id: string; + /** + * A feature is OFF if enabled is false. If enabled is true, then the feature is ON if there are no conditions (null or empty) or if the conditions are satisfied. + */ + enabled?: boolean; + /** + * The declaration of conditions used to dynamically enable the feature. + */ + conditions?: FeatureEnablementConditions; + /** + * The list of variants defined for this feature. A variant represents a configuration value of a feature flag that can be a string, a number, a boolean, or a JSON object. + */ + variants?: Variant[]; + /** + * Determines how variants should be allocated for the feature to various users. + */ + allocation?: VariantAllocation; + /** + * The declaration of options used to configure telemetry for this feature. + */ + telemetry?: TelemetryOptions +} + +/** +* The declaration of conditions used to dynamically enable the feature +*/ +interface FeatureEnablementConditions { + /** + * Determines whether any or all registered client filters must be evaluated as true for the feature to be considered enabled. + */ + requirement_type?: RequirementType; + /** + * Filters that must run on the client and be evaluated as true for the feature to be considered enabled. + */ + client_filters?: ClientFilter[]; +} + +export type RequirementType = "Any" | "All"; + +interface ClientFilter { + /** + * The name used to refer to a client filter. + */ + name: string; + /** + * Parameters for a given client filter. A client filter can require any set of parameters of any type. + */ + parameters?: Record; +} + +interface Variant { + /** + * The name used to refer to a feature variant. + */ + name: string; + /** + * The configuration value for this feature variant. + */ + configuration_value?: unknown; + /** + * Overrides the enabled state of the feature if the given variant is assigned. Does not override the state if value is None. + */ + status_override?: "None" | "Enabled" | "Disabled"; +} + +/** +* Determines how variants should be allocated for the feature to various users. +*/ +interface VariantAllocation { + /** + * Specifies which variant should be used when the feature is considered disabled. + */ + default_when_disabled?: string; + /** + * Specifies which variant should be used when the feature is considered enabled and no other allocation rules are applicable. + */ + default_when_enabled?: string; + /** + * A list of objects, each containing a variant name and list of users for whom that variant should be used. + */ + user?: UserAllocation[]; + /** + * A list of objects, each containing a variant name and list of groups for which that variant should be used. + */ + group?: GroupAllocation[]; + /** + * A list of objects, each containing a variant name and percentage range for which that variant should be used. + */ + percentile?: PercentileAllocation[] + /** + * The value percentile calculations are based on. The calculated percentile is consistent across features for a given user if the same nonempty seed is used. + */ + seed?: string; +} + +interface UserAllocation { + /** + * The name of the variant to use if the user allocation matches the current user. + */ + variant: string; + /** + * Collection of users where if any match the current user, the variant specified in the user allocation is used. + */ + users: string[]; +} + +interface GroupAllocation { + /** + * The name of the variant to use if the group allocation matches a group the current user is in. + */ + variant: string; + /** + * Collection of groups where if the current user is in any of these groups, the variant specified in the group allocation is used. + */ + groups: string[]; +} + +interface PercentileAllocation { + /** + * The name of the variant to use if the calculated percentile for the current user falls in the provided range. + */ + variant: string; + /** + * The lower end of the percentage range for which this variant will be used. + */ + from: number; + /** + * The upper end of the percentage range for which this variant will be used. + */ + to: number; +} + +/** +* The declaration of options used to configure telemetry for this feature. +*/ +interface TelemetryOptions { + /** + * Indicates if telemetry is enabled. + */ + enabled?: boolean; + /** + * A container for metadata that should be bundled with flag telemetry. + */ + metadata?: Record; +} + +// Feature Management Section fed into feature manager. +// Converted from https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureManagement.v1.0.0.schema.json + +export const FEATURE_MANAGEMENT_KEY = "feature_management"; +export const FEATURE_FLAGS_KEY = "feature_flags"; + +export interface FeatureManagementConfiguration { + feature_management: FeatureManagement +} + +/** + * Declares feature management configuration. + */ +export interface FeatureManagement { + feature_flags: FeatureFlag[]; +} \ No newline at end of file diff --git a/src/schema/validator.ts b/src/schema/validator.ts new file mode 100644 index 0000000..0c90fde --- /dev/null +++ b/src/schema/validator.ts @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * Validates a feature flag object, checking if it conforms to the schema. + * @param featureFlag The feature flag object to validate. + */ +export function validateFeatureFlag(featureFlag: any): void { + if (featureFlag === undefined) { + return; // no-op if feature flag is undefined, indicating that the feature flag is not found + } + if (featureFlag === null || typeof featureFlag !== "object") { // Note: typeof null = "object" + throw new TypeError("Feature flag must be an object."); + } + if (typeof featureFlag.id !== "string") { + throw new TypeError("Feature flag 'id' must be a string."); + } + if (featureFlag.enabled !== undefined && typeof featureFlag.enabled !== "boolean") { + throw new TypeError("Feature flag 'enabled' must be a boolean."); + } + if (featureFlag.conditions !== undefined) { + validateFeatureEnablementConditions(featureFlag.conditions); + } + if (featureFlag.variants !== undefined) { + validateVariants(featureFlag.variants); + } + if (featureFlag.allocation !== undefined) { + validateVariantAllocation(featureFlag.allocation); + } + if (featureFlag.telemetry !== undefined) { + validateTelemetryOptions(featureFlag.telemetry); + } +} + +function validateFeatureEnablementConditions(conditions: any) { + if (typeof conditions !== "object") { + throw new TypeError("Feature flag 'conditions' must be an object."); + } + if (conditions.requirement_type !== undefined && conditions.requirement_type !== "Any" && conditions.requirement_type !== "All") { + throw new TypeError("'requirement_type' must be 'Any' or 'All'."); + } + if (conditions.client_filters !== undefined) { + validateClientFilters(conditions.client_filters); + } +} + +function validateClientFilters(client_filters: any) { + if (!Array.isArray(client_filters)) { + throw new TypeError("Feature flag conditions 'client_filters' must be an array."); + } + + for (const filter of client_filters) { + if (typeof filter.name !== "string") { + throw new TypeError("Client filter 'name' must be a string."); + } + if (filter.parameters !== undefined && typeof filter.parameters !== "object") { + throw new TypeError("Client filter 'parameters' must be an object."); + } + } +} + +function validateVariants(variants: any) { + if (!Array.isArray(variants)) { + throw new TypeError("Feature flag 'variants' must be an array."); + } + + for (const variant of variants) { + if (typeof variant.name !== "string") { + throw new TypeError("Variant 'name' must be a string."); + } + // skip configuration_value validation as it accepts any type + if (variant.status_override !== undefined && typeof variant.status_override !== "string") { + throw new TypeError("Variant 'status_override' must be a string."); + } + if (variant.status_override !== undefined && variant.status_override !== "None" && variant.status_override !== "Enabled" && variant.status_override !== "Disabled") { + throw new TypeError("Variant 'status_override' must be 'None', 'Enabled', or 'Disabled'."); + } + } +} + +function validateVariantAllocation(allocation: any) { + if (typeof allocation !== "object") { + throw new TypeError("Variant 'allocation' must be an object."); + } + + if (allocation.default_when_disabled !== undefined && typeof allocation.default_when_disabled !== "string") { + throw new TypeError("Variant allocation 'default_when_disabled' must be a string."); + } + if (allocation.default_when_enabled !== undefined && typeof allocation.default_when_enabled !== "string") { + throw new TypeError("Variant allocation 'default_when_enabled' must be a string."); + } + if (allocation.user !== undefined) { + validateUserVariantAllocation(allocation.user); + } + if (allocation.group !== undefined) { + validateGroupVariantAllocation(allocation.group); + } + if (allocation.percentile !== undefined) { + validatePercentileVariantAllocation(allocation.percentile); + } + if (allocation.seed !== undefined && typeof allocation.seed !== "string") { + throw new TypeError("Variant allocation 'seed' must be a string."); + } +} + +function validateUserVariantAllocation(UserAllocations: any) { + if (!Array.isArray(UserAllocations)) { + throw new TypeError("Variant 'user' allocation must be an array."); + } + + for (const allocation of UserAllocations) { + if (typeof allocation !== "object") { + throw new TypeError("Elements in variant 'user' allocation must be an object."); + } + if (typeof allocation.variant !== "string") { + throw new TypeError("User allocation 'variant' must be a string."); + } + if (!Array.isArray(allocation.users)) { + throw new TypeError("User allocation 'users' must be an array."); + } + for (const user of allocation.users) { + if (typeof user !== "string") { + throw new TypeError("Elements in user allocation 'users' must be strings."); + } + } + } +} + +function validateGroupVariantAllocation(groupAllocations: any) { + if (!Array.isArray(groupAllocations)) { + throw new TypeError("Variant 'group' allocation must be an array."); + } + + for (const allocation of groupAllocations) { + if (typeof allocation !== "object") { + throw new TypeError("Elements in variant 'group' allocation must be an object."); + } + if (typeof allocation.variant !== "string") { + throw new TypeError("Group allocation 'variant' must be a string."); + } + if (!Array.isArray(allocation.groups)) { + throw new TypeError("Group allocation 'groups' must be an array."); + } + for (const group of allocation.groups) { + if (typeof group !== "string") { + throw new TypeError("Elements in group allocation 'groups' must be strings."); + } + } + } +} + +function validatePercentileVariantAllocation(percentileAllocations: any) { + if (!Array.isArray(percentileAllocations)) { + throw new TypeError("Variant 'percentile' allocation must be an array."); + } + + for (const allocation of percentileAllocations) { + if (typeof allocation !== "object") { + throw new TypeError("Elements in variant 'percentile' allocation must be an object."); + } + if (typeof allocation.variant !== "string") { + throw new TypeError("Percentile allocation 'variant' must be a string."); + } + if (typeof allocation.from !== "number" || allocation.from < 0 || allocation.from > 100) { + throw new TypeError("Percentile allocation 'from' must be a number between 0 and 100."); + } + if (typeof allocation.to !== "number" || allocation.to < 0 || allocation.to > 100) { + throw new TypeError("Percentile allocation 'to' must be a number between 0 and 100."); + } + } +} +// #endregion + +// #region Telemetry +function validateTelemetryOptions(telemetry: any) { + if (typeof telemetry !== "object") { + throw new TypeError("Feature flag 'telemetry' must be an object."); + } + if (telemetry.enabled !== undefined && typeof telemetry.enabled !== "boolean") { + throw new TypeError("Telemetry 'enabled' must be a boolean."); + } + if (telemetry.metadata !== undefined && typeof telemetry.metadata !== "object") { + throw new TypeError("Telemetry 'metadata' must be an object."); + } +} +// #endregion \ No newline at end of file diff --git a/test/featureManager.test.ts b/test/featureManager.test.ts index ea9c787..7a73425 100644 --- a/test/featureManager.test.ts +++ b/test/featureManager.test.ts @@ -72,6 +72,43 @@ describe("feature manager", () => { ]); }); + it("should evaluate features with conditions", () => { + const dataSource = new Map(); + dataSource.set("feature_management", { + feature_flags: [ + { + "id": "Gamma", + "description": "", + "enabled": true, + "conditions": { + "requirement_type": "invalid type", + "client_filters": [ + { "name": "Microsoft.Targeting", "parameters": { "Audience": { "DefaultRolloutPercentage": 50 } } } + ] + } + }, + { + "id": "Delta", + "description": "", + "enabled": true, + "conditions": { + "requirement_type": "Any", + "client_filters": [ + { "name": "Microsoft.Targeting", "parameters": "invalid parameter" } + ] + } + } + ], + }); + + const provider = new ConfigurationMapFeatureFlagProvider(dataSource); + const featureManager = new FeatureManager(provider); + return Promise.all([ + expect(featureManager.isEnabled("Gamma")).eventually.rejectedWith("'requirement_type' must be 'Any' or 'All'."), + expect(featureManager.isEnabled("Delta")).eventually.rejectedWith("Client filter 'parameters' must be an object.") + ]); + }); + it("should let the last feature flag win", () => { const jsonObject = { "feature_management": { @@ -104,4 +141,4 @@ describe("feature manager", () => { it("should evaluate features with conditions"); it("should override default filters with custom filters"); -}); +}); \ No newline at end of file diff --git a/test/noFilters.test.ts b/test/noFilters.test.ts index 55639f7..b40c432 100644 --- a/test/noFilters.test.ts +++ b/test/noFilters.test.ts @@ -61,10 +61,10 @@ describe("feature flags with no filters", () => { return Promise.all([ expect(featureManager.isEnabled("BooleanTrue")).eventually.eq(true), expect(featureManager.isEnabled("BooleanFalse")).eventually.eq(false), - expect(featureManager.isEnabled("InvalidEnabled")).eventually.rejectedWith("Feature flag InvalidEnabled has an invalid 'enabled' value."), + expect(featureManager.isEnabled("InvalidEnabled")).eventually.rejectedWith("Feature flag 'enabled' must be a boolean."), expect(featureManager.isEnabled("Minimal")).eventually.eq(true), expect(featureManager.isEnabled("NoEnabled")).eventually.eq(false), expect(featureManager.isEnabled("EmptyConditions")).eventually.eq(true) ]); }); -}); +}); \ No newline at end of file From 5c15b08d6626078b5cc9749df75eeeb5bb9ac2f3 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Wed, 6 Nov 2024 15:08:03 +0800 Subject: [PATCH 2/3] update --- package-lock.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 73b3276..b936b24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2225,12 +2225,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { From c122688af0de5c277eb34a423136eb9cbd7de7d8 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Wed, 6 Nov 2024 15:12:47 +0800 Subject: [PATCH 3/3] fix lint --- src/featureProvider.ts | 2 +- src/schema/model.ts | 2 +- src/schema/validator.ts | 2 +- test/featureManager.test.ts | 2 +- test/noFilters.test.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/featureProvider.ts b/src/featureProvider.ts index 5d91144..29f0802 100644 --- a/src/featureProvider.ts +++ b/src/featureProvider.ts @@ -64,4 +64,4 @@ export class ConfigurationObjectFeatureFlagProvider implements IFeatureFlagProvi validateFeatureFlag(featureFlag); return featureFlag; } -} \ No newline at end of file +} diff --git a/src/schema/model.ts b/src/schema/model.ts index cf15e5e..f9b3e1e 100644 --- a/src/schema/model.ts +++ b/src/schema/model.ts @@ -172,4 +172,4 @@ export interface FeatureManagementConfiguration { */ export interface FeatureManagement { feature_flags: FeatureFlag[]; -} \ No newline at end of file +} diff --git a/src/schema/validator.ts b/src/schema/validator.ts index 0c90fde..97bf5ed 100644 --- a/src/schema/validator.ts +++ b/src/schema/validator.ts @@ -183,4 +183,4 @@ function validateTelemetryOptions(telemetry: any) { throw new TypeError("Telemetry 'metadata' must be an object."); } } -// #endregion \ No newline at end of file +// #endregion diff --git a/test/featureManager.test.ts b/test/featureManager.test.ts index 7a73425..0bfa331 100644 --- a/test/featureManager.test.ts +++ b/test/featureManager.test.ts @@ -141,4 +141,4 @@ describe("feature manager", () => { it("should evaluate features with conditions"); it("should override default filters with custom filters"); -}); \ No newline at end of file +}); diff --git a/test/noFilters.test.ts b/test/noFilters.test.ts index b40c432..aaac209 100644 --- a/test/noFilters.test.ts +++ b/test/noFilters.test.ts @@ -67,4 +67,4 @@ describe("feature flags with no filters", () => { expect(featureManager.isEnabled("EmptyConditions")).eventually.eq(true) ]); }); -}); \ No newline at end of file +});