From cb338962f5c0a874080ecddc1517a1c6f619588b Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Fri, 31 Jul 2026 15:36:49 +0200 Subject: [PATCH] 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 ``` Review in cubic --- .../remove-undefined-from-record.util.spec.ts | 40 +++++++++++++++++++ .../remove-undefined-from-record.util.ts | 4 +- 2 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/record-crud/utils/__tests__/remove-undefined-from-record.util.spec.ts diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/utils/__tests__/remove-undefined-from-record.util.spec.ts b/packages/twenty-server/src/engine/core-modules/record-crud/utils/__tests__/remove-undefined-from-record.util.spec.ts new file mode 100644 index 0000000000..e1bcf4cc48 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/record-crud/utils/__tests__/remove-undefined-from-record.util.spec.ts @@ -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'] }); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/utils/remove-undefined-from-record.util.ts b/packages/twenty-server/src/engine/core-modules/record-crud/utils/remove-undefined-from-record.util.ts index 239b9f5878..94026a6782 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/utils/remove-undefined-from-record.util.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/utils/remove-undefined-from-record.util.ts @@ -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 = >( const result: Record = {}; for (const [key, value] of Object.entries(record)) { - if (!isDefined(value)) { + if (value === undefined) { continue; }