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,6 @@
import { isDefined } from 'twenty-shared/utils';
import { RelationType } from 'twenty-shared/types';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-universal-identifier-in-universal-flat-entity-maps-or-throw.util';
import { isMorphOrRelationUniversalFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type';
@@ -24,12 +25,16 @@ export const buildUniversalFlatObjectFieldByNameAndJoinColumnMaps = ({
for (const field of objectFields) {
fieldUniversalIdentifierByName[field.name] = field.universalIdentifier;
if (isMorphOrRelationUniversalFlatFieldMetadata(field)) {
if (isDefined(field.universalSettings.joinColumnName)) {
fieldUniversalIdentifierByJoinColumnName[
field.universalSettings.joinColumnName
] = field.universalIdentifier;
}
if (
isMorphOrRelationUniversalFlatFieldMetadata(field) &&
field.universalSettings.relationType === RelationType.MANY_TO_ONE
) {
const joinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: field.name,
});
fieldUniversalIdentifierByJoinColumnName[joinColumnName] =
field.universalIdentifier;
}
}
@@ -5,6 +5,7 @@ import { isDefined } from 'twenty-shared/utils';
import { type QueryRunner } from 'typeorm';
import { v4 } from 'uuid';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { type MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-maps.type';
@@ -214,13 +215,9 @@ export class CreateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
targetFlatObjectMetadata,
);
const joinColumnName = flatFieldMetadata.settings?.joinColumnName;
if (!isDefined(joinColumnName)) {
throw new Error(
'Join column name is not defined in a MANY_TO_ONE relation',
);
}
const joinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: flatFieldMetadata.name,
});
await this.workspaceSchemaManagerService.foreignKeyManager.createForeignKey(
{
@@ -3,10 +3,12 @@ import { Injectable } from '@nestjs/common';
import {
FieldMetadataSettingsMapping,
FieldMetadataType,
RelationType,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ColumnType, type QueryRunner } from 'typeorm';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@@ -223,35 +225,6 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
}
if (isDefined(update.settings)) {
// Handle joinColumnName rename
if (isMorphOrRelationFlatFieldMetadata(optimisticFlatFieldMetadata)) {
const fromSettings = optimisticFlatFieldMetadata.settings;
const toSettings = update.settings as
| FieldMetadataSettingsMapping['MORPH_RELATION']
| FieldMetadataSettingsMapping['RELATION'];
if (
isDefined(fromSettings?.joinColumnName) &&
isDefined(toSettings?.joinColumnName) &&
fromSettings.joinColumnName !== toSettings.joinColumnName
) {
await this.workspaceSchemaManagerService.columnManager.renameColumn({
queryRunner,
schemaName,
tableName,
oldColumnName: fromSettings.joinColumnName,
newColumnName: toSettings.joinColumnName,
});
optimisticFlatFieldMetadata = {
...optimisticFlatFieldMetadata,
settings: {
...optimisticFlatFieldMetadata.settings,
joinColumnName: toSettings.joinColumnName,
},
};
}
}
// Handle asExpression/generatedType change (for TS_VECTOR fields)
if (
isFlatFieldMetadataOfType(
@@ -302,19 +275,26 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
| FieldMetadataSettingsMapping['MORPH_RELATION']
| FieldMetadataSettingsMapping['RELATION'];
const isManyToOne =
optimisticFlatFieldMetadata.settings?.relationType ===
RelationType.MANY_TO_ONE;
if (
isDefined(optimisticFlatFieldMetadata.settings.joinColumnName) &&
isManyToOne &&
isDefined(fromSettings?.onDelete) &&
isDefined(toSettings?.onDelete) &&
toSettings.onDelete !== fromSettings.onDelete
) {
const joinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: optimisticFlatFieldMetadata.name,
});
const foreignKeyName =
await this.workspaceSchemaManagerService.foreignKeyManager.getForeignKeyName(
{
queryRunner,
schemaName,
tableName,
columnName: optimisticFlatFieldMetadata.settings.joinColumnName,
columnName: joinColumnName,
},
);
@@ -351,7 +331,7 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
schemaName,
foreignKey: {
tableName,
columnName: optimisticFlatFieldMetadata.settings.joinColumnName,
columnName: joinColumnName,
referencedTableName,
referencedColumnName: 'id',
onDelete:
@@ -403,7 +383,23 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
newColumnName: toCompositeColumnName,
});
}
} else if (!isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) {
} else if (isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) {
if (
flatFieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE
) {
await this.workspaceSchemaManagerService.columnManager.renameColumn({
queryRunner,
schemaName,
tableName,
oldColumnName: computeMorphOrRelationFieldJoinColumnName({
name: fromName,
}),
newColumnName: computeMorphOrRelationFieldJoinColumnName({
name: toName,
}),
});
}
} else {
await this.workspaceSchemaManagerService.columnManager.renameColumn({
queryRunner,
schemaName,
@@ -1,7 +1,8 @@
import { compositeTypeDefinitions } from 'twenty-shared/types';
import { compositeTypeDefinitions, RelationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type QueryRunner } from 'typeorm';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import {
@@ -41,14 +42,18 @@ export const computeFlatIndexFieldColumnNames = ({
}
if (isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) {
if (!isDefined(flatFieldMetadata.settings?.joinColumnName)) {
if (
flatFieldMetadata.settings?.relationType !== RelationType.MANY_TO_ONE
) {
throw new FlatEntityMapsException(
'Join column name is not defined for relation field',
'Cannot index a relation field that has no join column',
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
return flatFieldMetadata.settings.joinColumnName;
return computeMorphOrRelationFieldJoinColumnName({
name: flatFieldMetadata.name,
});
}
if (isCompositeFieldMetadataType(flatFieldMetadata.type)) {
@@ -1,6 +1,12 @@
import { type CompositeProperty, FieldMetadataType } from 'twenty-shared/types';
import {
type CompositeProperty,
FieldMetadataType,
RelationType,
} from 'twenty-shared/types';
import { type ColumnType } from 'typeorm';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/composite-field-metadata-type.type';
import {
computeColumnName,
@@ -109,14 +115,13 @@ const generateRelationColumnDefinition = (
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
>,
): WorkspaceSchemaColumnDefinition | null => {
if (
!flatFieldMetadata.settings ||
!flatFieldMetadata.settings.joinColumnName
) {
if (flatFieldMetadata.settings?.relationType !== RelationType.MANY_TO_ONE) {
return null;
}
const joinColumnName = flatFieldMetadata.settings.joinColumnName;
const joinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: flatFieldMetadata.name,
});
return {
name: joinColumnName,