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
@@ -14,6 +14,7 @@ import {
isDefined,
} from 'twenty-shared/utils';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { transformActorField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-actor-field.util';
import { transformAddressField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-address-field.util';
import { transformArrayField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-array-field.util';
@@ -56,12 +57,10 @@ import { transformLinksValue } from 'src/engine/core-modules/record-transformer/
import { transformPhonesValue } from 'src/engine/core-modules/record-transformer/utils/transform-phones-value.util';
import { transformRichTextValue } from 'src/engine/core-modules/record-transformer/utils/transform-rich-text.util';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
@Injectable()
@@ -245,14 +244,9 @@ export class DataArgProcessorService {
);
}
const joinColumnName = isFlatFieldMetadataOfType(
fieldMetadata,
FieldMetadataType.MORPH_RELATION,
)
? computeMorphOrRelationFieldJoinColumnName({
name: fieldMetadata.name,
})
: relationSettings.joinColumnName;
const joinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: fieldMetadata.name,
});
if (key === joinColumnName) {
return validateUUIDFieldOrThrow(value, key);
@@ -8,6 +8,7 @@ import {
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { validateAndTransformOperatorAndValue } from 'src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-and-transform-operator-and-value.util';
@@ -17,7 +18,6 @@ import {
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/composite-field-metadata-type.type';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
@@ -4,6 +4,7 @@ import { FieldMetadataType, type ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type FindOptionsRelations, type ObjectLiteral } from 'typeorm';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
@@ -191,8 +192,9 @@ export class ProcessNestedRelationsV2Helper {
select: columnsToSelect,
});
const joinColumnName =
sourceFieldMetadata.settings.joinColumnName ?? `${sourceFieldName}Id`;
const joinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: sourceFieldName,
});
const relationIds = this.getUniqueIds({
records: parentObjectRecords,
@@ -200,14 +202,28 @@ export class ProcessNestedRelationsV2Helper {
relationType === RelationType.ONE_TO_MANY ? 'id' : joinColumnName,
});
if (
relationType === RelationType.ONE_TO_MANY &&
!isDefined(targetRelationName)
) {
throw new GraphqlQueryRunnerException(
`Could not resolve target relation for one-to-many field ${sourceFieldName}`,
GraphqlQueryRunnerExceptionCode.RELATION_TARGET_OBJECT_METADATA_NOT_FOUND,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
const fieldMetadataTargetRelationColumnName =
targetRelation &&
isFieldMetadataEntityOfType(
targetRelation,
FieldMetadataType.MORPH_RELATION,
)
? `${targetRelation.settings?.joinColumnName}`
: `${targetRelationName}Id`;
computeMorphOrRelationFieldJoinColumnName({
name:
targetRelation &&
isFieldMetadataEntityOfType(
targetRelation,
FieldMetadataType.MORPH_RELATION,
)
? targetRelation.name
: (targetRelationName as string),
});
const { relationResults, relationAggregatedFieldsResult } =
await this.findRelations({
@@ -15,6 +15,7 @@ import { isDefined } from 'twenty-shared/utils';
import { FindOptionsRelations, In, ObjectLiteral } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import {
CommonQueryRunnerException,
@@ -385,10 +386,7 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ
| FieldMetadataSettingsMapping['RELATION']
| undefined;
if (
relationSettings?.relationType !== RelationType.MANY_TO_ONE ||
!relationSettings?.joinColumnName
) {
if (relationSettings?.relationType !== RelationType.MANY_TO_ONE) {
continue;
}
@@ -405,7 +403,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ
objectMetadata: objMetadata,
fieldName: field.name,
fieldId: field.id,
joinColumnName: relationSettings.joinColumnName,
joinColumnName: computeMorphOrRelationFieldJoinColumnName({
name: field.name,
}),
});
}
@@ -9,8 +9,8 @@ import {
compositeTypeDefinitions,
} from 'twenty-shared/types';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
@@ -11,8 +11,7 @@ export const getIsFlatFieldAJunctionRelationField = ({
const isJunctionRelationField =
isDefined(flatField.settings) &&
'relationType' in flatField.settings &&
flatField.settings.relationType === RelationType.MANY_TO_ONE &&
isDefined(flatField.settings.joinColumnName);
flatField.settings.relationType === RelationType.MANY_TO_ONE;
// TODO: refactor this when we remove hard-coded activity relations
const isActivityRelationField =
@@ -4,6 +4,7 @@ import {
} from 'twenty-shared/types';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { GraphqlQuerySelectedFieldsAggregateParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser';
import { GraphqlQuerySelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser';
import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/composite-field-metadata-type.type';
@@ -104,12 +105,11 @@ export class GraphqlQuerySelectedFieldsParser {
if (
isFlatFieldMetadataOfType(fieldMetadata, FieldMetadataType.RELATION)
) {
const joinColumnName = fieldMetadata.settings?.joinColumnName;
const joinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: fieldMetadata.name,
});
if (
isDefined(joinColumnName) &&
isDefined(graphqlSelectedFields[joinColumnName])
) {
if (isDefined(graphqlSelectedFields[joinColumnName])) {
accumulator.select[joinColumnName] = true;
}
@@ -149,12 +149,11 @@ export class GraphqlQuerySelectedFieldsParser {
continue;
}
const joinColumnName = fieldMetadata.settings?.joinColumnName;
const joinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: fieldMetadata.name,
});
if (
isDefined(joinColumnName) &&
isDefined(graphqlSelectedFields[joinColumnName])
) {
if (isDefined(graphqlSelectedFields[joinColumnName])) {
accumulator.select[joinColumnName] = true;
}
@@ -365,54 +365,4 @@ describe('buildColumnsToSelect', () => {
id: true,
});
});
it('should handle relation field without joinColumnName', () => {
const noJoinColumnCompanyField = createMockField({
id: companyFieldId,
type: FieldMetadataType.RELATION,
name: 'company',
label: 'Company',
objectMetadataId: personObjectId,
defaultValue: null,
settings: {
relationType: RelationType.MANY_TO_ONE,
joinColumnName: null,
},
relationTargetObjectMetadataId: companyObjectId,
});
const flatObjectMetadata = buildMockFlatObjectMetadata([
nameFieldId,
companyFieldId,
]);
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([
nameField,
noJoinColumnCompanyField,
]);
const flatObjectMetadataMaps = buildFlatObjectMetadataMaps([
flatObjectMetadata,
companyObjectMetadata,
]);
const select = {
nameFirstName: true,
};
const relations = {
company: {},
};
const result = buildColumnsToSelect({
select,
relations,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
});
expect(result).toEqual({
nameFirstName: true,
id: true,
});
});
});
@@ -1,6 +1,7 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
@@ -64,13 +65,14 @@ const getRequiredRelationColumns = (
if (
!isDefined(relationValue) ||
!isDefined(fieldMetadata?.settings?.joinColumnName) ||
fieldMetadata.settings?.relationType !== RelationType.MANY_TO_ONE
) {
continue;
}
requiredColumns.push(fieldMetadata.settings.joinColumnName);
requiredColumns.push(
computeMorphOrRelationFieldJoinColumnName({ name: fieldMetadata.name }),
);
}
if (
@@ -84,7 +86,7 @@ const getRequiredRelationColumns = (
: undefined;
if (
!fieldMetadata.settings?.relationType ||
fieldMetadata.settings?.relationType !== RelationType.MANY_TO_ONE ||
!isDefined(targetObjectMetadata)
) {
continue;
@@ -92,14 +94,13 @@ const getRequiredRelationColumns = (
const relationValue = relations[fieldMetadata.name];
if (
!isDefined(relationValue) ||
!isDefined(fieldMetadata?.settings?.joinColumnName)
) {
if (!isDefined(relationValue)) {
continue;
}
requiredColumns.push(fieldMetadata.settings.joinColumnName);
requiredColumns.push(
computeMorphOrRelationFieldJoinColumnName({ name: fieldMetadata.name }),
);
}
}
@@ -1,5 +1,6 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
export const extractGraphQLRelationFieldNames = (
@@ -7,14 +8,10 @@ export const extractGraphQLRelationFieldNames = (
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
>,
) => {
const settings = fieldMetadata.settings;
const joinColumnName = settings?.joinColumnName;
if (!joinColumnName) {
throw new Error('Join column name is not defined');
}
const fieldMetadataName = fieldMetadata.name;
const joinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: fieldMetadataName,
});
return { joinColumnName, fieldMetadataName };
};
@@ -1,9 +1,9 @@
type ComputeRelationFieldJoinColumnNameArgs = {
type ComputeMorphOrRelationFieldJoinColumnNameArgs = {
name: string;
};
export const computeMorphOrRelationFieldJoinColumnName = ({
name,
}: ComputeRelationFieldJoinColumnNameArgs) => {
}: ComputeMorphOrRelationFieldJoinColumnNameArgs): string => {
return `${name}Id`;
};
@@ -0,0 +1,34 @@
import { RelationType } from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils';
import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
type ComputeMorphRelationFlatFieldNameArgs = {
fieldName: string;
relationType: RelationType;
targetObjectMetadataNameSingular: string;
targetObjectMetadataNamePlural: string;
};
export const computeMorphRelationFlatFieldName = ({
fieldName,
relationType,
targetObjectMetadataNameSingular: nameSingular,
targetObjectMetadataNamePlural: namePlural,
}: ComputeMorphRelationFlatFieldNameArgs): string => {
if (relationType === RelationType.MANY_TO_ONE) {
return `${fieldName}${capitalize(nameSingular)}`;
}
if (relationType === RelationType.ONE_TO_MANY) {
return `${fieldName}${capitalize(namePlural)}`;
}
throw new FieldMetadataException(
`Invalid relation type (${relationType}) for field ${fieldName} on ${nameSingular}`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
};
@@ -1,7 +1,7 @@
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
@@ -2,10 +2,11 @@ import {
FieldMetadataType,
type RelationUpdatePayload,
} from 'twenty-shared/types';
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
import { isDefined } from 'twenty-shared/utils';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { computeMorphRelationFlatFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-flat-field-name.util';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
@@ -89,7 +90,7 @@ export const computeFlatFieldToUpdateFromMorphRelationUpdatePayload = ({
flatEntityMaps: flatObjectMetadataMaps,
});
const computedMorphName = computeMorphRelationFieldName({
const computedMorphName = computeMorphRelationFlatFieldName({
fieldName: morphNameWithoutObjectName,
relationType: fieldMetadataToUpdate.settings.relationType,
targetObjectMetadataNameSingular: newTargetObjectMetadata.nameSingular,
@@ -1,12 +1,13 @@
import { msg } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { computeMorphRelationFlatFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-flat-field-name.util';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type';
@@ -86,7 +87,7 @@ export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({
const morphId = v4();
const flatFieldsAndIndexes = morphRelationCreationPayload.reduce(
(acc, { relationCreationPayload, targetFlatObjectMetadata }) => {
const currentMorphRelationFieldName = computeMorphRelationFieldName({
const currentMorphRelationFieldName = computeMorphRelationFlatFieldName({
fieldName: createFieldInput.name,
relationType: relationCreationPayload.type,
targetObjectMetadataNameSingular: targetFlatObjectMetadata.nameSingular,
@@ -2,10 +2,10 @@ import { msg } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type';
@@ -4,12 +4,13 @@ import {
RelationOnDeleteAction,
RelationType,
} from 'twenty-shared/types';
import { v4 } from 'uuid';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { generateIndexForFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-index-for-flat-field-metadata.util';
import { getDefaultFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util';
@@ -1,6 +1,6 @@
import { RelationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-universal-identifier-in-universal-flat-entity-maps.util';
import { isMorphOrRelationUniversalFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { type UniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-maps.type';
@@ -39,15 +39,16 @@ export const getObjectFieldNamesAndJoinColumnNames = ({
universalFlatFieldMetadata,
) &&
universalFlatFieldMetadata.universalSettings.relationType ===
RelationType.MANY_TO_ONE &&
isDefined(universalFlatFieldMetadata.universalSettings.joinColumnName)
RelationType.MANY_TO_ONE
) {
return {
...acc,
fieldNames: [...acc.fieldNames, universalFlatFieldMetadata.name],
joinColumnNames: [
...acc.joinColumnNames,
universalFlatFieldMetadata.universalSettings.joinColumnName,
computeMorphOrRelationFieldJoinColumnName({
name: universalFlatFieldMetadata.name,
}),
],
};
}
@@ -3,9 +3,9 @@ import {
type FieldMetadataType,
type FromTo,
} from 'twenty-shared/types';
import { computeMorphRelationFieldName } from 'twenty-shared/utils';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { computeMorphRelationFlatFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-flat-field-name.util';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
@@ -37,7 +37,7 @@ const updateMorphFlatFieldName = ({
namePlural: fromRelationTargetFlatObjectMetadata.namePlural,
});
const newMorphFieldName = computeMorphRelationFieldName({
const newMorphFieldName = computeMorphRelationFlatFieldName({
fieldName: initialMorphRelationFieldName,
relationType: fromMorphFlatFieldMetadata.universalSettings.relationType,
targetObjectMetadataNameSingular:
@@ -1,9 +1,11 @@
import { RelationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { isMorphOrRelationUniversalFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { generateDeterministicIndexNameV2 } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name-v2';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
@@ -36,11 +38,15 @@ export const generateFlatIndexMetadataWithNameOrThrow = ({
);
}
const name = isMorphOrRelationUniversalFlatFieldMetadata(
relatedFlatFieldMetadata,
)
? (relatedFlatFieldMetadata.universalSettings.joinColumnName ??
relatedFlatFieldMetadata.name)
const isManyToOneRelation =
isMorphOrRelationUniversalFlatFieldMetadata(relatedFlatFieldMetadata) &&
relatedFlatFieldMetadata.universalSettings?.relationType ===
RelationType.MANY_TO_ONE;
const name = isManyToOneRelation
? computeMorphOrRelationFieldJoinColumnName({
name: relatedFlatFieldMetadata.name,
})
: relatedFlatFieldMetadata.name;
return {
@@ -5,10 +5,10 @@ import {
import { FieldMetadataType } from 'twenty-shared/types';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { generateMorphOrRelationFlatFieldMetadataPair } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util';
@@ -7,16 +7,13 @@ import {
import { isDefined } from 'twenty-shared/utils';
import { type ColumnType, type EntitySchemaColumnOptions } from 'typeorm';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
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 { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
import {
TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import {
type EntitySchemaFieldMetadata,
type EntitySchemaFieldMetadataMaps,
@@ -57,18 +54,14 @@ export class EntitySchemaColumnFactory {
if (isRelation) {
const isManyToOneRelation =
fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE;
const joinColumnName = fieldMetadata.settings?.joinColumnName;
if (!isManyToOneRelation) {
continue;
}
if (!isDefined(joinColumnName)) {
throw new TwentyORMException(
`Field ${fieldMetadata.id} of type ${fieldMetadata.type} is a many to one relation but does not have a join column name`,
TwentyORMExceptionCode.MALFORMED_METADATA,
);
}
const joinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: fieldMetadata.name,
});
entitySchemaColumnMap[joinColumnName] = {
name: joinColumnName,
@@ -1,4 +1,8 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import {
type FieldMetadataType,
RelationType as TwentyRelationType,
} from 'twenty-shared/types';
import { type RelationType } from 'typeorm/metadata/types/RelationTypes';
import {
@@ -10,6 +14,7 @@ import {
type EntitySchemaFieldMetadataMaps,
type EntitySchemaObjectMetadataMaps,
} from 'src/engine/twenty-orm/global-workspace-datasource/types/entity-schema-metadata.type';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { converRelationTypeToTypeORMRelationType } from 'src/engine/twenty-orm/utils/convert-relation-type-to-typeorm-relation-type.util';
interface RelationDetails {
@@ -65,12 +70,19 @@ export function determineSchemaRelationDetails(
throw new Error('Target field metadata not found');
}
const isManyToOne =
fieldMetadata.settings.relationType === TwentyRelationType.MANY_TO_ONE;
return {
relationType,
target: targetObjectMetadata.nameSingular,
inverseSide: targetFieldMetadata.name,
joinColumn: fieldMetadata.settings.joinColumnName
? { name: fieldMetadata.settings.joinColumnName }
joinColumn: isManyToOne
? {
name: computeMorphOrRelationFieldJoinColumnName({
name: fieldMetadata.name,
}),
}
: undefined,
};
}
@@ -1,8 +1,8 @@
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import {
type FieldMetadataSettingsMapping,
RelationType,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const formatColumnNameForRelationField = (
fieldName: string,
@@ -13,11 +13,7 @@ export const formatColumnNameForRelationField = (
}
if (fieldMetadataSettings.relationType === RelationType.MANY_TO_ONE) {
if (!isDefined(fieldMetadataSettings.joinColumnName)) {
throw new Error(`Join column name is not defined for field ${fieldName}`);
}
return fieldMetadataSettings.joinColumnName;
return computeMorphOrRelationFieldJoinColumnName({ name: fieldName });
}
return fieldName;
@@ -47,6 +47,7 @@ import {
isMatchingUUIDFilter,
} from 'twenty-shared/utils';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
@@ -208,8 +209,8 @@ export const isRecordMatchingRLSRowLevelPermissionPredicate = ({
objectFields.find(
(field) =>
field.type === FieldMetadataType.RELATION &&
(field.settings as { joinColumnName?: string } | undefined)
?.joinColumnName === filterKey,
computeMorphOrRelationFieldJoinColumnName({ name: field.name }) ===
filterKey,
);
if (!isDefined(objectMetadataField)) {
@@ -412,11 +413,9 @@ export const isRecordMatchingRLSRowLevelPermissionPredicate = ({
}
case FieldMetadataType.RELATION: {
const isJoinColumn =
(
objectMetadataField.settings as
| { joinColumnName?: string }
| undefined
)?.joinColumnName === filterKey;
computeMorphOrRelationFieldJoinColumnName({
name: objectMetadataField.name,
}) === filterKey;
if (isJoinColumn) {
return isMatchingUUIDFilter({
@@ -5,10 +5,10 @@ import {
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
@@ -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,