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,
subscriptions: {
'graphql-ws': {
onSubscribe(ctx, message) {
console.log('onSubscribe', ctx, message)
},
onConnect: (ctx: Context<Record<string, unknown>>) => {
if (!ctx.connectionParams) {
Logger.log('No connectionParams provided', 'GraphqlModule')

View File

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

File diff suppressed because one or more lines are too long