From 9e816187731078ce171bf6178c38ead90a74e1bf Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:20:18 +0200 Subject: [PATCH] Refactor morph field name and morph data loader (#14299) # Introduction Storing morph relation field names directly in database, using morphId to aggregate them Removing dynamic morph field metadata computation in schemas and data loader Will add integration tests on morph data loader entry closes https://github.com/twentyhq/core-team-issues/issues/1425 closes https://github.com/twentyhq/core-team-issues/issues/1424 closes https://github.com/twentyhq/core-team-issues/issues/1423 --- .../utils/mapFieldMetadataToGraphQLQuery.ts | 22 +- .../utils/mapObjectMetadataToGraphQLQuery.ts | 33 +-- .../useAttachMorphRelatedRecordFromRecord.ts | 14 +- .../useDetachMorphRelatedRecordFromRecord.ts | 18 +- .../hooks/useRecordTableRecordGqlFields.ts | 9 +- ...yToOneValueWithObjectNameFamilySelector.ts | 11 +- ...ToManyValueWithObjectNameFamilySelector.ts | 13 +- .../SettingsDataModelRelationFieldPreview.tsx | 12 +- ...data-name-for-workspace-object-metadata.ts | 19 ++ .../graphql-selected-fields.parser.ts | 11 +- ...ct-records-to-graphql-connection.helper.ts | 49 ++-- .../utils/build-columns-to-select.ts | 10 +- ...-field-metadata-from-graphql-field.util.ts | 9 +- ...xtend-object-type-definition-v2.factory.ts | 12 +- .../dataloaders/dataloader.interface.ts | 20 +- .../engine/dataloaders/dataloader.service.ts | 222 ++++++++++++------ ...er-morph-relation-duplicate-fields.util.ts | 8 +- .../field-metadata/dtos/field-metadata.dto.ts | 5 +- .../field-metadata/field-metadata.entity.ts | 6 + .../field-metadata/field-metadata.resolver.ts | 100 ++------ .../field-metadata-morph-relation.service.ts | 159 ++----------- .../field-metadata-relation.service.ts | 113 +-------- .../services/field-metadata.service-v2.ts | 51 ++-- .../services/field-metadata.service.ts | 103 +++----- .../utils/are-field-metadatas-of-type.util.ts | 12 + ...as-type-relation-or-morph-relation.util.ts | 20 -- ...r-relation-field-join-column-name.util.ts} | 2 +- .../compute-morph-relation-field-name.util.ts | 14 +- ...e-field-metadata-standard-override.util.ts | 1 + ...lat-field-metadatas-to-create.spec.ts.snap | 12 +- ...tion-flat-field-metadatas-or-throw.util.ts | 56 +++++ ...related-to-morph-relation-or-throw.util.ts | 75 ++---- ...eld-metadata-to-field-metadata-dto.util.ts | 31 ++- ...lat-field-metadata-to-relation-dto.util.ts | 26 ++ ...ield-input-to-flat-field-metadatas.util.ts | 19 +- ...ield-input-to-flat-field-metadatas.util.ts | 9 +- ...-relation-flat-field-metadata-pair.util.ts | 4 +- ...ct-metadata-to-object-metadata-dto.util.ts | 39 ++- ...ject-input-to-flat-object-metadata.util.ts | 16 +- ...tion-flat-field-metadatas-or-throw.util.ts | 10 +- ...etadatas-settings-join-column-name.util.ts | 46 ---- ...morph-field-on-object-names-update.util.ts | 87 +++++++ .../object-metadata-field-relation.service.ts | 16 +- .../workspace-metadata-version.service.ts | 2 +- .../entity-schema-relation.factory.ts | 12 +- ...et-custom-relation-field-seeds.constant.ts | 15 ++ ...etadata-morph-relation.integration-spec.ts | 97 ++++---- ...ield-metadata-relation.integration-spec.ts | 22 +- ...ith-morph-relation-v2.integration-spec.ts} | 102 +++++--- ...ate-morph-relation-between-objects.util.ts | 1 + .../force-create-one-object-metadata.util.ts | 2 +- ...ph-relation-field-join-column-name.util.ts | 13 - .../compute-morph-relation-field-name.util.ts | 34 --- packages/twenty-shared/src/utils/index.ts | 2 - 54 files changed, 807 insertions(+), 1019 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1756976545860-unique-field-metadata-name-for-workspace-object-metadata.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/are-field-metadatas-of-type.util.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/are-field-metadatas-type-relation-or-morph-relation.util.ts rename packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/{compute-relation-field-join-column-name.util.ts => compute-morph-or-relation-field-join-column-name.util.ts} (71%) create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-or-relation-flat-field-metadata-to-relation-dto.util.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-flat-object-metadata-morph-relation-flat-field-metadatas-settings-join-column-name.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts rename packages/twenty-server/test/integration/metadata/suites/object-metadata/morph-relation/{rename-object-metadata-with-morph-relation.integration-spec.ts => rename-object-metadata-with-morph-relation-v2.integration-spec.ts} (53%) delete mode 100644 packages/twenty-shared/src/utils/fieldMetadata/compute-morph-relation-field-join-column-name.util.ts delete mode 100644 packages/twenty-shared/src/utils/fieldMetadata/compute-morph-relation-field-name.util.ts diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts index 4060cd8c55..80a83bb336 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts @@ -6,11 +6,7 @@ import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObject import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; import { isNonCompositeField } from '@/object-record/object-filter-dropdown/utils/isNonCompositeField'; import { type ObjectPermissions } from 'twenty-shared/types'; -import { - computeMorphRelationFieldJoinColumnName, - computeMorphRelationFieldName, - isDefined, -} from 'twenty-shared/utils'; +import { isDefined } from 'twenty-shared/utils'; import { type FieldMetadataItem } from '../types/FieldMetadataItem'; type MapFieldMetadataToGraphQLQueryArgs = { @@ -44,6 +40,8 @@ export const mapFieldMetadataToGraphQLQuery = ({ return gqlField; } + // We could factorize morph relation fields mapping to be passing through the RELATION handler too as now they share + // the same name and join column name logic if ( fieldType === FieldMetadataType.MORPH_RELATION && (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY || @@ -51,12 +49,7 @@ export const mapFieldMetadataToGraphQLQuery = ({ ) { let gqlMorphField = ''; for (const morphRelation of fieldMetadata.morphRelations ?? []) { - const relationFieldName = computeMorphRelationFieldName({ - fieldName: fieldMetadata.name, - relationDirection: fieldMetadata.settings?.relationType, - nameSingular: morphRelation.targetObjectMetadata.nameSingular, - namePlural: morphRelation.targetObjectMetadata.namePlural, - }); + const relationFieldName = morphRelation.sourceFieldMetadata.name; const relationMetadataItem = objectMetadataItems.find( (objectMetadataItem) => objectMetadataItem.id === morphRelation.targetObjectMetadata.id, @@ -103,12 +96,7 @@ export const mapFieldMetadataToGraphQLQuery = ({ } if (fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE) { - const joinColumnName = computeMorphRelationFieldJoinColumnName({ - name: fieldMetadata.name, - targetObjectMetadataNameSingular: - morphRelation.targetObjectMetadata.nameSingular, - }); - if (gqlField === joinColumnName) { + if (gqlField === fieldMetadata.settings?.joinColumnName) { gqlMorphField += `${gqlField} `; continue; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts index 32faf894d0..cd9ac793a7 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts @@ -5,7 +5,7 @@ import { shouldFieldBeQueried } from '@/object-metadata/utils/shouldFieldBeQueri import { type RecordGqlFields } from '@/object-record/graphql/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 { isDefined } from 'twenty-shared/utils'; type MapObjectMetadataToGraphQLQueryArgs = { objectMetadataItems: ObjectMetadataItem[]; @@ -54,29 +54,25 @@ export const mapObjectMetadataToGraphQLQuery = ({ .filter((field) => isDefined(field.settings?.joinColumnName)); const manyToOneRelationGqlFieldWithFieldMetadata = - manyToOneRelationFields.flatMap((field) => { - const isMorphRelation = field.type === FieldMetadataType.MORPH_RELATION; + manyToOneRelationFields.flatMap((fieldMetadata) => { + const isMorphRelation = + fieldMetadata.type === FieldMetadataType.MORPH_RELATION; if (!isMorphRelation) { return { - gqlField: field.settings?.joinColumnName, - fieldMetadata: field, + gqlField: fieldMetadata.settings?.joinColumnName, + fieldMetadata: fieldMetadata, }; } - if (!isDefined(field.morphRelations)) { + if (!isDefined(fieldMetadata.morphRelations)) { throw new Error( - `Field ${field.name} is missing, please refresh the page. If the problem persists, please contact support.`, + `Field ${fieldMetadata.name} is missing, please refresh the page. If the problem persists, please contact support.`, ); } - return field.morphRelations.map((morphRelation) => ({ - gqlField: computeMorphRelationFieldName({ - fieldName: field.name, - relationDirection: morphRelation.type, - nameSingular: morphRelation.targetObjectMetadata.nameSingular, - namePlural: morphRelation.targetObjectMetadata.namePlural, - }), - fieldMetadata: field, + return fieldMetadata.morphRelations.map((morphRelation) => ({ + gqlField: morphRelation.sourceFieldMetadata.name, + fieldMetadata: fieldMetadata, })); }); @@ -103,12 +99,7 @@ export const mapObjectMetadataToGraphQLQuery = ({ } return fieldMetadata.morphRelations.map((morphRelation) => ({ - gqlField: computeMorphRelationFieldName({ - fieldName: fieldMetadata.name, - relationDirection: morphRelation.type, - nameSingular: morphRelation.targetObjectMetadata.nameSingular, - namePlural: morphRelation.targetObjectMetadata.namePlural, - }), + gqlField: morphRelation.sourceFieldMetadata.name, fieldMetadata, })); }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useAttachMorphRelatedRecordFromRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useAttachMorphRelatedRecordFromRecord.ts index 02df02bb21..ec18f90264 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useAttachMorphRelatedRecordFromRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useAttachMorphRelatedRecordFromRecord.ts @@ -12,7 +12,7 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { useContext } from 'react'; import { useRecoilCallback } from 'recoil'; -import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; +import { isDefined } from 'twenty-shared/utils'; export const useAttachMorphRelatedRecordFromRecord = () => { const apolloCoreClient = useApolloCoreClient(); @@ -61,14 +61,8 @@ export const useAttachMorphRelatedRecordFromRecord = () => { if (isDefined(parentRecord)) { relatedObjectMetadataItems.forEach((relatedObjectMetadataItem) => { - const computedFieldName = computeMorphRelationFieldName({ - fieldName: fieldDefinition.metadata.fieldName, - relationDirection: fieldDefinition.metadata.relationType, - nameSingular: relatedObjectMetadataItem.nameSingular, - namePlural: relatedObjectMetadataItem.namePlural, - }); - - const currentMorphFieldValue = parentRecord[computedFieldName]; + const currentMorphFieldValue = + parentRecord[fieldDefinition.metadata.fieldName]; const objectRecordFromCache = getRecordFromCache({ objectMetadataItem: relatedObjectMetadataItem, @@ -84,7 +78,7 @@ export const useAttachMorphRelatedRecordFromRecord = () => { set(recordStoreFamilyState(recordId), { ...parentRecord, - [computedFieldName]: [ + [fieldDefinition.metadata.fieldName]: [ ...currentMorphFieldValue, objectRecordFromCache, ], diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDetachMorphRelatedRecordFromRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useDetachMorphRelatedRecordFromRecord.ts index 50307e3b1e..fca0b20b18 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDetachMorphRelatedRecordFromRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDetachMorphRelatedRecordFromRecord.ts @@ -13,7 +13,7 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { useContext } from 'react'; import { useRecoilCallback } from 'recoil'; -import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; +import { isDefined } from 'twenty-shared/utils'; export const useDetachMorphRelatedRecordFromRecord = () => { const apolloCoreClient = useApolloCoreClient(); @@ -61,15 +61,8 @@ export const useDetachMorphRelatedRecordFromRecord = () => { if (isDefined(parentRecord)) { relatedObjectMetadataItems.forEach((relatedObjectMetadataItem) => { - const computedFieldName = computeMorphRelationFieldName({ - fieldName: fieldDefinition.metadata.fieldName, - relationDirection: fieldDefinition.metadata.relationType, - nameSingular: relatedObjectMetadataItem.nameSingular, - namePlural: relatedObjectMetadataItem.namePlural, - }); - const currentMorphFieldValue = parentRecord[ - computedFieldName + fieldDefinition.metadata.fieldName ] as ObjectRecord[]; const objectRecordFromCache = getRecordFromCache({ @@ -87,9 +80,10 @@ export const useDetachMorphRelatedRecordFromRecord = () => { if (Array.isArray(currentMorphFieldValue)) { set(recordStoreFamilyState(recordId), { ...parentRecord, - [computedFieldName]: currentMorphFieldValue.filter( - (record) => record.id !== relatedRecordId, - ), + [fieldDefinition.metadata.fieldName]: + currentMorphFieldValue.filter( + (record) => record.id !== relatedRecordId, + ), }); } }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordTableRecordGqlFields.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordTableRecordGqlFields.ts index fcc85e3b8e..a396b04b5f 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordTableRecordGqlFields.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordTableRecordGqlFields.ts @@ -7,7 +7,7 @@ import { visibleRecordFieldsComponentSelector } from '@/object-record/record-fie import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { FieldMetadataType } from 'twenty-shared/types'; -import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; +import { isDefined } from 'twenty-shared/utils'; export const useRecordTableRecordGqlFields = ({ objectMetadataItem, @@ -58,12 +58,7 @@ export const useRecordTableRecordGqlFields = ({ } return fieldMetadataItem.morphRelations.map((morphRelation) => [ - computeMorphRelationFieldName({ - fieldName: fieldMetadataItem.name, - relationDirection: morphRelation.type, - nameSingular: morphRelation.targetObjectMetadata.nameSingular, - namePlural: morphRelation.targetObjectMetadata.namePlural, - }), + morphRelation.sourceFieldMetadata.name, true, ]); }), diff --git a/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreMorphManyToOneValueWithObjectNameFamilySelector.ts b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreMorphManyToOneValueWithObjectNameFamilySelector.ts index a65303f5f5..af89493aaa 100644 --- a/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreMorphManyToOneValueWithObjectNameFamilySelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreMorphManyToOneValueWithObjectNameFamilySelector.ts @@ -3,8 +3,7 @@ import { selectorFamily } from 'recoil'; import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { RelationType } from 'twenty-shared/types'; -import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; +import { isDefined } from 'twenty-shared/utils'; export const recordStoreMorphManyToOneValueWithObjectNameFamilySelector = selectorFamily({ @@ -22,17 +21,11 @@ export const recordStoreMorphManyToOneValueWithObjectNameFamilySelector = ({ get }) => { const morphValuesWithObjectName = morphRelations.map( (morphRelation) => { - const computedFieldName = computeMorphRelationFieldName({ - fieldName, - relationDirection: RelationType.MANY_TO_ONE, - nameSingular: morphRelation.targetObjectMetadata.nameSingular, - namePlural: morphRelation.targetObjectMetadata.namePlural, - }); return { objectNameSingular: morphRelation.targetObjectMetadata.nameSingular, value: get(recordStoreFamilyState(recordId))?.[ - computedFieldName + fieldName ] as ObjectRecord, }; }, diff --git a/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreMorphOneToManyValueWithObjectNameFamilySelector.ts b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreMorphOneToManyValueWithObjectNameFamilySelector.ts index 101b1c453e..5d746e095f 100644 --- a/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreMorphOneToManyValueWithObjectNameFamilySelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreMorphOneToManyValueWithObjectNameFamilySelector.ts @@ -3,8 +3,6 @@ import { selectorFamily } from 'recoil'; import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { RelationType } from 'twenty-shared/types'; -import { computeMorphRelationFieldName } from 'twenty-shared/utils'; export const recordStoreMorphOneToManyValueWithObjectNameFamilySelector = selectorFamily({ @@ -22,18 +20,11 @@ export const recordStoreMorphOneToManyValueWithObjectNameFamilySelector = ({ get }) => { const morphValuesWithObjectName = morphRelations.map( (morphRelation) => { - const computedFieldName = computeMorphRelationFieldName({ - fieldName, - relationDirection: RelationType.ONE_TO_MANY, - nameSingular: morphRelation.targetObjectMetadata.nameSingular, - namePlural: morphRelation.targetObjectMetadata.namePlural, - }); return { objectNameSingular: morphRelation.targetObjectMetadata.nameSingular, - value: (get(recordStoreFamilyState(recordId))?.[ - computedFieldName - ] || []) as ObjectRecord[], + value: (get(recordStoreFamilyState(recordId))?.[fieldName] || + []) as ObjectRecord[], }; }, ); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreview.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreview.tsx index 7a1926474e..ee4cfe1d25 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreview.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreview.tsx @@ -9,7 +9,6 @@ import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldCont import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext'; import { SettingsDataModelSetFieldValueEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetFieldValueEffect'; import { useFieldPreviewValue } from '@/settings/data-model/fields/preview/hooks/useFieldPreviewValue'; -import { computeMorphRelationFieldName } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/display'; import { v4 } from 'uuid'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -118,16 +117,7 @@ export const SettingsDataModelRelationFieldPreview = ({ > diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1756976545860-unique-field-metadata-name-for-workspace-object-metadata.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1756976545860-unique-field-metadata-name-for-workspace-object-metadata.ts new file mode 100644 index 0000000000..602e4b054e --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1756976545860-unique-field-metadata-name-for-workspace-object-metadata.ts @@ -0,0 +1,19 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class UniqueFieldMetadataNameForWorkspaceObjectMetadata1756976545860 + implements MigrationInterface +{ + name = 'UniqueFieldMetadataNameForWorkspaceObjectMetadata1756976545860'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."fieldMetadata" ADD CONSTRAINT "IDX_FIELD_METADATA_NAME_OBJECT_METADATA_ID_WORKSPACE_ID_UNIQUE" UNIQUE ("name", "objectMetadataId", "workspaceId")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."fieldMetadata" DROP CONSTRAINT "IDX_FIELD_METADATA_NAME_OBJECT_METADATA_ID_WORKSPACE_ID_UNIQUE"`, + ); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts index 480ab83907..79de6f9985 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts @@ -5,7 +5,6 @@ import { GraphqlQuerySelectedFieldsAggregateParser } from 'src/engine/api/graphq import { GraphqlQuerySelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @@ -134,14 +133,8 @@ export class GraphqlQuerySelectedFieldsParser { accumulator.select[joinColumnName] = true; } - const morphRelationFieldName = computeMorphRelationFieldName({ - fieldName: fieldMetadata.name, - relationDirection: fieldMetadata.settings.relationType, - targetObjectMetadata, - }); - const graphqlSelectedFieldValue = - graphqlSelectedFields[morphRelationFieldName]; + graphqlSelectedFields[fieldMetadata.name]; if (!isDefined(graphqlSelectedFieldValue)) { continue; @@ -149,7 +142,7 @@ export class GraphqlQuerySelectedFieldsParser { this.graphqlQuerySelectedFieldsRelationParser.parseRelationField( fieldMetadata, - morphRelationFieldName, + fieldMetadata.name, graphqlSelectedFieldValue, accumulator, ); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts index baaee0b5f8..91cbd87fef 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts @@ -17,7 +17,6 @@ import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-q import { type AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util'; import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util'; @@ -255,52 +254,44 @@ export class ObjectRecordsToGraphqlConnectionHelper { continue; } - const morphRelationFieldName = computeMorphRelationFieldName({ - fieldName: fieldMetadata.name, - relationDirection: fieldMetadata.settings.relationType, - targetObjectMetadata, - }); - - const fieldMetadataNameWithId = `${morphRelationFieldName}Id`; + const fieldMetadataNameWithId = `${fieldMetadata.name}Id`; if (isDefined(objectRecord[fieldMetadataNameWithId])) { processedObjectRecord[fieldMetadataNameWithId] = objectRecord[fieldMetadataNameWithId]; } - const objectValue = objectRecord[morphRelationFieldName]; + const objectValue = objectRecord[fieldMetadata.name]; if (!isDefined(objectValue)) { continue; } if (Array.isArray(objectValue)) { - processedObjectRecord[morphRelationFieldName] = this.createConnection( - { - objectRecords: objectValue, - parentObjectRecord: objectRecord, - objectRecordsAggregatedValues: - objectRecordsAggregatedValues[fieldMetadata.name], - selectedAggregatedFields: - selectedAggregatedFields[fieldMetadata.name], - objectName: targetObjectMetadata.nameSingular, - take, - totalCount: - objectRecordsAggregatedValues[fieldMetadata.name]?.totalCount ?? - objectValue.length, - order, - hasNextPage: false, - hasPreviousPage: false, - depth: depth + 1, - }, - ); + processedObjectRecord[fieldMetadata.name] = this.createConnection({ + objectRecords: objectValue, + parentObjectRecord: objectRecord, + objectRecordsAggregatedValues: + objectRecordsAggregatedValues[fieldMetadata.name], + selectedAggregatedFields: + selectedAggregatedFields[fieldMetadata.name], + objectName: targetObjectMetadata.nameSingular, + take, + totalCount: + objectRecordsAggregatedValues[fieldMetadata.name]?.totalCount ?? + objectValue.length, + order, + hasNextPage: false, + hasPreviousPage: false, + depth: depth + 1, + }); } else if (isPlainObject(objectValue)) { const targetObjectMetadata = getTargetObjectMetadataOrThrow( fieldMetadata, this.objectMetadataMaps, ); - processedObjectRecord[morphRelationFieldName] = this.processRecord({ + processedObjectRecord[fieldMetadata.name] = this.processRecord({ objectRecord: objectValue, objectRecordsAggregatedValues: objectRecordsAggregatedValues[fieldMetadata.name], diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts index 0669f3c253..ad025dcd7e 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts @@ -2,11 +2,9 @@ import { isDefined } from 'twenty-shared/utils'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; -import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util'; import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; - export const buildColumnsToSelect = ({ select, relations, @@ -84,13 +82,7 @@ const getRequiredRelationColumns = ( continue; } - const morphRelationFieldName = computeMorphRelationFieldName({ - fieldName: fieldMetadata.name, - relationDirection: fieldMetadata.settings.relationType, - targetObjectMetadata, - }); - - const relationValue = relations[morphRelationFieldName]; + const relationValue = relations[fieldMetadata.name]; if ( !isDefined(relationValue) || diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-field-metadata-from-graphql-field.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-field-metadata-from-graphql-field.util.ts index b7827ccff4..48819f701e 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-field-metadata-from-graphql-field.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-field-metadata-from-graphql-field.util.ts @@ -3,7 +3,6 @@ import { isDefined } from 'twenty-shared/utils'; import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @@ -58,14 +57,8 @@ export function getFieldMetadataFromGraphQLField({ return; } - const name = computeMorphRelationFieldName({ - fieldName: morphRelation.fieldMetadata.name, - relationDirection: morphRelation.fieldMetadata.settings.relationType, - targetObjectMetadata: morphRelation.targetObjectMetadata, - }); - possibleGraphQLFieldNames.push({ - graphQLField: name, + graphQLField: morphRelation.fieldMetadata.name, fieldMetadata: morphRelation.fieldMetadata, targetObjectMetadata: morphRelation.targetObjectMetadata, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory.ts index dc6e2aa339..af10041623 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory.ts @@ -14,7 +14,6 @@ import { RelationTypeV2Factory } from 'src/engine/api/graphql/workspace-schema-b import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage'; import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util'; import { objectContainsRelationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field'; -import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-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'; @@ -169,16 +168,7 @@ export class ExtendObjectTypeDefinitionV2Factory { ); } - const fieldName = - fieldMetadata.type === FieldMetadataType.MORPH_RELATION - ? computeMorphRelationFieldName({ - fieldName: fieldMetadata.name, - relationDirection: fieldMetadata.settings.relationType, - targetObjectMetadata: objectMetadataTarget, - }) - : fieldMetadata.name; - - fields[fieldName] = { + fields[fieldMetadata.name] = { type: relationType, args: argsType, description: fieldMetadata.description, diff --git a/packages/twenty-server/src/engine/dataloaders/dataloader.interface.ts b/packages/twenty-server/src/engine/dataloaders/dataloader.interface.ts index 231c3c2c05..3f9c3acde2 100644 --- a/packages/twenty-server/src/engine/dataloaders/dataloader.interface.ts +++ b/packages/twenty-server/src/engine/dataloaders/dataloader.interface.ts @@ -9,31 +9,17 @@ import { type RelationLoaderPayload, } from 'src/engine/dataloaders/dataloader.service'; import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto'; import { type IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto'; import { type IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; export interface IDataloaders { - relationLoader: DataLoader< - RelationLoaderPayload, - { - sourceObjectMetadata: ObjectMetadataEntity; - targetObjectMetadata: ObjectMetadataEntity; - sourceFieldMetadata: FieldMetadataEntity; - targetFieldMetadata: FieldMetadataEntity; - } - >; + relationLoader: DataLoader; morphRelationLoader: DataLoader< MorphRelationLoaderPayload, - { - sourceObjectMetadata: ObjectMetadataEntity; - targetObjectMetadata: ObjectMetadataEntity; - sourceFieldMetadata: FieldMetadataEntity; - targetFieldMetadata: FieldMetadataEntity; - }[] + RelationDTO[] | null >; fieldMetadataLoader: DataLoader< diff --git a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts index e5a79b62a5..cee30ba4e2 100644 --- a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts +++ b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts @@ -2,19 +2,24 @@ import { Injectable } from '@nestjs/common'; import DataLoader from 'dataloader'; import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; +import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { type IndexMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-metadata.interface'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface'; -import { filterMorphRelationDuplicateFieldsDTO } from 'src/engine/dataloaders/utils/filter-morph-relation-duplicate-fields.util'; +import { filterMorphRelationDuplicateFields } from 'src/engine/dataloaders/utils/filter-morph-relation-duplicate-fields.util'; import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; +import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { FieldMetadataMorphRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service'; -import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service'; import { fromFieldMetadataEntityToFieldMetadataDto } from 'src/engine/metadata-modules/field-metadata/utils/from-field-metadata-entity-to-field-metadata-dto.util'; import { resolveFieldMetadataStandardOverride } from 'src/engine/metadata-modules/field-metadata/utils/resolve-field-metadata-standard-override.util'; +import { findAllOthersMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util'; +import { fromMorphOrRelationFlatFieldMetadataToRelationDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-morph-or-relation-flat-field-metadata-to-relation-dto.util'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { findFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util'; +import { findFlatObjectMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-in-flat-object-metadata-maps-or-throw.util'; import { type IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto'; import { type IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; @@ -28,27 +33,14 @@ export type RelationMetadataLoaderPayload = { export type RelationLoaderPayload = { workspaceId: string; - fieldMetadata: Pick< - FieldMetadataEntity, - | 'type' - | 'id' - | 'objectMetadataId' - | 'relationTargetFieldMetadataId' - | 'relationTargetObjectMetadataId' - >; + fieldMetadataId: string; + objectMetadataId: string; }; export type MorphRelationLoaderPayload = { workspaceId: string; - fieldMetadata: Pick< - FieldMetadataEntity, - | 'type' - | 'id' - | 'objectMetadataId' - | 'relationTargetFieldMetadataId' - | 'relationTargetObjectMetadataId' - | 'name' - >; + fieldMetadataId: string; + objectMetadataId: string; }; export type FieldMetadataLoaderPayload = { @@ -77,8 +69,6 @@ export type ObjectMetadataLoaderPayload = { export class DataloaderService { constructor( private readonly i18nService: I18nService, - private readonly fieldMetadataRelationService: FieldMetadataRelationService, - private readonly fieldMetadataMorphRelationService: FieldMetadataMorphRelationService, private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService, ) {} @@ -101,60 +91,139 @@ export class DataloaderService { } private createRelationLoader() { - return new DataLoader< - RelationLoaderPayload, - { - sourceObjectMetadata: ObjectMetadataEntity; - targetObjectMetadata: ObjectMetadataEntity; - sourceFieldMetadata: FieldMetadataEntity; - targetFieldMetadata: FieldMetadataEntity; - } - >(async (dataLoaderParams: RelationLoaderPayload[]) => { - const workspaceId = dataLoaderParams[0].workspaceId; - const fieldMetadataItems = dataLoaderParams.map( - (dataLoaderParam) => dataLoaderParam.fieldMetadata, - ); + return new DataLoader( + async (dataLoaderParams: RelationLoaderPayload[]) => { + const relationDtos: Array = []; + const workspaceId = dataLoaderParams[0].workspaceId; + const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } = + await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps( + { + workspaceId, + }, + ); - const fieldMetadataRelationCollection = - await this.fieldMetadataRelationService.findCachedFieldMetadataRelation( - fieldMetadataItems, - workspaceId, - ); + for (const { fieldMetadataId, objectMetadataId } of dataLoaderParams) { + const sourceFlatFieldMetadata = + findFlatFieldMetadataInFlatObjectMetadataMapsOrThrow({ + fieldMetadataId, + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + objectMetadataId, + }); - return fieldMetadataRelationCollection; - }); + if ( + !isFlatFieldMetadataOfType( + sourceFlatFieldMetadata, + FieldMetadataType.RELATION, + ) + ) { + relationDtos.push(null); + continue; + } + + const sourceFlatObjectMetadata = + findFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({ + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + objectMetadataId: sourceFlatFieldMetadata.objectMetadataId, + }); + + if ( + isFlatFieldMetadataOfType( + sourceFlatFieldMetadata.flatRelationTargetFieldMetadata, + FieldMetadataType.MORPH_RELATION, + ) + ) { + const allMorphFlatFieldMetadatas = + findAllOthersMorphRelationFlatFieldMetadatasOrThrow({ + flatFieldMetadata: + sourceFlatFieldMetadata.flatRelationTargetFieldMetadata, + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + }).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); + + relationDtos.push( + fromMorphOrRelationFlatFieldMetadataToRelationDto({ + flatFieldMetadata: { + ...sourceFlatFieldMetadata, + flatRelationTargetFieldMetadata: + allMorphFlatFieldMetadatas[0], + }, + sourceFlatObjectMetadata, + }), + ); + continue; + } + + relationDtos.push( + fromMorphOrRelationFlatFieldMetadataToRelationDto({ + flatFieldMetadata: sourceFlatFieldMetadata, + sourceFlatObjectMetadata, + }), + ); + } + + return relationDtos; + }, + ); } private createMorphRelationLoader() { - return new DataLoader< - MorphRelationLoaderPayload, - { - sourceObjectMetadata: ObjectMetadataEntity; - targetObjectMetadata: ObjectMetadataEntity; - sourceFieldMetadata: FieldMetadataEntity; - targetFieldMetadata: FieldMetadataEntity; - }[] - >(async (dataLoaderParams: MorphRelationLoaderPayload[]) => { - const workspaceId = dataLoaderParams[0].workspaceId; + return new DataLoader( + async (dataLoaderParams: MorphRelationLoaderPayload[]) => { + const workspaceId = dataLoaderParams[0].workspaceId; + const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } = + await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps( + { + workspaceId, + }, + ); + const relationDtos: Array = []; - const fieldMetadataItems = dataLoaderParams.map( - (dataLoaderParam) => dataLoaderParam.fieldMetadata, - ); + for (const { fieldMetadataId, objectMetadataId } of dataLoaderParams) { + const morphFlatFieldMetadata = + findFlatFieldMetadataInFlatObjectMetadataMapsOrThrow({ + fieldMetadataId, + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + objectMetadataId, + }); - const fieldMetadataMorphRelationCollection = - await this.fieldMetadataMorphRelationService.findCachedFieldMetadataMorphRelation( - fieldMetadataItems, - workspaceId, - ); + if ( + !isFlatFieldMetadataOfType( + morphFlatFieldMetadata, + FieldMetadataType.MORPH_RELATION, + ) + ) { + relationDtos.push(null); + continue; + } - return fieldMetadataItems.map((fieldMetadataItem) => { - return fieldMetadataMorphRelationCollection.filter( - (fieldMetadataMorphRelation) => - fieldMetadataItem.name === - fieldMetadataMorphRelation.sourceFieldMetadata.name, - ); - }); - }); + const sourceFlatObjectMetadata = + findFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({ + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + objectMetadataId: morphFlatFieldMetadata.objectMetadataId, + }); + + const relatedMorphFlatFieldMetadatas = + findAllOthersMorphRelationFlatFieldMetadatasOrThrow({ + flatFieldMetadata: morphFlatFieldMetadata, + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + }); + const allMorphFlatFieldMetadatas = [ + morphFlatFieldMetadata, + ...relatedMorphFlatFieldMetadatas, + ]; + + relationDtos.push( + allMorphFlatFieldMetadatas.map((flatFieldMetadata) => + fromMorphOrRelationFlatFieldMetadataToRelationDto({ + flatFieldMetadata, + sourceFlatObjectMetadata, + }), + ), + ); + } + + return relationDtos; + }, + ); } private createIndexMetadataLoader() { @@ -221,9 +290,9 @@ export class DataloaderService { return []; } - const fields = Object.values( + const overriddenFieldMetadataEntities = Object.values( objectMetadata.fieldsById, - ).map((fieldMetadata) => { + ).map((fieldMetadata) => { const overridesFieldToCompute = [ 'icon', 'label', @@ -252,14 +321,19 @@ export class DataloaderService { {}, ); - return fromFieldMetadataEntityToFieldMetadataDto({ + return { ...fieldMetadata, ...overrides, - }); + }; }); - return filterMorphRelationDuplicateFieldsDTO( - fields, + const filteredFieldMetadataEntities = + filterMorphRelationDuplicateFields( + overriddenFieldMetadataEntities, + ); + + return filteredFieldMetadataEntities.map( + fromFieldMetadataEntityToFieldMetadataDto, ); }); diff --git a/packages/twenty-server/src/engine/dataloaders/utils/filter-morph-relation-duplicate-fields.util.ts b/packages/twenty-server/src/engine/dataloaders/utils/filter-morph-relation-duplicate-fields.util.ts index e300ac6296..b07275b4d6 100644 --- a/packages/twenty-server/src/engine/dataloaders/utils/filter-morph-relation-duplicate-fields.util.ts +++ b/packages/twenty-server/src/engine/dataloaders/utils/filter-morph-relation-duplicate-fields.util.ts @@ -1,13 +1,13 @@ -export const filterMorphRelationDuplicateFieldsDTO = < - T extends { name: string; id: string }, +export const filterMorphRelationDuplicateFields = < + T extends { createdAt: Date; morphId: string | null }, >( fields: T[], ) => { return fields.filter((currentField) => { return !fields.some( (otherField) => - otherField.name === currentField.name && - otherField.id > currentField.id, + otherField.morphId === currentField.morphId && + otherField.createdAt.getTime() > currentField.createdAt.getTime(), ); }); }; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts index 9257450de0..b31dc9ffa9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts @@ -148,7 +148,10 @@ export class FieldMetadataDTO { @Field({ nullable: true }) isLabelSyncedWithName?: boolean; - @IsDateString() + @IsDateString(undefined, { + message: ({ value }) => + `Field metadata created at is invalid got ${JSON.stringify(value)} isDate: ${value instanceof Date}`, + }) @Field() createdAt: Date; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts index 17f771800d..edcb4ccc2e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts @@ -11,6 +11,7 @@ import { OneToOne, PrimaryGeneratedColumn, Relation, + Unique, UpdateDateColumn, } from 'typeorm'; @@ -36,6 +37,11 @@ import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permis @Index('IDX_FIELD_METADATA_RELATION_TARGET_OBJECT_METADATA_ID', [ 'relationTargetObjectMetadataId', ]) +@Unique('IDX_FIELD_METADATA_NAME_OBJECT_METADATA_ID_WORKSPACE_ID_UNIQUE', [ + 'name', + 'objectMetadataId', + 'workspaceId', +]) @Index('IDX_FIELD_METADATA_OBJECT_METADATA_ID_WORKSPACE_ID', [ 'objectMetadataId', 'workspaceId', diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts index 81a8e65595..4cc5b93517 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts @@ -8,7 +8,6 @@ import { Resolver, } from '@nestjs/graphql'; -import { type FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; @@ -33,21 +32,12 @@ import { UpdateOneFieldMetadataInput, type UpdateFieldInput, } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { - FieldMetadataException, - FieldMetadataExceptionCode, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { BeforeUpdateOneField } from 'src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook'; import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service'; import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2'; import { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util'; -import { fromFieldMetadataEntityToFieldMetadataDto } from 'src/engine/metadata-modules/field-metadata/utils/from-field-metadata-entity-to-field-metadata-dto.util'; -import { fromObjectMetadataEntityToObjectMetadataDto } from 'src/engine/metadata-modules/field-metadata/utils/from-object-metadata-entity-to-object-metadata-dto.util'; import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; -import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util'; -import { isMorphRelationFieldMetadataType } from 'src/engine/utils/is-morph-relation-field-metadata-type.util'; @UseGuards(WorkspaceAuthGuard) @UsePipes(ResolverValidationPipe) @@ -76,7 +66,7 @@ export class FieldMetadataResolver { workspaceId, }); } catch (error) { - fieldMetadataGraphqlApiExceptionHandler(error); + return fieldMetadataGraphqlApiExceptionHandler(error); } } @@ -171,44 +161,22 @@ export class FieldMetadataResolver { @ResolveField(() => RelationDTO, { nullable: true }) async relation( @AuthWorkspace() workspace: Workspace, - @Parent() fieldMetadata: FieldMetadataEntity, - @Context() context: { loaders: IDataloaders }, - ): Promise { - if (!isMorphOrRelationFieldMetadataType(fieldMetadata.type)) { - return null; - } + @Parent() + { + id: fieldMetadataId, + objectMetadataId, + }: Pick, + @Context() context: { loaders: IDataloaders }, + ): Promise { try { - const { - sourceObjectMetadata, - targetObjectMetadata, - sourceFieldMetadata, - targetFieldMetadata, - } = await context.loaders.relationLoader.load({ - fieldMetadata, + return await context.loaders.relationLoader.load({ + fieldMetadataId, + objectMetadataId, workspaceId: workspace.id, }); - - if (!isDefined(fieldMetadata.settings)) { - throw new FieldMetadataException( - 'Relation settings are required', - FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, - ); - } - - return { - type: fieldMetadata.settings.relationType, - sourceObjectMetadata: - fromObjectMetadataEntityToObjectMetadataDto(sourceObjectMetadata), - targetObjectMetadata: - fromObjectMetadataEntityToObjectMetadataDto(targetObjectMetadata), - sourceFieldMetadata: - fromFieldMetadataEntityToFieldMetadataDto(sourceFieldMetadata), - targetFieldMetadata: - fromFieldMetadataEntityToFieldMetadataDto(targetFieldMetadata), - }; } catch (error) { - fieldMetadataGraphqlApiExceptionHandler(error); + return fieldMetadataGraphqlApiExceptionHandler(error); } } @@ -216,46 +184,20 @@ export class FieldMetadataResolver { async morphRelations( @AuthWorkspace() workspace: Workspace, @Parent() - fieldMetadata: FieldMetadataEntity, + { + id: fieldMetadataId, + objectMetadataId, + }: Pick, @Context() context: { loaders: IDataloaders }, - ): Promise { - if (!isMorphRelationFieldMetadataType(fieldMetadata.type)) { - return null; - } - + ): Promise { try { - const morphRelations = await context.loaders.morphRelationLoader.load({ - fieldMetadata, + return await context.loaders.morphRelationLoader.load({ + fieldMetadataId, + objectMetadataId, workspaceId: workspace.id, }); - - // typescript issue, it's not possible to use the fieldMetadata.settings directly in morphRelations.map - const settings = fieldMetadata.settings; - - if (!isDefined(settings) || !isDefined(settings.relationType)) { - throw new FieldMetadataException( - `Morph relation settings ${isDefined(settings) && 'relationType'} are required`, - FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, - ); - } - - return morphRelations.map((morphRelation) => ({ - type: settings.relationType, - sourceObjectMetadata: fromObjectMetadataEntityToObjectMetadataDto( - morphRelation.sourceObjectMetadata, - ), - targetObjectMetadata: fromObjectMetadataEntityToObjectMetadataDto( - morphRelation.targetObjectMetadata, - ), - sourceFieldMetadata: fromFieldMetadataEntityToFieldMetadataDto( - morphRelation.sourceFieldMetadata, - ), - targetFieldMetadata: fromFieldMetadataEntityToFieldMetadataDto( - morphRelation.targetFieldMetadata, - ), - })); } catch (error) { - fieldMetadataGraphqlApiExceptionHandler(error); + return fieldMetadataGraphqlApiExceptionHandler(error); } } } diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts index 10a4d2ae60..d38a870144 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts @@ -2,10 +2,7 @@ import { Injectable } from '@nestjs/common'; import omit from 'lodash.omit'; import { FieldMetadataType } from 'twenty-shared/types'; -import { - computeMorphRelationFieldJoinColumnName, - isDefined, -} from 'twenty-shared/utils'; +import { isDefined } from 'twenty-shared/utils'; import { type Repository } from 'typeorm'; import { v4 } from 'uuid'; @@ -18,20 +15,17 @@ import { FieldMetadataExceptionCode, } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service'; -import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util'; +import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; +import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps'; import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; -import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; @Injectable() export class FieldMetadataMorphRelationService { constructor( private readonly fieldMetadataRelationService: FieldMetadataRelationService, - private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, ) {} async createMorphRelationFieldMetadataItems({ @@ -67,9 +61,9 @@ export class FieldMetadataMorphRelationService { const fieldsCreated: FieldMetadataEntity[] = []; const morphId = v4(); - for (const relation of morphRelationsCreationPayload) { + for (const relationCreationPayload of morphRelationsCreationPayload) { const targetObjectMetadata = - objectMetadataMaps.byId[relation.targetObjectMetadataId]; + objectMetadataMaps.byId[relationCreationPayload.targetObjectMetadataId]; if (!isDefined(targetObjectMetadata)) { throw new FieldMetadataException( @@ -78,15 +72,21 @@ export class FieldMetadataMorphRelationService { ); } + const currentMorphRelationFieldName = computeMorphRelationFieldName({ + fieldName: fieldMetadataForCreate.name, + relationType: relationCreationPayload.type, + targetObjectMetadata, + }); const relationFieldMetadataForCreate = this.fieldMetadataRelationService.computeCustomRelationFieldMetadataForCreation( { - fieldMetadataInput: fieldMetadataForCreate, - relationCreationPayload: relation, - joinColumnName: computeMorphRelationFieldJoinColumnName({ - name: fieldMetadataForCreate.name, - targetObjectMetadataNameSingular: - targetObjectMetadata.nameSingular, + fieldMetadataInput: { + ...fieldMetadataForCreate, + name: currentMorphRelationFieldName, + }, + relationCreationPayload: relationCreationPayload, + joinColumnName: computeMorphOrRelationFieldJoinColumnName({ + name: currentMorphRelationFieldName, }), }, ); @@ -106,16 +106,16 @@ export class FieldMetadataMorphRelationService { ); const targetFieldMetadataName = computeMetadataNameFromLabel( - relation.targetFieldLabel, + relationCreationPayload.targetFieldLabel, ); const targetFieldMetadataToCreate = prepareCustomFieldMetadataForCreation( { - objectMetadataId: relation.targetObjectMetadataId, + objectMetadataId: relationCreationPayload.targetObjectMetadataId, type: FieldMetadataType.RELATION, name: targetFieldMetadataName, - label: relation.targetFieldLabel, - icon: relation.targetFieldIcon, + label: relationCreationPayload.targetFieldLabel, + icon: relationCreationPayload.targetFieldIcon, workspaceId: fieldMetadataForCreate.workspaceId, settings: fieldMetadataForCreate.settings, }, @@ -130,11 +130,11 @@ export class FieldMetadataMorphRelationService { targetFieldLabel: fieldMetadataForCreate.label, targetFieldIcon: fieldMetadataForCreate.icon ?? 'Icon123', type: - relation.type === RelationType.ONE_TO_MANY + relationCreationPayload.type === RelationType.ONE_TO_MANY ? RelationType.MANY_TO_ONE : RelationType.ONE_TO_MANY, }, - joinColumnName: computeRelationFieldJoinColumnName({ + joinColumnName: computeMorphOrRelationFieldJoinColumnName({ name: targetFieldMetadataToCreate.name, }), }, @@ -163,117 +163,4 @@ export class FieldMetadataMorphRelationService { return fieldsCreated; } - - async findCachedFieldMetadataMorphRelation( - fieldMetadataItems: Array< - Pick< - FieldMetadataEntity, - | 'id' - | 'type' - | 'objectMetadataId' - | 'relationTargetFieldMetadataId' - | 'relationTargetObjectMetadataId' - | 'name' - > - >, - workspaceId: string, - ): Promise< - Array<{ - sourceObjectMetadata: ObjectMetadataEntity; - sourceFieldMetadata: FieldMetadataEntity; - targetObjectMetadata: ObjectMetadataEntity; - targetFieldMetadata: FieldMetadataEntity; - }> - > { - const objectMetadataMaps = - await this.workspaceCacheStorageService.getObjectMetadataMapsOrThrow( - workspaceId, - ); - - const fieldMetadataItemsAndMorphSiblings: Pick< - FieldMetadataEntity, - | 'id' - | 'type' - | 'objectMetadataId' - | 'relationTargetFieldMetadataId' - | 'relationTargetObjectMetadataId' - | 'name' - >[] = fieldMetadataItems.flatMap((fieldMetadataItem) => { - const fieldsById = - objectMetadataMaps.byId[fieldMetadataItem.objectMetadataId]?.fieldsById; - - if (!isDefined(fieldsById)) { - throw new FieldMetadataException( - `Fields by id not found for object metadata ${fieldMetadataItem.objectMetadataId}`, - FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, - ); - } - - return Object.values(fieldsById) - .filter( - (fieldMetadataById) => - fieldMetadataItem.name === fieldMetadataById.name, - ) - .map((fieldMetadataById) => { - return { - id: fieldMetadataById.id, - type: fieldMetadataById.type, - objectMetadataId: fieldMetadataById.objectMetadataId, - relationTargetFieldMetadataId: - fieldMetadataById.relationTargetFieldMetadataId, - relationTargetObjectMetadataId: - fieldMetadataById.relationTargetObjectMetadataId, - name: fieldMetadataById.name, - }; - }); - }); - - return fieldMetadataItemsAndMorphSiblings.map((fieldMetadataItem) => { - const { - id, - objectMetadataId, - relationTargetFieldMetadataId, - relationTargetObjectMetadataId, - } = fieldMetadataItem; - - if (!relationTargetObjectMetadataId || !relationTargetFieldMetadataId) { - throw new FieldMetadataException( - `Relation target object metadata id or relation target field metadata id not found for field metadata ${id}`, - FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, - ); - } - - const sourceObjectMetadata = objectMetadataMaps.byId[objectMetadataId]; - const targetObjectMetadata = - objectMetadataMaps.byId[relationTargetObjectMetadataId]; - const sourceFieldMetadata = sourceObjectMetadata?.fieldsById[id]; - const targetFieldMetadata = - targetObjectMetadata?.fieldsById[relationTargetFieldMetadataId]; - - if ( - !sourceObjectMetadata || - !targetObjectMetadata || - !sourceFieldMetadata || - !targetFieldMetadata - ) { - throw new FieldMetadataException( - `Field relation metadata not found for field metadata ${id}`, - FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, - ); - } - - return { - sourceObjectMetadata: - getObjectMetadataFromObjectMetadataItemWithFieldMaps( - sourceObjectMetadata, - ) as ObjectMetadataEntity, - sourceFieldMetadata: sourceFieldMetadata as FieldMetadataEntity, - targetObjectMetadata: - getObjectMetadataFromObjectMetadataItemWithFieldMaps( - targetObjectMetadata, - ) as ObjectMetadataEntity, - targetFieldMetadata: targetFieldMetadata as FieldMetadataEntity, - }; - }); - } } diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts index 94761c77b6..26a25f0e88 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts @@ -10,7 +10,6 @@ import { v4 } from 'uuid'; import { type FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; -import { filterMorphRelationDuplicateFieldsDTO } from 'src/engine/dataloaders/utils/filter-morph-relation-duplicate-fields.util'; import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; @@ -18,15 +17,12 @@ import { FieldMetadataException, FieldMetadataExceptionCode, } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; -import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util'; -import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util'; +import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util'; import { validateRelationCreationPayloadOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/validate-relation-creation-payload-or-throw.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps'; import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils'; import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-or-throw.utils'; import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; @@ -108,7 +104,7 @@ export class FieldMetadataRelationService { ? RelationType.MANY_TO_ONE : RelationType.ONE_TO_MANY, }, - joinColumnName: computeRelationFieldJoinColumnName({ + joinColumnName: computeMorphOrRelationFieldJoinColumnName({ name: targetFieldMetadataToCreate.name, }), }); @@ -215,111 +211,6 @@ export class FieldMetadataRelationService { return fieldMetadataInput; } - async findCachedFieldMetadataRelation( - fieldMetadataItems: Array< - Pick< - FieldMetadataEntity, - | 'id' - | 'type' - | 'objectMetadataId' - | 'relationTargetFieldMetadataId' - | 'relationTargetObjectMetadataId' - > - >, - workspaceId: string, - ): Promise< - Array<{ - sourceObjectMetadata: ObjectMetadataEntity; - sourceFieldMetadata: FieldMetadataEntity; - targetObjectMetadata: ObjectMetadataEntity; - targetFieldMetadata: FieldMetadataEntity; - }> - > { - const objectMetadataMaps = - await this.workspaceCacheStorageService.getObjectMetadataMapsOrThrow( - workspaceId, - ); - - return fieldMetadataItems.map((fieldMetadataItem) => { - const { - id, - objectMetadataId, - relationTargetFieldMetadataId, - relationTargetObjectMetadataId, - } = fieldMetadataItem; - - if (!relationTargetObjectMetadataId || !relationTargetFieldMetadataId) { - throw new FieldMetadataException( - `Relation target object metadata id or relation target field metadata id not found for field metadata ${id}`, - FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, - ); - } - - const sourceObjectMetadata = objectMetadataMaps.byId[objectMetadataId]; - const targetObjectMetadata = - objectMetadataMaps.byId[relationTargetObjectMetadataId]; - const sourceFieldMetadata = sourceObjectMetadata?.fieldsById[id]; - const targetFieldMetadata = - targetObjectMetadata?.fieldsById[relationTargetFieldMetadataId]; - - if ( - !sourceObjectMetadata || - !targetObjectMetadata || - !sourceFieldMetadata || - !targetFieldMetadata - ) { - throw new FieldMetadataException( - `Field relation metadata not found for field metadata ${id}`, - FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, - ); - } - - // Morph relation specific: - // to keep the fieldMetadata API simple - // we decided to override the targetFieldMetadata with the existing one - // in case the relation target is a morph relation - const isRelationTargetMorphRelation = - isFieldMetadataTypeMorphRelation(targetFieldMetadata); - - const targetObjectMetadataFields = Object.values( - targetObjectMetadata.fieldsById, - ); - - const targetObjectMetadataWithoutDuplicates = - filterMorphRelationDuplicateFieldsDTO( - targetObjectMetadataFields, - ); - const targetFieldMetadataSelected = - targetObjectMetadataWithoutDuplicates.find( - (fieldMetadata) => fieldMetadata.name === targetFieldMetadata.name, - ); - - if (!isDefined(targetFieldMetadataSelected)) { - throw new FieldMetadataException( - `Target field metadata not found for field metadata ${id}`, - FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, - ); - } - - const targetFieldMetadataOverride = isRelationTargetMorphRelation - ? targetFieldMetadataSelected - : targetFieldMetadata; - - return { - sourceObjectMetadata: - getObjectMetadataFromObjectMetadataItemWithFieldMaps( - sourceObjectMetadata, - ) as ObjectMetadataEntity, - sourceFieldMetadata: sourceFieldMetadata as FieldMetadataEntity, - targetObjectMetadata: - getObjectMetadataFromObjectMetadataItemWithFieldMaps( - targetObjectMetadata, - ) as ObjectMetadataEntity, - targetFieldMetadata: targetFieldMetadataOverride as FieldMetadataEntity, - }; - }); - } - // TODO refactor and strictly type computeCustomRelationFieldMetadataForCreation({ fieldMetadataInput, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts index 794d745db9..4b5a5f82d0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts @@ -1,10 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { - isDefined, - trimAndRemoveDuplicatedWhitespacesFromString, -} from 'twenty-shared/utils'; +import { isDefined } from 'twenty-shared/utils'; import { In, Repository } from 'typeorm'; import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; @@ -39,15 +36,15 @@ export class FieldMetadataServiceV2 { ) {} async createOne({ - fieldMetadataInput, + createFieldInput, workspaceId, }: { - fieldMetadataInput: Omit; + createFieldInput: Omit; workspaceId: string; }): Promise { const [createdFieldMetadata] = await this.createMany({ - fieldMetadataInputs: [fieldMetadataInput], workspaceId, + createFieldInputs: [createFieldInput], }); if (!isDefined(createdFieldMetadata)) { @@ -206,13 +203,13 @@ export class FieldMetadataServiceV2 { } async createMany({ - fieldMetadataInputs, + createFieldInputs, workspaceId, }: { - fieldMetadataInputs: Omit[]; + createFieldInputs: Omit[]; workspaceId: string; }): Promise { - if (!fieldMetadataInputs.length) { + if (createFieldInputs.length === 0) { return []; } @@ -221,24 +218,24 @@ export class FieldMetadataServiceV2 { { workspaceId }, ); - const allInputTranspilationsResults = ( - await Promise.all( - fieldMetadataInputs.map( - async (fieldMetadataInput) => - await fromCreateFieldInputToFlatFieldMetadatasToCreate({ - existingFlatObjectMetadataMaps, - workspaceId, - rawCreateFieldInput: fieldMetadataInput, - }), - ), - ) - ).flat(); + const allTranspiledTranspilationInputs = []; + + for (const createInput of createFieldInputs) { + allTranspiledTranspilationInputs.push( + await fromCreateFieldInputToFlatFieldMetadatasToCreate({ + existingFlatObjectMetadataMaps, + workspaceId, + rawCreateFieldInput: createInput, + }), + ); + } throwOnFieldInputTranspilationsError( - allInputTranspilationsResults, + allTranspiledTranspilationInputs, 'Multiple validation errors occurred while creating field', ); - const flatFieldMetadatasToCreate = allInputTranspilationsResults.flatMap( + + const flatFieldMetadatasToCreate = allTranspiledTranspilationInputs.flatMap( ({ result }) => result, ); @@ -289,10 +286,8 @@ export class FieldMetadataServiceV2 { return this.fieldMetadataRepository.find({ where: { name: In( - fieldMetadataInputs.map((flatFieldMetadata) => - trimAndRemoveDuplicatedWhitespacesFromString( - flatFieldMetadata.name, - ), + allTranspiledTranspilationInputs.map( + ({ result: flatFieldMetadatas }) => flatFieldMetadatas[0].name, ), ), workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts index 5e4131b117..4e576d27b1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts @@ -31,7 +31,7 @@ import { FieldMetadataRelatedRecordsService } from 'src/engine/metadata-modules/ import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service'; import { FieldMetadataValidationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service'; import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2'; -import { areFieldMetadatasTypeRelationOrMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/are-field-metadatas-type-relation-or-morph-relation.util'; +import { areFieldMetadatasOfType } from 'src/engine/metadata-modules/field-metadata/utils/are-field-metadatas-of-type.util'; import { assertDoesNotNullifyDefaultValueForNonNullableField } from 'src/engine/metadata-modules/field-metadata/utils/assert-does-not-nullify-default-value-for-non-nullable-field.util'; import { buildUpdatableStandardFieldInput } from 'src/engine/metadata-modules/field-metadata/utils/build-updatable-standard-field-input.util'; import { checkCanDeactivateFieldOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/check-can-deactivate-field-or-throw'; @@ -39,7 +39,7 @@ import { computeColumnName, computeCompositeColumnName, } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util'; +import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; import { createMigrationActions } from 'src/engine/metadata-modules/field-metadata/utils/create-migration-actions.util'; import { generateRatingOptions } from 'src/engine/metadata-modules/field-metadata/utils/generate-rating-optionts.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; @@ -74,9 +74,9 @@ import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; type GenerateMigrationArgs = { - fieldMetadata: FieldMetadataEntity< - FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION - >; + fieldMetadata: + | FieldMetadataEntity + | FieldMetadataEntity; workspaceId: string; queryRunner: QueryRunner; }; @@ -116,7 +116,7 @@ export class FieldMetadataService extends TypeOrmQueryService { - fieldMetadataIdsToDelete.push( - morphRelation.id, - morphRelation.relationTargetFieldMetadataId, - ); - }); - - await fieldMetadataRepository.delete({ - id: In(fieldMetadataIdsToDelete), - }); - - for (const morphRelation of morphRelationsWithSameName) { - await this.generateDeleteRelationMigration({ - fieldMetadata: morphRelation, - workspaceId, - queryRunner, - }); - } - } else { - fieldMetadataIdsToDelete.push( - fieldMetadata.id, - fieldMetadata.relationTargetFieldMetadataId, - ); - - await fieldMetadataRepository.delete({ - id: In(fieldMetadataIdsToDelete), - }); - - await this.generateDeleteRelationMigration({ - fieldMetadata, - workspaceId, - queryRunner, - }); - } + await this.generateDeleteRelationMigration({ + fieldMetadata, + workspaceId, + queryRunner, + }); } else if (isFieldMetadataTypeMorphRelation(fieldMetadata)) { const fieldMetadataIdsToDelete: string[] = []; - const morphRelationsWithSameName = - await this.getMorphRelationsWithSameName({ - fieldMetadataName: fieldMetadata.name, + const allMophFieldMetadatas = + await this.getAllMorphFieldMetadatasByMorphId({ + morphId: fieldMetadata.morphId, objectMetadataId: fieldMetadata.objectMetadataId, workspaceId, fieldMetadataRepository, }); - morphRelationsWithSameName.forEach((morphRelation) => { + allMophFieldMetadatas.forEach((morphRelation) => { fieldMetadataIdsToDelete.push( morphRelation.id, morphRelation.relationTargetFieldMetadataId, @@ -542,7 +509,7 @@ export class FieldMetadataService extends TypeOrmQueryService; - }): Promise< - FieldMetadataEntity< - FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION - >[] - > { + }): Promise[]> { const fieldMetadatas = await fieldMetadataRepository.find({ where: { - name: fieldMetadataName, + morphId, objectMetadataId, workspaceId, }, @@ -1100,9 +1063,11 @@ export class FieldMetadataService extends TypeOrmQueryService( + fieldMetadatas: FieldMetadataEntity[], + fieldMetadataType: T, +): fieldMetadatas is Array> => + fieldMetadatas.every((fieldMetadata) => + isFlatFieldMetadataOfType(fieldMetadata, fieldMetadataType), + ); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/are-field-metadatas-type-relation-or-morph-relation.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/are-field-metadatas-type-relation-or-morph-relation.util.ts deleted file mode 100644 index 7de21a78fc..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/are-field-metadatas-type-relation-or-morph-relation.util.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { type FieldMetadataType } from 'twenty-shared/types'; - -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util'; -import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util'; - -export const areFieldMetadatasTypeRelationOrMorphRelation = ( - fieldMetadatas: FieldMetadataEntity[], -): fieldMetadatas is Array< - FieldMetadataEntity & - FieldMetadataEntity< - FieldMetadataType.MORPH_RELATION | FieldMetadataType.RELATION - > -> => { - return fieldMetadatas.every( - (fieldMetadata) => - isFieldMetadataTypeRelation(fieldMetadata) || - isFieldMetadataTypeMorphRelation(fieldMetadata), - ); -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util.ts similarity index 71% rename from packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util.ts rename to packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util.ts index 8aa2002794..d581178e0b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util.ts @@ -2,7 +2,7 @@ type ComputeRelationFieldJoinColumnNameArgs = { name: string; }; -export const computeRelationFieldJoinColumnName = ({ +export const computeMorphOrRelationFieldJoinColumnName = ({ name, }: ComputeRelationFieldJoinColumnNameArgs) => { return `${name}Id`; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util.ts index b0987a9cb6..160cb0edcf 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util.ts @@ -6,7 +6,7 @@ import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-me type ComputeMorphRelationFieldNameArgs = { fieldName: string; - relationDirection: RelationType; + relationType: RelationType; targetObjectMetadata: Pick< ObjectMetadataEntity, 'nameSingular' | 'namePlural' @@ -15,18 +15,12 @@ type ComputeMorphRelationFieldNameArgs = { export const computeMorphRelationFieldName = ({ fieldName, - relationDirection, + relationType, targetObjectMetadata, }: ComputeMorphRelationFieldNameArgs): string => { - if (relationDirection === RelationType.MANY_TO_ONE) { + if (relationType === RelationType.MANY_TO_ONE) { return `${fieldName}${capitalize(targetObjectMetadata.nameSingular)}`; } - if (relationDirection === RelationType.ONE_TO_MANY) { - return `${fieldName}${capitalize(targetObjectMetadata.namePlural)}`; - } - - throw new Error( - `Invalid relation direction: ${relationDirection} for field ${fieldName}`, - ); + return `${fieldName}${capitalize(targetObjectMetadata.namePlural)}`; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/resolve-field-metadata-standard-override.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/resolve-field-metadata-standard-override.util.ts index d175c0b16d..807bcc70d2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/resolve-field-metadata-standard-override.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/resolve-field-metadata-standard-override.util.ts @@ -6,6 +6,7 @@ import { isDefined } from 'twenty-shared/utils'; import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId'; import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; +// TODO simplify export const resolveFieldMetadataStandardOverride = ( fieldMetadata: Pick< FieldMetadataDTO, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap index fd8faf6e62..49a30d81dd 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap @@ -138,7 +138,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "isUnique": null, "label": "newFieldLabel", "morphId": Any, - "name": "newField", + "name": "newFieldPets", "objectMetadataId": Any, "options": null, "relationTargetFieldMetadataId": Any, @@ -252,7 +252,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "isUnique": null, "label": "newFieldLabel", "morphId": Any, - "name": "newField", + "name": "newFieldPets", "objectMetadataId": Any, "options": null, "relationTargetFieldMetadataId": Any, @@ -317,7 +317,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "isUnique": null, "label": "newFieldLabel", "morphId": Any, - "name": "newField", + "name": "newFieldPets", "objectMetadataId": Any, "options": null, "relationTargetFieldMetadataId": Any, @@ -444,7 +444,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "isUnique": null, "label": "newFieldLabel", "morphId": Any, - "name": "newField", + "name": "newFieldCompanies", "objectMetadataId": Any, "options": null, "relationTargetFieldMetadataId": Any, @@ -558,7 +558,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "isUnique": null, "label": "newFieldLabel", "morphId": Any, - "name": "newField", + "name": "newFieldCompanies", "objectMetadataId": Any, "options": null, "relationTargetFieldMetadataId": Any, @@ -623,7 +623,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "isUnique": null, "label": "newFieldLabel", "morphId": Any, - "name": "newField", + "name": "newFieldCompanies", "objectMetadataId": Any, "options": null, "relationTargetFieldMetadataId": Any, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util.ts new file mode 100644 index 0000000000..366f71e3c9 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util.ts @@ -0,0 +1,56 @@ +import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +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'; +import { + FlatObjectMetadataMapsException, + FlatObjectMetadataMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-object-metadata-maps/flat-object-metadata-maps.exception'; +import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; + +export type FindAllMorphRelationFlatFieldMetadatasOrThrowArgs = { + flatObjectMetadataMaps: FlatObjectMetadataMaps; + flatFieldMetadata: FlatFieldMetadata; +}; +export const findAllOthersMorphRelationFlatFieldMetadatasOrThrow = ({ + flatObjectMetadataMaps, + flatFieldMetadata: morphRelationFlatFieldMetadata, +}: FindAllMorphRelationFlatFieldMetadatasOrThrowArgs): FlatFieldMetadata[] => { + const flatObjectMetadataWithFlatFieldMaps = + flatObjectMetadataMaps.byId[ + morphRelationFlatFieldMetadata.objectMetadataId + ]; + + if (!isDefined(flatObjectMetadataWithFlatFieldMaps)) { + throw new FlatObjectMetadataMapsException( + 'Morph field relation object metadata not found', + FlatObjectMetadataMapsExceptionCode.OBJECT_METADATA_NOT_FOUND, + ); + } + + if ( + !isDefined( + flatObjectMetadataWithFlatFieldMaps.fieldsById[ + morphRelationFlatFieldMetadata.id + ], + ) + ) { + throw new FlatObjectMetadataMapsException( + 'Morph relation field not found in related object metadata', + FlatObjectMetadataMapsExceptionCode.FIELD_METADATA_NOT_FOUND, + ); + } + + return flatObjectMetadataWithFlatFieldMaps.flatFieldMetadatas.filter( + ( + flatFieldMetadata, + ): flatFieldMetadata is FlatFieldMetadata => + isFlatFieldMetadataOfType( + flatFieldMetadata, + FieldMetadataType.MORPH_RELATION, + ) && + flatFieldMetadata.morphId === morphRelationFlatFieldMetadata.morphId && + flatFieldMetadata.id !== morphRelationFlatFieldMetadata.id, + ); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-flat-field-metadatas-related-to-morph-relation-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-flat-field-metadatas-related-to-morph-relation-or-throw.util.ts index 83d00782b5..38732b23b1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-flat-field-metadatas-related-to-morph-relation-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-flat-field-metadatas-related-to-morph-relation-or-throw.util.ts @@ -1,14 +1,9 @@ -import { FieldMetadataType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { type FieldMetadataType } from 'twenty-shared/types'; import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { findAllOthersMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util'; import { findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-relation-flat-field-metadatas-target-flat-field-metadata-or-throw.util'; -import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; -import { - FlatObjectMetadataMapsException, - FlatObjectMetadataMapsExceptionCode, -} from 'src/engine/metadata-modules/flat-object-metadata-maps/flat-object-metadata-maps.exception'; import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; export type FindFlatFieldMetadatasRelatedToMorphRelationOrThrowArgs = { @@ -19,54 +14,26 @@ export const findFlatFieldMetadatasRelatedToMorphRelationOrThrow = ({ flatObjectMetadataMaps, flatFieldMetadata: morphRelationFlatFieldMetadata, }: FindFlatFieldMetadatasRelatedToMorphRelationOrThrowArgs): FlatFieldMetadata[] => { - const flatObjectMetadataWithFlatFieldMaps = - flatObjectMetadataMaps.byId[ - morphRelationFlatFieldMetadata.objectMetadataId - ]; + const allMorphFlatFieldMetadatas = + findAllOthersMorphRelationFlatFieldMetadatasOrThrow({ + flatFieldMetadata: morphRelationFlatFieldMetadata, + flatObjectMetadataMaps, + }); - if (!isDefined(flatObjectMetadataWithFlatFieldMaps)) { - throw new FlatObjectMetadataMapsException( - 'Morph field relation object metadata not found', - FlatObjectMetadataMapsExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } + return [ + morphRelationFlatFieldMetadata, + ...allMorphFlatFieldMetadatas, + ].flatMap((flatFieldMetadata) => { + const relationTargetFlatFieldMetadata = + findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow({ + flatFieldMetadata, + flatObjectMetadataMaps, + }); - if ( - !isDefined( - flatObjectMetadataWithFlatFieldMaps.fieldsById[ - morphRelationFlatFieldMetadata.id - ], - ) - ) { - throw new FlatObjectMetadataMapsException( - 'Morph relation field not found in related object metadata', - FlatObjectMetadataMapsExceptionCode.FIELD_METADATA_NOT_FOUND, - ); - } + if (flatFieldMetadata.id === morphRelationFlatFieldMetadata.id) { + return [relationTargetFlatFieldMetadata]; + } - return flatObjectMetadataWithFlatFieldMaps.flatFieldMetadatas.flatMap( - (flatFieldMetadata) => { - if ( - !isFlatFieldMetadataOfType( - flatFieldMetadata, - FieldMetadataType.MORPH_RELATION, - ) || - flatFieldMetadata.name !== morphRelationFlatFieldMetadata.name - ) { - return []; - } - - const relationTargetFlatFieldMetadata = - findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow({ - flatFieldMetadata, - flatObjectMetadataMaps, - }); - - if (flatFieldMetadata.id === morphRelationFlatFieldMetadata.id) { - return [relationTargetFlatFieldMetadata]; - } - - return [flatFieldMetadata, relationTargetFlatFieldMetadata]; - }, - ); + return [flatFieldMetadata, relationTargetFlatFieldMetadata]; + }); }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util.ts index e0d34cc43f..23af2a4061 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util.ts @@ -1,8 +1,9 @@ import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatRelationTargetFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-relation-target-field-metadata.type'; export const fromFlatFieldMetadataToFieldMetadataDto = ( - flatFieldMetadata: FlatFieldMetadata, + flatFieldMetadata: FlatFieldMetadata | FlatRelationTargetFieldMetadata, ): FieldMetadataDTO => { const { createdAt, @@ -13,11 +14,35 @@ export const fromFlatFieldMetadataToFieldMetadataDto = ( isNullable, isUnique, settings, - ...rest + id, + label, + name, + objectMetadataId, + type, + workspaceId, + defaultValue, + isActive, + isCustom, + isLabelSyncedWithName, + isSystem, + isUIReadOnly, + options, } = flatFieldMetadata; return { - ...rest, + id, + label, + name, + objectMetadataId, + type, + workspaceId, + defaultValue, + isActive, + isCustom, + isLabelSyncedWithName, + isSystem, + isUIReadOnly, + options, createdAt: new Date(createdAt), updatedAt: new Date(updatedAt), description: description ?? undefined, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-or-relation-flat-field-metadata-to-relation-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-or-relation-flat-field-metadata-to-relation-dto.util.ts new file mode 100644 index 0000000000..4c83ef9afa --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-or-relation-flat-field-metadata-to-relation-dto.util.ts @@ -0,0 +1,26 @@ +import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { fromFlatObjectMetadataToObjectMetadataDto } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util'; + +export const fromMorphOrRelationFlatFieldMetadataToRelationDto = ({ + flatFieldMetadata, + sourceFlatObjectMetadata, +}: { + sourceFlatObjectMetadata: FlatObjectMetadata; + flatFieldMetadata: FlatFieldMetadata; +}) => ({ + type: flatFieldMetadata.settings.relationType, + sourceObjectMetadata: fromFlatObjectMetadataToObjectMetadataDto( + sourceFlatObjectMetadata, + ), + sourceFieldMetadata: + fromFlatFieldMetadataToFieldMetadataDto(flatFieldMetadata), + targetObjectMetadata: fromFlatObjectMetadataToObjectMetadataDto( + flatFieldMetadata.flatRelationTargetObjectMetadata, + ), + targetFieldMetadata: fromFlatFieldMetadataToFieldMetadataDto( + flatFieldMetadata.flatRelationTargetFieldMetadata, + ), +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts index 022af18258..1e06ee6e50 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts @@ -1,14 +1,13 @@ import { t } from '@lingui/core/macro'; import { type FieldMetadataType } from 'twenty-shared/types'; -import { - computeMorphRelationFieldJoinColumnName, - isDefined, -} from 'twenty-shared/utils'; +import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; 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 { 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 { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { generateMorphOrRelationFlatFieldMetadataPair } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util'; @@ -68,17 +67,21 @@ export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({ const morphId = v4(); const flatFieldMetadatas = morphRelationCreationPayload.flatMap( ({ relationCreationPayload, targetFlatObjectMetadata }) => { + const currentMorphRelationFieldName = computeMorphRelationFieldName({ + fieldName: createFieldInput.name, + relationType: relationCreationPayload.type, + targetObjectMetadata: targetFlatObjectMetadata, + }); const sourceFlatObjectMetadataJoinColumnName = - computeMorphRelationFieldJoinColumnName({ - name: createFieldInput.name, - targetObjectMetadataNameSingular: - targetFlatObjectMetadata.nameSingular, + computeMorphOrRelationFieldJoinColumnName({ + name: currentMorphRelationFieldName, }); return generateMorphOrRelationFlatFieldMetadataPair({ createFieldInput: { ...createFieldInput, relationCreationPayload, + name: currentMorphRelationFieldName, }, sourceFlatObjectMetadataJoinColumnName, sourceFlatObjectMetadata, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadatas.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadatas.util.ts index ce4d6ece0f..f780e5d964 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadatas.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadatas.util.ts @@ -4,7 +4,7 @@ import { isDefined } from 'twenty-shared/utils'; 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 { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util'; +import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { generateMorphOrRelationFlatFieldMetadataPair } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util'; @@ -58,9 +58,10 @@ export const fromRelationCreateFieldInputToFlatFieldMetadatas = async ({ ...createFieldInput, relationCreationPayload, }, - sourceFlatObjectMetadataJoinColumnName: computeRelationFieldJoinColumnName({ - name: createFieldInput.name, - }), + sourceFlatObjectMetadataJoinColumnName: + computeMorphOrRelationFieldJoinColumnName({ + name: createFieldInput.name, + }), sourceFlatObjectMetadata, targetFlatObjectMetadata, workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts index 3226f25a26..86eeb60dcd 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts @@ -3,7 +3,7 @@ import { v4 } from 'uuid'; 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 { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util'; +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 { getDefaultFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @@ -92,7 +92,7 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({ }; const targetFlatFieldMetadataSettings = computeFieldMetadataRelationSettingsForRelationType({ - joinColumnName: computeRelationFieldJoinColumnName({ + joinColumnName: computeMorphOrRelationFieldJoinColumnName({ name: targetCreateFieldInput.name, }), relationType: diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util.ts index 65f2c14bb1..890486f6fc 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util.ts @@ -1,8 +1,11 @@ -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { + type FlatObjectMetadataWithoutFields, + type FlatObjectMetadata, +} from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; export const fromFlatObjectMetadataToObjectMetadataDto = ( - flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadata: FlatObjectMetadata | FlatObjectMetadataWithoutFields, ): ObjectMetadataDTO => { const { createdAt, @@ -12,11 +15,39 @@ export const fromFlatObjectMetadataToObjectMetadataDto = ( standardOverrides, shortcut, duplicateCriteria, - ...rest + id, + isActive, + isCustom, + isLabelSyncedWithName, + isRemote, + isSearchable, + isSystem, + isUIReadOnly, + labelPlural, + labelSingular, + namePlural, + nameSingular, + workspaceId, + imageIdentifierFieldMetadataId, + labelIdentifierFieldMetadataId, } = flatObjectMetadata; return { - ...rest, + id, + isActive, + isCustom, + isLabelSyncedWithName, + isRemote, + isSearchable, + isSystem, + isUIReadOnly, + labelPlural, + labelSingular, + namePlural, + nameSingular, + workspaceId, + imageIdentifierFieldMetadataId, + labelIdentifierFieldMetadataId, createdAt: new Date(createdAt), updatedAt: new Date(updatedAt), description: description ?? undefined, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata.util.ts index cc81c7e3fb..2f08e5dac5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata.util.ts @@ -10,7 +10,7 @@ import { findFlatObjectMetadataInFlatObjectMetadataMaps } from 'src/engine/metad import { FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-properties-to-compare.constant'; import { type FlatObjectMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-properties-to-compare.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { renameFlatObjectMetadataManyToOneMorphRelationTargetFlatFieldMetadatasSettingsJoinColumnName } from 'src/engine/metadata-modules/flat-object-metadata/utils/rename-flat-object-metadata-morph-relation-flat-field-metadatas-settings-join-column-name.util'; +import { renameRelatedMorphFieldOnObjectNamesUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util'; import { OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/object-metadata/constants/object-metadata-standard-overrides-properties.constant'; import { type UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; import { @@ -130,14 +130,12 @@ export const fromUpdateObjectInputToFlatObjectMetadata = ({ [property]: updatedPropertyValue, }; const newUpdatedOtherObjectFlatFieldMetadatas = - property === 'nameSingular' - ? renameFlatObjectMetadataManyToOneMorphRelationTargetFlatFieldMetadatasSettingsJoinColumnName( - { - existingFlatObjectMetadataMaps, - fromFlatObjectMetadata: updatedFlatObjectMetadata, - toFlatObjectMetadata: updatedFlatObjectMetadata, - }, - ) + property === 'nameSingular' || property === 'namePlural' + ? renameRelatedMorphFieldOnObjectNamesUpdate({ + existingFlatObjectMetadataMaps, + fromFlatObjectMetadata: updatedFlatObjectMetadata, + toFlatObjectMetadata: updatedFlatObjectMetadata, + }) : []; return { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/get-flat-object-metadata-many-to-one-target-morph-relation-flat-field-metadatas-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/get-flat-object-metadata-many-to-one-target-morph-relation-flat-field-metadatas-or-throw.util.ts index 01876338c0..2643065069 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/get-flat-object-metadata-many-to-one-target-morph-relation-flat-field-metadatas-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/get-flat-object-metadata-many-to-one-target-morph-relation-flat-field-metadatas-or-throw.util.ts @@ -1,22 +1,20 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; - 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'; import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; import { findFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -type GetFlatObjectMetadataMorphRelationFlatFieldMetadatasArgs = { +type GetFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrowArgs = { flatObjectMetadata: FlatObjectMetadata; existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; }; -export const getFlatObjectMetadataManyToOneTargetMorphRelationFlatFieldMetadatasOrThrow = +export const getFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrow = ({ flatObjectMetadata, existingFlatObjectMetadataMaps, - }: GetFlatObjectMetadataMorphRelationFlatFieldMetadatasArgs) => { + }: GetFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrowArgs): FlatFieldMetadata[] => { const manyToOneMorphRelationTargetFlatFieldMetadataIds = flatObjectMetadata.flatFieldMetadatas .filter( @@ -27,8 +25,6 @@ export const getFlatObjectMetadataManyToOneTargetMorphRelationFlatFieldMetadatas flatFieldMetadata, FieldMetadataType.RELATION, ) && - flatFieldMetadata.settings.relationType === - RelationType.MANY_TO_ONE && isFlatFieldMetadataOfType( flatFieldMetadata.flatRelationTargetFieldMetadata, FieldMetadataType.MORPH_RELATION, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-flat-object-metadata-morph-relation-flat-field-metadatas-settings-join-column-name.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-flat-object-metadata-morph-relation-flat-field-metadatas-settings-join-column-name.util.ts deleted file mode 100644 index fef6765d05..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-flat-object-metadata-morph-relation-flat-field-metadatas-settings-join-column-name.util.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { type FieldMetadataType, type FromTo } from 'twenty-shared/types'; -import { computeMorphRelationFieldJoinColumnName } from 'twenty-shared/utils'; - -import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { getFlatObjectMetadataManyToOneTargetMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-flat-object-metadata-many-to-one-target-morph-relation-flat-field-metadatas-or-throw.util'; - -type RenameFlatObjectMetadataManyToOneMorphRelationTargetFlatFieldMetadatasSettingsJoinColumnNameArgs = - FromTo & { - existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; - }; -export const renameFlatObjectMetadataManyToOneMorphRelationTargetFlatFieldMetadatasSettingsJoinColumnName = - ({ - fromFlatObjectMetadata, - toFlatObjectMetadata, - existingFlatObjectMetadataMaps, - }: RenameFlatObjectMetadataManyToOneMorphRelationTargetFlatFieldMetadatasSettingsJoinColumnNameArgs): FlatFieldMetadata[] => { - const manyToOneMorphRelationFlatFieldMetadatas = - getFlatObjectMetadataManyToOneTargetMorphRelationFlatFieldMetadatasOrThrow( - { - flatObjectMetadata: fromFlatObjectMetadata, - existingFlatObjectMetadataMaps, - }, - ); - - const updatedFlatFieldMetadatas = - manyToOneMorphRelationFlatFieldMetadatas.map( - (morphRelationFlatFieldMetadata) => { - const newJoinColumnName = computeMorphRelationFieldJoinColumnName({ - name: morphRelationFlatFieldMetadata.name, - targetObjectMetadataNameSingular: toFlatObjectMetadata.nameSingular, - }); - - return { - ...morphRelationFlatFieldMetadata, - settings: { - ...morphRelationFlatFieldMetadata.settings, - joinColumnName: newJoinColumnName, - }, - }; - }, - ); - - return updatedFlatFieldMetadatas; - }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts new file mode 100644 index 0000000000..f9a73a3846 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts @@ -0,0 +1,87 @@ +import { + RelationType, + type FieldMetadataType, + type FromTo, +} from 'twenty-shared/types'; + +import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; +import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { getFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-flat-object-metadata-many-to-one-target-morph-relation-flat-field-metadatas-or-throw.util'; + +const searchAndReplaceLast = ({ + replace, + search, + source, +}: { + source: string; + search: string; + replace: string; +}) => { + const lastIndex = source.lastIndexOf(search); + + if (lastIndex === -1) return source; + + return ( + source.slice(0, lastIndex) + + replace + + source.slice(lastIndex + search.length) + ); +}; + +type RenameRelatedMorphFieldOnObjectNamesUpdateArgs = FromTo< + FlatObjectMetadata, + 'flatObjectMetadata' +> & { + existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; +}; +export const renameRelatedMorphFieldOnObjectNamesUpdate = ({ + fromFlatObjectMetadata, + existingFlatObjectMetadataMaps, + toFlatObjectMetadata, +}: RenameRelatedMorphFieldOnObjectNamesUpdateArgs): FlatFieldMetadata[] => { + const manyToOneMorphRelationFlatFieldMetadatas = + getFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrow({ + flatObjectMetadata: fromFlatObjectMetadata, + existingFlatObjectMetadataMaps, + }); + + const updatedFlatFieldMetadatas = + manyToOneMorphRelationFlatFieldMetadatas.map( + (morphRelationFlatFieldMetadata) => { + const isManyToOneRelationType = + morphRelationFlatFieldMetadata.settings.relationType === + RelationType.MANY_TO_ONE; + const initialMorphRelationFieldName = searchAndReplaceLast({ + source: morphRelationFlatFieldMetadata.name, + replace: '', + search: isManyToOneRelationType + ? fromFlatObjectMetadata.nameSingular + : fromFlatObjectMetadata.namePlural, + }); + const newMorphFieldName = computeMorphRelationFieldName({ + fieldName: initialMorphRelationFieldName, + relationType: morphRelationFlatFieldMetadata.settings.relationType, + targetObjectMetadata: toFlatObjectMetadata, + }); + const newJoinColumnName = isManyToOneRelationType + ? computeMorphOrRelationFieldJoinColumnName({ + name: newMorphFieldName, + }) + : undefined; + + return { + ...morphRelationFlatFieldMetadata, + name: newMorphFieldName, + settings: { + ...morphRelationFlatFieldMetadata.settings, + joinColumnName: newJoinColumnName, + }, + }; + }, + ); + + return updatedFlatFieldMetadatas; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service.ts index 96af092730..794e079d81 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service.ts @@ -2,17 +2,14 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { FieldMetadataType } from 'twenty-shared/types'; -import { - capitalize, - computeMorphRelationFieldJoinColumnName, - isDefined, -} from 'twenty-shared/utils'; +import { capitalize, isDefined } from 'twenty-shared/utils'; import { type QueryRunner, Repository } from 'typeorm'; import { v4 as uuidV4 } from 'uuid'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { 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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataException, @@ -522,9 +519,12 @@ export class ObjectMetadataFieldRelationService { return fieldMetadatas; } + // Not maintained on v1, this side effect is broken and will duplicated field name references on + // object update + // It's functional on v2 public async updateMorphRelationsJoinColumnName({ existingObjectMetadata, - objectMetadataForUpdate, + objectMetadataForUpdate: _, queryRunner, }: { existingObjectMetadata: Pick< @@ -567,10 +567,8 @@ export class ObjectMetadataFieldRelationService { if (morphRelationFieldMetadataToUpdate.length > 0) { for (const morphRelationFieldMetadata of morphRelationFieldMetadataToUpdate) { - const newJoinColumnName = computeMorphRelationFieldJoinColumnName({ + const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({ name: morphRelationFieldMetadata.name, - targetObjectMetadataNameSingular: - objectMetadataForUpdate.nameSingular, }); await fieldMetadataRepository.save({ diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service.ts index 0f16b4caf7..a5c0b0a650 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service.ts @@ -1,8 +1,8 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts index 69b7b7d8e6..194f6bb3b9 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts @@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common'; import { FieldMetadataType } from 'twenty-shared/types'; import { type EntitySchemaRelationOptions } from 'typeorm'; -import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { determineSchemaRelationDetails } from 'src/engine/twenty-orm/utils/determine-schema-relation-details.util'; @@ -62,16 +61,7 @@ export class EntitySchemaRelationFactory { ); } - const fieldName = - fieldMetadata.type === FieldMetadataType.MORPH_RELATION - ? computeMorphRelationFieldName({ - fieldName: fieldMetadata.name, - relationDirection: fieldMetadata.settings.relationType, - targetObjectMetadata, - }) - : fieldMetadata.name; - - entitySchemaRelationMap[fieldName] = { + entitySchemaRelationMap[fieldMetadata.name] = { type: schemaRelationDetails.relationType, target: schemaRelationDetails.target, inverseSide: schemaRelationDetails.inverseSide, diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/metadata/custom-fields/constants/pet-custom-relation-field-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/metadata/custom-fields/constants/pet-custom-relation-field-seeds.constant.ts index abe444c2c7..adc5323ad6 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/metadata/custom-fields/constants/pet-custom-relation-field-seeds.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/metadata/custom-fields/constants/pet-custom-relation-field-seeds.constant.ts @@ -20,4 +20,19 @@ export const PET_CUSTOM_RELATION_FIELD_SEEDS: (FieldMetadataSeed & { ], targetObjectMetadataNames: ['surveyResult', 'rocket'], }, + { + type: FieldMetadataType.MORPH_RELATION, + label: 'Helper of the Pet (rocket or survey)', + name: 'helper', + icon: 'IconRelationOneToMany', + morphRelationsCreationPayload: [ + { + type: RelationType.ONE_TO_MANY, + targetObjectMetadataId: 'to-be-resolved-later', + targetFieldLabel: 'Helped by', + targetFieldIcon: 'IconRelationOneToMany', + }, + ], + targetObjectMetadataNames: ['surveyResult', 'rocket'], + }, ]; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/create-one-field-metadata-morph-relation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/create-one-field-metadata-morph-relation.integration-spec.ts index e8bee11b06..19b1494b15 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/create-one-field-metadata-morph-relation.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/create-one-field-metadata-morph-relation.integration-spec.ts @@ -1,8 +1,10 @@ -import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util'; import { createMorphRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util'; import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { type EachTestingContext } from 'twenty-shared/testing'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; import { FieldMetadataType } from 'twenty-shared/types'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; @@ -18,6 +20,7 @@ describe('createOne FieldMetadataService morph relation fields', () => { createOneObject: { id: objectMetadataPersonId }, }, } = await createOneObjectMetadata({ + expectToFail: false, input: { nameSingular: 'personForMorphRelation', namePlural: 'peopleForMorphRelation', @@ -34,6 +37,7 @@ describe('createOne FieldMetadataService morph relation fields', () => { createOneObject: { id: objectMetadataCompanyId }, }, } = await createOneObjectMetadata({ + expectToFail: false, input: { nameSingular: 'companyForMorphRelation', namePlural: 'companiesForMorphRelation', @@ -50,6 +54,7 @@ describe('createOne FieldMetadataService morph relation fields', () => { createOneObject: { id: objectMetadataOpportunityId }, }, } = await createOneObjectMetadata({ + expectToFail: false, input: { nameSingular: 'opportunityForMorphRelation', namePlural: 'opportunitiesForMorphRelation', @@ -63,12 +68,15 @@ describe('createOne FieldMetadataService morph relation fields', () => { }); afterEach(async () => { await deleteOneObjectMetadata({ + expectToFail: false, input: { idToDelete: createdObjectMetadataPersonId }, }); await deleteOneObjectMetadata({ + expectToFail: false, input: { idToDelete: createdObjectMetadataOpportunityId }, }); await deleteOneObjectMetadata({ + expectToFail: false, input: { idToDelete: createdObjectMetadataCompanyId }, }); }); @@ -118,51 +126,52 @@ describe('createOne FieldMetadataService morph relation fields', () => { }, ]; - it.each(eachTestingContextArray)('$title', async ({ context }) => { - const contextPayload = - typeof context === 'function' - ? context({ - objectMetadataId: createdObjectMetadataOpportunityId, - firstTargetObjectMetadataId: createdObjectMetadataPersonId, - secondTargetObjectMetadataId: createdObjectMetadataCompanyId, - }) - : context; + it.each(eachTestingContextFilter(eachTestingContextArray))( + '$title', + async ({ context }) => { + const contextPayload = + typeof context === 'function' + ? context({ + objectMetadataId: createdObjectMetadataOpportunityId, + firstTargetObjectMetadataId: createdObjectMetadataPersonId, + secondTargetObjectMetadataId: createdObjectMetadataCompanyId, + }) + : context; - const createdField = await createMorphRelationBetweenObjects({ - objectMetadataId: contextPayload.objectMetadataId, - firstTargetObjectMetadataId: contextPayload.firstTargetObjectMetadataId, - secondTargetObjectMetadataId: contextPayload.secondTargetObjectMetadataId, - type: contextPayload.type, - relationType: contextPayload.relationType, - }); + const createdField = await createMorphRelationBetweenObjects({ + objectMetadataId: contextPayload.objectMetadataId, + firstTargetObjectMetadataId: contextPayload.firstTargetObjectMetadataId, + secondTargetObjectMetadataId: + contextPayload.secondTargetObjectMetadataId, + type: contextPayload.type, + relationType: contextPayload.relationType, + }); - expect(createdField.id).toBeDefined(); - expect(createdField.name).toBe('owner'); + expect(createdField.id).toBeDefined(); - const morphRelationTargetIds = createdField.morphRelations.map( - (relation) => relation.targetObjectMetadata.id, - ); - - expect(morphRelationTargetIds).toContain( - contextPayload.firstTargetObjectMetadataId, - ); - expect(morphRelationTargetIds).toContain( - contextPayload.secondTargetObjectMetadataId, - ); - - const isManyToOne = - contextPayload.relationType === RelationType.MANY_TO_ONE; - - if (isManyToOne) { - expect(createdField.settings?.joinColumnName).toBe( - 'ownerPersonForMorphRelationId', + const morphRelationTargetIds = createdField.morphRelations.map( + (relation) => relation.targetObjectMetadata.id, ); - } else { - expect(createdField.settings?.joinColumnName).toBeUndefined(); - } - await deleteOneFieldMetadata({ - input: { idToDelete: createdField.id }, - }).catch(); - }); + expect(morphRelationTargetIds).toContain( + contextPayload.firstTargetObjectMetadataId, + ); + expect(morphRelationTargetIds).toContain( + contextPayload.secondTargetObjectMetadataId, + ); + + const isManyToOne = + contextPayload.relationType === RelationType.MANY_TO_ONE; + + if (isManyToOne) { + expect(createdField.settings?.joinColumnName).toBe( + 'ownerPersonForMorphRelationId', + ); + expect(createdField.name).toBe('ownerPersonForMorphRelation'); + } else { + expect(createdField.settings?.joinColumnName).toBeUndefined(); + expect(createdField.name).toBe('ownerPeopleForMorphRelation'); + } + }, + ); }); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/create-one-field-metadata-relation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/create-one-field-metadata-relation.integration-spec.ts index 0f49158f02..8ef06661f3 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/create-one-field-metadata-relation.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/create-one-field-metadata-relation.integration-spec.ts @@ -1,12 +1,11 @@ import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util'; -import { findManyFieldsMetadataQueryFactory } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata-query-factory.util'; import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { createRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-relation-between-objects.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; import { type EachTestingContext } from 'twenty-shared/testing'; import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; +import { findManyFieldsMetadata } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata.util'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; @@ -155,7 +154,14 @@ const findFieldMetadata = async ({ }: { fieldMetadataId: string; }) => { - const operation = findManyFieldsMetadataQueryFactory({ + const { fields } = await findManyFieldsMetadata({ + input: { + filter: { + id: { eq: fieldMetadataId }, + }, + paging: { first: 10 }, + }, + expectToFail: false, gqlFields: ` id name @@ -174,16 +180,8 @@ const findFieldMetadata = async ({ } settings `, - input: { - filter: { - id: { eq: fieldMetadataId }, - }, - paging: { first: 10 }, - }, }); - - const fields = await makeMetadataAPIRequest(operation); - const field = fields.body.data.fields.edges?.[0]?.node; + const field = fields[0]?.node; return field; }; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/morph-relation/rename-object-metadata-with-morph-relation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/morph-relation/rename-object-metadata-with-morph-relation-v2.integration-spec.ts similarity index 53% rename from packages/twenty-server/test/integration/metadata/suites/object-metadata/morph-relation/rename-object-metadata-with-morph-relation.integration-spec.ts rename to packages/twenty-server/test/integration/metadata/suites/object-metadata/morph-relation/rename-object-metadata-with-morph-relation-v2.integration-spec.ts index 90ec223f35..fce0561c36 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/morph-relation/rename-object-metadata-with-morph-relation.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/morph-relation/rename-object-metadata-with-morph-relation-v2.integration-spec.ts @@ -1,88 +1,119 @@ import { findManyFieldsMetadataQueryFactory } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata-query-factory.util'; import { createMorphRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; +import { forceCreateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/force-create-one-object-metadata.util'; import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; import { FieldMetadataType } from 'twenty-shared/types'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; -import { type RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; describe('Rename an object metadata with morph relation should succeed', () => { - let opportunityId = ''; - let personId = ''; - let companyId = ''; - let morphRelationField: FieldMetadataEntity & { - morphRelations: RelationDTO[]; - }; + let createdObjectMetadataPersonId: string; + let createdObjectMetadataOpportunityId: string; + let createdObjectMetadataCompanyId: string; + + beforeAll(async () => { + await updateFeatureFlag({ + expectToFail: false, + featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + value: true, + }); + }); + + afterAll(async () => { + await updateFeatureFlag({ + expectToFail: false, + featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + value: false, + }); + }); beforeEach(async () => { const { data: { createOneObject: { id: aId }, }, - } = await createOneObjectMetadata({ + } = await forceCreateOneObjectMetadata({ input: { - nameSingular: 'opportunityForRename', - namePlural: 'opportunitiesForRename', + nameSingular: 'opportunityForRenameSecond', + namePlural: 'opportunitiesForRenameSecond', labelSingular: 'Opportunity For Rename', labelPlural: 'Opportunities For Rename', icon: 'IconOpportunity', }, }); - opportunityId = aId; + createdObjectMetadataOpportunityId = aId; const { data: { createOneObject: { id: bId }, }, - } = await createOneObjectMetadata({ + } = await forceCreateOneObjectMetadata({ input: { - nameSingular: 'personForRename', - namePlural: 'peopleForRename', + nameSingular: 'personForRenameSecond', + namePlural: 'peopleForRenameSecond', labelSingular: 'Person For Rename', labelPlural: 'People For Rename', icon: 'IconPerson', }, }); - personId = bId; + createdObjectMetadataPersonId = bId; const { data: { createOneObject: { id: cId }, }, - } = await createOneObjectMetadata({ + } = await forceCreateOneObjectMetadata({ input: { - nameSingular: 'companyForRename', - namePlural: 'companiesForRename', + nameSingular: 'companyForRenameSecond', + namePlural: 'companiesForRenameSecond', labelSingular: 'Company For Rename', labelPlural: 'Companies For Rename', icon: 'IconCompany', }, }); - companyId = cId; + createdObjectMetadataCompanyId = cId; }); afterEach(async () => { - await deleteOneObjectMetadata({ input: { idToDelete: opportunityId } }); - await deleteOneObjectMetadata({ input: { idToDelete: personId } }); - await deleteOneObjectMetadata({ input: { idToDelete: companyId } }); + const createdObjectMetadataIds = [ + createdObjectMetadataPersonId, + createdObjectMetadataOpportunityId, + createdObjectMetadataCompanyId, + ]; + + for (const objectMetadataId of createdObjectMetadataIds) { + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: objectMetadataId, + updatePayload: { isActive: false }, + }, + }); + await deleteOneObjectMetadata({ + expectToFail: false, + input: { idToDelete: objectMetadataId }, + }); + } }); - it('should rename custom object, and update the join column name of the morph relation that contains the object name', async () => { - morphRelationField = await createMorphRelationBetweenObjects({ + it('should rename custom object, and update both the field name and join column name of the morph relation that contains the object name', async () => { + const morphRelationField = await createMorphRelationBetweenObjects({ name: 'owner', - objectMetadataId: opportunityId, - firstTargetObjectMetadataId: personId, - secondTargetObjectMetadataId: companyId, + objectMetadataId: createdObjectMetadataOpportunityId, + firstTargetObjectMetadataId: createdObjectMetadataPersonId, + secondTargetObjectMetadataId: createdObjectMetadataCompanyId, type: FieldMetadataType.MORPH_RELATION, relationType: RelationType.MANY_TO_ONE, }); + expect(morphRelationField.morphRelations.length).toBe(2); + const { data } = await updateOneObjectMetadata({ gqlFields: ` nameSingular @@ -91,20 +122,21 @@ describe('Rename an object metadata with morph relation should succeed', () => { labelPlural `, input: { - idToUpdate: personId, + idToUpdate: createdObjectMetadataPersonId, updatePayload: { - nameSingular: 'personForRename2', - namePlural: 'peopleForRename2', + nameSingular: 'personForRenameSecond2', + namePlural: 'peopleForRenameSecond2', labelSingular: 'Person For Rename2', labelPlural: 'People For Rename2', }, }, }); - expect(data.updateOneObject.nameSingular).toBe('personForRename2'); + expect(data.updateOneObject.nameSingular).toBe('personForRenameSecond2'); const ownerFieldMetadataOnPersonId = morphRelationField.morphRelations.find( - (morphRelation) => morphRelation.targetObjectMetadata.id === personId, + (morphRelation) => + morphRelation.targetObjectMetadata.id === createdObjectMetadataPersonId, )?.sourceFieldMetadata.id; if (!ownerFieldMetadataOnPersonId) { @@ -118,7 +150,7 @@ describe('Rename an object metadata with morph relation should succeed', () => { }); expect(fieldAfterRenaming.settings.joinColumnName).toBe( - 'ownerPersonForRename2Id', + 'ownerPersonForRenameSecondId', ); }); }); diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util.ts index 539b558ba7..d39fa5f663 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util.ts @@ -58,6 +58,7 @@ export const createMorphRelationBetweenObjects = async ({ input: createFieldInput, gqlFields: ` id + type name label isLabelSyncedWithName diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/force-create-one-object-metadata.util.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/force-create-one-object-metadata.util.ts index 36278f0295..450a901de5 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/force-create-one-object-metadata.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/force-create-one-object-metadata.util.ts @@ -35,7 +35,7 @@ export const forceCreateOneObjectMetadata = async ({ }, }); - if (!isDefined(result.errors)) { + if (!isDefined(result.errors) || result.errors.length === 0) { return result; } diff --git a/packages/twenty-shared/src/utils/fieldMetadata/compute-morph-relation-field-join-column-name.util.ts b/packages/twenty-shared/src/utils/fieldMetadata/compute-morph-relation-field-join-column-name.util.ts deleted file mode 100644 index 6c73895785..0000000000 --- a/packages/twenty-shared/src/utils/fieldMetadata/compute-morph-relation-field-join-column-name.util.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { capitalize } from '@/utils/strings'; - -type ComputeMorphRelationFieldJoinColumnNameArgs = { - name: string; - targetObjectMetadataNameSingular: string; -}; - -export const computeMorphRelationFieldJoinColumnName = ({ - name, - targetObjectMetadataNameSingular, -}: ComputeMorphRelationFieldJoinColumnNameArgs) => { - return `${name}${capitalize(targetObjectMetadataNameSingular)}Id`; -}; diff --git a/packages/twenty-shared/src/utils/fieldMetadata/compute-morph-relation-field-name.util.ts b/packages/twenty-shared/src/utils/fieldMetadata/compute-morph-relation-field-name.util.ts deleted file mode 100644 index 04e8fdc956..0000000000 --- a/packages/twenty-shared/src/utils/fieldMetadata/compute-morph-relation-field-name.util.ts +++ /dev/null @@ -1,34 +0,0 @@ - -import { capitalize } from '@/utils/strings'; - -enum RelationType { - MANY_TO_ONE = 'MANY_TO_ONE', - ONE_TO_MANY = 'ONE_TO_MANY' -} - - -type ComputeMorphRelationFieldNameArgs = { - fieldName: string; - relationDirection: RelationType; - nameSingular: string; - namePlural: string; -}; - -export const computeMorphRelationFieldName = ({ - fieldName, - relationDirection, - nameSingular, - namePlural, -}: ComputeMorphRelationFieldNameArgs): string => { - if (relationDirection === RelationType.MANY_TO_ONE) { - return `${fieldName}${capitalize(nameSingular)}`; - } - - if (relationDirection === RelationType.ONE_TO_MANY) { - return `${fieldName}${capitalize(namePlural)}`; - } - - throw new Error( - `Invalid relation direction: ${relationDirection} for field ${fieldName}`, - ); -}; diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index cecf87a800..2d20512d96 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -14,8 +14,6 @@ export { sumByProperty } from './array/sumByProperty'; export { assertUnreachable } from './assertUnreachable'; export { deepMerge } from './deepMerge'; export { extractAndSanitizeObjectStringFields } from './extractAndSanitizeObjectStringFields'; -export { computeMorphRelationFieldJoinColumnName } from './fieldMetadata/compute-morph-relation-field-join-column-name.util'; -export { computeMorphRelationFieldName } from './fieldMetadata/compute-morph-relation-field-name.util'; export { isFieldMetadataDateKind } from './fieldMetadata/isFieldMetadataDateKind'; export { fromArrayToUniqueKeyRecord } from './from-array-to-unique-key-record.util'; export { fromArrayToValuesByKeyRecord } from './fromArrayToValuesByKeyRecord.util';