[messaging] Fix thread cleaner service subqueries (#4416)

* [messaging] Fix thread cleaner service subqueries

* add pagination

* various fixes

* Fix thread merging

* fix

* fix
This commit is contained in:
Weiko
2024-03-12 17:49:45 +01:00
committed by GitHub
parent 91f4e1a853
commit 4476f5215b
17 changed files with 311 additions and 115 deletions

View File

@ -79,7 +79,6 @@ export class GoogleGmailService {
connectedAccountId, connectedAccountId,
}, },
{ {
id: connectedAccountId,
retryLimit: 2, retryLimit: 2,
}, },
); );

View File

@ -26,10 +26,20 @@ export class GmailFullSyncJob implements MessageQueueJob<GmailFullSyncJobData> {
data.connectedAccountId data.connectedAccountId
} ${data.nextPageToken ? `and ${data.nextPageToken} pageToken` : ''}`, } ${data.nextPageToken ? `and ${data.nextPageToken} pageToken` : ''}`,
); );
await this.gmailRefreshAccessTokenService.refreshAndSaveAccessToken(
data.workspaceId, try {
data.connectedAccountId, await this.gmailRefreshAccessTokenService.refreshAndSaveAccessToken(
); data.workspaceId,
data.connectedAccountId,
);
} catch (e) {
this.logger.error(
`Error refreshing access token for connected account ${data.connectedAccountId} in workspace ${data.workspaceId}`,
e,
);
return;
}
await this.gmailFullSyncService.fetchConnectedAccountThreads( await this.gmailFullSyncService.fetchConnectedAccountThreads(
data.workspaceId, data.workspaceId,

View File

@ -25,10 +25,20 @@ export class GmailPartialSyncJob
this.logger.log( this.logger.log(
`gmail partial-sync for workspace ${data.workspaceId} and account ${data.connectedAccountId}`, `gmail partial-sync for workspace ${data.workspaceId} and account ${data.connectedAccountId}`,
); );
await this.gmailRefreshAccessTokenService.refreshAndSaveAccessToken(
data.workspaceId, try {
data.connectedAccountId, await this.gmailRefreshAccessTokenService.refreshAndSaveAccessToken(
); data.workspaceId,
data.connectedAccountId,
);
} catch (e) {
this.logger.error(
`Error refreshing access token for connected account ${data.connectedAccountId} in workspace ${data.workspaceId}`,
e,
);
return;
}
await this.gmailPartialSyncService.fetchConnectedAccountThreads( await this.gmailPartialSyncService.fetchConnectedAccountThreads(
data.workspaceId, data.workspaceId,

View File

@ -43,11 +43,11 @@ export class ConnectedAccountService {
); );
} }
public async getByIdOrFail( public async getById(
connectedAccountId: string, connectedAccountId: string,
workspaceId: string, workspaceId: string,
transactionManager?: EntityManager, transactionManager?: EntityManager,
): Promise<ObjectRecord<ConnectedAccountObjectMetadata>> { ): Promise<ObjectRecord<ConnectedAccountObjectMetadata> | undefined> {
const dataSourceSchema = const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId); this.workspaceDataSourceService.getSchemaName(workspaceId);
@ -59,13 +59,27 @@ export class ConnectedAccountService {
transactionManager, transactionManager,
); );
if (!connectedAccounts || connectedAccounts.length === 0) { return connectedAccounts[0];
}
public async getByIdOrFail(
connectedAccountId: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<ConnectedAccountObjectMetadata>> {
const connectedAccount = await this.getById(
connectedAccountId,
workspaceId,
transactionManager,
);
if (!connectedAccount) {
throw new NotFoundException( throw new NotFoundException(
`No connected account found for id ${connectedAccountId} in workspace ${workspaceId}`, `Connected account with id ${connectedAccountId} not found in workspace ${workspaceId}`,
); );
} }
return connectedAccounts[0]; return connectedAccount;
} }
public async updateLastSyncHistoryId( public async updateLastSyncHistoryId(

View File

@ -84,18 +84,6 @@ export class MessageChannelMessageAssociationService {
); );
} }
public async deleteByMessageChannelId(
messageChannelId: string,
workspaceId: string,
transactionManager?: EntityManager,
) {
this.deleteByMessageChannelIds(
[messageChannelId],
workspaceId,
transactionManager,
);
}
public async deleteByMessageChannelIds( public async deleteByMessageChannelIds(
messageChannelIds: string[], messageChannelIds: string[],
workspaceId: string, workspaceId: string,

View File

@ -32,17 +32,29 @@ export class MessageChannelService {
connectedAccountId: string, connectedAccountId: string,
workspaceId: string, workspaceId: string,
): Promise<ObjectRecord<MessageChannelObjectMetadata>> { ): Promise<ObjectRecord<MessageChannelObjectMetadata>> {
const messageChannels = await this.getByConnectedAccountId( const messageChannel = await this.getFirstByConnectedAccountId(
connectedAccountId, connectedAccountId,
workspaceId, workspaceId,
); );
if (!messageChannels || messageChannels.length === 0) { if (!messageChannel) {
throw new Error( throw new Error(
`No message channel found for connected account ${connectedAccountId} in workspace ${workspaceId}`, `Message channel for connected account ${connectedAccountId} not found in workspace ${workspaceId}`,
); );
} }
return messageChannel;
}
public async getFirstByConnectedAccountId(
connectedAccountId: string,
workspaceId: string,
): Promise<ObjectRecord<MessageChannelObjectMetadata> | undefined> {
const messageChannels = await this.getByConnectedAccountId(
connectedAccountId,
workspaceId,
);
return messageChannels[0]; return messageChannels[0];
} }

View File

@ -1,11 +1,16 @@
import { Module } from '@nestjs/common'; import { Module, forwardRef } from '@nestjs/common';
import { MessageChannelMessageAssociationModule } from 'src/workspace/messaging/repositories/message-channel-message-association/message-channel-message-assocation.module'; import { MessageChannelMessageAssociationModule } from 'src/workspace/messaging/repositories/message-channel-message-association/message-channel-message-assocation.module';
import { MessageThreadService } from 'src/workspace/messaging/repositories/message-thread/message-thread.service'; import { MessageThreadService } from 'src/workspace/messaging/repositories/message-thread/message-thread.service';
import { MessageModule } from 'src/workspace/messaging/repositories/message/message.module';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
@Module({ @Module({
imports: [WorkspaceDataSourceModule, MessageChannelMessageAssociationModule], imports: [
WorkspaceDataSourceModule,
MessageChannelMessageAssociationModule,
forwardRef(() => MessageModule),
],
providers: [MessageThreadService], providers: [MessageThreadService],
exports: [MessageThreadService], exports: [MessageThreadService],
}) })

View File

@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common'; import { Inject, Injectable, forwardRef } from '@nestjs/common';
import { EntityManager } from 'typeorm'; import { EntityManager } from 'typeorm';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
@ -6,33 +6,38 @@ import { v4 } from 'uuid';
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service'; import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity'; 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 { 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 { MessageService } from 'src/workspace/messaging/repositories/message/message.service';
import { ObjectRecord } from 'src/workspace/workspace-sync-metadata/types/object-record';
@Injectable() @Injectable()
export class MessageThreadService { export class MessageThreadService {
constructor( constructor(
private readonly messageChannelMessageAssociationService: MessageChannelMessageAssociationService, private readonly messageChannelMessageAssociationService: MessageChannelMessageAssociationService,
private readonly workspaceDataSourceService: WorkspaceDataSourceService, private readonly workspaceDataSourceService: WorkspaceDataSourceService,
@Inject(forwardRef(() => MessageService))
private readonly messageService: MessageService,
) {} ) {}
public async getOrphanThreads( public async getOrphanThreadIdsPaginated(
limit: number,
offset: number,
workspaceId: string, workspaceId: string,
transactionManager?: EntityManager, transactionManager?: EntityManager,
): Promise<ObjectRecord<MessageThreadObjectMetadata>[]> { ): Promise<string[]> {
const dataSourceSchema = const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId); this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery( const orphanThreads = await this.workspaceDataSourceService.executeRawQuery(
`SELECT mt.* FROM ${dataSourceSchema}."messageThread" mt `SELECT mt.id
WHERE NOT EXISTS ( FROM ${dataSourceSchema}."messageThread" mt
SELECT 1 FROM ${dataSourceSchema}."message" m LEFT JOIN ${dataSourceSchema}."message" m ON mt.id = m."messageThreadId"
WHERE m."messageThreadId" = mt.id WHERE m."messageThreadId" IS NULL
)`, LIMIT $1 OFFSET $2`,
[], [limit, offset],
workspaceId, workspaceId,
transactionManager, transactionManager,
); );
return orphanThreads.map(({ id }) => id);
} }
public async deleteByIds( public async deleteByIds(
@ -52,11 +57,13 @@ export class MessageThreadService {
} }
public async saveMessageThreadOrReturnExistingMessageThread( public async saveMessageThreadOrReturnExistingMessageThread(
headerMessageId: string,
messageThreadExternalId: string, messageThreadExternalId: string,
dataSourceMetadata: DataSourceEntity, dataSourceMetadata: DataSourceEntity,
workspaceId: string, workspaceId: string,
manager: EntityManager, manager: EntityManager,
) { ) {
// Check if message thread already exists via threadExternalId
const existingMessageChannelMessageAssociationByMessageThreadExternalId = const existingMessageChannelMessageAssociationByMessageThreadExternalId =
await this.messageChannelMessageAssociationService.getFirstByMessageThreadExternalId( await this.messageChannelMessageAssociationService.getFirstByMessageThreadExternalId(
messageThreadExternalId, messageThreadExternalId,
@ -71,6 +78,21 @@ export class MessageThreadService {
return Promise.resolve(existingMessageThread); return Promise.resolve(existingMessageThread);
} }
// Check if message thread already exists via existing message headerMessageId
const existingMessageWithSameHeaderMessageId =
await this.messageService.getFirstOrNullByHeaderMessageId(
headerMessageId,
workspaceId,
manager,
);
if (existingMessageWithSameHeaderMessageId) {
return Promise.resolve(
existingMessageWithSameHeaderMessageId.messageThreadId,
);
}
// If message thread does not exist, create new message thread
const newMessageThreadId = v4(); const newMessageThreadId = v4();
await manager.query( await manager.query(

View File

@ -1,4 +1,4 @@
import { Module } from '@nestjs/common'; import { Module, forwardRef } from '@nestjs/common';
import { MessageChannelModule } from 'src/workspace/messaging/repositories/message-channel/message-channel.module'; import { MessageChannelModule } from 'src/workspace/messaging/repositories/message-channel/message-channel.module';
import { MessageChannelMessageAssociationModule } from 'src/workspace/messaging/repositories/message-channel-message-association/message-channel-message-assocation.module'; import { MessageChannelMessageAssociationModule } from 'src/workspace/messaging/repositories/message-channel-message-association/message-channel-message-assocation.module';
@ -11,7 +11,7 @@ import { CreateCompaniesAndContactsModule } from 'src/workspace/messaging/servic
@Module({ @Module({
imports: [ imports: [
WorkspaceDataSourceModule, WorkspaceDataSourceModule,
MessageThreadModule, forwardRef(() => MessageThreadModule),
MessageParticipantModule, MessageParticipantModule,
MessageChannelMessageAssociationModule, MessageChannelMessageAssociationModule,
MessageChannelModule, MessageChannelModule,

View File

@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common'; import { Inject, Injectable, Logger, forwardRef } from '@nestjs/common';
import { DataSource, EntityManager } from 'typeorm'; import { DataSource, EntityManager } from 'typeorm';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
@ -9,42 +9,47 @@ import { ObjectRecord } from 'src/workspace/workspace-sync-metadata/types/object
import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity'; import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity';
import { GmailMessage } from 'src/workspace/messaging/types/gmail-message'; import { GmailMessage } from 'src/workspace/messaging/types/gmail-message';
import { ConnectedAccountObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/connected-account.object-metadata'; import { ConnectedAccountObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/connected-account.object-metadata';
import { MessageChannelService } from 'src/workspace/messaging/repositories/message-channel/message-channel.service';
import { MessageChannelMessageAssociationService } from 'src/workspace/messaging/repositories/message-channel-message-association/message-channel-message-association.service'; import { MessageChannelMessageAssociationService } from 'src/workspace/messaging/repositories/message-channel-message-association/message-channel-message-association.service';
import { MessageParticipantService } from 'src/workspace/messaging/repositories/message-participant/message-participant.service';
import { MessageThreadService } from 'src/workspace/messaging/repositories/message-thread/message-thread.service'; import { MessageThreadService } from 'src/workspace/messaging/repositories/message-thread/message-thread.service';
import { CreateCompaniesAndContactsService } from 'src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.service'; import { MessageChannelService } from 'src/workspace/messaging/repositories/message-channel/message-channel.service';
@Injectable() @Injectable()
export class MessageService { export class MessageService {
private readonly logger = new Logger(MessageService.name);
constructor( constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService, private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly messageChannelMessageAssociationService: MessageChannelMessageAssociationService, private readonly messageChannelMessageAssociationService: MessageChannelMessageAssociationService,
@Inject(forwardRef(() => MessageThreadService))
private readonly messageThreadService: MessageThreadService, private readonly messageThreadService: MessageThreadService,
private readonly messageParticipantService: MessageParticipantService,
private readonly messageChannelService: MessageChannelService, private readonly messageChannelService: MessageChannelService,
private readonly createCompaniesAndContactsService: CreateCompaniesAndContactsService,
) {} ) {}
public async getNonAssociatedMessages( public async getNonAssociatedMessageIdsPaginated(
limit: number,
offset: number,
workspaceId: string, workspaceId: string,
transactionManager?: EntityManager, transactionManager?: EntityManager,
): Promise<ObjectRecord<MessageObjectMetadata>[]> { ): Promise<string[]> {
const dataSourceSchema = const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId); this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery( const nonAssociatedMessages =
`SELECT m.* FROM ${dataSourceSchema}."message" m await this.workspaceDataSourceService.executeRawQuery(
WHERE NOT EXISTS ( `SELECT m.id FROM ${dataSourceSchema}."message" m
SELECT 1 FROM ${dataSourceSchema}."messageChannelMessageAssociation" mcma LEFT JOIN ${dataSourceSchema}."messageChannelMessageAssociation" mcma
WHERE mcma."messageId" = m.id ON m.id = mcma."messageId"
)`, WHERE mcma.id IS NULL
[], LIMIT $1 OFFSET $2`,
workspaceId, [limit, offset],
transactionManager, workspaceId,
); transactionManager,
);
return nonAssociatedMessages.map(({ id }) => id);
} }
public async getFirstByHeaderMessageId( public async getFirstOrNullByHeaderMessageId(
headerMessageId: string, headerMessageId: string,
workspaceId: string, workspaceId: string,
transactionManager?: EntityManager, transactionManager?: EntityManager,
@ -125,9 +130,32 @@ export class MessageService {
const messageExternalIdsAndIdsMap = new Map<string, string>(); const messageExternalIdsAndIdsMap = new Map<string, string>();
try { try {
let keepImporting = true;
for (const message of messages) { for (const message of messages) {
if (!keepImporting) {
break;
}
await workspaceDataSource?.transaction( await workspaceDataSource?.transaction(
async (manager: EntityManager) => { async (manager: EntityManager) => {
const gmailMessageChannel =
await this.messageChannelService.getByIds(
[gmailMessageChannelId],
workspaceId,
manager,
);
if (gmailMessageChannel.length === 0) {
this.logger.error(
`No message channel found for connected account ${connectedAccount.id} in workspace ${workspaceId} in saveMessages`,
);
keepImporting = false;
return;
}
const existingMessageChannelMessageAssociationsCount = const existingMessageChannelMessageAssociationsCount =
await this.messageChannelMessageAssociationService.countByMessageExternalIdsAndMessageChannelId( await this.messageChannelMessageAssociationService.countByMessageExternalIdsAndMessageChannelId(
[message.externalId], [message.externalId],
@ -140,8 +168,10 @@ export class MessageService {
return; return;
} }
// TODO: This does not handle all thread merging use cases and might create orphan threads.
const savedOrExistingMessageThreadId = const savedOrExistingMessageThreadId =
await this.messageThreadService.saveMessageThreadOrReturnExistingMessageThread( await this.messageThreadService.saveMessageThreadOrReturnExistingMessageThread(
message.headerMessageId,
message.messageThreadExternalId, message.messageThreadExternalId,
dataSourceMetadata, dataSourceMetadata,
workspaceId, workspaceId,
@ -193,7 +223,7 @@ export class MessageService {
workspaceId: string, workspaceId: string,
manager: EntityManager, manager: EntityManager,
): Promise<string> { ): Promise<string> {
const existingMessage = await this.getFirstByHeaderMessageId( const existingMessage = await this.getFirstOrNullByHeaderMessageId(
message.headerMessageId, message.headerMessageId,
workspaceId, workspaceId,
); );

View File

@ -46,11 +46,19 @@ export class GmailFullSyncService {
connectedAccountId: string, connectedAccountId: string,
nextPageToken?: string, nextPageToken?: string,
): Promise<void> { ): Promise<void> {
const connectedAccount = await this.connectedAccountService.getByIdOrFail( const connectedAccount = await this.connectedAccountService.getById(
connectedAccountId, connectedAccountId,
workspaceId, workspaceId,
); );
if (!connectedAccount) {
this.logger.error(
`Connected account ${connectedAccountId} not found in workspace ${workspaceId} during full-sync`,
);
return;
}
const accessToken = connectedAccount.accessToken; const accessToken = connectedAccount.accessToken;
const refreshToken = connectedAccount.refreshToken; const refreshToken = connectedAccount.refreshToken;
const workspaceMemberId = connectedAccount.accountOwnerId; const workspaceMemberId = connectedAccount.accountOwnerId;
@ -62,11 +70,19 @@ export class GmailFullSyncService {
} }
const gmailMessageChannel = const gmailMessageChannel =
await this.messageChannelService.getFirstByConnectedAccountIdOrFail( await this.messageChannelService.getFirstByConnectedAccountId(
connectedAccountId, connectedAccountId,
workspaceId, workspaceId,
); );
if (!gmailMessageChannel) {
this.logger.error(
`No message channel found for connected account ${connectedAccountId} in workspace ${workspaceId} during full-syn`,
);
return;
}
const gmailMessageChannelId = gmailMessageChannel.id; const gmailMessageChannelId = gmailMessageChannel.id;
const gmailClient = const gmailClient =
@ -173,7 +189,7 @@ export class GmailFullSyncService {
); );
if (messagesToSave.length > 0) { if (messagesToSave.length > 0) {
this.saveMessagesAndCreateContactsService.saveMessagesAndCreateContacts( await this.saveMessagesAndCreateContactsService.saveMessagesAndCreateContacts(
messagesToSave, messagesToSave,
connectedAccount, connectedAccount,
workspaceId, workspaceId,

View File

@ -48,11 +48,19 @@ export class GmailPartialSyncService {
connectedAccountId: string, connectedAccountId: string,
maxResults = 500, maxResults = 500,
): Promise<void> { ): Promise<void> {
const connectedAccount = await this.connectedAccountService.getByIdOrFail( const connectedAccount = await this.connectedAccountService.getById(
connectedAccountId, connectedAccountId,
workspaceId, workspaceId,
); );
if (!connectedAccount) {
this.logger.error(
`Connected account ${connectedAccountId} not found in workspace ${workspaceId} during partial-sync`,
);
return;
}
const lastSyncHistoryId = connectedAccount.lastSyncHistoryId; const lastSyncHistoryId = connectedAccount.lastSyncHistoryId;
if (!lastSyncHistoryId) { if (!lastSyncHistoryId) {
@ -135,11 +143,19 @@ export class GmailPartialSyncService {
} }
const gmailMessageChannel = const gmailMessageChannel =
await this.messageChannelService.getFirstByConnectedAccountIdOrFail( await this.messageChannelService.getFirstByConnectedAccountId(
connectedAccountId, connectedAccountId,
workspaceId, workspaceId,
); );
if (!gmailMessageChannel) {
this.logger.error(
`No message channel found for connected account ${connectedAccountId} in workspace ${workspaceId} during partial-sync`,
);
return;
}
const gmailMessageChannelId = gmailMessageChannel.id; const gmailMessageChannelId = gmailMessageChannel.id;
const { messagesAdded, messagesDeleted } = const { messagesAdded, messagesDeleted } =

View File

@ -16,11 +16,17 @@ export class GmailRefreshAccessTokenService {
workspaceId: string, workspaceId: string,
connectedAccountId: string, connectedAccountId: string,
): Promise<void> { ): Promise<void> {
const connectedAccount = await this.connectedAccountService.getByIdOrFail( const connectedAccount = await this.connectedAccountService.getById(
connectedAccountId, connectedAccountId,
workspaceId, workspaceId,
); );
if (!connectedAccount) {
throw new Error(
`No connected account found for ${connectedAccountId} in workspace ${workspaceId}`,
);
}
const refreshToken = connectedAccount.refreshToken; const refreshToken = connectedAccount.refreshToken;
if (!refreshToken) { if (!refreshToken) {

View File

@ -59,12 +59,23 @@ export class SaveMessagesAndCreateContactsService {
} in ${endTime - startTime}ms`, } in ${endTime - startTime}ms`,
); );
const isContactAutoCreationEnabled = const gmailMessageChannel =
await this.messageChannelService.getIsContactAutoCreationEnabledByConnectedAccountIdOrFail( await this.messageChannelService.getFirstByConnectedAccountId(
connectedAccount.id, connectedAccount.id,
workspaceId, workspaceId,
); );
if (!gmailMessageChannel) {
this.logger.error(
`No message channel found for connected account ${connectedAccount.id} in workspace ${workspaceId} in saveMessagesAndCreateContacts`,
);
return;
}
const isContactAutoCreationEnabled =
gmailMessageChannel.isContactAutoCreationEnabled;
const participantsWithMessageId: ParticipantWithMessageId[] = const participantsWithMessageId: ParticipantWithMessageId[] =
messagesToSave.flatMap((message) => { messagesToSave.flatMap((message) => {
const messageId = messageExternalIdsAndIdsMap.get(message.externalId); const messageId = messageExternalIdsAndIdsMap.get(message.externalId);

View File

@ -4,6 +4,7 @@ import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceService } from 'src/metadata/data-source/data-source.service'; import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { MessageThreadService } from 'src/workspace/messaging/repositories/message-thread/message-thread.service'; import { MessageThreadService } from 'src/workspace/messaging/repositories/message-thread/message-thread.service';
import { MessageService } from 'src/workspace/messaging/repositories/message/message.service'; import { MessageService } from 'src/workspace/messaging/repositories/message/message.service';
import { deleteUsingPagination } from 'src/workspace/messaging/services/thread-cleaner/utils/delete-using-pagination.util';
@Injectable() @Injectable()
export class ThreadCleanerService { export class ThreadCleanerService {
@ -15,48 +16,22 @@ export class ThreadCleanerService {
) {} ) {}
public async cleanWorkspaceThreads(workspaceId: string) { public async cleanWorkspaceThreads(workspaceId: string) {
const dataSourceMetadata = await deleteUsingPagination(
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( workspaceId,
workspaceId, 500,
); this.messageService.getNonAssociatedMessageIdsPaginated.bind(
this.messageService,
),
this.messageService.deleteByIds.bind(this.messageService),
);
const workspaceDataSource = await deleteUsingPagination(
await this.typeORMService.connectToDataSource(dataSourceMetadata); workspaceId,
500,
await workspaceDataSource?.transaction(async (transactionManager) => { this.messageThreadService.getOrphanThreadIdsPaginated.bind(
const messagesToDelete = this.messageThreadService,
await this.messageService.getNonAssociatedMessages( ),
workspaceId, this.messageThreadService.deleteByIds.bind(this.messageThreadService),
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,
);
}
});
} }
} }

View File

@ -0,0 +1,44 @@
import { deleteUsingPagination } from './delete-using-pagination.util';
describe('deleteUsingPagination', () => {
it('should delete items using pagination', async () => {
const workspaceId = 'workspace123';
const batchSize = 10;
const getterPaginated = jest
.fn()
.mockResolvedValueOnce(['id1', 'id2'])
.mockResolvedValueOnce([]);
const deleter = jest.fn();
const transactionManager = undefined;
await deleteUsingPagination(
workspaceId,
batchSize,
getterPaginated,
deleter,
transactionManager,
);
expect(getterPaginated).toHaveBeenNthCalledWith(
1,
batchSize,
0,
workspaceId,
transactionManager,
);
expect(getterPaginated).toHaveBeenNthCalledWith(
2,
batchSize,
batchSize,
workspaceId,
transactionManager,
);
expect(deleter).toHaveBeenNthCalledWith(
1,
['id1', 'id2'],
workspaceId,
transactionManager,
);
expect(deleter).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,38 @@
import { EntityManager } from 'typeorm';
export const deleteUsingPagination = async (
workspaceId: string,
batchSize: number,
getterPaginated: (
limit: number,
offset: number,
workspaceId: string,
transactionManager?: EntityManager,
) => Promise<string[]>,
deleter: (
ids: string[],
workspaceId: string,
transactionManager?: EntityManager,
) => Promise<void>,
transactionManager?: EntityManager,
) => {
let offset = 0;
let hasMoreData = true;
while (hasMoreData) {
const idsToDelete = await getterPaginated(
batchSize,
offset,
workspaceId,
transactionManager,
);
if (idsToDelete.length > 0) {
await deleter(idsToDelete, workspaceId, transactionManager);
} else {
hasMoreData = false;
}
offset += batchSize;
}
};