From e3057e5613314ae5ab29b75b85d0e52869c3158d Mon Sep 17 00:00:00 2001 From: Sonarly Claude Code Date: Thu, 2 Apr 2026 11:30:03 +0000 Subject: [PATCH] fix: gracefully handle unknown fields in SSE optimistic record updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://sonarly.com/issue/21148?type=bug When an SSE database event delivers a record update containing custom fields not yet in the frontend's cached metadata, `computeOptimisticRecordFromInput` throws a fatal error that breaks real-time update processing for the session. Fix: Converted the `throw new Error(...)` in `computeOptimisticRecordFromInput` (line 73-77) to `console.warn(...)`. **Why this is safe:** The function's main processing loop (line 80) iterates only over `objectMetadataItem.fields`, not over `recordInput` keys. Unknown fields in `recordInput` are naturally skipped and never included in the output. The `throw` was a defensive assertion added in commit `29745c67568` for mutation-triggered optimistic updates where the frontend controls the input. When SSE real-time events were added (commit `d51c988a9e`), this function started receiving server-controlled data via `useTriggerOptimisticEffectFromSseUpdateEvents`, which passes `event.properties.after` directly — including custom fields that may not yet be in the frontend's stale metadata cache. **What this fixes:** When an SSE event delivers a record containing custom fields (e.g., `testMs` on the `opportunity` object) and the frontend's metadata cache hasn't refreshed yet, the function now logs a warning and continues processing known fields instead of crashing the entire SSE event stream handler. Updated the test from asserting a throw to asserting a `console.warn` call and verifying that known fields (`city: 'Paris'`) are still correctly processed while unknown fields (`unknwon`, `foo`, `bar`) are skipped. --- .../computeOptimisticRecordFromInput.test.ts | 40 +++++++++++-------- .../utils/computeOptimisticRecordFromInput.ts | 4 +- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/utils/__tests__/computeOptimisticRecordFromInput.test.ts b/packages/twenty-front/src/modules/object-record/utils/__tests__/computeOptimisticRecordFromInput.test.ts index e3e1dddc894..c267b46cd05 100644 --- a/packages/twenty-front/src/modules/object-record/utils/__tests__/computeOptimisticRecordFromInput.test.ts +++ b/packages/twenty-front/src/modules/object-record/utils/__tests__/computeOptimisticRecordFromInput.test.ts @@ -252,26 +252,32 @@ describe('computeOptimisticRecordFromInput', () => { }); }); - it('should throw an error if recordInput contains fields unrelated to the current objectMetadata', () => { + it('should warn and skip unknown fields if recordInput contains fields unrelated to the current objectMetadata', () => { const cache = new InMemoryCache(); const personObjectMetadataItem = getMockObjectMetadataItemOrThrow('person'); + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); - expect(() => - computeOptimisticRecordFromInput({ - currentWorkspaceMember, - objectMetadataItems: getTestEnrichedObjectMetadataItemsMock(), - objectMetadataItem: personObjectMetadataItem, - recordInput: { - unknwon: 'unknown', - foo: 'foo', - bar: 'bar', - city: 'Paris', - }, - cache, - objectPermissionsByObjectMetadataId: {}, - }), - ).toThrowErrorMatchingInlineSnapshot( - `"Should never occur, encountered unknown fields unknwon, foo, bar in objectMetadataItem person"`, + const result = computeOptimisticRecordFromInput({ + currentWorkspaceMember, + objectMetadataItems: getTestEnrichedObjectMetadataItemsMock(), + objectMetadataItem: personObjectMetadataItem, + recordInput: { + unknwon: 'unknown', + foo: 'foo', + bar: 'bar', + city: 'Paris', + }, + cache, + objectPermissionsByObjectMetadataId: {}, + }); + + expect(consoleSpy).toHaveBeenCalledWith( + 'computeOptimisticRecordFromInput: skipping unknown fields unknwon, foo, bar in objectMetadataItem person', ); + expect(result).toEqual({ + city: 'Paris', + }); + + consoleSpy.mockRestore(); }); }); diff --git a/packages/twenty-front/src/modules/object-record/utils/computeOptimisticRecordFromInput.ts b/packages/twenty-front/src/modules/object-record/utils/computeOptimisticRecordFromInput.ts index a32b18b1de4..a52688e6f6a 100644 --- a/packages/twenty-front/src/modules/object-record/utils/computeOptimisticRecordFromInput.ts +++ b/packages/twenty-front/src/modules/object-record/utils/computeOptimisticRecordFromInput.ts @@ -71,8 +71,8 @@ export const computeOptimisticRecordFromInput = ({ }, ); if (unknownRecordInputFields.length > 0) { - throw new Error( - `Should never occur, encountered unknown fields ${unknownRecordInputFields.join(', ')} in objectMetadataItem ${objectMetadataItem.nameSingular}`, + console.warn( + `computeOptimisticRecordFromInput: skipping unknown fields ${unknownRecordInputFields.join(', ')} in objectMetadataItem ${objectMetadataItem.nameSingular}`, ); }