Skip to content

Commit 1f41e0d

Browse files
support snapshot
1 parent 71aebab commit 1f41e0d

File tree

3 files changed

+146
-48
lines changed

3 files changed

+146
-48
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 106 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
import { AppConfigurationClient, ConfigurationSetting, ConfigurationSettingId, GetConfigurationSettingOptions, GetConfigurationSettingResponse, ListConfigurationSettingsOptions, featureFlagPrefix, isFeatureFlag } from "@azure/app-configuration";
4+
import {
5+
AppConfigurationClient,
6+
ConfigurationSetting,
7+
ConfigurationSettingId,
8+
GetConfigurationSettingOptions,
9+
GetConfigurationSettingResponse,
10+
ListConfigurationSettingsOptions,
11+
featureFlagPrefix,
12+
isFeatureFlag,
13+
GetSnapshotOptions,
14+
GetSnapshotResponse,
15+
KnownSnapshotComposition
16+
} from "@azure/app-configuration";
517
import { isRestError } from "@azure/core-rest-pipeline";
618
import { AzureAppConfiguration, ConfigurationObjectConstructionOptions } from "./AzureAppConfiguration.js";
719
import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js";
@@ -35,7 +47,14 @@ import {
3547
} from "./featureManagement/constants.js";
3648
import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js";
3749
import { RefreshTimer } from "./refresh/RefreshTimer.js";
38-
import { RequestTracingOptions, getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js";
50+
import {
51+
RequestTracingOptions,
52+
getConfigurationSettingWithTrace,
53+
listConfigurationSettingsWithTrace,
54+
getSnapshotWithTrace,
55+
listConfigurationSettingsForSnapshotWithTrace,
56+
requestTracingEnabled
57+
} from "./requestTracing/utils.js";
3958
import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions.js";
4059
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
4160
import { ConfigurationClientManager } from "./ConfigurationClientManager.js";
@@ -363,26 +382,49 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
363382
);
364383

365384
for (const selector of selectorsToUpdate) {
366-
const listOptions: ListConfigurationSettingsOptions = {
367-
keyFilter: selector.keyFilter,
368-
labelFilter: selector.labelFilter
369-
};
370-
371-
const pageEtags: string[] = [];
372-
const pageIterator = listConfigurationSettingsWithTrace(
373-
this.#requestTraceOptions,
374-
client,
375-
listOptions
376-
).byPage();
377-
for await (const page of pageIterator) {
378-
pageEtags.push(page.etag ?? "");
379-
for (const setting of page.items) {
380-
if (loadFeatureFlag === isFeatureFlag(setting)) {
381-
loadedSettings.push(setting);
385+
if (selector.snapshotName === undefined) {
386+
const listOptions: ListConfigurationSettingsOptions = {
387+
keyFilter: selector.keyFilter,
388+
labelFilter: selector.labelFilter
389+
};
390+
const pageEtags: string[] = [];
391+
const pageIterator = listConfigurationSettingsWithTrace(
392+
this.#requestTraceOptions,
393+
client,
394+
listOptions
395+
).byPage();
396+
397+
for await (const page of pageIterator) {
398+
pageEtags.push(page.etag ?? "");
399+
for (const setting of page.items) {
400+
if (loadFeatureFlag === isFeatureFlag(setting)) {
401+
loadedSettings.push(setting);
402+
}
403+
}
404+
}
405+
selector.pageEtags = pageEtags;
406+
} else { // snapshot selector
407+
const snapshot = await this.#getSnapshot(selector.snapshotName);
408+
if (snapshot === undefined) {
409+
throw new Error(`Could not find snapshot with name ${selector.snapshotName}.`);
410+
}
411+
if (snapshot.compositionType != KnownSnapshotComposition.Key) {
412+
throw new Error(`Composition type for the selected snapshot with name ${selector.snapshotName} must be 'key'.`);
413+
}
414+
const pageIterator = listConfigurationSettingsForSnapshotWithTrace(
415+
this.#requestTraceOptions,
416+
client,
417+
selector.snapshotName
418+
).byPage();
419+
420+
for await (const page of pageIterator) {
421+
for (const setting of page.items) {
422+
if (loadFeatureFlag === isFeatureFlag(setting)) {
423+
loadedSettings.push(setting);
424+
}
382425
}
383426
}
384427
}
385-
selector.pageEtags = pageEtags;
386428
}
387429

388430
if (loadFeatureFlag) {
@@ -530,6 +572,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
530572
async #checkConfigurationSettingsChange(selectors: PagedSettingSelector[]): Promise<boolean> {
531573
const funcToExecute = async (client) => {
532574
for (const selector of selectors) {
575+
if (selector.snapshotName) { // skip snapshot selector
576+
continue;
577+
}
533578
const listOptions: ListConfigurationSettingsOptions = {
534579
keyFilter: selector.keyFilter,
535580
labelFilter: selector.labelFilter,
@@ -581,6 +626,29 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
581626
return response;
582627
}
583628

629+
async #getSnapshot(snapshotName: string, customOptions?: GetSnapshotOptions): Promise<GetSnapshotResponse | undefined> {
630+
const funcToExecute = async (client) => {
631+
return getSnapshotWithTrace(
632+
this.#requestTraceOptions,
633+
client,
634+
snapshotName,
635+
customOptions
636+
);
637+
};
638+
639+
let response: GetSnapshotResponse | undefined;
640+
try {
641+
response = await this.#executeWithFailoverPolicy(funcToExecute);
642+
} catch (error) {
643+
if (isRestError(error) && error.statusCode === 404) {
644+
response = undefined;
645+
} else {
646+
throw error;
647+
}
648+
}
649+
return response;
650+
}
651+
584652
async #executeWithFailoverPolicy(funcToExecute: (client: AppConfigurationClient) => Promise<any>): Promise<any> {
585653
let clientWrappers = await this.#clientManager.getClients();
586654
if (this.#options?.loadBalancingEnabled && this.#lastSuccessfulEndpoint !== "" && clientWrappers.length > 1) {
@@ -862,11 +930,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
862930
}
863931
}
864932

865-
function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {
866-
// below code deduplicates selectors by keyFilter and labelFilter, the latter selector wins
933+
function getValidSettingSelectors(selectors: SettingSelector[]): SettingSelector[] {
934+
// below code deduplicates selectors, the latter selector wins
867935
const uniqueSelectors: SettingSelector[] = [];
868936
for (const selector of selectors) {
869-
const existingSelectorIndex = uniqueSelectors.findIndex(s => s.keyFilter === selector.keyFilter && s.labelFilter === selector.labelFilter);
937+
const existingSelectorIndex = uniqueSelectors.findIndex(s => s.keyFilter === selector.keyFilter && s.labelFilter === selector.labelFilter && s.snapshotName === selector.snapshotName);
870938
if (existingSelectorIndex >= 0) {
871939
uniqueSelectors.splice(existingSelectorIndex, 1);
872940
}
@@ -875,14 +943,20 @@ function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {
875943

876944
return uniqueSelectors.map(selectorCandidate => {
877945
const selector = { ...selectorCandidate };
878-
if (!selector.keyFilter) {
879-
throw new Error("Key filter cannot be null or empty.");
880-
}
881-
if (!selector.labelFilter) {
882-
selector.labelFilter = LabelFilter.Null;
883-
}
884-
if (selector.labelFilter.includes("*") || selector.labelFilter.includes(",")) {
885-
throw new Error("The characters '*' and ',' are not supported in label filters.");
946+
if (selector.snapshotName) {
947+
if (selector.keyFilter || selector.labelFilter) {
948+
throw new Error("Key or label filter should not be used for a snapshot.");
949+
}
950+
} else {
951+
if (!selector.keyFilter) {
952+
throw new Error("Key filter cannot be null or empty.");
953+
}
954+
if (!selector.labelFilter) {
955+
selector.labelFilter = LabelFilter.Null;
956+
}
957+
if (selector.labelFilter.includes("*") || selector.labelFilter.includes(",")) {
958+
throw new Error("The characters '*' and ',' are not supported in label filters.");
959+
}
886960
}
887961
return selector;
888962
});
@@ -893,7 +967,7 @@ function getValidKeyValueSelectors(selectors?: SettingSelector[]): SettingSelect
893967
// Default selector: key: *, label: \0
894968
return [{ keyFilter: KeyFilter.Any, labelFilter: LabelFilter.Null }];
895969
}
896-
return getValidSelectors(selectors);
970+
return getValidSettingSelectors(selectors);
897971
}
898972

899973
function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSelector[] {
@@ -904,7 +978,7 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel
904978
selectors.forEach(selector => {
905979
selector.keyFilter = `${featureFlagPrefix}${selector.keyFilter}`;
906980
});
907-
return getValidSelectors(selectors);
981+
return getValidSettingSelectors(selectors);
908982
}
909983
}
910984

src/requestTracing/utils.ts

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
import { AppConfigurationClient, ConfigurationSettingId, GetConfigurationSettingOptions, ListConfigurationSettingsOptions } from "@azure/app-configuration";
4+
import { OperationOptions } from "@azure/core-client";
5+
import { AppConfigurationClient, ConfigurationSettingId, GetConfigurationSettingOptions, ListConfigurationSettingsOptions, GetSnapshotOptions, ListConfigurationSettingsForSnapshotOptions } from "@azure/app-configuration";
56
import { AzureAppConfigurationOptions } from "../AzureAppConfigurationOptions.js";
67
import { FeatureFlagTracingOptions } from "./FeatureFlagTracingOptions.js";
78
import {
@@ -45,15 +46,7 @@ export function listConfigurationSettingsWithTrace(
4546
client: AppConfigurationClient,
4647
listOptions: ListConfigurationSettingsOptions
4748
) {
48-
const actualListOptions = { ...listOptions };
49-
if (requestTracingOptions.enabled) {
50-
actualListOptions.requestOptions = {
51-
customHeaders: {
52-
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(requestTracingOptions)
53-
}
54-
};
55-
}
56-
49+
const actualListOptions = applyRequestTracing(requestTracingOptions, listOptions);
5750
return client.listConfigurationSettings(actualListOptions);
5851
}
5952

@@ -63,20 +56,43 @@ export function getConfigurationSettingWithTrace(
6356
configurationSettingId: ConfigurationSettingId,
6457
getOptions?: GetConfigurationSettingOptions,
6558
) {
66-
const actualGetOptions = { ...getOptions };
59+
const actualGetOptions = applyRequestTracing(requestTracingOptions, getOptions);
60+
return client.getConfigurationSetting(configurationSettingId, actualGetOptions);
61+
}
62+
63+
export function getSnapshotWithTrace(
64+
requestTracingOptions: RequestTracingOptions,
65+
client: AppConfigurationClient,
66+
snapshotName: string,
67+
getOptions?: GetSnapshotOptions
68+
) {
69+
const actualGetOptions = applyRequestTracing(requestTracingOptions, getOptions);
70+
return client.getSnapshot(snapshotName, actualGetOptions);
71+
}
6772

73+
export function listConfigurationSettingsForSnapshotWithTrace(
74+
requestTracingOptions: RequestTracingOptions,
75+
client: AppConfigurationClient,
76+
snapshotName: string,
77+
listOptions?: ListConfigurationSettingsForSnapshotOptions
78+
) {
79+
const actualListOptions = applyRequestTracing(requestTracingOptions, listOptions);
80+
return client.listConfigurationSettingsForSnapshot(snapshotName, actualListOptions);
81+
}
82+
83+
function applyRequestTracing<T extends OperationOptions>(requestTracingOptions: RequestTracingOptions, operationOptions?: T) {
84+
const actualOptions = { ...operationOptions };
6885
if (requestTracingOptions.enabled) {
69-
actualGetOptions.requestOptions = {
86+
actualOptions.requestOptions = {
7087
customHeaders: {
7188
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(requestTracingOptions)
7289
}
7390
};
7491
}
75-
76-
return client.getConfigurationSetting(configurationSettingId, actualGetOptions);
92+
return actualOptions;
7793
}
7894

79-
export function createCorrelationContextHeader(requestTracingOptions: RequestTracingOptions): string {
95+
function createCorrelationContextHeader(requestTracingOptions: RequestTracingOptions): string {
8096
/*
8197
RequestType: 'Startup' during application starting up, 'Watch' after startup completed.
8298
Host: identify with defined envs
@@ -200,4 +216,3 @@ export function isWebWorker() {
200216

201217
return workerGlobalScopeDefined && importScriptsAsGlobalFunction && isNavigatorDefinedAsExpected;
202218
}
203-

src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ export type SettingSelector = {
2929
* @defaultValue `LabelFilter.Null`, matching key-values without a label.
3030
*/
3131
labelFilter?: string
32+
33+
/**
34+
* The name of snapshot to load from App Configuration.
35+
*
36+
* @remarks
37+
* Snapshot is a set of key-values selected from the App Configuration store based on the composition type and filters. Once created, it is stored as an immutable entity that can be referenced by name.
38+
* If snapshot name is used in a selector, no key and label filter should be used for it. Otherwise, an exception will be thrown.
39+
*/
40+
snapshotName?: string
3241
};
3342

3443
/**

0 commit comments

Comments
 (0)