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,5 +1,5 @@
import { type FieldMorphRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { computeMorphRelationFieldName } from 'twenty-shared/utils';
import { computeMorphRelationGqlFieldName } from 'twenty-shared/utils';
export const computePossibleMorphGqlFieldForFieldName = ({
fieldMetadata,
}: {
@@ -9,7 +9,7 @@ export const computePossibleMorphGqlFieldForFieldName = ({
>;
}) =>
fieldMetadata.morphRelations.map((morphRelation) => {
return computeMorphRelationFieldName({
return computeMorphRelationGqlFieldName({
fieldName: fieldMetadata.fieldName,
relationType: morphRelation.type,
targetObjectMetadataNameSingular:
@@ -1,6 +1,7 @@
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { computePossibleMorphGqlFieldForFieldName } from '@/object-record/cache/utils/computePossibleMorphGqlFieldForFieldName';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { computeRelationGqlFieldJoinColumnName } from 'twenty-shared/utils';
export const getFieldMetadataFromGqlField = ({
objectMetadataItem,
@@ -12,7 +13,9 @@ export const getFieldMetadataFromGqlField = ({
return (
objectMetadataItem.fields.find((field) => field.name === gqlField) ??
objectMetadataItem.fields.find(
(field) => field.settings?.joinColumnName === gqlField,
(field) =>
computeRelationGqlFieldJoinColumnName({ name: field.name }) ===
gqlField,
) ??
objectMetadataItem.fields.filter(isFieldMorphRelation).find((field) => {
const morphRelations = field.morphRelations;
@@ -27,7 +30,9 @@ export const getFieldMetadataFromGqlField = ({
return possibleMorphRelationsNames
.flatMap((possibleMorphRelationName) => [
possibleMorphRelationName,
`${possibleMorphRelationName}Id`,
computeRelationGqlFieldJoinColumnName({
name: possibleMorphRelationName,
}),
])
.includes(gqlField);
})
@@ -1,7 +1,7 @@
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type FieldMorphRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { computeMorphRelationFieldName } from 'twenty-shared/utils';
import { computeMorphRelationGqlFieldName } from 'twenty-shared/utils';
export const getMorphRelationFromFieldMetadataAndGqlField = ({
objectMetadataItems,
@@ -24,7 +24,7 @@ export const getMorphRelationFromFieldMetadataAndGqlField = ({
morphRelation: undefined,
};
}
const computedName = computeMorphRelationFieldName({
const computedName = computeMorphRelationGqlFieldName({
fieldName: morphRelation.sourceFieldMetadata.name,
relationType: morphRelation.type,
targetObjectMetadataNameSingular: targetObjectMetadata.nameSingular,
@@ -9,7 +9,12 @@ import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getR
import { getRefName } from '@/object-record/cache/utils/getRefName';
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getNodeTypename, isDefined, pascalCase } from 'twenty-shared/utils';
import {
computeRelationGqlFieldJoinColumnName,
getNodeTypename,
isDefined,
pascalCase,
} from 'twenty-shared/utils';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
export const getRecordNodeFromRecord = <T extends ObjectRecord>({
@@ -132,7 +137,9 @@ export const getRecordNodeFromRecord = <T extends ObjectRecord>({
switch (field.type) {
case FieldMetadataType.RELATION: {
const isJoinColumn = field.settings?.joinColumnName === gqlField;
const isJoinColumn =
computeRelationGqlFieldJoinColumnName({ name: field.name }) ===
gqlField;
if (isJoinColumn) {
return [gqlField, value];
}