11// Copyright (c) Microsoft Corporation.
22// Licensed under the MIT license.
33
4- import { AppConfigurationClient , ConfigurationSetting , ConfigurationSettingId , GetConfigurationSettingOptions , GetConfigurationSettingResponse , ListConfigurationSettingsOptions , featureFlagPrefix , isFeatureFlag , isSecretReference } from "@azure/app-configuration" ;
4+ import {
5+ AppConfigurationClient ,
6+ ConfigurationSetting ,
7+ ConfigurationSettingId ,
8+ GetConfigurationSettingOptions ,
9+ GetConfigurationSettingResponse ,
10+ ListConfigurationSettingsOptions ,
11+ featureFlagPrefix ,
12+ isFeatureFlag ,
13+ isSecretReference ,
14+ GetSnapshotOptions ,
15+ GetSnapshotResponse ,
16+ KnownSnapshotComposition
17+ } from "@azure/app-configuration" ;
518import { isRestError } from "@azure/core-rest-pipeline" ;
619import { AzureAppConfiguration , ConfigurationObjectConstructionOptions } from "./AzureAppConfiguration.js" ;
720import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js" ;
@@ -29,7 +42,14 @@ import { FM_PACKAGE_NAME, AI_MIME_PROFILE, AI_CHAT_COMPLETION_MIME_PROFILE } fro
2942import { parseContentType , isJsonContentType , isFeatureFlagContentType , isSecretReferenceContentType } from "./common/contentType.js" ;
3043import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js" ;
3144import { RefreshTimer } from "./refresh/RefreshTimer.js" ;
32- import { RequestTracingOptions , getConfigurationSettingWithTrace , listConfigurationSettingsWithTrace , requestTracingEnabled } from "./requestTracing/utils.js" ;
45+ import {
46+ RequestTracingOptions ,
47+ getConfigurationSettingWithTrace ,
48+ listConfigurationSettingsWithTrace ,
49+ getSnapshotWithTrace ,
50+ listConfigurationSettingsForSnapshotWithTrace ,
51+ requestTracingEnabled
52+ } from "./requestTracing/utils.js" ;
3353import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions.js" ;
3454import { AIConfigurationTracingOptions } from "./requestTracing/AIConfigurationTracingOptions.js" ;
3555import { KeyFilter , LabelFilter , SettingSelector } from "./types.js" ;
@@ -453,26 +473,49 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
453473 ) ;
454474
455475 for ( const selector of selectorsToUpdate ) {
456- const listOptions : ListConfigurationSettingsOptions = {
457- keyFilter : selector . keyFilter ,
458- labelFilter : selector . labelFilter
459- } ;
460-
461- const pageEtags : string [ ] = [ ] ;
462- const pageIterator = listConfigurationSettingsWithTrace (
463- this . #requestTraceOptions,
464- client ,
465- listOptions
466- ) . byPage ( ) ;
467- for await ( const page of pageIterator ) {
468- pageEtags . push ( page . etag ?? "" ) ;
469- for ( const setting of page . items ) {
470- if ( loadFeatureFlag === isFeatureFlag ( setting ) ) {
471- loadedSettings . push ( setting ) ;
476+ if ( selector . snapshotName === undefined ) {
477+ const listOptions : ListConfigurationSettingsOptions = {
478+ keyFilter : selector . keyFilter ,
479+ labelFilter : selector . labelFilter
480+ } ;
481+ const pageEtags : string [ ] = [ ] ;
482+ const pageIterator = listConfigurationSettingsWithTrace (
483+ this . #requestTraceOptions,
484+ client ,
485+ listOptions
486+ ) . byPage ( ) ;
487+
488+ for await ( const page of pageIterator ) {
489+ pageEtags . push ( page . etag ?? "" ) ;
490+ for ( const setting of page . items ) {
491+ if ( loadFeatureFlag === isFeatureFlag ( setting ) ) {
492+ loadedSettings . push ( setting ) ;
493+ }
494+ }
495+ }
496+ selector . pageEtags = pageEtags ;
497+ } else { // snapshot selector
498+ const snapshot = await this . #getSnapshot( selector . snapshotName ) ;
499+ if ( snapshot === undefined ) {
500+ throw new InvalidOperationError ( `Could not find snapshot with name ${ selector . snapshotName } .` ) ;
501+ }
502+ if ( snapshot . compositionType != KnownSnapshotComposition . Key ) {
503+ throw new InvalidOperationError ( `Composition type for the selected snapshot with name ${ selector . snapshotName } must be 'key'.` ) ;
504+ }
505+ const pageIterator = listConfigurationSettingsForSnapshotWithTrace (
506+ this . #requestTraceOptions,
507+ client ,
508+ selector . snapshotName
509+ ) . byPage ( ) ;
510+
511+ for await ( const page of pageIterator ) {
512+ for ( const setting of page . items ) {
513+ if ( loadFeatureFlag === isFeatureFlag ( setting ) ) {
514+ loadedSettings . push ( setting ) ;
515+ }
472516 }
473517 }
474518 }
475- selector . pageEtags = pageEtags ;
476519 }
477520
478521 if ( loadFeatureFlag ) {
@@ -644,6 +687,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
644687 async #checkConfigurationSettingsChange( selectors : PagedSettingSelector [ ] ) : Promise < boolean > {
645688 const funcToExecute = async ( client ) => {
646689 for ( const selector of selectors ) {
690+ if ( selector . snapshotName ) { // skip snapshot selector
691+ continue ;
692+ }
647693 const listOptions : ListConfigurationSettingsOptions = {
648694 keyFilter : selector . keyFilter ,
649695 labelFilter : selector . labelFilter ,
@@ -695,6 +741,29 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
695741 return response ;
696742 }
697743
744+ async #getSnapshot( snapshotName : string , customOptions ?: GetSnapshotOptions ) : Promise < GetSnapshotResponse | undefined > {
745+ const funcToExecute = async ( client ) => {
746+ return getSnapshotWithTrace (
747+ this . #requestTraceOptions,
748+ client ,
749+ snapshotName ,
750+ customOptions
751+ ) ;
752+ } ;
753+
754+ let response : GetSnapshotResponse | undefined ;
755+ try {
756+ response = await this . #executeWithFailoverPolicy( funcToExecute ) ;
757+ } catch ( error ) {
758+ if ( isRestError ( error ) && error . statusCode === 404 ) {
759+ response = undefined ;
760+ } else {
761+ throw error ;
762+ }
763+ }
764+ return response ;
765+ }
766+
698767 // Only operations related to Azure App Configuration should be executed with failover policy.
699768 async #executeWithFailoverPolicy( funcToExecute : ( client : AppConfigurationClient ) => Promise < any > ) : Promise < any > {
700769 let clientWrappers = await this . #clientManager. getClients ( ) ;
@@ -838,11 +907,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
838907 }
839908}
840909
841- function getValidSelectors ( selectors : SettingSelector [ ] ) : SettingSelector [ ] {
842- // below code deduplicates selectors by keyFilter and labelFilter , the latter selector wins
910+ function getValidSettingSelectors ( selectors : SettingSelector [ ] ) : SettingSelector [ ] {
911+ // below code deduplicates selectors, the latter selector wins
843912 const uniqueSelectors : SettingSelector [ ] = [ ] ;
844913 for ( const selector of selectors ) {
845- const existingSelectorIndex = uniqueSelectors . findIndex ( s => s . keyFilter === selector . keyFilter && s . labelFilter === selector . labelFilter ) ;
914+ const existingSelectorIndex = uniqueSelectors . findIndex ( s => s . keyFilter === selector . keyFilter && s . labelFilter === selector . labelFilter && s . snapshotName === selector . snapshotName ) ;
846915 if ( existingSelectorIndex >= 0 ) {
847916 uniqueSelectors . splice ( existingSelectorIndex , 1 ) ;
848917 }
@@ -851,14 +920,20 @@ function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {
851920
852921 return uniqueSelectors . map ( selectorCandidate => {
853922 const selector = { ...selectorCandidate } ;
854- if ( ! selector . keyFilter ) {
855- throw new ArgumentError ( "Key filter cannot be null or empty." ) ;
856- }
857- if ( ! selector . labelFilter ) {
858- selector . labelFilter = LabelFilter . Null ;
859- }
860- if ( selector . labelFilter . includes ( "*" ) || selector . labelFilter . includes ( "," ) ) {
861- throw new ArgumentError ( "The characters '*' and ',' are not supported in label filters." ) ;
923+ if ( selector . snapshotName ) {
924+ if ( selector . keyFilter || selector . labelFilter ) {
925+ throw new ArgumentError ( "Key or label filter should not be used for a snapshot." ) ;
926+ }
927+ } else {
928+ if ( ! selector . keyFilter ) {
929+ throw new ArgumentError ( "Key filter cannot be null or empty." ) ;
930+ }
931+ if ( ! selector . labelFilter ) {
932+ selector . labelFilter = LabelFilter . Null ;
933+ }
934+ if ( selector . labelFilter . includes ( "*" ) || selector . labelFilter . includes ( "," ) ) {
935+ throw new ArgumentError ( "The characters '*' and ',' are not supported in label filters." ) ;
936+ }
862937 }
863938 return selector ;
864939 } ) ;
@@ -869,7 +944,7 @@ function getValidKeyValueSelectors(selectors?: SettingSelector[]): SettingSelect
869944 // Default selector: key: *, label: \0
870945 return [ { keyFilter : KeyFilter . Any , labelFilter : LabelFilter . Null } ] ;
871946 }
872- return getValidSelectors ( selectors ) ;
947+ return getValidSettingSelectors ( selectors ) ;
873948}
874949
875950function getValidFeatureFlagSelectors ( selectors ?: SettingSelector [ ] ) : SettingSelector [ ] {
@@ -878,7 +953,9 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel
878953 return [ { keyFilter : `${ featureFlagPrefix } ${ KeyFilter . Any } ` , labelFilter : LabelFilter . Null } ] ;
879954 }
880955 selectors . forEach ( selector => {
881- selector . keyFilter = `${ featureFlagPrefix } ${ selector . keyFilter } ` ;
956+ if ( selector . keyFilter ) {
957+ selector . keyFilter = `${ featureFlagPrefix } ${ selector . keyFilter } ` ;
958+ }
882959 } ) ;
883- return getValidSelectors ( selectors ) ;
960+ return getValidSettingSelectors ( selectors ) ;
884961}
0 commit comments