Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
1f41e0d
support snapshot
zhiyuanliang-ms Dec 19, 2024
a331e98
add testcase
zhiyuanliang-ms Dec 20, 2024
1105531
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Dec 20, 2024
7d98b58
add testcase
zhiyuanliang-ms Dec 20, 2024
9bffa88
fix lint
zhiyuanliang-ms Dec 20, 2024
28c2d0e
update
zhiyuanliang-ms Dec 24, 2024
92d6531
Merge pull request #162 from Azure/merge-main-to-preview
zhiyuanliang-ms Feb 12, 2025
501cfe4
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Feb 12, 2025
2a09f46
Merge branch 'zhiyuanliang/select-snapshot' of https://github.com/Azu…
zhiyuanliang-ms Feb 12, 2025
fed19f8
Merge branch 'main' into zhiyuanliang/select-snapshot
zhiyuanliang-ms Feb 20, 2025
071215e
Merge branch 'main' into zhiyuanliang/select-snapshot
zhiyuanliang-ms Apr 2, 2025
160f30a
Merge branch 'main' into zhiyuanliang/select-snapshot
zhiyuanliang-ms Apr 23, 2025
53544af
wip
zhiyuanliang-ms Apr 23, 2025
64f7604
wip
zhiyuanliang-ms Apr 23, 2025
5fc57b0
support tag filter
zhiyuanliang-ms Apr 23, 2025
1f5d9d8
add test
zhiyuanliang-ms Apr 23, 2025
994c10e
update test
zhiyuanliang-ms Apr 23, 2025
5b89cb6
Merge branch 'zhiyuanliang/select-snapshot' of https://github.com/Azu…
zhiyuanliang-ms Apr 23, 2025
bda172e
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Apr 29, 2025
cad828a
update testcase
zhiyuanliang-ms Apr 29, 2025
00421f9
Merge branch 'zhiyuanliang/select-snapshot' of https://github.com/Azu…
zhiyuanliang-ms Apr 29, 2025
667f721
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms May 13, 2025
11b5325
Merge branch 'zhiyuanliang/select-snapshot' of https://github.com/Azu…
zhiyuanliang-ms May 13, 2025
ca3c113
update
zhiyuanliang-ms May 19, 2025
855e909
add more testcases
zhiyuanliang-ms May 19, 2025
030d72d
update
zhiyuanliang-ms May 19, 2025
381d833
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms May 19, 2025
c0d611c
Merge branch 'zhiyuanliang/select-snapshot' of https://github.com/Azu…
zhiyuanliang-ms May 19, 2025
b985509
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms May 22, 2025
7a246d9
fix lint
zhiyuanliang-ms May 22, 2025
58ff6c3
update testcase
zhiyuanliang-ms May 22, 2025
1809619
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Aug 1, 2025
fe98b43
update
zhiyuanliang-ms Aug 1, 2025
b373d1e
add more testcases
zhiyuanliang-ms Aug 4, 2025
4bfc623
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Aug 4, 2025
505c542
correct null tag test
zhiyuanliang-ms Aug 5, 2025
1433789
Merge branch 'main' into zhiyuanliang/tag-filter
zhiyuanliang-ms Aug 7, 2025
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
32 changes: 27 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"uuid": "^9.0.1"
},
"dependencies": {
"@azure/app-configuration": "^1.6.1",
"@azure/app-configuration": "^1.8.0",
"@azure/identity": "^4.2.1",
"@azure/keyvault-secrets": "^4.7.0"
}
Expand Down
178 changes: 146 additions & 32 deletions src/AzureAppConfigurationImpl.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { AppConfigurationClient, ConfigurationSetting, ConfigurationSettingId, GetConfigurationSettingOptions, GetConfigurationSettingResponse, ListConfigurationSettingsOptions, featureFlagPrefix, isFeatureFlag } from "@azure/app-configuration";
import {
AppConfigurationClient,
ConfigurationSetting,
ConfigurationSettingId,
GetConfigurationSettingOptions,
GetConfigurationSettingResponse,
ListConfigurationSettingsOptions,
featureFlagPrefix,
isFeatureFlag,
GetSnapshotOptions,
GetSnapshotResponse,
KnownSnapshotComposition
} from "@azure/app-configuration";
import { isRestError } from "@azure/core-rest-pipeline";
import { AzureAppConfiguration, ConfigurationObjectConstructionOptions } from "./AzureAppConfiguration.js";
import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js";
Expand Down Expand Up @@ -29,7 +41,14 @@ import { FM_PACKAGE_NAME, AI_MIME_PROFILE, AI_CHAT_COMPLETION_MIME_PROFILE } fro
import { parseContentType, isJsonContentType, isFeatureFlagContentType, isSecretReferenceContentType } from "./common/contentType.js";
import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js";
import { RefreshTimer } from "./refresh/RefreshTimer.js";
import { RequestTracingOptions, getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js";
import {
RequestTracingOptions,
getConfigurationSettingWithTrace,
listConfigurationSettingsWithTrace,
getSnapshotWithTrace,
listConfigurationSettingsForSnapshotWithTrace,
requestTracingEnabled
} from "./requestTracing/utils.js";
import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions.js";
import { AIConfigurationTracingOptions } from "./requestTracing/AIConfigurationTracingOptions.js";
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
Expand All @@ -39,6 +58,8 @@ import { InvalidOperationError, ArgumentError, isFailoverableError, isInputError

const MIN_DELAY_FOR_UNHANDLED_FAILURE = 5_000; // 5 seconds

const MAX_TAG_FILTERS = 5;

type PagedSettingSelector = SettingSelector & {
/**
* Key: page eTag, Value: feature flag configurations
Expand Down Expand Up @@ -446,26 +467,50 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
);

for (const selector of selectorsToUpdate) {
const listOptions: ListConfigurationSettingsOptions = {
keyFilter: selector.keyFilter,
labelFilter: selector.labelFilter
};

const pageEtags: string[] = [];
const pageIterator = listConfigurationSettingsWithTrace(
this.#requestTraceOptions,
client,
listOptions
).byPage();
for await (const page of pageIterator) {
pageEtags.push(page.etag ?? "");
for (const setting of page.items) {
if (loadFeatureFlag === isFeatureFlag(setting)) {
loadedSettings.push(setting);
if (selector.snapshotName === undefined) {
const listOptions: ListConfigurationSettingsOptions = {
keyFilter: selector.keyFilter,
labelFilter: selector.labelFilter,
tagsFilter: selector.tagFilters
};
const pageEtags: string[] = [];
const pageIterator = listConfigurationSettingsWithTrace(
this.#requestTraceOptions,
client,
listOptions
).byPage();

for await (const page of pageIterator) {
pageEtags.push(page.etag ?? "");
for (const setting of page.items) {
if (loadFeatureFlag === isFeatureFlag(setting)) {
loadedSettings.push(setting);
}
}
}
selector.pageEtags = pageEtags;
} else { // snapshot selector
const snapshot = await this.#getSnapshot(selector.snapshotName);
if (snapshot === undefined) {
throw new Error(`Could not find snapshot with name ${selector.snapshotName}.`);
}
if (snapshot.compositionType != KnownSnapshotComposition.Key) {
throw new Error(`Composition type for the selected snapshot with name ${selector.snapshotName} must be 'key'.`);
}
const pageIterator = listConfigurationSettingsForSnapshotWithTrace(
this.#requestTraceOptions,
client,
selector.snapshotName
).byPage();

for await (const page of pageIterator) {
for (const setting of page.items) {
if (loadFeatureFlag === isFeatureFlag(setting)) {
loadedSettings.push(setting);
}
}
}
}
selector.pageEtags = pageEtags;
}

if (loadFeatureFlag) {
Expand Down Expand Up @@ -623,9 +668,13 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
async #checkConfigurationSettingsChange(selectors: PagedSettingSelector[]): Promise<boolean> {
const funcToExecute = async (client) => {
for (const selector of selectors) {
if (selector.snapshotName) { // skip snapshot selector
continue;
}
const listOptions: ListConfigurationSettingsOptions = {
keyFilter: selector.keyFilter,
labelFilter: selector.labelFilter,
tagsFilter: selector.tagFilters,
pageEtags: selector.pageEtags
};

Expand Down Expand Up @@ -674,6 +723,29 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
return response;
}

async #getSnapshot(snapshotName: string, customOptions?: GetSnapshotOptions): Promise<GetSnapshotResponse | undefined> {
const funcToExecute = async (client) => {
return getSnapshotWithTrace(
this.#requestTraceOptions,
client,
snapshotName,
customOptions
);
};

let response: GetSnapshotResponse | undefined;
try {
response = await this.#executeWithFailoverPolicy(funcToExecute);
} catch (error) {
if (isRestError(error) && error.statusCode === 404) {
response = undefined;
} else {
throw error;
}
}
return response;
}

// Only operations related to Azure App Configuration should be executed with failover policy.
async #executeWithFailoverPolicy(funcToExecute: (client: AppConfigurationClient) => Promise<any>): Promise<any> {
let clientWrappers = await this.#clientManager.getClients();
Expand Down Expand Up @@ -817,11 +889,15 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
}
}

function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {
// below code deduplicates selectors by keyFilter and labelFilter, the latter selector wins
function getValidSettingSelectors(selectors: SettingSelector[]): SettingSelector[] {
// below code deduplicates selectors, the latter selector wins
const uniqueSelectors: SettingSelector[] = [];
for (const selector of selectors) {
const existingSelectorIndex = uniqueSelectors.findIndex(s => s.keyFilter === selector.keyFilter && s.labelFilter === selector.labelFilter);
const existingSelectorIndex = uniqueSelectors.findIndex(
s => s.keyFilter === selector.keyFilter &&
s.labelFilter === selector.labelFilter &&
s.snapshotName === selector.snapshotName &&
areTagFiltersEqual(s.tagFilters, selector.tagFilters));
if (existingSelectorIndex >= 0) {
uniqueSelectors.splice(existingSelectorIndex, 1);
}
Expand All @@ -830,25 +906,51 @@ function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {

return uniqueSelectors.map(selectorCandidate => {
const selector = { ...selectorCandidate };
if (!selector.keyFilter) {
throw new ArgumentError("Key filter cannot be null or empty.");
}
if (!selector.labelFilter) {
selector.labelFilter = LabelFilter.Null;
}
if (selector.labelFilter.includes("*") || selector.labelFilter.includes(",")) {
throw new ArgumentError("The characters '*' and ',' are not supported in label filters.");
if (selector.snapshotName) {
if (selector.keyFilter || selector.labelFilter || selector.tagFilters) {
throw new ArgumentError("Key, label or tag filter should not be used for a snapshot.");
}
} else {
if (!selector.keyFilter && (!selector.tagFilters || selector.tagFilters.length === 0)) {
throw new ArgumentError("Key filter cannot be null or empty.");
}
if (!selector.labelFilter) {
selector.labelFilter = LabelFilter.Null;
}
if (selector.labelFilter.includes("*") || selector.labelFilter.includes(",")) {
throw new ArgumentError("The characters '*' and ',' are not supported in label filters.");
}
if (selector.tagFilters) {
validateTagFilters(selector.tagFilters);
}
}
return selector;
});
}

function areTagFiltersEqual(tagsA?: string[], tagsB?: string[]): boolean {
if (!tagsA && !tagsB) {
return true;
}
if (!tagsA || !tagsB) {
return false;
}
if (tagsA.length !== tagsB.length) {
return false;
}

const sortedStringA = [...tagsA].sort().join("\n");
const sortedStringB = [...tagsB].sort().join("\n");

return sortedStringA === sortedStringB;
}

function getValidKeyValueSelectors(selectors?: SettingSelector[]): SettingSelector[] {
if (selectors === undefined || selectors.length === 0) {
// Default selector: key: *, label: \0
return [{ keyFilter: KeyFilter.Any, labelFilter: LabelFilter.Null }];
}
return getValidSelectors(selectors);
return getValidSettingSelectors(selectors);
}

function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSelector[] {
Expand All @@ -859,5 +961,17 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel
selectors.forEach(selector => {
selector.keyFilter = `${featureFlagPrefix}${selector.keyFilter}`;
});
return getValidSelectors(selectors);
return getValidSettingSelectors(selectors);
}

function validateTagFilters(tagFilters: string[]): void {
if (tagFilters.length > MAX_TAG_FILTERS) {
throw new Error(`The number of tag filters cannot exceed ${MAX_TAG_FILTERS}.`);
}
for (const tagFilter of tagFilters) {
const res = tagFilter.split("=");
if (res[0] === "" || res.length !== 2) {
throw new Error(`Invalid tag filter: ${tagFilter}. Tag filter must follow the format "tagName=tagValue".`);
}
}
}
Loading