mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-13 08:05:19 +09:00
fix(dashboard): materialise an immutable ancestor only when a leaf exists
The guard keyed on the intermediate ancestor being present in the original rather than on the immutable leaf being restorable, so an explicit null the user submitted at that ancestor was traded for an empty object with nothing put back into it. Confirm the leaf over the remaining path first, wildcards included. Assisted-by: LLM Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
This commit is contained in:
1 parent
b4259fee03
commit
00e074a77a
2 files changed
+46
-2
No files matched your search
@@ -415,6 +415,17 @@ describe("overlayImmutable", () => {
|
||||
expect(result).toEqual({ spec: { storage: "keep-me" } })
|
||||
})
|
||||
|
||||
it("leaves a null ancestor alone when original has no leaf to restore", () => {
|
||||
// Materialising on the ancestor merely existing would trade the user's
|
||||
// explicit null for an empty object with nothing put back into it.
|
||||
const submitted = { spec: null } as Record<string, unknown>
|
||||
const original = { spec: { storage: { size: "10Gi" } } }
|
||||
const result = overlayImmutable(submitted, original, [
|
||||
["spec", "storage", "storageClass"],
|
||||
])
|
||||
expect(result).toEqual({ spec: null })
|
||||
})
|
||||
|
||||
it("array reordering by the user with index-aligned overlay re-anchors source values to the new index (pinned current behaviour)", () => {
|
||||
// Tracked in cozystack/cozystack-ui#10.
|
||||
// The overlay walks by index, not by content identity. Reordering an
|
||||
|
||||
@@ -153,6 +153,36 @@ function wildcardArrayLengthChanged(
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether source still holds a value at the end of the remaining path.
|
||||
* Materialising a missing intermediate object is only justified when there is
|
||||
* a leaf to restore into it — keyed on the ancestor merely existing, an
|
||||
* explicit null the user parked at that ancestor would be replaced by an empty
|
||||
* object with nothing put back.
|
||||
*/
|
||||
function sourceHasLeaf(
|
||||
source: unknown,
|
||||
path: ImmutablePath,
|
||||
depth: number,
|
||||
): boolean {
|
||||
if (depth === path.length) return source !== undefined
|
||||
const seg = path[depth]
|
||||
if (seg === "*") {
|
||||
// A trailing wildcard freezes the whole collection, so the collection
|
||||
// being present is itself the value to restore.
|
||||
if (depth === path.length - 1) return source !== undefined
|
||||
if (Array.isArray(source)) {
|
||||
return source.some((v) => sourceHasLeaf(v, path, depth + 1))
|
||||
}
|
||||
if (isPlainObject(source)) {
|
||||
return Object.values(source).some((v) => sourceHasLeaf(v, path, depth + 1))
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (!isPlainObject(source)) return false
|
||||
return sourceHasLeaf(source[seg], path, depth + 1)
|
||||
}
|
||||
|
||||
function overlayPath(
|
||||
target: unknown,
|
||||
source: unknown,
|
||||
@@ -175,8 +205,11 @@ function overlayPath(
|
||||
const targetObj = isPlainObject(target)
|
||||
? (target as Record<string, unknown>)
|
||||
: // A YAML edit can drop the whole intermediate object; materialise it so
|
||||
// the immutable leaf underneath is still restored from source.
|
||||
sourceVal !== undefined && (target === undefined || target === null)
|
||||
// the immutable leaf underneath is still restored from source. Only when
|
||||
// source actually has that leaf, so a null the user parked here is not
|
||||
// traded for an empty object holding nothing.
|
||||
(target === undefined || target === null) &&
|
||||
sourceHasLeaf(sourceVal, path, depth + 1)
|
||||
? {}
|
||||
: null
|
||||
if (!targetObj) return target
|
||||
|
||||
Reference in new issue
Block a user