From a97aab0e2fe9dd5ef1f3d7acbb35f4ff32035c93 Mon Sep 17 00:00:00 2001 From: Bertin M Date: Wed, 11 Sep 2024 23:16:42 +0200 Subject: [PATCH] CRUD Operations for tickets --- package-lock.json | 7 - src/resolvers/ticket.resolver.ts | 364 +++++++++++++++---------------- src/schema/ticket.shema.ts | 10 + src/seeders/ticket.seed.ts | 34 ++- 4 files changed, 220 insertions(+), 195 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4def3734..c932564d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2389,17 +2389,13 @@ "dependencies": { "@smithy/middleware-endpoint": "^3.1.0", "@smithy/middleware-retry": "^3.0.15", - "@smithy/middleware-retry": "^3.0.15", "@smithy/middleware-serde": "^3.0.3", "@smithy/protocol-http": "^4.1.0", "@smithy/smithy-client": "^3.2.0", - "@smithy/smithy-client": "^3.2.0", "@smithy/types": "^3.3.0", "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-middleware": "^3.0.3", "@smithy/util-utf8": "^3.0.0", - "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { @@ -2514,7 +2510,6 @@ "@smithy/protocol-http": "^4.1.0", "@smithy/service-error-classification": "^3.0.3", "@smithy/smithy-client": "^3.2.0", - "@smithy/smithy-client": "^3.2.0", "@smithy/types": "^3.3.0", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", @@ -2690,7 +2685,6 @@ "@smithy/protocol-http": "^4.1.0", "@smithy/types": "^3.3.0", "@smithy/util-stream": "^3.1.3", - "@smithy/util-stream": "^3.1.3", "tslib": "^2.6.2" }, "engines": { @@ -2873,7 +2867,6 @@ "integrity": "sha512-FIv/bRhIlAxC0U7xM1BCnF2aDRPq0UaelqBHkM2lsCp26mcBbgI0tCVTv+jGdsQLUmAMybua/bjDsSu8RQHbmw==", "optional": true, "dependencies": { - "@smithy/fetch-http-handler": "^3.2.4", "@smithy/fetch-http-handler": "^3.2.4", "@smithy/node-http-handler": "^3.1.4", "@smithy/types": "^3.3.0", diff --git a/src/resolvers/ticket.resolver.ts b/src/resolvers/ticket.resolver.ts index 6b9e09be..3c97ab2d 100644 --- a/src/resolvers/ticket.resolver.ts +++ b/src/resolvers/ticket.resolver.ts @@ -3,11 +3,13 @@ import Ticket from '../models/ticket.model' import { Context } from '../context' import { checkUserLoggedIn } from '../helpers/user.helpers' import { User } from '../models/user' -import { Notification } from '../models/notification.model' import { PubSub } from 'graphql-subscriptions' +import { pushNotification } from '../utils/notification/pushNotification' + const pubsub = new PubSub() export type TicketType = InstanceType +export type UserType = InstanceType type Reply = { ticketId: string @@ -15,81 +17,51 @@ type Reply = { } async function createReply( - ticket: any, - user: any, + ticket: TicketType, + user: UserType, context: Context, - replyMessage: string, - pubsub: any + replyMessage: string ) { - // Replyier is SuperAdmin sender: context.userId, receiver: user - if (context?.role === 'superAdmin') { - const reply = { + try { + const isSuperAdmin = context?.role === 'superAdmin' + const reply: any = { sender: context?.userId, - receiver: user?.toString(), + receiver: isSuperAdmin + ? user?._id.toString() + : (await User.findOne({ role: 'superAdmin' }))?._id.toString(), replyMessage, } ticket.replies.push(reply) - ticket.status = 'admin-reply' + ticket.status = isSuperAdmin ? 'admin-reply' : 'customer-reply' await ticket.save({ validateBeforeSave: false }) const ticketReply = ticket.replies[ticket.replies.length - 1] - const sender = await User.findById(ticketReply.sender.toString()) + const sender: any = await User.findById(ticketReply.sender.toString()) .populate('profile') .select('email profile id role organization') - const notification: any = await Notification.create({ - receiver: user?.toString(), - message: `A reply on ticket has been sent. ${ticket._id}`, - sender: context?.userId, - read: false, - createdAt: new Date(), - }) - - notification.sender = sender - - pubsub.publish('SEND_NOTIFICATION_ON_TICKETS', { - sendNotsOnTickets: notification, - }) - - ticketReply.sender = sender - - return ticketReply - } else { - const superAdmin = await User.findOne({ role: 'superAdmin' }).lean() - const reply = { - sender: context.userId, - receiver: superAdmin?._id.toString(), - replyMessage, + if (!sender) { + throw new GraphQLError('Sender not found', { + extensions: { code: 'NOT_FOUND' }, + }) } - ticket.replies.push(reply) - ticket.status = 'customer-reply' - - await ticket.save({ validateBeforeSave: false }) - const ticketReply = ticket.replies[ticket.replies.length - 1] - - const sender = await User.findById(ticketReply.sender.toString()) - .populate('profile') - .select('email profile id role organization') - - const notification: any = await Notification.create({ - receiver: superAdmin?._id.toString(), - message: `A reply on ticket has been sent. ${ticket._id}`, - sender: context.userId, - read: false, - createdAt: new Date(), - }) - - notification.sender = sender - - pubsub.publish('SEND_NOTIFICATION_ON_TICKETS', { - sendNotsOnTickets: notification, - }) ticketReply.sender = sender + const senderId: any = context.userId + // Send notification + await pushNotification( + reply.receiver, + `A reply on ticket has been sent. ${ticket._id}`, + senderId + ) return ticketReply + } catch (error: any) { + throw new GraphQLError(error.message, { + extensions: { code: '500' }, + }) } } @@ -98,18 +70,16 @@ const resolvers = { ticketCreated: { subscribe: () => pubsub.asyncIterator('TICKET_CREATED'), }, - replyAdded: { subscribe: () => pubsub.asyncIterator('REPLY_ADDED'), }, - sendNotsOnTickets: { subscribe: () => pubsub.asyncIterator('SEND_NOTIFICATION_ON_TICKETS'), }, }, Query: { - getAllTickets: async (_: any, arg: any, context: Context) => { + getAllTickets: async (_: any, __: any, context: Context) => { try { ;(await checkUserLoggedIn(context))([ 'superAdmin', @@ -121,35 +91,30 @@ const resolvers = { 'users', ]) - const filterObj: any = {} - - // if (context.role !== 'superAdmin') filterObj.user = context.userId - if (context.role === 'superAdmin') { - // SuperAdmin can view all tickets - } else if (context.role === 'admin') { - // Admin can view all tickets - } else { - // Regular user can only view their own tickets - filterObj.user = context.userId - } + const filterObj: any = (() => { + if (context.role === 'superAdmin') { + return {} // SuperAdmin can see all tickets + } + if (context.role === 'admin' || context.role === 'coordinator') { + return {} // Admin and Coordinator can see all tickets + } + if (context.role === 'trainee') { + return { user: context.userId } // Trainee can see only their tickets + } + return { user: context.userId } // Default case for unrecognized roles + })() const tickets = await Ticket.find(filterObj) .populate({ path: 'user', - populate: { - path: 'profile', - model: 'Profile', - }, + populate: { path: 'profile', model: 'Profile' }, }) .populate({ path: 'replies', populate: { path: 'sender', model: 'User', - populate: { - path: 'profile', - model: 'Profile', - }, + populate: { path: 'profile', model: 'Profile' }, }, }) .exec() @@ -157,9 +122,7 @@ const resolvers = { return tickets } catch (error: any) { throw new GraphQLError(error.message, { - extensions: { - code: '500', - }, + extensions: { code: '500' }, }) } }, @@ -168,132 +131,102 @@ const resolvers = { Mutation: { createTicket: async (_: any, args: TicketType, context: Context) => { try { - ;(await checkUserLoggedIn(context))([ - 'admin', - 'coordinator', - 'manager', - 'trainee', - 'user', - ]) - const { subject, message }: TicketType = args + ;(await checkUserLoggedIn(context))(['admin', 'superAdmin']) + const { userId, subject, message }: any = args const ticket = await Ticket.create({ - user: context.userId, + user: userId, subject, message, }) - const subPayload: any = ticket.toJSON() - - const user = await User.findById(subPayload.user.toString()) + const user = await User.findById(context.userId) .populate('profile') .select('email profile id role organization') - subPayload.user = user + if (!user) { + throw new GraphQLError('User not found', { + extensions: { code: 'NOT_FOUND' }, + }) + } pubsub.publish('TICKET_CREATED', { - ticketCreated: { - ...subPayload, - }, - }) - - const superAdmin = await User.findOne({ role: 'superAdmin' }) - - const notification: any = await Notification.create({ - receiver: superAdmin?._id, - message: `Ticket has been sent to you. ${ticket._id}`, - sender: context.userId, - read: false, - createdAt: new Date(), + ticketCreated: { ...ticket.toJSON(), user }, }) - notification.sender = user - - pubsub.publish('SEND_NOTIFICATION_ON_TICKETS', { - sendNotsOnTickets: notification, - }) + const receiverId: any = (await User.findOne({ role: 'superAdmin' })) + ?._id + const senderId: any = context.userId + await pushNotification( + receiverId, + `Ticket has been sent to you. ${ticket._id}`, + senderId + ) - if (ticket) - return { - responseMsg: - 'Your message has been received! We will get back to you shortly.', - } + return { + responseMsg: + 'Your message has been received! We will get back to you shortly.', + } } catch (error: any) { throw new GraphQLError(error.message, { - extensions: { - code: '500', - }, + extensions: { code: '500' }, }) } }, - replyToTicket: async (_: any, args: Reply, context: Context) => { + replyToTicket: async ( + _: any, + { ticketId, replyMessage }: Reply, + context: Context + ) => { try { if (!context.userId) - throw new GraphQLError('Loggin first', { - extensions: { - code: 'VALIDATION_ERROR', - }, + throw new GraphQLError('Login first', { + extensions: { code: 'VALIDATION_ERROR' }, }) - const { ticketId, replyMessage } = args const ticket = await Ticket.findById(ticketId) if (!ticket) - throw new GraphQLError('Ticket you are replying to does not exist.', { - extensions: { - code: 'VALIDATION_ERROR', - }, + throw new GraphQLError('Ticket does not exist.', { + extensions: { code: 'VALIDATION_ERROR' }, }) - const { user } = ticket - // Allow both superAdmin and admin to reply to the ticket + const { user }: any = ticket if ( context.role !== 'superAdmin' && context.role !== 'admin' && user?.toString() !== context.userId - ) - throw new GraphQLError( - 'Access denied! You can only reply if you are the owner of the ticket, or you are an admin/super admin.', - { - extensions: { - code: 'VALIDATION_ERROR', - }, - } - ) - - const res = await createReply( - ticket, - user, - context, - replyMessage, - pubsub - ) - - const subPayload = { - id: res._id.toString(), - sender: res.sender, - receiver: res.receiver, - replyMessage: res.replyMessage, - createdAt: res.createdAt, - ticket: ticketId, + ) { + throw new GraphQLError('Access denied!', { + extensions: { code: 'VALIDATION_ERROR' }, + }) } + const reply = await createReply(ticket, user, context, replyMessage) + pubsub.publish('REPLY_ADDED', { - replyAdded: subPayload, + replyAdded: { + sender: reply.sender, + receiver: reply.receiver, + replyMessage: reply.replyMessage, + createdAt: reply.createdAt, + ticket: ticketId, + }, }) - return res + return reply } catch (error: any) { throw new GraphQLError(error.message, { - extensions: { - code: '500', - }, + extensions: { code: '500' }, }) } }, - closeTicket: async (_: any, args: Reply, context: Context) => { - const { ticketId } = args + closeTicket: async ( + _: any, + { ticketId }: { ticketId: string }, + context: Context + ) => { try { ;(await checkUserLoggedIn(context))([ 'admin', @@ -303,43 +236,104 @@ const resolvers = { 'user', ]) - const ticket = await Ticket.findById(ticketId) - + const ticket: any = await Ticket.findById(ticketId) if (!ticket) throw new GraphQLError('Ticket does not exist.', { - extensions: { - code: 'VALIDATION_ERROR', - }, + extensions: { code: 'VALIDATION_ERROR' }, }) if ( - context.userId !== ticket?.user?.toString() && + context.userId !== ticket.user.toString() && context.role !== 'superAdmin' - ) - throw new GraphQLError( - 'Access denied! You do not possess permission to close this ticket.', - { - extensions: { - code: 'VALIDATION_ERROR', - }, - } - ) + ) { + throw new GraphQLError('Access denied!', { + extensions: { code: 'VALIDATION_ERROR' }, + }) + } ticket.status = 'closed' await ticket.save({ validateBeforeSave: false }) + const senderId: any = context.userId + // Send notification + await pushNotification( + ticket.user, + `Your ticket ${ticketId} has been closed.`, + senderId + ) - return { - responseMsg: 'Ticket was successfully closed!', + return { responseMsg: 'Ticket was successfully closed!' } + } catch (error: any) { + throw new GraphQLError(error.message, { + extensions: { code: '500' }, + }) + } + }, + + updateTicket: async ( + _: any, + { id, input }: { id: string; input: any }, + context: Context + ) => { + try { + ;(await checkUserLoggedIn(context))(['admin', 'superAdmin']) + + const ticket = await Ticket.findById(id) + if (!ticket) + throw new GraphQLError('Ticket not found', { + extensions: { code: 'NOT_FOUND' }, + }) + + if ( + context.userId !== ticket.user.toString() && + context.role !== 'superAdmin' + ) { + throw new GraphQLError('Access denied!', { + extensions: { code: 'VALIDATION_ERROR' }, + }) } + + Object.assign(ticket, input) + await ticket.save({ validateBeforeSave: false }) + + return ticket } catch (error: any) { throw new GraphQLError(error.message, { - extensions: { - code: '500', - }, + extensions: { code: '500' }, + }) + } + }, + + deleteTicket: async (_: any, { id }: { id: string }, context: Context) => { + try { + ;(await checkUserLoggedIn(context))(['superAdmin', 'admin']) + + const ticket = await Ticket.findById(id) + if (!ticket) + throw new GraphQLError('Ticket not found', { + extensions: { code: 'NOT_FOUND' }, + }) + + if ( + context.userId !== ticket.user.toString() && + context.role !== 'superAdmin' + ) { + throw new GraphQLError('Access denied!', { + extensions: { code: 'VALIDATION_ERROR' }, + }) + } + + await ticket.remove() + + return { responseMsg: 'Ticket was successfully deleted!' } + } catch (error: any) { + throw new GraphQLError(error.message, { + extensions: { code: '500' }, }) } }, }, + + // Define other resolvers and types as needed } export default resolvers diff --git a/src/schema/ticket.shema.ts b/src/schema/ticket.shema.ts index 067a7685..e69b6eeb 100644 --- a/src/schema/ticket.shema.ts +++ b/src/schema/ticket.shema.ts @@ -52,13 +52,23 @@ const ticketSchema = gql` responseMsg: String! } + # Updated: Change this to input type + input UpdateTicketInput { + subject: String! + message: String! + status: String + } + type Query { getAllTickets: [Ticket!]! } + type Mutation { createTicket(subject: String!, message: String!): TicketResponseMsg! replyToTicket(ticketId: String!, replyMessage: String!): Reply! closeTicket(ticketId: String!): TicketResponseMsg + updateTicket(id: ID!, input: UpdateTicketInput!): Ticket! + deleteTicket(id: ID!): Ticket } ` diff --git a/src/seeders/ticket.seed.ts b/src/seeders/ticket.seed.ts index f1aa6698..6396882e 100644 --- a/src/seeders/ticket.seed.ts +++ b/src/seeders/ticket.seed.ts @@ -1,9 +1,37 @@ import Ticket from '../models/ticket.model' +import { User } from '../models/user' -const seedTickets = async () => { - await Ticket.deleteMany({}) +const seedTickets = async (): Promise => { + try { + await Ticket.deleteMany({}) - return null + const roles = ['admin', 'superAdmin', 'manager', 'coordinator', 'user'] + const roleUserMap: Record = {} + + for (const role of roles) { + const users = await User.find({ role }).select('_id').exec() + roleUserMap[role] = users.map((user) => user._id.toString()) + } + + const sampleTickets = [] + for (const role in roleUserMap) { + for (const userId of roleUserMap[role]) { + sampleTickets.push({ + subject: `Issue for ${role}`, + message: `This is a sample ticket for ${role}.`, + user: userId, + status: 'open', + }) + } + } + + await Ticket.insertMany(sampleTickets) + + console.log('Sample tickets have been seeded successfully.') + } catch (error) { + console.error('Error seeding tickets:', error) + throw new Error('Failed to seed tickets') + } } export default seedTickets