Files
epess-web-backend/src/Quiz/quiz.schema.ts
2024-12-09 20:44:39 +07:00

343 lines
10 KiB
TypeScript

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)
}