Skip to content

Commit 74cb29f

Browse files
authored
Auto parse JSON string for compatible content type (#8)
1 parent c1b8595 commit 74cb29f

File tree

3 files changed

+125
-1
lines changed

3 files changed

+125
-1
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter";
88
import { KeyFilter } from "./KeyFilter";
99
import { LabelFilter } from "./LabelFilter";
1010
import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter";
11+
import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter";
1112

1213
export class AzureAppConfigurationImpl extends Map<string, unknown> implements AzureAppConfiguration {
1314
private adapters: IKeyValueAdapter[] = [];
@@ -26,8 +27,9 @@ export class AzureAppConfigurationImpl extends Map<string, unknown> implements A
2627
this.sortedTrimKeyPrefixes = [...options.trimKeyPrefixes].sort((a, b) => b.localeCompare(a));
2728
}
2829
// TODO: should add more adapters to process different type of values
29-
// feature flag, json, others
30+
// feature flag, others
3031
this.adapters.push(new AzureKeyVaultKeyValueAdapter(options?.keyVaultOptions));
32+
this.adapters.push(new JsonKeyValueAdapter());
3133
}
3234

3335
public async load() {

src/JsonKeyValueAdapter.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { ConfigurationSetting, secretReferenceContentType } from "@azure/app-configuration";
5+
import { IKeyValueAdapter } from "./IKeyValueAdapter";
6+
7+
8+
export class JsonKeyValueAdapter implements IKeyValueAdapter {
9+
private static readonly ExcludedJsonContentTypes: string[] = [
10+
secretReferenceContentType
11+
// TODO: exclude application/vnd.microsoft.appconfig.ff+json after feature management is supported
12+
];
13+
14+
public canProcess(setting: ConfigurationSetting): boolean {
15+
if (!setting.contentType) {
16+
return false;
17+
}
18+
if (JsonKeyValueAdapter.ExcludedJsonContentTypes.includes(setting.contentType)) {
19+
return false;
20+
}
21+
return isJsonContentType(setting.contentType);
22+
}
23+
24+
public async processKeyValue(setting: ConfigurationSetting): Promise<[string, unknown]> {
25+
if (!setting.value) {
26+
throw new Error("Unexpected empty value for application/json content type.");
27+
}
28+
let parsedValue: any;
29+
try {
30+
parsedValue = JSON.parse(setting.value);
31+
} catch (error) {
32+
parsedValue = setting.value;
33+
}
34+
return [setting.key, parsedValue];
35+
}
36+
}
37+
38+
// Determine whether a content type string is a valid JSON content type.
39+
// https://docs.microsoft.com/en-us/azure/azure-app-configuration/howto-leverage-json-content-type
40+
function isJsonContentType(contentTypeValue: string): boolean {
41+
if (!contentTypeValue) {
42+
return false;
43+
}
44+
45+
let contentTypeNormalized: string = contentTypeValue.trim().toLowerCase();
46+
let mimeType: string = contentTypeNormalized.split(";", 1)[0].trim();
47+
let typeParts: string[] = mimeType.split("/");
48+
if (typeParts.length !== 2) {
49+
return false;
50+
}
51+
52+
if (typeParts[0] !== "application") {
53+
return false;
54+
}
55+
56+
return typeParts[1].split("+").includes("json");
57+
}

test/json.test.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
const chai = require("chai");
5+
const chaiAsPromised = require("chai-as-promised");
6+
chai.use(chaiAsPromised);
7+
const expect = chai.expect;
8+
const { load } = require("../dist/index");
9+
const {
10+
mockAppConfigurationClientListConfigurationSettings,
11+
restoreMocks,
12+
createMockedConnectionString,
13+
createMockedKeyVaultReference
14+
} = require("./utils/testHelper");
15+
16+
const jsonKeyValue = {
17+
value: '{"Test":{"Level":"Debug"},"Prod":{"Level":"Warning"}}',
18+
key: 'json.settings.logging',
19+
label: null,
20+
contentType: 'application/json',
21+
lastModified: '2023-05-04T04:32:56.000Z',
22+
tags: {},
23+
etag: 'GdmsLWq3mFjFodVEXUYRmvFr3l_qRiKAW_KdpFbxZKk',
24+
isReadOnly: false
25+
};
26+
const keyVaultKeyValue = createMockedKeyVaultReference("TestKey", "https://fake-vault-name.vault.azure.net/secrets/fakeSecretName");
27+
28+
describe("json", function () {
29+
beforeEach(() => {
30+
});
31+
32+
afterEach(() => {
33+
restoreMocks();
34+
})
35+
36+
it("should load and parse if content type is application/json", async () => {
37+
mockAppConfigurationClientListConfigurationSettings([jsonKeyValue]);
38+
39+
const connectionString = createMockedConnectionString();
40+
const settings = await load(connectionString);
41+
expect(settings).not.undefined;
42+
const logging = settings.get("json.settings.logging");
43+
expect(logging).not.undefined;
44+
expect(logging.Test).not.undefined;
45+
expect(logging.Test.Level).eq("Debug");
46+
expect(logging.Prod).not.undefined;
47+
expect(logging.Prod.Level).eq("Warning");
48+
});
49+
50+
it("should not parse key-vault reference", async () => {
51+
mockAppConfigurationClientListConfigurationSettings([jsonKeyValue, keyVaultKeyValue]);
52+
53+
const connectionString = createMockedConnectionString();
54+
const settings = await load(connectionString, {
55+
keyVaultOptions: {
56+
secretResolver: (url) => `Resolved: ${url.toString()}`
57+
}
58+
});
59+
expect(settings).not.undefined;
60+
const resolvedSecret = settings.get("TestKey");
61+
expect(resolvedSecret).not.undefined;
62+
expect(resolvedSecret.uri).undefined;
63+
expect(typeof resolvedSecret).eq("string");
64+
});
65+
})

0 commit comments

Comments
 (0)