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)
This commit is contained in:
Weiko
2025-08-26 16:39:00 +02:00
committed by GitHub
parent 0f347e2f42
commit c09487b943
6 changed files with 193 additions and 25 deletions
@@ -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<Workspace>,
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<void> {
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,
);
}
}
@@ -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 {}
@@ -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],
@@ -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,
};
}
@@ -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<ViewWorkspaceEntity> | null;
@WorkspaceJoinColumn('view')
viewId: string;
@WorkspaceDynamicRelation({
@@ -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<ViewSortWorkspaceEntity[]>;
@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<FavoriteWorkspaceEntity[]>;
@WorkspaceField({
standardId: VIEW_STANDARD_FIELD_IDS.kanbanAggregateOperation,
type: FieldMetadataType.SELECT,