Fix ORM event mixing records in batch updates (#15985)
While investigating the issue a customer was facing, I discovered that before records and after records could be in different order, making the orm event and timeline activity engine mix records
This commit is contained in:
+1
-1
@@ -196,7 +196,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
options?.objectRecordsPermissions ?? {},
|
||||
this.internalContext,
|
||||
options?.shouldBypassPermissionChecks ?? false,
|
||||
undefined,
|
||||
{},
|
||||
this.getFeatureFlagMap(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,5 +19,7 @@ export enum TwentyORMExceptionCode {
|
||||
ENUM_TYPE_NAME_NOT_FOUND = 'ENUM_TYPE_NAME_NOT_FOUND',
|
||||
QUERY_READ_TIMEOUT = 'QUERY_READ_TIMEOUT',
|
||||
DUPLICATE_ENTRY_DETECTED = 'DUPLICATE_ENTRY_DETECTED',
|
||||
TOO_MANY_RECORDS_TO_UPDATE = 'TOO_MANY_RECORDS_TO_UPDATE',
|
||||
INVALID_INPUT = 'INVALID_INPUT',
|
||||
ORM_EVENT_DATA_CORRUPTED = 'ORM_EVENT_DATA_CORRUPTED',
|
||||
}
|
||||
|
||||
+15
-19
@@ -20,10 +20,11 @@ import {
|
||||
TwentyORMExceptionCode,
|
||||
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
|
||||
import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils';
|
||||
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
import { type WorkspaceSoftDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-soft-delete-query-builder';
|
||||
import { type WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder';
|
||||
import { applyTableAliasOnWhereCondition } from 'src/engine/twenty-orm/utils/apply-table-alias-on-where-condition';
|
||||
import { computeEventSelectQueryBuilder } from 'src/engine/twenty-orm/utils/compute-event-select-query-builder.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
@@ -35,15 +36,15 @@ export class WorkspaceDeleteQueryBuilder<
|
||||
private objectRecordsPermissions: ObjectsPermissions;
|
||||
private shouldBypassPermissionChecks: boolean;
|
||||
private internalContext: WorkspaceInternalContext;
|
||||
private authContext?: AuthContext;
|
||||
private featureFlagMap?: FeatureFlagMap;
|
||||
private authContext: AuthContext;
|
||||
private featureFlagMap: FeatureFlagMap;
|
||||
constructor(
|
||||
queryBuilder: DeleteQueryBuilder<T>,
|
||||
objectRecordsPermissions: ObjectsPermissions,
|
||||
internalContext: WorkspaceInternalContext,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
authContext?: AuthContext,
|
||||
featureFlagMap?: FeatureFlagMap,
|
||||
authContext: AuthContext,
|
||||
featureFlagMap: FeatureFlagMap,
|
||||
) {
|
||||
super(queryBuilder);
|
||||
this.objectRecordsPermissions = objectRecordsPermissions;
|
||||
@@ -62,6 +63,7 @@ export class WorkspaceDeleteQueryBuilder<
|
||||
this.internalContext,
|
||||
this.shouldBypassPermissionChecks,
|
||||
this.authContext,
|
||||
this.featureFlagMap,
|
||||
) as this;
|
||||
}
|
||||
|
||||
@@ -81,26 +83,20 @@ export class WorkspaceDeleteQueryBuilder<
|
||||
this.internalContext,
|
||||
);
|
||||
|
||||
const eventSelectQueryBuilder = new WorkspaceSelectQueryBuilder(
|
||||
this as unknown as WorkspaceSelectQueryBuilder<T>,
|
||||
this.objectRecordsPermissions,
|
||||
this.internalContext,
|
||||
true,
|
||||
this.authContext,
|
||||
this.featureFlagMap,
|
||||
);
|
||||
const eventSelectQueryBuilder = computeEventSelectQueryBuilder<T>({
|
||||
queryBuilder: this,
|
||||
authContext: this.authContext,
|
||||
internalContext: this.internalContext,
|
||||
featureFlagMap: this.featureFlagMap,
|
||||
expressionMap: this.expressionMap,
|
||||
objectRecordsPermissions: this.objectRecordsPermissions,
|
||||
});
|
||||
|
||||
const tableName = computeTableName(
|
||||
objectMetadata.nameSingular,
|
||||
objectMetadata.isCustom,
|
||||
);
|
||||
|
||||
eventSelectQueryBuilder.expressionMap.wheres = this.expressionMap.wheres;
|
||||
|
||||
eventSelectQueryBuilder.expressionMap.aliases =
|
||||
this.expressionMap.aliases;
|
||||
eventSelectQueryBuilder.setParameters(this.getParameters());
|
||||
|
||||
const before = await eventSelectQueryBuilder.getOne();
|
||||
|
||||
this.expressionMap.wheres = applyTableAliasOnWhereCondition({
|
||||
|
||||
+5
-4
@@ -38,8 +38,8 @@ export class WorkspaceInsertQueryBuilder<
|
||||
private objectRecordsPermissions: ObjectsPermissions;
|
||||
private shouldBypassPermissionChecks: boolean;
|
||||
private internalContext: WorkspaceInternalContext;
|
||||
private authContext?: AuthContext;
|
||||
private featureFlagMap?: FeatureFlagMap;
|
||||
private authContext: AuthContext;
|
||||
private featureFlagMap: FeatureFlagMap;
|
||||
private relationNestedQueries: RelationNestedQueries;
|
||||
private relationNestedConfig:
|
||||
| [RelationConnectQueryConfig[], RelationDisconnectQueryFieldsByEntityIndex]
|
||||
@@ -50,8 +50,8 @@ export class WorkspaceInsertQueryBuilder<
|
||||
objectRecordsPermissions: ObjectsPermissions,
|
||||
internalContext: WorkspaceInternalContext,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
authContext?: AuthContext,
|
||||
featureFlagMap?: FeatureFlagMap,
|
||||
authContext: AuthContext,
|
||||
featureFlagMap: FeatureFlagMap,
|
||||
) {
|
||||
super(queryBuilder);
|
||||
this.objectRecordsPermissions = objectRecordsPermissions;
|
||||
@@ -123,6 +123,7 @@ export class WorkspaceInsertQueryBuilder<
|
||||
this.internalContext,
|
||||
this.shouldBypassPermissionChecks,
|
||||
this.authContext,
|
||||
this.featureFlagMap,
|
||||
);
|
||||
|
||||
const updatedValues =
|
||||
|
||||
+4
-4
@@ -33,15 +33,15 @@ export class WorkspaceSelectQueryBuilder<
|
||||
objectRecordsPermissions: ObjectsPermissions;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
internalContext: WorkspaceInternalContext;
|
||||
authContext?: AuthContext;
|
||||
featureFlagMap?: FeatureFlagMap;
|
||||
authContext: AuthContext;
|
||||
featureFlagMap: FeatureFlagMap;
|
||||
constructor(
|
||||
queryBuilder: SelectQueryBuilder<T>,
|
||||
objectRecordsPermissions: ObjectsPermissions,
|
||||
internalContext: WorkspaceInternalContext,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
authContext?: AuthContext,
|
||||
featureFlagMap?: FeatureFlagMap,
|
||||
authContext: AuthContext,
|
||||
featureFlagMap: FeatureFlagMap,
|
||||
) {
|
||||
super(queryBuilder);
|
||||
this.objectRecordsPermissions = objectRecordsPermissions;
|
||||
|
||||
+16
-19
@@ -20,9 +20,10 @@ import {
|
||||
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
|
||||
import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils';
|
||||
import { type WorkspaceDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-delete-query-builder';
|
||||
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
import { type WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder';
|
||||
import { applyTableAliasOnWhereCondition } from 'src/engine/twenty-orm/utils/apply-table-alias-on-where-condition';
|
||||
import { computeEventSelectQueryBuilder } from 'src/engine/twenty-orm/utils/compute-event-select-query-builder.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
@@ -34,16 +35,16 @@ export class WorkspaceSoftDeleteQueryBuilder<
|
||||
private objectRecordsPermissions: ObjectsPermissions;
|
||||
private shouldBypassPermissionChecks: boolean;
|
||||
private internalContext: WorkspaceInternalContext;
|
||||
private authContext?: AuthContext;
|
||||
private featureFlagMap?: FeatureFlagMap;
|
||||
private authContext: AuthContext;
|
||||
private featureFlagMap: FeatureFlagMap;
|
||||
|
||||
constructor(
|
||||
queryBuilder: SoftDeleteQueryBuilder<T>,
|
||||
objectRecordsPermissions: ObjectsPermissions,
|
||||
internalContext: WorkspaceInternalContext,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
authContext?: AuthContext,
|
||||
featureFlagMap?: FeatureFlagMap,
|
||||
authContext: AuthContext,
|
||||
featureFlagMap: FeatureFlagMap,
|
||||
) {
|
||||
super(queryBuilder);
|
||||
this.objectRecordsPermissions = objectRecordsPermissions;
|
||||
@@ -62,6 +63,7 @@ export class WorkspaceSoftDeleteQueryBuilder<
|
||||
this.internalContext,
|
||||
this.shouldBypassPermissionChecks,
|
||||
this.authContext,
|
||||
this.featureFlagMap,
|
||||
) as this;
|
||||
}
|
||||
|
||||
@@ -81,26 +83,21 @@ export class WorkspaceSoftDeleteQueryBuilder<
|
||||
this.internalContext,
|
||||
);
|
||||
|
||||
const eventSelectQueryBuilder = new WorkspaceSelectQueryBuilder(
|
||||
this as unknown as WorkspaceSelectQueryBuilder<T>,
|
||||
this.objectRecordsPermissions,
|
||||
this.internalContext,
|
||||
true,
|
||||
this.authContext,
|
||||
this.featureFlagMap,
|
||||
);
|
||||
const beforeEventSelectQueryBuilder = computeEventSelectQueryBuilder<T>({
|
||||
queryBuilder: this,
|
||||
authContext: this.authContext,
|
||||
internalContext: this.internalContext,
|
||||
featureFlagMap: this.featureFlagMap,
|
||||
expressionMap: this.expressionMap,
|
||||
objectRecordsPermissions: this.objectRecordsPermissions,
|
||||
});
|
||||
|
||||
const tableName = computeTableName(
|
||||
objectMetadata.nameSingular,
|
||||
objectMetadata.isCustom,
|
||||
);
|
||||
|
||||
eventSelectQueryBuilder.expressionMap.wheres = this.expressionMap.wheres;
|
||||
eventSelectQueryBuilder.expressionMap.aliases =
|
||||
this.expressionMap.aliases;
|
||||
eventSelectQueryBuilder.setParameters(this.getParameters());
|
||||
|
||||
const before = await eventSelectQueryBuilder.getMany();
|
||||
const before = await beforeEventSelectQueryBuilder.getMany();
|
||||
|
||||
this.expressionMap.wheres = applyTableAliasOnWhereCondition({
|
||||
condition: this.expressionMap.wheres,
|
||||
|
||||
+36
-29
@@ -1,9 +1,11 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
UpdateQueryBuilder,
|
||||
type EntityTarget,
|
||||
type ObjectLiteral,
|
||||
UpdateQueryBuilder,
|
||||
type UpdateResult,
|
||||
} from 'typeorm';
|
||||
import { type QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
@@ -28,6 +30,7 @@ import { type WorkspaceDeleteQueryBuilder } from 'src/engine/twenty-orm/reposito
|
||||
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
import { type WorkspaceSoftDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-soft-delete-query-builder';
|
||||
import { applyTableAliasOnWhereCondition } from 'src/engine/twenty-orm/utils/apply-table-alias-on-where-condition';
|
||||
import { computeEventSelectQueryBuilder } from 'src/engine/twenty-orm/utils/compute-event-select-query-builder.util';
|
||||
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
|
||||
@@ -40,8 +43,8 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
private objectRecordsPermissions: ObjectsPermissions;
|
||||
private shouldBypassPermissionChecks: boolean;
|
||||
private internalContext: WorkspaceInternalContext;
|
||||
private authContext?: AuthContext;
|
||||
private featureFlagMap?: FeatureFlagMap;
|
||||
private authContext: AuthContext;
|
||||
private featureFlagMap: FeatureFlagMap;
|
||||
private relationNestedQueries: RelationNestedQueries;
|
||||
private relationNestedConfig:
|
||||
| [RelationConnectQueryConfig[], RelationDisconnectQueryFieldsByEntityIndex]
|
||||
@@ -56,8 +59,8 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
objectRecordsPermissions: ObjectsPermissions,
|
||||
internalContext: WorkspaceInternalContext,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
authContext?: AuthContext,
|
||||
featureFlagMap?: FeatureFlagMap,
|
||||
authContext: AuthContext,
|
||||
featureFlagMap: FeatureFlagMap,
|
||||
) {
|
||||
super(queryBuilder);
|
||||
this.objectRecordsPermissions = objectRecordsPermissions;
|
||||
@@ -103,27 +106,32 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
this.internalContext,
|
||||
);
|
||||
|
||||
const eventSelectQueryBuilder = new WorkspaceSelectQueryBuilder(
|
||||
this as unknown as WorkspaceSelectQueryBuilder<T>,
|
||||
this.objectRecordsPermissions,
|
||||
this.internalContext,
|
||||
true,
|
||||
this.authContext,
|
||||
this.featureFlagMap,
|
||||
);
|
||||
const eventSelectQueryBuilder = computeEventSelectQueryBuilder<T>({
|
||||
queryBuilder: this,
|
||||
authContext: this.authContext,
|
||||
internalContext: this.internalContext,
|
||||
featureFlagMap: this.featureFlagMap,
|
||||
expressionMap: this.expressionMap,
|
||||
objectRecordsPermissions: this.objectRecordsPermissions,
|
||||
});
|
||||
|
||||
const tableName = computeTableName(
|
||||
objectMetadata.nameSingular,
|
||||
objectMetadata.isCustom,
|
||||
);
|
||||
|
||||
eventSelectQueryBuilder.expressionMap.wheres = this.expressionMap.wheres;
|
||||
eventSelectQueryBuilder.expressionMap.aliases =
|
||||
this.expressionMap.aliases;
|
||||
eventSelectQueryBuilder.setParameters(this.getParameters());
|
||||
|
||||
const before = await eventSelectQueryBuilder.getMany();
|
||||
|
||||
if (before.length > QUERY_MAX_RECORDS) {
|
||||
throw new TwentyORMException(
|
||||
`Cannot update more than ${QUERY_MAX_RECORDS} records at once`,
|
||||
TwentyORMExceptionCode.TOO_MANY_RECORDS_TO_UPDATE,
|
||||
{
|
||||
userFriendlyMessage: msg`You can only update up to ${QUERY_MAX_RECORDS} records at once.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
this.expressionMap.wheres = applyTableAliasOnWhereCondition({
|
||||
condition: this.expressionMap.wheres,
|
||||
tableName,
|
||||
@@ -136,6 +144,7 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
this.internalContext,
|
||||
this.shouldBypassPermissionChecks,
|
||||
this.authContext,
|
||||
this.featureFlagMap,
|
||||
);
|
||||
|
||||
if (isDefined(this.relationNestedConfig)) {
|
||||
@@ -238,21 +247,18 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
this.internalContext,
|
||||
);
|
||||
|
||||
const eventSelectQueryBuilder = new WorkspaceSelectQueryBuilder(
|
||||
this as unknown as WorkspaceSelectQueryBuilder<T>,
|
||||
this.objectRecordsPermissions,
|
||||
this.internalContext,
|
||||
true,
|
||||
this.authContext,
|
||||
this.featureFlagMap,
|
||||
);
|
||||
const eventSelectQueryBuilder = computeEventSelectQueryBuilder<T>({
|
||||
queryBuilder: this,
|
||||
authContext: this.authContext,
|
||||
internalContext: this.internalContext,
|
||||
featureFlagMap: this.featureFlagMap,
|
||||
expressionMap: this.expressionMap,
|
||||
objectRecordsPermissions: this.objectRecordsPermissions,
|
||||
});
|
||||
|
||||
eventSelectQueryBuilder.whereInIds(
|
||||
this.manyInputs.map((input) => input.criteria),
|
||||
);
|
||||
eventSelectQueryBuilder.expressionMap.aliases =
|
||||
this.expressionMap.aliases;
|
||||
eventSelectQueryBuilder.setParameters(this.getParameters());
|
||||
|
||||
const beforeRecords = await eventSelectQueryBuilder.getMany();
|
||||
|
||||
@@ -270,6 +276,7 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
this.internalContext,
|
||||
this.shouldBypassPermissionChecks,
|
||||
this.authContext,
|
||||
this.featureFlagMap,
|
||||
);
|
||||
|
||||
this.relationNestedConfig =
|
||||
|
||||
@@ -81,7 +81,7 @@ export class WorkspaceRepository<
|
||||
this.objectRecordsPermissions,
|
||||
this.internalContext,
|
||||
this.shouldBypassPermissionChecks,
|
||||
this.authContext,
|
||||
this.authContext ?? {},
|
||||
this.featureFlagMap,
|
||||
);
|
||||
}
|
||||
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||
import {
|
||||
TwentyORMException,
|
||||
TwentyORMExceptionCode,
|
||||
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
|
||||
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
|
||||
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
|
||||
import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock';
|
||||
|
||||
describe('formatTwentyOrmEventToDatabaseBatchEvent', () => {
|
||||
const objectMetadataItemWithFieldMaps =
|
||||
getMockObjectMetadataItemWithFieldsMaps({
|
||||
id: 'object-id',
|
||||
workspaceId: 'workspace-id',
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
indexMetadatas: [],
|
||||
fieldIdByJoinColumnName: {},
|
||||
fieldIdByName: {
|
||||
name: 'name-id',
|
||||
age: 'age-id',
|
||||
fullName: 'fullname-id',
|
||||
},
|
||||
fieldsById: {
|
||||
'name-id': getMockFieldMetadataEntity({
|
||||
workspaceId: 'workspace-id',
|
||||
objectMetadataId: 'object-id',
|
||||
id: 'name-id',
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
isLabelSyncedWithName: true,
|
||||
isNullable: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const mockWorkspaceId = 'workspace-id';
|
||||
const mockAuthContext = {
|
||||
user: { id: 'user-id' },
|
||||
workspaceMemberId: 'workspace-member-id',
|
||||
} as any;
|
||||
|
||||
describe('UPDATED action', () => {
|
||||
it('should throw TwentyORMException when no matching before entity is found in array of beforeEntities', () => {
|
||||
const afterEntities = [
|
||||
{
|
||||
id: 'record-1',
|
||||
name: 'John Doe Updated',
|
||||
},
|
||||
{
|
||||
id: 'record-2',
|
||||
name: 'Jane Doe Updated',
|
||||
},
|
||||
];
|
||||
|
||||
const beforeEntities = [
|
||||
{
|
||||
id: 'record-1',
|
||||
name: 'John Doe',
|
||||
},
|
||||
{
|
||||
id: 'record-3',
|
||||
name: 'Bob Smith',
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
formatTwentyOrmEventToDatabaseBatchEvent({
|
||||
action: DatabaseEventAction.UPDATED,
|
||||
objectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
workspaceId: mockWorkspaceId,
|
||||
authContext: mockAuthContext,
|
||||
entities: afterEntities,
|
||||
beforeEntities: beforeEntities,
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TwentyORMException);
|
||||
expect((error as TwentyORMException).code).toBe(
|
||||
TwentyORMExceptionCode.ORM_EVENT_DATA_CORRUPTED,
|
||||
);
|
||||
expect((error as TwentyORMException).message).toBe(
|
||||
'Record mismatch detected while computing event data for UPDATED action',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should successfully create update events when matching before entities are found and in the right order', () => {
|
||||
const afterEntities = [
|
||||
{
|
||||
id: 'record-1',
|
||||
name: 'John Doe Updated',
|
||||
},
|
||||
{
|
||||
id: 'record-2',
|
||||
name: 'Jane Doe Updated',
|
||||
},
|
||||
];
|
||||
|
||||
const beforeEntities = [
|
||||
{
|
||||
id: 'record-2',
|
||||
name: 'Jane Doe',
|
||||
},
|
||||
{
|
||||
id: 'record-1',
|
||||
name: 'John Doe',
|
||||
},
|
||||
];
|
||||
|
||||
const result = formatTwentyOrmEventToDatabaseBatchEvent({
|
||||
action: DatabaseEventAction.UPDATED,
|
||||
objectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
workspaceId: mockWorkspaceId,
|
||||
authContext: mockAuthContext,
|
||||
entities: afterEntities,
|
||||
beforeEntities: beforeEntities,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.action).toBe(DatabaseEventAction.UPDATED);
|
||||
expect(result?.events).toHaveLength(2);
|
||||
|
||||
const updateEvent1 = result?.events[0] as ObjectRecordUpdateEvent<
|
||||
(typeof afterEntities)[0]
|
||||
>;
|
||||
const updateEvent2 = result?.events[1] as ObjectRecordUpdateEvent<
|
||||
(typeof afterEntities)[1]
|
||||
>;
|
||||
|
||||
expect(updateEvent1.recordId).toBe('record-1');
|
||||
expect(updateEvent2.recordId).toBe('record-2');
|
||||
|
||||
expect(updateEvent1.properties?.before?.name).toBe('John Doe');
|
||||
expect(updateEvent1.properties?.after?.name).toBe('John Doe Updated');
|
||||
expect(updateEvent2.properties?.before?.name).toBe('Jane Doe');
|
||||
expect(updateEvent2.properties?.after?.name).toBe('Jane Doe Updated');
|
||||
});
|
||||
|
||||
it('should handle single entity (non-array) for both before and after', () => {
|
||||
const afterEntity = {
|
||||
id: 'record-1',
|
||||
name: 'John Doe Updated',
|
||||
};
|
||||
|
||||
const beforeEntity = {
|
||||
id: 'record-1',
|
||||
name: 'John Doe',
|
||||
};
|
||||
|
||||
const result = formatTwentyOrmEventToDatabaseBatchEvent({
|
||||
action: DatabaseEventAction.UPDATED,
|
||||
objectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
workspaceId: mockWorkspaceId,
|
||||
authContext: mockAuthContext,
|
||||
entities: afterEntity,
|
||||
beforeEntities: beforeEntity,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.action).toBe(DatabaseEventAction.UPDATED);
|
||||
expect(result?.events).toHaveLength(1);
|
||||
expect(result?.events[0].recordId).toBe('record-1');
|
||||
});
|
||||
});
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
import { type ObjectLiteral } from 'typeorm';
|
||||
import { type QueryExpressionMap } from 'typeorm/query-builder/QueryExpressionMap';
|
||||
|
||||
import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||
import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
||||
|
||||
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { type WorkspaceDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-delete-query-builder';
|
||||
import { type WorkspaceInsertQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-insert-query-builder';
|
||||
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
import { type WorkspaceSoftDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-soft-delete-query-builder';
|
||||
import { type WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder';
|
||||
|
||||
type EventSelectQueryBuilderFactoryArgs<T extends ObjectLiteral> = {
|
||||
queryBuilder:
|
||||
| WorkspaceUpdateQueryBuilder<T>
|
||||
| WorkspaceSoftDeleteQueryBuilder<T>
|
||||
| WorkspaceDeleteQueryBuilder<T>
|
||||
| WorkspaceInsertQueryBuilder<T>;
|
||||
authContext: AuthContext;
|
||||
internalContext: WorkspaceInternalContext;
|
||||
featureFlagMap: FeatureFlagMap;
|
||||
expressionMap: QueryExpressionMap;
|
||||
objectRecordsPermissions: ObjectsPermissions;
|
||||
};
|
||||
|
||||
export const computeEventSelectQueryBuilder = <T extends ObjectLiteral>({
|
||||
queryBuilder,
|
||||
authContext,
|
||||
featureFlagMap,
|
||||
internalContext,
|
||||
expressionMap,
|
||||
objectRecordsPermissions,
|
||||
}: EventSelectQueryBuilderFactoryArgs<T>): WorkspaceSelectQueryBuilder<T> => {
|
||||
const eventSelectQueryBuilder = new WorkspaceSelectQueryBuilder(
|
||||
queryBuilder as unknown as WorkspaceSelectQueryBuilder<T>,
|
||||
objectRecordsPermissions,
|
||||
internalContext,
|
||||
true,
|
||||
authContext,
|
||||
featureFlagMap,
|
||||
);
|
||||
|
||||
eventSelectQueryBuilder.expressionMap.wheres = expressionMap.wheres;
|
||||
eventSelectQueryBuilder.expressionMap.aliases = expressionMap.aliases;
|
||||
eventSelectQueryBuilder.setParameters(expressionMap.parameters);
|
||||
|
||||
return eventSelectQueryBuilder;
|
||||
};
|
||||
+16
-5
@@ -1,5 +1,5 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import type { ObjectLiteral } from 'typeorm';
|
||||
|
||||
@@ -13,6 +13,10 @@ import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/t
|
||||
import { ObjectRecordUpsertEvent } from 'src/engine/core-modules/event-emitter/types/object-record-upsert.event';
|
||||
import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values';
|
||||
import type { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import {
|
||||
TwentyORMException,
|
||||
TwentyORMExceptionCode,
|
||||
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
|
||||
import { type DatabaseBatchEventInput } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
|
||||
export const formatTwentyOrmEventToDatabaseBatchEvent = <
|
||||
@@ -65,15 +69,22 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = <
|
||||
break;
|
||||
case DatabaseEventAction.UPDATED:
|
||||
events = entityArray
|
||||
.map((after, idx) => {
|
||||
.map((after) => {
|
||||
if (!beforeEntities) {
|
||||
throw new Error('beforeEntities is required for UPDATED action');
|
||||
}
|
||||
|
||||
const before = Array.isArray(beforeEntities)
|
||||
? beforeEntities?.[idx]
|
||||
? beforeEntities.find((before) => before.id === after.id)
|
||||
: beforeEntities;
|
||||
|
||||
if (!isDefined(before)) {
|
||||
throw new TwentyORMException(
|
||||
'Record mismatch detected while computing event data for UPDATED action',
|
||||
TwentyORMExceptionCode.ORM_EVENT_DATA_CORRUPTED,
|
||||
);
|
||||
}
|
||||
|
||||
const diff = objectRecordChangedValues(
|
||||
before,
|
||||
after,
|
||||
@@ -127,7 +138,7 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = <
|
||||
});
|
||||
break;
|
||||
case DatabaseEventAction.UPSERTED:
|
||||
events = entityArray.map((after, index) => {
|
||||
events = entityArray.map((after) => {
|
||||
const event = new ObjectRecordUpsertEvent<T>();
|
||||
|
||||
event.userId = authContext?.user?.id;
|
||||
@@ -136,7 +147,7 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = <
|
||||
|
||||
const before = beforeEntities
|
||||
? Array.isArray(beforeEntities)
|
||||
? beforeEntities[index]
|
||||
? beforeEntities.find((before) => before.id === after.id)
|
||||
: beforeEntities
|
||||
: undefined;
|
||||
|
||||
|
||||
+1
@@ -13,6 +13,7 @@ export const twentyORMGraphqlApiExceptionHandler = (
|
||||
case TwentyORMExceptionCode.CONNECT_RECORD_NOT_FOUND:
|
||||
case TwentyORMExceptionCode.CONNECT_NOT_ALLOWED:
|
||||
case TwentyORMExceptionCode.CONNECT_UNIQUE_CONSTRAINT_ERROR:
|
||||
case TwentyORMExceptionCode.TOO_MANY_RECORDS_TO_UPDATE:
|
||||
throw new UserInputError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user