From 214807588a87d62b47c2ae2fcadd6f9fd1d5f5f1 Mon Sep 17 00:00:00 2001 From: Weiko Date: Mon, 26 Feb 2024 21:29:44 +0100 Subject: [PATCH] [messaging] clean orphan threads and messages after connected account deletion (#4195) * [messaging] add connected account associated data delete * add threadCleanerService * fix * fix import * add thread cleaner import * remove log --- .../integrations/message-queue/jobs.module.ts | 9 ++- .../relation-metadata.entity.ts | 1 + ...te-connected-acount-associated-data.job.ts | 35 +++++++++++ ...message-channel-message-association.job.ts | 40 ------------ ...-contact-auto-creation-enabled-listener.ts | 41 ------------ .../messaging-connected-account.listener.ts | 32 ++++++++++ .../messaging-message-channel.listener.ts | 37 ++++++----- .../workspace/messaging/messaging.module.ts | 4 +- .../repositories/company/company.service.ts | 21 +++++-- ...age-channel-message-association.service.ts | 37 ++++++++++- .../message-thread/message-thread.service.ts | 21 +++++++ .../repositories/message/message.service.ts | 19 ++++++ .../create-company/create-company.service.ts | 10 +-- .../thread-cleaner/thread-cleaner.module.ts | 19 ++++++ .../thread-cleaner/thread-cleaner.service.ts | 62 +++++++++++++++++++ .../relation-metadata.health.service.ts | 3 +- ...vert-on-delete-action-to-on-delete.util.ts | 22 +++++++ .../workspace-migration-runner.service.ts | 3 +- .../connected-account.object-metadata.ts | 6 +- .../message-channel.object-metadata.ts | 6 +- .../message-thread.object-metadata.ts | 7 ++- .../message.object-metadata.ts | 7 ++- 22 files changed, 326 insertions(+), 116 deletions(-) create mode 100644 packages/twenty-server/src/workspace/messaging/jobs/delete-connected-acount-associated-data.job.ts delete mode 100644 packages/twenty-server/src/workspace/messaging/jobs/delete-message-channel-message-association.job.ts delete mode 100644 packages/twenty-server/src/workspace/messaging/listeners/is-contact-auto-creation-enabled-listener.ts create mode 100644 packages/twenty-server/src/workspace/messaging/listeners/messaging-connected-account.listener.ts create mode 100644 packages/twenty-server/src/workspace/messaging/services/thread-cleaner/thread-cleaner.module.ts create mode 100644 packages/twenty-server/src/workspace/messaging/services/thread-cleaner/thread-cleaner.service.ts create mode 100644 packages/twenty-server/src/workspace/workspace-migration-runner/utils/convert-on-delete-action-to-on-delete.util.ts diff --git a/packages/twenty-server/src/integrations/message-queue/jobs.module.ts b/packages/twenty-server/src/integrations/message-queue/jobs.module.ts index 0b03314c26..036f41a509 100644 --- a/packages/twenty-server/src/integrations/message-queue/jobs.module.ts +++ b/packages/twenty-server/src/integrations/message-queue/jobs.module.ts @@ -24,8 +24,10 @@ import { CreateCompaniesAndContactsAfterSyncJob } from 'src/workspace/messaging/ import { CreateCompaniesAndContactsModule } from 'src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.module'; import { MessageChannelModule } from 'src/workspace/messaging/repositories/message-channel/message-channel.module'; import { MessageParticipantModule } from 'src/workspace/messaging/repositories/message-participant/message-participant.module'; -import { DataSeedDemoWorkspaceJob } from 'src/database/commands/data-seed-demo-workspace/jobs/data-seed-demo-workspace.job'; import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module'; +import { DataSeedDemoWorkspaceJob } from 'src/database/commands/data-seed-demo-workspace/jobs/data-seed-demo-workspace.job'; +import { DeleteConnectedAccountAssociatedDataJob } from 'src/workspace/messaging/jobs/delete-connected-acount-associated-data.job'; +import { ThreadCleanerModule } from 'src/workspace/messaging/services/thread-cleaner/thread-cleaner.module'; @Module({ imports: [ @@ -44,6 +46,7 @@ import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-dem CreateCompaniesAndContactsModule, MessageChannelModule, DataSeedDemoWorkspaceModule, + ThreadCleanerModule, ], providers: [ { @@ -83,6 +86,10 @@ import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-dem provide: DataSeedDemoWorkspaceJob.name, useClass: DataSeedDemoWorkspaceJob, }, + { + provide: DeleteConnectedAccountAssociatedDataJob.name, + useClass: DeleteConnectedAccountAssociatedDataJob, + }, ], }) export class JobsModule { diff --git a/packages/twenty-server/src/metadata/relation-metadata/relation-metadata.entity.ts b/packages/twenty-server/src/metadata/relation-metadata/relation-metadata.entity.ts index 5e98f396d8..6b6f8f6573 100644 --- a/packages/twenty-server/src/metadata/relation-metadata/relation-metadata.entity.ts +++ b/packages/twenty-server/src/metadata/relation-metadata/relation-metadata.entity.ts @@ -24,6 +24,7 @@ export enum RelationOnDeleteAction { CASCADE = 'CASCADE', RESTRICT = 'RESTRICT', SET_NULL = 'SET_NULL', + NO_ACTION = 'NO_ACTION', } @Entity('relationMetadata') diff --git a/packages/twenty-server/src/workspace/messaging/jobs/delete-connected-acount-associated-data.job.ts b/packages/twenty-server/src/workspace/messaging/jobs/delete-connected-acount-associated-data.job.ts new file mode 100644 index 0000000000..81c5bb35c9 --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/jobs/delete-connected-acount-associated-data.job.ts @@ -0,0 +1,35 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { MessageQueueJob } from 'src/integrations/message-queue/interfaces/message-queue-job.interface'; + +import { ThreadCleanerService } from 'src/workspace/messaging/services/thread-cleaner/thread-cleaner.service'; + +export type DeleteConnectedAccountAssociatedDataJobData = { + workspaceId: string; + connectedAccountId: string; +}; + +@Injectable() +export class DeleteConnectedAccountAssociatedDataJob + implements MessageQueueJob +{ + private readonly logger = new Logger( + DeleteConnectedAccountAssociatedDataJob.name, + ); + + constructor(private readonly threadCleanerService: ThreadCleanerService) {} + + async handle( + data: DeleteConnectedAccountAssociatedDataJobData, + ): Promise { + this.logger.log( + `Deleting connected account ${data.connectedAccountId} associated data in workspace ${data.workspaceId}`, + ); + + await this.threadCleanerService.cleanWorkspaceThreads(data.workspaceId); + + this.logger.log( + `Deleted connected account ${data.connectedAccountId} associated data in workspace ${data.workspaceId}`, + ); + } +} diff --git a/packages/twenty-server/src/workspace/messaging/jobs/delete-message-channel-message-association.job.ts b/packages/twenty-server/src/workspace/messaging/jobs/delete-message-channel-message-association.job.ts deleted file mode 100644 index 15ddcec857..0000000000 --- a/packages/twenty-server/src/workspace/messaging/jobs/delete-message-channel-message-association.job.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import { MessageQueueJob } from 'src/integrations/message-queue/interfaces/message-queue-job.interface'; - -import { MessageChannelMessageAssociationService } from 'src/workspace/messaging/repositories/message-channel-message-association/message-channel-message-association.service'; - -export type DeleteMessageChannelMessageAssociationJobData = { - workspaceId: string; - messageChannelId: string; -}; - -@Injectable() -export class DeleteMessageChannelMessageAssociationJob - implements MessageQueueJob -{ - private readonly logger = new Logger( - DeleteMessageChannelMessageAssociationJob.name, - ); - - constructor( - private readonly messageChannelMessageAssociationService: MessageChannelMessageAssociationService, - ) {} - - async handle( - data: DeleteMessageChannelMessageAssociationJobData, - ): Promise { - this.logger.log( - `Deleting message channel message association for message channel ${data.messageChannelId} in workspace ${data.workspaceId}`, - ); - - await this.messageChannelMessageAssociationService.deleteByMessageChannelId( - data.messageChannelId, - data.workspaceId, - ); - - this.logger.log( - `Deleted message channel message association for message channel ${data.messageChannelId} in workspace ${data.workspaceId}`, - ); - } -} diff --git a/packages/twenty-server/src/workspace/messaging/listeners/is-contact-auto-creation-enabled-listener.ts b/packages/twenty-server/src/workspace/messaging/listeners/is-contact-auto-creation-enabled-listener.ts deleted file mode 100644 index 02c29283a6..0000000000 --- a/packages/twenty-server/src/workspace/messaging/listeners/is-contact-auto-creation-enabled-listener.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Injectable, Inject } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; - -import { ObjectRecordUpdateEvent } from 'src/integrations/event-emitter/types/object-record-update.event'; -import { MessageQueue } from 'src/integrations/message-queue/message-queue.constants'; -import { MessageQueueService } from 'src/integrations/message-queue/services/message-queue.service'; -import { MessageChannelObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata'; -import { objectRecordChangedProperties as objectRecordUpdateEventChangedProperties } from 'src/integrations/event-emitter/utils/object-record-changed-properties.util'; -import { - CreateCompaniesAndContactsAfterSyncJob, - CreateCompaniesAndContactsAfterSyncJobData, -} from 'src/workspace/messaging/jobs/create-companies-and-contacts-after-sync.job'; - -@Injectable() -export class IsContactAutoCreationEnabledListener { - constructor( - @Inject(MessageQueue.messagingQueue) - private readonly messageQueueService: MessageQueueService, - ) {} - - @OnEvent('messageChannel.updated') - handleUpdatedEvent( - payload: ObjectRecordUpdateEvent, - ) { - if ( - objectRecordUpdateEventChangedProperties( - payload.previousRecord, - payload.updatedRecord, - ).includes('isContactAutoCreationEnabled') && - payload.updatedRecord.isContactAutoCreationEnabled - ) { - this.messageQueueService.add( - CreateCompaniesAndContactsAfterSyncJob.name, - { - workspaceId: payload.workspaceId, - messageChannelId: payload.updatedRecord.id, - }, - ); - } - } -} diff --git a/packages/twenty-server/src/workspace/messaging/listeners/messaging-connected-account.listener.ts b/packages/twenty-server/src/workspace/messaging/listeners/messaging-connected-account.listener.ts new file mode 100644 index 0000000000..1aeee7dd8c --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/listeners/messaging-connected-account.listener.ts @@ -0,0 +1,32 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; + +import { ObjectRecordDeleteEvent } from 'src/integrations/event-emitter/types/object-record-delete.event'; +import { MessageQueue } from 'src/integrations/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/integrations/message-queue/services/message-queue.service'; +import { + DeleteConnectedAccountAssociatedDataJobData, + DeleteConnectedAccountAssociatedDataJob, +} from 'src/workspace/messaging/jobs/delete-connected-acount-associated-data.job'; +import { ConnectedAccountObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/connected-account.object-metadata'; + +@Injectable() +export class MessagingConnectedAccountListener { + constructor( + @Inject(MessageQueue.messagingQueue) + private readonly messageQueueService: MessageQueueService, + ) {} + + @OnEvent('connectedAccount.deleted') + handleDeletedEvent( + payload: ObjectRecordDeleteEvent, + ) { + this.messageQueueService.add( + DeleteConnectedAccountAssociatedDataJob.name, + { + workspaceId: payload.workspaceId, + connectedAccountId: payload.deletedRecord.id, + }, + ); + } +} diff --git a/packages/twenty-server/src/workspace/messaging/listeners/messaging-message-channel.listener.ts b/packages/twenty-server/src/workspace/messaging/listeners/messaging-message-channel.listener.ts index 55978a22ad..5dc19f04de 100644 --- a/packages/twenty-server/src/workspace/messaging/listeners/messaging-message-channel.listener.ts +++ b/packages/twenty-server/src/workspace/messaging/listeners/messaging-message-channel.listener.ts @@ -1,13 +1,14 @@ import { Inject, Injectable } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; -import { ObjectRecordDeleteEvent } from 'src/integrations/event-emitter/types/object-record-delete.event'; +import { ObjectRecordUpdateEvent } from 'src/integrations/event-emitter/types/object-record-update.event'; +import { objectRecordChangedProperties } from 'src/integrations/event-emitter/utils/object-record-changed-properties.util'; import { MessageQueue } from 'src/integrations/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/integrations/message-queue/services/message-queue.service'; import { - DeleteMessageChannelMessageAssociationJob, - DeleteMessageChannelMessageAssociationJobData, -} from 'src/workspace/messaging/jobs/delete-message-channel-message-association.job'; + CreateCompaniesAndContactsAfterSyncJobData, + CreateCompaniesAndContactsAfterSyncJob, +} from 'src/workspace/messaging/jobs/create-companies-and-contacts-after-sync.job'; import { MessageChannelObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata'; @Injectable() @@ -17,16 +18,24 @@ export class MessagingMessageChannelListener { private readonly messageQueueService: MessageQueueService, ) {} - @OnEvent('messageChannel.deleted') - handleDeletedEvent( - payload: ObjectRecordDeleteEvent, + @OnEvent('messageChannel.updated') + handleUpdatedEvent( + payload: ObjectRecordUpdateEvent, ) { - this.messageQueueService.add( - DeleteMessageChannelMessageAssociationJob.name, - { - workspaceId: payload.workspaceId, - messageChannelId: payload.deletedRecord.id, - }, - ); + if ( + objectRecordChangedProperties( + payload.previousRecord, + payload.updatedRecord, + ).includes('isContactAutoCreationEnabled') && + payload.updatedRecord.isContactAutoCreationEnabled + ) { + this.messageQueueService.add( + CreateCompaniesAndContactsAfterSyncJob.name, + { + workspaceId: payload.workspaceId, + messageChannelId: payload.updatedRecord.id, + }, + ); + } } } diff --git a/packages/twenty-server/src/workspace/messaging/messaging.module.ts b/packages/twenty-server/src/workspace/messaging/messaging.module.ts index f7079e4fbe..fa82fcb3e4 100644 --- a/packages/twenty-server/src/workspace/messaging/messaging.module.ts +++ b/packages/twenty-server/src/workspace/messaging/messaging.module.ts @@ -18,7 +18,6 @@ import { GmailRefreshAccessTokenService } from 'src/workspace/messaging/services import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; import { MessageParticipantModule } from 'src/workspace/messaging/repositories/message-participant/message-participant.module'; import { MessagingWorkspaceMemberListener } from 'src/workspace/messaging/listeners/messaging-workspace-member.listener'; -import { IsContactAutoCreationEnabledListener } from 'src/workspace/messaging/listeners/is-contact-auto-creation-enabled-listener'; import { MessagingMessageChannelListener } from 'src/workspace/messaging/listeners/messaging-message-channel.listener'; import { MessageService } from 'src/workspace/messaging/repositories/message/message.service'; import { WorkspaceMemberModule } from 'src/workspace/messaging/repositories/workspace-member/workspace-member.module'; @@ -26,6 +25,7 @@ import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity'; import { CreateCompaniesAndContactsModule } from 'src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.module'; import { CompanyModule } from 'src/workspace/messaging/repositories/company/company.module'; import { PersonModule } from 'src/workspace/messaging/repositories/person/person.module'; +import { MessagingConnectedAccountListener } from 'src/workspace/messaging/listeners/messaging-connected-account.listener'; @Module({ imports: [ EnvironmentModule, @@ -52,9 +52,9 @@ import { PersonModule } from 'src/workspace/messaging/repositories/person/person CreateCompanyService, MessagingPersonListener, MessagingWorkspaceMemberListener, - IsContactAutoCreationEnabledListener, MessagingMessageChannelListener, MessageService, + MessagingConnectedAccountListener, ], exports: [ GmailPartialSyncService, diff --git a/packages/twenty-server/src/workspace/messaging/repositories/company/company.service.ts b/packages/twenty-server/src/workspace/messaging/repositories/company/company.service.ts index e3f69253b6..a3a1698ccf 100644 --- a/packages/twenty-server/src/workspace/messaging/repositories/company/company.service.ts +++ b/packages/twenty-server/src/workspace/messaging/repositories/company/company.service.ts @@ -4,6 +4,13 @@ import { EntityManager } from 'typeorm'; import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service'; +export type CompanyToCreate = { + id: string; + domainName: string; + name?: string; + city?: string; +}; + // TODO: Move outside of the messaging module @Injectable() export class CompanyService { @@ -31,20 +38,22 @@ export class CompanyService { } public async createCompany( - id: string, - name: string, - domainName: string, - city: string, workspaceId: string, + companyToCreate: CompanyToCreate, transactionManager?: EntityManager, ): Promise { const dataSourceSchema = this.workspaceDataSourceService.getSchemaName(workspaceId); await this.workspaceDataSourceService.executeRawQuery( - `INSERT INTO ${dataSourceSchema}.company (id, name, "domainName", address) + `INSERT INTO ${dataSourceSchema}.company (id, "domainName", name, address) VALUES ($1, $2, $3, $4)`, - [id, name, domainName, city], + [ + companyToCreate.id, + companyToCreate.domainName, + companyToCreate.name ?? '', + companyToCreate.city ?? '', + ], workspaceId, transactionManager, ); diff --git a/packages/twenty-server/src/workspace/messaging/repositories/message-channel-message-association/message-channel-message-association.service.ts b/packages/twenty-server/src/workspace/messaging/repositories/message-channel-message-association/message-channel-message-association.service.ts index 72c4564628..18649d722a 100644 --- a/packages/twenty-server/src/workspace/messaging/repositories/message-channel-message-association/message-channel-message-association.service.ts +++ b/packages/twenty-server/src/workspace/messaging/repositories/message-channel-message-association/message-channel-message-association.service.ts @@ -67,17 +67,50 @@ export class MessageChannelMessageAssociationService { ); } + public async getByMessageChannelIds( + messageChannelIds: string[], + workspaceId: string, + transactionManager?: EntityManager, + ): Promise[]> { + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); + + return await this.workspaceDataSourceService.executeRawQuery( + `SELECT * FROM ${dataSourceSchema}."messageChannelMessageAssociation" + WHERE "messageChannelId" = ANY($1)`, + [messageChannelIds], + workspaceId, + transactionManager, + ); + } + public async deleteByMessageChannelId( messageChannelId: string, workspaceId: string, transactionManager?: EntityManager, ) { + this.deleteByMessageChannelIds( + [messageChannelId], + workspaceId, + transactionManager, + ); + } + + public async deleteByMessageChannelIds( + messageChannelIds: string[], + workspaceId: string, + transactionManager?: EntityManager, + ) { + if (messageChannelIds.length === 0) { + return; + } + const dataSourceSchema = this.workspaceDataSourceService.getSchemaName(workspaceId); await this.workspaceDataSourceService.executeRawQuery( - `DELETE FROM ${dataSourceSchema}."messageChannelMessageAssociation" WHERE "messageChannelId" = $1`, - [messageChannelId], + `DELETE FROM ${dataSourceSchema}."messageChannelMessageAssociation" WHERE "messageChannelId" = ANY($1)`, + [messageChannelIds], workspaceId, transactionManager, ); diff --git a/packages/twenty-server/src/workspace/messaging/repositories/message-thread/message-thread.service.ts b/packages/twenty-server/src/workspace/messaging/repositories/message-thread/message-thread.service.ts index 294934d5ef..eb0b98e9c2 100644 --- a/packages/twenty-server/src/workspace/messaging/repositories/message-thread/message-thread.service.ts +++ b/packages/twenty-server/src/workspace/messaging/repositories/message-thread/message-thread.service.ts @@ -6,6 +6,8 @@ import { v4 } from 'uuid'; import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service'; import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity'; import { MessageChannelMessageAssociationService } from 'src/workspace/messaging/repositories/message-channel-message-association/message-channel-message-association.service'; +import { MessageThreadObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-thread.object-metadata'; +import { ObjectRecord } from 'src/workspace/workspace-sync-metadata/types/object-record'; @Injectable() export class MessageThreadService { @@ -14,6 +16,25 @@ export class MessageThreadService { private readonly workspaceDataSourceService: WorkspaceDataSourceService, ) {} + public async getOrphanThreads( + workspaceId: string, + transactionManager?: EntityManager, + ): Promise[]> { + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); + + return await this.workspaceDataSourceService.executeRawQuery( + `SELECT mt.* FROM ${dataSourceSchema}."messageThread" mt + WHERE NOT EXISTS ( + SELECT 1 FROM ${dataSourceSchema}."message" m + WHERE m."messageThreadId" = mt.id + )`, + [], + workspaceId, + transactionManager, + ); + } + public async deleteByIds( messageThreadIds: string[], workspaceId: string, diff --git a/packages/twenty-server/src/workspace/messaging/repositories/message/message.service.ts b/packages/twenty-server/src/workspace/messaging/repositories/message/message.service.ts index 0ecd39ba1b..c13d977a36 100644 --- a/packages/twenty-server/src/workspace/messaging/repositories/message/message.service.ts +++ b/packages/twenty-server/src/workspace/messaging/repositories/message/message.service.ts @@ -26,6 +26,25 @@ export class MessageService { private readonly createCompaniesAndContactsService: CreateCompaniesAndContactsService, ) {} + public async getNonAssociatedMessages( + workspaceId: string, + transactionManager?: EntityManager, + ): Promise[]> { + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); + + return await this.workspaceDataSourceService.executeRawQuery( + `SELECT m.* FROM ${dataSourceSchema}."message" m + WHERE NOT EXISTS ( + SELECT 1 FROM ${dataSourceSchema}."messageChannelMessageAssociation" mcma + WHERE mcma."messageId" = m.id + )`, + [], + workspaceId, + transactionManager, + ); + } + public async getFirstByHeaderMessageId( headerMessageId: string, workspaceId: string, diff --git a/packages/twenty-server/src/workspace/messaging/services/create-company/create-company.service.ts b/packages/twenty-server/src/workspace/messaging/services/create-company/create-company.service.ts index cf3077e522..6452b202a4 100644 --- a/packages/twenty-server/src/workspace/messaging/services/create-company/create-company.service.ts +++ b/packages/twenty-server/src/workspace/messaging/services/create-company/create-company.service.ts @@ -77,11 +77,13 @@ export class CreateCompanyService { const { name, city } = await this.getCompanyInfoFromDomainName(domainName); this.companyService.createCompany( - companyId, - name, - domainName, - city, workspaceId, + { + id: companyId, + domainName, + name, + city, + }, transactionManager, ); diff --git a/packages/twenty-server/src/workspace/messaging/services/thread-cleaner/thread-cleaner.module.ts b/packages/twenty-server/src/workspace/messaging/services/thread-cleaner/thread-cleaner.module.ts new file mode 100644 index 0000000000..92b71c6e87 --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/services/thread-cleaner/thread-cleaner.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; + +import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; +import { DataSourceModule } from 'src/metadata/data-source/data-source.module'; +import { MessageThreadModule } from 'src/workspace/messaging/repositories/message-thread/message-thread.module'; +import { MessageModule } from 'src/workspace/messaging/repositories/message/message.module'; +import { ThreadCleanerService } from 'src/workspace/messaging/services/thread-cleaner/thread-cleaner.service'; + +@Module({ + imports: [ + DataSourceModule, + TypeORMModule, + MessageThreadModule, + MessageModule, + ], + providers: [ThreadCleanerService], + exports: [ThreadCleanerService], +}) +export class ThreadCleanerModule {} diff --git a/packages/twenty-server/src/workspace/messaging/services/thread-cleaner/thread-cleaner.service.ts b/packages/twenty-server/src/workspace/messaging/services/thread-cleaner/thread-cleaner.service.ts new file mode 100644 index 0000000000..2d07c8cb74 --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/services/thread-cleaner/thread-cleaner.service.ts @@ -0,0 +1,62 @@ +import { Injectable } from '@nestjs/common'; + +import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { DataSourceService } from 'src/metadata/data-source/data-source.service'; +import { MessageThreadService } from 'src/workspace/messaging/repositories/message-thread/message-thread.service'; +import { MessageService } from 'src/workspace/messaging/repositories/message/message.service'; + +@Injectable() +export class ThreadCleanerService { + constructor( + private readonly dataSourceService: DataSourceService, + private readonly typeORMService: TypeORMService, + private readonly messageService: MessageService, + private readonly messageThreadService: MessageThreadService, + ) {} + + public async cleanWorkspaceThreads(workspaceId: string) { + const dataSourceMetadata = + await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( + workspaceId, + ); + + const workspaceDataSource = + await this.typeORMService.connectToDataSource(dataSourceMetadata); + + await workspaceDataSource?.transaction(async (transactionManager) => { + const messagesToDelete = + await this.messageService.getNonAssociatedMessages( + workspaceId, + transactionManager, + ); + + const messageIdsToDelete = messagesToDelete.map(({ id }) => id); + + if (messageIdsToDelete.length > 0) { + await this.messageService.deleteByIds( + messageIdsToDelete, + workspaceId, + transactionManager, + ); + } + + const messageThreadsToDelete = + await this.messageThreadService.getOrphanThreads( + workspaceId, + transactionManager, + ); + + const messageThreadToDeleteIds = messageThreadsToDelete.map( + ({ id }) => id, + ); + + if (messageThreadToDeleteIds.length > 0) { + await this.messageThreadService.deleteByIds( + messageThreadToDeleteIds, + workspaceId, + transactionManager, + ); + } + }); + } +} diff --git a/packages/twenty-server/src/workspace/workspace-health/services/relation-metadata.health.service.ts b/packages/twenty-server/src/workspace/workspace-health/services/relation-metadata.health.service.ts index 93f060a748..b85f67b9ff 100644 --- a/packages/twenty-server/src/workspace/workspace-health/services/relation-metadata.health.service.ts +++ b/packages/twenty-server/src/workspace/workspace-health/services/relation-metadata.health.service.ts @@ -23,6 +23,7 @@ import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metada import { createRelationForeignKeyColumnName } from 'src/metadata/relation-metadata/utils/create-relation-foreign-key-column-name.util'; import { createRelationForeignKeyFieldMetadataName } from 'src/metadata/relation-metadata/utils/create-relation-foreign-key-field-metadata-name.util'; import { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util'; +import { convertOnDeleteActionToOnDelete } from 'src/workspace/workspace-migration-runner/utils/convert-on-delete-action-to-on-delete.util'; @Injectable() export class RelationMetadataHealthService { @@ -210,7 +211,7 @@ export class RelationMetadataHealthService { } if ( - relationMetadata.onDeleteAction?.replace(/_/g, ' ') !== + convertOnDeleteActionToOnDelete(relationMetadata.onDeleteAction) !== relationColumn.onDeleteAction ) { issues.push({ diff --git a/packages/twenty-server/src/workspace/workspace-migration-runner/utils/convert-on-delete-action-to-on-delete.util.ts b/packages/twenty-server/src/workspace/workspace-migration-runner/utils/convert-on-delete-action-to-on-delete.util.ts new file mode 100644 index 0000000000..393fde8187 --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-migration-runner/utils/convert-on-delete-action-to-on-delete.util.ts @@ -0,0 +1,22 @@ +import { RelationOnDeleteAction } from 'src/metadata/relation-metadata/relation-metadata.entity'; + +export const convertOnDeleteActionToOnDelete = ( + onDeleteAction: RelationOnDeleteAction | undefined, +): 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION' | undefined => { + if (!onDeleteAction) { + return; + } + + switch (onDeleteAction) { + case 'CASCADE': + return 'CASCADE'; + case 'SET_NULL': + return 'SET NULL'; + case 'RESTRICT': + return 'RESTRICT'; + case 'NO_ACTION': + return 'NO ACTION'; + default: + throw new Error('Invalid onDeleteAction'); + } +}; diff --git a/packages/twenty-server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts b/packages/twenty-server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts index b60e6f9c30..2c87e1f79e 100644 --- a/packages/twenty-server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts +++ b/packages/twenty-server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts @@ -21,6 +21,7 @@ import { } from 'src/metadata/workspace-migration/workspace-migration.entity'; import { WorkspaceCacheVersionService } from 'src/metadata/workspace-cache-version/workspace-cache-version.service'; import { WorkspaceMigrationEnumService } from 'src/workspace/workspace-migration-runner/services/workspace-migration-enum.service'; +import { convertOnDeleteActionToOnDelete } from 'src/workspace/workspace-migration-runner/utils/convert-on-delete-action-to-on-delete.util'; import { customTableDefaultColumns } from './utils/custom-table-default-column.util'; import { WorkspaceMigrationTypeService } from './services/workspace-migration-type.service'; @@ -343,7 +344,7 @@ export class WorkspaceMigrationRunnerService { referencedColumnNames: [migrationColumn.referencedTableColumnName], referencedTableName: migrationColumn.referencedTableName, referencedSchema: schemaName, - onDelete: migrationColumn.onDelete?.replace(/_/g, ' '), + onDelete: convertOnDeleteActionToOnDelete(migrationColumn.onDelete), }), ); diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/connected-account.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/connected-account.object-metadata.ts index f29176bc23..5545358475 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/connected-account.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/connected-account.object-metadata.ts @@ -1,5 +1,8 @@ import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; -import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity'; +import { + RelationMetadataType, + RelationOnDeleteAction, +} from 'src/metadata/relation-metadata/relation-metadata.entity'; import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator'; import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator'; import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator'; @@ -76,6 +79,7 @@ export class ConnectedAccountObjectMetadata extends BaseObjectMetadata { @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, objectName: 'messageChannel', + onDelete: RelationOnDeleteAction.CASCADE, }) @IsNullable() messageChannels: MessageChannelObjectMetadata[]; diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata.ts index 2019ddfe30..224f4340da 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata.ts @@ -1,5 +1,8 @@ import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; -import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity'; +import { + RelationMetadataType, + RelationOnDeleteAction, +} from 'src/metadata/relation-metadata/relation-metadata.entity'; import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator'; import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator'; import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator'; @@ -85,6 +88,7 @@ export class MessageChannelObjectMetadata extends BaseObjectMetadata { @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, objectName: 'messageChannelMessageAssociation', + onDelete: RelationOnDeleteAction.CASCADE, }) @IsNullable() messageChannelMessageAssociations: MessageChannelMessageAssociationObjectMetadata[]; diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-thread.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-thread.object-metadata.ts index cbdaca2d4c..ac8957119a 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-thread.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-thread.object-metadata.ts @@ -1,5 +1,8 @@ import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; -import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity'; +import { + RelationMetadataType, + RelationOnDeleteAction, +} from 'src/metadata/relation-metadata/relation-metadata.entity'; import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator'; import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator'; import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator'; @@ -27,6 +30,7 @@ export class MessageThreadObjectMetadata extends BaseObjectMetadata { @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, objectName: 'message', + onDelete: RelationOnDeleteAction.CASCADE, }) @IsNullable() messages: MessageObjectMetadata[]; @@ -40,6 +44,7 @@ export class MessageThreadObjectMetadata extends BaseObjectMetadata { @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, objectName: 'messageChannelMessageAssociation', + onDelete: RelationOnDeleteAction.RESTRICT, }) @IsNullable() messageChannelMessageAssociations: MessageChannelMessageAssociationObjectMetadata[]; diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message.object-metadata.ts index bf40de0102..c8dec5287a 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message.object-metadata.ts @@ -1,5 +1,8 @@ import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; -import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity'; +import { + RelationMetadataType, + RelationOnDeleteAction, +} from 'src/metadata/relation-metadata/relation-metadata.entity'; import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator'; import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator'; import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator'; @@ -93,6 +96,7 @@ export class MessageObjectMetadata extends BaseObjectMetadata { type: RelationMetadataType.ONE_TO_MANY, objectName: 'messageParticipant', inverseSideFieldName: 'message', + onDelete: RelationOnDeleteAction.CASCADE, }) @IsNullable() messageParticipants: MessageParticipantObjectMetadata[]; @@ -106,6 +110,7 @@ export class MessageObjectMetadata extends BaseObjectMetadata { @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, objectName: 'messageChannelMessageAssociation', + onDelete: RelationOnDeleteAction.CASCADE, }) @IsNullable() messageChannelMessageAssociations: MessageChannelMessageAssociationObjectMetadata[];