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
@@ -44,7 +44,7 @@ describe('shouldFieldBeQueried', () => {
const res = shouldFieldBeQueried({
gqlField: 'fieldNameId',
fieldMetadata: {
name: 'fieldNameId',
name: 'fieldName',
type: FieldMetadataType.RELATION,
settings: {
relationType: RelationType.MANY_TO_ONE,
@@ -10,7 +10,11 @@ import {
type ObjectPermissions,
RelationType,
} from 'twenty-shared/types';
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
import {
computeMorphRelationGqlFieldName,
computeRelationGqlFieldJoinColumnName,
isDefined,
} from 'twenty-shared/utils';
type MapFieldMetadataToGraphQLQueryArgs = {
objectMetadataItems: EnrichedObjectMetadataItem[];
@@ -52,7 +56,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
) {
let gqlMorphField = '';
for (const morphRelation of fieldMetadata.morphRelations ?? []) {
const relationFieldName = computeMorphRelationFieldName({
const relationFieldName = computeMorphRelationGqlFieldName({
fieldName: fieldMetadata.name,
relationType: fieldMetadata.settings?.relationType,
targetObjectMetadataNameSingular:
@@ -168,7 +172,10 @@ ${mapObjectMetadataToGraphQLQuery({
}
}
if (gqlField === fieldMetadata.settings?.joinColumnName) {
if (
gqlField ===
computeRelationGqlFieldJoinColumnName({ name: fieldMetadata.name })
) {
return `${gqlField}`;
}
@@ -4,8 +4,17 @@ import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapField
import { shouldFieldBeQueried } from '@/object-metadata/utils/shouldFieldBeQueried';
import { type RecordGqlFields } from '@/object-record/graphql/record-gql-fields/types/RecordGqlFields';
import { isRecordGqlFieldsNode } from '@/object-record/graphql/utils/isRecordGraphlFieldsNode';
import { FieldMetadataType, type ObjectPermissions } from 'twenty-shared/types';
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
import {
FieldMetadataType,
RelationType,
type ObjectPermissions,
} from 'twenty-shared/types';
import {
computeMorphRelationGqlFieldJoinColumnName,
computeMorphRelationGqlFieldName,
computeRelationGqlFieldJoinColumnName,
isDefined,
} from 'twenty-shared/utils';
type MapObjectMetadataToGraphQLQueryArgs = {
objectMetadataItems: EnrichedObjectMetadataItem[];
@@ -46,12 +55,15 @@ export const mapObjectMetadataToGraphQLQuery = ({
const manyToOneRelationFields = objectMetadataItem?.readableFields
.filter((field) => field.isActive)
.filter(
(field) =>
field.type === FieldMetadataType.RELATION ||
field.type === FieldMetadataType.MORPH_RELATION,
)
.filter((field) => isDefined(field.settings?.joinColumnName));
.filter((field) => {
if (field.type === FieldMetadataType.RELATION) {
return field.relation?.type === RelationType.MANY_TO_ONE;
}
if (field.type === FieldMetadataType.MORPH_RELATION) {
return field.settings?.relationType === RelationType.MANY_TO_ONE;
}
return false;
});
const manyToOneRelationGqlFieldWithFieldMetadata =
manyToOneRelationFields.flatMap((fieldMetadata) => {
@@ -59,7 +71,9 @@ export const mapObjectMetadataToGraphQLQuery = ({
fieldMetadata.type === FieldMetadataType.MORPH_RELATION;
if (!isMorphRelation) {
return {
gqlField: fieldMetadata.settings?.joinColumnName,
gqlField: computeRelationGqlFieldJoinColumnName({
name: fieldMetadata.name,
}),
fieldMetadata: fieldMetadata,
};
}
@@ -69,14 +83,14 @@ export const mapObjectMetadataToGraphQLQuery = ({
}
return fieldMetadata.morphRelations.map((morphRelation) => ({
gqlField: `${computeMorphRelationFieldName({
gqlField: computeMorphRelationGqlFieldJoinColumnName({
fieldName: fieldMetadata.name,
relationType: morphRelation.type,
targetObjectMetadataNameSingular:
morphRelation.targetObjectMetadata.nameSingular,
targetObjectMetadataNamePlural:
morphRelation.targetObjectMetadata.namePlural,
})}Id`,
}),
fieldMetadata: fieldMetadata,
}));
});
@@ -102,7 +116,7 @@ export const mapObjectMetadataToGraphQLQuery = ({
}
return fieldMetadata.morphRelations.map((morphRelation) => ({
gqlField: computeMorphRelationFieldName({
gqlField: computeMorphRelationGqlFieldName({
fieldName: fieldMetadata.name,
relationType: morphRelation.type,
targetObjectMetadataNameSingular:
@@ -4,7 +4,11 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
import { isDefined } from 'twenty-shared/utils';
import {
computeMorphRelationGqlFieldJoinColumnName,
computeRelationGqlFieldJoinColumnName,
isDefined,
} from 'twenty-shared/utils';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
export const shouldFieldBeQueried = ({
@@ -13,13 +17,35 @@ export const shouldFieldBeQueried = ({
recordGqlFields,
}: {
gqlField: string;
fieldMetadata: Pick<FieldMetadataItem, 'name' | 'type' | 'settings'>;
fieldMetadata: Pick<
FieldMetadataItem,
'name' | 'type' | 'settings' | 'morphRelations'
>;
objectRecord?: ObjectRecord;
recordGqlFields?: RecordGqlOperationGqlRecordFields;
}): any => {
const isRelationJoinColumn =
isFieldRelation(fieldMetadata) &&
computeRelationGqlFieldJoinColumnName({ name: fieldMetadata.name }) ===
gqlField;
const isMorphRelationJoinColumn =
isFieldMorphRelation(fieldMetadata) &&
isDefined(fieldMetadata.morphRelations) &&
fieldMetadata.morphRelations.some(
(morphRelation) =>
computeMorphRelationGqlFieldJoinColumnName({
fieldName: fieldMetadata.name,
relationType: morphRelation.type,
targetObjectMetadataNameSingular:
morphRelation.targetObjectMetadata.nameSingular,
targetObjectMetadataNamePlural:
morphRelation.targetObjectMetadata.namePlural,
}) === gqlField,
);
const isJoinColumn: boolean =
(isFieldRelation(fieldMetadata) || isFieldMorphRelation(fieldMetadata)) &&
fieldMetadata.settings.joinColumnName === gqlField;
isRelationJoinColumn || isMorphRelationJoinColumn;
if (
isUndefinedOrNull(recordGqlFields) &&