Compare commits

...
Author SHA1 Message Date
Thomas Trompette 546b486bd3 Fix: composite update events not received (#19053)
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.
2026-04-01 11:10:46 +02:00
6 changed files with 48 additions and 17 deletions
@@ -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,
@@ -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<T[]>(
afterResult,
@@ -102,10 +102,16 @@ export class WorkspaceSelectQueryBuilder<
}
}
override async getMany(): Promise<T[]> {
override async getMany(options?: { noFormatting?: boolean }): Promise<T[]> {
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<T[]>(
result,
objectMetadata,
@@ -150,10 +154,20 @@ export class WorkspaceSelectQueryBuilder<
}
}
override async getOne(): Promise<T | null> {
override async getOne(options?: {
noFormatting?: boolean;
}): Promise<T | null> {
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<T>(
result,
objectMetadata,
@@ -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<T[]>(
afterWithAllFields,
@@ -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<T[]>(
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<T[]>(
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<T[]>(
afterRecords,
@@ -182,8 +182,13 @@ function formatResultInternal<T>(
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