From 68e23064e70363614c6f8d8bab9619a67fc32646 Mon Sep 17 00:00:00 2001 From: adams85 <31276480+adams85@users.noreply.github.com> Date: Mon, 15 Jan 2024 16:28:40 +0100 Subject: [PATCH] Add option for detecting changes to flag overrides (#101) * Add the ability to detect changes to the flag override map * Move createFlagOverridesFromMap to common-js to reduce redundancy * Bump version --- package-lock.json | 4 +- package.json | 2 +- src/FlagOverrides.ts | 25 +++++-- src/index.ts | 25 +++++-- test/ConfigV2EvaluationTests.ts | 3 +- test/OverrideTests.ts | 116 ++++++++++++++++++++++++++++---- 6 files changed, 146 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 496bf87..5af3137 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "configcat-common", - "version": "9.1.0", + "version": "9.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "configcat-common", - "version": "9.1.0", + "version": "9.2.0", "license": "MIT", "dependencies": { "tslib": "^2.4.1" diff --git a/package.json b/package.json index cf5739a..a16d27c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "configcat-common", - "version": "9.1.0", + "version": "9.2.0", "description": "ConfigCat is a configuration as a service that lets you manage your features and configurations without actually deploying new code.", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/src/FlagOverrides.ts b/src/FlagOverrides.ts index 1b7a24d..8202cbf 100644 --- a/src/FlagOverrides.ts +++ b/src/FlagOverrides.ts @@ -31,20 +31,31 @@ export interface IOverrideDataSource { } export class MapOverrideDataSource implements IOverrideDataSource { - private readonly map: { [name: string]: Setting } = {}; + private static getCurrentSettings(map: { [name: string]: NonNullable }) { + return Object.fromEntries(Object.entries(map) + .map(([key, value]) => [key, Setting.fromValue(value)])); + } + + private readonly initialSettings: { [name: string]: Setting }; + private readonly map?: { [name: string]: NonNullable }; + + private readonly ["constructor"]!: typeof MapOverrideDataSource; - constructor(map: { [name: string]: NonNullable }) { - this.map = Object.fromEntries(Object.entries(map).map(([key, value]) => { - return [key, Setting.fromValue(value)]; - })); + constructor(map: { [name: string]: NonNullable }, watchChanges?: boolean) { + this.initialSettings = this.constructor.getCurrentSettings(map); + if (watchChanges) { + this.map = map; + } } getOverrides(): Promise<{ [name: string]: Setting }> { - return Promise.resolve(this.map); + return Promise.resolve(this.getOverridesSync()); } getOverridesSync(): { [name: string]: Setting } { - return this.map; + return this.map + ? this.constructor.getCurrentSettings(this.map) + : this.initialSettings; } } diff --git a/src/index.ts b/src/index.ts index cd4292f..793e37c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,9 @@ import type { IAutoPollOptions, ILazyLoadingOptions, IManualPollOptions, Options import { PollingMode } from "./ConfigCatClientOptions"; import type { IConfigCatLogger } from "./ConfigCatLogger"; import { ConfigCatConsoleLogger, LogLevel } from "./ConfigCatLogger"; +import { FlagOverrides, MapOverrideDataSource, OverrideBehaviour } from "./FlagOverrides"; import { setupPolyfills } from "./Polyfills"; +import type { SettingValue } from "./ProjectConfig"; setupPolyfills(); @@ -37,6 +39,19 @@ export function createConsoleLogger(logLevel: LogLevel): IConfigCatLogger { return new ConfigCatConsoleLogger(logLevel); } +/** + * Creates an instance of `FlagOverrides` that uses a map data source. + * @param map The map that contains the overrides. + * @param behaviour The override behaviour. + * Specifies whether the local values should override the remote values + * or local values should only be used when a remote value doesn't exist + * or the local values should be used only. + * @param watchChanges If set to `true`, the input map will be tracked for changes. + */ +export function createFlagOverridesFromMap(map: { [name: string]: NonNullable }, behaviour: OverrideBehaviour, watchChanges?: boolean): FlagOverrides { + return new FlagOverrides(new MapOverrideDataSource(map, watchChanges), behaviour); +} + /* Public types for platform-specific SDKs */ // List types here which are required to implement the platform-specific SDKs but shouldn't be exposed to end users. @@ -51,14 +66,10 @@ export type { OptionsBase } from "./ConfigCatClientOptions"; export type { IConfigCache } from "./ConfigCatCache"; -export { ExternalConfigCache } from "./ConfigCatCache"; +export { InMemoryConfigCache, ExternalConfigCache } from "./ConfigCatCache"; export type { IEventProvider, IEventEmitter } from "./EventEmitter"; -export type { IOverrideDataSource } from "./FlagOverrides"; - -export { FlagOverrides, MapOverrideDataSource } from "./FlagOverrides"; - /* Public types for end users */ // List types here which are part of the public API of platform-specific SDKs, thus, should be exposed to end users. @@ -101,7 +112,9 @@ export type { UserAttributeValue } from "./User"; export { User } from "./User"; -export { OverrideBehaviour } from "./FlagOverrides"; +export type { FlagOverrides }; + +export { OverrideBehaviour }; export { ClientCacheState, RefreshResult } from "./ConfigServiceBase"; diff --git a/test/ConfigV2EvaluationTests.ts b/test/ConfigV2EvaluationTests.ts index 331e6bf..063e635 100644 --- a/test/ConfigV2EvaluationTests.ts +++ b/test/ConfigV2EvaluationTests.ts @@ -1,7 +1,8 @@ import { assert } from "chai"; import "mocha"; -import { FlagOverrides, IManualPollOptions, MapOverrideDataSource, OverrideBehaviour, SettingValue, User, UserAttributeValue } from "../src"; +import { IManualPollOptions, OverrideBehaviour, SettingValue, User, UserAttributeValue } from "../src"; import { LogLevel, LoggerWrapper } from "../src/ConfigCatLogger"; +import { FlagOverrides, MapOverrideDataSource } from "../src/FlagOverrides"; import { RolloutEvaluator, evaluate, isAllowedValue } from "../src/RolloutEvaluator"; import { errorToString } from "../src/Utils"; import { CdnConfigLocation, LocalFileConfigLocation } from "./helpers/ConfigLocation"; diff --git a/test/OverrideTests.ts b/test/OverrideTests.ts index 386929b..b637ec1 100644 --- a/test/OverrideTests.ts +++ b/test/OverrideTests.ts @@ -15,25 +15,117 @@ describe("Local Overrides", () => { sdkType: "common", sdkVersion: "1.0.0" }; + + const overrideMap = { + enabledFeature: true, + disabledFeature: false, + intSetting: 5, + doubleSetting: 3.14, + stringSetting: "test" + }; + const options: AutoPollOptions = new AutoPollOptions("localhost", "common", "1.0.0", { flagOverrides: { - dataSource: new MapOverrideDataSource({ - enabledFeature: true, - disabledFeature: false, - intSetting: 5, - doubleSetting: 3.14, - stringSetting: "test" - }), + dataSource: new MapOverrideDataSource(overrideMap), behaviour: OverrideBehaviour.LocalOnly } }, null); const client: IConfigCatClient = new ConfigCatClient(options, configCatKernel); - assert.equal(await client.getValueAsync("enabledFeature", false), true); - assert.equal(await client.getValueAsync("disabledFeature", true), false); - assert.equal(await client.getValueAsync("intSetting", 0), 5); - assert.equal(await client.getValueAsync("doubleSetting", 0), 3.14); - assert.equal(await client.getValueAsync("stringSetting", ""), "test"); + assert.equal(await client.getValueAsync("enabledFeature", null), true); + assert.equal(await client.getValueAsync("disabledFeature", null), false); + assert.equal(await client.getValueAsync("intSetting", null), 5); + assert.equal(await client.getValueAsync("doubleSetting", null), 3.14); + assert.equal(await client.getValueAsync("stringSetting", null), "test"); + + overrideMap.disabledFeature = true; + overrideMap.intSetting = -5; + + assert.equal(await client.getValueAsync("enabledFeature", null), true); + assert.equal(await client.getValueAsync("disabledFeature", null), false); + assert.equal(await client.getValueAsync("intSetting", null), 5); + assert.equal(await client.getValueAsync("doubleSetting", null), 3.14); + assert.equal(await client.getValueAsync("stringSetting", null), "test"); + }); + + it("Values from map - LocalOnly - watch changes - async", async () => { + const configCatKernel: FakeConfigCatKernel = { + configFetcher: new FakeConfigFetcherBase("{\"f\": { \"fakeKey\": { \"v\": false, \"p\": [], \"r\": [] } } }"), + sdkType: "common", + sdkVersion: "1.0.0" + }; + + const overrideMap = { + enabledFeature: true, + disabledFeature: false, + intSetting: 5, + doubleSetting: 3.14, + stringSetting: "test" + }; + + const options: AutoPollOptions = new AutoPollOptions("localhost", "common", "1.0.0", { + flagOverrides: { + dataSource: new MapOverrideDataSource(overrideMap, true), + behaviour: OverrideBehaviour.LocalOnly + } + }, null); + const client: IConfigCatClient = new ConfigCatClient(options, configCatKernel); + + assert.equal(await client.getValueAsync("enabledFeature", null), true); + assert.equal(await client.getValueAsync("disabledFeature", null), false); + assert.equal(await client.getValueAsync("intSetting", null), 5); + assert.equal(await client.getValueAsync("doubleSetting", null), 3.14); + assert.equal(await client.getValueAsync("stringSetting", null), "test"); + + overrideMap.disabledFeature = true; + overrideMap.intSetting = -5; + + assert.equal(await client.getValueAsync("enabledFeature", null), true); + assert.equal(await client.getValueAsync("disabledFeature", null), true); + assert.equal(await client.getValueAsync("intSetting", null), -5); + assert.equal(await client.getValueAsync("doubleSetting", null), 3.14); + assert.equal(await client.getValueAsync("stringSetting", null), "test"); + }); + + it("Values from map - LocalOnly - watch changes - sync", async () => { + const configCatKernel: FakeConfigCatKernel = { + configFetcher: new FakeConfigFetcherBase("{\"f\": { \"fakeKey\": { \"v\": false, \"p\": [], \"r\": [] } } }"), + sdkType: "common", + sdkVersion: "1.0.0" + }; + + const overrideMap = { + enabledFeature: true, + disabledFeature: false, + intSetting: 5, + doubleSetting: 3.14, + stringSetting: "test" + }; + + const options: AutoPollOptions = new AutoPollOptions("localhost", "common", "1.0.0", { + flagOverrides: { + dataSource: new MapOverrideDataSource(overrideMap, true), + behaviour: OverrideBehaviour.LocalOnly + } + }, null); + const client: IConfigCatClient = new ConfigCatClient(options, configCatKernel); + + let snapshot = client.snapshot(); + assert.equal(await snapshot.getValue("enabledFeature", null), true); + assert.equal(await snapshot.getValue("disabledFeature", null), false); + assert.equal(await snapshot.getValue("intSetting", null), 5); + assert.equal(await snapshot.getValue("doubleSetting", null), 3.14); + assert.equal(await snapshot.getValue("stringSetting", null), "test"); + + overrideMap.disabledFeature = true; + overrideMap.intSetting = -5; + + snapshot = client.snapshot(); + assert.equal(await snapshot.getValue("enabledFeature", null), true); + assert.equal(await snapshot.getValue("disabledFeature", null), true); + assert.equal(await snapshot.getValue("intSetting", null), -5); + assert.equal(await snapshot.getValue("doubleSetting", null), 3.14); + assert.equal(await snapshot.getValue("stringSetting", null), "test"); }); it("Values from map - LocalOverRemote", async () => {