diff --git a/packages/twenty-server/.eslintrc.cjs b/packages/twenty-server/.eslintrc.cjs index dd968f156..e534c0c6a 100644 --- a/packages/twenty-server/.eslintrc.cjs +++ b/packages/twenty-server/.eslintrc.cjs @@ -2,9 +2,10 @@ module.exports = { plugins: ['@stylistic'], extends: ['../../.eslintrc.global.cjs'], ignorePatterns: [ - 'src/engine/workspace-manager/demo-objects-prefill-data/**', - 'src/engine/seeder/data-seeds/**', - 'src/engine/seeder/metadata-seeds/**', + 'src/engine/workspace-manager/dev-seeder/data/constants/**', + 'src/engine/workspace-manager/dev-seeder/data/seeds/**', + 'src/utils/email-providers.ts', + 'src/engine/core-modules/i18n/locales/generated/**', 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project/src/index.ts', ], overrides: [ 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 3f6fe1ca8..920b618d1 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 @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { isDefined } from 'class-validator'; import omit from 'lodash.omit'; import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; import { v4 } from 'uuid'; @@ -19,6 +19,8 @@ import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modul import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; +import { computeMorphRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-join-column-name.util'; +import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util'; @Injectable() export class FieldMetadataMorphRelationService { @@ -59,12 +61,26 @@ export class FieldMetadataMorphRelationService { const fieldsCreated: FieldMetadataEntity[] = []; for (const relation of morphRelationsCreationPayload) { + const targetObjectMetadata = + objectMetadataMaps.byId[relation.targetObjectMetadataId]; + + if (!isDefined(targetObjectMetadata)) { + throw new FieldMetadataException( + 'Target object metadata does not exist in the object metadata maps', + FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, + ); + } + const relationFieldMetadataForCreate = await this.fieldMetadataRelationService.addCustomRelationFieldMetadataForCreation( { fieldMetadataInput: fieldMetadataForCreate, relationCreationPayload: relation, - objectMetadata, + joinColumnName: computeMorphRelationFieldJoinColumnName({ + name: fieldMetadataForCreate.name, + targetObjectMetadataNameSingular: + targetObjectMetadata.nameSingular, + }), }, ); @@ -109,7 +125,9 @@ export class FieldMetadataMorphRelationService { ? RelationType.MANY_TO_ONE : RelationType.ONE_TO_MANY, }, - objectMetadata, + joinColumnName: computeRelationFieldJoinColumnName({ + name: targetFieldMetadataToCreate.name, + }), }, ); 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 3e2b18acb..06b59adc5 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 @@ -3,7 +3,7 @@ import { Injectable, ValidationError } from '@nestjs/common'; import { plainToInstance } from 'class-transformer'; import { IsEnum, IsString, IsUUID, validateOrReject } from 'class-validator'; import { FieldMetadataType } from 'twenty-shared/types'; -import { capitalize, isDefined } from 'twenty-shared/utils'; +import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; import { v4 } from 'uuid'; @@ -28,6 +28,7 @@ import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/v import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util'; export class RelationCreationPayloadValidation { @IsUUID() @@ -103,7 +104,9 @@ export class FieldMetadataRelationService { ? RelationType.MANY_TO_ONE : RelationType.ONE_TO_MANY, }, - objectMetadata, + joinColumnName: computeRelationFieldJoinColumnName({ + name: targetFieldMetadataToCreate.name, + }), }); // todo better type @@ -285,11 +288,11 @@ export class FieldMetadataRelationService { addCustomRelationFieldMetadataForCreation({ fieldMetadataInput, relationCreationPayload, - objectMetadata, + joinColumnName, }: { fieldMetadataInput: CreateFieldInput; relationCreationPayload: CreateFieldInput['relationCreationPayload']; - objectMetadata: ObjectMetadataItemWithFieldMaps; + joinColumnName: string; }) { const isRelation = isFieldMetadataInterfaceOfType( @@ -309,13 +312,6 @@ export class FieldMetadataRelationService { const defaultIcon = 'IconRelationOneToMany'; - const joinColumnName = isFieldMetadataInterfaceOfType( - fieldMetadataInput, - FieldMetadataType.MORPH_RELATION, - ) - ? `${fieldMetadataInput.name}${capitalize(objectMetadata.nameSingular)}Id` - : `${fieldMetadataInput.name}Id`; - return { ...fieldMetadataInput, icon: fieldMetadataInput.icon ?? defaultIcon, 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 1d9bd3bde..28b07a3e3 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 @@ -32,6 +32,7 @@ import { computeColumnName, computeCompositeColumnName, } from 'src/engine/metadata-modules/field-metadata/utils/compute-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'; import { isSelectOrMultiSelectFieldMetadata } from 'src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util'; @@ -54,9 +55,9 @@ import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; import { ViewService } from 'src/modules/view/services/view.service'; +import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util'; @Injectable() export class FieldMetadataService extends TypeOrmQueryService { @@ -578,15 +579,14 @@ export class FieldMetadataService extends TypeOrmQueryService { - if (isRemoteCreation) { - return []; - } - - const migrationActions: WorkspaceMigrationTableAction[] = []; - - for (const createdFieldMetadata of createdFieldMetadataItems) { - if ( - isFieldMetadataEntityOfType( - createdFieldMetadata, - FieldMetadataType.RELATION, - ) - ) { - const relationType = createdFieldMetadata.settings?.relationType; - - if (relationType === RelationType.ONE_TO_MANY) { - continue; - } - } - - const objectMetadata = - objectMetadataMap[createdFieldMetadata.objectMetadataId]; - - if (!isDefined(objectMetadata)) { - throw new FieldMetadataException( - 'Object metadata does not exist', - FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } - - migrationActions.push({ - name: computeObjectTargetTable(objectMetadata), - action: WorkspaceMigrationTableActionType.ALTER, - columns: this.workspaceMigrationFactory.createColumnActions( - WorkspaceMigrationColumnActionType.CREATE, - createdFieldMetadata, - ), - }); - } - - return migrationActions; - } } diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-join-column-name.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-join-column-name.util.ts new file mode 100644 index 000000000..b09e70f43 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-join-column-name.util.ts @@ -0,0 +1,13 @@ +import { capitalize } from 'twenty-shared/utils'; + +type ComputeMorphRelationFieldJoinColumnNameArgs = { + name: string; + targetObjectMetadataNameSingular: string; +}; + +export const computeMorphRelationFieldJoinColumnName = ({ + name, + targetObjectMetadataNameSingular, +}: ComputeMorphRelationFieldJoinColumnNameArgs) => { + return `${name}${capitalize(targetObjectMetadataNameSingular)}Id`; +}; 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-relation-field-join-column-name.util.ts new file mode 100644 index 000000000..8aa200279 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util.ts @@ -0,0 +1,9 @@ +type ComputeRelationFieldJoinColumnNameArgs = { + name: string; +}; + +export const computeRelationFieldJoinColumnName = ({ + name, +}: ComputeRelationFieldJoinColumnNameArgs) => { + return `${name}Id`; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/create-migration-actions.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/create-migration-actions.util.ts new file mode 100644 index 000000000..baa4bcfa9 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/create-migration-actions.util.ts @@ -0,0 +1,77 @@ +import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +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 { + FieldMetadataException, + FieldMetadataExceptionCode, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { + WorkspaceMigrationColumnActionType, + WorkspaceMigrationTableAction, + WorkspaceMigrationTableActionType, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; +import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; +import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; + +export const createMigrationActions = async ({ + createdFieldMetadataItems, + objectMetadataMap, + isRemoteCreation, + workspaceMigrationFactory, +}: { + createdFieldMetadataItems: FieldMetadataEntity[]; + objectMetadataMap: ObjectMetadataMaps['byId']; + isRemoteCreation: boolean; + workspaceMigrationFactory: WorkspaceMigrationFactory; +}): Promise => { + if (isRemoteCreation) { + return []; + } + + const migrationActions: WorkspaceMigrationTableAction[] = []; + + for (const createdFieldMetadata of createdFieldMetadataItems) { + if ( + isFieldMetadataEntityOfType( + createdFieldMetadata, + FieldMetadataType.RELATION, + ) || + isFieldMetadataEntityOfType( + createdFieldMetadata, + FieldMetadataType.MORPH_RELATION, + ) + ) { + const relationType = createdFieldMetadata.settings?.relationType; + + if (relationType === RelationType.ONE_TO_MANY) { + continue; + } + } + + const objectMetadata = + objectMetadataMap[createdFieldMetadata.objectMetadataId]; + + if (!isDefined(objectMetadata)) { + throw new FieldMetadataException( + 'Object metadata does not exist', + FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, + ); + } + + migrationActions.push({ + name: computeObjectTargetTable(objectMetadata), + action: WorkspaceMigrationTableActionType.ALTER, + columns: workspaceMigrationFactory.createColumnActions( + WorkspaceMigrationColumnActionType.CREATE, + createdFieldMetadata, + ), + }); + } + + return migrationActions; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/morph-relation-column-action.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/morph-relation-column-action.factory.ts index 5083b101a..17f8f7089 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/morph-relation-column-action.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/morph-relation-column-action.factory.ts @@ -3,30 +3,93 @@ import { Injectable, Logger } from '@nestjs/common'; import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface'; import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory'; +import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; import { + WorkspaceMigrationColumnActionType, WorkspaceMigrationColumnAlter, WorkspaceMigrationColumnCreate, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { + WorkspaceMigrationException, + WorkspaceMigrationExceptionCode, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception'; @Injectable() export class MorphRelationColumnActionFactory extends ColumnActionAbstractFactory { protected readonly logger = new Logger(MorphRelationColumnActionFactory.name); protected handleCreateAction( - _fieldMetadata: FieldMetadataInterface, + fieldMetadata: FieldMetadataInterface, _options?: WorkspaceColumnActionOptions, ): WorkspaceMigrationColumnCreate[] { - return []; + if (!fieldMetadata.settings || !fieldMetadata.settings.joinColumnName) { + return []; + } + + const joinColumnName = fieldMetadata.settings.joinColumnName; + + return [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: joinColumnName, + columnType: fieldMetadataTypeToColumnType(FieldMetadataType.UUID), + isArray: false, + isNullable: fieldMetadata.isNullable ?? true, + isUnique: false, + defaultValue: null, + }, + ]; } protected handleAlterAction( - _currentFieldMetadata: FieldMetadataInterface, - _alteredFieldMetadata: FieldMetadataInterface, + currentFieldMetadata: FieldMetadataInterface, + alteredFieldMetadata: FieldMetadataInterface, _options?: WorkspaceColumnActionOptions, ): WorkspaceMigrationColumnAlter[] { - return []; + if (!currentFieldMetadata.settings || !alteredFieldMetadata.settings) { + return []; + } + + if ( + currentFieldMetadata.settings.relationType === RelationType.ONE_TO_MANY + ) { + return []; + } + + const currentJoinColumnName = currentFieldMetadata.settings.joinColumnName; + const alteredJoinColumnName = alteredFieldMetadata.settings.joinColumnName; + + if (!currentJoinColumnName || !alteredJoinColumnName) { + throw new WorkspaceMigrationException( + `Column name not found for current or altered field metadata`, + WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA, + ); + } + + return [ + { + action: WorkspaceMigrationColumnActionType.ALTER, + currentColumnDefinition: { + columnName: currentJoinColumnName, + columnType: fieldMetadataTypeToColumnType(FieldMetadataType.UUID), + isArray: false, + isNullable: currentFieldMetadata.isNullable ?? true, + isUnique: false, + defaultValue: null, + }, + alteredColumnDefinition: { + columnName: alteredJoinColumnName, + columnType: fieldMetadataTypeToColumnType(FieldMetadataType.UUID), + isArray: false, + isNullable: alteredFieldMetadata.isNullable ?? true, + isUnique: false, + defaultValue: null, + }, + }, + ]; } } diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-field-metadata-morph-relation-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-field-metadata-morph-relation-creation.integration-spec.ts.snap index a06842223..40a37e558 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-field-metadata-morph-relation-creation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-field-metadata-morph-relation-creation.integration-spec.ts.snap @@ -60,7 +60,7 @@ exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE "exceptionEventId": "mocked-exception-id", "userFriendlyMessage": "An error occurred.", }, - "message": "Object metadata relation target not found for relation creation payload", + "message": "Target object metadata does not exist in the object metadata maps", }, ] `; @@ -153,7 +153,7 @@ exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY "exceptionEventId": "mocked-exception-id", "userFriendlyMessage": "An error occurred.", }, - "message": "Object metadata relation target not found for relation creation payload", + "message": "Target object metadata does not exist in the object metadata maps", }, ] `; 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 81f965373..8c7896f38 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 @@ -149,7 +149,7 @@ describe('createOne FieldMetadataService morph relation fields', () => { if (isManyToOne) { expect(createdField.settings?.joinColumnName).toBe( - 'ownerOpportunityForMorphRelationId', + 'ownerPersonForMorphRelationId', ); } else { expect(createdField.settings?.joinColumnName).toBeUndefined();