diff --git a/src/config/merge-patch.ts b/src/config/merge-patch.ts index 982ccf44d1..5878fc57e1 100644 --- a/src/config/merge-patch.ts +++ b/src/config/merge-patch.ts @@ -2,7 +2,48 @@ import { isPlainObject } from "../utils.js"; type PlainObject = Record; -export function applyMergePatch(base: unknown, patch: unknown): unknown { +type MergePatchOptions = { + mergeObjectArraysById?: boolean; +}; + +function isObjectWithStringId(value: unknown): value is Record & { id: string } { + if (!isPlainObject(value)) { + return false; + } + return typeof value.id === "string" && value.id.length > 0; +} + +function mergeObjectArraysById(base: unknown[], patch: unknown[], options: MergePatchOptions) { + if (!base.every(isObjectWithStringId) || !patch.every(isObjectWithStringId)) { + return undefined; + } + const merged = [...base] as Array & { id: string }>; + const indexById = new Map(); + for (const [index, entry] of merged.entries()) { + indexById.set(entry.id, index); + } + + for (const entry of patch) { + const existingIndex = indexById.get(entry.id); + if (existingIndex === undefined) { + merged.push(structuredClone(entry)); + indexById.set(entry.id, merged.length - 1); + continue; + } + merged[existingIndex] = applyMergePatch(merged[existingIndex], entry, options) as Record< + string, + unknown + > & { id: string }; + } + + return merged; +} + +export function applyMergePatch( + base: unknown, + patch: unknown, + options: MergePatchOptions = {}, +): unknown { if (!isPlainObject(patch)) { return patch; } @@ -14,9 +55,16 @@ export function applyMergePatch(base: unknown, patch: unknown): unknown { delete result[key]; continue; } + if (options.mergeObjectArraysById && Array.isArray(result[key]) && Array.isArray(value)) { + const mergedArray = mergeObjectArraysById(result[key] as unknown[], value, options); + if (mergedArray) { + result[key] = mergedArray; + continue; + } + } if (isPlainObject(value)) { const baseValue = result[key]; - result[key] = applyMergePatch(isPlainObject(baseValue) ? baseValue : {}, value); + result[key] = applyMergePatch(isPlainObject(baseValue) ? baseValue : {}, value, options); continue; } result[key] = value; diff --git a/src/gateway/server-methods/config.ts b/src/gateway/server-methods/config.ts index e0dab1a497..807451842e 100644 --- a/src/gateway/server-methods/config.ts +++ b/src/gateway/server-methods/config.ts @@ -342,7 +342,9 @@ export const configHandlers: GatewayRequestHandlers = { ); return; } - const merged = applyMergePatch(snapshot.config, parsedRes.parsed); + const merged = applyMergePatch(snapshot.config, parsedRes.parsed, { + mergeObjectArraysById: true, + }); const schemaPatch = loadSchemaWithPlugins(); const restoredMerge = restoreRedactedValues(merged, snapshot.config, schemaPatch.uiHints); if (!restoredMerge.ok) {