diff --git a/apps/challenges/src/controllers/questionaire.ts b/apps/challenges/src/controllers/questionaire.ts index 3bd1f8ee..1f2e652f 100644 --- a/apps/challenges/src/controllers/questionaire.ts +++ b/apps/challenges/src/controllers/questionaire.ts @@ -1,9 +1,12 @@ import { Request, Response } from "express"; const asyncHandler = require('express-async-handler'); -import Question from '../model/question'; +import Question, { CreateQuestionReq } from '../model/question'; import Submission from '../model/submission'; import Season from "../model/season"; import { isValidObjectId } from "../utils/db"; +import QuestionService from "../service/questionService"; +import { isValidCreateQuestionRequest } from "../utils/validator"; +import { z } from "zod"; // @desc Get questions // @route GET /api/question @@ -35,7 +38,7 @@ const getQuestion = asyncHandler(async (req: Request, res: Response) => { } try { - const question = await Question.findById(questionId); + const question = await QuestionService.getQuestionByID(questionId); if (!question) { return res.status(404).json({ message: 'Question not found' }); @@ -54,21 +57,20 @@ const getQuestion = asyncHandler(async (req: Request, res: Response) => { // @access Private const setQuestion = asyncHandler(async (req: Request, res: Response) => { try { - const question = await Question.create({ - question_no: req.body.question_no, - question_title: req.body.question_title, - question_desc: req.body.question_desc, - question_date: req.body.question_date, - seasonID: req.body.season_id, - expiry: req.body.expiry, - points: req.body.points, - answer: req.body.answer, - }); - - res.status(201).json(question); - } catch (error) { - if ((error as Error).name === 'ValidationError') { - return res.status(400).json({ message: (error as Error).message }); + const question = isValidCreateQuestionRequest.parse(req.body); + + const createQuestionReq: CreateQuestionReq = { + ...question, + question_date: new Date(question.question_date), + expiry: new Date(question.expiry), + } + + const resp = await QuestionService.createQuestion(createQuestionReq); + + res.status(resp.status).json(resp); + } catch (err) { + if (err instanceof z.ZodError) { + return res.status(400).json({ message: (err as Error).message }); } res.status(500).json({ message: 'Internal Server Error' }); diff --git a/apps/challenges/src/controllers/submission.ts b/apps/challenges/src/controllers/submission.ts index 5846e06e..1b296579 100644 --- a/apps/challenges/src/controllers/submission.ts +++ b/apps/challenges/src/controllers/submission.ts @@ -43,96 +43,36 @@ const getSubmission = asyncHandler(async (req: Request, res: Response) => { }) // @desc Set submission -// @route POST /api/submission +// @route POST /api/submission/ // @access Private - const setSubmission = asyncHandler(async (req: Request, res: Response) => { - const questionId = req.body.question; + const questionID = req.body.question; - if (!isValidObjectId(questionId)) { + if (!isValidObjectId(questionID)) { return res.status(400).json({ message: 'Invalid question ID' }); } try { const submission = isValidCreateSubmissionRequest.parse(req.body); const createSubmissionReq = { - question: new mongoose.Types.ObjectId(submission.question), user: new mongoose.Types.ObjectId(submission.user), + question: new mongoose.Types.ObjectId(submission.question), answer: submission.answer }; - await SubmissionService.createSubmission(createSubmissionReq); - - res.status(201).json({ message: 'Answer submitted' }); - - } catch (error) { - res.status(500).json({ message: 'Internal Server Error' }); - } -}) - -// @desc Update submission -// @route PUT /api/submission/:id -// @access Private -const updateSubmission = asyncHandler(async (req: Request, res: Response) => { - const submissionId = req.params.id; - - if (!isValidObjectId(submissionId)) { - return res.status(400).json({ message: 'Invalid submission ID' }); - } - - try { - const submission = await Submission.findById(submissionId); - - if (!submission) { - return res.status(404).json({ message: 'Submission not found' }); - } - - const updatedSubmission = await Submission.findByIdAndUpdate(submissionId, req.body, { new: true }); + const resp = await SubmissionService.createSubmission(createSubmissionReq); - // Re-evaluate the points awarded - + res.status(resp.status).json(resp); - - res.status(200).json(updatedSubmission); - } catch (error) { - console.error(error); - res.status(500).json({ message: 'Internal Server Error' }); - } -}) - -// @desc Delete submission -// @route DELETE /api/submission/:id -// @access Private -const deleteSubmission = asyncHandler(async (req: Request, res: Response) => { - const submissionId = req.params.id; - - if (!isValidObjectId(submissionId)) { - return res.status(400).json({ message: 'Invalid submission ID' }); - } - - try { - const submission = await Submission.findById(submissionId); - - if (!submission) { - return res.status(404).json({ message: 'Submission not found' }); - } - - await submission.remove() - - res.status(200).json({message: 'Submission deleted'}); } catch (error) { - console.error(error); res.status(500).json({ message: 'Internal Server Error' }); } }) - const SubmissionController = { getSubmissions, getSubmission, setSubmission, - updateSubmission, - deleteSubmission }; export { SubmissionController as default }; \ No newline at end of file diff --git a/apps/challenges/src/model/question.ts b/apps/challenges/src/model/question.ts index b712cc9b..ae2aad2b 100644 --- a/apps/challenges/src/model/question.ts +++ b/apps/challenges/src/model/question.ts @@ -1,6 +1,29 @@ import mongoose, { Schema, Document } from 'mongoose'; +export interface CreateQuestionReq { + question_no: string; + question_title: string; + question_desc: string; + question_date: Date; + season_id: string; + expiry: Date; + points: number; + validation_function: string; +} + +export interface GetUserQuestionResp { + id: string; + question_no: string; + question_title: string; + question_desc: string; + question_date: Date; + seasonID: string; + expiry: Date; + points: number; +} + export interface QuestionModel { + _id: mongoose.Types.ObjectId; question_no: string; question_title: string; question_desc: string; @@ -8,11 +31,11 @@ export interface QuestionModel { seasonID: mongoose.Types.ObjectId; expiry: Date; points: number; - answer: string; submissions: Array; submissions_count: number; correct_submissions_count: number; active: boolean; + validation_function : string; } const questionSchema: Schema = new Schema({ @@ -45,10 +68,6 @@ const questionSchema: Schema = new Schema({ type: Number, required: [true, 'Please add a points value'] }, - answer: { - type: String, - required: [true, 'Please add an answer'] - }, submissions: { type: [mongoose.Types.ObjectId], ref: 'Submission' @@ -65,6 +84,10 @@ const questionSchema: Schema = new Schema({ type: Boolean, default: true }, + validation_function: { + type: String, + required: [true, 'Please add a validation function'] + } }, { timestamps: true }); diff --git a/apps/challenges/src/repo/questionRepo.ts b/apps/challenges/src/repo/questionRepo.ts index 054f103b..a5cdc53a 100644 --- a/apps/challenges/src/repo/questionRepo.ts +++ b/apps/challenges/src/repo/questionRepo.ts @@ -1,4 +1,4 @@ -import Question, { QuestionModel } from "../model/question" +import Question, { CreateQuestionReq, QuestionModel } from "../model/question" import mongoose from 'mongoose'; const getQuestionByID = async ( @@ -10,6 +10,32 @@ const getQuestionByID = async ( return question; } +const createQuestionByReq = async ( + req: CreateQuestionReq +): Promise => { + const questionModel = { + _id: new mongoose.Types.ObjectId(), + question_no: req.question_no, + question_title: req.question_title, + question_desc: req.question_desc, + question_date: req.question_date, + seasonID: new mongoose.Types.ObjectId(req.season_id), + expiry: req.expiry, + points: req.points, + submissions: [], + submissions_count: 0, + correct_submissions_count: 0, + active: true, + validation_function: req.validation_function + } + + const question = await Question.create(questionModel); + + await question.save(); + + return question; +} + const updateQuestionByID = async ( questionID: mongoose.Types.ObjectId, questionModel: QuestionModel @@ -39,6 +65,7 @@ const updateQuestionSubmissions = async ( const QuestionRepo = { getQuestionByID, + createQuestionByReq, updateQuestionByID, updateQuestionSubmissions } diff --git a/apps/challenges/src/routes/questionaire.ts b/apps/challenges/src/routes/questionaire.ts index ab928f0c..ec17dea4 100644 --- a/apps/challenges/src/routes/questionaire.ts +++ b/apps/challenges/src/routes/questionaire.ts @@ -4,7 +4,6 @@ import QuestionController from "../controllers/questionaire"; import SubmissionController from "../controllers/submission"; router.route('/').get(QuestionController.getQuestions).post(QuestionController.setQuestion); -router.route('/active').get(QuestionController.getActiveQuestions); router.route('/:id').get(QuestionController.getQuestion).delete(QuestionController.deleteQuestion).put(QuestionController.updateQuestion); export { router as default }; \ No newline at end of file diff --git a/apps/challenges/src/routes/submission.ts b/apps/challenges/src/routes/submission.ts index 290bce8f..3726980f 100644 --- a/apps/challenges/src/routes/submission.ts +++ b/apps/challenges/src/routes/submission.ts @@ -3,6 +3,6 @@ const router = express.Router(); import SubmissionController from "../controllers/submission"; router.route('/').get(SubmissionController.getSubmissions).post(SubmissionController.setSubmission); -router.route('/:id').get(SubmissionController.getSubmission).delete(SubmissionController.deleteSubmission).put(SubmissionController.updateSubmission); +router.route('/:id').get(SubmissionController.getSubmission); export { router as default }; \ No newline at end of file diff --git a/apps/challenges/src/service/questionService.ts b/apps/challenges/src/service/questionService.ts index c250cde2..59fe4eeb 100644 --- a/apps/challenges/src/service/questionService.ts +++ b/apps/challenges/src/service/questionService.ts @@ -1,5 +1,8 @@ import mongoose from 'mongoose'; import QuestionRepo from '../repo/questionRepo'; +import { CreateQuestionReq } from '../model/question'; +import ValidationService from './validationService'; +import { GeneralResp } from '../model/response'; const getQuestionByID = async( questionID: string, @@ -12,6 +15,27 @@ const getQuestionByID = async( return question; } +const createQuestion = async ( + req: CreateQuestionReq, +): Promise => { + if (!ValidationService.getValidationFunction(req.validation_function)){ + console.log('Invalid validation function'); + return { + status: 400, + message: 'Invalid validation function', + data: null, + }; + } + + const question = await QuestionRepo.createQuestionByReq(req); + + return { + status: 201, + message: 'Question created', + data: question, + }; +} + const updateQuestionSubmissions = async( questionID: string, submissionID: string, @@ -28,7 +52,8 @@ const updateQuestionSubmissions = async( const QuestionService = { getQuestionByID, - updateQuestionSubmissions + updateQuestionSubmissions, + createQuestion } export { QuestionService as default } \ No newline at end of file diff --git a/apps/challenges/src/service/submissionService.ts b/apps/challenges/src/service/submissionService.ts index d74fdffa..9b97a035 100644 --- a/apps/challenges/src/service/submissionService.ts +++ b/apps/challenges/src/service/submissionService.ts @@ -2,6 +2,7 @@ import { CreateSubmissionReq, SubmissionModel } from "../model/submission"; import SubmissionRepo from "../repo/submissionRepo"; import QuestionService from "./questionService"; import { GeneralResp } from "../model/response"; +import ValidationService from "./validationService"; const createSubmission = async ( submission: CreateSubmissionReq @@ -28,14 +29,21 @@ const createSubmission = async ( } } + let isCorrect = false; + try { + isCorrect = await ValidationService.validateAnswer(submission.question.toString(), submission.answer); + } catch (err) { + console.log("submissionService createSubmission fail to validate answer: ", err); + } + try { const dbSubmission = { user: submission.user, seasonID: question.seasonID, question: submission.question, answer: submission.answer, - correct: submission.answer === question.answer, - points_awarded: submission.answer === question.answer ? question.points : 0 + correct: isCorrect, + points_awarded: isCorrect ? question.points : 0 } const result = await SubmissionRepo.createSubmission(dbSubmission); @@ -43,7 +51,7 @@ const createSubmission = async ( question = await QuestionService.updateQuestionSubmissions( submission.question.toString(), result._id.toString(), - submission.answer === question.answer + isCorrect ); if (!question) { @@ -63,6 +71,8 @@ const createSubmission = async ( } } + + const SubmissionService = { createSubmission, } diff --git a/apps/challenges/src/service/validationService.ts b/apps/challenges/src/service/validationService.ts new file mode 100644 index 00000000..dba08ec3 --- /dev/null +++ b/apps/challenges/src/service/validationService.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; +import QuestionService from "./questionService"; + +const isHello = (input: any) => { + // use zod to confirm input is string + z.string().parse(input); + + return input === 'hello'; +} + +// DO NOT EXPOSE this map. Add new validation function by directly adding to the map +const validationFunctionMap: Map = new Map([ + ['isHello', isHello], +]); + +const getValidationFunction = (functionName: string) => { + if (validationFunctionMap.has(functionName)) { + return validationFunctionMap.get(functionName); + } + return null; +} + +const validateAnswer = async ( + questionID: string, + submission: any, +): Promise => { + + const question = await QuestionService.getQuestionByID(questionID); + if (!question) { + throw new Error('Question not found'); + } + + //check if validation function exists as keys + const invokeFunction = getValidationFunction(question.validation_function); + if (!invokeFunction) { + throw new Error('Validation function not found'); + } + + try{ + const isCorrect = invokeFunction(submission); + return isCorrect; + }catch (e) { + console.log(`validationService validateAnswer fail to validate answer: ${e}`) + return false; + } +} + +const ValidationService = { + validateAnswer, + getValidationFunction, +} + +export { ValidationService as default } \ No newline at end of file diff --git a/apps/challenges/src/utils/validator.ts b/apps/challenges/src/utils/validator.ts index b57ee50f..36eebb9f 100644 --- a/apps/challenges/src/utils/validator.ts +++ b/apps/challenges/src/utils/validator.ts @@ -11,12 +11,28 @@ export const zodIsValidObjectId = z.string().refine( { message: 'Invalid ObjectId' } ); +export const zodIsValidRFC3339 = z.string().refine( + (val) => new Date(val).toISOString() === val, + { message: 'Invalid RFC3339 date' } +); + export const isValidCreateSubmissionRequest = z.object({ user: zodIsValidObjectId, question: zodIsValidObjectId, answer: z.string(), }); +export const isValidCreateQuestionRequest = z.object({ + question_no: z.string(), + question_title: z.string(), + question_desc: z.string(), + question_date: zodIsValidRFC3339, + season_id: zodIsValidObjectId, + expiry: zodIsValidRFC3339, + points: z.number().int(), + validation_function: z.string(), +}); + export const isPositiveInteger = z.number().int().min(1); export const isNonNegativeInteger = z.number().int().min(0); \ No newline at end of file