Support morph relations in workflow record nodes (#21403)

## Support morph (polymorphic) relations in workflow record nodes

Morph relations (e.g. a polymorphic `Owner` on `Pet` targeting `Person`
or `Company`) were not selectable in the workflow **Create / Update /
Upsert Record** nodes. This PR adds full support for setting them.

### What changed

**Frontend**
- `shouldDisplayFormField`: allow `MORPH_RELATION` (many-to-one) so
morph fields appear in record forms.
- New `FormMorphRelationToOneFieldInput`: a polymorphic record picker
across the morph's target objects, storing a self-describing value `{
targetObjectMetadataId, id }`.
- Wired the morph branch into `FormFieldInput`.

**Backend**
- New `formatWorkflowRecordMorphRelationFields` util: resolves the form
value (stored under the base field name, e.g. `owner`) into the correct
per-target join column (`ownerCompanyId`), nulling siblings to keep
exactly one target referenced.
- Wired into the create / update / upsert workflow actions (update also
expands `fieldsToUpdate` to the concrete join columns).

### Permissions handling
- The picker's search is scoped to only the morph targets the user can
read (`canReadObjectRecords`), so it no longer breaks when a target
object is inaccessible.
- If an existing value points to an object the user can't read, the
field shows the reused **"Not shared"** lock display instead of an empty
field, while remaining editable when other targets are readable.

### Notes
- No data schema / migration changes — reuses the existing per-target
morph columns and stores the selection in the existing workflow step
JSON settings.

<img width="607" height="717" alt="Screenshot 2026-06-10 at 14 57 40"
src="https://github.com/user-attachments/assets/496442a1-04a5-40f8-8b56-b28e38b00d5a"
/>

Also handles the case where the selected record is not readable
<img width="596" height="737" alt="image"
src="https://github.com/user-attachments/assets/c5ffb94e-3838-4db5-853e-f8e490331f23"
/>
This commit is contained in:
Marie
2026-06-15 14:56:17 +02:00
committed by GitHub
parent 5207493cda
commit 4be76e3fd1
16 changed files with 450 additions and 18 deletions
@@ -8,6 +8,10 @@ import { FormEmailsFieldInput } from '@/object-record/record-field/ui/form-types
import { FormFilesFieldInput } from '@/object-record/record-field/ui/form-types/components/FormFilesFieldInput';
import { FormFullNameFieldInput } from '@/object-record/record-field/ui/form-types/components/FormFullNameFieldInput';
import { FormLinksFieldInput } from '@/object-record/record-field/ui/form-types/components/FormLinksFieldInput';
import {
FormMorphRelationToOneFieldInput,
type FormMorphRelationToOneValue,
} from '@/object-record/record-field/ui/form-types/components/FormMorphRelationToOneFieldInput';
import { FormMultiSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput';
import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput';
import { FormPhoneFieldInput } from '@/object-record/record-field/ui/form-types/components/FormPhoneFieldInput';
@@ -43,6 +47,7 @@ import { isFieldEmails } from '@/object-record/record-field/ui/types/guards/isFi
import { isFieldFiles } from '@/object-record/record-field/ui/types/guards/isFieldFiles';
import { isFieldFullName } from '@/object-record/record-field/ui/types/guards/isFieldFullName';
import { isFieldLinks } from '@/object-record/record-field/ui/types/guards/isFieldLinks';
import { isFieldMorphRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne';
import { isFieldMultiSelect } from '@/object-record/record-field/ui/types/guards/isFieldMultiSelect';
import { isFieldNumber } from '@/object-record/record-field/ui/types/guards/isFieldNumber';
import { isFieldPhones } from '@/object-record/record-field/ui/types/guards/isFieldPhones';
@@ -241,6 +246,16 @@ export const FormFieldInput = ({
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : isFieldMorphRelationManyToOne(field) ? (
<FormMorphRelationToOneFieldInput
label={field.label}
morphRelations={field.metadata.morphRelations}
defaultValue={defaultValue as FormMorphRelationToOneValue}
onClear={onClear}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : isFieldArray(field) ? (
<FormArrayFieldInput
label={field.label}
@@ -0,0 +1,172 @@
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputRowContainer';
import { FormFieldPlaceholder } from '@/object-record/record-field/ui/form-types/components/FormFieldPlaceholder';
import {
FormSingleRecordPicker,
type RecordId,
type Variable,
} from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker';
import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent';
import { ForbiddenFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/ForbiddenFieldDisplay';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { isString } from '@sniptt/guards';
import { useId } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
import { type JsonValue } from 'type-fest';
const StyledReadonlyContainer = styled.div`
cursor: default;
display: flex;
height: 32px;
width: 100%;
`;
const StyledForbiddenFieldDisplayContainer = styled.div`
display: flex;
margin: ${themeCssVariables.spacing[2]};
`;
export type FormMorphRelationToOneValue =
| {
targetObjectMetadataId: string;
id: string;
}
| Variable
| null;
type FormMorphRelationToOneFieldInputProps = {
label?: string;
morphRelations: FieldMetadataItemRelation[];
defaultValue?: FormMorphRelationToOneValue;
onChange: (value: JsonValue) => void;
onClear?: () => void;
readonly?: boolean;
testId?: string;
VariablePicker?: VariablePickerComponent;
};
export const FormMorphRelationToOneFieldInput = ({
label,
morphRelations,
defaultValue,
onChange,
onClear,
readonly,
testId,
VariablePicker,
}: FormMorphRelationToOneFieldInputProps) => {
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const componentId = useId();
const readableObjectNameSingulars = [
...new Set(
morphRelations
.filter(
(morphRelation) =>
objectPermissionsByObjectMetadataId[
morphRelation.targetObjectMetadata.id
]?.canReadObjectRecords === true,
)
.map(
(morphRelation) => morphRelation.targetObjectMetadata.nameSingular,
),
),
];
const selectedMorphValue =
isDefined(defaultValue) && !isString(defaultValue) ? defaultValue : null;
const recordIdOrVariable: RecordId | Variable | null | undefined = isString(
defaultValue,
)
? defaultValue
: isDefined(defaultValue)
? defaultValue.id
: defaultValue;
const selectedTargetIsReadable =
!isDefined(selectedMorphValue) ||
objectPermissionsByObjectMetadataId[
selectedMorphValue.targetObjectMetadataId
]?.canReadObjectRecords === true;
const hasForbiddenSelectedRecord =
isDefined(selectedMorphValue) && !selectedTargetIsReadable;
const selectedObjectMetadataItem = isDefined(selectedMorphValue)
? objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id === selectedMorphValue.targetObjectMetadataId,
)
: undefined;
const selectedObjectNameSingular =
selectedObjectMetadataItem?.nameSingular ?? readableObjectNameSingulars[0];
if (hasForbiddenSelectedRecord) {
return (
<FormFieldInputContainer data-testid={testId}>
{label ? <InputLabel>{label}</InputLabel> : null}
<FormFieldInputRowContainer>
<StyledReadonlyContainer>
<FormFieldInputInnerContainer
formFieldInputInstanceId={componentId}
hasRightElement={false}
>
<StyledForbiddenFieldDisplayContainer>
<ForbiddenFieldDisplay />
</StyledForbiddenFieldDisplayContainer>
</FormFieldInputInnerContainer>
</StyledReadonlyContainer>
</FormFieldInputRowContainer>
</FormFieldInputContainer>
);
}
if (readableObjectNameSingulars.length === 0) {
return (
<FormFieldInputContainer data-testid={testId}>
{label ? <InputLabel>{label}</InputLabel> : null}
<FormFieldInputRowContainer>
<StyledReadonlyContainer>
<FormFieldInputInnerContainer
formFieldInputInstanceId={componentId}
hasRightElement={false}
>
<FormFieldPlaceholder>{t`No record`}</FormFieldPlaceholder>
</FormFieldInputInnerContainer>
</StyledReadonlyContainer>
</FormFieldInputRowContainer>
</FormFieldInputContainer>
);
}
return (
<FormSingleRecordPicker
label={label}
testId={testId}
defaultValue={recordIdOrVariable}
objectNameSingulars={readableObjectNameSingulars}
selectedObjectNameSingular={selectedObjectNameSingular}
onChange={onChange}
onClear={onClear}
onMorphItemSelected={(selectedMorphItem) =>
onChange({
targetObjectMetadataId: selectedMorphItem.objectMetadataId,
id: selectedMorphItem.recordId,
})
}
disabled={readonly}
VariablePicker={VariablePicker}
/>
);
};
@@ -60,6 +60,10 @@ export type FormSingleRecordPickerProps = {
onClear?: () => void;
onCreate?: (searchInput?: string) => void | Promise<void>;
objectNameSingulars: string[];
selectedObjectNameSingular?: string;
onMorphItemSelected?: (
selectedMorphItem: RecordPickerPickableMorphItem,
) => void;
disabled?: boolean;
testId?: string;
VariablePicker?: VariablePickerComponent;
@@ -69,8 +73,10 @@ export const FormSingleRecordPicker = ({
label,
defaultValue,
objectNameSingulars,
selectedObjectNameSingular,
onChange,
onClear,
onMorphItemSelected,
onCreate,
disabled,
testId,
@@ -78,6 +84,9 @@ export const FormSingleRecordPicker = ({
}: FormSingleRecordPickerProps) => {
const { theme } = useContext(ThemeContext);
const resolvedObjectNameSingular =
selectedObjectNameSingular ?? objectNameSingulars[0];
const draftValue: FormSingleRecordPickerValue =
defaultValue === null
? { type: 'no-record', value: null }
@@ -97,7 +106,7 @@ export const FormSingleRecordPicker = ({
isDefined(defaultValue) && !isStandaloneVariableString(defaultValue)
? defaultValue
: '',
objectNameSingular: objectNameSingulars[0],
objectNameSingular: resolvedObjectNameSingular,
withSoftDeleted: true,
skip: !isDefined(defaultValue) || !isValidUuid(defaultValue),
});
@@ -133,6 +142,8 @@ export const FormSingleRecordPicker = ({
if (defaultValue === selectedMorphItem.recordId) {
onClear?.();
} else if (isDefined(onMorphItemSelected)) {
onMorphItemSelected(selectedMorphItem);
} else {
onChange(selectedMorphItem.recordId);
}
@@ -185,7 +196,7 @@ export const FormSingleRecordPicker = ({
<FormSingleRecordFieldChip
draftValue={draftValue}
selectedRecord={selectedRecord}
objectNameSingular={objectNameSingulars[0]}
objectNameSingular={resolvedObjectNameSingular}
onRemove={handleUnlinkVariable}
disabled={disabled}
/>
@@ -212,7 +223,7 @@ export const FormSingleRecordPicker = ({
<FormSingleRecordFieldChip
draftValue={draftValue}
selectedRecord={selectedRecord}
objectNameSingular={objectNameSingulars[0]}
objectNameSingular={resolvedObjectNameSingular}
onRemove={handleUnlinkVariable}
disabled={disabled}
/>
@@ -248,6 +259,7 @@ export const FormSingleRecordPicker = ({
onVariableSelect={handleVariableTagInsert}
shouldDisplayRecordObjects={true}
shouldDisplayRecordFields={false}
objectNameSingularsToSelect={objectNameSingulars}
/>
)}
</FormFieldInputRowContainer>
@@ -5,4 +5,5 @@ export type VariablePickerComponent = React.FC<{
onVariableSelect: (variableName: string) => void;
shouldDisplayRecordObjects?: boolean;
shouldDisplayRecordFields?: boolean;
objectNameSingularsToSelect?: string[];
}>;
@@ -166,6 +166,15 @@ export const WorkflowEditActionCreateRecord = ({
saveAction(newFormData);
};
const handleFieldClear = (fieldName: keyof CreateRecordFormData) => {
const newFormData: CreateRecordFormData = { ...formData };
delete newFormData[fieldName];
setFormData(newFormData);
saveAction(newFormData);
};
const saveAction = useDebouncedCallback(
async (formData: CreateRecordFormData) => {
if (actionOptions.readonly === true) {
@@ -242,6 +251,9 @@ export const WorkflowEditActionCreateRecord = ({
onChange={(value) => {
handleFieldChange(fieldDefinition.metadata.fieldName, value);
}}
onClear={() => {
handleFieldClear(fieldDefinition.metadata.fieldName);
}}
VariablePicker={WorkflowVariablePicker}
readonly={isFormDisabled}
/>
@@ -80,6 +80,15 @@ export const WorkflowEditActionUpdateRecord = ({
saveAction(newFormData);
};
const handleFieldClear = (fieldName: keyof UpdateRecordFormData) => {
const newFormData: UpdateRecordFormData = { ...formData };
delete newFormData[fieldName];
setFormData(newFormData);
saveAction(newFormData);
};
const selectedObjectMetadataItem = activeNonSystemObjectMetadataItems.find(
(item) => item.nameSingular === formData.objectNameSingular,
);
@@ -229,6 +238,9 @@ export const WorkflowEditActionUpdateRecord = ({
onChange={(value) => {
handleFieldChange(fieldName, value);
}}
onClear={() => {
handleFieldClear(fieldName);
}}
VariablePicker={WorkflowVariablePicker}
readonly={isFormDisabled}
/>
@@ -183,6 +183,15 @@ export const WorkflowEditActionUpsertRecord = ({
saveAction(newFormData);
};
const handleFieldClear = (fieldName: keyof UpsertRecordFormData) => {
const newFormData: UpsertRecordFormData = { ...formData };
delete newFormData[fieldName];
setFormData(newFormData);
saveAction(newFormData);
};
const saveAction = useDebouncedCallback(
async (formData: UpsertRecordFormData) => {
if (actionOptions.readonly === true) {
@@ -269,6 +278,9 @@ export const WorkflowEditActionUpsertRecord = ({
onChange={(recordId) => {
handleFieldChange('id', recordId);
}}
onClear={() => {
handleFieldClear('id');
}}
objectNameSingulars={
isDefined(objectNameSingular) ? [objectNameSingular] : []
}
@@ -299,6 +311,9 @@ export const WorkflowEditActionUpsertRecord = ({
onChange={(value) => {
handleFieldChange(fieldDefinition.metadata.fieldName, value);
}}
onClear={() => {
handleFieldClear(fieldDefinition.metadata.fieldName);
}}
VariablePicker={WorkflowVariablePicker}
readonly={isFormDisabled}
/>
@@ -22,6 +22,7 @@ const SUPPORTED_FORM_FIELD_TYPES = [
FieldMetadataType.UUID,
FieldMetadataType.ARRAY,
FieldMetadataType.RELATION,
FieldMetadataType.MORPH_RELATION,
FieldMetadataType.RICH_TEXT,
];
@@ -37,8 +38,10 @@ export const shouldDisplayFormField = ({
}
const isIdField = fieldMetadataItem.name === 'id';
const isNotSupportedRelation =
fieldMetadataItem.type === FieldMetadataType.RELATION &&
(fieldMetadataItem.type === FieldMetadataType.RELATION ||
fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION) &&
fieldMetadataItem.settings?.['relationType'] !== 'MANY_TO_ONE';
switch (actionType) {
@@ -63,6 +63,7 @@ export const WorkflowVariablePicker: VariablePickerComponent = ({
onVariableSelect,
shouldDisplayRecordObjects = false,
shouldDisplayRecordFields = true,
objectNameSingularsToSelect,
}) => {
const dropdownId = `${SEARCH_VARIABLES_DROPDOWN_ID}-${instanceId}`;
const isDropdownOpen = useAtomComponentStateValue(
@@ -82,6 +83,7 @@ export const WorkflowVariablePicker: VariablePickerComponent = ({
disabled={disabled}
shouldDisplayRecordObjects={shouldDisplayRecordObjects}
shouldDisplayRecordFields={shouldDisplayRecordFields}
objectNameSingularsToSelect={objectNameSingularsToSelect}
/>
</StyledSearchVariablesDropdownContainer>
);
@@ -39,6 +39,7 @@ export const WorkflowVariablesDropdown = ({
onVariableSelect,
shouldDisplayRecordFields,
shouldDisplayRecordObjects,
objectNameSingularsToSelect,
}: {
clickableComponent?: React.ReactNode;
disabled?: boolean;
@@ -47,6 +48,7 @@ export const WorkflowVariablesDropdown = ({
onVariableSelect: (variableName: string) => void;
shouldDisplayRecordFields: boolean;
shouldDisplayRecordObjects: boolean;
objectNameSingularsToSelect?: string[];
}) => {
const { theme } = useContext(ThemeContext);
const dropdownId = `${SEARCH_VARIABLES_DROPDOWN_ID}-${instanceId}`;
@@ -119,6 +121,7 @@ export const WorkflowVariablesDropdown = ({
onSelect={handleSubItemSelect}
onBack={handleBack}
shouldDisplayRecordObjects={shouldDisplayRecordObjects}
objectNameSingularsToSelect={objectNameSingularsToSelect}
/>
)
}
@@ -29,6 +29,7 @@ type WorkflowVariablesDropdownStepItemsProps = {
onSelect: (value: string) => void;
onBack: () => void;
shouldDisplayRecordObjects: boolean;
objectNameSingularsToSelect?: string[];
};
export const WorkflowVariablesDropdownStepItems = ({
@@ -36,6 +37,7 @@ export const WorkflowVariablesDropdownStepItems = ({
onSelect,
onBack,
shouldDisplayRecordObjects,
objectNameSingularsToSelect,
}: WorkflowVariablesDropdownStepItemsProps) => {
const { t } = useLingui();
const { getIcon } = useIcons();
@@ -97,8 +99,18 @@ export const WorkflowVariablesDropdownStepItems = ({
: true;
const objectLabel = displayedSubStepObjectMetadata?.labelSingular;
const isSubStepObjectSelectable =
!isDefined(objectNameSingularsToSelect) ||
(isDefined(displayedSubStepObjectMetadata) &&
objectNameSingularsToSelect.includes(
displayedSubStepObjectMetadata.nameSingular,
));
const shouldDisplaySubStepObject =
shouldDisplayRecordObjects && isObjectFoundThroughSearch;
shouldDisplayRecordObjects &&
isObjectFoundThroughSearch &&
isSubStepObjectSelectable;
const displayedSubStepObjectIconProps = isDefined(
displayedSubStepObjectMetadata,
@@ -15,9 +15,14 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
@CoreResolver()
@UseFilters(SearchApiExceptionFilter, PreventNestToAutoLogGraphqlErrorsFilter)
@UseFilters(
SearchApiExceptionFilter,
PermissionsGraphqlApiExceptionFilter,
PreventNestToAutoLogGraphqlErrorsFilter,
)
@UsePipes(ResolverValidationPipe)
@UseGuards(WorkspaceAuthGuard, CustomPermissionGuard)
export class SearchResolver {
@@ -2,11 +2,44 @@ import { isObject, isString } from '@sniptt/guards';
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
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 { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { getMorphNameFromMorphFieldMetadataName } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util';
import { type ObjectMetadataInfo } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
type MorphRelationTargetJoinColumn = {
joinColumnName: string;
targetObjectMetadataId: string;
};
type ExtractedMorphValue = {
targetObjectMetadataId: string;
id: string;
};
const extractMorphValue = (value: unknown): ExtractedMorphValue | null => {
if (!isObject(value)) {
return null;
}
const record = value as Record<string, unknown>;
if (
isString(record.targetObjectMetadataId) &&
isString(record.id) &&
isDefined(record.id)
) {
return {
targetObjectMetadataId: record.targetObjectMetadataId,
id: record.id,
};
}
return null;
};
const extractLegacyRelationId = (value: unknown): string | undefined => {
if (!isObject(value)) {
return undefined;
@@ -21,7 +54,113 @@ const extractLegacyRelationId = (value: unknown): string | undefined => {
return record.id;
};
export const formatWorkflowRecordRelationFields = (
const formatWorkflowRecordMorphRelationFields = (
record: Record<string, unknown>,
objectMetadataInfo: ObjectMetadataInfo,
): {
formattedRecord: Record<string, unknown>;
joinColumnNamesByMorphFieldName: Record<string, string[]>;
} => {
const { flatObjectMetadata, flatObjectMetadataMaps, flatFieldMetadataMaps } =
objectMetadataInfo;
const objectFields = getFlatFieldsFromFlatObjectMetadata(
flatObjectMetadata,
flatFieldMetadataMaps,
);
const targetJoinColumnsByMorphFieldName = new Map<
string,
MorphRelationTargetJoinColumn[]
>();
for (const field of objectFields) {
if (
!isFlatFieldMetadataOfType(field, FieldMetadataType.MORPH_RELATION) ||
field.settings.relationType !== RelationType.MANY_TO_ONE
) {
continue;
}
const targetObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: field.relationTargetObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
if (!isDefined(targetObjectMetadata)) {
continue;
}
const morphFieldName = getMorphNameFromMorphFieldMetadataName({
morphRelationFlatFieldMetadata: field,
nameSingular: targetObjectMetadata.nameSingular,
namePlural: targetObjectMetadata.namePlural,
});
const joinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: field.name,
});
const existing =
targetJoinColumnsByMorphFieldName.get(morphFieldName) ?? [];
targetJoinColumnsByMorphFieldName.set(morphFieldName, [
...existing,
{
joinColumnName,
targetObjectMetadataId: field.relationTargetObjectMetadataId,
},
]);
}
const formattedRecord: Record<string, unknown> = {};
const joinColumnNamesByMorphFieldName: Record<string, string[]> = {};
for (const [key, value] of Object.entries(record)) {
const targetJoinColumns = targetJoinColumnsByMorphFieldName.get(key);
if (!isDefined(targetJoinColumns)) {
formattedRecord[key] = value;
continue;
}
const registerJoinColumns = () => {
joinColumnNamesByMorphFieldName[key] = targetJoinColumns.map(
(targetJoinColumn) => targetJoinColumn.joinColumnName,
);
for (const { joinColumnName } of targetJoinColumns) {
formattedRecord[joinColumnName] = null;
}
};
if (value === null) {
registerJoinColumns();
continue;
}
const morphValue = extractMorphValue(value);
const matchingTargetJoinColumn = isDefined(morphValue)
? targetJoinColumns.find(
(targetJoinColumn) =>
targetJoinColumn.targetObjectMetadataId ===
morphValue.targetObjectMetadataId,
)
: undefined;
if (!isDefined(morphValue) || !isDefined(matchingTargetJoinColumn)) {
continue;
}
registerJoinColumns();
formattedRecord[matchingTargetJoinColumn.joinColumnName] = morphValue.id;
}
return { formattedRecord, joinColumnNamesByMorphFieldName };
};
const formatWorkflowRecordSimpleRelationFields = (
record: Record<string, unknown>,
objectMetadataInfo: ObjectMetadataInfo,
): Record<string, unknown> => {
@@ -70,3 +209,23 @@ export const formatWorkflowRecordRelationFields = (
return formattedRecord;
};
export const formatWorkflowRecordRelationFields = (
record: Record<string, unknown>,
objectMetadataInfo: ObjectMetadataInfo,
): {
formattedRecord: Record<string, unknown>;
joinColumnNamesByMorphFieldName: Record<string, string[]>;
} => {
const {
formattedRecord: recordWithMorphRelations,
joinColumnNamesByMorphFieldName,
} = formatWorkflowRecordMorphRelationFields(record, objectMetadataInfo);
const formattedRecord = formatWorkflowRecordSimpleRelationFields(
recordWithMorphRelations,
objectMetadataInfo,
);
return { formattedRecord, joinColumnNamesByMorphFieldName };
};
@@ -59,10 +59,11 @@ export class CreateRecordWorkflowAction implements WorkflowAction {
context,
) as WorkflowCreateRecordActionInput;
const formattedObjectRecord = formatWorkflowRecordRelationFields(
workflowActionInput.objectRecord,
objectMetadataInfo,
);
const { formattedRecord: formattedObjectRecord } =
formatWorkflowRecordRelationFields(
workflowActionInput.objectRecord,
objectMetadataInfo,
);
const filteredObjectRecord = filterValidFieldsInRecord(
formattedObjectRecord,
@@ -82,7 +82,10 @@ export class UpdateRecordWorkflowAction implements WorkflowAction {
);
}
const formattedObjectRecord = formatWorkflowRecordRelationFields(
const {
formattedRecord: formattedObjectRecord,
joinColumnNamesByMorphFieldName,
} = formatWorkflowRecordRelationFields(
workflowActionInput.objectRecord,
objectMetadataInfo,
);
@@ -93,7 +96,11 @@ export class UpdateRecordWorkflowAction implements WorkflowAction {
objectMetadataInfo.flatFieldMetadataMaps,
);
const filteredFieldsToUpdate = workflowActionInput.fieldsToUpdate?.filter(
const expandedFieldsToUpdate = workflowActionInput.fieldsToUpdate?.flatMap(
(fieldName) => joinColumnNamesByMorphFieldName[fieldName] ?? [fieldName],
);
const filteredFieldsToUpdate = expandedFieldsToUpdate?.filter(
(fieldName) => fieldName in filteredObjectRecord,
);
@@ -77,10 +77,11 @@ export class UpsertRecordWorkflowAction implements WorkflowAction {
);
}
const formattedObjectRecord = formatWorkflowRecordRelationFields(
workflowActionInput.objectRecord,
objectMetadataInfo,
);
const { formattedRecord: formattedObjectRecord } =
formatWorkflowRecordRelationFields(
workflowActionInput.objectRecord,
objectMetadataInfo,
);
const filteredObjectRecord = filterValidFieldsInRecord(
formattedObjectRecord,