Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/AzureAppConfigurationImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> implements AzureAppConfiguration {
private adapters: IKeyValueAdapter[] = [];
Expand All @@ -26,8 +27,9 @@ export class AzureAppConfigurationImpl extends Map<string, unknown> 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() {
Expand Down
57 changes: 57 additions & 0 deletions src/JsonKeyValueAdapter.ts
Original file line number Diff line number Diff line change
@@ -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");
}
65 changes: 65 additions & 0 deletions test/json.test.js
Original file line number Diff line number Diff line change
@@ -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");
});
})