mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-30 20:27:55 +00:00 
			
		
		
		
	Clean views without object metadata (#7153)
Add command for cleaning + clean on object deletion
This commit is contained in:
		| @@ -2,13 +2,14 @@ import { InjectRepository } from '@nestjs/typeorm'; | |||||||
|  |  | ||||||
| import chalk from 'chalk'; | import chalk from 'chalk'; | ||||||
| import { Command } from 'nest-commander'; | import { Command } from 'nest-commander'; | ||||||
| import { Repository } from 'typeorm'; | import { In, Repository } from 'typeorm'; | ||||||
|  |  | ||||||
| import { | import { | ||||||
|   ActiveWorkspacesCommandOptions, |   ActiveWorkspacesCommandOptions, | ||||||
|   ActiveWorkspacesCommandRunner, |   ActiveWorkspacesCommandRunner, | ||||||
| } from 'src/database/commands/active-workspaces.command'; | } from 'src/database/commands/active-workspaces.command'; | ||||||
| import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; | import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; | ||||||
|  | import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; | ||||||
| import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | ||||||
| import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; | import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; | ||||||
| import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; | import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; | ||||||
| @@ -21,6 +22,8 @@ export class BackfillWorkspaceFavoritesCommand extends ActiveWorkspacesCommandRu | |||||||
|   constructor( |   constructor( | ||||||
|     @InjectRepository(Workspace, 'core') |     @InjectRepository(Workspace, 'core') | ||||||
|     protected readonly workspaceRepository: Repository<Workspace>, |     protected readonly workspaceRepository: Repository<Workspace>, | ||||||
|  |     @InjectRepository(ObjectMetadataEntity, 'metadata') | ||||||
|  |     private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>, | ||||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, |     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||||
|   ) { |   ) { | ||||||
|     super(workspaceRepository); |     super(workspaceRepository); | ||||||
| @@ -37,11 +40,17 @@ export class BackfillWorkspaceFavoritesCommand extends ActiveWorkspacesCommandRu | |||||||
|       this.logger.log(`Running command for workspace ${workspaceId}`); |       this.logger.log(`Running command for workspace ${workspaceId}`); | ||||||
|  |  | ||||||
|       try { |       try { | ||||||
|         const workspaceIndexViews = await this.getIndexViews(workspaceId); |         const allWorkspaceIndexViews = await this.getIndexViews(workspaceId); | ||||||
|  |  | ||||||
|  |         const activeWorkspaceIndexViews = | ||||||
|  |           await this.filterViewsWithoutObjectMetadata( | ||||||
|  |             workspaceId, | ||||||
|  |             allWorkspaceIndexViews, | ||||||
|  |           ); | ||||||
|  |  | ||||||
|         await this.createViewWorkspaceFavorites( |         await this.createViewWorkspaceFavorites( | ||||||
|           workspaceId, |           workspaceId, | ||||||
|           workspaceIndexViews.map((view) => view.id), |           activeWorkspaceIndexViews.map((view) => view.id), | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         this.logger.log( |         this.logger.log( | ||||||
| @@ -85,6 +94,26 @@ export class BackfillWorkspaceFavoritesCommand extends ActiveWorkspacesCommandRu | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private async filterViewsWithoutObjectMetadata( | ||||||
|  |     workspaceId: string, | ||||||
|  |     views: ViewWorkspaceEntity[], | ||||||
|  |   ): Promise<ViewWorkspaceEntity[]> { | ||||||
|  |     const viewObjectMetadataIds = views.map((view) => view.objectMetadataId); | ||||||
|  |  | ||||||
|  |     const objectMetadataEntities = await this.objectMetadataRepository.find({ | ||||||
|  |       where: { | ||||||
|  |         workspaceId, | ||||||
|  |         id: In(viewObjectMetadataIds), | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const objectMetadataIds = new Set( | ||||||
|  |       objectMetadataEntities.map((entity) => entity.id), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return views.filter((view) => objectMetadataIds.has(view.objectMetadataId)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private async createViewWorkspaceFavorites( |   private async createViewWorkspaceFavorites( | ||||||
|     workspaceId: string, |     workspaceId: string, | ||||||
|     viewIds: string[], |     viewIds: string[], | ||||||
|   | |||||||
| @@ -0,0 +1,115 @@ | |||||||
|  | import { InjectRepository } from '@nestjs/typeorm'; | ||||||
|  |  | ||||||
|  | import chalk from 'chalk'; | ||||||
|  | import { Command } from 'nest-commander'; | ||||||
|  | import { In, Repository } from 'typeorm'; | ||||||
|  |  | ||||||
|  | import { | ||||||
|  |   ActiveWorkspacesCommandOptions, | ||||||
|  |   ActiveWorkspacesCommandRunner, | ||||||
|  | } from 'src/database/commands/active-workspaces.command'; | ||||||
|  | import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; | ||||||
|  | import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; | ||||||
|  | import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | ||||||
|  | import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; | ||||||
|  |  | ||||||
|  | @Command({ | ||||||
|  |   name: 'upgrade-0.31:clean-views-with-deleted-object-metadata', | ||||||
|  |   description: 'Clean views with deleted object metadata', | ||||||
|  | }) | ||||||
|  | export class CleanViewsWithDeletedObjectMetadataCommand extends ActiveWorkspacesCommandRunner { | ||||||
|  |   constructor( | ||||||
|  |     @InjectRepository(Workspace, 'core') | ||||||
|  |     protected readonly workspaceRepository: Repository<Workspace>, | ||||||
|  |     @InjectRepository(ObjectMetadataEntity, 'metadata') | ||||||
|  |     private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>, | ||||||
|  |     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||||
|  |   ) { | ||||||
|  |     super(workspaceRepository); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async executeActiveWorkspacesCommand( | ||||||
|  |     _passedParam: string[], | ||||||
|  |     _options: ActiveWorkspacesCommandOptions, | ||||||
|  |     workspaceIds: string[], | ||||||
|  |   ): Promise<void> { | ||||||
|  |     this.logger.log('Running command to fix migration'); | ||||||
|  |  | ||||||
|  |     for (const workspaceId of workspaceIds) { | ||||||
|  |       this.logger.log(`Running command for workspace ${workspaceId}`); | ||||||
|  |  | ||||||
|  |       try { | ||||||
|  |         this.logger.log(chalk.green(`Cleaning views of ${workspaceId}.`)); | ||||||
|  |  | ||||||
|  |         await this.cleanViewsWithDeletedObjectMetadata( | ||||||
|  |           workspaceId, | ||||||
|  |           _options.dryRun ?? false, | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         await this.twentyORMGlobalManager.destroyDataSourceForWorkspace( | ||||||
|  |           workspaceId, | ||||||
|  |         ); | ||||||
|  |       } catch (error) { | ||||||
|  |         this.logger.log( | ||||||
|  |           chalk.red( | ||||||
|  |             `Running command on workspace ${workspaceId} failed with error: ${error}`, | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |         continue; | ||||||
|  |       } finally { | ||||||
|  |         this.logger.log( | ||||||
|  |           chalk.green(`Finished running command for workspace ${workspaceId}.`), | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       this.logger.log(chalk.green(`Command completed!`)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async cleanViewsWithDeletedObjectMetadata( | ||||||
|  |     workspaceId: string, | ||||||
|  |     dryRun: boolean, | ||||||
|  |   ): Promise<void> { | ||||||
|  |     const viewRepository = | ||||||
|  |       await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>( | ||||||
|  |         workspaceId, | ||||||
|  |         'view', | ||||||
|  |         false, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |     const allViews = await viewRepository.find(); | ||||||
|  |  | ||||||
|  |     const viewObjectMetadataIds = allViews.map((view) => view.objectMetadataId); | ||||||
|  |  | ||||||
|  |     const objectMetadataEntities = await this.objectMetadataRepository.find({ | ||||||
|  |       where: { | ||||||
|  |         id: In(viewObjectMetadataIds), | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const validObjectMetadataIds = new Set( | ||||||
|  |       objectMetadataEntities.map((entity) => entity.id), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const viewIdsToDelete = allViews | ||||||
|  |       .filter((view) => !validObjectMetadataIds.has(view.objectMetadataId)) | ||||||
|  |       .map((view) => view.id); | ||||||
|  |  | ||||||
|  |     if (dryRun) { | ||||||
|  |       this.logger.log( | ||||||
|  |         chalk.green( | ||||||
|  |           `Found ${viewIdsToDelete.length} views to clean in workspace ${workspaceId}.`, | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (viewIdsToDelete.length > 0 && !dryRun) { | ||||||
|  |       await viewRepository.delete(viewIdsToDelete); | ||||||
|  |       this.logger.log(chalk.green(`Cleaning ${viewIdsToDelete.length} views.`)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (viewIdsToDelete.length === 0) { | ||||||
|  |       this.logger.log(chalk.green(`No views to clean.`)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -5,7 +5,9 @@ import { Repository } from 'typeorm'; | |||||||
|  |  | ||||||
| import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; | import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; | ||||||
| import { BackfillWorkspaceFavoritesCommand } from 'src/database/commands/upgrade-version/0-31/0-31-backfill-workspace-favorites.command'; | import { BackfillWorkspaceFavoritesCommand } from 'src/database/commands/upgrade-version/0-31/0-31-backfill-workspace-favorites.command'; | ||||||
|  | import { CleanViewsWithDeletedObjectMetadataCommand } from 'src/database/commands/upgrade-version/0-31/0-31-clean-views-with-deleted-object-metadata.command'; | ||||||
| import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; | 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'; | ||||||
|  |  | ||||||
| interface UpdateTo0_31CommandOptions { | interface UpdateTo0_31CommandOptions { | ||||||
|   workspaceId?: string; |   workspaceId?: string; | ||||||
| @@ -19,7 +21,9 @@ export class UpgradeTo0_31Command extends ActiveWorkspacesCommandRunner { | |||||||
|   constructor( |   constructor( | ||||||
|     @InjectRepository(Workspace, 'core') |     @InjectRepository(Workspace, 'core') | ||||||
|     protected readonly workspaceRepository: Repository<Workspace>, |     protected readonly workspaceRepository: Repository<Workspace>, | ||||||
|  |     private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand, | ||||||
|     private readonly backfillWorkspaceFavoritesCommand: BackfillWorkspaceFavoritesCommand, |     private readonly backfillWorkspaceFavoritesCommand: BackfillWorkspaceFavoritesCommand, | ||||||
|  |     private readonly cleanViewsWithDeletedObjectMetadataCommand: CleanViewsWithDeletedObjectMetadataCommand, | ||||||
|   ) { |   ) { | ||||||
|     super(workspaceRepository); |     super(workspaceRepository); | ||||||
|   } |   } | ||||||
| @@ -29,12 +33,23 @@ export class UpgradeTo0_31Command extends ActiveWorkspacesCommandRunner { | |||||||
|     options: UpdateTo0_31CommandOptions, |     options: UpdateTo0_31CommandOptions, | ||||||
|     workspaceIds: string[], |     workspaceIds: string[], | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     await this.backfillWorkspaceFavoritesCommand.executeActiveWorkspacesCommand( |     await this.syncWorkspaceMetadataCommand.executeActiveWorkspacesCommand( | ||||||
|       passedParam, |       passedParam, | ||||||
|       { |       { | ||||||
|         ...options, |         ...options, | ||||||
|  |         force: true, | ||||||
|       }, |       }, | ||||||
|       workspaceIds, |       workspaceIds, | ||||||
|     ); |     ); | ||||||
|  |     await this.cleanViewsWithDeletedObjectMetadataCommand.executeActiveWorkspacesCommand( | ||||||
|  |       passedParam, | ||||||
|  |       options, | ||||||
|  |       workspaceIds, | ||||||
|  |     ); | ||||||
|  |     await this.backfillWorkspaceFavoritesCommand.executeActiveWorkspacesCommand( | ||||||
|  |       passedParam, | ||||||
|  |       options, | ||||||
|  |       workspaceIds, | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,11 +2,22 @@ import { Module } from '@nestjs/common'; | |||||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | import { TypeOrmModule } from '@nestjs/typeorm'; | ||||||
|  |  | ||||||
| import { BackfillWorkspaceFavoritesCommand } from 'src/database/commands/upgrade-version/0-31/0-31-backfill-workspace-favorites.command'; | import { BackfillWorkspaceFavoritesCommand } from 'src/database/commands/upgrade-version/0-31/0-31-backfill-workspace-favorites.command'; | ||||||
|  | import { CleanViewsWithDeletedObjectMetadataCommand } from 'src/database/commands/upgrade-version/0-31/0-31-clean-views-with-deleted-object-metadata.command'; | ||||||
| import { UpgradeTo0_31Command } from 'src/database/commands/upgrade-version/0-31/0-31-upgrade-version.command'; | import { UpgradeTo0_31Command } from 'src/database/commands/upgrade-version/0-31/0-31-upgrade-version.command'; | ||||||
| import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; | import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; | ||||||
|  | import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; | ||||||
|  | import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module'; | ||||||
|  |  | ||||||
| @Module({ | @Module({ | ||||||
|   imports: [TypeOrmModule.forFeature([Workspace], 'core')], |   imports: [ | ||||||
|   providers: [UpgradeTo0_31Command, BackfillWorkspaceFavoritesCommand], |     TypeOrmModule.forFeature([Workspace], 'core'), | ||||||
|  |     TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'), | ||||||
|  |     WorkspaceSyncMetadataCommandsModule, | ||||||
|  |   ], | ||||||
|  |   providers: [ | ||||||
|  |     UpgradeTo0_31Command, | ||||||
|  |     BackfillWorkspaceFavoritesCommand, | ||||||
|  |     CleanViewsWithDeletedObjectMetadataCommand, | ||||||
|  |   ], | ||||||
| }) | }) | ||||||
| export class UpgradeTo0_31CommandModule {} | export class UpgradeTo0_31CommandModule {} | ||||||
|   | |||||||
| @@ -10,7 +10,6 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; | |||||||
|  |  | ||||||
| import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; | import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; | ||||||
| import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; | import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; | ||||||
| import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; |  | ||||||
| import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; | import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; | ||||||
| import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; | 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 { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; | ||||||
| @@ -44,7 +43,6 @@ import { UpdateObjectPayload } from './dtos/update-object.input'; | |||||||
|         WorkspaceMigrationModule, |         WorkspaceMigrationModule, | ||||||
|         WorkspaceMigrationRunnerModule, |         WorkspaceMigrationRunnerModule, | ||||||
|         WorkspaceMetadataVersionModule, |         WorkspaceMetadataVersionModule, | ||||||
|         FeatureFlagModule, |  | ||||||
|         RemoteTableRelationsModule, |         RemoteTableRelationsModule, | ||||||
|       ], |       ], | ||||||
|       services: [ObjectMetadataService], |       services: [ObjectMetadataService], | ||||||
|   | |||||||
| @@ -10,7 +10,6 @@ import { FindManyOptions, FindOneOptions, In, Repository } from 'typeorm'; | |||||||
| import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; | import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; | ||||||
|  |  | ||||||
| import { TypeORMService } from 'src/database/typeorm/typeorm.service'; | import { TypeORMService } from 'src/database/typeorm/typeorm.service'; | ||||||
| import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; |  | ||||||
| import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; | import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; | ||||||
| import { | import { | ||||||
|   FieldMetadataEntity, |   FieldMetadataEntity, | ||||||
| @@ -60,6 +59,7 @@ import { | |||||||
|   createRelationDeterministicUuid, |   createRelationDeterministicUuid, | ||||||
| } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util'; | } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util'; | ||||||
| import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; | import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; | ||||||
|  | import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; | ||||||
|  |  | ||||||
| import { ObjectMetadataEntity } from './object-metadata.entity'; | import { ObjectMetadataEntity } from './object-metadata.entity'; | ||||||
|  |  | ||||||
| @@ -85,7 +85,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt | |||||||
|     private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, |     private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, | ||||||
|     private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, |     private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, | ||||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, |     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||||
|     private readonly featureFlagService: FeatureFlagService, |  | ||||||
|   ) { |   ) { | ||||||
|     super(objectMetadataRepository); |     super(objectMetadataRepository); | ||||||
|   } |   } | ||||||
| @@ -143,6 +142,24 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt | |||||||
|       await this.deleteAllRelationsAndDropTable(objectMetadata, workspaceId); |       await this.deleteAllRelationsAndDropTable(objectMetadata, workspaceId); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // DELETE VIEWS | ||||||
|  |     const viewRepository = | ||||||
|  |       await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>( | ||||||
|  |         workspaceId, | ||||||
|  |         'view', | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |     const views = await viewRepository.find({ | ||||||
|  |       where: { | ||||||
|  |         objectMetadataId: objectMetadata.id, | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     if (views.length > 0) { | ||||||
|  |       await viewRepository.delete(views.map((view) => view.id)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // DELETE OBJECT | ||||||
|     await this.objectMetadataRepository.delete(objectMetadata.id); |     await this.objectMetadataRepository.delete(objectMetadata.id); | ||||||
|  |  | ||||||
|     await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( |     await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Thomas Trompette
					Thomas Trompette