import crypto from 'crypto' import { Inject, Injectable, Logger } from '@nestjs/common' import { AnswerType, Role } from '@prisma/client' import { QuestionType } from '@prisma/client' import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos' import { Builder } from '../Graphql/graphql.builder' import { PrismaService } from '../Prisma/prisma.service' @Injectable() export class QuizSchema extends PothosSchema { constructor( @Inject(SchemaBuilderToken) private readonly builder: Builder, private readonly prisma: PrismaService, ) { super() } // Types section @PothosRef() quiz() { return this.builder.prismaObject('Quiz', { fields: (t) => ({ id: t.exposeID('id'), serviceId: t.exposeID('serviceId'), service: t.relation('service'), quizTitle: t.exposeString('quizTitle'), quizSynopsis: t.exposeString('quizSynopsis'), progressBarColor: t.exposeString('progressBarColor'), nrOfQuestions: t.exposeInt('nrOfQuestions'), questions: t.relation('questions'), createdAt: t.expose('createdAt', { type: 'DateTime', }), updatedAt: t.expose('updatedAt', { type: 'DateTime', }), }), }) } @PothosRef() question() { return this.builder.prismaObject('Question', { fields: (t) => ({ id: t.exposeID('id'), quizId: t.exposeID('quizId'), quiz: t.relation('quiz'), question: t.exposeString('question'), questionType: t.expose('questionType', { type: QuestionType, }), questionPic: t.exposeString('questionPic'), answerSelectionType: t.expose('answerSelectionType', { type: AnswerType, }), answers: t.exposeStringList('answers'), correctAnswer: t.field({ type: this.correctAnswerObject(), resolve: (parent) => parent.correctAnswer, }), messageForCorrectAnswer: t.exposeString('messageForCorrectAnswer'), messageForIncorrectAnswer: t.exposeString('messageForIncorrectAnswer'), explanation: t.exposeString('explanation'), point: t.exposeInt('point'), createdAt: t.expose('createdAt', { type: 'DateTime', }), updatedAt: t.expose('updatedAt', { type: 'DateTime', }), }), }) } @PothosRef() quizAttempt() { return this.builder.prismaObject('QuizAttempt', { fields: (t) => ({ id: t.exposeID('id'), quizId: t.exposeID('quizId'), quiz: t.relation('quiz'), userId: t.exposeID('userId'), user: t.relation('user'), score: t.exposeInt('score'), questions: t.relation('questions'), answers: t.exposeStringList('answers'), createdAt: t.expose('createdAt', { type: 'DateTime', }), updatedAt: t.expose('updatedAt', { type: 'DateTime', }), }), }) } @PothosRef() stringType() { return this.builder.objectRef('StringType') } @PothosRef() stringListType() { return this.builder.objectRef('StringListType') } @PothosRef() correctAnswerObject() { const StringType = this.builder.objectType(this.stringType(), { fields: (t) => ({ value: t.string({ resolve: (parent) => parent as string, }), }), }) const StringListType = this.builder.objectType(this.stringListType(), { fields: (t) => ({ items: t.stringList({ resolve: (parent) => parent as string[], }), }), }) return this.builder.unionType('CorrectAnswerObject', { types: [StringType, StringListType], resolveType: (value) => { return Array.isArray(value) ? StringListType : StringType }, }) } @Pothos() init(): void { this.builder.queryFields((t) => ({ quiz: t.prismaField({ type: this.quiz(), args: this.builder.generator.findUniqueArgs('Quiz'), resolve: async (query, _root, args, ctx, _info) => { if (ctx.isSubscription) { throw new Error('Subscription is not allowed') } if (!ctx.http.me) { throw new Error('Unauthorized') } return await this.prisma.quiz.findUnique({ ...query, where: { id: args.where.id } }) }, }), quizzes: t.prismaField({ type: [this.quiz()], args: { serviceId: t.arg({ type: 'String', required: true, }), scheduleId: t.arg({ type: 'String', required: false, }), }, resolve: async (query, _root, args, ctx, _info) => { if (ctx.isSubscription) { throw new Error('Subscription is not allowed') } if (!ctx.http.me) { throw new Error('Unauthorized') } // use case 1: customer if (ctx.http.me.role === Role.CUSTOMER) { // using pseudo random to get amount of quizzes based on userid as seed const random = getRandomWithSeed( parseInt(crypto.createHash('sha256').update(ctx.http.me.id).digest('hex'), 16), ) if (!args.scheduleId) { throw new Error('Schedule ID is required') } // get schedule from scheduleId const schedule = await this.prisma.schedule.findUnique({ where: { id: args.scheduleId }, include: { managedService: true, }, }) if (!schedule) { throw new Error('Schedule not found') } // get centerMentorId from schedule const centerMentorId = schedule.managedService.mentorId if (!centerMentorId) { throw new Error('Center mentor not found') } const quizzes = await this.prisma.quiz.findMany({ ...query, where: { serviceId: args.serviceId, centerMentorId: centerMentorId, }, }) // get amount of questions using nrOfQuestions and random index based on random const randomIndex = quizzes.length > 0 ? Math.floor(random * quizzes.length) : 0 const nrOfQuestions = quizzes[0]?.nrOfQuestions ?? 1 const result = quizzes.slice(randomIndex, randomIndex + nrOfQuestions) return result } // use case 2: center mentor or center owner if (ctx.http.me.role === Role.CENTER_MENTOR || ctx.http.me.role === Role.CENTER_OWNER) { const centerMentor = await this.prisma.centerMentor.findUnique({ where: { mentorId: ctx.http.me.id }, }) if (!centerMentor) { throw new Error('Center mentor not found') } return await this.prisma.quiz.findMany({ ...query, where: { serviceId: args.serviceId, centerMentorId: centerMentor.mentorId, }, }) } }, }), })) this.builder.mutationFields((t) => ({ createQuiz: t.prismaField({ type: this.quiz(), args: { data: t.arg({ type: this.builder.generator.getCreateInput('Quiz', ['id', 'centerMentorId', 'createdAt', 'updatedAt']), required: true, }), }, resolve: async (query, _root, args, ctx, _info) => { if (ctx.isSubscription) { throw new Error('Subscription is not allowed') } if (!ctx.http.me) { throw new Error('Unauthorized') } if (!args.data) { throw new Error('Data is required') } if (!args.data.service?.connect?.id) { throw new Error('Service ID is required') } args.data.centerMentor = { connect: { mentorId: ctx.http.me.id, }, } return await this.prisma.quiz.create({ ...query, data: { ...args.data, }, }) }, }), updateQuiz: t.prismaField({ type: this.quiz(), args: { where: t.arg({ type: this.builder.generator.getWhereUnique('Quiz'), required: true, }), data: t.arg({ type: this.builder.generator.getUpdateInput('Quiz'), required: true, }), }, resolve: async (query, _root, args, ctx, _info) => { if (ctx.isSubscription) { throw new Error('Subscription is not allowed') } if (!ctx.http.me) { throw new Error('Unauthorized') } return await this.prisma.quiz.update({ ...query, where: args.where, data: args.data, }) }, }), submitQuiz: t.prismaField({ type: this.quizAttempt(), args: { data: t.arg({ type: this.builder.generator.getCreateInput('QuizAttempt', [ 'id', 'createdAt', 'updatedAt', 'quiz', 'user', 'userId', ]), }), }, resolve: async (query, _root, args, ctx, _info) => { if (ctx.isSubscription) { throw new Error('Subscription is not allowed') } if (!ctx.http.me) { throw new Error('Unauthorized') } if (!args.data) { throw new Error('Data is required') } if (!args.data.quiz?.connect?.id) { throw new Error('Quiz ID is required') } // query the quiz to get the questions const quiz = await this.prisma.quiz.findUnique({ where: { id: args.data.quiz.connect.id }, }) if (!quiz) { throw new Error('Quiz not found') } return await this.prisma.quizAttempt.create({ ...query, data: { ...args.data, quiz: { connect: { id: args.data.quiz.connect.id } }, user: { connect: { id: ctx.http.me.id } }, }, }) }, }), })) } } function seededRandom(seed: number) { const x = Math.sin(seed) * 10000 return x - Math.floor(x) } function getRandomWithSeed(seed: number) { return seededRandom(seed) }