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, ) { super() } @PothosRef() message() { return this.builder.prismaObject('Message', { description: 'A message in the system.', fields: (t) => ({ id: t.exposeID('id', { description: 'The ID of the message.', }), senderId: t.exposeID('senderId', { description: 'The ID of the sender.', }), chatRoomId: t.exposeID('chatRoomId', { description: 'The ID of the chat room.', }), type: t.expose('type', { type: MessageType, description: 'The type of the message.', }), content: t.exposeString('content', { description: 'The message content.', }), sentAt: t.expose('sentAt', { type: 'DateTime', description: 'The date and time the message was sent.', }), context: t.expose('context', { type: MessageContextType, description: 'The context of the message.', }), metadata: t.expose('metadata', { type: 'Json', nullable: true, description: 'The metadata of the message.', }), sender: t.relation('sender', { description: 'The sender of the message.', }), chatRoom: t.relation('chatRoom', { description: 'The chat room.', }), }), }) } @Pothos() init(): void { 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'), 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'), resolve: async (query, _root, args, ctx, _info) => { if (ctx.isSubscription) { throw new Error('Not allowed') } 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.recipientId = ctx.http.me?.id } } return await this.prisma.message.findMany({ ...query, skip: args.skip ?? undefined, 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'), 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.', args: { input: t.arg({ type: this.builder.generator.getCreateInput('Message', [ 'id', 'senderId', 'sender', 'sentAt', 'context', 'recipient', 'recipientId', ]), description: 'The message to send.', required: true, }), }, resolve: async (query, _root, args, ctx, _info) => { if (ctx.isSubscription) { throw new Error('Not allowed') } 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') } 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 if (!chatRoomId) { 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 const chatRoom = await this.prisma.chatRoom.findUnique({ where: { id: chatRoomId, }, }) if (chatRoom?.type === ChatRoomType.SUPPORT) { 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') } const lastActivity = DateTimeUtils.now() const message = await this.prisma.$transaction(async (tx) => { const message = await tx.message.create({ ...query, data: { ...args.input, context: MessageContextType.CHAT, }, }) await tx.chatRoom.update({ where: { id: message.chatRoomId!, }, data: { lastActivity: lastActivity.toJSDate(), }, }) 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 }, }), })) this.builder.subscriptionFields((t) => ({ messageSent: t.field({ 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.', }), }, subscribe: (_, args, ctx: SchemaContext) => { if (!ctx.isSubscription) { throw new Error('Not allowed') } return ctx.websocket.pubSub.asyncIterator([ `${PubSubEvent.MESSAGE_SENT}.${args.chatRoomId}`, ]) as unknown as AsyncIterable }, resolve: (payload: Message) => payload, }), })) } }