diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index b54c00cb..a9929af4 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -8,6 +8,7 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter"; import { KeyFilter } from "./KeyFilter"; import { LabelFilter } from "./LabelFilter"; import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter"; +import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter"; export class AzureAppConfigurationImpl extends Map implements AzureAppConfiguration { private adapters: IKeyValueAdapter[] = []; @@ -26,8 +27,9 @@ export class AzureAppConfigurationImpl extends Map implements A this.sortedTrimKeyPrefixes = [...options.trimKeyPrefixes].sort((a, b) => b.localeCompare(a)); } // TODO: should add more adapters to process different type of values - // feature flag, json, others + // feature flag, others this.adapters.push(new AzureKeyVaultKeyValueAdapter(options?.keyVaultOptions)); + this.adapters.push(new JsonKeyValueAdapter()); } public async load() { diff --git a/src/JsonKeyValueAdapter.ts b/src/JsonKeyValueAdapter.ts new file mode 100644 index 00000000..fe930c71 --- /dev/null +++ b/src/JsonKeyValueAdapter.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { ConfigurationSetting, secretReferenceContentType } from "@azure/app-configuration"; +import { IKeyValueAdapter } from "./IKeyValueAdapter"; + + +export class JsonKeyValueAdapter implements IKeyValueAdapter { + private static readonly ExcludedJsonContentTypes: string[] = [ + secretReferenceContentType + // TODO: exclude application/vnd.microsoft.appconfig.ff+json after feature management is supported + ]; + + public canProcess(setting: ConfigurationSetting): boolean { + if (!setting.contentType) { + return false; + } + if (JsonKeyValueAdapter.ExcludedJsonContentTypes.includes(setting.contentType)) { + return false; + } + return isJsonContentType(setting.contentType); + } + + public async processKeyValue(setting: ConfigurationSetting): Promise<[string, unknown]> { + if (!setting.value) { + throw new Error("Unexpected empty value for application/json content type."); + } + let parsedValue: any; + try { + parsedValue = JSON.parse(setting.value); + } catch (error) { + parsedValue = setting.value; + } + return [setting.key, parsedValue]; + } +} + +// Determine whether a content type string is a valid JSON content type. +// https://docs.microsoft.com/en-us/azure/azure-app-configuration/howto-leverage-json-content-type +function isJsonContentType(contentTypeValue: string): boolean { + if (!contentTypeValue) { + return false; + } + + let contentTypeNormalized: string = contentTypeValue.trim().toLowerCase(); + let mimeType: string = contentTypeNormalized.split(";", 1)[0].trim(); + let typeParts: string[] = mimeType.split("/"); + if (typeParts.length !== 2) { + return false; + } + + if (typeParts[0] !== "application") { + return false; + } + + return typeParts[1].split("+").includes("json"); +} \ No newline at end of file diff --git a/test/json.test.js b/test/json.test.js new file mode 100644 index 00000000..ea2a1bef --- /dev/null +++ b/test/json.test.js @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +const chai = require("chai"); +const chaiAsPromised = require("chai-as-promised"); +chai.use(chaiAsPromised); +const expect = chai.expect; +const { load } = require("../dist/index"); +const { + mockAppConfigurationClientListConfigurationSettings, + restoreMocks, + createMockedConnectionString, + createMockedKeyVaultReference +} = require("./utils/testHelper"); + +const jsonKeyValue = { + value: '{"Test":{"Level":"Debug"},"Prod":{"Level":"Warning"}}', + key: 'json.settings.logging', + label: null, + contentType: 'application/json', + lastModified: '2023-05-04T04:32:56.000Z', + tags: {}, + etag: 'GdmsLWq3mFjFodVEXUYRmvFr3l_qRiKAW_KdpFbxZKk', + isReadOnly: false +}; +const keyVaultKeyValue = createMockedKeyVaultReference("TestKey", "https://fake-vault-name.vault.azure.net/secrets/fakeSecretName"); + +describe("json", function () { + beforeEach(() => { + }); + + afterEach(() => { + restoreMocks(); + }) + + it("should load and parse if content type is application/json", async () => { + mockAppConfigurationClientListConfigurationSettings([jsonKeyValue]); + + const connectionString = createMockedConnectionString(); + const settings = await load(connectionString); + expect(settings).not.undefined; + const logging = settings.get("json.settings.logging"); + expect(logging).not.undefined; + expect(logging.Test).not.undefined; + expect(logging.Test.Level).eq("Debug"); + expect(logging.Prod).not.undefined; + expect(logging.Prod.Level).eq("Warning"); + }); + + it("should not parse key-vault reference", async () => { + mockAppConfigurationClientListConfigurationSettings([jsonKeyValue, keyVaultKeyValue]); + + const connectionString = createMockedConnectionString(); + const settings = await load(connectionString, { + keyVaultOptions: { + secretResolver: (url) => `Resolved: ${url.toString()}` + } + }); + expect(settings).not.undefined; + const resolvedSecret = settings.get("TestKey"); + expect(resolvedSecret).not.undefined; + expect(resolvedSecret.uri).undefined; + expect(typeof resolvedSecret).eq("string"); + }); +})