-
Notifications
You must be signed in to change notification settings - Fork 1
Return configuration changes to caller #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR refactors the import logic to distinguish between added, modified, and deleted configuration settings, and introduces a new ConfigurationDiff interface that is returned when the dryRun parameter is enabled.
Key changes:
- Introduces
ConfigurationDiffinterface withAdded,Modified, andDeletedarrays - Refactors import logic into
analyzeConfigurationChangesmethod to separate analysis from execution - Modified settings are now tracked separately from additions
Reviewed Changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| libraries/azure-app-configuration-importer/src/models.ts | Adds ConfigurationDiff interface and imports required types from @azure/app-configuration |
| libraries/azure-app-configuration-importer/src/index.ts | Exports the new ConfigurationDiff interface |
| libraries/azure-app-configuration-importer/src/appConfigurationImporter.ts | Refactors import logic to distinguish between added/modified/deleted settings and returns ConfigurationDiff when dryRun=true |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
libraries/azure-app-configuration-importer/src/appConfigurationImporter.ts
Outdated
Show resolved
Hide resolved
| if (!settingsAreEqual) { | ||
| configurationSettingToModify.push(incoming); | ||
| // Remove from add list since it's a modification, not an addition | ||
| const addIndex: number = configurationSettingToAdd.findIndex(addSetting => | ||
| addSetting.key === incoming.key && addSetting.label === incoming.label); | ||
| if (addIndex !== -1) { | ||
| configurationSettingToAdd.splice(addIndex, 1); | ||
| } |
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inefficient array operations: for each existing setting that differs, the code performs a findIndex followed by splice on the configurationSettingToAdd array. With N incoming settings and M existing settings that differ, this results in O(N*M) complexity. Consider using a Set or Map data structure to track which settings should be added vs modified, or remove from the add list during the initial loop rather than searching for it later.
| else if (importMode == ImportMode.IgnoreMatch) { | ||
| // Remove unchanged settings from add list | ||
| const addIndex = configurationSettingToAdd.findIndex(addSetting => | ||
| addSetting.key === incoming.key && addSetting.label === incoming.label); | ||
| if (addIndex !== -1) { | ||
| configurationSettingToAdd.splice(addIndex, 1); | ||
| } |
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar performance issue: this block also performs findIndex and splice operations within a loop. This should be optimized along with the modification logic in lines 123-127.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
libraries/azure-app-configuration-importer/src/appConfigurationImporter.ts
Outdated
Show resolved
Hide resolved
libraries/azure-app-configuration-importer/src/appConfigurationImporter.ts
Outdated
Show resolved
Hide resolved
libraries/azure-app-configuration-importer/src/appConfigurationImporter.ts
Outdated
Show resolved
Hide resolved
libraries/azure-app-configuration-importer/src/appConfigurationImporter.ts
Show resolved
Hide resolved
libraries/azure-app-configuration-importer/src/appConfigurationImporter.ts
Outdated
Show resolved
Hide resolved
libraries/azure-app-configuration-importer/src/appConfigurationImporter.ts
Outdated
Show resolved
Hide resolved
libraries/azure-app-configuration-importer/src/appConfigurationImporter.ts
Outdated
Show resolved
Hide resolved
libraries/azure-app-configuration-importer/tests/appConfigurationImporter.spec.ts
Show resolved
Hide resolved
libraries/azure-app-configuration-importer/tests/appConfigurationImporter.spec.ts
Outdated
Show resolved
Hide resolved
| * @param customHeadersOption - Custom headers for the operation. | ||
| * @returns ConfigurationChanges object containing Added, Modified, and Deleted settings | ||
| */ | ||
| public async getConfigurationChanges( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the first letter be upper case given this is a public function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, updated thank you
| configSettingsSource: ConfigurationSettingsSource, | ||
| strict = false, | ||
| importMode?: ImportMode, | ||
| customHeadersOption?: OperationOptions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is this used in this function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The customHeadersOptions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. It does look like needed for this function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We use customHeadersOption to pass the correlationRequestId to link requests involved in the Import process. When the Import API calls the GetConfigurationChanges API, we pass the same correlationRequestId to associate both the read and write operations.
Additionally, we could use this to link the call to request the diff and Import on the portal for better traceability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussed offline. Updated to generate correlationrequestid if not passed to GetConfigurationChanges
| } | ||
| } | ||
|
|
||
| export interface ConfigurationChanges { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a config hasn't change, do we also return it as part of the ConfigurationChanges
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only when import mode is equal to ALL; if a config hasn't changed, then it will be part of the changes
| timeout: number, | ||
| strict = false, | ||
| progressCallback?: (progress: ImportProgress) => unknown, | ||
| importMode?: ImportMode |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can set a defaultValue for importMode
public async Import(
configSettingsSource: ConfigurationSettingsSource,
timeout: number,
strict = false,
progressCallback?: (progress: ImportProgress) => unknown,
importMode = ImportMode.IgnoreMatch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much cleaner - Updated
| "author": "Microsoft Corporation", | ||
| "description": "A client library for importing/exporting key-values between configuration file sources and Azure App Configuration service", | ||
| "version": "1.1.3-preview", | ||
| "version": "2.1.3-preview", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be 2.0.0-preview
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
| } | ||
|
|
||
| /** | ||
| * Get configuration changes between source settings and Azure App Configuration service without any applying changes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does it mean "without any applying changes" ? What do you think of "Get configuration changes between source settings and existing settings in Azure App Configuration service...`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant - "without applying any changes". Don't you think its beneficial to mention this so that users can understand they can call it safely just to preview changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggesting -"Get configuration changes between source settings and existing settings in Azure App Configuration service without applying any changes"
| importMode = ImportMode.IgnoreMatch; | ||
| } | ||
|
|
||
| this.validateImportMode(importMode); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we will validateImportMode twice in Import function and in function to GetConfigurationChanges
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should still validateImportMode in GetConfigurationChanges since it might be called independently
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But since we call GetConfigurationChanges in Import, and in GetConfigurationChanges we validateImportMode, do we need to validateImportMode again in Import ?
|
|
||
| this.validateImportMode(importMode); | ||
|
|
||
| // Generate correlation ID for operations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Generate correlationRequestId for operations in the same activity
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
| if (!settingsAreEqual) { | ||
| configurationSettingToModify.push(incoming); | ||
| // Remove from add list since it's a modification, not an addition | ||
| const addIndex = configurationSettingToAdd.indexOf(incoming); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can simplify this to configurationSettingsToAdd.splice(configurationSettingToAdd.indexOf(incoming), 1).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
| Deleted: ConfigurationSetting<string>[]; | ||
| Modified: SetConfigurationSettingParam<string | FeatureFlagValue | SecretReferenceValue>[]; | ||
| Added: SetConfigurationSettingParam<string | FeatureFlagValue | SecretReferenceValue>[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about rename these fields to ToDelete ToModify, ToAdd? The current names seem to imply that the change has been made.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense to me. Updated
| configurationSettingToDelete.push(existing); | ||
| } | ||
|
|
||
| const incoming = configSettings.find(configSetting => configSetting.key == existing.key && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to suggest instead of adding settings then removing them, maybe we shouldn't add them in the first place, maybe we can have something like this
if (importMode == ImportMode.IgnoreMatch) {
const incoming = configSettings.find(configSetting => configSetting.key == existing.key &&
configSetting.label == existing.label);
if (!incoming) {
configSettingToAdd.push(existing);
} else if (incoming && !isConfigSettingEqual(incoming, existing)) {
configSettingToModify.push(existing);
}
}
Doesn't have to be exactly like this just an idea
| } | ||
| }; | ||
|
|
||
| const configurationChanges: ConfigurationChanges = await this.GetConfigurationChanges(configSettingsSource, strict, importMode, customHeadersOption); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm trying to imagine, how the GetConfigurationChanges() and Import() will be used at client side. One possible situation is, client calls GetConfigurationChanges to preview what are the settings to be changed, then it may ask user 's promotion to apply the change or not, if user confirm to apply the change by calling Import(), In this case, I think Import() should no longer need to call the GetConfigurationChanges() again, just apply the chanages directly.
So seems we can't just blindly call GetConfigurationChanges in Import, Import should have another overload that can accept ConfigurationChanges as an argument.
Thoughts? @MaryanneNjeri @ChristineWanjau
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@RichardChen820 I agree, and Import would no longer need to take parameters like strict or import mode ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I think of the current design, the caller would call GetConfigurationChanges twice, once when they call GetConfigurationChanges and second time when they want to Import.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good point. This is the most likely flow in the client side especially portal. We are planning to preview the changes to the caller then they can click on apply immediately after.
Just one concern I have is consistency issues, what if the pre-calculated changes may no longer be accurate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, it completely does not make sense, and also have risk that GetConfigurationChanges may return different values in these two attempts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we provide a preview, only performing the changes exactly as shown in the preview can be considered consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This leads to a result where we delete an item that the customer was never told would be deleted. This is the real inconsistency from the customer’s point of view
I see, this makes sense.
Considering we won't delete anything added into the store before import happens, to some extent there is not much risk involved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just confirm so we have a second overload of the Import() function that takes in ConfigurationChanges.
public async Import(
configSettingsSource: ConfigurationSettingsSource,
timeout: number,
strict?: boolean,
progressCallback?: (progress: ImportProgress) => unknown,
importMode?: ImportMode
): Promise<void>;
/**
* Import pre-calculated configuration changes into the Azure App Configuration service
*
*
* @param configurationChanges - Pre-calculated changes from GetConfigurationChanges()
* @param timeout
* @param progressCallback
* @returns Promise<void>
*/
public async Import(
configurationChanges: ConfigurationChanges,
timeout: number,
progressCallback?: (progress: ImportProgress) => unknown
): Promise<void>;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! I have updated the function
This change removes the concept of dry-run from the JSImporter and introduces a new approach for exposing configuration changes. Instead of handling the printing of diffs internally, the importer now simply returns the configuration diff, leaving it to the caller to decide how to use it (e.g., print to console or process further).
The previous design tightly coupled the importer with output behavior (printing diffs), which limited flexibility. By returning the diff directly, portal, pipeline tasks or other external callers can decide on how to use the diff.