From b6626099481bc22aae15b450a0050c3472107776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Wed, 22 Jan 2025 17:01:54 +0100 Subject: [PATCH] feat: add targetFieldMetadataId and migration script for relations (#9793) Fix https://github.com/twentyhq/core-team-issues/issues/238 and https://github.com/twentyhq/core-team-issues/issues/239 --- ...ate-relations-to-field-metadata.command.ts | 140 ++++++++++++++++++ .../0-41/0-41-upgrade-version.command.ts | 8 + .../0-41/0-41-upgrade-version.module.ts | 4 + ...tionTargetFieldAndObjectToFieldMetadata.ts | 55 +++++++ .../field-metadata/field-metadata.entity.ts | 27 ++++ .../field-metadata-settings.interface.ts | 9 ++ .../relation-on-delete-action.interface.ts | 6 + .../interfaces/relation-type.interface.ts | 5 + .../object-metadata/object-metadata.entity.ts | 6 + 9 files changed, 260 insertions(+) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-41/0-41-migrate-relations-to-field-metadata.command.ts create mode 100644 packages/twenty-server/src/database/typeorm/metadata/migrations/1737561084251-addRelationTargetFieldAndObjectToFieldMetadata.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface.ts diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-41/0-41-migrate-relations-to-field-metadata.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-41/0-41-migrate-relations-to-field-metadata.command.ts new file mode 100644 index 000000000..93f1ba9ed --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-41/0-41-migrate-relations-to-field-metadata.command.ts @@ -0,0 +1,140 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { Command } from 'nest-commander'; +import { FieldMetadataType } from 'twenty-shared'; +import { Repository } from 'typeorm'; + +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; + +import { + ActiveWorkspacesCommandOptions, + ActiveWorkspacesCommandRunner, +} from 'src/database/commands/active-workspaces.command'; +import { isCommandLogger } from 'src/database/commands/logger'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { + deduceRelationDirection, + RelationDirection, +} from 'src/engine/utils/deduce-relation-direction.util'; + +@Command({ + name: 'upgrade-0.41:migrate-relations-to-field-metadata', + description: 'Migrate relations to field metadata', +}) +export class MigrateRelationsToFieldMetadataCommand extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + _passedParam: string[], + options: ActiveWorkspacesCommandOptions, + workspaceIds: string[], + ): Promise { + this.logger.log('Running command to create many to one relations'); + + if (isCommandLogger(this.logger)) { + this.logger.setVerbose(options.verbose ?? false); + } + + try { + for (const [index, workspaceId] of workspaceIds.entries()) { + await this.processWorkspace(workspaceId, index, workspaceIds.length); + } + + this.logger.log(chalk.green('Command completed!')); + } catch (error) { + this.logger.log(chalk.red('Error in workspace')); + } + } + + private async processWorkspace( + workspaceId: string, + index: number, + total: number, + ): Promise { + try { + this.logger.log( + `Running command for workspace ${workspaceId} ${index + 1}/${total}`, + ); + + const fieldMetadataCollection = (await this.fieldMetadataRepository.find({ + where: { workspaceId, type: FieldMetadataType.RELATION }, + relations: ['fromRelationMetadata', 'toRelationMetadata'], + })) as unknown as FieldMetadataEntity[]; + + if (!fieldMetadataCollection.length) { + this.logger.log( + chalk.yellow( + `No relation field metadata found for workspace ${workspaceId}.`, + ), + ); + + return; + } + + const fieldMetadataToUpdateCollection = fieldMetadataCollection.map( + (fieldMetadata) => this.mapFieldMetadata(fieldMetadata), + ); + + if (fieldMetadataToUpdateCollection.length > 0) { + await this.fieldMetadataRepository.save( + fieldMetadataToUpdateCollection, + ); + } + + this.logger.log( + chalk.green(`Command completed for workspace ${workspaceId}.`), + ); + } catch { + this.logger.log(chalk.red(`Error in workspace ${workspaceId}.`)); + } + } + + private mapFieldMetadata( + fieldMetadata: FieldMetadataEntity, + ): FieldMetadataEntity { + const relationMetadata = + fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata; + + const relationDirection = deduceRelationDirection( + fieldMetadata, + relationMetadata, + ); + let relationType = relationMetadata.relationType as unknown as RelationType; + + if ( + relationDirection === RelationDirection.TO && + relationType === RelationType.ONE_TO_MANY + ) { + relationType = RelationType.MANY_TO_ONE; + } + + const relationTargetFieldMetadataId = + relationDirection === RelationDirection.FROM + ? relationMetadata.toFieldMetadataId + : relationMetadata.fromFieldMetadataId; + + const relationTargetObjectMetadataId = + relationDirection === RelationDirection.FROM + ? relationMetadata.toObjectMetadataId + : relationMetadata.fromObjectMetadataId; + + return { + ...fieldMetadata, + settings: { + relationType, + onDelete: relationMetadata.onDeleteAction, + }, + relationTargetFieldMetadataId, + relationTargetObjectMetadataId, + }; + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-41/0-41-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-41/0-41-upgrade-version.command.ts index a60dbc994..5a8d6216e 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-41/0-41-upgrade-version.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-41/0-41-upgrade-version.command.ts @@ -5,6 +5,7 @@ import { Repository } from 'typeorm'; import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; import { BaseCommandOptions } from 'src/database/commands/base.command'; +import { MigrateRelationsToFieldMetadataCommand } from 'src/database/commands/upgrade-version/0-41/0-41-migrate-relations-to-field-metadata.command'; import { SeedWorkflowViewsCommand } from 'src/database/commands/upgrade-version/0-41/0-41-seed-workflow-views.command'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command'; @@ -19,6 +20,7 @@ export class UpgradeTo0_41Command extends ActiveWorkspacesCommandRunner { protected readonly workspaceRepository: Repository, private readonly seedWorkflowViewsCommand: SeedWorkflowViewsCommand, private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand, + private readonly migrateRelationsToFieldMetadata: MigrateRelationsToFieldMetadataCommand, ) { super(workspaceRepository); } @@ -44,5 +46,11 @@ export class UpgradeTo0_41Command extends ActiveWorkspacesCommandRunner { options, workspaceIds, ); + + await this.migrateRelationsToFieldMetadata.executeActiveWorkspacesCommand( + passedParam, + options, + workspaceIds, + ); } } diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-41/0-41-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-41/0-41-upgrade-version.module.ts index 4c4026052..fe417eaa2 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-41/0-41-upgrade-version.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-41/0-41-upgrade-version.module.ts @@ -1,11 +1,13 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { MigrateRelationsToFieldMetadataCommand } from 'src/database/commands/upgrade-version/0-41/0-41-migrate-relations-to-field-metadata.command'; import { SeedWorkflowViewsCommand } from 'src/database/commands/upgrade-version/0-41/0-41-seed-workflow-views.command'; import { UpgradeTo0_41Command } from 'src/database/commands/upgrade-version/0-41/0-41-upgrade-version.command'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module'; import { SyncWorkspaceLoggerService } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/services/sync-workspace-logger.service'; @@ -16,6 +18,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp @Module({ imports: [ TypeOrmModule.forFeature([Workspace], 'core'), + TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'), TypeORMModule, DataSourceModule, ObjectMetadataModule, @@ -28,6 +31,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp SyncWorkspaceMetadataCommand, SeedWorkflowViewsCommand, UpgradeTo0_41Command, + MigrateRelationsToFieldMetadataCommand, ], }) export class UpgradeTo0_41CommandModule {} diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1737561084251-addRelationTargetFieldAndObjectToFieldMetadata.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1737561084251-addRelationTargetFieldAndObjectToFieldMetadata.ts new file mode 100644 index 000000000..e8932a7c4 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/metadata/migrations/1737561084251-addRelationTargetFieldAndObjectToFieldMetadata.ts @@ -0,0 +1,55 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddRelationTargetFieldAndObjectToFieldMetadata1737561084251 + implements MigrationInterface +{ + name = 'AddRelationTargetFieldAndObjectToFieldMetadata1737561084251'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "metadata"."fieldMetadata" ADD "relationTargetFieldMetadataId" uuid`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."fieldMetadata" ADD CONSTRAINT "UQ_47a6c57e1652b6475f8248cff78" UNIQUE ("relationTargetFieldMetadataId")`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."fieldMetadata" ADD "relationTargetObjectMetadataId" uuid`, + ); + await queryRunner.query( + `CREATE INDEX "IndexOnRelationTargetObjectMetadataId" ON "metadata"."fieldMetadata" ("relationTargetObjectMetadataId") `, + ); + await queryRunner.query( + `CREATE INDEX "IndexOnRelationTargetFieldMetadataId" ON "metadata"."fieldMetadata" ("relationTargetFieldMetadataId") `, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."fieldMetadata" ADD CONSTRAINT "FK_47a6c57e1652b6475f8248cff78" FOREIGN KEY ("relationTargetFieldMetadataId") REFERENCES "metadata"."fieldMetadata"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."fieldMetadata" ADD CONSTRAINT "FK_6f6c87ec32cca956d8be321071c" FOREIGN KEY ("relationTargetObjectMetadataId") REFERENCES "metadata"."objectMetadata"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "metadata"."fieldMetadata" DROP CONSTRAINT "FK_6f6c87ec32cca956d8be321071c"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."fieldMetadata" DROP CONSTRAINT "FK_47a6c57e1652b6475f8248cff78"`, + ); + await queryRunner.query( + `DROP INDEX "metadata"."IndexOnRelationTargetFieldMetadataId"`, + ); + await queryRunner.query( + `DROP INDEX "metadata"."IndexOnRelationTargetObjectMetadataId"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."fieldMetadata" DROP COLUMN "relationTargetObjectMetadataId"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."fieldMetadata" DROP CONSTRAINT "UQ_47a6c57e1652b6475f8248cff78"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."fieldMetadata" DROP COLUMN "relationTargetFieldMetadataId"`, + ); + } +} 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 400e6643b..a59750474 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 @@ -3,6 +3,7 @@ import { Column, CreateDateColumn, Entity, + Index, JoinColumn, ManyToOne, OneToMany, @@ -28,6 +29,12 @@ import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-met 'objectMetadataId', 'workspaceId', ]) +@Index('IndexOnRelationTargetFieldMetadataId', [ + 'relationTargetFieldMetadataId', +]) +@Index('IndexOnRelationTargetObjectMetadataId', [ + 'relationTargetObjectMetadataId', +]) export class FieldMetadataEntity< T extends FieldMetadataType | 'default' = 'default', > implements FieldMetadataInterface @@ -95,6 +102,26 @@ export class FieldMetadataEntity< @Column({ default: false }) isLabelSyncedWithName: boolean; + @Column({ nullable: true, type: 'uuid' }) + relationTargetFieldMetadataId: string; + @OneToOne( + () => FieldMetadataEntity, + (fieldMetadata: FieldMetadataEntity) => + fieldMetadata.relationTargetFieldMetadataId, + ) + @JoinColumn({ name: 'relationTargetFieldMetadataId' }) + relationTargetFieldMetadata: Relation; + + @Column({ nullable: true, type: 'uuid' }) + relationTargetObjectMetadataId: string; + @ManyToOne( + () => ObjectMetadataEntity, + (objectMetadata: ObjectMetadataEntity) => + objectMetadata.targetRelationFields, + ) + @JoinColumn({ name: 'relationTargetObjectMetadataId' }) + relationTargetObjectMetadata: Relation; + @OneToOne( () => RelationMetadataEntity, (relation: RelationMetadataEntity) => relation.fromFieldMetadata, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts index 150c451af..230cfcdaa 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts @@ -1,5 +1,8 @@ import { FieldMetadataType } from 'twenty-shared'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; + export enum NumberDataType { FLOAT = 'float', INT = 'int', @@ -30,11 +33,17 @@ export type FieldMetadataDateTimeSettings = { displayAsRelativeDate?: boolean; }; +export type FieldMetadataRelationSettings = { + relationType: RelationType; + onDelete?: RelationOnDeleteAction; +}; + type FieldMetadataSettingsMapping = { [FieldMetadataType.NUMBER]: FieldMetadataNumberSettings; [FieldMetadataType.DATE]: FieldMetadataDateSettings; [FieldMetadataType.DATE_TIME]: FieldMetadataDateTimeSettings; [FieldMetadataType.TEXT]: FieldMetadataTextSettings; + [FieldMetadataType.RELATION]: FieldMetadataRelationSettings; }; type SettingsByFieldMetadata = diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface.ts new file mode 100644 index 000000000..6f42eb399 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface.ts @@ -0,0 +1,6 @@ +export enum RelationOnDeleteAction { + CASCADE = 'CASCADE', + RESTRICT = 'RESTRICT', + SET_NULL = 'SET_NULL', + NO_ACTION = 'NO_ACTION', +} diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface.ts new file mode 100644 index 000000000..300fe72ca --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface.ts @@ -0,0 +1,5 @@ +export enum RelationType { + ONE_TO_ONE = 'ONE_TO_ONE', + ONE_TO_MANY = 'ONE_TO_MANY', + MANY_TO_ONE = 'MANY_TO_ONE', +} diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts index b307e1ed8..eeea46d11 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts @@ -112,6 +112,12 @@ export class ObjectMetadataEntity implements ObjectMetadataInterface { ) toRelations: Relation; + @OneToMany( + () => FieldMetadataEntity, + (field) => field.relationTargetObjectMetadataId, + ) + targetRelationFields: Relation; + @ManyToOne(() => DataSourceEntity, (dataSource) => dataSource.objects, { onDelete: 'CASCADE', })