fix(config): harden object-array merge-by-id fallback

This commit is contained in:
sebslight
2026-02-16 08:20:31 -05:00
parent dddb1bc942
commit f4b2fd00bc
3 changed files with 67 additions and 21 deletions

View File

@@ -13,25 +13,35 @@ function isObjectWithStringId(value: unknown): value is Record<string, unknown>
return typeof value.id === "string" && value.id.length > 0;
}
function mergeObjectArraysById(base: unknown[], patch: unknown[], options: MergePatchOptions) {
// Require all *base* entries to have string ids — if the existing array
// isn't id-keyed there's nothing sensible to merge against.
/**
* Merge arrays of object-like entries keyed by `id`.
*
* Contract:
* - Base array must be fully id-keyed; otherwise return undefined (caller should replace).
* - Patch entries with valid id merge by id (or append when the id is new).
* - Patch entries without valid id append as-is, avoiding destructive full-array replacement.
*/
function mergeObjectArraysById(
base: unknown[],
patch: unknown[],
options: MergePatchOptions,
): unknown[] | undefined {
if (!base.every(isObjectWithStringId)) {
return undefined;
}
const merged = [...base] as Array<Record<string, unknown> & { id: string }>;
const merged: unknown[] = [...base];
const indexById = new Map<string, number>();
for (const [index, entry] of merged.entries()) {
if (!isObjectWithStringId(entry)) {
return undefined;
}
indexById.set(entry.id, index);
}
for (const patchEntry of patch) {
// Patch entries without a valid id are appended as-is (best-effort).
// This prevents the entire merge from falling back to full replacement
// just because one patch element is missing an id field.
if (!isObjectWithStringId(patchEntry)) {
merged.push(structuredClone(patchEntry) as Record<string, unknown> & { id: string });
merged.push(structuredClone(patchEntry));
continue;
}
@@ -41,10 +51,8 @@ function mergeObjectArraysById(base: unknown[], patch: unknown[], options: Merge
indexById.set(patchEntry.id, merged.length - 1);
continue;
}
merged[existingIndex] = applyMergePatch(merged[existingIndex], patchEntry, options) as Record<
string,
unknown
> & { id: string };
merged[existingIndex] = applyMergePatch(merged[existingIndex], patchEntry, options);
}
return merged;