From c09487b943d58800fc3369d138f71b63d8500928 Mon Sep 17 00:00:00 2001 From: Weiko Date: Tue, 26 Aug 2025 16:39:00 +0200 Subject: [PATCH] Remove view FK on workspace favorite entity (#14086) ## Context We are about to move view from workspace schema to core schema however favorite table is still referencing the old table which means we can't insert new records without breaking the constraint. This PR removes it (and we don't have any plan to add a new one between core / workspaceSchema, in fact, we might move the favorite table to core schema as well in the future) --- ...5-remove-favorite-view-relation.command.ts | 151 ++++++++++++++++++ .../1-5/1-5-upgrade-version-command.module.ts | 25 +++ .../upgrade-version-command.module.ts | 2 + .../upgrade.command.ts | 10 ++ .../favorite.workspace-entity.ts | 17 +- .../standard-objects/view.workspace-entity.ts | 13 -- 6 files changed, 193 insertions(+), 25 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-upgrade-version-command.module.ts diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command.ts new file mode 100644 index 0000000000..5fc1adf1f7 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command.ts @@ -0,0 +1,151 @@ +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { DataSource, Repository } from 'typeorm'; + +import { + ActiveOrSuspendedWorkspacesMigrationCommandRunner, + type RunOnWorkspaceArgs, +} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service'; +import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; +import { + FAVORITE_STANDARD_FIELD_IDS, + VIEW_STANDARD_FIELD_IDS, +} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; + +@Command({ + name: 'migrate:1-5:remove-favorite-view-relation', + description: 'Remove favorite view relation.', +}) +export class RemoveFavoriteViewRelation extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly workspaceSchemaManager: WorkspaceSchemaManagerService, + @InjectDataSource('core') + protected readonly coreDataSource: DataSource, + private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, + ) { + super(workspaceRepository, twentyORMGlobalManager); + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + const queryRunner = this.coreDataSource.createQueryRunner(); + + await queryRunner.connect(); + + await queryRunner.startTransaction(); + + try { + const objectMetadataRepository = + queryRunner.manager.getRepository(ObjectMetadataEntity); + + const fieldMetadataRepository = + queryRunner.manager.getRepository(FieldMetadataEntity); + + const [favoriteObjectMetadata] = await objectMetadataRepository.find({ + where: { + standardId: STANDARD_OBJECT_IDS.favorite, + workspaceId, + }, + }); + + if (!favoriteObjectMetadata) { + throw new Error('Favorite object metadata not found'); + } + + const [viewObjectMetadata] = await objectMetadataRepository.find({ + where: { + standardId: STANDARD_OBJECT_IDS.view, + workspaceId, + }, + }); + + if (!viewObjectMetadata) { + throw new Error('View object metadata not found'); + } + + const [favoriteViewFieldMetadata] = await fieldMetadataRepository.find({ + where: { + objectMetadataId: favoriteObjectMetadata.id, + standardId: FAVORITE_STANDARD_FIELD_IDS.view, + workspaceId, + }, + }); + + const [viewFavoriteFieldMetadata] = await fieldMetadataRepository.find({ + where: { + objectMetadataId: viewObjectMetadata.id, + standardId: VIEW_STANDARD_FIELD_IDS.favorites, + workspaceId, + }, + }); + + if (!viewFavoriteFieldMetadata || !favoriteViewFieldMetadata) { + this.logger.warn( + 'View or favorite view field metadata not found or already migrated, skipping...', + ); + + return; + } + + if (!options.dryRun) { + await fieldMetadataRepository.delete(viewFavoriteFieldMetadata.id); + await fieldMetadataRepository.update(favoriteViewFieldMetadata.id, { + name: 'viewId', + type: FieldMetadataType.UUID, + label: 'ViewId', + description: 'ViewId', + icon: 'IconView', + isSystem: true, + isNullable: true, + relationTargetFieldMetadataId: null, + relationTargetObjectMetadataId: null, + settings: null, + }); + } + + const workspaceSchemaName = getWorkspaceSchemaName(workspaceId); + + const foreignKeyName = + queryRunner.connection.namingStrategy.foreignKeyName( + favoriteObjectMetadata.nameSingular, + [`${favoriteViewFieldMetadata.name}Id`], + `${workspaceSchemaName}.${viewObjectMetadata.nameSingular}`, + [`${viewFavoriteFieldMetadata.name}`], + ); + + if (!options.dryRun) { + await this.workspaceSchemaManager.foreignKeyManager.dropForeignKey({ + queryRunner, + schemaName: workspaceSchemaName, + tableName: favoriteObjectMetadata.nameSingular, + foreignKeyName, + }); + } + + await queryRunner.commitTransaction(); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + + await this.workspaceMetadataVersionService.incrementMetadataVersion( + workspaceId, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-upgrade-version-command.module.ts new file mode 100644 index 0000000000..0cf2fe865c --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-5/1-5-upgrade-version-command.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { RemoveFavoriteViewRelation } from 'src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; +import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module'; +import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature( + [Workspace, FieldMetadataEntity, ObjectMetadataEntity], + 'core', + ), + WorkspaceDataSourceModule, + WorkspaceSchemaManagerModule, + WorkspaceMetadataVersionModule, + ], + providers: [RemoveFavoriteViewRelation], + exports: [RemoveFavoriteViewRelation], +}) +export class V1_5_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts index e6afb3ee54..69abcdee3a 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts @@ -6,6 +6,7 @@ import { V0_55_UpgradeVersionCommandModule } from 'src/database/commands/upgrade import { V1_1_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-1/1-1-upgrade-version-command.module'; import { V1_2_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-2/1-2-upgrade-version-command.module'; import { V1_3_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-3/1-3-upgrade-version-command.module'; +import { V1_5_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-5/1-5-upgrade-version-command.module'; import { DatabaseMigrationService, UpgradeCommand, @@ -21,6 +22,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp V1_1_UpgradeVersionCommandModule, V1_2_UpgradeVersionCommandModule, V1_3_UpgradeVersionCommandModule, + V1_5_UpgradeVersionCommandModule, WorkspaceSyncMetadataModule, ], providers: [DatabaseMigrationService, UpgradeCommand], diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index ff338d3978..cdaec72b56 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -31,6 +31,7 @@ import { RemoveWorkflowRunsWithoutState } from 'src/database/commands/upgrade-ve import { AddNextStepIdsToWorkflowRunsTrigger } from 'src/database/commands/upgrade-version-command/1-3/1-3-add-next-step-ids-to-workflow-runs-trigger.command'; import { AssignRolesToExistingApiKeysCommand } from 'src/database/commands/upgrade-version-command/1-3/1-3-assign-roles-to-existing-api-keys.command'; import { UpdateTimestampColumnTypeInWorkspaceSchemaCommand } from 'src/database/commands/upgrade-version-command/1-3/1-3-update-timestamp-column-type-in-workspace-schema.command'; +import { RemoveFavoriteViewRelation } from 'src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @@ -161,6 +162,9 @@ export class UpgradeCommand extends UpgradeCommandRunner { protected readonly assignRolesToExistingApiKeysCommand: AssignRolesToExistingApiKeysCommand, protected readonly addNextStepIdsToWorkflowRunsTrigger: AddNextStepIdsToWorkflowRunsTrigger, protected readonly updateTimestampColumnTypeInWorkspaceSchemaCommand: UpdateTimestampColumnTypeInWorkspaceSchemaCommand, + + // 1.5 Commands + protected readonly removeFavoriteViewRelation: RemoveFavoriteViewRelation, ) { super( workspaceRepository, @@ -234,6 +238,11 @@ export class UpgradeCommand extends UpgradeCommandRunner { afterSyncMetadata: [], }; + const commands_150: VersionCommands = { + beforeSyncMetadata: [this.removeFavoriteViewRelation], + afterSyncMetadata: [], + }; + this.allCommands = { '0.53.0': commands_053, '0.54.0': commands_054, @@ -244,6 +253,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { '1.2.0': commands_120, '1.3.0': commands_130, '1.4.0': commands_140, + '1.5.0': commands_150, }; } diff --git a/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts b/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts index ebbd0df837..36892e519e 100644 --- a/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts +++ b/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts @@ -23,7 +23,6 @@ import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.work import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity'; import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity'; -import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; import { WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; @@ -211,20 +210,14 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceJoinColumn('note') noteId: string; - @WorkspaceRelation({ + @WorkspaceField({ standardId: FAVORITE_STANDARD_FIELD_IDS.view, - type: RelationType.MANY_TO_ONE, - label: msg`View`, - description: msg`Favorite view`, - icon: 'IconLayoutCollage', - inverseSideTarget: () => ViewWorkspaceEntity, - inverseSideFieldKey: 'favorites', - onDelete: RelationOnDeleteAction.CASCADE, + type: FieldMetadataType.UUID, + label: msg`ViewId`, + description: msg`ViewId`, + icon: 'IconView', }) @WorkspaceIsNullable() - view: Relation | null; - - @WorkspaceJoinColumn('view') viewId: string; @WorkspaceDynamicRelation({ diff --git a/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts index cadd69d03f..477a5c717c 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts @@ -17,7 +17,6 @@ import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-re import { VIEW_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; -import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; import { ViewFilterGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity'; import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity'; @@ -201,18 +200,6 @@ export class ViewWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceIsNullable() viewSorts: Relation; - @WorkspaceRelation({ - standardId: VIEW_STANDARD_FIELD_IDS.favorites, - type: RelationType.ONE_TO_MANY, - label: msg`Favorites`, - description: msg`Favorites linked to the view`, - icon: 'IconHeart', - inverseSideTarget: () => FavoriteWorkspaceEntity, - onDelete: RelationOnDeleteAction.CASCADE, - }) - @WorkspaceIsSystem() - favorites: Relation; - @WorkspaceField({ standardId: VIEW_STANDARD_FIELD_IDS.kanbanAggregateOperation, type: FieldMetadataType.SELECT,