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,8 +1,8 @@
import { computeMorphRelationFieldName } from '@/utils/fieldMetadata/compute-morph-relation-field-name';
import { computeMorphRelationGqlFieldName } from '@/utils/fieldMetadata/compute-morph-relation-gql-field-name';
describe('computeMorphRelationFieldName', () => {
describe('computeMorphRelationGqlFieldName', () => {
it('should return singular-based name for MANY_TO_ONE', () => {
const result = computeMorphRelationFieldName({
const result = computeMorphRelationGqlFieldName({
fieldName: 'assigned',
relationType: 'MANY_TO_ONE' as any,
targetObjectMetadataNameSingular: 'person',
@@ -13,7 +13,7 @@ describe('computeMorphRelationFieldName', () => {
});
it('should return plural-based name for ONE_TO_MANY', () => {
const result = computeMorphRelationFieldName({
const result = computeMorphRelationGqlFieldName({
fieldName: 'assigned',
relationType: 'ONE_TO_MANY' as any,
targetObjectMetadataNameSingular: 'person',
@@ -25,7 +25,7 @@ describe('computeMorphRelationFieldName', () => {
it('should throw for invalid relation type', () => {
expect(() =>
computeMorphRelationFieldName({
computeMorphRelationGqlFieldName({
fieldName: 'assigned',
relationType: 'INVALID' as any,
targetObjectMetadataNameSingular: 'person',
@@ -0,0 +1,76 @@
import {
computeMorphRelationGqlFieldJoinColumnName,
computeRelationGqlFieldJoinColumnName,
} from '@/utils/fieldMetadata/compute-relation-gql-field-join-column-name';
describe('computeRelationGqlFieldJoinColumnName', () => {
it('should append `Id` to a simple field name', () => {
expect(computeRelationGqlFieldJoinColumnName({ name: 'company' })).toBe(
'companyId',
);
});
it('should preserve camelCase field names', () => {
expect(
computeRelationGqlFieldJoinColumnName({ name: 'pointOfContact' }),
).toBe('pointOfContactId');
});
it('should append `Id` to a morph-aware field name', () => {
expect(
computeRelationGqlFieldJoinColumnName({ name: 'targetOpportunity' }),
).toBe('targetOpportunityId');
});
it('should not strip an existing trailing `Id` (it just appends)', () => {
expect(computeRelationGqlFieldJoinColumnName({ name: 'companyId' })).toBe(
'companyIdId',
);
});
});
describe('computeMorphRelationGqlFieldJoinColumnName', () => {
it('should combine field name and capitalized singular target for MANY_TO_ONE', () => {
expect(
computeMorphRelationGqlFieldJoinColumnName({
fieldName: 'target',
relationType: 'MANY_TO_ONE' as any,
targetObjectMetadataNameSingular: 'opportunity',
targetObjectMetadataNamePlural: 'opportunities',
}),
).toBe('targetOpportunityId');
});
it('should combine field name and capitalized plural target for ONE_TO_MANY', () => {
expect(
computeMorphRelationGqlFieldJoinColumnName({
fieldName: 'caretaker',
relationType: 'ONE_TO_MANY' as any,
targetObjectMetadataNameSingular: 'person',
targetObjectMetadataNamePlural: 'people',
}),
).toBe('caretakerPeopleId');
});
it('should handle simple plurals (e.g. `companies`)', () => {
expect(
computeMorphRelationGqlFieldJoinColumnName({
fieldName: 'parent',
relationType: 'ONE_TO_MANY' as any,
targetObjectMetadataNameSingular: 'company',
targetObjectMetadataNamePlural: 'companies',
}),
).toBe('parentCompaniesId');
});
it('should throw on an invalid relation type', () => {
expect(() =>
computeMorphRelationGqlFieldJoinColumnName({
fieldName: 'target',
relationType: 'INVALID' as any,
targetObjectMetadataNameSingular: 'opportunity',
targetObjectMetadataNamePlural: 'opportunities',
}),
).toThrow();
});
});
@@ -6,19 +6,19 @@ enum RelationType {
ONE_TO_MANY = 'ONE_TO_MANY',
}
type ComputeMorphRelationFieldNameArgs = {
type ComputeMorphRelationGqlFieldNameArgs = {
fieldName: string;
relationType: RelationType;
targetObjectMetadataNameSingular: string;
targetObjectMetadataNamePlural: string;
};
export const computeMorphRelationFieldName = ({
export const computeMorphRelationGqlFieldName = ({
fieldName,
relationType,
targetObjectMetadataNameSingular: nameSingular,
targetObjectMetadataNamePlural: namePlural,
}: ComputeMorphRelationFieldNameArgs): string => {
}: ComputeMorphRelationGqlFieldNameArgs): string => {
if (relationType === RelationType.MANY_TO_ONE) {
return `${fieldName}${capitalize(nameSingular)}`;
}
@@ -29,6 +29,6 @@ export const computeMorphRelationFieldName = ({
throw new CustomError(
`Invalid relation type (${relationType}) for field ${fieldName} on ${nameSingular}`,
'INVALID_RELATION_TYPE_FOR_COMPUTE_MORPH_RELATION_FIELD_NAME',
'INVALID_RELATION_TYPE_FOR_COMPUTE_MORPH_RELATION_GQL_FIELD_NAME',
);
};
@@ -0,0 +1,39 @@
import { computeMorphRelationGqlFieldName } from '@/utils/fieldMetadata/compute-morph-relation-gql-field-name';
enum RelationType {
MANY_TO_ONE = 'MANY_TO_ONE',
ONE_TO_MANY = 'ONE_TO_MANY',
}
type ComputeRelationGqlFieldJoinColumnNameArgs = {
name: string;
};
export const computeRelationGqlFieldJoinColumnName = ({
name,
}: ComputeRelationGqlFieldJoinColumnNameArgs): string => {
return `${name}Id`;
};
type ComputeMorphRelationGqlFieldJoinColumnNameArgs = {
fieldName: string;
relationType: RelationType;
targetObjectMetadataNameSingular: string;
targetObjectMetadataNamePlural: string;
};
export const computeMorphRelationGqlFieldJoinColumnName = ({
fieldName,
relationType,
targetObjectMetadataNameSingular,
targetObjectMetadataNamePlural,
}: ComputeMorphRelationGqlFieldJoinColumnNameArgs): string => {
const morphGqlFieldName = computeMorphRelationGqlFieldName({
fieldName,
relationType,
targetObjectMetadataNameSingular,
targetObjectMetadataNamePlural,
});
return computeRelationGqlFieldJoinColumnName({ name: morphGqlFieldName });
};