diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts index e3045e4c2e..7f182c6414 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts @@ -1,6 +1,9 @@ -import { FieldMetadataType } from 'twenty-shared/types'; +import { FieldMetadataType, RelationType } from 'twenty-shared/types'; -import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values'; +import { + computeUpdatedFieldsFromDiff, + objectRecordChangedValues, +} from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values'; import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @@ -198,4 +201,110 @@ describe('objectRecordChangedValues', () => { }); expect(result).not.toHaveProperty('position'); }); + + describe('with a MANY_TO_ONE relation field', () => { + const relationFieldId = 'company-field-id'; + const relationUniversalId = 'company-universal-id'; + + const objectMetadataWithRelation: FlatObjectMetadata = { + ...mockObjectMetadata, + fieldIds: [relationFieldId], + }; + + const flatFieldMetadataMapsWithRelation: FlatEntityMaps = + { + byUniversalIdentifier: { + [relationUniversalId]: { + id: relationFieldId, + name: 'company', + type: FieldMetadataType.RELATION, + universalIdentifier: relationUniversalId, + settings: { + relationType: RelationType.MANY_TO_ONE, + joinColumnName: 'companyId', + }, + } as FlatFieldMetadata, + }, + universalIdentifierById: { + [relationFieldId]: relationUniversalId, + }, + universalIdentifiersByApplicationId: {}, + }; + + it('records join column changes under the relation field name', () => { + const oldRecord = { + id: '74316f58-29b0-4a6a-b8fa-d2b506d5516p', + companyId: 'old-company-id', + }; + const newRecord = { + id: '74316f58-29b0-4a6a-b8fa-d2b506d5516p', + companyId: 'new-company-id', + }; + + const result = objectRecordChangedValues( + oldRecord, + newRecord, + objectMetadataWithRelation, + flatFieldMetadataMapsWithRelation, + ); + + expect(result).toEqual({ + company: { + before: { id: 'old-company-id' }, + after: { id: 'new-company-id' }, + }, + }); + }); + + it('computes updatedFields with both relation field name and join column name', () => { + const diff = { + company: { + before: { id: 'old-company-id' }, + after: { id: 'new-company-id' }, + }, + name: { before: 'Original', after: 'Updated' }, + }; + + const updatedFields = computeUpdatedFieldsFromDiff( + diff, + objectMetadataWithRelation, + flatFieldMetadataMapsWithRelation, + ); + + expect(updatedFields).toEqual(['company', 'companyId', 'name']); + }); + + it('uses the canonical computed join column name even when settings diverge', () => { + const flatFieldMetadataMapsWithDivergentJoinColumn: FlatEntityMaps = + { + ...flatFieldMetadataMapsWithRelation, + byUniversalIdentifier: { + [relationUniversalId]: { + ...flatFieldMetadataMapsWithRelation.byUniversalIdentifier[ + relationUniversalId + ], + settings: { + relationType: RelationType.MANY_TO_ONE, + joinColumnName: 'legacyCompanyId', + }, + } as FlatFieldMetadata, + }, + }; + + const diff = { + company: { + before: { id: 'old-company-id' }, + after: { id: 'new-company-id' }, + }, + }; + + const updatedFields = computeUpdatedFieldsFromDiff( + diff, + objectMetadataWithRelation, + flatFieldMetadataMapsWithDivergentJoinColumn, + ); + + expect(updatedFields).toEqual(['company', 'companyId']); + }); + }); }); diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts index e5f59e4b27..37f25b12bf 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts @@ -44,6 +44,39 @@ const getJoinColumnNameForRelationField = ( ); }; +export const computeUpdatedFieldsFromDiff = ( + diff: Record, + objectMetadataItem: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, +): string[] => { + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + objectMetadataItem, + ); + + return Object.keys(diff).flatMap((diffKey) => { + const fieldId = fieldIdByName[diffKey]; + + if (!isDefined(fieldId)) { + return [diffKey]; + } + + const field = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: fieldId, + flatEntityMaps: flatFieldMetadataMaps, + }); + + if (isDefined(field) && isManyToOneRelationField(field)) { + return [ + diffKey, + computeMorphOrRelationFieldJoinColumnName({ name: field.name }), + ]; + } + + return [diffKey]; + }); +}; + export const objectRecordChangedValues = ( oldRecord: Partial, newRecord: Partial, diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-twenty-orm-event-to-database-batch-event.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-twenty-orm-event-to-database-batch-event.util.spec.ts index 99bd7ade01..fdc5bdfb54 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-twenty-orm-event-to-database-batch-event.util.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-twenty-orm-event-to-database-batch-event.util.spec.ts @@ -1,5 +1,5 @@ import { type ObjectRecordUpdateEvent } from 'twenty-shared/database-events'; -import { FieldMetadataType } from 'twenty-shared/types'; +import { FieldMetadataType, RelationType } from 'twenty-shared/types'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; @@ -181,5 +181,62 @@ describe('formatTwentyOrmEventToDatabaseBatchEvent', () => { expect(updateEvent2.properties?.before?.name).toBe('Jane Doe'); expect(updateEvent2.properties?.after?.name).toBe('Jane Doe Updated'); }); + + it('should include both relation field name and join column name in updatedFields', () => { + const companyField = createMockField({ + id: 'company-id', + type: FieldMetadataType.RELATION, + name: 'company', + label: 'Company', + settings: { + relationType: RelationType.MANY_TO_ONE, + joinColumnName: 'companyId', + }, + } as Parameters[0]); + + const flatFieldMetadataMapsWithRelation: FlatEntityMaps = + { + byUniversalIdentifier: { + 'name-id': nameField, + 'company-id': companyField, + }, + universalIdentifierById: { + 'name-id': 'name-id', + 'company-id': 'company-id', + }, + universalIdentifiersByApplicationId: {}, + }; + + const flatObjectMetadataWithRelation = { + ...flatObjectMetadata, + fieldIds: ['name-id', 'company-id'], + } as FlatObjectMetadata; + + const result = formatTwentyOrmEventToDatabaseBatchEvent({ + action: DatabaseEventAction.UPDATED, + objectMetadataItem: flatObjectMetadataWithRelation, + flatFieldMetadataMaps: flatFieldMetadataMapsWithRelation, + workspaceId: mockWorkspaceId, + authContext: mockAuthContext, + recordsAfter: [{ id: 'record-1', companyId: 'new-company-id' }], + recordsBefore: [{ id: 'record-1', companyId: 'old-company-id' }], + }); + + const updateEvent = result?.events[0] as ObjectRecordUpdateEvent<{ + id: string; + companyId: string; + }>; + + expect(updateEvent.properties?.updatedFields).toEqual([ + 'company', + 'companyId', + ]); + expect(updateEvent.properties?.diff).toEqual({ + company: { + before: { id: 'old-company-id' }, + after: { id: 'new-company-id' }, + }, + }); + }); }); }); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util.ts index 080f5b6061..6a0c1b7580 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util.ts @@ -17,7 +17,10 @@ import type { ObjectLiteral } from 'typeorm'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { type RawAuthContext } from 'src/engine/core-modules/auth/types/raw-auth-context.type'; -import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values'; +import { + computeUpdatedFieldsFromDiff, + objectRecordChangedValues, +} from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values'; import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import type { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @@ -128,7 +131,11 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = < flatFieldMetadataMaps, ) as Partial>; - const updatedFields = Object.keys(diff); + const updatedFields = computeUpdatedFieldsFromDiff( + diff, + objectMetadataItem, + flatFieldMetadataMaps, + ); if (updatedFields.length === 0) { return; @@ -226,7 +233,11 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = < flatFieldMetadataMaps, ) as Partial>; - updatedFields = Object.keys(diff); + updatedFields = computeUpdatedFieldsFromDiff( + diff, + objectMetadataItem, + flatFieldMetadataMaps, + ); event.properties = { after: recordAfter,