refactor: stop reading joinColumnName from relation field settings (#20304)

## Summary

`joinColumnName` on relation field settings is always derivable from the
field name (and the target object name for morph relations). This PR
stops reading it from settings anywhere in production code; the stored
value is no longer used.

The settings field is **not** removed from data yet — a follow-up can
drop it once we are confident nothing depends on the stored value.

## Helpers

The helpers are split by layer because frontend and backend hold morph
relations differently: the frontend has a base name plus a
`morphRelations[]` array, the backend has one row per target with the
name already morph-resolved.

| Helper | Layer | When to use |
|---|---|---|
| `computeRelationGqlFieldJoinColumnName` | Shared / frontend
(`gqlField`) | Non-morph relation on the frontend. |
| `computeMorphRelationGqlFieldName` | Shared / frontend (`gqlField`) |
Need the per-target morph gqlField name (e.g. `targetCompany`). |
| `computeMorphRelationGqlFieldJoinColumnName` | Shared / frontend
(`gqlField`) | Per-target morph join column on the frontend. Prefer over
the non-morph helper for any morph field — it forces the per-target
inputs. |
| `computeMorphOrRelationFieldJoinColumnName` | Backend
(`FlatFieldMetadata.name`) | Any backend read or write — the flat name
is already morph-resolved, so one helper covers both cases. |
| `computeMorphRelationFlatFieldName` | Backend
(`FlatFieldMetadata.name`) | **Mutation paths only** (create / update /
object rename). Reads consume the stored `field.name` and never call
this. |

## Test plan

- [x] Typecheck and lint (front, server, shared)
- [x] Existing unit tests pass
- [ ] CI green
This commit is contained in:
Charles Bochet
2026-05-07 09:53:00 +02:00
committed by GitHub
parent 4b56ad0607
commit 10876138d2
73 changed files with 592 additions and 387 deletions
@@ -1,9 +1,9 @@
type ComputeRelationFieldJoinColumnNameArgs = {
type ComputeMorphOrRelationFieldJoinColumnNameArgs = {
name: string;
};
export const computeMorphOrRelationFieldJoinColumnName = ({
name,
}: ComputeRelationFieldJoinColumnNameArgs) => {
}: ComputeMorphOrRelationFieldJoinColumnNameArgs): string => {
return `${name}Id`;
};
@@ -0,0 +1,34 @@
import { RelationType } from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils';
import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
type ComputeMorphRelationFlatFieldNameArgs = {
fieldName: string;
relationType: RelationType;
targetObjectMetadataNameSingular: string;
targetObjectMetadataNamePlural: string;
};
export const computeMorphRelationFlatFieldName = ({
fieldName,
relationType,
targetObjectMetadataNameSingular: nameSingular,
targetObjectMetadataNamePlural: namePlural,
}: ComputeMorphRelationFlatFieldNameArgs): string => {
if (relationType === RelationType.MANY_TO_ONE) {
return `${fieldName}${capitalize(nameSingular)}`;
}
if (relationType === RelationType.ONE_TO_MANY) {
return `${fieldName}${capitalize(namePlural)}`;
}
throw new FieldMetadataException(
`Invalid relation type (${relationType}) for field ${fieldName} on ${nameSingular}`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
};
@@ -1,7 +1,7 @@
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 { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.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 { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
@@ -2,10 +2,11 @@ import {
FieldMetadataType,
type RelationUpdatePayload,
} from 'twenty-shared/types';
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
import { isDefined } from 'twenty-shared/utils';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { computeMorphRelationFlatFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-flat-field-name.util';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
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 { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
@@ -89,7 +90,7 @@ export const computeFlatFieldToUpdateFromMorphRelationUpdatePayload = ({
flatEntityMaps: flatObjectMetadataMaps,
});
const computedMorphName = computeMorphRelationFieldName({
const computedMorphName = computeMorphRelationFlatFieldName({
fieldName: morphNameWithoutObjectName,
relationType: fieldMetadataToUpdate.settings.relationType,
targetObjectMetadataNameSingular: newTargetObjectMetadata.nameSingular,
@@ -1,12 +1,13 @@
import { msg } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { computeMorphRelationFlatFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-flat-field-name.util';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
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 { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type';
@@ -86,7 +87,7 @@ export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({
const morphId = v4();
const flatFieldsAndIndexes = morphRelationCreationPayload.reduce(
(acc, { relationCreationPayload, targetFlatObjectMetadata }) => {
const currentMorphRelationFieldName = computeMorphRelationFieldName({
const currentMorphRelationFieldName = computeMorphRelationFlatFieldName({
fieldName: createFieldInput.name,
relationType: relationCreationPayload.type,
targetObjectMetadataNameSingular: targetFlatObjectMetadata.nameSingular,
@@ -2,10 +2,10 @@ import { msg } from '@lingui/core/macro';
import { FieldMetadataType } 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 { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
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 { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type';
@@ -4,12 +4,13 @@ import {
RelationOnDeleteAction,
RelationType,
} from 'twenty-shared/types';
import { v4 } from 'uuid';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { generateIndexForFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-index-for-flat-field-metadata.util';
import { getDefaultFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util';
@@ -1,6 +1,6 @@
import { 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 { findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-universal-identifier-in-universal-flat-entity-maps.util';
import { isMorphOrRelationUniversalFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { type UniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-maps.type';
@@ -39,15 +39,16 @@ export const getObjectFieldNamesAndJoinColumnNames = ({
universalFlatFieldMetadata,
) &&
universalFlatFieldMetadata.universalSettings.relationType ===
RelationType.MANY_TO_ONE &&
isDefined(universalFlatFieldMetadata.universalSettings.joinColumnName)
RelationType.MANY_TO_ONE
) {
return {
...acc,
fieldNames: [...acc.fieldNames, universalFlatFieldMetadata.name],
joinColumnNames: [
...acc.joinColumnNames,
universalFlatFieldMetadata.universalSettings.joinColumnName,
computeMorphOrRelationFieldJoinColumnName({
name: universalFlatFieldMetadata.name,
}),
],
};
}
@@ -3,9 +3,9 @@ import {
type FieldMetadataType,
type FromTo,
} from 'twenty-shared/types';
import { computeMorphRelationFieldName } from 'twenty-shared/utils';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { computeMorphRelationFlatFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-flat-field-name.util';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-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 { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
@@ -37,7 +37,7 @@ const updateMorphFlatFieldName = ({
namePlural: fromRelationTargetFlatObjectMetadata.namePlural,
});
const newMorphFieldName = computeMorphRelationFieldName({
const newMorphFieldName = computeMorphRelationFlatFieldName({
fieldName: initialMorphRelationFieldName,
relationType: fromMorphFlatFieldMetadata.universalSettings.relationType,
targetObjectMetadataNameSingular:
@@ -1,9 +1,11 @@
import { RelationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { isMorphOrRelationUniversalFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { generateDeterministicIndexNameV2 } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name-v2';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
@@ -36,11 +38,15 @@ export const generateFlatIndexMetadataWithNameOrThrow = ({
);
}
const name = isMorphOrRelationUniversalFlatFieldMetadata(
relatedFlatFieldMetadata,
)
? (relatedFlatFieldMetadata.universalSettings.joinColumnName ??
relatedFlatFieldMetadata.name)
const isManyToOneRelation =
isMorphOrRelationUniversalFlatFieldMetadata(relatedFlatFieldMetadata) &&
relatedFlatFieldMetadata.universalSettings?.relationType ===
RelationType.MANY_TO_ONE;
const name = isManyToOneRelation
? computeMorphOrRelationFieldJoinColumnName({
name: relatedFlatFieldMetadata.name,
})
: relatedFlatFieldMetadata.name;
return {
@@ -5,10 +5,10 @@ import {
import { FieldMetadataType } from 'twenty-shared/types';
import { capitalize, 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 { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
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 { generateMorphOrRelationFlatFieldMetadataPair } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util';