fix(server): include relation join column names in updatedFields of update events (#21405)
## Context Since #21052, update-event diffs are keyed by the relation field name (e.g. `company`) instead of the join column name (e.g. `companyId`). `updatedFields` is derived from the diff keys, so any **workflow database-event trigger** (or webhook) configured with a field filter on a relation join column **silently stopped firing** — no run is created at all. We hit this in production: a `cloudWorkspace.updated` trigger filtered on `twentyContactId` stopped creating runs the same day #21052 was deployed. Updating the record's relation produced `updatedFields: ["twentyContact"]`, which no longer matches the stored settings `fields: ["twentyContactId"]` in `WorkflowDatabaseEventTriggerListener.shouldTriggerJob`. ## Solution Keep the diff keyed by relation field name (the timeline rendering from #21052 relies on it — adding both keys to the diff would display relation changes twice), but expose **both** the relation field name and its join column name in `updatedFields`: - New `computeUpdatedFieldsFromDiff()` in `object-record-changed-values.ts`: expands MANY_TO_ONE relation diff keys with their join column name. - Used in `formatTwentyOrmEventToDatabaseBatchEvent` for UPDATED/DELETED/RESTORED and UPSERTED events instead of `Object.keys(diff)`. This restores matching for pre-existing trigger/webhook configurations (join column names) while keeping configurations using relation field names working. ## Test plan - [x] Unit tests: relation diff keyed by relation name; `updatedFields` contains both `company` and `companyId` - [x] End-to-end util test on UPDATED event: `updatedFields: ['company', 'companyId']`, diff keyed by `company` - [x] Downstream consumer specs pass (workflow trigger listener, webhooks, subscriptions, logic-function triggers) - [x] Verified against the production workspace that a `twentyContactId` update currently produces no workflow run with the old behavior
This commit is contained in:
+58
-1
@@ -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<typeof createMockField>[0]);
|
||||
|
||||
const flatFieldMetadataMapsWithRelation: FlatEntityMaps<FlatFieldMetadata> =
|
||||
{
|
||||
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' },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+14
-3
@@ -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<ObjectRecordDiff<T>>;
|
||||
|
||||
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<ObjectRecordDiff<T>>;
|
||||
|
||||
updatedFields = Object.keys(diff);
|
||||
updatedFields = computeUpdatedFieldsFromDiff(
|
||||
diff,
|
||||
objectMetadataItem,
|
||||
flatFieldMetadataMaps,
|
||||
);
|
||||
|
||||
event.properties = {
|
||||
after: recordAfter,
|
||||
|
||||
Reference in New Issue
Block a user