Rework messaging modules (#5710)
In this PR, I'm refactoring the messaging module into smaller pieces that have **ONE** responsibility: import messages, clean messages, handle message participant creation, instead of having ~30 modules (1 per service, jobs, cron, ...). This is mandatory to start introducing drivers (gmails, office365, ...) IMO. It is too difficult to enforce common interfaces as we have too many interfaces (30 modules...). All modules should not be exposed Right now, we have services that are almost functions: do-that-and-this.service.ts / do-that-and-this.module.ts I believe we should have something more organized at a high level and it does not matter that much if we have a bit of code duplicates. Note that the proposal is not fully implemented in the current PR that has only focused on messaging folder (biggest part) Here is the high level proposal: - connected-account: token-refresher - blocklist - messaging: message-importer, message-cleaner, message-participants, ... (right now I'm keeping a big messaging-common but this will disappear see below) - calendar: calendar-importer, calendar-cleaner, ... Consequences: 1) It's OK to re-implement several times some things. Example: - error handling in connected-account, messaging, and calendar instead of trying to unify. They are actually different error handling. The only things that might be in common is the GmailError => CommonError parsing and I'm not even sure it makes a lot of sense as these 3 apis might have different format actually - auto-creation. Calendar and Messaging could actually have different rules 2) **We should not have circular dependencies:** - I believe this was the reason why we had so many modules, to be able to cherry pick the one we wanted to avoid circular deps. This is not the right approach IMO, we need architect the whole messaging by defining high level blocks that won't have circular dependencies by design. If we encounter one, we should rethink and break the block in a way that makes sense. - ex: connected-account.resolver is not in the same module as token-refresher. ==> connected-account.resolver => message-importer (as we trigger full sync job when we connect an account) => token-refresher (as we refresh token on message import). connected-account.resolver and token-refresher both in connected-account folder but should be in different modules. Otherwise it's a circular dependency. It does not mean that we should create 1 module per service as it was done before In a nutshell: The code needs to be thought in term of reponsibilities and in a way that enforce high level interfaces (and avoid circular dependencies) Bonus: As you can see, this code is also removing a lot of code because of the removal of many .module.ts (also because I'm removing the sync scripts v2 feature flag end removing old code) Bonus: I have prefixed services name with Messaging to improve dev xp. GmailErrorHandler could be different between MessagingGmailErrorHandler and CalendarGmailErrorHandler for instance
This commit is contained in:
@ -0,0 +1,60 @@
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { AddPersonIdAndWorkspaceMemberIdModule } from 'src/modules/calendar-messaging-participant/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.module';
|
||||
import { MessagingChannelSyncStatusService } from 'src/modules/messaging/common/services/messaging-channel-sync-status.service';
|
||||
import { MessagingErrorHandlingService } from 'src/modules/messaging/common/services/messaging-error-handling.service';
|
||||
import { MessagingFetchByBatchesService } from 'src/modules/messaging/common/services/messaging-fetch-by-batch.service';
|
||||
import { MessagingMessageParticipantService } from 'src/modules/messaging/common/services/messaging-message-participant.service';
|
||||
import { MessagingMessageThreadService } from 'src/modules/messaging/common/services/messaging-message-thread.service';
|
||||
import { MessagingMessageService } from 'src/modules/messaging/common/services/messaging-message.service';
|
||||
import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/common/services/messaging-save-messages-and-enqueue-contact-creation.service';
|
||||
import { MessagingTelemetryService } from 'src/modules/messaging/common/services/messaging-telemetry.service';
|
||||
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||
import { MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity';
|
||||
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
HttpModule.register({
|
||||
baseURL: 'https://www.googleapis.com/batch/gmail/v1',
|
||||
}),
|
||||
AnalyticsModule,
|
||||
WorkspaceDataSourceModule,
|
||||
ObjectMetadataRepositoryModule.forFeature([
|
||||
PersonWorkspaceEntity,
|
||||
MessageParticipantWorkspaceEntity,
|
||||
MessageWorkspaceEntity,
|
||||
MessageThreadWorkspaceEntity,
|
||||
]),
|
||||
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
||||
AddPersonIdAndWorkspaceMemberIdModule,
|
||||
],
|
||||
providers: [
|
||||
MessagingMessageService,
|
||||
MessagingMessageThreadService,
|
||||
MessagingSaveMessagesAndEnqueueContactCreationService,
|
||||
MessagingErrorHandlingService,
|
||||
MessagingTelemetryService,
|
||||
MessagingChannelSyncStatusService,
|
||||
MessagingMessageParticipantService,
|
||||
MessagingFetchByBatchesService,
|
||||
],
|
||||
exports: [
|
||||
MessagingMessageService,
|
||||
MessagingMessageThreadService,
|
||||
MessagingSaveMessagesAndEnqueueContactCreationService,
|
||||
MessagingErrorHandlingService,
|
||||
MessagingTelemetryService,
|
||||
MessagingChannelSyncStatusService,
|
||||
MessagingMessageParticipantService,
|
||||
MessagingFetchByBatchesService,
|
||||
],
|
||||
})
|
||||
export class MessagingCommonModule {}
|
||||
@ -0,0 +1,105 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import groupBy from 'lodash.groupby';
|
||||
|
||||
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
|
||||
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
|
||||
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/common/repositories/message-channel-message-association.repository';
|
||||
import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository';
|
||||
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class MessageFindManyPreQueryHook implements WorkspacePreQueryHook {
|
||||
constructor(
|
||||
@InjectObjectMetadataRepository(
|
||||
MessageChannelMessageAssociationWorkspaceEntity,
|
||||
)
|
||||
private readonly messageChannelMessageAssociationService: MessageChannelMessageAssociationRepository,
|
||||
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
|
||||
private readonly messageChannelService: MessageChannelRepository,
|
||||
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
|
||||
private readonly connectedAccountRepository: ConnectedAccountRepository,
|
||||
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
|
||||
private readonly workspaceMemberRepository: WorkspaceMemberRepository,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
payload: FindManyResolverArgs,
|
||||
): Promise<void> {
|
||||
if (!payload?.filter?.messageThreadId?.eq) {
|
||||
throw new BadRequestException('messageThreadId filter is required');
|
||||
}
|
||||
|
||||
const messageChannelMessageAssociations =
|
||||
await this.messageChannelMessageAssociationService.getByMessageThreadId(
|
||||
payload?.filter?.messageThreadId?.eq,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (messageChannelMessageAssociations.length === 0) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await this.canAccessMessageThread(
|
||||
userId,
|
||||
workspaceId,
|
||||
messageChannelMessageAssociations,
|
||||
);
|
||||
}
|
||||
|
||||
private async canAccessMessageThread(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
messageChannelMessageAssociations: any[],
|
||||
) {
|
||||
const messageChannels = await this.messageChannelService.getByIds(
|
||||
messageChannelMessageAssociations.map(
|
||||
(association) => association.messageChannelId,
|
||||
),
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const messageChannelsGroupByVisibility = groupBy(
|
||||
messageChannels,
|
||||
(channel) => channel.visibility,
|
||||
);
|
||||
|
||||
if (messageChannelsGroupByVisibility.share_everything) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWorkspaceMember =
|
||||
await this.workspaceMemberRepository.getByIdOrFail(userId, workspaceId);
|
||||
|
||||
const messageChannelsConnectedAccounts =
|
||||
await this.connectedAccountRepository.getByIds(
|
||||
messageChannels.map((channel) => channel.connectedAccountId),
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const messageChannelsWorkspaceMemberIds =
|
||||
messageChannelsConnectedAccounts.map(
|
||||
(connectedAccount) => connectedAccount.accountOwnerId,
|
||||
);
|
||||
|
||||
if (messageChannelsWorkspaceMemberIds.includes(currentWorkspaceMember.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Injectable, MethodNotAllowedException } from '@nestjs/common';
|
||||
|
||||
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
|
||||
import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
@Injectable()
|
||||
export class MessageFindOnePreQueryHook implements WorkspacePreQueryHook {
|
||||
async execute(
|
||||
_userId: string,
|
||||
_workspaceId: string,
|
||||
_payload: FindOneResolverArgs,
|
||||
): Promise<void> {
|
||||
throw new MethodNotAllowedException('Method not allowed.');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { MessageFindManyPreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-many.pre-query.hook';
|
||||
import { MessageFindOnePreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-one.pre-query-hook';
|
||||
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ObjectMetadataRepositoryModule.forFeature([
|
||||
MessageChannelMessageAssociationWorkspaceEntity,
|
||||
MessageChannelWorkspaceEntity,
|
||||
ConnectedAccountWorkspaceEntity,
|
||||
WorkspaceMemberWorkspaceEntity,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: MessageFindOnePreQueryHook.name,
|
||||
useClass: MessageFindOnePreQueryHook,
|
||||
},
|
||||
{
|
||||
provide: MessageFindManyPreQueryHook.name,
|
||||
useClass: MessageFindManyPreQueryHook,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class MessagingQueryHookModule {}
|
||||
@ -0,0 +1,266 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
|
||||
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class MessageChannelMessageAssociationRepository {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
) {}
|
||||
|
||||
public async getByMessageExternalIdsAndMessageChannelId(
|
||||
messageExternalIds: string[],
|
||||
messageChannelId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<MessageChannelMessageAssociationWorkspaceEntity>[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."messageChannelMessageAssociation"
|
||||
WHERE "messageExternalId" = ANY($1) AND "messageChannelId" = $2`,
|
||||
[messageExternalIds, messageChannelId],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async countByMessageExternalIdsAndMessageChannelId(
|
||||
messageExternalIds: string[],
|
||||
messageChannelId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<number> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const result = await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT COUNT(*) FROM ${dataSourceSchema}."messageChannelMessageAssociation"
|
||||
WHERE "messageExternalId" = ANY($1) AND "messageChannelId" = $2`,
|
||||
[messageExternalIds, messageChannelId],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return result[0]?.count;
|
||||
}
|
||||
|
||||
public async deleteByMessageExternalIdsAndMessageChannelId(
|
||||
messageExternalIds: string[],
|
||||
messageChannelId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
) {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`DELETE FROM ${dataSourceSchema}."messageChannelMessageAssociation" WHERE "messageExternalId" = ANY($1) AND "messageChannelId" = $2`,
|
||||
[messageExternalIds, messageChannelId],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async deleteByMessageParticipantHandleAndMessageChannelIdsAndRoles(
|
||||
messageParticipantHandle: string,
|
||||
messageChannelIds: string[],
|
||||
rolesToDelete: ('from' | 'to' | 'cc' | 'bcc')[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
) {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const isHandleDomain = messageParticipantHandle.startsWith('@');
|
||||
|
||||
const messageChannelMessageAssociationIdsToDelete =
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT "messageChannelMessageAssociation".id
|
||||
FROM ${dataSourceSchema}."messageChannelMessageAssociation" "messageChannelMessageAssociation"
|
||||
JOIN ${dataSourceSchema}."message" ON "messageChannelMessageAssociation"."messageId" = ${dataSourceSchema}."message"."id"
|
||||
JOIN ${dataSourceSchema}."messageParticipant" "messageParticipant" ON ${dataSourceSchema}."message"."id" = "messageParticipant"."messageId"
|
||||
WHERE "messageParticipant"."handle" ${
|
||||
isHandleDomain ? 'ILIKE' : '='
|
||||
} $1 AND "messageParticipant"."role" = ANY($2) AND "messageChannelMessageAssociation"."messageChannelId" = ANY($3)`,
|
||||
[
|
||||
isHandleDomain
|
||||
? `%${messageParticipantHandle}`
|
||||
: messageParticipantHandle,
|
||||
rolesToDelete,
|
||||
messageChannelIds,
|
||||
],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const messageChannelMessageAssociationIdsToDeleteArray =
|
||||
messageChannelMessageAssociationIdsToDelete.map(
|
||||
(messageChannelMessageAssociation: { id: string }) =>
|
||||
messageChannelMessageAssociation.id,
|
||||
);
|
||||
|
||||
await this.deleteByIds(
|
||||
messageChannelMessageAssociationIdsToDeleteArray,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async getByMessageChannelIds(
|
||||
messageChannelIds: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<MessageChannelMessageAssociationWorkspaceEntity>[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."messageChannelMessageAssociation"
|
||||
WHERE "messageChannelId" = ANY($1)`,
|
||||
[messageChannelIds],
|
||||
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" = ANY($1)`,
|
||||
[messageChannelIds],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async deleteByIds(
|
||||
ids: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
) {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`DELETE FROM ${dataSourceSchema}."messageChannelMessageAssociation" WHERE "id" = ANY($1)`,
|
||||
[ids],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async getByMessageThreadExternalIds(
|
||||
messageThreadExternalIds: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<MessageChannelMessageAssociationWorkspaceEntity>[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."messageChannelMessageAssociation"
|
||||
WHERE "messageThreadExternalId" = ANY($1)`,
|
||||
[messageThreadExternalIds],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async getFirstByMessageThreadExternalId(
|
||||
messageThreadExternalId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<MessageChannelMessageAssociationWorkspaceEntity> | null> {
|
||||
const existingMessageChannelMessageAssociations =
|
||||
await this.getByMessageThreadExternalIds(
|
||||
[messageThreadExternalId],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
if (
|
||||
!existingMessageChannelMessageAssociations ||
|
||||
existingMessageChannelMessageAssociations.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return existingMessageChannelMessageAssociations[0];
|
||||
}
|
||||
|
||||
public async getByMessageIds(
|
||||
messageIds: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<MessageChannelMessageAssociationWorkspaceEntity>[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."messageChannelMessageAssociation"
|
||||
WHERE "messageId" = ANY($1)`,
|
||||
[messageIds],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async getByMessageThreadId(
|
||||
messageThreadId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<MessageChannelMessageAssociationWorkspaceEntity>[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."messageChannelMessageAssociation"
|
||||
WHERE "messageThreadId" = $1`,
|
||||
[messageThreadId],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async insert(
|
||||
messageChannelId: string,
|
||||
messageId: string,
|
||||
messageExternalId: string,
|
||||
messageThreadId: string,
|
||||
messageThreadExternalId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<void> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`INSERT INTO ${dataSourceSchema}."messageChannelMessageAssociation" ("messageChannelId", "messageId", "messageExternalId", "messageThreadId", "messageThreadExternalId") VALUES ($1, $2, $3, $4, $5)`,
|
||||
[
|
||||
messageChannelId,
|
||||
messageId,
|
||||
messageExternalId,
|
||||
messageThreadId,
|
||||
messageThreadExternalId,
|
||||
],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,243 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
|
||||
import {
|
||||
MessageChannelWorkspaceEntity,
|
||||
MessageChannelSyncStatus,
|
||||
MessageChannelSyncSubStatus,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class MessageChannelRepository {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
) {}
|
||||
|
||||
public async create(
|
||||
messageChannel: Pick<
|
||||
ObjectRecord<MessageChannelWorkspaceEntity>,
|
||||
'id' | 'connectedAccountId' | 'type' | 'handle' | 'visibility'
|
||||
>,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<void> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`INSERT INTO ${dataSourceSchema}."messageChannel" ("id", "connectedAccountId", "type", "handle", "visibility")
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[
|
||||
messageChannel.id,
|
||||
messageChannel.connectedAccountId,
|
||||
messageChannel.type,
|
||||
messageChannel.handle,
|
||||
messageChannel.visibility,
|
||||
],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async resetSync(
|
||||
connectedAccountId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<void> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`UPDATE ${dataSourceSchema}."messageChannel" SET "syncStatus" = NULL, "syncCursor" = '', "ongoingSyncStartedAt" = NULL
|
||||
WHERE "connectedAccountId" = $1`,
|
||||
[connectedAccountId],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async getAll(
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<MessageChannelWorkspaceEntity>[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."messageChannel"`,
|
||||
[],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async getByConnectedAccountId(
|
||||
connectedAccountId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<MessageChannelWorkspaceEntity>[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."messageChannel" WHERE "connectedAccountId" = $1 AND "type" = 'email' LIMIT 1`,
|
||||
[connectedAccountId],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async getFirstByConnectedAccountIdOrFail(
|
||||
connectedAccountId: string,
|
||||
workspaceId: string,
|
||||
): Promise<ObjectRecord<MessageChannelWorkspaceEntity>> {
|
||||
const messageChannel = await this.getFirstByConnectedAccountId(
|
||||
connectedAccountId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!messageChannel) {
|
||||
throw new Error(
|
||||
`Message channel for connected account ${connectedAccountId} not found in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
return messageChannel;
|
||||
}
|
||||
|
||||
public async getFirstByConnectedAccountId(
|
||||
connectedAccountId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<MessageChannelWorkspaceEntity> | undefined> {
|
||||
const messageChannels = await this.getByConnectedAccountId(
|
||||
connectedAccountId,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return messageChannels[0];
|
||||
}
|
||||
|
||||
public async getByIds(
|
||||
ids: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<MessageChannelWorkspaceEntity>[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."messageChannel" WHERE "id" = ANY($1)`,
|
||||
[ids],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async getIdsByWorkspaceMemberId(
|
||||
workspaceMemberId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<MessageChannelWorkspaceEntity>[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const messageChannelIds =
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT "messageChannel".id FROM ${dataSourceSchema}."messageChannel" "messageChannel"
|
||||
JOIN ${dataSourceSchema}."connectedAccount" ON "messageChannel"."connectedAccountId" = ${dataSourceSchema}."connectedAccount"."id"
|
||||
WHERE ${dataSourceSchema}."connectedAccount"."accountOwnerId" = $1`,
|
||||
[workspaceMemberId],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return messageChannelIds;
|
||||
}
|
||||
|
||||
public async updateSyncStatus(
|
||||
id: string,
|
||||
syncStatus: MessageChannelSyncStatus,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<void> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const needsToUpdateSyncedAt =
|
||||
syncStatus === MessageChannelSyncStatus.SUCCEEDED;
|
||||
|
||||
const needsToUpdateOngoingSyncStartedAt =
|
||||
syncStatus === MessageChannelSyncStatus.ONGOING;
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`UPDATE ${dataSourceSchema}."messageChannel" SET "syncStatus" = $1 ${
|
||||
needsToUpdateSyncedAt ? `, "syncedAt" = NOW()` : ''
|
||||
} ${
|
||||
needsToUpdateOngoingSyncStartedAt
|
||||
? `, "ongoingSyncStartedAt" = NOW()`
|
||||
: `, "ongoingSyncStartedAt" = NULL`
|
||||
} WHERE "id" = $2`,
|
||||
[syncStatus, id],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async updateSyncSubStatus(
|
||||
id: string,
|
||||
syncSubStatus: MessageChannelSyncSubStatus,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<void> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`UPDATE ${dataSourceSchema}."messageChannel" SET "syncSubStatus" = $1 WHERE "id" = $2`,
|
||||
[syncSubStatus, id],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async updateLastSyncCursorIfHigher(
|
||||
id: string,
|
||||
syncCursor: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
) {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`UPDATE ${dataSourceSchema}."messageChannel" SET "syncCursor" = $1
|
||||
WHERE "id" = $2
|
||||
AND ("syncCursor" < $1 OR "syncCursor" = '')`,
|
||||
[syncCursor, id],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async resetSyncCursor(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
) {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`UPDATE ${dataSourceSchema}."messageChannel" SET "syncCursor" = ''
|
||||
WHERE "id" = $1`,
|
||||
[id],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,193 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
|
||||
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||
import { ParticipantWithId } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message';
|
||||
|
||||
@Injectable()
|
||||
export class MessageParticipantRepository {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
) {}
|
||||
|
||||
public async getByHandles(
|
||||
handles: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<MessageParticipantWorkspaceEntity>[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."messageParticipant" WHERE "handle" = ANY($1)`,
|
||||
[handles],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async updateParticipantsPersonId(
|
||||
participantIds: string[],
|
||||
personId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
) {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`UPDATE ${dataSourceSchema}."messageParticipant" SET "personId" = $1 WHERE "id" = ANY($2)`,
|
||||
[personId, participantIds],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async updateParticipantsWorkspaceMemberId(
|
||||
participantIds: string[],
|
||||
workspaceMemberId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
) {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`UPDATE ${dataSourceSchema}."messageParticipant" SET "workspaceMemberId" = $1 WHERE "id" = ANY($2)`,
|
||||
[workspaceMemberId, participantIds],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async removePersonIdByHandle(
|
||||
handle: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
) {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`UPDATE ${dataSourceSchema}."messageParticipant" SET "personId" = NULL WHERE "handle" = $1`,
|
||||
[handle],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async removeWorkspaceMemberIdByHandle(
|
||||
handle: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
) {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`UPDATE ${dataSourceSchema}."messageParticipant" SET "workspaceMemberId" = NULL WHERE "handle" = $1`,
|
||||
[handle],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async getByMessageChannelIdWithoutPersonIdAndWorkspaceMemberIdAndMessageOutgoing(
|
||||
messageChannelId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ParticipantWithId[]> {
|
||||
if (!messageChannelId || !workspaceId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const messageParticipants: ParticipantWithId[] =
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT "messageParticipant".id,
|
||||
"messageParticipant"."role",
|
||||
"messageParticipant"."handle",
|
||||
"messageParticipant"."displayName",
|
||||
"messageParticipant"."personId",
|
||||
"messageParticipant"."workspaceMemberId",
|
||||
"messageParticipant"."messageId"
|
||||
FROM ${dataSourceSchema}."messageParticipant" "messageParticipant"
|
||||
LEFT JOIN ${dataSourceSchema}."message" ON "messageParticipant"."messageId" = ${dataSourceSchema}."message"."id"
|
||||
LEFT JOIN ${dataSourceSchema}."messageChannelMessageAssociation" ON ${dataSourceSchema}."messageChannelMessageAssociation"."messageId" = ${dataSourceSchema}."message"."id"
|
||||
WHERE ${dataSourceSchema}."messageChannelMessageAssociation"."messageChannelId" = $1
|
||||
AND "messageParticipant"."personId" IS NULL
|
||||
AND "messageParticipant"."workspaceMemberId" IS NULL
|
||||
AND ${dataSourceSchema}."message"."direction" = 'outgoing'`,
|
||||
[messageChannelId],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return messageParticipants;
|
||||
}
|
||||
|
||||
public async getByMessageChannelIdWithoutPersonIdAndWorkspaceMemberId(
|
||||
messageChannelId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ParticipantWithId[]> {
|
||||
if (!messageChannelId || !workspaceId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const messageParticipants: ParticipantWithId[] =
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT "messageParticipant".id,
|
||||
"messageParticipant"."role",
|
||||
"messageParticipant"."handle",
|
||||
"messageParticipant"."displayName",
|
||||
"messageParticipant"."personId",
|
||||
"messageParticipant"."workspaceMemberId",
|
||||
"messageParticipant"."messageId"
|
||||
FROM ${dataSourceSchema}."messageParticipant" "messageParticipant"
|
||||
LEFT JOIN ${dataSourceSchema}."message" ON "messageParticipant"."messageId" = ${dataSourceSchema}."message"."id"
|
||||
LEFT JOIN ${dataSourceSchema}."messageChannelMessageAssociation" ON ${dataSourceSchema}."messageChannelMessageAssociation"."messageId" = ${dataSourceSchema}."message"."id"
|
||||
WHERE ${dataSourceSchema}."messageChannelMessageAssociation"."messageChannelId" = $1
|
||||
AND "messageParticipant"."personId" IS NULL
|
||||
AND "messageParticipant"."workspaceMemberId" IS NULL`,
|
||||
[messageChannelId],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return messageParticipants;
|
||||
}
|
||||
|
||||
public async getWithoutPersonIdAndWorkspaceMemberId(
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ParticipantWithId[]> {
|
||||
if (!workspaceId) {
|
||||
throw new Error('WorkspaceId is required');
|
||||
}
|
||||
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const messageParticipants: ParticipantWithId[] =
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT "messageParticipant".*
|
||||
FROM ${dataSourceSchema}."messageParticipant" "messageParticipant"
|
||||
WHERE "messageParticipant"."personId" IS NULL
|
||||
AND "messageParticipant"."workspaceMemberId" IS NULL`,
|
||||
[],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return messageParticipants;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
|
||||
@Injectable()
|
||||
export class MessageThreadRepository {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
) {}
|
||||
|
||||
public async getOrphanThreadIdsPaginated(
|
||||
limit: number,
|
||||
offset: number,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<string[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const orphanThreads = await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT mt.id
|
||||
FROM ${dataSourceSchema}."messageThread" mt
|
||||
LEFT JOIN ${dataSourceSchema}."message" m ON mt.id = m."messageThreadId"
|
||||
WHERE m."messageThreadId" IS NULL
|
||||
LIMIT $1 OFFSET $2`,
|
||||
[limit, offset],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return orphanThreads.map(({ id }) => id);
|
||||
}
|
||||
|
||||
public async deleteByIds(
|
||||
messageThreadIds: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<void> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`DELETE FROM ${dataSourceSchema}."messageThread" WHERE id = ANY($1)`,
|
||||
[messageThreadIds],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async insert(
|
||||
messageThreadId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<void> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`INSERT INTO ${dataSourceSchema}."messageThread" (id) VALUES ($1)`,
|
||||
[messageThreadId],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,138 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
|
||||
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class MessageRepository {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
) {}
|
||||
|
||||
public async getNonAssociatedMessageIdsPaginated(
|
||||
limit: number,
|
||||
offset: number,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<string[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const nonAssociatedMessages =
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT m.id FROM ${dataSourceSchema}."message" m
|
||||
LEFT JOIN ${dataSourceSchema}."messageChannelMessageAssociation" mcma
|
||||
ON m.id = mcma."messageId"
|
||||
WHERE mcma.id IS NULL
|
||||
LIMIT $1 OFFSET $2`,
|
||||
[limit, offset],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return nonAssociatedMessages.map(({ id }) => id);
|
||||
}
|
||||
|
||||
public async getFirstOrNullByHeaderMessageId(
|
||||
headerMessageId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<MessageWorkspaceEntity> | null> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const messages = await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."message" WHERE "headerMessageId" = $1 LIMIT 1`,
|
||||
[headerMessageId],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
if (!messages || messages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return messages[0];
|
||||
}
|
||||
|
||||
public async getByIds(
|
||||
messageIds: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<MessageWorkspaceEntity>[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."message" WHERE "id" = ANY($1)`,
|
||||
[messageIds],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async deleteByIds(
|
||||
messageIds: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<void> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`DELETE FROM ${dataSourceSchema}."message" WHERE "id" = ANY($1)`,
|
||||
[messageIds],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async getByMessageThreadIds(
|
||||
messageThreadIds: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<MessageWorkspaceEntity>[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."message" WHERE "messageThreadId" = ANY($1)`,
|
||||
[messageThreadIds],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async insert(
|
||||
id: string,
|
||||
headerMessageId: string,
|
||||
subject: string,
|
||||
receivedAt: Date,
|
||||
messageDirection: string,
|
||||
messageThreadId: string,
|
||||
text: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<void> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`INSERT INTO ${dataSourceSchema}."message" ("id", "headerMessageId", "subject", "receivedAt", "direction", "messageThreadId", "text") VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[
|
||||
id,
|
||||
headerMessageId,
|
||||
subject,
|
||||
receivedAt,
|
||||
messageDirection,
|
||||
messageThreadId,
|
||||
text,
|
||||
],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,156 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { CacheStorageService } from 'src/engine/integrations/cache-storage/cache-storage.service';
|
||||
import { InjectCacheStorage } from 'src/engine/integrations/cache-storage/decorators/cache-storage.decorator';
|
||||
import { CacheStorageNamespace } from 'src/engine/integrations/cache-storage/types/cache-storage-namespace.enum';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository';
|
||||
import {
|
||||
MessageChannelWorkspaceEntity,
|
||||
MessageChannelSyncSubStatus,
|
||||
MessageChannelSyncStatus,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class MessagingChannelSyncStatusService {
|
||||
constructor(
|
||||
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
|
||||
private readonly messageChannelRepository: MessageChannelRepository,
|
||||
@InjectCacheStorage(CacheStorageNamespace.Messaging)
|
||||
private readonly cacheStorage: CacheStorageService,
|
||||
) {}
|
||||
|
||||
public async scheduleFullMessageListFetch(
|
||||
messageChannelId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
await this.messageChannelRepository.updateSyncSubStatus(
|
||||
messageChannelId,
|
||||
MessageChannelSyncSubStatus.FULL_MESSAGE_LIST_FETCH_PENDING,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
public async schedulePartialMessageListFetch(
|
||||
messageChannelId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
await this.messageChannelRepository.updateSyncSubStatus(
|
||||
messageChannelId,
|
||||
MessageChannelSyncSubStatus.PARTIAL_MESSAGE_LIST_FETCH_PENDING,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
public async scheduleMessagesImport(
|
||||
messageChannelId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
await this.messageChannelRepository.updateSyncSubStatus(
|
||||
messageChannelId,
|
||||
MessageChannelSyncSubStatus.MESSAGES_IMPORT_PENDING,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
public async resetAndScheduleFullMessageListFetch(
|
||||
messageChannelId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
await this.cacheStorage.setPop(
|
||||
`messages-to-import:${workspaceId}:gmail:${messageChannelId}`,
|
||||
);
|
||||
|
||||
// TODO: remove nextPageToken from cache
|
||||
|
||||
await this.messageChannelRepository.resetSyncCursor(
|
||||
messageChannelId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.scheduleFullMessageListFetch(messageChannelId, workspaceId);
|
||||
}
|
||||
|
||||
public async markAsMessagesListFetchOngoing(
|
||||
messageChannelId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
await this.messageChannelRepository.updateSyncSubStatus(
|
||||
messageChannelId,
|
||||
MessageChannelSyncSubStatus.MESSAGE_LIST_FETCH_ONGOING,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.messageChannelRepository.updateSyncStatus(
|
||||
messageChannelId,
|
||||
MessageChannelSyncStatus.ONGOING,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
public async markAsCompletedAndSchedulePartialMessageListFetch(
|
||||
messageChannelId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
await this.messageChannelRepository.updateSyncStatus(
|
||||
messageChannelId,
|
||||
MessageChannelSyncStatus.COMPLETED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.schedulePartialMessageListFetch(messageChannelId, workspaceId);
|
||||
}
|
||||
|
||||
public async markAsMessagesImportOngoing(
|
||||
messageChannelId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
await this.messageChannelRepository.updateSyncSubStatus(
|
||||
messageChannelId,
|
||||
MessageChannelSyncSubStatus.MESSAGES_IMPORT_ONGOING,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
public async markAsFailedUnknownAndFlushMessagesToImport(
|
||||
messageChannelId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
await this.cacheStorage.setPop(
|
||||
`messages-to-import:${workspaceId}:gmail:${messageChannelId}`,
|
||||
);
|
||||
|
||||
await this.messageChannelRepository.updateSyncSubStatus(
|
||||
messageChannelId,
|
||||
MessageChannelSyncSubStatus.FAILED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.messageChannelRepository.updateSyncStatus(
|
||||
messageChannelId,
|
||||
MessageChannelSyncStatus.FAILED_UNKNOWN,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
public async markAsFailedInsufficientPermissionsAndFlushMessagesToImport(
|
||||
messageChannelId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
await this.cacheStorage.setPop(
|
||||
`messages-to-import:${workspaceId}:gmail:${messageChannelId}`,
|
||||
);
|
||||
|
||||
await this.messageChannelRepository.updateSyncSubStatus(
|
||||
messageChannelId,
|
||||
MessageChannelSyncSubStatus.FAILED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.messageChannelRepository.updateSyncStatus(
|
||||
messageChannelId,
|
||||
MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,192 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import snakeCase from 'lodash.snakecase';
|
||||
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
|
||||
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { MessagingTelemetryService } from 'src/modules/messaging/common/services/messaging-telemetry.service';
|
||||
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { MessagingChannelSyncStatusService } from 'src/modules/messaging/common/services/messaging-channel-sync-status.service';
|
||||
|
||||
type SyncStep =
|
||||
| 'partial-message-list-fetch'
|
||||
| 'full-message-list-fetch'
|
||||
| 'messages-import';
|
||||
|
||||
export type GmailError = {
|
||||
code: number;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class MessagingErrorHandlingService {
|
||||
constructor(
|
||||
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
|
||||
private readonly connectedAccountRepository: ConnectedAccountRepository,
|
||||
private readonly messagingChannelSyncStatusService: MessagingChannelSyncStatusService,
|
||||
private readonly messagingTelemetryService: MessagingTelemetryService,
|
||||
) {}
|
||||
|
||||
public async handleGmailError(
|
||||
error: GmailError,
|
||||
syncStep: SyncStep,
|
||||
messageChannel: ObjectRecord<MessageChannelWorkspaceEntity>,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
const { code, reason } = error;
|
||||
|
||||
switch (code) {
|
||||
case 400:
|
||||
if (reason === 'invalid_grant') {
|
||||
await this.handleInsufficientPermissions(
|
||||
error,
|
||||
syncStep,
|
||||
messageChannel,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 404:
|
||||
await this.handleNotFound(error, syncStep, messageChannel, workspaceId);
|
||||
break;
|
||||
|
||||
case 429:
|
||||
await this.handleRateLimitExceeded(
|
||||
error,
|
||||
syncStep,
|
||||
messageChannel,
|
||||
workspaceId,
|
||||
);
|
||||
break;
|
||||
|
||||
case 403:
|
||||
if (
|
||||
reason === 'rateLimitExceeded' ||
|
||||
reason === 'userRateLimitExceeded'
|
||||
) {
|
||||
await this.handleRateLimitExceeded(
|
||||
error,
|
||||
syncStep,
|
||||
messageChannel,
|
||||
workspaceId,
|
||||
);
|
||||
} else {
|
||||
await this.handleInsufficientPermissions(
|
||||
error,
|
||||
syncStep,
|
||||
messageChannel,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 401:
|
||||
await this.handleInsufficientPermissions(
|
||||
error,
|
||||
syncStep,
|
||||
messageChannel,
|
||||
workspaceId,
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
await this.messagingChannelSyncStatusService.markAsFailedUnknownAndFlushMessagesToImport(
|
||||
messageChannel.id,
|
||||
workspaceId,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async handleRateLimitExceeded(
|
||||
error: GmailError,
|
||||
syncStep: SyncStep,
|
||||
messageChannel: ObjectRecord<MessageChannelWorkspaceEntity>,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
await this.messagingTelemetryService.track({
|
||||
eventName: `${snakeCase(syncStep)}.error.rate_limit_exceeded`,
|
||||
workspaceId,
|
||||
connectedAccountId: messageChannel.connectedAccountId,
|
||||
messageChannelId: messageChannel.id,
|
||||
message: `${error.code}: ${error.reason}`,
|
||||
});
|
||||
|
||||
switch (syncStep) {
|
||||
case 'full-message-list-fetch':
|
||||
await this.messagingChannelSyncStatusService.scheduleFullMessageListFetch(
|
||||
messageChannel.id,
|
||||
workspaceId,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'partial-message-list-fetch':
|
||||
await this.messagingChannelSyncStatusService.schedulePartialMessageListFetch(
|
||||
messageChannel.id,
|
||||
workspaceId,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'messages-import':
|
||||
await this.messagingChannelSyncStatusService.scheduleMessagesImport(
|
||||
messageChannel.id,
|
||||
workspaceId,
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async handleInsufficientPermissions(
|
||||
error: GmailError,
|
||||
syncStep: SyncStep,
|
||||
messageChannel: ObjectRecord<MessageChannelWorkspaceEntity>,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
await this.messagingTelemetryService.track({
|
||||
eventName: `${snakeCase(syncStep)}.error.insufficient_permissions`,
|
||||
workspaceId,
|
||||
connectedAccountId: messageChannel.connectedAccountId,
|
||||
messageChannelId: messageChannel.id,
|
||||
message: `${error.code}: ${error.reason}`,
|
||||
});
|
||||
|
||||
await this.messagingChannelSyncStatusService.markAsFailedInsufficientPermissionsAndFlushMessagesToImport(
|
||||
messageChannel.id,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.connectedAccountRepository.updateAuthFailedAt(
|
||||
messageChannel.connectedAccountId,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
public async handleNotFound(
|
||||
error: GmailError,
|
||||
syncStep: SyncStep,
|
||||
messageChannel: ObjectRecord<MessageChannelWorkspaceEntity>,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
if (syncStep === 'messages-import') {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.messagingTelemetryService.track({
|
||||
eventName: `${snakeCase(syncStep)}.error.not_found`,
|
||||
workspaceId,
|
||||
connectedAccountId: messageChannel.connectedAccountId,
|
||||
messageChannelId: messageChannel.id,
|
||||
message: `404: ${error.reason}`,
|
||||
});
|
||||
|
||||
await this.messagingChannelSyncStatusService.resetAndScheduleFullMessageListFetch(
|
||||
messageChannel.id,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,128 @@
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
import { GmailMessageParsedResponse } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message-parsed-response';
|
||||
import { BatchQueries } from 'src/modules/messaging/message-import-manager/types/batch-queries';
|
||||
|
||||
@Injectable()
|
||||
export class MessagingFetchByBatchesService {
|
||||
constructor(private readonly httpService: HttpService) {}
|
||||
|
||||
async fetchAllByBatches(
|
||||
queries: BatchQueries,
|
||||
accessToken: string,
|
||||
boundary: string,
|
||||
): Promise<AxiosResponse<any, any>[]> {
|
||||
const batchLimit = 50;
|
||||
|
||||
let batchOffset = 0;
|
||||
|
||||
let batchResponses: AxiosResponse<any, any>[] = [];
|
||||
|
||||
while (batchOffset < queries.length) {
|
||||
const batchResponse = await this.fetchBatch(
|
||||
queries,
|
||||
accessToken,
|
||||
batchOffset,
|
||||
batchLimit,
|
||||
boundary,
|
||||
);
|
||||
|
||||
batchResponses = batchResponses.concat(batchResponse);
|
||||
|
||||
batchOffset += batchLimit;
|
||||
}
|
||||
|
||||
return batchResponses;
|
||||
}
|
||||
|
||||
async fetchBatch(
|
||||
queries: BatchQueries,
|
||||
accessToken: string,
|
||||
batchOffset: number,
|
||||
batchLimit: number,
|
||||
boundary: string,
|
||||
): Promise<AxiosResponse<any, any>> {
|
||||
const limitedQueries = queries.slice(batchOffset, batchOffset + batchLimit);
|
||||
|
||||
const response = await this.httpService.axiosRef.post(
|
||||
'/',
|
||||
this.createBatchBody(limitedQueries, boundary),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/mixed; boundary=' + boundary,
|
||||
Authorization: 'Bearer ' + accessToken,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
createBatchBody(queries: BatchQueries, boundary: string): string {
|
||||
let batchBody: string[] = [];
|
||||
|
||||
queries.forEach(function (call) {
|
||||
const method = 'GET';
|
||||
const uri = call.uri;
|
||||
|
||||
batchBody = batchBody.concat([
|
||||
'--',
|
||||
boundary,
|
||||
'\r\n',
|
||||
'Content-Type: application/http',
|
||||
'\r\n\r\n',
|
||||
|
||||
method,
|
||||
' ',
|
||||
uri,
|
||||
'\r\n\r\n',
|
||||
]);
|
||||
});
|
||||
|
||||
return batchBody.concat(['--', boundary, '--']).join('');
|
||||
}
|
||||
|
||||
parseBatch(
|
||||
responseCollection: AxiosResponse<any, any>,
|
||||
): GmailMessageParsedResponse[] {
|
||||
const responseItems: GmailMessageParsedResponse[] = [];
|
||||
|
||||
const boundary = this.getBatchSeparator(responseCollection);
|
||||
|
||||
const responseLines: string[] = responseCollection.data.split(
|
||||
'--' + boundary,
|
||||
);
|
||||
|
||||
responseLines.forEach(function (response) {
|
||||
const startJson = response.indexOf('{');
|
||||
const endJson = response.lastIndexOf('}');
|
||||
|
||||
if (startJson < 0 || endJson < 0) return;
|
||||
|
||||
const responseJson = response.substring(startJson, endJson + 1);
|
||||
|
||||
const item = JSON.parse(responseJson);
|
||||
|
||||
responseItems.push(item);
|
||||
});
|
||||
|
||||
return responseItems;
|
||||
}
|
||||
|
||||
getBatchSeparator(responseCollection: AxiosResponse<any, any>): string {
|
||||
const headers = responseCollection.headers;
|
||||
|
||||
const contentType: string = headers['content-type'];
|
||||
|
||||
if (!contentType) return '';
|
||||
|
||||
const components = contentType.split('; ');
|
||||
|
||||
const boundary = components.find((item) => item.startsWith('boundary='));
|
||||
|
||||
return boundary?.replace('boundary=', '').trim() || '';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,172 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { PersonRepository } from 'src/modules/person/repositories/person.repository';
|
||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { getFlattenedValuesAndValuesStringForBatchRawQuery } from 'src/modules/calendar/utils/get-flattened-values-and-values-string-for-batch-raw-query.util';
|
||||
import { AddPersonIdAndWorkspaceMemberIdService } from 'src/modules/calendar-messaging-participant/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.service';
|
||||
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
|
||||
import { MessageParticipantRepository } from 'src/modules/messaging/common/repositories/message-participant.repository';
|
||||
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||
import { ParticipantWithMessageId } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message';
|
||||
|
||||
// Todo: this is not the right place for this file. The code needs to be refactored in term of business modules with a precise scope.
|
||||
// Putting it here to avoid circular dependencies for now.
|
||||
@Injectable()
|
||||
export class MessagingMessageParticipantService {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
@InjectObjectMetadataRepository(MessageParticipantWorkspaceEntity)
|
||||
private readonly messageParticipantRepository: MessageParticipantRepository,
|
||||
@InjectObjectMetadataRepository(PersonWorkspaceEntity)
|
||||
private readonly personRepository: PersonRepository,
|
||||
private readonly addPersonIdAndWorkspaceMemberIdService: AddPersonIdAndWorkspaceMemberIdService,
|
||||
) {}
|
||||
|
||||
public async updateMessageParticipantsAfterPeopleCreation(
|
||||
createdPeople: ObjectRecord<PersonWorkspaceEntity>[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<void> {
|
||||
const participants = await this.messageParticipantRepository.getByHandles(
|
||||
createdPeople.map((person) => person.email),
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
if (!participants) return;
|
||||
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const handles = participants.map((participant) => participant.handle);
|
||||
|
||||
const participantPersonIds = await this.personRepository.getByEmails(
|
||||
handles,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const messageParticipantsToUpdate = participants.map((participant) => ({
|
||||
id: participant.id,
|
||||
personId: participantPersonIds.find(
|
||||
(e: { id: string; email: string }) => e.email === participant.handle,
|
||||
)?.id,
|
||||
}));
|
||||
|
||||
if (messageParticipantsToUpdate.length === 0) return;
|
||||
|
||||
const { flattenedValues, valuesString } =
|
||||
getFlattenedValuesAndValuesStringForBatchRawQuery(
|
||||
messageParticipantsToUpdate,
|
||||
{
|
||||
id: 'uuid',
|
||||
personId: 'uuid',
|
||||
},
|
||||
);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`UPDATE ${dataSourceSchema}."messageParticipant" AS "messageParticipant" SET "personId" = "data"."personId"
|
||||
FROM (VALUES ${valuesString}) AS "data"("id", "personId")
|
||||
WHERE "messageParticipant"."id" = "data"."id"`,
|
||||
flattenedValues,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async saveMessageParticipants(
|
||||
participants: ParticipantWithMessageId[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<void> {
|
||||
if (!participants) return;
|
||||
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const messageParticipantsToSave =
|
||||
await this.addPersonIdAndWorkspaceMemberIdService.addPersonIdAndWorkspaceMemberId(
|
||||
participants,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const { flattenedValues, valuesString } =
|
||||
getFlattenedValuesAndValuesStringForBatchRawQuery(
|
||||
messageParticipantsToSave,
|
||||
{
|
||||
messageId: 'uuid',
|
||||
role: `${dataSourceSchema}."messageParticipant_role_enum"`,
|
||||
handle: 'text',
|
||||
displayName: 'text',
|
||||
personId: 'uuid',
|
||||
workspaceMemberId: 'uuid',
|
||||
},
|
||||
);
|
||||
|
||||
if (messageParticipantsToSave.length === 0) return;
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`INSERT INTO ${dataSourceSchema}."messageParticipant" ("messageId", "role", "handle", "displayName", "personId", "workspaceMemberId") VALUES ${valuesString}`,
|
||||
flattenedValues,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async matchMessageParticipants(
|
||||
workspaceId: string,
|
||||
email: string,
|
||||
personId?: string,
|
||||
workspaceMemberId?: string,
|
||||
) {
|
||||
const messageParticipantsToUpdate =
|
||||
await this.messageParticipantRepository.getByHandles(
|
||||
[email],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const messageParticipantIdsToUpdate = messageParticipantsToUpdate.map(
|
||||
(participant) => participant.id,
|
||||
);
|
||||
|
||||
if (personId) {
|
||||
await this.messageParticipantRepository.updateParticipantsPersonId(
|
||||
messageParticipantIdsToUpdate,
|
||||
personId,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
if (workspaceMemberId) {
|
||||
await this.messageParticipantRepository.updateParticipantsWorkspaceMemberId(
|
||||
messageParticipantIdsToUpdate,
|
||||
workspaceMemberId,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async unmatchMessageParticipants(
|
||||
workspaceId: string,
|
||||
handle: string,
|
||||
personId?: string,
|
||||
workspaceMemberId?: string,
|
||||
) {
|
||||
if (personId) {
|
||||
await this.messageParticipantRepository.removePersonIdByHandle(
|
||||
handle,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
if (workspaceMemberId) {
|
||||
await this.messageParticipantRepository.removeWorkspaceMemberIdByHandle(
|
||||
handle,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/common/repositories/message-channel-message-association.repository';
|
||||
import { MessageThreadRepository } from 'src/modules/messaging/common/repositories/message-thread.repository';
|
||||
import { MessageRepository } from 'src/modules/messaging/common/repositories/message.repository';
|
||||
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
import { MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity';
|
||||
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class MessagingMessageThreadService {
|
||||
constructor(
|
||||
@InjectObjectMetadataRepository(
|
||||
MessageChannelMessageAssociationWorkspaceEntity,
|
||||
)
|
||||
private readonly messageChannelMessageAssociationRepository: MessageChannelMessageAssociationRepository,
|
||||
@InjectObjectMetadataRepository(MessageWorkspaceEntity)
|
||||
private readonly messageRepository: MessageRepository,
|
||||
@InjectObjectMetadataRepository(MessageThreadWorkspaceEntity)
|
||||
private readonly messageThreadRepository: MessageThreadRepository,
|
||||
) {}
|
||||
|
||||
public async saveMessageThreadOrReturnExistingMessageThread(
|
||||
headerMessageId: string,
|
||||
messageThreadExternalId: string,
|
||||
workspaceId: string,
|
||||
manager: EntityManager,
|
||||
) {
|
||||
// Check if message thread already exists via threadExternalId
|
||||
const existingMessageChannelMessageAssociationByMessageThreadExternalId =
|
||||
await this.messageChannelMessageAssociationRepository.getFirstByMessageThreadExternalId(
|
||||
messageThreadExternalId,
|
||||
workspaceId,
|
||||
manager,
|
||||
);
|
||||
|
||||
const existingMessageThread =
|
||||
existingMessageChannelMessageAssociationByMessageThreadExternalId?.messageThreadId;
|
||||
|
||||
if (existingMessageThread) {
|
||||
return Promise.resolve(existingMessageThread);
|
||||
}
|
||||
|
||||
// Check if message thread already exists via existing message headerMessageId
|
||||
const existingMessageWithSameHeaderMessageId =
|
||||
await this.messageRepository.getFirstOrNullByHeaderMessageId(
|
||||
headerMessageId,
|
||||
workspaceId,
|
||||
manager,
|
||||
);
|
||||
|
||||
if (existingMessageWithSameHeaderMessageId) {
|
||||
return Promise.resolve(
|
||||
existingMessageWithSameHeaderMessageId.messageThreadId,
|
||||
);
|
||||
}
|
||||
|
||||
// If message thread does not exist, create new message thread
|
||||
const newMessageThreadId = v4();
|
||||
|
||||
await this.messageThreadRepository.insert(
|
||||
newMessageThreadId,
|
||||
workspaceId,
|
||||
manager,
|
||||
);
|
||||
|
||||
return Promise.resolve(newMessageThreadId);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,323 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { DataSource, EntityManager } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/common/repositories/message-channel-message-association.repository';
|
||||
import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository';
|
||||
import { MessageThreadRepository } from 'src/modules/messaging/common/repositories/message-thread.repository';
|
||||
import { MessageRepository } from 'src/modules/messaging/common/repositories/message.repository';
|
||||
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity';
|
||||
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
import { GmailMessage } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message';
|
||||
import { MessagingMessageThreadService } from 'src/modules/messaging/common/services/messaging-message-thread.service';
|
||||
|
||||
@Injectable()
|
||||
export class MessagingMessageService {
|
||||
private readonly logger = new Logger(MessagingMessageService.name);
|
||||
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
@InjectObjectMetadataRepository(
|
||||
MessageChannelMessageAssociationWorkspaceEntity,
|
||||
)
|
||||
private readonly messageChannelMessageAssociationRepository: MessageChannelMessageAssociationRepository,
|
||||
@InjectObjectMetadataRepository(MessageWorkspaceEntity)
|
||||
private readonly messageRepository: MessageRepository,
|
||||
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
|
||||
private readonly messageChannelRepository: MessageChannelRepository,
|
||||
@InjectObjectMetadataRepository(MessageThreadWorkspaceEntity)
|
||||
private readonly messageThreadRepository: MessageThreadRepository,
|
||||
private readonly messageThreadService: MessagingMessageThreadService,
|
||||
) {}
|
||||
|
||||
public async saveMessagesWithinTransaction(
|
||||
messages: GmailMessage[],
|
||||
connectedAccount: ObjectRecord<ConnectedAccountWorkspaceEntity>,
|
||||
gmailMessageChannelId: string,
|
||||
workspaceId: string,
|
||||
transactionManager: EntityManager,
|
||||
): Promise<Map<string, string>> {
|
||||
const messageExternalIdsAndIdsMap = new Map<string, string>();
|
||||
|
||||
for (const message of messages) {
|
||||
const existingMessageChannelMessageAssociationsCount =
|
||||
await this.messageChannelMessageAssociationRepository.countByMessageExternalIdsAndMessageChannelId(
|
||||
[message.externalId],
|
||||
gmailMessageChannelId,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
if (existingMessageChannelMessageAssociationsCount > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: This does not handle all thread merging use cases and might create orphan threads.
|
||||
const savedOrExistingMessageThreadId =
|
||||
await this.messageThreadService.saveMessageThreadOrReturnExistingMessageThread(
|
||||
message.headerMessageId,
|
||||
message.messageThreadExternalId,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const savedOrExistingMessageId =
|
||||
await this.saveMessageOrReturnExistingMessage(
|
||||
message,
|
||||
savedOrExistingMessageThreadId,
|
||||
connectedAccount,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
messageExternalIdsAndIdsMap.set(
|
||||
message.externalId,
|
||||
savedOrExistingMessageId,
|
||||
);
|
||||
|
||||
await this.messageChannelMessageAssociationRepository.insert(
|
||||
gmailMessageChannelId,
|
||||
savedOrExistingMessageId,
|
||||
message.externalId,
|
||||
savedOrExistingMessageThreadId,
|
||||
message.messageThreadExternalId,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
return messageExternalIdsAndIdsMap;
|
||||
}
|
||||
|
||||
public async saveMessages(
|
||||
messages: GmailMessage[],
|
||||
workspaceDataSource: DataSource,
|
||||
connectedAccount: ObjectRecord<ConnectedAccountWorkspaceEntity>,
|
||||
gmailMessageChannelId: string,
|
||||
workspaceId: string,
|
||||
): Promise<Map<string, string>> {
|
||||
const messageExternalIdsAndIdsMap = new Map<string, string>();
|
||||
|
||||
try {
|
||||
let keepImporting = true;
|
||||
|
||||
for (const message of messages) {
|
||||
if (!keepImporting) {
|
||||
break;
|
||||
}
|
||||
|
||||
await workspaceDataSource?.transaction(
|
||||
async (manager: EntityManager) => {
|
||||
const gmailMessageChannel =
|
||||
await this.messageChannelRepository.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 =
|
||||
await this.messageChannelMessageAssociationRepository.countByMessageExternalIdsAndMessageChannelId(
|
||||
[message.externalId],
|
||||
gmailMessageChannelId,
|
||||
workspaceId,
|
||||
manager,
|
||||
);
|
||||
|
||||
if (existingMessageChannelMessageAssociationsCount > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: This does not handle all thread merging use cases and might create orphan threads.
|
||||
const savedOrExistingMessageThreadId =
|
||||
await this.messageThreadService.saveMessageThreadOrReturnExistingMessageThread(
|
||||
message.headerMessageId,
|
||||
message.messageThreadExternalId,
|
||||
workspaceId,
|
||||
manager,
|
||||
);
|
||||
|
||||
const savedOrExistingMessageId =
|
||||
await this.saveMessageOrReturnExistingMessage(
|
||||
message,
|
||||
savedOrExistingMessageThreadId,
|
||||
connectedAccount,
|
||||
workspaceId,
|
||||
manager,
|
||||
);
|
||||
|
||||
messageExternalIdsAndIdsMap.set(
|
||||
message.externalId,
|
||||
savedOrExistingMessageId,
|
||||
);
|
||||
|
||||
await this.messageChannelMessageAssociationRepository.insert(
|
||||
gmailMessageChannelId,
|
||||
savedOrExistingMessageId,
|
||||
message.externalId,
|
||||
savedOrExistingMessageThreadId,
|
||||
message.messageThreadExternalId,
|
||||
workspaceId,
|
||||
manager,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Error saving connected account ${connectedAccount.id} messages to workspace ${workspaceId}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
return messageExternalIdsAndIdsMap;
|
||||
}
|
||||
|
||||
private async saveMessageOrReturnExistingMessage(
|
||||
message: GmailMessage,
|
||||
messageThreadId: string,
|
||||
connectedAccount: ObjectRecord<ConnectedAccountWorkspaceEntity>,
|
||||
workspaceId: string,
|
||||
manager: EntityManager,
|
||||
): Promise<string> {
|
||||
const existingMessage =
|
||||
await this.messageRepository.getFirstOrNullByHeaderMessageId(
|
||||
message.headerMessageId,
|
||||
workspaceId,
|
||||
);
|
||||
const existingMessageId = existingMessage?.id;
|
||||
|
||||
if (existingMessageId) {
|
||||
return Promise.resolve(existingMessageId);
|
||||
}
|
||||
|
||||
const newMessageId = v4();
|
||||
|
||||
const messageDirection =
|
||||
connectedAccount.handle === message.fromHandle ? 'outgoing' : 'incoming';
|
||||
|
||||
const receivedAt = new Date(parseInt(message.internalDate));
|
||||
|
||||
await this.messageRepository.insert(
|
||||
newMessageId,
|
||||
message.headerMessageId,
|
||||
message.subject,
|
||||
receivedAt,
|
||||
messageDirection,
|
||||
messageThreadId,
|
||||
message.text,
|
||||
workspaceId,
|
||||
manager,
|
||||
);
|
||||
|
||||
return Promise.resolve(newMessageId);
|
||||
}
|
||||
|
||||
public async deleteMessages(
|
||||
messagesDeletedMessageExternalIds: string[],
|
||||
gmailMessageChannelId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const workspaceDataSource =
|
||||
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await workspaceDataSource?.transaction(async (manager: EntityManager) => {
|
||||
const messageChannelMessageAssociationsToDelete =
|
||||
await this.messageChannelMessageAssociationRepository.getByMessageExternalIdsAndMessageChannelId(
|
||||
messagesDeletedMessageExternalIds,
|
||||
gmailMessageChannelId,
|
||||
workspaceId,
|
||||
manager,
|
||||
);
|
||||
|
||||
const messageChannelMessageAssociationIdsToDeleteIds =
|
||||
messageChannelMessageAssociationsToDelete.map(
|
||||
(messageChannelMessageAssociationToDelete) =>
|
||||
messageChannelMessageAssociationToDelete.id,
|
||||
);
|
||||
|
||||
await this.messageChannelMessageAssociationRepository.deleteByIds(
|
||||
messageChannelMessageAssociationIdsToDeleteIds,
|
||||
workspaceId,
|
||||
manager,
|
||||
);
|
||||
|
||||
const messageIdsFromMessageChannelMessageAssociationsToDelete =
|
||||
messageChannelMessageAssociationsToDelete.map(
|
||||
(messageChannelMessageAssociationToDelete) =>
|
||||
messageChannelMessageAssociationToDelete.messageId,
|
||||
);
|
||||
|
||||
const messageChannelMessageAssociationByMessageIds =
|
||||
await this.messageChannelMessageAssociationRepository.getByMessageIds(
|
||||
messageIdsFromMessageChannelMessageAssociationsToDelete,
|
||||
workspaceId,
|
||||
manager,
|
||||
);
|
||||
|
||||
const messageIdsFromMessageChannelMessageAssociationByMessageIds =
|
||||
messageChannelMessageAssociationByMessageIds.map(
|
||||
(messageChannelMessageAssociation) =>
|
||||
messageChannelMessageAssociation.messageId,
|
||||
);
|
||||
|
||||
const messageIdsToDelete =
|
||||
messageIdsFromMessageChannelMessageAssociationsToDelete.filter(
|
||||
(messageId) =>
|
||||
!messageIdsFromMessageChannelMessageAssociationByMessageIds.includes(
|
||||
messageId,
|
||||
),
|
||||
);
|
||||
|
||||
await this.messageRepository.deleteByIds(
|
||||
messageIdsToDelete,
|
||||
workspaceId,
|
||||
manager,
|
||||
);
|
||||
|
||||
const messageThreadIdsFromMessageChannelMessageAssociationsToDelete =
|
||||
messageChannelMessageAssociationsToDelete.map(
|
||||
(messageChannelMessageAssociationToDelete) =>
|
||||
messageChannelMessageAssociationToDelete.messageThreadId,
|
||||
);
|
||||
|
||||
const messagesByThreadIds =
|
||||
await this.messageRepository.getByMessageThreadIds(
|
||||
messageThreadIdsFromMessageChannelMessageAssociationsToDelete,
|
||||
workspaceId,
|
||||
manager,
|
||||
);
|
||||
|
||||
const threadIdsToDelete =
|
||||
messageThreadIdsFromMessageChannelMessageAssociationsToDelete.filter(
|
||||
(threadId) =>
|
||||
!messagesByThreadIds.find(
|
||||
(message) => message.messageThreadId === threadId,
|
||||
),
|
||||
);
|
||||
|
||||
await this.messageThreadRepository.deleteByIds(
|
||||
threadIdsToDelete,
|
||||
workspaceId,
|
||||
manager,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,114 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { EntityManager, Repository } from 'typeorm';
|
||||
|
||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import {
|
||||
CreateCompanyAndContactJobData,
|
||||
CreateCompanyAndContactJob,
|
||||
} from 'src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job';
|
||||
import {
|
||||
FeatureFlagEntity,
|
||||
FeatureFlagKeys,
|
||||
} from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import {
|
||||
GmailMessage,
|
||||
ParticipantWithMessageId,
|
||||
} from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message';
|
||||
import { MessagingMessageService } from 'src/modules/messaging/common/services/messaging-message.service';
|
||||
import { MessagingMessageParticipantService } from 'src/modules/messaging/common/services/messaging-message-participant.service';
|
||||
|
||||
@Injectable()
|
||||
export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
@Inject(MessageQueue.messagingQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
private readonly messageService: MessagingMessageService,
|
||||
private readonly messageParticipantService: MessagingMessageParticipantService,
|
||||
@InjectRepository(FeatureFlagEntity, 'core')
|
||||
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
||||
) {}
|
||||
|
||||
async saveMessagesAndEnqueueContactCreationJob(
|
||||
messagesToSave: GmailMessage[],
|
||||
messageChannel: ObjectRecord<MessageChannelWorkspaceEntity>,
|
||||
connectedAccount: ObjectRecord<ConnectedAccountWorkspaceEntity>,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const workspaceDataSource =
|
||||
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const isContactCreationForSentAndReceivedEmailsEnabledFeatureFlag =
|
||||
await this.featureFlagRepository.findOneBy({
|
||||
workspaceId: workspaceId,
|
||||
key: FeatureFlagKeys.IsContactCreationForSentAndReceivedEmailsEnabled,
|
||||
value: true,
|
||||
});
|
||||
|
||||
const isContactCreationForSentAndReceivedEmailsEnabled =
|
||||
isContactCreationForSentAndReceivedEmailsEnabledFeatureFlag?.value;
|
||||
|
||||
const participantsWithMessageId = await workspaceDataSource?.transaction(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const messageExternalIdsAndIdsMap =
|
||||
await this.messageService.saveMessagesWithinTransaction(
|
||||
messagesToSave,
|
||||
connectedAccount,
|
||||
messageChannel.id,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const participantsWithMessageId: (ParticipantWithMessageId & {
|
||||
shouldCreateContact: boolean;
|
||||
})[] = messagesToSave.flatMap((message) => {
|
||||
const messageId = messageExternalIdsAndIdsMap.get(message.externalId);
|
||||
|
||||
return messageId
|
||||
? message.participants.map((participant) => ({
|
||||
...participant,
|
||||
messageId,
|
||||
shouldCreateContact:
|
||||
messageChannel.isContactAutoCreationEnabled &&
|
||||
(isContactCreationForSentAndReceivedEmailsEnabled ||
|
||||
message.participants.find((p) => p.role === 'from')
|
||||
?.handle === connectedAccount.handle),
|
||||
}))
|
||||
: [];
|
||||
});
|
||||
|
||||
await this.messageParticipantService.saveMessageParticipants(
|
||||
participantsWithMessageId,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return participantsWithMessageId;
|
||||
},
|
||||
);
|
||||
|
||||
if (messageChannel.isContactAutoCreationEnabled) {
|
||||
const contactsToCreate = participantsWithMessageId.filter(
|
||||
(participant) => participant.shouldCreateContact,
|
||||
);
|
||||
|
||||
await this.messageQueueService.add<CreateCompanyAndContactJobData>(
|
||||
CreateCompanyAndContactJob.name,
|
||||
{
|
||||
workspaceId,
|
||||
connectedAccountHandle: connectedAccount.handle,
|
||||
contactsToCreate,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.service';
|
||||
|
||||
type MessagingTelemetryTrackInput = {
|
||||
eventName: string;
|
||||
workspaceId: string;
|
||||
userId?: string;
|
||||
connectedAccountId?: string;
|
||||
messageChannelId?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class MessagingTelemetryService {
|
||||
constructor(private readonly analyticsService: AnalyticsService) {}
|
||||
|
||||
public async track({
|
||||
eventName,
|
||||
workspaceId,
|
||||
userId,
|
||||
connectedAccountId,
|
||||
messageChannelId,
|
||||
message,
|
||||
}: MessagingTelemetryTrackInput): Promise<void> {
|
||||
await this.analyticsService.create(
|
||||
{
|
||||
type: 'track',
|
||||
data: {
|
||||
eventName: `messaging.${eventName}`,
|
||||
workspaceId,
|
||||
userId,
|
||||
connectedAccountId,
|
||||
messageChannelId,
|
||||
message,
|
||||
},
|
||||
},
|
||||
userId,
|
||||
workspaceId,
|
||||
'', // voluntarely not retrieving this
|
||||
'', // to avoid slowing down
|
||||
'',
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity';
|
||||
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.messageChannelMessageAssociation,
|
||||
namePlural: 'messageChannelMessageAssociations',
|
||||
labelSingular: 'Message Channel Message Association',
|
||||
labelPlural: 'Message Channel Message Associations',
|
||||
description: 'Message Synced with a Message Channel',
|
||||
icon: 'IconMessage',
|
||||
})
|
||||
@WorkspaceIsNotAuditLogged()
|
||||
@WorkspaceIsSystem()
|
||||
export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceField({
|
||||
standardId:
|
||||
MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_STANDARD_FIELD_IDS.messageExternalId,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Message External Id',
|
||||
description: 'Message id from the messaging provider',
|
||||
icon: 'IconHash',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
messageExternalId: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId:
|
||||
MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_STANDARD_FIELD_IDS.messageThreadExternalId,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Thread External Id',
|
||||
description: 'Thread id from the messaging provider',
|
||||
icon: 'IconHash',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
messageThreadExternalId: string;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId:
|
||||
MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_STANDARD_FIELD_IDS.messageChannel,
|
||||
type: RelationMetadataType.MANY_TO_ONE,
|
||||
label: 'Message Channel Id',
|
||||
description: 'Message Channel Id',
|
||||
icon: 'IconHash',
|
||||
joinColumn: 'messageChannelId',
|
||||
inverseSideTarget: () => MessageChannelWorkspaceEntity,
|
||||
inverseSideFieldKey: 'messageChannelMessageAssociations',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
messageChannel: Relation<MessageChannelWorkspaceEntity>;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_STANDARD_FIELD_IDS.message,
|
||||
type: RelationMetadataType.MANY_TO_ONE,
|
||||
label: 'Message Id',
|
||||
description: 'Message Id',
|
||||
icon: 'IconHash',
|
||||
joinColumn: 'messageId',
|
||||
inverseSideTarget: () => MessageWorkspaceEntity,
|
||||
inverseSideFieldKey: 'messageChannelMessageAssociations',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
message: Relation<MessageWorkspaceEntity>;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId:
|
||||
MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_STANDARD_FIELD_IDS.messageThread,
|
||||
type: RelationMetadataType.MANY_TO_ONE,
|
||||
label: 'Message Thread Id',
|
||||
description: 'Message Thread Id',
|
||||
icon: 'IconHash',
|
||||
joinColumn: 'messageThreadId',
|
||||
inverseSideTarget: () => MessageThreadWorkspaceEntity,
|
||||
inverseSideFieldKey: 'messageChannelMessageAssociations',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
messageThread: Relation<MessageThreadWorkspaceEntity>;
|
||||
}
|
||||
@ -0,0 +1,313 @@
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
RelationMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { MESSAGE_CHANNEL_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
|
||||
export enum MessageChannelSyncStatus {
|
||||
// TO BE DEPRECATED
|
||||
PENDING = 'PENDING',
|
||||
SUCCEEDED = 'SUCCEEDED',
|
||||
FAILED = 'FAILED',
|
||||
|
||||
// NEW STATUSES
|
||||
NOT_SYNCED = 'NOT_SYNCED',
|
||||
ONGOING = 'ONGOING',
|
||||
COMPLETED = 'COMPLETED',
|
||||
FAILED_INSUFFICIENT_PERMISSIONS = 'FAILED_INSUFFICIENT_PERMISSIONS',
|
||||
FAILED_UNKNOWN = 'FAILED_UNKNOWN',
|
||||
}
|
||||
|
||||
export enum MessageChannelSyncSubStatus {
|
||||
FULL_MESSAGE_LIST_FETCH_PENDING = 'FULL_MESSAGE_LIST_FETCH_PENDING',
|
||||
PARTIAL_MESSAGE_LIST_FETCH_PENDING = 'PARTIAL_MESSAGE_LIST_FETCH_PENDING',
|
||||
MESSAGE_LIST_FETCH_ONGOING = 'MESSAGE_LIST_FETCH_ONGOING',
|
||||
MESSAGES_IMPORT_PENDING = 'MESSAGES_IMPORT_PENDING',
|
||||
MESSAGES_IMPORT_ONGOING = 'MESSAGES_IMPORT_ONGOING',
|
||||
FAILED = 'FAILED',
|
||||
}
|
||||
|
||||
export enum MessageChannelVisibility {
|
||||
METADATA = 'metadata',
|
||||
SUBJECT = 'subject',
|
||||
SHARE_EVERYTHING = 'share_everything',
|
||||
}
|
||||
|
||||
export enum MessageChannelType {
|
||||
EMAIL = 'email',
|
||||
SMS = 'sms',
|
||||
}
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.messageChannel,
|
||||
namePlural: 'messageChannels',
|
||||
labelSingular: 'Message Channel',
|
||||
labelPlural: 'Message Channels',
|
||||
description: 'Message Channels',
|
||||
icon: 'IconMessage',
|
||||
})
|
||||
@WorkspaceIsNotAuditLogged()
|
||||
@WorkspaceIsSystem()
|
||||
export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.visibility,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Visibility',
|
||||
description: 'Visibility',
|
||||
icon: 'IconEyeglass',
|
||||
options: [
|
||||
{
|
||||
value: MessageChannelVisibility.METADATA,
|
||||
label: 'Metadata',
|
||||
position: 0,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
value: MessageChannelVisibility.SUBJECT,
|
||||
label: 'Subject',
|
||||
position: 1,
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
value: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
label: 'Share Everything',
|
||||
position: 2,
|
||||
color: 'orange',
|
||||
},
|
||||
],
|
||||
defaultValue: `'${MessageChannelVisibility.SHARE_EVERYTHING}'`,
|
||||
})
|
||||
visibility: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.handle,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Handle',
|
||||
description: 'Handle',
|
||||
icon: 'IconAt',
|
||||
})
|
||||
handle: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.type,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Type',
|
||||
description: 'Channel Type',
|
||||
icon: 'IconMessage',
|
||||
options: [
|
||||
{
|
||||
value: MessageChannelType.EMAIL,
|
||||
label: 'Email',
|
||||
position: 0,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
value: MessageChannelType.SMS,
|
||||
label: 'SMS',
|
||||
position: 1,
|
||||
color: 'blue',
|
||||
},
|
||||
],
|
||||
defaultValue: `'${MessageChannelType.EMAIL}'`,
|
||||
})
|
||||
type: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.isContactAutoCreationEnabled,
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: 'Is Contact Auto Creation Enabled',
|
||||
description: 'Is Contact Auto Creation Enabled',
|
||||
icon: 'IconUserCircle',
|
||||
defaultValue: true,
|
||||
})
|
||||
isContactAutoCreationEnabled: boolean;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.isSyncEnabled,
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: 'Is Sync Enabled',
|
||||
description: 'Is Sync Enabled',
|
||||
icon: 'IconRefresh',
|
||||
defaultValue: true,
|
||||
})
|
||||
isSyncEnabled: boolean;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.syncCursor,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Last sync cursor',
|
||||
description: 'Last sync cursor',
|
||||
icon: 'IconHistory',
|
||||
})
|
||||
syncCursor: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.syncedAt,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Last sync date',
|
||||
description: 'Last sync date',
|
||||
icon: 'IconHistory',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
syncedAt: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.syncStatus,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Sync status',
|
||||
description: 'Sync status',
|
||||
icon: 'IconStatusChange',
|
||||
options: [
|
||||
// TO BE DEPRECATED: PENDING, SUCCEEDED, FAILED
|
||||
{
|
||||
value: MessageChannelSyncStatus.PENDING,
|
||||
label: 'Pending',
|
||||
position: 0,
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
value: MessageChannelSyncStatus.SUCCEEDED,
|
||||
label: 'Succeeded',
|
||||
position: 2,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
value: MessageChannelSyncStatus.FAILED,
|
||||
label: 'Failed',
|
||||
position: 3,
|
||||
color: 'red',
|
||||
},
|
||||
// NEW STATUSES
|
||||
{
|
||||
value: MessageChannelSyncStatus.ONGOING,
|
||||
label: 'Ongoing',
|
||||
position: 1,
|
||||
color: 'yellow',
|
||||
},
|
||||
{
|
||||
value: MessageChannelSyncStatus.NOT_SYNCED,
|
||||
label: 'Not Synced',
|
||||
position: 4,
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
value: MessageChannelSyncStatus.COMPLETED,
|
||||
label: 'Completed',
|
||||
position: 5,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
value: MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS,
|
||||
label: 'Failed Insufficient Permissions',
|
||||
position: 6,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
value: MessageChannelSyncStatus.FAILED_UNKNOWN,
|
||||
label: 'Failed Unknown',
|
||||
position: 7,
|
||||
color: 'red',
|
||||
},
|
||||
],
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
syncStatus: MessageChannelSyncStatus;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.syncSubStatus,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Sync sub status',
|
||||
description: 'Sync sub status',
|
||||
icon: 'IconStatusChange',
|
||||
options: [
|
||||
{
|
||||
value: MessageChannelSyncSubStatus.FULL_MESSAGE_LIST_FETCH_PENDING,
|
||||
label: 'Full messages list fetch pending',
|
||||
position: 0,
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
value: MessageChannelSyncSubStatus.PARTIAL_MESSAGE_LIST_FETCH_PENDING,
|
||||
label: 'Partial messages list fetch pending',
|
||||
position: 1,
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
value: MessageChannelSyncSubStatus.MESSAGE_LIST_FETCH_ONGOING,
|
||||
label: 'Messages list fetch ongoing',
|
||||
position: 2,
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
value: MessageChannelSyncSubStatus.MESSAGES_IMPORT_PENDING,
|
||||
label: 'Messages import pending',
|
||||
position: 3,
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
value: MessageChannelSyncSubStatus.MESSAGES_IMPORT_ONGOING,
|
||||
label: 'Messages import ongoing',
|
||||
position: 4,
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
value: MessageChannelSyncSubStatus.FAILED,
|
||||
label: 'Failed',
|
||||
position: 5,
|
||||
color: 'red',
|
||||
},
|
||||
],
|
||||
defaultValue: `'${MessageChannelSyncSubStatus.FULL_MESSAGE_LIST_FETCH_PENDING}'`,
|
||||
})
|
||||
syncSubStatus: MessageChannelSyncSubStatus;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.ongoingSyncStartedAt,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Ongoing sync started at',
|
||||
description: 'Ongoing sync started at',
|
||||
icon: 'IconHistory',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
ongoingSyncStartedAt: string;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.connectedAccount,
|
||||
type: RelationMetadataType.MANY_TO_ONE,
|
||||
label: 'Connected Account',
|
||||
description: 'Connected Account',
|
||||
icon: 'IconUserCircle',
|
||||
joinColumn: 'connectedAccountId',
|
||||
inverseSideTarget: () => ConnectedAccountWorkspaceEntity,
|
||||
inverseSideFieldKey: 'messageChannels',
|
||||
})
|
||||
connectedAccount: Relation<ConnectedAccountWorkspaceEntity>;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId:
|
||||
MESSAGE_CHANNEL_STANDARD_FIELD_IDS.messageChannelMessageAssociations,
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
label: 'Message Channel Association',
|
||||
description: 'Messages from the channel.',
|
||||
icon: 'IconMessage',
|
||||
inverseSideTarget: () => MessageChannelMessageAssociationWorkspaceEntity,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
messageChannelMessageAssociations: Relation<
|
||||
MessageChannelMessageAssociationWorkspaceEntity[]
|
||||
>;
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.messageParticipant,
|
||||
namePlural: 'messageParticipants',
|
||||
labelSingular: 'Message Participant',
|
||||
labelPlural: 'Message Participants',
|
||||
description: 'Message Participants',
|
||||
icon: 'IconUserCircle',
|
||||
})
|
||||
@WorkspaceIsNotAuditLogged()
|
||||
@WorkspaceIsSystem()
|
||||
export class MessageParticipantWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS.role,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Role',
|
||||
description: 'Role',
|
||||
icon: 'IconAt',
|
||||
options: [
|
||||
{ value: 'from', label: 'From', position: 0, color: 'green' },
|
||||
{ value: 'to', label: 'To', position: 1, color: 'blue' },
|
||||
{ value: 'cc', label: 'Cc', position: 2, color: 'orange' },
|
||||
{ value: 'bcc', label: 'Bcc', position: 3, color: 'red' },
|
||||
],
|
||||
defaultValue: "'from'",
|
||||
})
|
||||
role: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS.handle,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Handle',
|
||||
description: 'Handle',
|
||||
icon: 'IconAt',
|
||||
})
|
||||
handle: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS.displayName,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Display Name',
|
||||
description: 'Display Name',
|
||||
icon: 'IconUser',
|
||||
})
|
||||
displayName: string;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS.message,
|
||||
type: RelationMetadataType.MANY_TO_ONE,
|
||||
label: 'Message',
|
||||
description: 'Message',
|
||||
icon: 'IconMessage',
|
||||
joinColumn: 'messageId',
|
||||
inverseSideTarget: () => MessageWorkspaceEntity,
|
||||
inverseSideFieldKey: 'messageParticipants',
|
||||
})
|
||||
message: Relation<MessageWorkspaceEntity>;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS.person,
|
||||
type: RelationMetadataType.MANY_TO_ONE,
|
||||
label: 'Person',
|
||||
description: 'Person',
|
||||
icon: 'IconUser',
|
||||
joinColumn: 'personId',
|
||||
inverseSideTarget: () => PersonWorkspaceEntity,
|
||||
inverseSideFieldKey: 'messageParticipants',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
person: Relation<PersonWorkspaceEntity>;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS.workspaceMember,
|
||||
type: RelationMetadataType.MANY_TO_ONE,
|
||||
label: 'Workspace Member',
|
||||
description: 'Workspace member',
|
||||
icon: 'IconCircleUser',
|
||||
joinColumn: 'workspaceMemberId',
|
||||
inverseSideTarget: () => WorkspaceMemberWorkspaceEntity,
|
||||
inverseSideFieldKey: 'messageParticipants',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
workspaceMember: Relation<WorkspaceMemberWorkspaceEntity>;
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import {
|
||||
RelationMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { MESSAGE_THREAD_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.messageThread,
|
||||
namePlural: 'messageThreads',
|
||||
labelSingular: 'Message Thread',
|
||||
labelPlural: 'Message Threads',
|
||||
description: 'Message Thread',
|
||||
icon: 'IconMessage',
|
||||
})
|
||||
@WorkspaceIsNotAuditLogged()
|
||||
@WorkspaceIsSystem()
|
||||
export class MessageThreadWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceRelation({
|
||||
standardId: MESSAGE_THREAD_STANDARD_FIELD_IDS.messages,
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
label: 'Messages',
|
||||
description: 'Messages from the thread.',
|
||||
icon: 'IconMessage',
|
||||
inverseSideTarget: () => MessageWorkspaceEntity,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
messages: Relation<MessageWorkspaceEntity[]>;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId:
|
||||
MESSAGE_THREAD_STANDARD_FIELD_IDS.messageChannelMessageAssociations,
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
label: 'Message Channel Association',
|
||||
description: 'Messages from the channel',
|
||||
icon: 'IconMessage',
|
||||
inverseSideTarget: () => MessageChannelMessageAssociationWorkspaceEntity,
|
||||
onDelete: RelationOnDeleteAction.RESTRICT,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
messageChannelMessageAssociations: Relation<
|
||||
MessageChannelMessageAssociationWorkspaceEntity[]
|
||||
>;
|
||||
}
|
||||
@ -0,0 +1,122 @@
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
RelationMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { MESSAGE_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||
import { MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity';
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.message,
|
||||
namePlural: 'messages',
|
||||
labelSingular: 'Message',
|
||||
labelPlural: 'Messages',
|
||||
description: 'Message',
|
||||
icon: 'IconMessage',
|
||||
})
|
||||
@WorkspaceIsNotAuditLogged()
|
||||
@WorkspaceIsSystem()
|
||||
export class MessageWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_STANDARD_FIELD_IDS.headerMessageId,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Header message Id',
|
||||
description: 'Message id from the message header',
|
||||
icon: 'IconHash',
|
||||
})
|
||||
headerMessageId: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_STANDARD_FIELD_IDS.direction,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Direction',
|
||||
description: 'Message Direction',
|
||||
icon: 'IconDirection',
|
||||
options: [
|
||||
{ value: 'incoming', label: 'Incoming', position: 0, color: 'green' },
|
||||
{ value: 'outgoing', label: 'Outgoing', position: 1, color: 'blue' },
|
||||
],
|
||||
defaultValue: "'incoming'",
|
||||
})
|
||||
direction: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_STANDARD_FIELD_IDS.subject,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Subject',
|
||||
description: 'Subject',
|
||||
icon: 'IconMessage',
|
||||
})
|
||||
subject: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_STANDARD_FIELD_IDS.text,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Text',
|
||||
description: 'Text',
|
||||
icon: 'IconMessage',
|
||||
})
|
||||
text: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_STANDARD_FIELD_IDS.receivedAt,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Received At',
|
||||
description: 'The date the message was received',
|
||||
icon: 'IconCalendar',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
receivedAt: string;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: MESSAGE_STANDARD_FIELD_IDS.messageThread,
|
||||
type: RelationMetadataType.MANY_TO_ONE,
|
||||
label: 'Message Thread Id',
|
||||
description: 'Message Thread Id',
|
||||
icon: 'IconHash',
|
||||
joinColumn: 'messageThreadId',
|
||||
inverseSideTarget: () => MessageThreadWorkspaceEntity,
|
||||
inverseSideFieldKey: 'messages',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
messageThread: Relation<MessageThreadWorkspaceEntity>;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: MESSAGE_STANDARD_FIELD_IDS.messageParticipants,
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
label: 'Message Participants',
|
||||
description: 'Message Participants',
|
||||
icon: 'IconUserCircle',
|
||||
inverseSideTarget: () => MessageParticipantWorkspaceEntity,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
messageParticipants: Relation<MessageParticipantWorkspaceEntity[]>;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: MESSAGE_STANDARD_FIELD_IDS.messageChannelMessageAssociations,
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
label: 'Message Channel Association',
|
||||
description: 'Messages from the channel.',
|
||||
icon: 'IconMessage',
|
||||
inverseSideTarget: () => MessageChannelMessageAssociationWorkspaceEntity,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
messageChannelMessageAssociations: Relation<
|
||||
MessageChannelMessageAssociationWorkspaceEntity[]
|
||||
>;
|
||||
}
|
||||
Reference in New Issue
Block a user