fix(server): preserve null values in record update util (#23639)
## Problem Record updates through the MCP server silently drop `null` values. Setting a field to `null` (to clear it) returns success but changes nothing. ## Root cause `removeUndefinedFromRecord` is called by `UpdateRecordService.execute` before the DB write. It used `isDefined(value)` to decide what to strip, but `isDefined` returns false for **both** `undefined` and `null`: ```ts export const isDefined = (value) => !isUndefined(value) && !isNull(value); ``` So despite the name (and the comment stating the validation layer expects "a value or null"), the util also discarded `null`. The MCP `update_one` path deliberately keeps nulls (it filters only `!== undefined`), but this util then removed them one layer down. The workflow UPDATE_RECORD action was less affected because it passes an explicit `fieldsToUpdate` list, but the value-level strip sits on both paths. ## Fix Strip only `undefined`; preserve `null` so a field can be explicitly cleared. Added unit tests covering undefined stripping, null preservation, and nested composite fields. ## Test ``` npx jest remove-undefined-from-record # 5 passed ``` <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23639?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+40
@@ -0,0 +1,40 @@
|
||||
import { removeUndefinedFromRecord } from 'src/engine/core-modules/record-crud/utils/remove-undefined-from-record.util';
|
||||
|
||||
describe('removeUndefinedFromRecord', () => {
|
||||
it('should strip undefined values', () => {
|
||||
expect(removeUndefinedFromRecord({ name: 'John', age: undefined })).toEqual(
|
||||
{
|
||||
name: 'John',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should preserve null values so a field can be cleared', () => {
|
||||
expect(
|
||||
removeUndefinedFromRecord({ name: 'John', closeDate: null }),
|
||||
).toEqual({ name: 'John', closeDate: null });
|
||||
});
|
||||
|
||||
it('should preserve null sub-properties in composite fields', () => {
|
||||
expect(
|
||||
removeUndefinedFromRecord({
|
||||
emails: { primaryEmail: null, additionalEmails: undefined },
|
||||
}),
|
||||
).toEqual({ emails: { primaryEmail: null } });
|
||||
});
|
||||
|
||||
it('should drop nested objects that only contain undefined', () => {
|
||||
expect(
|
||||
removeUndefinedFromRecord({
|
||||
emails: { primaryEmail: undefined },
|
||||
name: 'John',
|
||||
}),
|
||||
).toEqual({ name: 'John' });
|
||||
});
|
||||
|
||||
it('should preserve arrays as-is', () => {
|
||||
expect(
|
||||
removeUndefinedFromRecord({ tags: ['a', 'b'], removed: undefined }),
|
||||
).toEqual({ tags: ['a', 'b'] });
|
||||
});
|
||||
});
|
||||
+1
-3
@@ -1,5 +1,3 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
// Recursively removes undefined values from an object
|
||||
// This is needed because workflows/tools may pass partial composite fields
|
||||
// with undefined sub-properties, but the validation layer expects either
|
||||
@@ -10,7 +8,7 @@ export const removeUndefinedFromRecord = <T extends Record<string, unknown>>(
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(record)) {
|
||||
if (!isDefined(value)) {
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user