From dc7fccb0a89197b831a69ad2e2f75a2523d1ba63 Mon Sep 17 00:00:00 2001 From: Weiko Date: Tue, 23 Jan 2024 17:28:14 +0100 Subject: [PATCH] Merge messages and threads #1 (#3583) * Merge messages and threads * rename messageChannelSync to messageChannelMessage * add merge logic * remove deprecated methods * restore enqueue GmailFullSyncJob after connectedAccount creation --- .../activities/emails/components/Threads.tsx | 19 +- .../auth/services/google-gmail.service.ts | 9 +- .../messaging/timeline-messaging.resolver.ts | 2 +- .../messaging/timeline-messaging.service.ts | 6 +- .../services/gmail-full-sync.service.ts | 47 ++- .../services/gmail-partial-sync.service.ts | 66 +--- .../gmail-refresh-access-token.service.ts | 2 +- .../services/messaging-utils.service.ts | 289 +++++++----------- .../standard-objects/index.ts | 2 + ...message-channel-message.object-metadata.ts | 71 +++++ .../message-channel.object-metadata.ts | 35 ++- .../message-thread.object-metadata.ts | 48 +-- .../message.object-metadata.ts | 32 +- 13 files changed, 291 insertions(+), 337 deletions(-) create mode 100644 packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel-message.object-metadata.ts diff --git a/packages/twenty-front/src/modules/activities/emails/components/Threads.tsx b/packages/twenty-front/src/modules/activities/emails/components/Threads.tsx index bb9fc06b10d3..d1f78b5056c4 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/Threads.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/Threads.tsx @@ -62,19 +62,22 @@ export const Threads = ({ entity }: { entity: ActivityTargetableObject }) => { title={ <> Inbox{' '} - {timelineThreads.length} + + {timelineThreads && timelineThreads.length} + } fontColor={H1TitleFontColor.Primary} /> - {timelineThreads.map((thread: TimelineThread, index: number) => ( - - ))} + {timelineThreads && + timelineThreads.map((thread: TimelineThread, index: number) => ( + + ))} diff --git a/packages/twenty-server/src/core/auth/services/google-gmail.service.ts b/packages/twenty-server/src/core/auth/services/google-gmail.service.ts index cb73dab967e7..286c3e82f7b8 100644 --- a/packages/twenty-server/src/core/auth/services/google-gmail.service.ts +++ b/packages/twenty-server/src/core/auth/services/google-gmail.service.ts @@ -21,13 +21,14 @@ export class GoogleGmailService { private readonly messageQueueService: MessageQueueService, ) {} + providerName = 'google'; + async saveConnectedAccount( saveConnectedAccountInput: SaveConnectedAccountInput, ) { const { handle, workspaceId, - provider, accessToken, refreshToken, workspaceMemberId, @@ -43,7 +44,7 @@ export class GoogleGmailService { const connectedAccount = await workspaceDataSource?.query( `SELECT * FROM ${dataSourceMetadata.schema}."connectedAccount" WHERE "handle" = $1 AND "provider" = $2 AND "accountOwnerId" = $3`, - [handle, provider, workspaceMemberId], + [handle, this.providerName, workspaceMemberId], ); if (connectedAccount.length > 0) { @@ -60,7 +61,7 @@ export class GoogleGmailService { [ connectedAccountId, handle, - provider, + this.providerName, accessToken, refreshToken, workspaceMemberId, @@ -69,7 +70,7 @@ export class GoogleGmailService { await manager.query( `INSERT INTO ${dataSourceMetadata.schema}."messageChannel" ("visibility", "handle", "connectedAccountId", "type") VALUES ($1, $2, $3, $4)`, - ['share_everything', handle, connectedAccountId, 'gmail'], + ['share_everything', handle, connectedAccountId, 'email'], ); }); diff --git a/packages/twenty-server/src/core/messaging/timeline-messaging.resolver.ts b/packages/twenty-server/src/core/messaging/timeline-messaging.resolver.ts index 591af18b649c..f98df9e13ef6 100644 --- a/packages/twenty-server/src/core/messaging/timeline-messaging.resolver.ts +++ b/packages/twenty-server/src/core/messaging/timeline-messaging.resolver.ts @@ -10,7 +10,7 @@ import { TimelineMessagingService } from 'src/core/messaging/timeline-messaging. @Entity({ name: 'timelineThread', schema: 'core' }) @ObjectType('TimelineThread') -class TimelineThread { +export class TimelineThread { @Field() @Column() read: boolean; diff --git a/packages/twenty-server/src/core/messaging/timeline-messaging.service.ts b/packages/twenty-server/src/core/messaging/timeline-messaging.service.ts index 1e31954a5414..46784068ea17 100644 --- a/packages/twenty-server/src/core/messaging/timeline-messaging.service.ts +++ b/packages/twenty-server/src/core/messaging/timeline-messaging.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { TimelineThread } from 'src/core/messaging/timeline-messaging.resolver'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { DataSourceService } from 'src/metadata/data-source/data-source.service'; @@ -10,7 +11,10 @@ export class TimelineMessagingService { private readonly typeORMService: TypeORMService, ) {} - async getMessagesFromPersonIds(workspaceId: string, personIds: string[]) { + async getMessagesFromPersonIds( + workspaceId: string, + personIds: string[], + ): Promise { const dataSourceMetadata = await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( workspaceId, diff --git a/packages/twenty-server/src/workspace/messaging/services/gmail-full-sync.service.ts b/packages/twenty-server/src/workspace/messaging/services/gmail-full-sync.service.ts index 1fa1f632efe0..efe8cd2b4cdd 100644 --- a/packages/twenty-server/src/workspace/messaging/services/gmail-full-sync.service.ts +++ b/packages/twenty-server/src/workspace/messaging/services/gmail-full-sync.service.ts @@ -30,6 +30,19 @@ export class GmailFullSyncService { throw new Error('No refresh token found'); } + const gmailMessageChannel = await workspaceDataSource?.query( + `SELECT * FROM ${dataSourceMetadata.schema}."messageChannel" WHERE "connectedAccountId" = $1 AND "type" = 'email' LIMIT 1`, + [connectedAccountId], + ); + + if (!gmailMessageChannel.length) { + throw new Error( + `No gmail message channel found for connected account ${connectedAccountId}`, + ); + } + + const gmailMessageChannelId = gmailMessageChannel[0].id; + const gmailClient = await this.gmailClientProvider.getGmailClient(refreshToken); @@ -48,20 +61,8 @@ export class GmailFullSyncService { return; } - const { savedMessageIds, savedThreadIds } = - await this.utils.getSavedMessageIdsAndThreadIds( - messageExternalIds, - connectedAccountId, - dataSourceMetadata, - workspaceDataSource, - ); - - const messageIdsToSave = messageExternalIds.filter( - (messageId) => !savedMessageIds.includes(messageId), - ); - const messageQueries = - this.utils.createQueriesFromMessageIds(messageIdsToSave); + this.utils.createQueriesFromMessageIds(messageExternalIds); const { messages: messagesToSave, errors } = await this.fetchMessagesByBatchesService.fetchAllMessages( @@ -69,32 +70,20 @@ export class GmailFullSyncService { accessToken, ); - const threads = this.utils.getThreadsFromMessages(messagesToSave); - - const threadsToSave = threads.filter( - (threadId) => !savedThreadIds.includes(threadId.id), - ); - - await this.utils.saveMessageThreads( - threadsToSave, - dataSourceMetadata, - workspaceDataSource, - connectedAccount.id, - ); + if (messagesToSave.length === 0) { + return; + } await this.utils.saveMessages( messagesToSave, dataSourceMetadata, workspaceDataSource, connectedAccount, + gmailMessageChannelId, ); if (errors.length) throw new Error('Error fetching messages'); - if (messagesToSave.length === 0) { - return; - } - const lastModifiedMessageId = messagesData[0].id; const historyId = messagesToSave.find( diff --git a/packages/twenty-server/src/workspace/messaging/services/gmail-partial-sync.service.ts b/packages/twenty-server/src/workspace/messaging/services/gmail-partial-sync.service.ts index 63022acadef1..0b1a5ff8afaf 100644 --- a/packages/twenty-server/src/workspace/messaging/services/gmail-partial-sync.service.ts +++ b/packages/twenty-server/src/workspace/messaging/services/gmail-partial-sync.service.ts @@ -111,40 +111,24 @@ export class GmailPartialSyncService { return; } - const { messagesAdded, messagesDeleted } = - await this.getMessageIdsAndThreadIdsFromHistory(history); - - const { - savedMessageIds: messagesAddedAlreadySaved, - savedThreadIds: threadsAddedAlreadySaved, - } = await this.utils.getSavedMessageIdsAndThreadIds( - messagesAdded, - connectedAccountId, - dataSourceMetadata, - workspaceDataSource, - ); - - const messageExternalIdsToSave = messagesAdded.filter( - (messageId) => - !messagesAddedAlreadySaved.includes(messageId) && - !messagesDeleted.includes(messageId), + const gmailMessageChannel = await workspaceDataSource?.query( + `SELECT * FROM ${dataSourceMetadata.schema}."messageChannel" WHERE "connectedAccountId" = $1 AND "type" = 'email' LIMIT 1`, + [connectedAccountId], ); - const { savedMessageIds: messagesDeletedAlreadySaved } = - await this.utils.getSavedMessageIdsAndThreadIds( - messagesDeleted, - connectedAccountId, - dataSourceMetadata, - workspaceDataSource, + if (!gmailMessageChannel.length) { + throw new Error( + `No gmail message channel found for connected account ${connectedAccountId}`, ); + } - const messageExternalIdsToDelete = messagesDeleted.filter((messageId) => - messagesDeletedAlreadySaved.includes(messageId), - ); + const gmailMessageChannelId = gmailMessageChannel[0].id; - const messageQueries = this.utils.createQueriesFromMessageIds( - messageExternalIdsToSave, - ); + const { messagesAdded, messagesDeleted } = + await this.getMessageIdsAndThreadIdsFromHistory(history); + + const messageQueries = + this.utils.createQueriesFromMessageIds(messagesAdded); const { messages: messagesToSave, errors } = await this.fetchMessagesByBatchesService.fetchAllMessages( @@ -152,35 +136,17 @@ export class GmailPartialSyncService { accessToken, ); - const threads = this.utils.getThreadsFromMessages(messagesToSave); - - const threadsToSave = threads.filter( - (thread) => !threadsAddedAlreadySaved.includes(thread.id), - ); - - await this.utils.saveMessageThreads( - threadsToSave, - dataSourceMetadata, - workspaceDataSource, - connectedAccount.id, - ); - await this.utils.saveMessages( messagesToSave, dataSourceMetadata, workspaceDataSource, connectedAccount, + gmailMessageChannelId, ); - await this.utils.deleteMessages( - messageExternalIdsToDelete, - dataSourceMetadata, - workspaceDataSource, - ); - - await this.utils.deleteEmptyThreads( + await this.utils.deleteMessageChannelMessages( messagesDeleted, - connectedAccountId, + gmailMessageChannelId, dataSourceMetadata, workspaceDataSource, ); diff --git a/packages/twenty-server/src/workspace/messaging/services/gmail-refresh-access-token.service.ts b/packages/twenty-server/src/workspace/messaging/services/gmail-refresh-access-token.service.ts index fcfbb45c6d19..cd08d00cbc06 100644 --- a/packages/twenty-server/src/workspace/messaging/services/gmail-refresh-access-token.service.ts +++ b/packages/twenty-server/src/workspace/messaging/services/gmail-refresh-access-token.service.ts @@ -31,7 +31,7 @@ export class GmailRefreshAccessTokenService { } const connectedAccounts = await workspaceDataSource?.query( - `SELECT * FROM ${dataSourceMetadata.schema}."connectedAccount" WHERE "provider" = 'gmail' AND "id" = $1`, + `SELECT * FROM ${dataSourceMetadata.schema}."connectedAccount" WHERE "provider" = 'google' AND "id" = $1`, [connectedAccountId], ); diff --git a/packages/twenty-server/src/workspace/messaging/services/messaging-utils.service.ts b/packages/twenty-server/src/workspace/messaging/services/messaging-utils.service.ts index 955c9b103d8a..30d54e122e30 100644 --- a/packages/twenty-server/src/workspace/messaging/services/messaging-utils.service.ts +++ b/packages/twenty-server/src/workspace/messaging/services/messaging-utils.service.ts @@ -10,7 +10,6 @@ import { GmailMessage, Participant, } from 'src/workspace/messaging/types/gmailMessage'; -import { GmailThread } from 'src/workspace/messaging/types/gmailThread'; import { MessageQuery } from 'src/workspace/messaging/types/messageOrThreadQuery'; @Injectable() @@ -28,137 +27,129 @@ export class MessagingUtilsService { })); } - public getThreadsFromMessages(messages: GmailMessage[]): GmailThread[] { - return messages.reduce((acc, message) => { - if (message.externalId === message.messageThreadExternalId) { - acc.push({ - id: message.messageThreadExternalId, - subject: message.subject, - }); - } - - return acc; - }, [] as GmailThread[]); - } - - public async saveMessageThreads( - threads: GmailThread[], + public async saveMessages( + messages: GmailMessage[], dataSourceMetadata: DataSourceEntity, workspaceDataSource: DataSource, - connectedAccountId: string, + connectedAccount, + gmailMessageChannelId: string, ) { - const messageChannel = await workspaceDataSource?.query( - `SELECT * FROM ${dataSourceMetadata.schema}."messageChannel" WHERE "connectedAccountId" = $1`, - [connectedAccountId], - ); - - if (!messageChannel.length) { - throw new Error('No message channel found for this connected account'); - } + for (const message of messages) { + await workspaceDataSource?.transaction(async (manager) => { + const savedOrExistingMessageThreadId = + await this.saveMessageThreadOrReturnExistingMessageThread( + message.messageThreadExternalId, + dataSourceMetadata, + workspaceDataSource, + ); + + const savedOrExistingMessageId = + await this.saveMessageOrReturnExistingMessage( + message, + savedOrExistingMessageThreadId, + connectedAccount, + dataSourceMetadata, + manager, + ); - for (const thread of threads) { - await workspaceDataSource?.query( - `INSERT INTO ${dataSourceMetadata.schema}."messageThread" ("externalId", "subject", "messageChannelId", "visibility") VALUES ($1, $2, $3, $4)`, - [thread.id, thread.subject, messageChannel[0].id, 'default'], - ); + await manager.query( + `INSERT INTO ${dataSourceMetadata.schema}."messageChannelMessage" ("messageChannelId", "messageId", "messageExternalId", "messageThreadId", "messageThreadExternalId") VALUES ($1, $2, $3, $4, $5)`, + [ + gmailMessageChannelId, + savedOrExistingMessageId, + message.externalId, + savedOrExistingMessageThreadId, + message.messageThreadExternalId, + ], + ); + }); } } - public async saveMessages( - messages: GmailMessage[], - dataSourceMetadata: DataSourceEntity, - workspaceDataSource: DataSource, + private async saveMessageOrReturnExistingMessage( + message: GmailMessage, + messageThreadId: string, connectedAccount, - ) { - for (const message of messages) { - const { - externalId, - headerMessageId, - subject, - messageThreadExternalId, - internalDate, - fromHandle, - fromDisplayName, - participants, - text, - } = message; - - const receivedAt = new Date(parseInt(internalDate)); - - const messageThread = await workspaceDataSource?.query( - `SELECT * FROM ${dataSourceMetadata.schema}."messageThread" WHERE "externalId" = $1`, - [messageThreadExternalId], - ); + dataSourceMetadata: DataSourceEntity, + manager: EntityManager, + ): Promise { + const existingMessages = await manager.query( + `SELECT "message"."id" FROM ${dataSourceMetadata.schema}."message" WHERE ${dataSourceMetadata.schema}."message"."headerMessageId" = $1 LIMIT 1`, + [message.headerMessageId], + ); + const existingMessageId: string = existingMessages[0]?.id; - const messageId = v4(); + if (existingMessageId) { + return Promise.resolve(existingMessageId); + } - const person = await workspaceDataSource?.query( - `SELECT * FROM ${dataSourceMetadata.schema}."person" WHERE "email" = $1`, - [fromHandle], - ); + const newMessageId = v4(); - const personId = person[0]?.id; + const messageDirection = + connectedAccount.handle === message.fromHandle ? 'outgoing' : 'incoming'; - const workspaceMember = await workspaceDataSource?.query( - `SELECT "workspaceMember"."id" FROM ${dataSourceMetadata.schema}."workspaceMember" - JOIN ${dataSourceMetadata.schema}."connectedAccount" ON ${dataSourceMetadata.schema}."workspaceMember"."id" = ${dataSourceMetadata.schema}."connectedAccount"."accountOwnerId" - WHERE ${dataSourceMetadata.schema}."connectedAccount"."handle" = $1`, - [fromHandle], - ); + const receivedAt = new Date(parseInt(message.internalDate)); - const workspaceMemberId = workspaceMember[0]?.id; + await manager.query( + `INSERT INTO ${dataSourceMetadata.schema}."message" ("id", "headerMessageId", "subject", "receivedAt", "direction", "messageThreadId", "body") VALUES ($1, $2, $3, $4, $5, $6, $7)`, + [ + newMessageId, + message.headerMessageId, + message.subject, + receivedAt, + messageDirection, + messageThreadId, + message.text, + ], + ); - const messageDirection = - connectedAccount.handle === fromHandle ? 'outgoing' : 'incoming'; + await this.saveMessageParticipants( + message.participants, + newMessageId, + dataSourceMetadata, + manager, + ); - await workspaceDataSource?.transaction(async (manager) => { - await manager.query( - `INSERT INTO ${dataSourceMetadata.schema}."message" ("id", "externalId", "headerMessageId", "subject", "receivedAt", "messageThreadId", "direction", "body") VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, - [ - messageId, - externalId, - headerMessageId, - subject, - receivedAt, - messageThread[0]?.id, - messageDirection, - text, - ], - ); + return Promise.resolve(newMessageId); + } - await manager.query( - `INSERT INTO ${dataSourceMetadata.schema}."messageParticipant" ("messageId", "role", "handle", "displayName", "personId", "workspaceMemberId") VALUES ($1, $2, $3, $4, $5, $6)`, - [ - messageId, - 'from', - fromHandle, - fromDisplayName, - personId, - workspaceMemberId, - ], - ); + private async saveMessageThreadOrReturnExistingMessageThread( + messageThreadExternalId: string, + dataSourceMetadata: DataSourceEntity, + workspaceDataSource: DataSource, + ) { + const existingMessageThreads = await workspaceDataSource?.query( + `SELECT "messageChannelMessage"."messageThreadId" FROM ${dataSourceMetadata.schema}."messageChannelMessage" WHERE "messageThreadExternalId" = $1 LIMIT 1`, + [messageThreadExternalId], + ); - await this.saveMessageParticipants( - participants, - dataSourceMetadata, - messageId, - manager, - ); - }); + const existingMessageThread = existingMessageThreads[0]?.messageThreadId; + + if (existingMessageThread) { + return Promise.resolve(existingMessageThread); } + + const newMessageThreadId = v4(); + + await workspaceDataSource?.query( + `INSERT INTO ${dataSourceMetadata.schema}."messageThread" ("id") VALUES ($1)`, + [newMessageThreadId], + ); + + return Promise.resolve(newMessageThreadId); } - public async saveMessageParticipants( + private async saveMessageParticipants( participants: Participant[], - dataSourceMetadata: DataSourceEntity, messageId: string, + dataSourceMetadata: DataSourceEntity, manager: EntityManager, ): Promise { if (!participants) return; for (const participant of participants) { const participantPerson = await manager.query( - `SELECT * FROM ${dataSourceMetadata.schema}."person" WHERE "email" = $1`, + `SELECT "person"."id" FROM ${dataSourceMetadata.schema}."person" WHERE "email" = $1 LIMIT 1`, [participant.handle], ); @@ -167,7 +158,8 @@ export class MessagingUtilsService { const workspaceMember = await manager.query( `SELECT "workspaceMember"."id" FROM ${dataSourceMetadata.schema}."workspaceMember" JOIN ${dataSourceMetadata.schema}."connectedAccount" ON ${dataSourceMetadata.schema}."workspaceMember"."id" = ${dataSourceMetadata.schema}."connectedAccount"."accountOwnerId" - WHERE ${dataSourceMetadata.schema}."connectedAccount"."handle" = $1`, + WHERE ${dataSourceMetadata.schema}."connectedAccount"."handle" = $1 + LIMIT 1`, [participant.handle], ); @@ -187,41 +179,16 @@ export class MessagingUtilsService { } } - public async getSavedMessageIdsAndThreadIds( - messageEternalIds: string[], + public async deleteMessageChannelMessages( + messageExternalIds: string[], connectedAccountId: string, dataSourceMetadata: DataSourceEntity, workspaceDataSource: DataSource, - ): Promise<{ - savedMessageIds: string[]; - savedThreadIds: string[]; - }> { - const messageIdsInDatabase: { - messageExternalId: string; - messageThreadExternalId: string; - }[] = await workspaceDataSource?.query( - `SELECT message."externalId" AS "messageExternalId", - "messageThread"."externalId" AS "messageThreadExternalId" - FROM ${dataSourceMetadata.schema}."message" message - LEFT JOIN ${dataSourceMetadata.schema}."messageThread" "messageThread" ON message."messageThreadId" = "messageThread"."id" - LEFT JOIN ${dataSourceMetadata.schema}."messageChannel" ON "messageThread"."messageChannelId" = ${dataSourceMetadata.schema}."messageChannel"."id" - WHERE ${dataSourceMetadata.schema}."messageChannel"."connectedAccountId" = $1 - AND message."externalId" = ANY($2)`, - [connectedAccountId, messageEternalIds], + ) { + await workspaceDataSource?.query( + `DELETE FROM ${dataSourceMetadata.schema}."messageChannelMessage" WHERE "messageExternalId" = ANY($1) AND "messageChannelId" = $2`, + [messageExternalIds, connectedAccountId], ); - - return { - savedMessageIds: messageIdsInDatabase.map( - (message) => message.messageExternalId, - ), - savedThreadIds: [ - ...new Set( - messageIdsInDatabase.map( - (message) => message.messageThreadExternalId, - ), - ), - ], - }; } public async getConnectedAccountsFromWorkspaceId( @@ -240,7 +207,7 @@ export class MessagingUtilsService { } const connectedAccounts = await workspaceDataSource?.query( - `SELECT * FROM ${dataSourceMetadata.schema}."connectedAccount" WHERE "provider" = 'gmail'`, + `SELECT * FROM ${dataSourceMetadata.schema}."connectedAccount" WHERE "provider" = 'google'`, ); if (!connectedAccounts || connectedAccounts.length === 0) { @@ -271,7 +238,7 @@ export class MessagingUtilsService { } const connectedAccounts = await workspaceDataSource?.query( - `SELECT * FROM ${dataSourceMetadata.schema}."connectedAccount" WHERE "provider" = 'gmail' AND "id" = $1`, + `SELECT * FROM ${dataSourceMetadata.schema}."connectedAccount" WHERE "provider" = 'google' AND "id" = $1`, [connectedAccountId], ); @@ -297,50 +264,4 @@ export class MessagingUtilsService { [historyId, connectedAccountId], ); } - - public async deleteMessages( - messageIds: string[], - dataSourceMetadata: DataSourceEntity, - workspaceDataSource: DataSource, - ) { - if (!messageIds || messageIds.length === 0) { - return; - } - - await workspaceDataSource?.query( - `DELETE FROM ${dataSourceMetadata.schema}."message" WHERE "externalId" = ANY($1)`, - [messageIds], - ); - } - - public async deleteEmptyThreads( - messageIds: string[], - connectedAccountId: string, - dataSourceMetadata: DataSourceEntity, - workspaceDataSource: DataSource, - ) { - const messageThreadsToDelete = await workspaceDataSource?.query( - `SELECT "messageThread"."id" FROM ${dataSourceMetadata.schema}."messageThread" "messageThread" - LEFT JOIN ${dataSourceMetadata.schema}."message" message ON "messageThread"."id" = message."messageThreadId" - LEFT JOIN ${dataSourceMetadata.schema}."messageChannel" ON "messageThread"."messageChannelId" = ${dataSourceMetadata.schema}."messageChannel"."id" - WHERE "messageThread"."externalId" = ANY($1) - AND ${dataSourceMetadata.schema}."messageChannel"."connectedAccountId" = $2 - GROUP BY "messageThread"."id" - HAVING COUNT(message."id") = 0`, - [messageIds, connectedAccountId], - ); - - if (!messageThreadsToDelete || messageThreadsToDelete.length === 0) { - return; - } - - const messageThreadIdsToDelete = messageThreadsToDelete.map( - (messageThread) => messageThread.id, - ); - - await workspaceDataSource?.query( - `DELETE FROM ${dataSourceMetadata.schema}."messageThread" WHERE "id" = ANY($1)`, - [messageThreadIdsToDelete], - ); - } } diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/index.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/index.ts index ab09b5f9e636..a3120406937e 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/index.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/index.ts @@ -6,6 +6,7 @@ import { CommentObjectMetadata } from 'src/workspace/workspace-sync-metadata/sta import { CompanyObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/company.object-metadata'; import { ConnectedAccountObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/connected-account.object-metadata'; import { FavoriteObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/favorite.object-metadata'; +import { MessageChannelMessageObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-channel-message.object-metadata'; import { MessageChannelObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata'; import { MessageParticipantObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-participant.object-metadata'; import { MessageThreadObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-thread.object-metadata'; @@ -42,4 +43,5 @@ export const standardObjectMetadata = [ MessageObjectMetadata, MessageChannelObjectMetadata, MessageParticipantObjectMetadata, + MessageChannelMessageObjectMetadata, ]; diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel-message.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel-message.object-metadata.ts new file mode 100644 index 000000000000..da585ad61d73 --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel-message.object-metadata.ts @@ -0,0 +1,71 @@ +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; +import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator'; +import { Gate } from 'src/workspace/workspace-sync-metadata/decorators/gate.decorator'; +import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator'; +import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator'; +import { ObjectMetadata } from 'src/workspace/workspace-sync-metadata/decorators/object-metadata.decorator'; +import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata'; +import { MessageChannelObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata'; +import { MessageThreadObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-thread.object-metadata'; +import { MessageObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message.object-metadata'; + +@ObjectMetadata({ + namePlural: 'messageChannelMessages', + labelSingular: 'Message Channel Message', + labelPlural: 'Message Channel Messages', + description: 'Message Synced with a Message Channel', + icon: 'IconMessage', +}) +@Gate({ + featureFlag: 'IS_MESSAGING_ENABLED', +}) +@IsSystem() +export class MessageChannelMessageObjectMetadata extends BaseObjectMetadata { + @FieldMetadata({ + type: FieldMetadataType.RELATION, + label: 'Message Channel Id', + description: 'Message Channel Id', + icon: 'IconHash', + joinColumn: 'messageChannelId', + }) + @IsNullable() + messageChannel: MessageChannelObjectMetadata; + + @FieldMetadata({ + type: FieldMetadataType.RELATION, + label: 'Message Id', + description: 'Message Id', + icon: 'IconHash', + joinColumn: 'messageId', + }) + @IsNullable() + message: MessageObjectMetadata; + + @FieldMetadata({ + type: FieldMetadataType.TEXT, + label: 'Message External Id', + description: 'Message id from the messaging provider', + icon: 'IconHash', + }) + @IsNullable() + messageExternalId: string; + + @FieldMetadata({ + type: FieldMetadataType.RELATION, + label: 'Message Thread Id', + description: 'Message Thread Id', + icon: 'IconHash', + joinColumn: 'messageThreadId', + }) + @IsNullable() + messageThread: MessageThreadObjectMetadata; + + @FieldMetadata({ + type: FieldMetadataType.TEXT, + label: 'Thread External Id', + description: 'Thread id from the messaging provider', + icon: 'IconHash', + }) + @IsNullable() + messageThreadExternalId: string; +} diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata.ts index 3d0cec4d85a6..470ea4f139e2 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata.ts @@ -8,7 +8,7 @@ import { ObjectMetadata } from 'src/workspace/workspace-sync-metadata/decorators import { RelationMetadata } from 'src/workspace/workspace-sync-metadata/decorators/relation-metadata.decorator'; import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata'; import { ConnectedAccountObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/connected-account.object-metadata'; -import { MessageThreadObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-thread.object-metadata'; +import { MessageChannelMessageObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-channel-message.object-metadata'; @ObjectMetadata({ namePlural: 'messageChannels', @@ -23,12 +23,21 @@ import { MessageThreadObjectMetadata } from 'src/workspace/workspace-sync-metada @IsSystem() export class MessageChannelObjectMetadata extends BaseObjectMetadata { @FieldMetadata({ - // This will be a type select later: metadata, subject, share_everything - type: FieldMetadataType.TEXT, + type: FieldMetadataType.SELECT, label: 'Visibility', description: 'Visibility', icon: 'IconEyeglass', - defaultValue: { value: 'metadata' }, + options: [ + { value: 'metadata', label: 'Metadata', position: 0, color: 'green' }, + { value: 'subject', label: 'Subject', position: 1, color: 'blue' }, + { + value: 'share_everything', + label: 'Share Everything', + position: 2, + color: 'orange', + }, + ], + defaultValue: { value: 'share_everything' }, }) visibility: string; @@ -50,24 +59,28 @@ export class MessageChannelObjectMetadata extends BaseObjectMetadata { connectedAccount: ConnectedAccountObjectMetadata; @FieldMetadata({ - // This will be a type select later : email, sms, chat - type: FieldMetadataType.TEXT, + type: FieldMetadataType.SELECT, label: 'Type', - description: 'Type', + description: 'Channel Type', icon: 'IconMessage', + options: [ + { value: 'email', label: 'Email', position: 0, color: 'green' }, + { value: 'sms', label: 'SMS', position: 1, color: 'blue' }, + ], + defaultValue: { value: 'email' }, }) type: string; @FieldMetadata({ type: FieldMetadataType.RELATION, - label: 'Message Threads', - description: 'Threads from the channel.', + label: 'Message Channel Syncs', + description: 'Messages from the channel.', icon: 'IconMessage', }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'messageThread', + objectName: 'messageChannelMessage', }) @IsNullable() - messageThreads: MessageThreadObjectMetadata[]; + messageChannelMessage: MessageChannelMessageObjectMetadata[]; } diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-thread.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-thread.object-metadata.ts index c5e1edc60824..cb7e094bd2c0 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-thread.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-thread.object-metadata.ts @@ -7,7 +7,7 @@ import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-sy import { ObjectMetadata } from 'src/workspace/workspace-sync-metadata/decorators/object-metadata.decorator'; import { RelationMetadata } from 'src/workspace/workspace-sync-metadata/decorators/relation-metadata.decorator'; import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata'; -import { MessageChannelObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata'; +import { MessageChannelMessageObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-channel-message.object-metadata'; import { MessageObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message.object-metadata'; @ObjectMetadata({ @@ -23,52 +23,28 @@ import { MessageObjectMetadata } from 'src/workspace/workspace-sync-metadata/sta @IsSystem() export class MessageThreadObjectMetadata extends BaseObjectMetadata { @FieldMetadata({ - // will be an array - type: FieldMetadataType.TEXT, - label: 'External Id', - description: 'Thread id from the messaging provider', - icon: 'IconMessage', - }) - externalId: string; - - @FieldMetadata({ - type: FieldMetadataType.TEXT, - label: 'Subject', - description: 'Subject', + type: FieldMetadataType.RELATION, + label: 'Messages', + description: 'Messages from the thread.', icon: 'IconMessage', }) - subject: string; - - @FieldMetadata({ - type: FieldMetadataType.RELATION, - label: 'Message Channel Id', - description: 'Message Channel Id', - icon: 'IconHash', - joinColumn: 'messageChannelId', + @RelationMetadata({ + type: RelationMetadataType.ONE_TO_MANY, + objectName: 'message', }) @IsNullable() - messageChannel: MessageChannelObjectMetadata; - - @FieldMetadata({ - // This will be a type select later: default, subject, share_everything - type: FieldMetadataType.TEXT, - label: 'Visibility', - description: 'Visibility', - icon: 'IconEyeglass', - defaultValue: { value: 'default' }, - }) - visibility: string; + messages: MessageObjectMetadata[]; @FieldMetadata({ type: FieldMetadataType.RELATION, - label: 'Messages', - description: 'Messages from the thread.', + label: 'Message Channel Syncs', + description: 'Messages from the channel.', icon: 'IconMessage', }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - objectName: 'message', + objectName: 'messageChannelMessage', }) @IsNullable() - messages: MessageObjectMetadata[]; + messageChannelMessage: MessageChannelMessageObjectMetadata[]; } diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message.object-metadata.ts index 7bd755dc9e44..f1c65732e0dc 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message.object-metadata.ts @@ -7,6 +7,7 @@ import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-sy import { ObjectMetadata } from 'src/workspace/workspace-sync-metadata/decorators/object-metadata.decorator'; import { RelationMetadata } from 'src/workspace/workspace-sync-metadata/decorators/relation-metadata.decorator'; import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata'; +import { MessageChannelMessageObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-channel-message.object-metadata'; import { MessageParticipantObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-participant.object-metadata'; import { MessageThreadObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-thread.object-metadata'; @@ -22,15 +23,6 @@ import { MessageThreadObjectMetadata } from 'src/workspace/workspace-sync-metada }) @IsSystem() export class MessageObjectMetadata extends BaseObjectMetadata { - @FieldMetadata({ - // will be an array - type: FieldMetadataType.TEXT, - label: 'External Id', - description: 'Message id from the messaging provider', - icon: 'IconHash', - }) - externalId: string; - @FieldMetadata({ type: FieldMetadataType.TEXT, label: 'Header message Id', @@ -50,11 +42,14 @@ export class MessageObjectMetadata extends BaseObjectMetadata { messageThread: MessageThreadObjectMetadata; @FieldMetadata({ - // will be a select later: incoming, outgoing - type: FieldMetadataType.TEXT, + type: FieldMetadataType.SELECT, label: 'Direction', - description: 'Direction', + description: 'Message Direction', icon: 'IconDirection', + options: [ + { value: 'incoming', label: 'Incoming', position: 0, color: 'green' }, + { value: 'outgoing', label: 'Outgoing', position: 1, color: 'blue' }, + ], defaultValue: { value: 'incoming' }, }) direction: string; @@ -97,4 +92,17 @@ export class MessageObjectMetadata extends BaseObjectMetadata { }) @IsNullable() messageParticipants: MessageParticipantObjectMetadata[]; + + @FieldMetadata({ + type: FieldMetadataType.RELATION, + label: 'Message Channel Syncs', + description: 'Messages from the channel.', + icon: 'IconMessage', + }) + @RelationMetadata({ + type: RelationMetadataType.ONE_TO_MANY, + objectName: 'messageChannelMessage', + }) + @IsNullable() + messageChannelMessage: MessageChannelMessageObjectMetadata[]; }