Fix phantom targetId field in workflow triggers for morph relations (#23290)
## Problem
In a workflow **record-change trigger** on `noteTarget`, the output
variables offered a `targetId` field that doesn't exist. A morph
relation reaches the frontend as a single field named `target` (the
per-target morph fields are grouped by `morphId`), so the output-schema
generators synthesized its foreign-key column as `` `${field.name}Id` ``
→ `targetId`. But `noteTarget` has no `targetId` column; its FKs are one
per target type: `targetCompanyId`, `targetPersonId`,
`targetOpportunityId`, etc. The phantom `targetId` never matched
anything in the event payload.
## Fix
New helper `getRelationIdFieldNames` returns the actual FK id column(s)
for a relation field:
- normal relation → `[`${name}Id`]`
- morph relation → one column per `morphRelations` target, via the
existing `computeMorphRelationGqlFieldJoinColumnName`
(`targetCompanyId`, `targetPersonId`, ...).
Used by the two output-schema generators:
- `generateRecordEventOutputSchema` — the record-change trigger output
variables (the reported symptom).
- `generateRecordOutputSchema` — record output for
form/find/update-record output schemas.
Scoped strictly to the output schema; no workflow component changes.
## Testing
- Unit tests updated to assert per-target columns instead of the phantom
`targetId` (both generators). 28 passing.
This commit is contained in:
+27
-3
@@ -261,7 +261,7 @@ describe('generateRecordEventOutputSchema', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert MORPH_RELATION fields to prefixed UUID id fields when MANY_TO_ONE', () => {
|
||||
it('should expand MORPH_RELATION fields into one prefixed UUID id field per target when MANY_TO_ONE', () => {
|
||||
const objectMetadataItem = createMockObjectMetadataItem({
|
||||
fields: [
|
||||
{
|
||||
@@ -275,6 +275,22 @@ describe('generateRecordEventOutputSchema', () => {
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
},
|
||||
morphRelations: [
|
||||
{
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
targetObjectMetadata: {
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
targetObjectMetadata: {
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as any,
|
||||
});
|
||||
@@ -284,8 +300,16 @@ describe('generateRecordEventOutputSchema', () => {
|
||||
DatabaseEventAction.CREATED,
|
||||
);
|
||||
|
||||
expect(Object.keys(result.fields)).toContain('properties.after.targetId');
|
||||
expect(result.fields['properties.after.targetId']).toMatchObject({
|
||||
expect(Object.keys(result.fields)).toContain(
|
||||
'properties.after.targetCompanyId',
|
||||
);
|
||||
expect(Object.keys(result.fields)).toContain(
|
||||
'properties.after.targetPersonId',
|
||||
);
|
||||
expect(Object.keys(result.fields)).not.toContain(
|
||||
'properties.after.targetId',
|
||||
);
|
||||
expect(result.fields['properties.after.targetCompanyId']).toMatchObject({
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.UUID,
|
||||
});
|
||||
|
||||
+22
-4
@@ -233,7 +233,7 @@ describe('generateRecordOutputSchema', () => {
|
||||
expect(result.object.icon).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should convert MORPH_RELATION fields to UUID id fields when MANY_TO_ONE', () => {
|
||||
it('should expand MORPH_RELATION fields into one UUID id field per target when MANY_TO_ONE', () => {
|
||||
const objectMetadataItem = createMockObjectMetadataItem({
|
||||
fields: [
|
||||
{
|
||||
@@ -247,6 +247,22 @@ describe('generateRecordOutputSchema', () => {
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
},
|
||||
morphRelations: [
|
||||
{
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
targetObjectMetadata: {
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
targetObjectMetadata: {
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as any,
|
||||
});
|
||||
@@ -254,11 +270,13 @@ describe('generateRecordOutputSchema', () => {
|
||||
const result = generateRecordOutputSchema(objectMetadataItem);
|
||||
|
||||
expect(result.fields).not.toHaveProperty('target');
|
||||
expect(result.fields).toHaveProperty('targetId');
|
||||
expect(result.fields.targetId).toMatchObject({
|
||||
expect(result.fields).not.toHaveProperty('targetId');
|
||||
expect(result.fields).toHaveProperty('targetCompanyId');
|
||||
expect(result.fields).toHaveProperty('targetPersonId');
|
||||
expect(result.fields.targetCompanyId).toMatchObject({
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: 'Target Id',
|
||||
label: 'Target Company Id',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+14
-12
@@ -1,4 +1,5 @@
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { getRelationIdFieldNames } from '@/object-metadata/utils/getRelationIdFieldNames';
|
||||
import { type DatabaseEventTriggerOutputSchema } from '@/workflow/workflow-variables/types/DatabaseEventTriggerOutputSchema';
|
||||
import {
|
||||
type FieldOutputSchemaV2,
|
||||
@@ -121,18 +122,19 @@ const generatePrefixedRecordFields = (
|
||||
fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION;
|
||||
|
||||
if (isRelationField) {
|
||||
const relationIdFieldName = `${fieldMetadataItem.name}Id`;
|
||||
const relationIdFieldLabel = camelToTitleCase(relationIdFieldName);
|
||||
|
||||
result[`${prefix}.${relationIdFieldName}`] = {
|
||||
isLeaf: true,
|
||||
icon: fieldMetadataItem.icon ?? undefined,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: relationIdFieldLabel,
|
||||
value: generateFakeValue(FieldMetadataType.UUID, 'FieldMetadataType'),
|
||||
fieldMetadataId: fieldMetadataItem.id,
|
||||
isCompositeSubField: false,
|
||||
};
|
||||
for (const relationIdFieldName of getRelationIdFieldNames(
|
||||
fieldMetadataItem,
|
||||
)) {
|
||||
result[`${prefix}.${relationIdFieldName}`] = {
|
||||
isLeaf: true,
|
||||
icon: fieldMetadataItem.icon ?? undefined,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: camelToTitleCase(relationIdFieldName),
|
||||
value: generateFakeValue(FieldMetadataType.UUID, 'FieldMetadataType'),
|
||||
fieldMetadataId: fieldMetadataItem.id,
|
||||
isCompositeSubField: false,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
Object.assign(
|
||||
result,
|
||||
|
||||
+14
-12
@@ -1,5 +1,6 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { getRelationIdFieldNames } from '@/object-metadata/utils/getRelationIdFieldNames';
|
||||
import {
|
||||
type FieldOutputSchemaV2,
|
||||
type RecordFieldLeaf,
|
||||
@@ -112,18 +113,19 @@ const generateRecordFields = (
|
||||
fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION;
|
||||
|
||||
if (isRelationField) {
|
||||
const relationIdFieldName = `${fieldMetadataItem.name}Id`;
|
||||
const relationIdFieldLabel = camelToTitleCase(relationIdFieldName);
|
||||
|
||||
result[relationIdFieldName] = {
|
||||
isLeaf: true,
|
||||
icon: fieldMetadataItem.icon ?? undefined,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: relationIdFieldLabel,
|
||||
value: generateFakeValue(FieldMetadataType.UUID, 'FieldMetadataType'),
|
||||
fieldMetadataId: fieldMetadataItem.id,
|
||||
isCompositeSubField: false,
|
||||
};
|
||||
for (const relationIdFieldName of getRelationIdFieldNames(
|
||||
fieldMetadataItem,
|
||||
)) {
|
||||
result[relationIdFieldName] = {
|
||||
isLeaf: true,
|
||||
icon: fieldMetadataItem.icon ?? undefined,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: camelToTitleCase(relationIdFieldName),
|
||||
value: generateFakeValue(FieldMetadataType.UUID, 'FieldMetadataType'),
|
||||
fieldMetadataId: fieldMetadataItem.id,
|
||||
isCompositeSubField: false,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
result[fieldMetadataItem.name] = generateRecordField(fieldMetadataItem);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user