[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:
@ -79,7 +79,6 @@ export class GoogleGmailService {
|
|||||||
connectedAccountId,
|
connectedAccountId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: connectedAccountId,
|
|
||||||
retryLimit: 2,
|
retryLimit: 2,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 } =
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user