diff --git a/examples/refresh.mjs b/examples/refresh.mjs index 12231fcb..68e7aaab 100644 --- a/examples/refresh.mjs +++ b/examples/refresh.mjs @@ -24,6 +24,7 @@ const settings = await load(connectionString, { }], trimKeyPrefixes: ["app.settings."], refreshOptions: { + enabled: true, watchedSettings: [{ key: "app.settings.sentinel" }], refreshIntervalInMs: 10 * 1000 // Default value is 30 seconds, shorted for this sample } @@ -41,4 +42,4 @@ while (true) { // Waiting before the next refresh await sleepInMs(5000); -} \ No newline at end of file +} diff --git a/src/AzureAppConfiguration.ts b/src/AzureAppConfiguration.ts index 716a60f7..1f38d29b 100644 --- a/src/AzureAppConfiguration.ts +++ b/src/AzureAppConfiguration.ts @@ -12,7 +12,7 @@ export type AzureAppConfiguration = { /** * API to register callback listeners, which will be called only when a refresh operation successfully updates key-values. * - * @param listener - Callback funtion to be registered. + * @param listener - Callback function to be registered. * @param thisArg - Optional. Value to use as `this` when executing callback. */ onRefresh(listener: () => any, thisArg?: any): Disposable; diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 41e66a62..746fb959 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { AppConfigurationClient, ConfigurationSetting, ConfigurationSettingId, GetConfigurationSettingOptions, GetConfigurationSettingResponse, ListConfigurationSettingsOptions } from "@azure/app-configuration"; +import { AppConfigurationClient, ConfigurationSetting, ConfigurationSettingId, GetConfigurationSettingOptions, GetConfigurationSettingResponse, ListConfigurationSettingsOptions, isFeatureFlag } from "@azure/app-configuration"; import { RestError } from "@azure/core-rest-pipeline"; import { AzureAppConfiguration, ConfigurationObjectConstructionOptions } from "./AzureAppConfiguration"; import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions"; @@ -150,7 +150,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { const settings = this.#client.listConfigurationSettings(listOptions); for await (const setting of settings) { - loadedSettings.push(setting); + if (!isFeatureFlag(setting)) { // exclude feature flags + loadedSettings.push(setting); + } } } return loadedSettings; @@ -368,17 +370,17 @@ function getValidSelectors(selectors?: SettingSelector[]) { return [{ keyFilter: KeyFilter.Any, labelFilter: LabelFilter.Null }]; } - // below code dedupes selectors by keyFilter and labelFilter, the latter selector wins - const dedupedSelectors: SettingSelector[] = []; + // below code deduplicates selectors by keyFilter and labelFilter, the latter selector wins + const uniqueSelectors: SettingSelector[] = []; for (const selector of selectors) { - const existingSelectorIndex = dedupedSelectors.findIndex(s => s.keyFilter === selector.keyFilter && s.labelFilter === selector.labelFilter); + const existingSelectorIndex = uniqueSelectors.findIndex(s => s.keyFilter === selector.keyFilter && s.labelFilter === selector.labelFilter); if (existingSelectorIndex >= 0) { - dedupedSelectors.splice(existingSelectorIndex, 1); + uniqueSelectors.splice(existingSelectorIndex, 1); } - dedupedSelectors.push(selector); + uniqueSelectors.push(selector); } - return dedupedSelectors.map(selectorCandidate => { + return uniqueSelectors.map(selectorCandidate => { const selector = { ...selectorCandidate }; if (!selector.keyFilter) { throw new Error("Key filter cannot be null or empty."); diff --git a/src/JsonKeyValueAdapter.ts b/src/JsonKeyValueAdapter.ts index 4be758a1..e821af70 100644 --- a/src/JsonKeyValueAdapter.ts +++ b/src/JsonKeyValueAdapter.ts @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { ConfigurationSetting, secretReferenceContentType } from "@azure/app-configuration"; +import { ConfigurationSetting, featureFlagContentType, secretReferenceContentType } from "@azure/app-configuration"; import { IKeyValueAdapter } from "./IKeyValueAdapter"; export class JsonKeyValueAdapter implements IKeyValueAdapter { static readonly #ExcludedJsonContentTypes: string[] = [ - secretReferenceContentType - // TODO: exclude application/vnd.microsoft.appconfig.ff+json after feature management is supported + secretReferenceContentType, + featureFlagContentType ]; canProcess(setting: ConfigurationSetting): boolean { diff --git a/test/load.test.ts b/test/load.test.ts index 63f1cfd1..abe7cb3a 100644 --- a/test/load.test.ts +++ b/test/load.test.ts @@ -6,7 +6,7 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; import { load } from "./exportedApi"; -import { mockAppConfigurationClientListConfigurationSettings, restoreMocks, createMockedConnectionString, createMockedEnpoint, createMockedTokenCredential, createMockedKeyValue } from "./utils/testHelper"; +import { mockAppConfigurationClientListConfigurationSettings, restoreMocks, createMockedConnectionString, createMockedEndpoint, createMockedTokenCredential, createMockedKeyValue } from "./utils/testHelper"; const mockedKVs = [{ key: "app.settings.fontColor", @@ -62,6 +62,17 @@ const mockedKVs = [{ }, { key: "app5.settings", value: "placeholder" +}, { + key: ".appconfig.featureflag/Beta", + value: JSON.stringify({ + "id": "Beta", + "description": "", + "enabled": true, + "conditions": { + "client_filters": [] + } + }), + contentType: "application/vnd.microsoft.appconfig.ff+json;charset=utf-8" } ].map(createMockedKeyValue); @@ -84,7 +95,7 @@ describe("load", function () { }); it("should load data from config store with aad + endpoint URL", async () => { - const endpoint = createMockedEnpoint(); + const endpoint = createMockedEndpoint(); const credential = createMockedTokenCredential(); const settings = await load(new URL(endpoint), credential); expect(settings).not.undefined; @@ -93,7 +104,7 @@ describe("load", function () { }); it("should load data from config store with aad + endpoint string", async () => { - const endpoint = createMockedEnpoint(); + const endpoint = createMockedEndpoint(); const credential = createMockedTokenCredential(); const settings = await load(endpoint, credential); expect(settings).not.undefined; @@ -110,6 +121,13 @@ describe("load", function () { return expect(load("invalid-endpoint-url", credential)).eventually.rejectedWith("Invalid endpoint URL."); }); + it("should not include feature flags directly in the settings", async () => { + const connectionString = createMockedConnectionString(); + const settings = await load(connectionString); + expect(settings).not.undefined; + expect(settings.get(".appconfig.featureflag/Beta")).undefined; + }); + it("should filter by key and label, has(key) and get(key) should work", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { @@ -251,7 +269,7 @@ describe("load", function () { expect(settings.get("TestKey")).eq("TestValueForProd"); }); - it("should dedup exact same selectors but keeping the precedence", async () => { + it("should deduplicate exact same selectors but keeping the precedence", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { selectors: [{ @@ -317,7 +335,7 @@ describe("load", function () { * key: "app3.settings.fontColor" => value: "yellow" * * get() will return "placeholder" for "app3.settings" and "yellow" for "app3.settings.fontColor", as expected. - * data.app3.settings will return "placeholder" as a whole JSON object, which is not guarenteed to be correct. + * data.app3.settings will return "placeholder" as a whole JSON object, which is not guaranteed to be correct. */ it("Edge case 1: Hierarchical key-value pairs with overlapped key prefix.", async () => { const connectionString = createMockedConnectionString(); @@ -337,7 +355,7 @@ describe("load", function () { * key: "app5.settings.fontColor" => value: "yellow" * key: "app5.settings" => value: "placeholder" * - * When ocnstructConfigurationObject() is called, it first constructs from key "app5.settings.fontColor" and then from key "app5.settings". + * When constructConfigurationObject() is called, it first constructs from key "app5.settings.fontColor" and then from key "app5.settings". * An error will be thrown when constructing from key "app5.settings" because there is ambiguity between the two keys. */ it("Edge case 1: Hierarchical key-value pairs with overlapped key prefix.", async () => { diff --git a/test/utils/testHelper.ts b/test/utils/testHelper.ts index 82f9c888..d4297823 100644 --- a/test/utils/testHelper.ts +++ b/test/utils/testHelper.ts @@ -15,7 +15,7 @@ const TEST_TENANT_ID = "00000000-0000-0000-0000-000000000000"; const TEST_CLIENT_SECRET = "0000000000000000000000000000000000000000"; function mockAppConfigurationClientListConfigurationSettings(kvList: ConfigurationSetting[]) { - function* testKvSetGnerator(kvs: any[]) { + function* testKvSetGenerator(kvs: any[]) { yield* kvs; } sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings").callsFake((listOptions) => { @@ -36,7 +36,7 @@ function mockAppConfigurationClientListConfigurationSettings(kvList: Configurati } return keyMatched && labelMatched; }) - return testKvSetGnerator(kvs) as any; + return testKvSetGenerator(kvs) as any; }); } @@ -79,9 +79,9 @@ function restoreMocks() { sinon.restore(); } -const createMockedEnpoint = (name = "azure") => `https://${name}.azconfig.io`; +const createMockedEndpoint = (name = "azure") => `https://${name}.azconfig.io`; -const createMockedConnectionString = (endpoint = createMockedEnpoint(), secret = "secret", id = "b1d9b31") => { +const createMockedConnectionString = (endpoint = createMockedEndpoint(), secret = "secret", id = "b1d9b31") => { const toEncodeAsBytes = Buffer.from(secret); const returnValue = toEncodeAsBytes.toString("base64"); return `Endpoint=${endpoint};Id=${id};Secret=${returnValue}`; @@ -99,7 +99,7 @@ const createMockedKeyVaultReference = (key: string, vaultUri: string): Configura lastModified: new Date(), tags: { }, - etag: "SPJSMnJ2ph4BAjftWfdIctV2VIyQxtcIzRbh1oxTBkM", + etag: uuid.v4(), isReadOnly: false, }); @@ -109,7 +109,7 @@ const createMockedJsonKeyValue = (key: string, value: any): ConfigurationSetting contentType: "application/json", lastModified: new Date(), tags: {}, - etag: "GdmsLWq3mFjFodVEXUYRmvFr3l_qRiKAW_KdpFbxZKk", + etag: uuid.v4(), isReadOnly: false }); @@ -130,7 +130,7 @@ export { mockSecretClientGetSecret, restoreMocks, - createMockedEnpoint, + createMockedEndpoint, createMockedConnectionString, createMockedTokenCredential, createMockedKeyVaultReference,