3263 modify timeline messagingservice to allow the frontend to get multiple participants in a thread (#3611)

* wip

* wip

* add pagination

* wip

* wip

* wip

* update resolver

* wip

* wip

* endpoint is working but there is still work to do

* merge main

* wip

* subject is now first subject

* number of messages is working

* improving query

* fix bug

* fix bug

* added parameter

* pagination introduced a bug

* pagination is working

* fix type

* improve typing

* improve typing

* fix bug

* add displayName

* display displayName in the frontend

* move entities

* fix

* generate metadata

* add avatarUrl

* modify after comments on PR

* updates

* remove email mocks

* remove console log

* move files

* remove mock

* use constant

* use constant

* use fragments

* remove console.log

* generate

* changes made

* update DTO

* generate
This commit is contained in:
bosiraphael
2024-01-25 17:04:51 +01:00
committed by GitHub
parent 6f98d1847f
commit 6004969096
19 changed files with 617 additions and 207 deletions

View File

@ -0,0 +1,2 @@
export const TIMELINE_THREADS_DEFAULT_PAGE_SIZE = 20;
export const TIMELINE_THREADS_MAX_PAGE_SIZE = 50;

View File

@ -0,0 +1,25 @@
import { ObjectType, Field, ID } from '@nestjs/graphql';
@ObjectType('TimelineThreadParticipant')
export class TimelineThreadParticipant {
@Field(() => ID, { nullable: true })
personId: string;
@Field(() => ID, { nullable: true })
workspaceMemberId: string;
@Field()
firstName: string;
@Field()
lastName: string;
@Field()
displayName: string;
@Field()
avatarUrl: string;
@Field()
handle: string;
}

View File

@ -0,0 +1,35 @@
import { ObjectType, Field, ID } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { TimelineThreadParticipant } from 'src/core/messaging/dtos/timeline-thread-participant.dto';
@ObjectType('TimelineThread')
export class TimelineThread {
@IDField(() => ID)
id: string;
@Field()
read: boolean;
@Field()
firstParticipant: TimelineThreadParticipant;
@Field(() => [TimelineThreadParticipant])
lastTwoParticipants: TimelineThreadParticipant[];
@Field()
lastMessageReceivedAt: Date;
@Field()
lastMessageBody: string;
@Field()
subject: string;
@Field()
numberOfMessagesInThread: number;
@Field()
participantCount: number;
}

View File

@ -1,43 +1,47 @@
import { Args, Query, Field, Resolver, ObjectType } from '@nestjs/graphql';
import {
Args,
Query,
Resolver,
Int,
ArgsType,
Field,
ID,
} from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { Column, Entity } from 'typeorm';
import { Max } from 'class-validator';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { TimelineMessagingService } from 'src/core/messaging/timeline-messaging.service';
import { TimelineThread } from 'src/core/messaging/dtos/timeline-thread.dto';
import { TIMELINE_THREADS_MAX_PAGE_SIZE } from 'src/core/messaging/constants/messaging.constants';
@Entity({ name: 'timelineThread', schema: 'core' })
@ObjectType('TimelineThread')
export class TimelineThread {
@Field()
@Column()
read: boolean;
@ArgsType()
class GetTimelineThreadsFromPersonIdArgs {
@Field(() => ID)
personId: string;
@Field()
@Column()
senderName: string;
@Field(() => Int)
page: number;
@Field()
@Column()
senderPictureUrl: string;
@Field(() => Int)
@Max(TIMELINE_THREADS_MAX_PAGE_SIZE)
pageSize: number;
}
@Field()
@Column()
numberOfMessagesInThread: number;
@ArgsType()
class GetTimelineThreadsFromCompanyIdArgs {
@Field(() => ID)
companyId: string;
@Field()
@Column()
subject: string;
@Field(() => Int)
page: number;
@Field()
@Column()
body: string;
@Field()
@Column()
receivedAt: Date;
@Field(() => Int)
@Max(TIMELINE_THREADS_MAX_PAGE_SIZE)
pageSize: number;
}
@UseGuards(JwtAuthGuard)
@ -50,12 +54,14 @@ export class TimelineMessagingResolver {
@Query(() => [TimelineThread])
async getTimelineThreadsFromPersonId(
@AuthWorkspace() { id: workspaceId }: Workspace,
@Args('personId') personId: string,
@Args() { personId, page, pageSize }: GetTimelineThreadsFromPersonIdArgs,
) {
const timelineThreads =
await this.timelineMessagingService.getMessagesFromPersonIds(
workspaceId,
[personId],
page,
pageSize,
);
return timelineThreads;
@ -64,12 +70,14 @@ export class TimelineMessagingResolver {
@Query(() => [TimelineThread])
async getTimelineThreadsFromCompanyId(
@AuthWorkspace() { id: workspaceId }: Workspace,
@Args('companyId') companyId: string,
@Args() { companyId, page, pageSize }: GetTimelineThreadsFromCompanyIdArgs,
) {
const timelineThreads =
await this.timelineMessagingService.getMessagesFromCompanyId(
workspaceId,
companyId,
page,
pageSize,
);
return timelineThreads;

View File

@ -1,9 +1,20 @@
import { Injectable } from '@nestjs/common';
import { TimelineThread } from 'src/core/messaging/timeline-messaging.resolver';
import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from 'src/core/messaging/constants/messaging.constants';
import { TimelineThread } from 'src/core/messaging/dtos/timeline-thread.dto';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
type TimelineThreadParticipant = {
personId: string;
workspaceMemberId: string;
firstName: string;
lastName: string;
displayName: string;
avatarUrl: string;
handle: string;
};
@Injectable()
export class TimelineMessagingService {
constructor(
@ -14,7 +25,11 @@ export class TimelineMessagingService {
async getMessagesFromPersonIds(
workspaceId: string,
personIds: string[],
page: number = 1,
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
): Promise<TimelineThread[]> {
const offset = (page - 1) * TIMELINE_THREADS_DEFAULT_PAGE_SIZE;
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId,
@ -23,61 +38,327 @@ export class TimelineMessagingService {
const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSourceMetadata);
// 10 first threads This hard limit is just for the POC, we will implement pagination later
const messageThreads = await workspaceDataSource?.query(
const messageThreads:
| {
id: string;
lastMessageReceivedAt: Date;
lastMessageId: string;
lastMessageBody: string;
rowNumber: number;
}[]
| undefined = await workspaceDataSource?.query(
`
SELECT
subquery.*,
message_count,
last_message_subject,
last_message_text,
last_message_received_at,
last_message_participant_handle,
last_message_participant_displayName
FROM (
SELECT
mt.*,
COUNT(m."id") OVER (PARTITION BY mt."id") AS message_count,
FIRST_VALUE(m."subject") OVER (PARTITION BY mt."id" ORDER BY m."receivedAt" DESC) AS last_message_subject,
FIRST_VALUE(m."text") OVER (PARTITION BY mt."id" ORDER BY m."receivedAt" DESC) AS last_message_text,
FIRST_VALUE(m."receivedAt") OVER (PARTITION BY mt."id" ORDER BY m."receivedAt" DESC) AS last_message_received_at,
FIRST_VALUE(mr."handle") OVER (PARTITION BY mt."id" ORDER BY m."receivedAt" DESC) AS last_message_participant_handle,
FIRST_VALUE(mr."displayName") OVER (PARTITION BY mt."id" ORDER BY m."receivedAt" DESC) AS last_message_participant_displayName,
ROW_NUMBER() OVER (PARTITION BY mt."id" ORDER BY m."receivedAt" DESC) AS rn
FROM
${dataSourceMetadata.schema}."messageThread" mt
LEFT JOIN
${dataSourceMetadata.schema}."message" m ON mt."id" = m."messageThreadId"
LEFT JOIN
${dataSourceMetadata.schema}."messageParticipant" mr ON m."id" = mr."messageId"
WHERE
mr."personId" IN (SELECT unnest($1::uuid[]))
) AS subquery
WHERE
subquery.rn = 1
ORDER BY
subquery.last_message_received_at DESC
LIMIT 10;
`,
[personIds],
SELECT *
FROM
(SELECT "messageThread".id,
MAX(message."receivedAt") AS "lastMessageReceivedAt",
message.id AS "lastMessageId",
message.text AS "lastMessageBody",
ROW_NUMBER() OVER (PARTITION BY "messageThread".id ORDER BY MAX(message."receivedAt") DESC) AS "rowNumber"
FROM
${dataSourceMetadata.schema}."message" message
LEFT JOIN
${dataSourceMetadata.schema}."messageThread" "messageThread" ON "messageThread".id = message."messageThreadId"
LEFT JOIN
${dataSourceMetadata.schema}."messageParticipant" "messageParticipant" ON "messageParticipant"."messageId" = message.id
LEFT JOIN
${dataSourceMetadata.schema}."person" person ON person.id = "messageParticipant"."personId"
LEFT JOIN
${dataSourceMetadata.schema}."workspaceMember" "workspaceMember" ON "workspaceMember".id = "messageParticipant"."workspaceMemberId"
WHERE
person.id = ANY($1)
GROUP BY
"messageThread".id,
message.id
ORDER BY
message."receivedAt" DESC
) AS "messageThreads"
WHERE
"rowNumber" = 1
LIMIT $2
OFFSET $3
`,
[personIds, pageSize, offset],
);
const formattedMessageThreads = messageThreads.map((messageThread) => {
if (!messageThreads) {
return [];
}
const messageThreadIds = messageThreads.map(
(messageThread) => messageThread.id,
);
const threadSubjects:
| {
id: string;
subject: string;
}[]
| undefined = await workspaceDataSource?.query(
`
SELECT *
FROM
(SELECT
"messageThread".id,
message.subject,
ROW_NUMBER() OVER (PARTITION BY "messageThread".id ORDER BY MAX(message."receivedAt") ASC) AS "rowNumber"
FROM
${dataSourceMetadata.schema}."message" message
LEFT JOIN
${dataSourceMetadata.schema}."messageThread" "messageThread" ON "messageThread".id = message."messageThreadId"
WHERE
"messageThread".id = ANY($1)
GROUP BY
"messageThread".id,
message.id
ORDER BY
message."receivedAt" DESC
) AS "messageThreads"
WHERE
"rowNumber" = 1
`,
[messageThreadIds],
);
const numberOfMessagesInThread:
| {
id: string;
numberOfMessagesInThread: number;
}[]
| undefined = await workspaceDataSource?.query(
`
SELECT
"messageThread".id,
COUNT(message.id) AS "numberOfMessagesInThread"
FROM
${dataSourceMetadata.schema}."message" message
LEFT JOIN
${dataSourceMetadata.schema}."messageThread" "messageThread" ON "messageThread".id = message."messageThreadId"
WHERE
"messageThread".id = ANY($1)
GROUP BY
"messageThread".id
`,
[messageThreadIds],
);
const messageThreadsByMessageThreadId: {
[key: string]: {
id: string;
lastMessageReceivedAt: Date;
lastMessageBody: string;
};
} = messageThreads.reduce((messageThreadAcc, messageThread) => {
messageThreadAcc[messageThread.id] = messageThread;
return messageThreadAcc;
}, {});
const subjectsByMessageThreadId:
| {
[key: string]: {
id: string;
subject: string;
};
}
| undefined = threadSubjects?.reduce(
(threadSubjectAcc, threadSubject) => {
threadSubjectAcc[threadSubject.id] = threadSubject;
return threadSubjectAcc;
},
{},
);
const numberOfMessagesByMessageThreadId:
| {
[key: string]: {
id: string;
numberOfMessagesInThread: number;
};
}
| undefined = numberOfMessagesInThread?.reduce(
(numberOfMessagesAcc, numberOfMessages) => {
numberOfMessagesAcc[numberOfMessages.id] = numberOfMessages;
return numberOfMessagesAcc;
},
{},
);
const threadMessagesFromActiveParticipants:
| {
id: string;
messageId: string;
receivedAt: Date;
body: string;
subject: string;
personId: string;
workspaceMemberId: string;
handle: string;
personFirstName: string;
personLastName: string;
personAvatarUrl: string;
workspaceMemberFirstName: string;
workspaceMemberLastName: string;
workspaceMemberAvatarUrl: string;
messageDisplayName: string;
}[]
| undefined = await workspaceDataSource?.query(
`
SELECT DISTINCT "messageThread".id,
message.id AS "messageId",
message."receivedAt",
message.text,
message."subject",
"messageParticipant"."personId",
"messageParticipant"."workspaceMemberId",
"messageParticipant".handle,
"person"."nameFirstName" as "personFirstName",
"person"."nameLastName" as "personLastName",
"person"."avatarUrl" as "personAvatarUrl",
"workspaceMember"."nameFirstName" as "workspaceMemberFirstName",
"workspaceMember"."nameLastName" as "workspaceMemberLastName",
"workspaceMember"."avatarUrl" as "workspaceMemberAvatarUrl",
"messageParticipant"."displayName" as "messageDisplayName"
FROM
${dataSourceMetadata.schema}."message" message
LEFT JOIN
${dataSourceMetadata.schema}."messageThread" "messageThread" ON "messageThread".id = message."messageThreadId"
LEFT JOIN
(SELECT * FROM ${dataSourceMetadata.schema}."messageParticipant" WHERE "messageParticipant".role = 'from') "messageParticipant" ON "messageParticipant"."messageId" = message.id
LEFT JOIN
${dataSourceMetadata.schema}."person" person ON person."id" = "messageParticipant"."personId"
LEFT JOIN
${dataSourceMetadata.schema}."workspaceMember" "workspaceMember" ON "workspaceMember".id = "messageParticipant"."workspaceMemberId"
WHERE
"messageThread".id = ANY($1)
ORDER BY
message."receivedAt" DESC
`,
[messageThreadIds],
);
const threadParticipantsByThreadId: {
[key: string]: TimelineThreadParticipant[];
} = messageThreadIds.reduce((messageThreadIdAcc, messageThreadId) => {
const threadMessages = threadMessagesFromActiveParticipants?.filter(
(threadMessage) => threadMessage.id === messageThreadId,
);
const threadParticipants = threadMessages?.reduce(
(
threadMessageAcc,
threadMessage,
): {
[key: string]: TimelineThreadParticipant;
} => {
const threadParticipant = threadMessageAcc[threadMessage.handle];
const firstName =
threadMessage.personFirstName ||
threadMessage.workspaceMemberFirstName ||
'';
const lastName =
threadMessage.personLastName ||
threadMessage.workspaceMemberLastName ||
'';
const displayName =
firstName ||
threadMessage.messageDisplayName ||
threadMessage.handle;
if (!threadParticipant) {
threadMessageAcc[threadMessage.handle] = {
personId: threadMessage.personId,
workspaceMemberId: threadMessage.workspaceMemberId,
firstName,
lastName,
displayName,
avatarUrl:
threadMessage.personAvatarUrl ??
threadMessage.workspaceMemberAvatarUrl ??
'',
handle: threadMessage.handle,
};
}
return threadMessageAcc;
},
{},
);
messageThreadIdAcc[messageThreadId] = threadParticipants
? Object.values(threadParticipants)
: [];
return messageThreadIdAcc;
}, {});
const timelineThreads = messageThreadIds.map((messageThreadId) => {
const threadParticipants = threadParticipantsByThreadId[messageThreadId];
const firstParticipant = threadParticipants[0];
const threadParticipantsWithoutFirstParticipant =
threadParticipants.filter(
(threadParticipant) =>
threadParticipant.handle !== firstParticipant.handle,
);
const lastTwoParticipants: TimelineThreadParticipant[] = [];
const lastParticipant =
threadParticipantsWithoutFirstParticipant.slice(-1)[0];
if (lastParticipant) {
lastTwoParticipants.push(lastParticipant);
const threadParticipantsWithoutFirstAndLastParticipants =
threadParticipantsWithoutFirstParticipant.filter(
(threadParticipant) =>
threadParticipant.handle !== lastParticipant.handle,
);
if (threadParticipantsWithoutFirstAndLastParticipants.length > 0)
lastTwoParticipants.push(
threadParticipantsWithoutFirstAndLastParticipants.slice(-1)[0],
);
}
const thread = messageThreadsByMessageThreadId[messageThreadId];
const threadSubject =
subjectsByMessageThreadId?.[messageThreadId].subject ?? '';
const numberOfMessages =
numberOfMessagesByMessageThreadId?.[messageThreadId]
.numberOfMessagesInThread ?? 1;
return {
id: messageThreadId,
read: true,
senderName: messageThread.last_message_participant_handle,
senderPictureUrl: '',
numberOfMessagesInThread: messageThread.message_count,
subject: messageThread.last_message_subject,
body: messageThread.last_message_text,
receivedAt: messageThread.last_message_received_at,
firstParticipant,
lastTwoParticipants,
lastMessageReceivedAt: thread.lastMessageReceivedAt,
lastMessageBody: thread.lastMessageBody,
subject: threadSubject,
numberOfMessagesInThread: numberOfMessages,
participantCount: threadParticipants.length,
};
});
return formattedMessageThreads;
return timelineThreads;
}
async getMessagesFromCompanyId(workspaceId: string, companyId: string) {
async getMessagesFromCompanyId(
workspaceId: string,
companyId: string,
page: number = 1,
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
) {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId,
@ -102,11 +383,15 @@ export class TimelineMessagingService {
return [];
}
const formattedPersonIds = personIds.map((personId) => personId.id);
const formattedPersonIds = personIds.map(
(personId: { id: string }) => personId.id,
);
const messageThreads = await this.getMessagesFromPersonIds(
workspaceId,
formattedPersonIds,
page,
pageSize,
);
return messageThreads;