diff --git a/sdk/appconfiguration/app-configuration/CHANGELOG.md b/sdk/appconfiguration/app-configuration/CHANGELOG.md index 654a886add7f..0b009a1ecacc 100644 --- a/sdk/appconfiguration/app-configuration/CHANGELOG.md +++ b/sdk/appconfiguration/app-configuration/CHANGELOG.md @@ -4,6 +4,11 @@ ### Features Added +- Support snapshot referece. + - New types for SnapshotReference - `ConfigurationSetting` and `ConfigurationSetting` + - Upon using `getConfigurationSetting`(or add/update), use `parseSnapshotReference` methods to access the properties(to translate `ConfigurationSetting` into the types above). + - Helper method `isSnapshotReference` checks the contentType and return boolean values. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/appconfiguration/app-configuration/assets.json b/sdk/appconfiguration/app-configuration/assets.json index f1c3e2cc87aa..4a08c7afbd28 100644 --- a/sdk/appconfiguration/app-configuration/assets.json +++ b/sdk/appconfiguration/app-configuration/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "js", "TagPrefix": "js/appconfiguration/app-configuration", - "Tag": "js/appconfiguration/app-configuration_bdaf29d71a" + "Tag": "js/appconfiguration/app-configuration_257c4f0dd5" } diff --git a/sdk/appconfiguration/app-configuration/review/app-configuration-node.api.md b/sdk/appconfiguration/app-configuration/review/app-configuration-node.api.md index 1117a9201e28..2a876debe0f3 100644 --- a/sdk/appconfiguration/app-configuration/review/app-configuration-node.api.md +++ b/sdk/appconfiguration/app-configuration/review/app-configuration-node.api.md @@ -17,7 +17,7 @@ export interface AddConfigurationSettingOptions extends OperationOptions { } // @public -export type AddConfigurationSettingParam = ConfigurationSettingParam; +export type AddConfigurationSettingParam = ConfigurationSettingParam; // @public export interface AddConfigurationSettingResponse extends ConfigurationSetting, SyncTokenHeaderField, HttpResponseField { @@ -27,7 +27,7 @@ export interface AddConfigurationSettingResponse extends ConfigurationSetting, S export class AppConfigurationClient { constructor(connectionString: string, options?: AppConfigurationClientOptions); constructor(endpoint: string, tokenCredential: TokenCredential, options?: AppConfigurationClientOptions); - addConfigurationSetting(configurationSetting: AddConfigurationSettingParam | AddConfigurationSettingParam | AddConfigurationSettingParam, options?: AddConfigurationSettingOptions): Promise; + addConfigurationSetting(configurationSetting: AddConfigurationSettingParam | AddConfigurationSettingParam | AddConfigurationSettingParam | AddConfigurationSettingParam, options?: AddConfigurationSettingOptions): Promise; archiveSnapshot(name: string, options?: UpdateSnapshotOptions): Promise; beginCreateSnapshot(snapshot: SnapshotInfo, options?: CreateSnapshotOptions): Promise, CreateSnapshotResponse>>; beginCreateSnapshotAndWait(snapshot: SnapshotInfo, options?: CreateSnapshotOptions): Promise; @@ -40,7 +40,7 @@ export class AppConfigurationClient { listRevisions(options?: ListRevisionsOptions): PagedAsyncIterableIterator; listSnapshots(options?: ListSnapshotsOptions): PagedAsyncIterableIterator; recoverSnapshot(name: string, options?: UpdateSnapshotOptions): Promise; - setConfigurationSetting(configurationSetting: SetConfigurationSettingParam | SetConfigurationSettingParam | SetConfigurationSettingParam, options?: SetConfigurationSettingOptions): Promise; + setConfigurationSetting(configurationSetting: SetConfigurationSettingParam | SetConfigurationSettingParam | SetConfigurationSettingParam | SetConfigurationSettingParam, options?: SetConfigurationSettingOptions): Promise; setReadOnly(id: ConfigurationSettingId, readOnly: boolean, options?: SetReadOnlyOptions): Promise; updateSyncToken(syncToken: string): void; } @@ -52,7 +52,7 @@ export interface AppConfigurationClientOptions extends CommonClientOptions { } // @public -export type ConfigurationSetting = ConfigurationSettingParam & { +export type ConfigurationSetting = ConfigurationSettingParam & { isReadOnly: boolean; lastModified?: Date; }; @@ -65,7 +65,7 @@ export interface ConfigurationSettingId { } // @public -export type ConfigurationSettingParam = ConfigurationSettingId & { +export type ConfigurationSettingParam = ConfigurationSettingId & { contentType?: string; tags?: { [propertyName: string]: string; @@ -198,6 +198,9 @@ export function isFeatureFlag(setting: ConfigurationSetting): setting is Configu // @public export function isSecretReference(setting: ConfigurationSetting): setting is ConfigurationSetting & Required>; +// @public +export function isSnapshotReference(setting: ConfigurationSetting): setting is ConfigurationSetting & Required>; + // @public export enum KnownAppConfigAudience { AzureChina = "https://appconfig.azure.cn", @@ -302,6 +305,9 @@ export function parseFeatureFlag(setting: ConfigurationSetting): ConfigurationSe // @public export function parseSecretReference(setting: ConfigurationSetting): ConfigurationSetting; +// @public +export function parseSnapshotReference(setting: ConfigurationSetting): ConfigurationSetting; + // @public export interface RetryOptions { maxRetries?: number; @@ -321,7 +327,7 @@ export interface SetConfigurationSettingOptions extends HttpOnlyIfUnchangedField } // @public -export type SetConfigurationSettingParam = ConfigurationSettingParam; +export type SetConfigurationSettingParam = ConfigurationSettingParam; // @public export interface SetConfigurationSettingResponse extends ConfigurationSetting, SyncTokenHeaderField, HttpResponseField { @@ -354,6 +360,14 @@ export interface SnapshotInfo { }; } +// @public +export const snapshotReferenceContentType = "application/json; profile=\"https://azconfig.io/mime-profiles/snapshot-ref\"; charset=utf-8"; + +// @public +export interface SnapshotReferenceValue { + snapshotName: string; +} + // @public export interface SnapshotResponse extends ConfigurationSnapshot, SyncTokenHeaderField { } diff --git a/sdk/appconfiguration/app-configuration/src/appConfigurationClient.ts b/sdk/appconfiguration/app-configuration/src/appConfigurationClient.ts index f87f8537eccc..dd0816af600b 100644 --- a/sdk/appconfiguration/app-configuration/src/appConfigurationClient.ts +++ b/sdk/appconfiguration/app-configuration/src/appConfigurationClient.ts @@ -85,6 +85,7 @@ import { import { AppConfiguration } from "./generated/src/appConfiguration.js"; import type { FeatureFlagValue } from "./featureFlag.js"; import type { SecretReferenceValue } from "./secretReference.js"; +import type { SnapshotReferenceValue } from "./snapshotReference.js"; import { appConfigKeyCredentialPolicy } from "./appConfigCredential.js"; import { tracingClient } from "./internal/tracing.js"; import { logger } from "./logger.js"; @@ -225,7 +226,8 @@ export class AppConfigurationClient { configurationSetting: | AddConfigurationSettingParam | AddConfigurationSettingParam - | AddConfigurationSettingParam, + | AddConfigurationSettingParam + | AddConfigurationSettingParam, options: AddConfigurationSettingOptions = {}, ): Promise { return tracingClient.withSpan( @@ -655,7 +657,8 @@ export class AppConfigurationClient { configurationSetting: | SetConfigurationSettingParam | SetConfigurationSettingParam - | SetConfigurationSettingParam, + | SetConfigurationSettingParam + | SetConfigurationSettingParam, options: SetConfigurationSettingOptions = {}, ): Promise { return tracingClient.withSpan( diff --git a/sdk/appconfiguration/app-configuration/src/index.ts b/sdk/appconfiguration/app-configuration/src/index.ts index b3789d4569d9..fc63686ee70e 100644 --- a/sdk/appconfiguration/app-configuration/src/index.ts +++ b/sdk/appconfiguration/app-configuration/src/index.ts @@ -16,3 +16,9 @@ export { secretReferenceContentType, SecretReferenceValue, } from "./secretReference.js"; +export { + isSnapshotReference, + parseSnapshotReference, + snapshotReferenceContentType, + SnapshotReferenceValue, +} from "./snapshotReference.js"; diff --git a/sdk/appconfiguration/app-configuration/src/internal/helpers.ts b/sdk/appconfiguration/app-configuration/src/internal/helpers.ts index a5c763c61e03..fa0d4fefa592 100644 --- a/sdk/appconfiguration/app-configuration/src/internal/helpers.ts +++ b/sdk/appconfiguration/app-configuration/src/internal/helpers.ts @@ -27,6 +27,8 @@ import type { } from "../generated/src/models/index.js"; import type { SecretReferenceValue } from "../secretReference.js"; import { SecretReferenceHelper, secretReferenceContentType } from "../secretReference.js"; +import type { SnapshotReferenceValue } from "../snapshotReference.js"; +import { SnapshotReferenceHelper, snapshotReferenceContentType } from "../snapshotReference.js"; import { isDefined } from "@azure/core-util"; import { logger } from "../logger.js"; import type { OperationOptions } from "@azure/core-client"; @@ -299,6 +301,19 @@ function isConfigSettingWithSecretReferenceValue( ); } +/** + * @internal + */ +function isConfigSettingWithSnapshotReferenceValue( + setting: any, +): setting is ConfigurationSetting { + return ( + setting.contentType === snapshotReferenceContentType && + isDefined(setting.value) && + typeof setting.value !== "string" + ); +} + /** * @internal */ @@ -326,7 +341,8 @@ export function serializeAsConfigurationSettingParam( setting: | ConfigurationSettingParam | ConfigurationSettingParam - | ConfigurationSettingParam, + | ConfigurationSettingParam + | ConfigurationSettingParam, ): ConfigurationSettingParam { if (isSimpleConfigSetting(setting)) { return setting as ConfigurationSettingParam; @@ -338,6 +354,9 @@ export function serializeAsConfigurationSettingParam( if (isConfigSettingWithSecretReferenceValue(setting)) { return SecretReferenceHelper.toConfigurationSettingParam(setting); } + if (isConfigSettingWithSnapshotReferenceValue(setting)) { + return SnapshotReferenceHelper.toConfigurationSettingParam(setting); + } } catch (error: any) { return setting as ConfigurationSettingParam; } diff --git a/sdk/appconfiguration/app-configuration/src/internal/jsonModels.ts b/sdk/appconfiguration/app-configuration/src/internal/jsonModels.ts index 99d6e87bfe72..40cd6db221f2 100644 --- a/sdk/appconfiguration/app-configuration/src/internal/jsonModels.ts +++ b/sdk/appconfiguration/app-configuration/src/internal/jsonModels.ts @@ -22,3 +22,12 @@ export type JsonFeatureFlagValue = { export interface JsonSecretReferenceValue { uri: string; } + +// snapshot reference + +/** + * @internal + */ +export interface JsonSnapshotReferenceValue { + snapshot_name: string; +} diff --git a/sdk/appconfiguration/app-configuration/src/models.ts b/sdk/appconfiguration/app-configuration/src/models.ts index 170c17fd1bf7..5cb0fcb10629 100644 --- a/sdk/appconfiguration/app-configuration/src/models.ts +++ b/sdk/appconfiguration/app-configuration/src/models.ts @@ -5,6 +5,7 @@ import type { CompatResponse } from "@azure/core-http-compat"; import type { FeatureFlagValue } from "./featureFlag.js"; import type { CommonClientOptions, OperationOptions } from "@azure/core-client"; import type { SecretReferenceValue } from "./secretReference.js"; +import type { SnapshotReferenceValue } from "./snapshotReference.js"; import type { SnapshotComposition, ConfigurationSettingsFilter, @@ -73,7 +74,7 @@ export interface ConfigurationSettingId { * Necessary fields for updating or creating a new configuration setting */ export type ConfigurationSettingParam< - T extends string | FeatureFlagValue | SecretReferenceValue = string, + T extends string | FeatureFlagValue | SecretReferenceValue | SnapshotReferenceValue = string, > = ConfigurationSettingId & { /** * The content type of the setting's value @@ -103,7 +104,7 @@ export type ConfigurationSettingParam< * its etag, whether it is currently readOnly and when it was last modified. */ export type ConfigurationSetting< - T extends string | FeatureFlagValue | SecretReferenceValue = string, + T extends string | FeatureFlagValue | SecretReferenceValue | SnapshotReferenceValue = string, > = ConfigurationSettingParam & { /** * Whether or not the setting is read-only @@ -150,14 +151,14 @@ export interface HttpResponseField { * Parameters for adding a new configuration setting */ export type AddConfigurationSettingParam< - T extends string | FeatureFlagValue | SecretReferenceValue = string, + T extends string | FeatureFlagValue | SecretReferenceValue | SnapshotReferenceValue = string, > = ConfigurationSettingParam; /** * Parameters for creating or updating a new configuration setting */ export type SetConfigurationSettingParam< - T extends string | FeatureFlagValue | SecretReferenceValue = string, + T extends string | FeatureFlagValue | SecretReferenceValue | SnapshotReferenceValue = string, > = ConfigurationSettingParam; /** diff --git a/sdk/appconfiguration/app-configuration/src/snapshotReference.ts b/sdk/appconfiguration/app-configuration/src/snapshotReference.ts new file mode 100644 index 000000000000..ece7c878edea --- /dev/null +++ b/sdk/appconfiguration/app-configuration/src/snapshotReference.ts @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { ConfigurationSetting, ConfigurationSettingParam } from "./models.js"; +import type { JsonSnapshotReferenceValue } from "./internal/jsonModels.js"; +import { logger } from "./logger.js"; + +/** + * content-type for the snapshot reference. + */ +export const snapshotReferenceContentType = + 'application/json; profile="https://azconfig.io/mime-profiles/snapshot-ref"; charset=utf-8'; + +/** + * Necessary fields for updating or creating a new snapshot reference. + */ +export interface SnapshotReferenceValue { + /** + * snapshot name. + */ + snapshotName: string; +} + +/** + * @internal + */ +export const SnapshotReferenceHelper = { + /** + * Takes the SnapshotReference (JSON) and returns a ConfigurationSetting (with the props encodeed in the value). + */ + toConfigurationSettingParam: ( + snapshotReference: ConfigurationSettingParam, + ): ConfigurationSettingParam => { + logger.info("Encoding SnapshotReference value in a ConfigurationSetting:", snapshotReference); + if (!snapshotReference.value) { + logger.error(`SnapshotReference has an unexpected value`, snapshotReference); + throw new TypeError(`SnapshotReference has an unexpected value - ${snapshotReference.value}`); + } + + const jsonSnapshotReferenceValue: JsonSnapshotReferenceValue = { + snapshot_name: snapshotReference.value.snapshotName, + }; + + const configSetting = { + ...snapshotReference, + value: JSON.stringify(jsonSnapshotReferenceValue), + }; + return configSetting; + }, +}; + +/** + * Takes the ConfigurationSetting as input and returns the ConfigurationSetting by parsing the value string. + */ +export function parseSnapshotReference( + setting: ConfigurationSetting, +): ConfigurationSetting { + logger.info( + "[parseSnapshotReference] Parsing the value to return the SnapshotReferenceValue", + setting, + ); + if (!isSnapshotReference(setting)) { + logger.error("Invalid SnapshotReference input", setting); + throw TypeError( + `Setting with key ${setting.key} is not a valid SnapshotReference, make sure to have the correct content-type and a valid non-null value.`, + ); + } + + const jsonSnapshotReferenceValue = JSON.parse(setting.value) as JsonSnapshotReferenceValue; + + const snapshotReference: ConfigurationSetting = { + ...setting, + value: { snapshotName: jsonSnapshotReferenceValue.snapshot_name }, + }; + return snapshotReference; +} + +/** + * Lets you know if the ConfigurationSetting is a snapshot reference. + * + * [Checks if the content type is snapshotReferenceContentType `"application/json; profile=\"https://azconfig.io/mime-profiles/snapshot-ref\"; charset=utf-8"`] + */ +export function isSnapshotReference( + setting: ConfigurationSetting, +): setting is ConfigurationSetting & Required> { + return ( + setting && + setting.contentType === snapshotReferenceContentType && + typeof setting.value === "string" + ); +} diff --git a/sdk/appconfiguration/app-configuration/test/public/snapshotReference.spec.ts b/sdk/appconfiguration/app-configuration/test/public/snapshotReference.spec.ts new file mode 100644 index 000000000000..244a9ed40f40 --- /dev/null +++ b/sdk/appconfiguration/app-configuration/test/public/snapshotReference.spec.ts @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { + AddConfigurationSettingResponse, + AppConfigurationClient, + ConfigurationSetting, + SnapshotReferenceValue, +} from "../../src/index.js"; +import { + isSnapshotReference, + parseSnapshotReference, + snapshotReferenceContentType, +} from "../../src/index.js"; +import type { Recorder } from "@azure-tools/test-recorder"; +import { createAppConfigurationClientForTests, startRecorder } from "./utils/testHelpers.js"; +import { describe, it, assert, beforeEach, afterEach } from "vitest"; + +describe("AppConfigurationClient - SnapshotReference", () => { + let client: AppConfigurationClient; + let recorder: Recorder; + + beforeEach(async (ctx) => { + recorder = await startRecorder(ctx); + client = createAppConfigurationClientForTests(recorder.configureClientOptions({})); + }); + + afterEach(async () => { + await recorder.stop(); + }); + + describe("SnapshotReference configuration setting", () => { + const getBaseSetting = (): ConfigurationSetting => { + return { + value: { + snapshotName: recorder.variable( + "snapshot-name", + `snapshot-name${Math.floor(Math.random() * 1000)}`, + ), + }, + isReadOnly: false, + key: recorder.variable("snapshot-ref", `snapshot-ref${Math.floor(Math.random() * 1000)}`), + label: "label-sn", + contentType: snapshotReferenceContentType, + }; + }; + + function assertSnapshotReferenceProps( + actual: Omit, + expected: ConfigurationSetting, + ): void { + assert.equal(isSnapshotReference(actual), true, "Expected to get the SnapshotReference"); + const actualSnapshotReference = parseSnapshotReference(actual); + if (isSnapshotReference(actual)) { + assert.equal( + actual.key, + expected.key, + "Key from the response from get request is not as expected", + ); + assert.equal(actualSnapshotReference.value.snapshotName, expected.value.snapshotName); + assert.equal(actual.isReadOnly, expected.isReadOnly); + assert.equal(actual.label, expected.label); + assert.equal(actual.contentType, expected.contentType); + } + } + + let addResponse: AddConfigurationSettingResponse; + let baseSetting: ConfigurationSetting; + beforeEach(async () => { + baseSetting = getBaseSetting(); + addResponse = await client.addConfigurationSetting(baseSetting); + }); + + afterEach(async () => { + await client.setReadOnly( + { + key: baseSetting.key, + label: baseSetting.label, + }, + false, + ); + await client.deleteConfigurationSetting({ + key: baseSetting.key, + label: baseSetting.label, + }); + }); + + it("can add and get SnapshotReference", async () => { + assertSnapshotReferenceProps(addResponse, baseSetting); + const getResponse = await client.getConfigurationSetting({ + key: baseSetting.key, + label: baseSetting.label, + }); + assertSnapshotReferenceProps(getResponse, baseSetting); + }); + + it("can add and update SnapshotReference", async () => { + const getResponse = await client.getConfigurationSetting({ + key: baseSetting.key, + label: baseSetting.label, + }); + const newSnapshotName = recorder.variable( + "snapshot-name-2", + `snapshot-name-2${Math.floor(Math.random() * 1000)}`, + ); + + assertSnapshotReferenceProps(getResponse, baseSetting); + const snapshotReference = parseSnapshotReference(getResponse); + snapshotReference.value.snapshotName = newSnapshotName; + + const setResponse = await client.setConfigurationSetting(snapshotReference); + assertSnapshotReferenceProps(setResponse, { + ...baseSetting, + value: { snapshotName: newSnapshotName }, + }); + + const getResponseAfterUpdate = await client.getConfigurationSetting({ + key: baseSetting.key, + label: baseSetting.label, + }); + assertSnapshotReferenceProps(getResponseAfterUpdate, { + ...baseSetting, + value: { snapshotName: newSnapshotName }, + }); + }); + + it("can add, list and update multiple SnapshotReferences", async () => { + const secondSetting: ConfigurationSetting = { + ...baseSetting, + key: `${baseSetting.key}-2`, + }; + const newSnapshotName = recorder.variable( + "snapshot-name-3", + `snapshot-name-3${Math.floor(Math.random() * 1000)}`, + ); + await client.addConfigurationSetting(secondSetting); + + let numberOfSnapshotReferencesReceived = 0; + for await (const setting of client.listConfigurationSettings({ + keyFilter: `${baseSetting.key}*`, + })) { + numberOfSnapshotReferencesReceived++; + if (setting.key === baseSetting.key) { + assertSnapshotReferenceProps(setting, baseSetting); + await client.setConfigurationSetting({ + ...baseSetting, + value: { snapshotName: newSnapshotName }, + }); + } else { + assertSnapshotReferenceProps(setting, secondSetting); + await client.setReadOnly( + { key: setting.key, label: setting.label }, + !secondSetting.isReadOnly, + ); + } + } + assert.equal( + numberOfSnapshotReferencesReceived, + 2, + "Unexpected number of SnapshotReferences seen", + ); + for await (const setting of client.listConfigurationSettings({ + keyFilter: `${baseSetting.key}*`, + })) { + numberOfSnapshotReferencesReceived--; + if (setting.key === baseSetting.key) { + assertSnapshotReferenceProps(setting, { + ...baseSetting, + value: { snapshotName: newSnapshotName }, + }); + } else { + assertSnapshotReferenceProps(setting, { + ...secondSetting, + isReadOnly: !secondSetting.isReadOnly, + }); + } + } + + assert.equal( + numberOfSnapshotReferencesReceived, + 0, + "Unexpected number of SnapshotReferences seen after updating", + ); + await client.setReadOnly({ key: secondSetting.key, label: secondSetting.label }, false); + await client.deleteConfigurationSetting({ + key: secondSetting.key, + label: secondSetting.label, + }); + }); + }); + + describe("serializeAsConfigurationSettingParam", () => { + [`[]`, "Hello World"].forEach((value) => { + it(`Unexpected value ${value} as snapshot reference value`, async () => { + const setting: ConfigurationSetting = { + contentType: snapshotReferenceContentType, + key: recorder.variable( + "snapshot-ref-1", + `snapshot-ref-1${Math.floor(Math.random() * 1000)}`, + ), + isReadOnly: false, + value: { snapshotName: "name" }, + }; + setting.value = value as any; + await client.addConfigurationSetting(setting as any); + assert.equal( + (await client.getConfigurationSetting({ key: setting.key })).value, + value, + "message", + ); + await client.deleteConfigurationSetting({ key: setting.key }); + }); + }); + }); +});