From d10c0a643984ed653663c56ad41915fbbe92c56b Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Tue, 31 Mar 2026 13:58:12 +0200 Subject: [PATCH] Fix: composite update events not received (#19053) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Database batch events (update / insert / soft-delete / hard-delete) were building recordsBefore / recordsAfter after formatResult ran twice: once inside WorkspaceSelectQueryBuilder.getMany() / getOne(), and again in the CUD query builders. On the second pass, already-shaped composite fields (e.g. emails) went through formatFieldMetadataValue and kept the same object references as the live TypeORM row, so recordsBefore could change when the entity was updated—breaking workflow triggers and diffs. --- .../workspace-delete-query-builder.ts | 4 ++- .../workspace-insert-query-builder.ts | 4 ++- .../workspace-select-query-builder.ts | 26 +++++++++++++------ .../workspace-soft-delete-query-builder.ts | 8 ++++-- .../workspace-update-query-builder.ts | 16 +++++++++--- .../twenty-orm/utils/format-result.util.ts | 7 ++++- 6 files changed, 48 insertions(+), 17 deletions(-) diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts index d38fdf6816..a3030580d6 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts @@ -104,7 +104,9 @@ export class WorkspaceDeleteQueryBuilder< objectMetadata.isCustom, ); - const before = await eventSelectQueryBuilder.getOne(); + const before = await eventSelectQueryBuilder.getOne({ + noFormatting: true, + }); this.expressionMap.wheres = applyTableAliasOnWhereCondition({ condition: this.expressionMap.wheres, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts index f54369f585..6979b9f17a 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts @@ -241,7 +241,9 @@ export class WorkspaceInsertQueryBuilder< result.identifiers.map((identifier) => identifier.id), ); - const afterResult = await eventSelectQueryBuilder.getMany(); + const afterResult = await eventSelectQueryBuilder.getMany({ + noFormatting: true, + }); const formattedResultForEvent = formatResult( afterResult, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts index 7cca65361e..bcca85bb8a 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts @@ -102,10 +102,16 @@ export class WorkspaceSelectQueryBuilder< } } - override async getMany(): Promise { + override async getMany(options?: { noFormatting?: boolean }): Promise { try { this.validatePermissions(); + const result = await super.getMany(); + + if (options?.noFormatting === true) { + return result; + } + const mainAliasTarget = this.getMainAliasTarget(); const objectMetadata = getObjectMetadataFromEntityTarget( @@ -113,8 +119,6 @@ export class WorkspaceSelectQueryBuilder< this.internalContext, ); - const result = await super.getMany(); - const formattedResult = formatResult( result, objectMetadata, @@ -150,10 +154,20 @@ export class WorkspaceSelectQueryBuilder< } } - override async getOne(): Promise { + override async getOne(options?: { + noFormatting?: boolean; + }): Promise { try { this.validatePermissions(); + this.take(1); + + const result = await super.getOne(); + + if (options?.noFormatting === true) { + return result; + } + const mainAliasTarget = this.getMainAliasTarget(); const objectMetadata = getObjectMetadataFromEntityTarget( @@ -161,10 +175,6 @@ export class WorkspaceSelectQueryBuilder< this.internalContext, ); - this.take(1); - - const result = await super.getOne(); - const formattedResult = formatResult( result, objectMetadata, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts index e4ded2836f..5e7acd15a7 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts @@ -103,7 +103,9 @@ export class WorkspaceSoftDeleteQueryBuilder< objectMetadata.isCustom, ); - const before = await beforeEventSelectQueryBuilder.getMany(); + const before = await beforeEventSelectQueryBuilder.getMany({ + noFormatting: true, + }); this.expressionMap.wheres = applyTableAliasOnWhereCondition({ condition: this.expressionMap.wheres, @@ -113,7 +115,9 @@ export class WorkspaceSoftDeleteQueryBuilder< const typeORMSoftRemoveResultWithOnlyIdColumn = await super.execute(); - const afterWithAllFields = await beforeEventSelectQueryBuilder.getMany(); + const afterWithAllFields = await beforeEventSelectQueryBuilder.getMany({ + noFormatting: true, + }); const formattedAfter = formatResult( afterWithAllFields, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts index fb49df1e85..e205353275 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts @@ -136,7 +136,9 @@ export class WorkspaceUpdateQueryBuilder< objectMetadata.isCustom, ); - const before = await eventSelectQueryBuilder.getMany(); + const before = await eventSelectQueryBuilder.getMany({ + noFormatting: true, + }); if (before.length > QUERY_MAX_RECORDS) { throw new TwentyORMException( @@ -236,7 +238,9 @@ export class WorkspaceUpdateQueryBuilder< await this.filesFieldSync.updateFileEntityRecords(filesFieldFileIds); } - const after = await eventSelectQueryBuilder.getMany(); + const after = await eventSelectQueryBuilder.getMany({ + noFormatting: true, + }); const formattedAfter = formatResult( after, @@ -338,7 +342,9 @@ export class WorkspaceUpdateQueryBuilder< this.manyInputs.map((input) => input.criteria), ); - const beforeRecords = await eventSelectQueryBuilder.getMany(); + const beforeRecords = await eventSelectQueryBuilder.getMany({ + noFormatting: true, + }); const formattedBefore = formatResult( beforeRecords, @@ -447,7 +453,9 @@ export class WorkspaceUpdateQueryBuilder< await this.filesFieldSync.updateFileEntityRecords(filesFieldFileIds); } - const afterRecords = await eventSelectQueryBuilder.getMany(); + const afterRecords = await eventSelectQueryBuilder.getMany({ + noFormatting: true, + }); const formattedAfter = formatResult( afterRecords, diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts index 00e9bf565f..c1718feeab 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts @@ -182,8 +182,13 @@ function formatResultInternal( continue; } + const formattedFieldValue = formatFieldMetadataValue( + value, + fieldMetadata.type, + ); + // @ts-expect-error legacy noImplicitAny - newData[key] = formatFieldMetadataValue(value, fieldMetadata.type); + newData[key] = formattedFieldValue; } // After assembling composite fields, handle those with missing required subfields