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:
+15
-5
@@ -17,7 +17,11 @@ import { isFieldUuid } from '@/object-record/record-field/ui/types/guards/isFiel
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { buildOptimisticActorFieldValueFromCurrentWorkspaceMember } from '@/object-record/utils/buildOptimisticActorFieldValueFromCurrentWorkspaceMember';
|
||||
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
computeMorphRelationGqlFieldName,
|
||||
computeRelationGqlFieldJoinColumnName,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
type ComputeOptimisticCacheRecordInputArgs = {
|
||||
@@ -46,7 +50,8 @@ export const computeOptimisticRecordFromInput = ({
|
||||
objectMetadataItem.fields.find(
|
||||
(field) =>
|
||||
field.type === FieldMetadataType.RELATION &&
|
||||
field.settings?.joinColumnName === recordKey,
|
||||
computeRelationGqlFieldJoinColumnName({ name: field.name }) ===
|
||||
recordKey,
|
||||
);
|
||||
|
||||
const potentialMorphRelationJoinColumnNameFieldMetadataItem =
|
||||
@@ -200,7 +205,7 @@ export const computeOptimisticRecordFromInput = ({
|
||||
|
||||
const relationGqlFields = fieldMetadataItem.morphRelations?.map(
|
||||
(morphRelation) => {
|
||||
return computeMorphRelationFieldName({
|
||||
return computeMorphRelationGqlFieldName({
|
||||
fieldName: fieldMetadataItem.name,
|
||||
relationType,
|
||||
targetObjectMetadataNameSingular:
|
||||
@@ -212,10 +217,15 @@ export const computeOptimisticRecordFromInput = ({
|
||||
);
|
||||
|
||||
const relationGqlField = relationGqlFields?.find(
|
||||
(relationGqlField) => recordInput[`${relationGqlField}Id`],
|
||||
(relationGqlField) =>
|
||||
recordInput[
|
||||
computeRelationGqlFieldJoinColumnName({ name: relationGqlField })
|
||||
],
|
||||
);
|
||||
|
||||
const relationGqlFieldWithId = `${relationGqlField}Id`;
|
||||
const relationGqlFieldWithId = computeRelationGqlFieldJoinColumnName({
|
||||
name: relationGqlField ?? '',
|
||||
});
|
||||
|
||||
if (isUndefined(relationGqlField)) {
|
||||
continue;
|
||||
|
||||
@@ -4,7 +4,11 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
computeMorphRelationGqlFieldName,
|
||||
computeRelationGqlFieldJoinColumnName,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
type PrefillRecordArgs = {
|
||||
@@ -26,13 +30,15 @@ export const prefillRecord = <T extends ObjectRecord>({
|
||||
fieldMetadataItem.type === FieldMetadataType.RELATION &&
|
||||
fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE
|
||||
) {
|
||||
const joinColumnValue =
|
||||
input[fieldMetadataItem.settings?.joinColumnName] ?? null;
|
||||
const joinColumnName = computeRelationGqlFieldJoinColumnName({
|
||||
name: fieldMetadataItem.name,
|
||||
});
|
||||
const joinColumnValue = input[joinColumnName] ?? null;
|
||||
throwIfInputRelationDataIsInconsistent(input, fieldMetadataItem);
|
||||
|
||||
return [
|
||||
[fieldMetadataItem.name, fieldValue],
|
||||
[fieldMetadataItem.settings?.joinColumnName, joinColumnValue],
|
||||
[joinColumnName, joinColumnValue],
|
||||
];
|
||||
}
|
||||
if (
|
||||
@@ -41,7 +47,7 @@ export const prefillRecord = <T extends ObjectRecord>({
|
||||
) {
|
||||
const gqlFields = fieldMetadataItem.morphRelations?.map(
|
||||
(morphRelation) => {
|
||||
return computeMorphRelationFieldName({
|
||||
return computeMorphRelationGqlFieldName({
|
||||
fieldName: fieldMetadataItem.name,
|
||||
relationType: morphRelation.type,
|
||||
targetObjectMetadataNameSingular:
|
||||
@@ -52,10 +58,15 @@ export const prefillRecord = <T extends ObjectRecord>({
|
||||
},
|
||||
);
|
||||
|
||||
return gqlFields?.flatMap((gqlField) => [
|
||||
[gqlField, fieldValue],
|
||||
[`${gqlField}Id`, input[`${gqlField}Id`] ?? null],
|
||||
]);
|
||||
return gqlFields?.flatMap((gqlField) => {
|
||||
const joinColumnName = computeRelationGqlFieldJoinColumnName({
|
||||
name: gqlField,
|
||||
});
|
||||
return [
|
||||
[gqlField, fieldValue],
|
||||
[joinColumnName, input[joinColumnName] ?? null],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
return [[fieldMetadataItem.name, fieldValue]];
|
||||
@@ -70,7 +81,9 @@ const throwIfInputRelationDataIsInconsistent = (
|
||||
fieldMetadataItem: FieldMetadataItem,
|
||||
) => {
|
||||
const inputValue = input[fieldMetadataItem.name];
|
||||
const relationIdFieldName = `${fieldMetadataItem.name}Id`;
|
||||
const relationIdFieldName = computeRelationGqlFieldJoinColumnName({
|
||||
name: fieldMetadataItem.name,
|
||||
});
|
||||
if (isDefined(inputValue) && !isDefined(input[relationIdFieldName])) {
|
||||
throw new Error(
|
||||
`Inconsistent input: ${fieldMetadataItem.name} is specified but ${relationIdFieldName} is missing`,
|
||||
|
||||
@@ -2,7 +2,11 @@ import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/Enriche
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isSystemSearchVectorField } from '@/object-record/utils/isSystemSearchVectorField';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
computeMorphRelationGqlFieldName,
|
||||
computeRelationGqlFieldJoinColumnName,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const sanitizeRecordInput = ({
|
||||
@@ -26,13 +30,14 @@ export const sanitizeRecordInput = ({
|
||||
objectMetadataItem.fields.find(
|
||||
(field) =>
|
||||
field.type === FieldMetadataType.RELATION &&
|
||||
field.settings?.joinColumnName === fieldName,
|
||||
computeRelationGqlFieldJoinColumnName({ name: field.name }) ===
|
||||
fieldName,
|
||||
);
|
||||
const potentialMorphRelationJoinColumnNameFieldMetadataItem =
|
||||
objectMetadataItem.fields.find((field) => {
|
||||
if (!isFieldMorphRelation(field)) return false;
|
||||
return field.morphRelations?.some((morphRelation) => {
|
||||
const computedFieldName = computeMorphRelationFieldName({
|
||||
const computedFieldName = computeMorphRelationGqlFieldName({
|
||||
fieldName: field.name,
|
||||
relationType: morphRelation.type,
|
||||
targetObjectMetadataNameSingular:
|
||||
|
||||
Reference in New Issue
Block a user