diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/field-metadata-config-by-field-name.constant.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/field-metadata-config-by-field-name.constant.ts index a07d592f90..991f7640a7 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/field-metadata-config-by-field-name.constant.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/field-metadata-config-by-field-name.constant.ts @@ -161,7 +161,7 @@ export const fieldMetadataConfigByFieldName: Record< isNullable: true, }, [joinColumnNameForManyToOneMorphRelationField1]: { - name: joinColumnNameForManyToOneMorphRelationField1, + name: joinColumnNameForManyToOneMorphRelationField1.replace(/Id$/, ''), type: FieldMetadataType.MORPH_RELATION, isNullable: true, settings: { diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/successful-inputs-by-field-metadata-type.constant.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/successful-inputs-by-field-metadata-type.constant.ts index 89bc9a9f71..82ceadbb7d 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/successful-inputs-by-field-metadata-type.constant.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/successful-inputs-by-field-metadata-type.constant.ts @@ -53,7 +53,9 @@ export const successfulInputsByFieldMetadataType: { }, { input: { [joinColumnNameForManyToOneMorphRelationField1]: null }, - expectedOutput: { [joinColumnNameForManyToOneMorphRelationField1]: null }, + expectedOutput: { + [joinColumnNameForManyToOneMorphRelationField1]: null, + }, }, ], [FieldMetadataType.RAW_JSON]: [ diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/data-arg-processor.service.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/data-arg-processor.service.spec.ts index 121e814194..6d7147ac85 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/data-arg-processor.service.spec.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/data-arg-processor.service.spec.ts @@ -51,7 +51,7 @@ describe('DataArgProcessorService', () => { byUniversalIdentifier[universalId] = { id: fieldId, - name: fieldName, + name: config.name, type: config.type ?? FieldMetadataType.TEXT, isNullable: config.isNullable ?? true, objectMetadataId: 'object-id', diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service.ts index 6fef5e71ad..e67bbeecd6 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service.ts @@ -15,6 +15,7 @@ import { } from 'twenty-shared/utils'; import { transformActorField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-actor-field.util'; +import { isRelationNestedOperation } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-relation-nested-operation.util'; import { transformAddressField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-address-field.util'; import { transformArrayField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-array-field.util'; import { transformCurrencyField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-currency-field.util'; @@ -57,7 +58,9 @@ import { transformRichTextV2Value } from 'src/engine/core-modules/record-transfo import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @@ -224,13 +227,11 @@ export class DataArgProcessorService { } case FieldMetadataType.RELATION: case FieldMetadataType.MORPH_RELATION: { - const fieldMetadataRelationSettings = - fieldMetadata.settings as FieldMetadataSettingsMapping['RELATION']; + const relationSettings = fieldMetadata.settings as + | FieldMetadataSettingsMapping['RELATION'] + | FieldMetadataSettingsMapping['MORPH_RELATION']; - if ( - fieldMetadataRelationSettings.relationType === - RelationType.ONE_TO_MANY - ) { + if (relationSettings.relationType === RelationType.ONE_TO_MANY) { throw new CommonQueryRunnerException( `One-to-many relation ${key} field does not support write operations.`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, @@ -238,10 +239,27 @@ export class DataArgProcessorService { ); } - if (key === fieldMetadataRelationSettings.joinColumnName) { + const joinColumnName = isFlatFieldMetadataOfType( + fieldMetadata, + FieldMetadataType.MORPH_RELATION, + ) + ? computeMorphOrRelationFieldJoinColumnName({ + name: fieldMetadata.name, + }) + : relationSettings.joinColumnName; + + if (key === joinColumnName) { return validateUUIDFieldOrThrow(value, key); } + if (isDefined(joinColumnName) && !isRelationNestedOperation(value)) { + throw new CommonQueryRunnerException( + `Relation "${key}" requires connect or disconnect operation`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, + ); + } + return value; } case FieldMetadataType.PHONES: { diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/utils/is-relation-nested-operation.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/utils/is-relation-nested-operation.util.ts new file mode 100644 index 0000000000..3527d40a9f --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/utils/is-relation-nested-operation.util.ts @@ -0,0 +1,18 @@ +import { isObject } from '@sniptt/guards'; +import { RELATION_NESTED_QUERY_KEYWORDS } from 'twenty-shared/constants'; +import { isDefined } from 'twenty-shared/utils'; + +const { CONNECT, DISCONNECT } = RELATION_NESTED_QUERY_KEYWORDS; + +export const isRelationNestedOperation = (value: unknown): boolean => { + if (!isObject(value)) { + return false; + } + + const obj = value as Record; + + return ( + (CONNECT in obj && isDefined(obj[CONNECT])) || + (DISCONNECT in obj && isDefined(obj[DISCONNECT])) + ); +}; diff --git a/packages/twenty-server/src/engine/api/common/common-select-fields/common-select-fields-helper.ts b/packages/twenty-server/src/engine/api/common/common-select-fields/common-select-fields-helper.ts index 985e4ed791..9d84c5be9f 100644 --- a/packages/twenty-server/src/engine/api/common/common-select-fields/common-select-fields-helper.ts +++ b/packages/twenty-server/src/engine/api/common/common-select-fields/common-select-fields-helper.ts @@ -92,7 +92,10 @@ export class CommonSelectFieldsHelper { flatEntityId: fieldId, }); - if (!isFlatFieldMetadataOfType(flatField, FieldMetadataType.RELATION)) { + if ( + !isFlatFieldMetadataOfType(flatField, FieldMetadataType.RELATION) && + !isFlatFieldMetadataOfType(flatField, FieldMetadataType.MORPH_RELATION) + ) { continue; } diff --git a/packages/twenty-server/src/engine/api/common/common-select-fields/utils/__tests__/get-all-selectable-fields.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-select-fields/utils/__tests__/get-all-selectable-fields.util.spec.ts index 32c2a22da2..963bfc4416 100644 --- a/packages/twenty-server/src/engine/api/common/common-select-fields/utils/__tests__/get-all-selectable-fields.util.spec.ts +++ b/packages/twenty-server/src/engine/api/common/common-select-fields/utils/__tests__/get-all-selectable-fields.util.spec.ts @@ -523,7 +523,7 @@ describe('getAllSelectableFields', () => { }); }); - it('should include MANY_TO_ONE relations without joinColumnName as regular fields', () => { + it('should compute join column name for MANY_TO_ONE relations without joinColumnName in settings', () => { const nameField = createMockField({ id: 'field-1', name: 'name', @@ -555,7 +555,7 @@ describe('getAllSelectableFields', () => { expect(result).toEqual({ name: true, - company: true, // MANY_TO_ONE without joinColumnName are included as regular fields + companyId: true, }); }); }); diff --git a/packages/twenty-server/src/engine/api/common/common-select-fields/utils/get-all-selectable-fields.util.ts b/packages/twenty-server/src/engine/api/common/common-select-fields/utils/get-all-selectable-fields.util.ts index df316e9b6d..d8ab4e345a 100644 --- a/packages/twenty-server/src/engine/api/common/common-select-fields/utils/get-all-selectable-fields.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-select-fields/utils/get-all-selectable-fields.util.ts @@ -8,9 +8,9 @@ import { type RestrictedFieldsPermissions, compositeTypeDefinitions, } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; @@ -80,11 +80,18 @@ export const getAllSelectableFields = ({ result[flatField.name] = compositeFields; } else if ( - isFlatFieldMetadataOfType(flatField, FieldMetadataType.RELATION) && - flatField.settings.relationType === RelationType.MANY_TO_ONE && - isDefined(flatField.settings.joinColumnName) + (isFlatFieldMetadataOfType(flatField, FieldMetadataType.RELATION) || + isFlatFieldMetadataOfType( + flatField, + FieldMetadataType.MORPH_RELATION, + )) && + flatField.settings.relationType === RelationType.MANY_TO_ONE ) { - result[flatField.settings.joinColumnName] = true; + const joinColumnName = computeMorphOrRelationFieldJoinColumnName({ + name: flatField.name, + }); + + result[joinColumnName] = true; } else { result[flatField.name] = true; } diff --git a/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/__tests__/selected-fields-handler.spec.ts b/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/__tests__/selected-fields-handler.spec.ts index ce1d00d531..da8d52fc18 100644 --- a/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/__tests__/selected-fields-handler.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/__tests__/selected-fields-handler.spec.ts @@ -436,11 +436,12 @@ describe('RestToCommonSelectedFieldsHandler', () => { expect(result).toEqual({ name: true, + companyId: true, company: { name: true, people: { name: true, - company: true, // Nested relation at depth 2 shows as boolean + companyId: true, }, }, }); @@ -573,12 +574,12 @@ describe('RestToCommonSelectedFieldsHandler', () => { depth: 1, }); - // For noteTarget, only note and task relations are included - // but company is a MANY_TO_ONE so it's included as a boolean expect(result).toEqual({ + noteId: true, note: { title: true, }, + companyId: true, company: { name: true, }, diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts index 87b5dbfa83..01bbe95b60 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts @@ -3,7 +3,7 @@ import { type FieldMetadataDefaultValue, FieldMetadataType, } from 'twenty-shared/types'; -import { capitalize } from 'twenty-shared/utils'; +import { capitalize, isDefined } from 'twenty-shared/utils'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; @@ -99,43 +99,61 @@ const getSchemaComponentsRelationProperties = ( >['flatObjectMetadataMaps'], ): Properties => { return flatFieldMetadatas.reduce((node, field) => { - if (field.type !== FieldMetadataType.RELATION) { + const isRelationField = + isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) || + isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION); + + if (!isRelationField) { return node; } + if (!isDefined(field.relationTargetObjectMetadataId)) { + throw new Error( + `Relation field "${field.name}" has no relationTargetObjectMetadataId`, + ); + } + + const relationType = field.settings?.relationType; + + if (!isDefined(relationType)) { + throw new Error( + `Relation field "${field.name}" has no relationType in settings`, + ); + } + + const targetObjectMetadata = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: field.relationTargetObjectMetadataId, + flatEntityMaps: flatObjectMetadataMaps, + }); + + if (!targetObjectMetadata) { + throw new Error( + `Relation field "${field.name}" target object metadata not found for id ${field.relationTargetObjectMetadataId}`, + ); + } + let itemProperty = {} as Property; - if (isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION)) { - const targetObjectMetadata = findFlatEntityByIdInFlatEntityMaps({ - flatEntityId: field.relationTargetObjectMetadataId, - flatEntityMaps: flatObjectMetadataMaps, - }); - - if (!targetObjectMetadata) { - return node; - } - - if (field.settings?.relationType === RelationType.MANY_TO_ONE) { - itemProperty = { - type: 'object', - oneOf: [ - { - $ref: `#/components/schemas/${capitalize( - targetObjectMetadata.nameSingular, - )}ForResponse`, - }, - ], - }; - } else if (field.settings?.relationType === RelationType.ONE_TO_MANY) { - itemProperty = { - type: 'array', - items: { + if (relationType === RelationType.MANY_TO_ONE) { + itemProperty = { + type: 'object', + oneOf: [ + { $ref: `#/components/schemas/${capitalize( targetObjectMetadata.nameSingular, )}ForResponse`, }, - }; - } + ], + }; + } else if (relationType === RelationType.ONE_TO_MANY) { + itemProperty = { + type: 'array', + items: { + $ref: `#/components/schemas/${capitalize( + targetObjectMetadata.nameSingular, + )}ForResponse`, + }, + }; } if (field.description) { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util.ts index a9458cbad5..6488a8ce8e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util.ts @@ -1,7 +1,10 @@ +import { FieldMetadataType, RelationType } from 'twenty-shared/types'; + import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; +import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; 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 { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export type FieldMapsForObject = { @@ -24,14 +27,16 @@ export const buildFieldMapsFromFlatObjectMetadata = ( for (const field of objectFields) { fieldIdByName[field.name] = field.id; - if (isMorphOrRelationFlatFieldMetadata(field)) { - const joinColumnName = ( - field.settings as { joinColumnName?: string } | undefined - )?.joinColumnName; + if ( + (isFlatFieldMetadataOfType(field, FieldMetadataType.RELATION) || + isFlatFieldMetadataOfType(field, FieldMetadataType.MORPH_RELATION)) && + field.settings.relationType === RelationType.MANY_TO_ONE + ) { + const joinColumnName = computeMorphOrRelationFieldJoinColumnName({ + name: field.name, + }); - if (joinColumnName) { - fieldIdByJoinColumnName[joinColumnName] = field.id; - } + fieldIdByJoinColumnName[joinColumnName] = field.id; } } diff --git a/packages/twenty-server/src/engine/utils/convert-object-metadata-to-schema-properties.util.ts b/packages/twenty-server/src/engine/utils/convert-object-metadata-to-schema-properties.util.ts index 77a7cd6e8d..04ac52ec5a 100644 --- a/packages/twenty-server/src/engine/utils/convert-object-metadata-to-schema-properties.util.ts +++ b/packages/twenty-server/src/engine/utils/convert-object-metadata-to-schema-properties.util.ts @@ -8,6 +8,7 @@ import { isDefined } from 'twenty-shared/utils'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; @@ -102,13 +103,19 @@ export const convertObjectMetadataToSchemaProperties = ({ return node; } - if ( - isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) && - field.settings?.relationType === RelationType.MANY_TO_ONE - ) { + const isRelationManyToOne = + (isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) || + isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION)) && + field.settings?.relationType === RelationType.MANY_TO_ONE; + + if (isRelationManyToOne) { + const key = computeMorphOrRelationFieldJoinColumnName({ + name: field.name, + }); + return { ...node, - [`${field.name}Id`]: { + [key]: { type: 'string', format: 'uuid', }, @@ -116,7 +123,8 @@ export const convertObjectMetadataToSchemaProperties = ({ } if ( - isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) && + (isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) || + isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION)) && field.settings?.relationType === RelationType.ONE_TO_MANY ) { return node; diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows.ts index 5d16a3e061..3347548344 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows.ts @@ -270,9 +270,7 @@ export const prefillWorkflows = async ( '{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.email}}', additionalEmails: [], }, - company: { - id: '{{0715b6cd-7cc1-4b98-971b-00f54dfe643b.id}}', - }, + companyId: '{{0715b6cd-7cc1-4b98-971b-00f54dfe643b.id}}', }, }, outputSchema: { diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/utils/format-workflow-record-relation-fields.util.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/utils/format-workflow-record-relation-fields.util.ts new file mode 100644 index 0000000000..03ae757176 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/utils/format-workflow-record-relation-fields.util.ts @@ -0,0 +1,72 @@ +import { isObject, isString } from '@sniptt/guards'; +import { FieldMetadataType, RelationType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; +import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { type ObjectMetadataInfo } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; + +const extractLegacyRelationId = (value: unknown): string | undefined => { + if (!isObject(value)) { + return undefined; + } + + const record = value as Record; + + if (Object.keys(record).length !== 1 || !isString(record.id)) { + return undefined; + } + + return record.id; +}; + +export const formatWorkflowRecordRelationFields = ( + record: Record, + objectMetadataInfo: ObjectMetadataInfo, +): Record => { + const { flatObjectMetadata, flatFieldMetadataMaps } = objectMetadataInfo; + + const objectFields = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, + ); + + const manyToOneRelationFieldNames = new Set(); + + for (const field of objectFields) { + if ( + (isFlatFieldMetadataOfType(field, FieldMetadataType.RELATION) || + isFlatFieldMetadataOfType(field, FieldMetadataType.MORPH_RELATION)) && + field.settings.relationType === RelationType.MANY_TO_ONE + ) { + manyToOneRelationFieldNames.add(field.name); + } + } + + const formattedRecord: Record = {}; + + for (const [key, value] of Object.entries(record)) { + if (!manyToOneRelationFieldNames.has(key)) { + formattedRecord[key] = value; + continue; + } + + const legacyId = extractLegacyRelationId(value); + + if (!isDefined(legacyId)) { + formattedRecord[key] = value; + continue; + } + + const joinColumnName = computeMorphOrRelationFieldJoinColumnName({ + name: key, + }); + + if (!isDefined(record[joinColumnName])) { + formattedRecord[joinColumnName] = legacyId; + } + } + + return formattedRecord; +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts index d55a59fc3b..6c0290be2f 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts @@ -11,6 +11,7 @@ import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type'; import { buildWorkflowActorMetadata } from 'src/modules/workflow/workflow-executor/utils/build-workflow-actor-metadata.util'; import { filterValidFieldsInRecord } from 'src/modules/workflow/workflow-executor/utils/filter-valid-fields-in-record.util'; +import { formatWorkflowRecordRelationFields } from 'src/modules/workflow/workflow-executor/utils/format-workflow-record-relation-fields.util'; import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util'; import { resolveRichTextFieldsInRecord } from 'src/modules/workflow/workflow-executor/utils/resolve-rich-text-fields-in-record.util'; import { type WorkflowCreateRecordActionInput } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type'; @@ -58,8 +59,13 @@ export class CreateRecordWorkflowAction implements WorkflowAction { context, ) as WorkflowCreateRecordActionInput; - const filteredObjectRecord = filterValidFieldsInRecord( + const formattedObjectRecord = formatWorkflowRecordRelationFields( workflowActionInput.objectRecord, + objectMetadataInfo, + ); + + const filteredObjectRecord = filterValidFieldsInRecord( + formattedObjectRecord, objectMetadataInfo.flatObjectMetadata, objectMetadataInfo.flatFieldMetadataMaps, ); diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts index 2ce5ef240c..5f9f93a9ad 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts @@ -15,6 +15,7 @@ import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type'; import { buildWorkflowActorMetadata } from 'src/modules/workflow/workflow-executor/utils/build-workflow-actor-metadata.util'; import { filterValidFieldsInRecord } from 'src/modules/workflow/workflow-executor/utils/filter-valid-fields-in-record.util'; +import { formatWorkflowRecordRelationFields } from 'src/modules/workflow/workflow-executor/utils/format-workflow-record-relation-fields.util'; import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util'; import { resolveRichTextFieldsInRecord } from 'src/modules/workflow/workflow-executor/utils/resolve-rich-text-fields-in-record.util'; import { isWorkflowUpdateRecordAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/guards/is-workflow-update-record-action.guard'; @@ -81,8 +82,13 @@ export class UpdateRecordWorkflowAction implements WorkflowAction { ); } - const filteredObjectRecord = filterValidFieldsInRecord( + const formattedObjectRecord = formatWorkflowRecordRelationFields( workflowActionInput.objectRecord, + objectMetadataInfo, + ); + + const filteredObjectRecord = filterValidFieldsInRecord( + formattedObjectRecord, objectMetadataInfo.flatObjectMetadata, objectMetadataInfo.flatFieldMetadataMaps, ); diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action.ts index a28108b5c5..173bab49d3 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action.ts @@ -14,6 +14,7 @@ import { WorkflowExecutionContextService } from 'src/modules/workflow/workflow-e import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input'; import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type'; import { filterValidFieldsInRecord } from 'src/modules/workflow/workflow-executor/utils/filter-valid-fields-in-record.util'; +import { formatWorkflowRecordRelationFields } from 'src/modules/workflow/workflow-executor/utils/format-workflow-record-relation-fields.util'; import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util'; import { resolveRichTextFieldsInRecord } from 'src/modules/workflow/workflow-executor/utils/resolve-rich-text-fields-in-record.util'; import { isWorkflowUpsertRecordAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/guards/is-workflow-upsert-record-action.guard'; @@ -76,8 +77,13 @@ export class UpsertRecordWorkflowAction implements WorkflowAction { ); } - const filteredObjectRecord = filterValidFieldsInRecord( + const formattedObjectRecord = formatWorkflowRecordRelationFields( workflowActionInput.objectRecord, + objectMetadataInfo, + ); + + const filteredObjectRecord = filterValidFieldsInRecord( + formattedObjectRecord, objectMetadataInfo.flatObjectMetadata, objectMetadataInfo.flatFieldMetadataMaps, );