Skip to content

Commit 9286453

Browse files
authored
fix deep-nesting (#16644)
1 parent 90fd523 commit 9286453

2 files changed

Lines changed: 110 additions & 2 deletions

File tree

shell/utils/__tests__/object.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,83 @@ describe('fx: diff', () => {
210210

211211
expect(result).toStrictEqual(expected);
212212
});
213+
it('should preserve nested object values when transitioning from empty object to populated object', () => {
214+
const from = { parent: { child: { config: {} } } };
215+
const to = {
216+
parent: {
217+
child: {
218+
config: {
219+
annotations: { hello: 'world' },
220+
labels: { key: 'value' }
221+
}
222+
}
223+
}
224+
};
225+
226+
const result = diff(from, to);
227+
const expected = {
228+
parent: {
229+
child: {
230+
config: {
231+
annotations: { hello: 'world' },
232+
labels: { key: 'value' }
233+
}
234+
}
235+
}
236+
};
237+
238+
expect(result).toStrictEqual(expected);
239+
});
240+
241+
it('should preserve explicit empty object when clearing nested object values', () => {
242+
const from = { parent: { child: { config: { annotations: { hello: 'world' } } } } };
243+
const to = { parent: { child: { config: {} } } };
244+
245+
const result = diff(from, to);
246+
const expected = { parent: { child: { config: {} } } };
247+
248+
expect(result).toStrictEqual(expected);
249+
});
250+
251+
it('should not emit nested empty objects when there are no differences', () => {
252+
const from = { parent: { child: { config: { annotations: { hello: 'world' } } } } };
253+
const to = { parent: { child: { config: { annotations: { hello: 'world' } } } } };
254+
255+
const result = diff(from, to);
256+
const expected = {};
257+
258+
expect(result).toStrictEqual(expected);
259+
});
260+
261+
it('should correctly nullify nested keys when removed', () => {
262+
const from = {
263+
parent: {
264+
child: {
265+
config: {
266+
annotations: { hello: 'world' },
267+
labels: { key: 'value' }
268+
}
269+
}
270+
}
271+
};
272+
const to = { parent: { child: { config: { annotations: { hello: 'world' } } } } };
273+
274+
const result = diff(from, to);
275+
const expected = { parent: { child: { config: { labels: { key: null } } } } };
276+
277+
expect(result).toStrictEqual(expected);
278+
});
279+
280+
it('should not nullify child keys when parent object is updated', () => {
281+
const from = { parent: { child: { config: {} } } };
282+
const to = { parent: { child: { config: { annotations: { hello: 'world' } } } } };
283+
284+
const result = diff(from, to);
285+
const expected = { parent: { child: { config: { annotations: { hello: 'world' } } } } };
286+
287+
expect(result).toStrictEqual(expected);
288+
expect(result.parent.child.config).not.toHaveProperty('annotations', null);
289+
});
213290
});
214291

215292
describe('fx: definedKeys', () => {

shell/utils/object.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,14 @@ export function diff(from, to, preventNull = false) {
220220
if ( Array.isArray(toVal) || Array.isArray(fromVal) ) {
221221
// Don't diff arrays, just use the whole value
222222
res[k] = toVal;
223-
} else if ( isObject(toVal) && isObject(from[k]) ) {
224-
res[k] = diff(fromVal, toVal);
223+
} else if ( isObject(toVal) && isObject(fromVal) ) {
224+
const nested = diff(fromVal, toVal, preventNull);
225+
226+
if (isEmpty(toVal) && !isEmpty(fromVal)) {
227+
res[k] = {};
228+
} else if (!isEmpty(nested)) {
229+
res[k] = nested;
230+
}
225231
} else {
226232
res[k] = toVal;
227233
}
@@ -235,6 +241,31 @@ export function diff(from, to, preventNull = false) {
235241

236242
for ( const k of missing ) {
237243
// we need to gate this in order to prevent unforseen problems with addonConfig
244+
const hasDescendant = toKeys.some(
245+
(tk) => tk.startsWith(`${ k }.`)
246+
);
247+
248+
if (hasDescendant) {
249+
continue;
250+
}
251+
252+
// If we already have a value in 'out' for any parent of this missing key, do NOT nullify the child.
253+
const parts = splitObjectPath(k);
254+
let hasUpdatedParent = false;
255+
256+
for (let i = 1; i < parts.length; i++) {
257+
const parentPath = joinObjectPath(parts.slice(0, i));
258+
259+
if (get(out, parentPath) !== undefined) {
260+
hasUpdatedParent = true;
261+
break;
262+
}
263+
}
264+
265+
if (hasUpdatedParent) {
266+
continue;
267+
}
268+
238269
if (preventNull) {
239270
// keys that come from "definedKeys" method are strings with "" chars inside... We need to clean them up
240271
// so that we can access the value of the obj property

0 commit comments

Comments
 (0)