Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion sdk/appconfiguration/app-configuration/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "js",
"TagPrefix": "js/appconfiguration/app-configuration",
"Tag": "js/appconfiguration/app-configuration_b28373e403"
"Tag": "js/appconfiguration/app-configuration_6270a8c013"
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class AppConfigurationClient {
archiveSnapshot(name: string, options?: UpdateSnapshotOptions): Promise<UpdateSnapshotResponse>;
beginCreateSnapshot(snapshot: SnapshotInfo, options?: CreateSnapshotOptions): Promise<SimplePollerLike<OperationState<CreateSnapshotResponse>, CreateSnapshotResponse>>;
beginCreateSnapshotAndWait(snapshot: SnapshotInfo, options?: CreateSnapshotOptions): Promise<CreateSnapshotResponse>;
checkConfigurationSettings(options?: CheckConfigurationSettingsOptions): PagedAsyncIterableIterator<ConfigurationSetting, ListConfigurationSettingPage, PageSettings>;
deleteConfigurationSetting(id: ConfigurationSettingId, options?: DeleteConfigurationSettingOptions): Promise<DeleteConfigurationSettingResponse>;
getConfigurationSetting(id: ConfigurationSettingId, options?: GetConfigurationSettingOptions): Promise<GetConfigurationSettingResponse>;
getSnapshot(name: string, options?: GetSnapshotOptions): Promise<GetSnapshotResponse>;
Expand All @@ -51,6 +52,11 @@ export interface AppConfigurationClientOptions extends CommonClientOptions {
audience?: string;
}

// @public
export interface CheckConfigurationSettingsOptions extends OperationOptions, ListSettingsOptions {
pageEtags?: string[];
}

// @public
export type ConfigurationSetting<T extends string | FeatureFlagValue | SecretReferenceValue | SnapshotReferenceValue = string> = ConfigurationSettingParam<T> & {
isReadOnly: boolean;
Expand Down
100 changes: 100 additions & 0 deletions sdk/appconfiguration/app-configuration/src/appConfigurationClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type AddConfigurationSettingParam,
type AddConfigurationSettingResponse,
type AppConfigurationClientOptions,
type CheckConfigurationSettingsOptions,
type ConfigurationSetting,
type ConfigurationSettingId,
type CreateSnapshotOptions,
Expand Down Expand Up @@ -44,9 +45,11 @@ import type {
AppConfigurationGetKeyValuesHeaders,
AppConfigurationGetRevisionsHeaders,
AppConfigurationGetSnapshotsHeaders,
AppConfigurationCheckKeyValuesHeaders,
GetKeyValuesResponse,
GetRevisionsResponse,
GetSnapshotsResponse,
CheckKeyValuesResponse,
ConfigurationSnapshot,
GetLabelsResponse,
AppConfigurationGetLabelsHeaders,
Expand Down Expand Up @@ -447,6 +450,81 @@ export class AppConfigurationClient {
return getPagedAsyncIterator(pagedResult);
}

/**
* Checks settings from the Azure App Configuration service using a HEAD request, returning only headers without the response body.
* This is useful for efficiently checking if settings have changed by comparing ETags.
*
* Example code:
* ```ts snippet:CheckConfigurationSettings
* import { DefaultAzureCredential } from "@azure/identity";
* import { AppConfigurationClient } from "@azure/app-configuration";
*
* // The endpoint for your App Configuration resource
* const endpoint = "https://example.azconfig.io";
* const credential = new DefaultAzureCredential();
* const client = new AppConfigurationClient(endpoint, credential);
*
* const pageIterator = client.checkConfigurationSettings({ keyFilter: "MyKey" }).byPage();
* ```
* @param options - Optional parameters for the request.
*/
checkConfigurationSettings(
options: CheckConfigurationSettingsOptions = {},
): PagedAsyncIterableIterator<ConfigurationSetting, ListConfigurationSettingPage, PageSettings> {
const pageEtags = options.pageEtags ? [...options.pageEtags] : undefined;
delete options.pageEtags;
const pagedResult: PagedResult<ListConfigurationSettingPage, PageSettings, string | undefined> =
{
firstPageLink: undefined,
getPage: async (pageLink: string | undefined) => {
const etag = pageEtags?.shift();
try {
const response = await this.checkConfigurationSettingsRequest(
{ ...options, etag },
pageLink,
);
const link = response?._response?.headers?.get("link");
const continuationToken = link ? extractAfterTokenFromLinkHeader(link) : undefined;
const currentResponse: ListConfigurationSettingPage = {
...response,
etag: response?._response?.headers?.get("etag") ?? undefined,
items: [],
continuationToken: continuationToken,
_response: response._response,
};
return {
page: currentResponse,
nextPageLink: currentResponse.continuationToken,
};
} catch (error) {
const err = error as RestError;

const link = err.response?.headers?.get("link");
const continuationToken = link ? extractAfterTokenFromLinkHeader(link) : undefined;

if (err.statusCode === 304) {
err.message = `Status 304: No updates for this page`;
logger.info(
`[checkConfigurationSettings] No updates for this page. The current etag for the page is ${etag}`,
);
return {
page: {
items: [],
etag,
_response: { ...err.response, status: 304 },
} as unknown as ListConfigurationSettingPage,
nextPageLink: continuationToken,
};
}

throw err;
}
},
toElements: (page) => page.items,
};
return getPagedAsyncIterator(pagedResult);
}

/**
* Lists settings from the Azure App Configuration service for snapshots based on name, optionally
* filtered by key names, labels and accept datetime.
Expand Down Expand Up @@ -580,6 +658,28 @@ export class AppConfigurationClient {
);
}

private async checkConfigurationSettingsRequest(
options: SendConfigurationSettingsOptions & PageSettings = {},
pageLink: string | undefined,
): Promise<CheckKeyValuesResponse & HttpResponseField<AppConfigurationCheckKeyValuesHeaders>> {
return tracingClient.withSpan(
"AppConfigurationClient.checkConfigurationSettings",
options,
async (updatedOptions) => {
const response = await this.client.checkKeyValues({
...updatedOptions,
...formatAcceptDateTime(options),
...formatConfigurationSettingsFiltersAndSelect(options),
...checkAndFormatIfAndIfNoneMatch({ etag: options.etag }, { onlyIfChanged: true }),
after: pageLink,
});

return response as CheckKeyValuesResponse &
HttpResponseField<AppConfigurationCheckKeyValuesHeaders>;
},
);
}

/**
* Lists revisions of a set of keys, optionally filtered by key names,
* labels and accept datetime.
Expand Down
10 changes: 10 additions & 0 deletions sdk/appconfiguration/app-configuration/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,16 @@ export interface ListConfigurationSettingsOptions extends OperationOptions, List
pageEtags?: string[];
}

/**
* Options for checkConfigurationSettings that allow for filtering based on keys, labels and other fields.
*/
export interface CheckConfigurationSettingsOptions extends OperationOptions, ListSettingsOptions {
/**
* Etags list for page
*/
pageEtags?: string[];
}

/**
* Options for listLabels
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,104 @@ describe("AppConfigurationClient", () => {
});
});

describe("checkConfigurationSettings", () => {
let count = 0;

const keys: {
checkConfigSettingA: string;
} = {
checkConfigSettingA: "",
};

beforeEach(async () => {
keys.checkConfigSettingA = recorder.variable(
`checkConfigSetting${count}A`,
`checkConfigSetting${count}A${Math.floor(Math.random() * 100000)}`,
);
count += 1;

await client.addConfigurationSetting({
key: keys.checkConfigSettingA,
value: "[A] production value",
});
});

afterEach(async () => {
try {
await deleteKeyCompletely([keys.checkConfigSettingA], client);
} catch (e: any) {
/** empty */
}
});

it("returns empty items with valid response structure", async () => {
const pageIterator = client
.checkConfigurationSettings({ keyFilter: keys.checkConfigSettingA })
.byPage();

const firstPage = await pageIterator.next();
assert.isFalse(firstPage.done);
assert.isDefined(firstPage.value);
assert.equal(firstPage.value.items.length, 0, "items should be empty for HEAD request");
assert.isDefined(firstPage.value.etag, "etag should be present");
assert.equal(firstPage.value._response.status, 200);
assert.isDefined(firstPage.value._response.headers.get("x-ms-date"));
});

it("returns 304 when using valid etag and no changes occurred", async () => {
// First call to get the etag
const pageIterator1 = client
.checkConfigurationSettings({ keyFilter: keys.checkConfigSettingA })
.byPage();
const firstPage1 = await pageIterator1.next();
const etag = firstPage1.value.etag;

assert.isDefined(etag);
const etags: string[] = [etag!];

// Second call with the same etag - should return 304
const pageIterator2 = client
.checkConfigurationSettings({ keyFilter: keys.checkConfigSettingA, pageEtags: etags })
.byPage();
const firstPage2 = await pageIterator2.next();

assert.isFalse(firstPage2.done);
assert.equal(firstPage2.value.items.length, 0);
assert.equal(firstPage2.value._response.status, 304, "should return 304 Not Modified");
assert.equal(firstPage2.value.etag, etag, "etag should be the same");
assert.isDefined(firstPage2.value._response.headers.get("x-ms-date"));
});

it("returns 200 when using etag but changes were made", async () => {
// First call to get the etag
const pageIterator1 = client
.checkConfigurationSettings({ keyFilter: keys.checkConfigSettingA })
.byPage();
const firstPage1 = await pageIterator1.next();
const etag = firstPage1.value.etag;

assert.isDefined(etag);
const etags: string[] = [etag!];

// Make a change
await client.setConfigurationSetting({
key: keys.checkConfigSettingA,
value: "[A] modified value",
});

// Second call with the old etag - should return 200 because content changed
const pageIterator2 = client
.checkConfigurationSettings({ keyFilter: keys.checkConfigSettingA, pageEtags: etags })
.byPage();
const firstPage2 = await pageIterator2.next();

assert.isFalse(firstPage2.done);
assert.equal(firstPage2.value.items.length, 0);
assert.equal(firstPage2.value._response.status, 200, "should return 200 with changes");
assert.notEqual(firstPage2.value.etag, etag, "etag should be different");
});
});

describe("listConfigSettings", () => {
let key1: string;
let key2: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,15 @@ describe("snippets", () => {
const allSettingsWithLabel = client.listConfigurationSettings({ labelFilter: "MyLabel" });
});

it("CheckConfigurationSettings", async () => {
// The endpoint for your App Configuration resource
const endpoint = "https://example.azconfig.io";
const credential = new DefaultAzureCredential();
const client = new AppConfigurationClient(endpoint, credential);
// @ts-preserve-whitespace
const pageIterator = client.checkConfigurationSettings({ keyFilter: "MyKey" }).byPage();
});

it("ListConfigurationSettingsForSnashots", async () => {
// The endpoint for your App Configuration resource
const endpoint = "https://example.azconfig.io";
Expand Down