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
This commit is contained in:
Weiko
2024-01-23 17:28:14 +01:00
committed by GitHub
parent 23a3614b54
commit dc7fccb0a8
13 changed files with 298 additions and 344 deletions

View File

@ -62,19 +62,22 @@ export const Threads = ({ entity }: { entity: ActivityTargetableObject }) => {
title={ title={
<> <>
Inbox{' '} Inbox{' '}
<StyledEmailCount>{timelineThreads.length}</StyledEmailCount> <StyledEmailCount>
{timelineThreads && timelineThreads.length}
</StyledEmailCount>
</> </>
} }
fontColor={H1TitleFontColor.Primary} fontColor={H1TitleFontColor.Primary}
/> />
<Card> <Card>
{timelineThreads.map((thread: TimelineThread, index: number) => ( {timelineThreads &&
<ThreadPreview timelineThreads.map((thread: TimelineThread, index: number) => (
key={index} <ThreadPreview
divider={index < timelineThreads.length - 1} key={index}
thread={thread} divider={index < timelineThreads.length - 1}
/> thread={thread}
))} />
))}
</Card> </Card>
</Section> </Section>
</StyledContainer> </StyledContainer>

View File

@ -21,13 +21,14 @@ export class GoogleGmailService {
private readonly messageQueueService: MessageQueueService, private readonly messageQueueService: MessageQueueService,
) {} ) {}
providerName = 'google';
async saveConnectedAccount( async saveConnectedAccount(
saveConnectedAccountInput: SaveConnectedAccountInput, saveConnectedAccountInput: SaveConnectedAccountInput,
) { ) {
const { const {
handle, handle,
workspaceId, workspaceId,
provider,
accessToken, accessToken,
refreshToken, refreshToken,
workspaceMemberId, workspaceMemberId,
@ -43,7 +44,7 @@ export class GoogleGmailService {
const connectedAccount = await workspaceDataSource?.query( const connectedAccount = await workspaceDataSource?.query(
`SELECT * FROM ${dataSourceMetadata.schema}."connectedAccount" WHERE "handle" = $1 AND "provider" = $2 AND "accountOwnerId" = $3`, `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) { if (connectedAccount.length > 0) {
@ -60,7 +61,7 @@ export class GoogleGmailService {
[ [
connectedAccountId, connectedAccountId,
handle, handle,
provider, this.providerName,
accessToken, accessToken,
refreshToken, refreshToken,
workspaceMemberId, workspaceMemberId,
@ -69,7 +70,7 @@ export class GoogleGmailService {
await manager.query( await manager.query(
`INSERT INTO ${dataSourceMetadata.schema}."messageChannel" ("visibility", "handle", "connectedAccountId", "type") VALUES ($1, $2, $3, $4)`, `INSERT INTO ${dataSourceMetadata.schema}."messageChannel" ("visibility", "handle", "connectedAccountId", "type") VALUES ($1, $2, $3, $4)`,
['share_everything', handle, connectedAccountId, 'gmail'], ['share_everything', handle, connectedAccountId, 'email'],
); );
}); });

View File

@ -10,7 +10,7 @@ import { TimelineMessagingService } from 'src/core/messaging/timeline-messaging.
@Entity({ name: 'timelineThread', schema: 'core' }) @Entity({ name: 'timelineThread', schema: 'core' })
@ObjectType('TimelineThread') @ObjectType('TimelineThread')
class TimelineThread { export class TimelineThread {
@Field() @Field()
@Column() @Column()
read: boolean; read: boolean;

View File

@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TimelineThread } from 'src/core/messaging/timeline-messaging.resolver';
import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceService } from 'src/metadata/data-source/data-source.service'; import { DataSourceService } from 'src/metadata/data-source/data-source.service';
@ -10,7 +11,10 @@ export class TimelineMessagingService {
private readonly typeORMService: TypeORMService, private readonly typeORMService: TypeORMService,
) {} ) {}
async getMessagesFromPersonIds(workspaceId: string, personIds: string[]) { async getMessagesFromPersonIds(
workspaceId: string,
personIds: string[],
): Promise<TimelineThread[]> {
const dataSourceMetadata = const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId, workspaceId,

View File

@ -30,6 +30,19 @@ export class GmailFullSyncService {
throw new Error('No refresh token found'); 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 = const gmailClient =
await this.gmailClientProvider.getGmailClient(refreshToken); await this.gmailClientProvider.getGmailClient(refreshToken);
@ -48,20 +61,8 @@ export class GmailFullSyncService {
return; return;
} }
const { savedMessageIds, savedThreadIds } =
await this.utils.getSavedMessageIdsAndThreadIds(
messageExternalIds,
connectedAccountId,
dataSourceMetadata,
workspaceDataSource,
);
const messageIdsToSave = messageExternalIds.filter(
(messageId) => !savedMessageIds.includes(messageId),
);
const messageQueries = const messageQueries =
this.utils.createQueriesFromMessageIds(messageIdsToSave); this.utils.createQueriesFromMessageIds(messageExternalIds);
const { messages: messagesToSave, errors } = const { messages: messagesToSave, errors } =
await this.fetchMessagesByBatchesService.fetchAllMessages( await this.fetchMessagesByBatchesService.fetchAllMessages(
@ -69,32 +70,20 @@ export class GmailFullSyncService {
accessToken, accessToken,
); );
const threads = this.utils.getThreadsFromMessages(messagesToSave); if (messagesToSave.length === 0) {
return;
const threadsToSave = threads.filter( }
(threadId) => !savedThreadIds.includes(threadId.id),
);
await this.utils.saveMessageThreads(
threadsToSave,
dataSourceMetadata,
workspaceDataSource,
connectedAccount.id,
);
await this.utils.saveMessages( await this.utils.saveMessages(
messagesToSave, messagesToSave,
dataSourceMetadata, dataSourceMetadata,
workspaceDataSource, workspaceDataSource,
connectedAccount, connectedAccount,
gmailMessageChannelId,
); );
if (errors.length) throw new Error('Error fetching messages'); if (errors.length) throw new Error('Error fetching messages');
if (messagesToSave.length === 0) {
return;
}
const lastModifiedMessageId = messagesData[0].id; const lastModifiedMessageId = messagesData[0].id;
const historyId = messagesToSave.find( const historyId = messagesToSave.find(

View File

@ -111,40 +111,24 @@ export class GmailPartialSyncService {
return; return;
} }
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 { messagesAdded, messagesDeleted } = const { messagesAdded, messagesDeleted } =
await this.getMessageIdsAndThreadIdsFromHistory(history); await this.getMessageIdsAndThreadIdsFromHistory(history);
const { const messageQueries =
savedMessageIds: messagesAddedAlreadySaved, this.utils.createQueriesFromMessageIds(messagesAdded);
savedThreadIds: threadsAddedAlreadySaved,
} = await this.utils.getSavedMessageIdsAndThreadIds(
messagesAdded,
connectedAccountId,
dataSourceMetadata,
workspaceDataSource,
);
const messageExternalIdsToSave = messagesAdded.filter(
(messageId) =>
!messagesAddedAlreadySaved.includes(messageId) &&
!messagesDeleted.includes(messageId),
);
const { savedMessageIds: messagesDeletedAlreadySaved } =
await this.utils.getSavedMessageIdsAndThreadIds(
messagesDeleted,
connectedAccountId,
dataSourceMetadata,
workspaceDataSource,
);
const messageExternalIdsToDelete = messagesDeleted.filter((messageId) =>
messagesDeletedAlreadySaved.includes(messageId),
);
const messageQueries = this.utils.createQueriesFromMessageIds(
messageExternalIdsToSave,
);
const { messages: messagesToSave, errors } = const { messages: messagesToSave, errors } =
await this.fetchMessagesByBatchesService.fetchAllMessages( await this.fetchMessagesByBatchesService.fetchAllMessages(
@ -152,35 +136,17 @@ export class GmailPartialSyncService {
accessToken, 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( await this.utils.saveMessages(
messagesToSave, messagesToSave,
dataSourceMetadata, dataSourceMetadata,
workspaceDataSource, workspaceDataSource,
connectedAccount, connectedAccount,
gmailMessageChannelId,
); );
await this.utils.deleteMessages( await this.utils.deleteMessageChannelMessages(
messageExternalIdsToDelete,
dataSourceMetadata,
workspaceDataSource,
);
await this.utils.deleteEmptyThreads(
messagesDeleted, messagesDeleted,
connectedAccountId, gmailMessageChannelId,
dataSourceMetadata, dataSourceMetadata,
workspaceDataSource, workspaceDataSource,
); );

View File

@ -31,7 +31,7 @@ export class GmailRefreshAccessTokenService {
} }
const connectedAccounts = await workspaceDataSource?.query( 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], [connectedAccountId],
); );

View File

@ -10,7 +10,6 @@ import {
GmailMessage, GmailMessage,
Participant, Participant,
} from 'src/workspace/messaging/types/gmailMessage'; } from 'src/workspace/messaging/types/gmailMessage';
import { GmailThread } from 'src/workspace/messaging/types/gmailThread';
import { MessageQuery } from 'src/workspace/messaging/types/messageOrThreadQuery'; import { MessageQuery } from 'src/workspace/messaging/types/messageOrThreadQuery';
@Injectable() @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[],
dataSourceMetadata: DataSourceEntity,
workspaceDataSource: DataSource,
connectedAccountId: 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 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'],
);
}
}
public async saveMessages( public async saveMessages(
messages: GmailMessage[], messages: GmailMessage[],
dataSourceMetadata: DataSourceEntity, dataSourceMetadata: DataSourceEntity,
workspaceDataSource: DataSource, workspaceDataSource: DataSource,
connectedAccount, connectedAccount,
gmailMessageChannelId: string,
) { ) {
for (const message of messages) { 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],
);
const messageId = v4();
const person = await workspaceDataSource?.query(
`SELECT * FROM ${dataSourceMetadata.schema}."person" WHERE "email" = $1`,
[fromHandle],
);
const personId = person[0]?.id;
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 workspaceMemberId = workspaceMember[0]?.id;
const messageDirection =
connectedAccount.handle === fromHandle ? 'outgoing' : 'incoming';
await workspaceDataSource?.transaction(async (manager) => { await workspaceDataSource?.transaction(async (manager) => {
await manager.query( const savedOrExistingMessageThreadId =
`INSERT INTO ${dataSourceMetadata.schema}."message" ("id", "externalId", "headerMessageId", "subject", "receivedAt", "messageThreadId", "direction", "body") VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, await this.saveMessageThreadOrReturnExistingMessageThread(
[ message.messageThreadExternalId,
messageId, dataSourceMetadata,
externalId, workspaceDataSource,
headerMessageId, );
subject,
receivedAt, const savedOrExistingMessageId =
messageThread[0]?.id, await this.saveMessageOrReturnExistingMessage(
messageDirection, message,
text, savedOrExistingMessageThreadId,
], connectedAccount,
); dataSourceMetadata,
manager,
);
await manager.query( await manager.query(
`INSERT INTO ${dataSourceMetadata.schema}."messageParticipant" ("messageId", "role", "handle", "displayName", "personId", "workspaceMemberId") VALUES ($1, $2, $3, $4, $5, $6)`, `INSERT INTO ${dataSourceMetadata.schema}."messageChannelMessage" ("messageChannelId", "messageId", "messageExternalId", "messageThreadId", "messageThreadExternalId") VALUES ($1, $2, $3, $4, $5)`,
[ [
messageId, gmailMessageChannelId,
'from', savedOrExistingMessageId,
fromHandle, message.externalId,
fromDisplayName, savedOrExistingMessageThreadId,
personId, message.messageThreadExternalId,
workspaceMemberId,
], ],
); );
await this.saveMessageParticipants(
participants,
dataSourceMetadata,
messageId,
manager,
);
}); });
} }
} }
public async saveMessageParticipants( private async saveMessageOrReturnExistingMessage(
participants: Participant[], message: GmailMessage,
messageThreadId: string,
connectedAccount,
dataSourceMetadata: DataSourceEntity, dataSourceMetadata: DataSourceEntity,
manager: EntityManager,
): Promise<string> {
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;
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 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,
],
);
await this.saveMessageParticipants(
message.participants,
newMessageId,
dataSourceMetadata,
manager,
);
return Promise.resolve(newMessageId);
}
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],
);
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);
}
private async saveMessageParticipants(
participants: Participant[],
messageId: string, messageId: string,
dataSourceMetadata: DataSourceEntity,
manager: EntityManager, manager: EntityManager,
): Promise<void> { ): Promise<void> {
if (!participants) return; if (!participants) return;
for (const participant of participants) { for (const participant of participants) {
const participantPerson = await manager.query( 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], [participant.handle],
); );
@ -167,7 +158,8 @@ export class MessagingUtilsService {
const workspaceMember = await manager.query( const workspaceMember = await manager.query(
`SELECT "workspaceMember"."id" FROM ${dataSourceMetadata.schema}."workspaceMember" `SELECT "workspaceMember"."id" FROM ${dataSourceMetadata.schema}."workspaceMember"
JOIN ${dataSourceMetadata.schema}."connectedAccount" ON ${dataSourceMetadata.schema}."workspaceMember"."id" = ${dataSourceMetadata.schema}."connectedAccount"."accountOwnerId" 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], [participant.handle],
); );
@ -187,41 +179,16 @@ export class MessagingUtilsService {
} }
} }
public async getSavedMessageIdsAndThreadIds( public async deleteMessageChannelMessages(
messageEternalIds: string[], messageExternalIds: string[],
connectedAccountId: string, connectedAccountId: string,
dataSourceMetadata: DataSourceEntity, dataSourceMetadata: DataSourceEntity,
workspaceDataSource: DataSource, workspaceDataSource: DataSource,
): Promise<{ ) {
savedMessageIds: string[]; await workspaceDataSource?.query(
savedThreadIds: string[]; `DELETE FROM ${dataSourceMetadata.schema}."messageChannelMessage" WHERE "messageExternalId" = ANY($1) AND "messageChannelId" = $2`,
}> { [messageExternalIds, connectedAccountId],
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],
); );
return {
savedMessageIds: messageIdsInDatabase.map(
(message) => message.messageExternalId,
),
savedThreadIds: [
...new Set(
messageIdsInDatabase.map(
(message) => message.messageThreadExternalId,
),
),
],
};
} }
public async getConnectedAccountsFromWorkspaceId( public async getConnectedAccountsFromWorkspaceId(
@ -240,7 +207,7 @@ export class MessagingUtilsService {
} }
const connectedAccounts = await workspaceDataSource?.query( 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) { if (!connectedAccounts || connectedAccounts.length === 0) {
@ -271,7 +238,7 @@ export class MessagingUtilsService {
} }
const connectedAccounts = await workspaceDataSource?.query( 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], [connectedAccountId],
); );
@ -297,50 +264,4 @@ export class MessagingUtilsService {
[historyId, connectedAccountId], [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],
);
}
} }

View File

@ -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 { 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 { 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 { 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 { 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 { 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'; import { MessageThreadObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-thread.object-metadata';
@ -42,4 +43,5 @@ export const standardObjectMetadata = [
MessageObjectMetadata, MessageObjectMetadata,
MessageChannelObjectMetadata, MessageChannelObjectMetadata,
MessageParticipantObjectMetadata, MessageParticipantObjectMetadata,
MessageChannelMessageObjectMetadata,
]; ];

View File

@ -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;
}

View File

@ -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 { 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 { 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 { 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({ @ObjectMetadata({
namePlural: 'messageChannels', namePlural: 'messageChannels',
@ -23,12 +23,21 @@ import { MessageThreadObjectMetadata } from 'src/workspace/workspace-sync-metada
@IsSystem() @IsSystem()
export class MessageChannelObjectMetadata extends BaseObjectMetadata { export class MessageChannelObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({ @FieldMetadata({
// This will be a type select later: metadata, subject, share_everything type: FieldMetadataType.SELECT,
type: FieldMetadataType.TEXT,
label: 'Visibility', label: 'Visibility',
description: 'Visibility', description: 'Visibility',
icon: 'IconEyeglass', 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; visibility: string;
@ -50,24 +59,28 @@ export class MessageChannelObjectMetadata extends BaseObjectMetadata {
connectedAccount: ConnectedAccountObjectMetadata; connectedAccount: ConnectedAccountObjectMetadata;
@FieldMetadata({ @FieldMetadata({
// This will be a type select later : email, sms, chat type: FieldMetadataType.SELECT,
type: FieldMetadataType.TEXT,
label: 'Type', label: 'Type',
description: 'Type', description: 'Channel Type',
icon: 'IconMessage', icon: 'IconMessage',
options: [
{ value: 'email', label: 'Email', position: 0, color: 'green' },
{ value: 'sms', label: 'SMS', position: 1, color: 'blue' },
],
defaultValue: { value: 'email' },
}) })
type: string; type: string;
@FieldMetadata({ @FieldMetadata({
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
label: 'Message Threads', label: 'Message Channel Syncs',
description: 'Threads from the channel.', description: 'Messages from the channel.',
icon: 'IconMessage', icon: 'IconMessage',
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'messageThread', objectName: 'messageChannelMessage',
}) })
@IsNullable() @IsNullable()
messageThreads: MessageThreadObjectMetadata[]; messageChannelMessage: MessageChannelMessageObjectMetadata[];
} }

View File

@ -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 { ObjectMetadata } from 'src/workspace/workspace-sync-metadata/decorators/object-metadata.decorator';
import { RelationMetadata } from 'src/workspace/workspace-sync-metadata/decorators/relation-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 { 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'; import { MessageObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message.object-metadata';
@ObjectMetadata({ @ObjectMetadata({
@ -22,43 +22,6 @@ import { MessageObjectMetadata } from 'src/workspace/workspace-sync-metadata/sta
}) })
@IsSystem() @IsSystem()
export class MessageThreadObjectMetadata extends BaseObjectMetadata { 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',
icon: 'IconMessage',
})
subject: string;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Message Channel Id',
description: 'Message Channel Id',
icon: 'IconHash',
joinColumn: 'messageChannelId',
})
@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;
@FieldMetadata({ @FieldMetadata({
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
label: 'Messages', label: 'Messages',
@ -71,4 +34,17 @@ export class MessageThreadObjectMetadata extends BaseObjectMetadata {
}) })
@IsNullable() @IsNullable()
messages: MessageObjectMetadata[]; messages: MessageObjectMetadata[];
@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[];
} }

View File

@ -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 { ObjectMetadata } from 'src/workspace/workspace-sync-metadata/decorators/object-metadata.decorator';
import { RelationMetadata } from 'src/workspace/workspace-sync-metadata/decorators/relation-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 { 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 { 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'; 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() @IsSystem()
export class MessageObjectMetadata extends BaseObjectMetadata { 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({ @FieldMetadata({
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
label: 'Header message Id', label: 'Header message Id',
@ -50,11 +42,14 @@ export class MessageObjectMetadata extends BaseObjectMetadata {
messageThread: MessageThreadObjectMetadata; messageThread: MessageThreadObjectMetadata;
@FieldMetadata({ @FieldMetadata({
// will be a select later: incoming, outgoing type: FieldMetadataType.SELECT,
type: FieldMetadataType.TEXT,
label: 'Direction', label: 'Direction',
description: 'Direction', description: 'Message Direction',
icon: 'IconDirection', icon: 'IconDirection',
options: [
{ value: 'incoming', label: 'Incoming', position: 0, color: 'green' },
{ value: 'outgoing', label: 'Outgoing', position: 1, color: 'blue' },
],
defaultValue: { value: 'incoming' }, defaultValue: { value: 'incoming' },
}) })
direction: string; direction: string;
@ -97,4 +92,17 @@ export class MessageObjectMetadata extends BaseObjectMetadata {
}) })
@IsNullable() @IsNullable()
messageParticipants: MessageParticipantObjectMetadata[]; 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[];
} }