chore: update subproject commit and enhance GraphQL message schema

- Updated the subproject commit reference in epess-database.
- Added logging functionality in the GraphQL module for subscription handling to improve debugging.
- Refactored the MessageSchema to standardize formatting, enhance readability, and add a new field `isRead` to track message status.
- Improved error handling and validation in message-related operations, ensuring better user experience and data integrity.
- Updated the generated types in pothos.generated.ts to reflect the latest schema changes.

These changes aim to improve code maintainability, enhance debugging capabilities, and ensure a more robust message handling process.
This commit is contained in:
2024-12-20 12:41:00 +07:00
parent be35684ba2
commit 461f2653e3
4 changed files with 115 additions and 88 deletions

View File

@@ -112,6 +112,9 @@ import { GraphqlService } from './graphql.service'
installSubscriptionHandlers: true, installSubscriptionHandlers: true,
subscriptions: { subscriptions: {
'graphql-ws': { 'graphql-ws': {
onSubscribe(ctx, message) {
console.log('onSubscribe', ctx, message)
},
onConnect: (ctx: Context<Record<string, unknown>>) => { onConnect: (ctx: Context<Record<string, unknown>>) => {
if (!ctx.connectionParams) { if (!ctx.connectionParams) {
Logger.log('No connectionParams provided', 'GraphqlModule') Logger.log('No connectionParams provided', 'GraphqlModule')

View File

@@ -1,62 +1,75 @@
import { Inject, Injectable, Logger } from '@nestjs/common' import { Inject, Injectable, Logger } from "@nestjs/common";
import { ChatRoomType, Message, MessageContextType, MessageType } from '@prisma/client' import {
import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos' ChatRoomType,
import { Builder, SchemaContext } from '../Graphql/graphql.builder' Message,
import { PrismaService } from '../Prisma/prisma.service' MessageContextType,
import { PubSubEvent } from '../common/pubsub/pubsub-event' MessageType,
import { DateTimeUtils } from '../common/utils/datetime.utils' } from "@prisma/client";
import {
Pothos,
PothosRef,
PothosSchema,
SchemaBuilderToken,
} from "@smatch-corp/nestjs-pothos";
import { Builder, SchemaContext } from "../Graphql/graphql.builder";
import { PrismaService } from "../Prisma/prisma.service";
import { PubSubEvent } from "../common/pubsub/pubsub-event";
import { DateTimeUtils } from "../common/utils/datetime.utils";
@Injectable() @Injectable()
export class MessageSchema extends PothosSchema { export class MessageSchema extends PothosSchema {
constructor( constructor(
@Inject(SchemaBuilderToken) private readonly builder: Builder, @Inject(SchemaBuilderToken) private readonly builder: Builder,
private readonly prisma: PrismaService, private readonly prisma: PrismaService
) { ) {
super() super();
} }
@PothosRef() @PothosRef()
message() { message() {
return this.builder.prismaObject('Message', { return this.builder.prismaObject("Message", {
description: 'A message in the system.', description: "A message in the system.",
fields: (t) => ({ fields: (t) => ({
id: t.exposeID('id', { id: t.exposeID("id", {
description: 'The ID of the message.', description: "The ID of the message.",
}), }),
senderId: t.exposeID('senderId', { senderId: t.exposeID("senderId", {
description: 'The ID of the sender.', description: "The ID of the sender.",
}), }),
chatRoomId: t.exposeID('chatRoomId', { chatRoomId: t.exposeID("chatRoomId", {
description: 'The ID of the chat room.', description: "The ID of the chat room.",
}), }),
type: t.expose('type', { type: t.expose("type", {
type: MessageType, type: MessageType,
description: 'The type of the message.', description: "The type of the message.",
}), }),
content: t.exposeString('content', { content: t.exposeString("content", {
description: 'The message content.', description: "The message content.",
}), }),
sentAt: t.expose('sentAt', { sentAt: t.expose("sentAt", {
type: 'DateTime', type: "DateTime",
description: 'The date and time the message was sent.', description: "The date and time the message was sent.",
}), }),
context: t.expose('context', { context: t.expose("context", {
type: MessageContextType, type: MessageContextType,
description: 'The context of the message.', description: "The context of the message.",
}), }),
metadata: t.expose('metadata', { metadata: t.expose("metadata", {
type: 'Json', type: "Json",
nullable: true, nullable: true,
description: 'The metadata of the message.', description: "The metadata of the message.",
}), }),
sender: t.relation('sender', { sender: t.relation("sender", {
description: 'The sender of the message.', description: "The sender of the message.",
}), }),
chatRoom: t.relation('chatRoom', { chatRoom: t.relation("chatRoom", {
description: 'The chat room.', description: "The chat room.",
}),
isRead: t.exposeBoolean("isRead", {
description: "Whether the message has been read.",
}), }),
}), }),
}) });
} }
@Pothos() @Pothos()
@@ -64,30 +77,35 @@ export class MessageSchema extends PothosSchema {
this.builder.queryFields((t) => ({ this.builder.queryFields((t) => ({
message: t.prismaField({ message: t.prismaField({
type: this.message(), type: this.message(),
description: 'Retrieve a single message by its unique identifier.', description: "Retrieve a single message by its unique identifier.",
args: this.builder.generator.findUniqueArgs('Message'), args: this.builder.generator.findUniqueArgs("Message"),
resolve: async (query, _root, args) => { resolve: async (query, _root, args) => {
return await this.prisma.message.findUnique({ return await this.prisma.message.findUnique({
...query, ...query,
where: args.where, where: args.where,
}) });
}, },
}), }),
messages: t.prismaField({ messages: t.prismaField({
type: [this.message()], type: [this.message()],
description: 'Retrieve a list of messages with optional filtering, ordering, and pagination.', description:
args: this.builder.generator.findManyArgs('Message'), "Retrieve a list of messages with optional filtering, ordering, and pagination.",
args: this.builder.generator.findManyArgs("Message"),
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (ctx.isSubscription) {
throw new Error('Not allowed') throw new Error("Not allowed");
} }
if (args.filter?.context && typeof args.filter.context === 'object') { if (args.filter?.context && typeof args.filter.context === "object") {
// if args.context is NOTIFICATION or SYSTEM, filter by recipientId // if args.context is NOTIFICATION or SYSTEM, filter by recipientId
if ( if (
args.filter.context.in?.toString().includes(MessageContextType.NOTIFICATION) || args.filter.context.in
args.filter.context.in?.toString().includes(MessageContextType.SYSTEM) ?.toString()
.includes(MessageContextType.NOTIFICATION) ||
args.filter.context.in
?.toString()
.includes(MessageContextType.SYSTEM)
) { ) {
args.filter.recipientId = ctx.http.me?.id args.filter.recipientId = ctx.http.me?.id;
} }
} }
return await this.prisma.message.findMany({ return await this.prisma.message.findMany({
@@ -96,63 +114,63 @@ export class MessageSchema extends PothosSchema {
take: args.take ?? undefined, take: args.take ?? undefined,
orderBy: args.orderBy ?? undefined, orderBy: args.orderBy ?? undefined,
where: args.filter ?? undefined, where: args.filter ?? undefined,
}) });
}, },
}), }),
messagesByChatRoomId: t.prismaField({ messagesByChatRoomId: t.prismaField({
type: [this.message()], type: [this.message()],
description: 'Retrieve a list of messages by chat room ID.', description: "Retrieve a list of messages by chat room ID.",
args: this.builder.generator.findManyArgs('Message'), args: this.builder.generator.findManyArgs("Message"),
resolve: async (query, _root, args) => { resolve: async (query, _root, args) => {
return await this.prisma.message.findMany({ return await this.prisma.message.findMany({
...query, ...query,
where: args.filter ?? undefined, where: args.filter ?? undefined,
}) });
}, },
}), }),
})) }));
// mutations // mutations
this.builder.mutationFields((t) => ({ this.builder.mutationFields((t) => ({
sendMessage: t.prismaField({ sendMessage: t.prismaField({
type: this.message(), type: this.message(),
description: 'Send a message to a chat room.', description: "Send a message to a chat room.",
args: { args: {
input: t.arg({ input: t.arg({
type: this.builder.generator.getCreateInput('Message', [ type: this.builder.generator.getCreateInput("Message", [
'id', "id",
'senderId', "senderId",
'sender', "sender",
'sentAt', "sentAt",
'context', "context",
'recipient', "recipient",
'recipientId', "recipientId",
]), ]),
description: 'The message to send.', description: "The message to send.",
required: true, required: true,
}), }),
}, },
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (ctx.isSubscription) {
throw new Error('Not allowed') throw new Error("Not allowed");
} }
const messageContext = MessageContextType.CHAT const messageContext = MessageContextType.CHAT;
// get the sender from the context and add it to the input // get the sender from the context and add it to the input
args.input.sender = { args.input.sender = {
connect: { connect: {
id: ctx.http.me?.id, id: ctx.http.me?.id,
}, },
} };
if (!args.input.sender) { if (!args.input.sender) {
throw new Error('Cannot get sender from context') throw new Error("Cannot get sender from context");
} }
let userIds: string[] = [] let userIds: string[] = [];
// get the recipient if messageContext is CHAT // get the recipient if messageContext is CHAT
if (messageContext === MessageContextType.CHAT) { if (messageContext === MessageContextType.CHAT) {
// get chatRoomId from input // get chatRoomId from input
const chatRoomId = args.input.chatRoom?.connect?.id const chatRoomId = args.input.chatRoom?.connect?.id;
if (!chatRoomId) { if (!chatRoomId) {
throw new Error('Cannot get chatRoomId from input') throw new Error("Cannot get chatRoomId from input");
} }
// if chatroom type is SUPPORT, user 1 is mentorId, user 2 is customerId // if chatroom type is SUPPORT, user 1 is mentorId, user 2 is customerId
// query the chatRoom to get the userIds // query the chatRoom to get the userIds
@@ -160,16 +178,16 @@ export class MessageSchema extends PothosSchema {
where: { where: {
id: chatRoomId, id: chatRoomId,
}, },
}) });
if (chatRoom?.type === ChatRoomType.SUPPORT) { if (chatRoom?.type === ChatRoomType.SUPPORT) {
userIds = [chatRoom.mentorId!, chatRoom.customerId!] userIds = [chatRoom.mentorId!, chatRoom.customerId!];
} }
} }
// check if content is empty // check if content is empty
if (!args.input.content || args.input.content.trim() === '') { if (!args.input.content || args.input.content.trim() === "") {
throw new Error('Content cannot be empty') throw new Error("Content cannot be empty");
} }
const lastActivity = DateTimeUtils.now() const lastActivity = DateTimeUtils.now();
const message = await this.prisma.$transaction(async (tx) => { const message = await this.prisma.$transaction(async (tx) => {
const message = await tx.message.create({ const message = await tx.message.create({
...query, ...query,
@@ -177,7 +195,7 @@ export class MessageSchema extends PothosSchema {
...args.input, ...args.input,
context: MessageContextType.CHAT, context: MessageContextType.CHAT,
}, },
}) });
await tx.chatRoom.update({ await tx.chatRoom.update({
where: { where: {
id: message.chatRoomId!, id: message.chatRoomId!,
@@ -185,39 +203,45 @@ export class MessageSchema extends PothosSchema {
data: { data: {
lastActivity: lastActivity.toJSDate(), lastActivity: lastActivity.toJSDate(),
}, },
}) });
return message return message;
}) });
ctx.http.pubSub.publish(`${PubSubEvent.MESSAGE_SENT}.${message.chatRoomId}`, message) ctx.http.pubSub.publish(
`${PubSubEvent.MESSAGE_SENT}.${message.chatRoomId}`,
message
);
// publish to new message subscribers // publish to new message subscribers
userIds.forEach((userId: string) => { userIds.forEach((userId: string) => {
ctx.http.pubSub.publish(`${PubSubEvent.NEW_MESSAGE}.${userId}`, message) ctx.http.pubSub.publish(
}) `${PubSubEvent.NEW_MESSAGE}.${userId}`,
return message message
);
});
return message;
}, },
}), }),
})) }));
this.builder.subscriptionFields((t) => ({ this.builder.subscriptionFields((t) => ({
messageSent: t.field({ messageSent: t.field({
description: 'Subscribe to messages sent by users.', description: "Subscribe to messages sent by users.",
type: this.message(), type: this.message(),
args: { args: {
chatRoomId: t.arg({ chatRoomId: t.arg({
type: 'String', type: "String",
description: 'The ID of the chat room to subscribe to.', description: "The ID of the chat room to subscribe to.",
}), }),
}, },
subscribe: (_, args, ctx: SchemaContext) => { subscribe: (_, args, ctx: SchemaContext) => {
if (!ctx.isSubscription) { if (!ctx.isSubscription) {
throw new Error('Not allowed') throw new Error("Not allowed");
} }
return ctx.websocket.pubSub.asyncIterator([ return ctx.websocket.pubSub.asyncIterator([
`${PubSubEvent.MESSAGE_SENT}.${args.chatRoomId}`, `${PubSubEvent.MESSAGE_SENT}.${args.chatRoomId}`,
]) as unknown as AsyncIterable<Message> ]) as unknown as AsyncIterable<Message>;
}, },
resolve: (payload: Message) => payload, resolve: (payload: Message) => payload,
}), }),
})) }));
} }
} }

File diff suppressed because one or more lines are too long