From ba962151ac46d8eaaa7d72339bce4567f211eb14 Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Sun, 22 Sep 2024 00:22:43 +0800 Subject: [PATCH 1/4] Add read endpoints -implement endpoint to read questions for question list (with filter and search functionality) -implement endpoint to read question using specific question id --- .../src/controllers/questionController.ts | 98 ++++++++++++++++++- .../src/routes/questionRoutes.ts | 6 ++ .../question-service/src/utils/constants.ts | 8 ++ 3 files changed, 108 insertions(+), 4 deletions(-) diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index 632f02ccc6..6ae41343e7 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -5,6 +5,10 @@ import { DUPLICATE_QUESTION_RESPONSE_MESSAGE, QN_DESC_EXCEED_CHAR_LIMIT_RESPONSE_MESSAGE, QN_DESC_CHAR_LIMIT, + QN_CREATED, + QN_NOT_FOUND, + SERVER_ERROR, + QN_RETRIEVED, } from "../utils/constants.ts"; export const createQuestion = async ( @@ -39,11 +43,11 @@ export const createQuestion = async ( await newQuestion.save(); res.status(201).json({ - message: "Question created successfully", + message: QN_CREATED, question: newQuestion, }); } catch (error) { - res.status(500).json({ message: "Server error", error }); + res.status(500).json({ message: SERVER_ERROR, error }); } }; @@ -57,7 +61,7 @@ export const updateQuestion = async ( const currentQuestion = await Question.findById(id); if (!currentQuestion) { - res.status(404).json({ message: "Question not found" }); + res.status(404).json({ message: QN_NOT_FOUND }); return; } @@ -85,6 +89,92 @@ export const updateQuestion = async ( question: updatedQuestion, }); } catch (error) { - res.status(500).json({ message: "Server error", error }); + res.status(500).json({ message: SERVER_ERROR, error }); + } +}; + +export const readQuestionsList = async ( + req: Request, + res: Response, +): Promise => { + try { + const qnLimit = 10; + + const { currPage } = req.params; + const currPageInt = parseInt(currPage); + + if (!req.body || Object.keys(req.body).length == 0) { + const totalQuestions = await Question.countDocuments(); + if (totalQuestions == 0) { + res.status(404).json({ message: QN_NOT_FOUND }); + return; + } + const totalPages = Math.ceil(totalQuestions / qnLimit); + + const currPageQuestions = await Question.find() + .skip((currPageInt - 1) * qnLimit) + .limit(qnLimit); + + res.status(200).json({ + message: QN_RETRIEVED, + pages: totalPages, + questions: currPageQuestions, + }); + } else { + const { title, complexities, categories } = req.body; + const query: any = {}; + + if (title) { + query.title = { $regex: new RegExp(title, "i") }; + } + + if (complexities && complexities.length > 0) { + query.complexity = { $in: complexities }; + } + + if (categories && categories.length > 0) { + query.category = { $in: categories }; + } + + const filteredTotalQuestions = await Question.countDocuments(query); + if (filteredTotalQuestions == 0) { + res.status(404).json({ message: QN_NOT_FOUND }); + return; + } + const filteredTotalPages = Math.ceil(filteredTotalQuestions / qnLimit); + + const filteredQuestions = await Question.find(query) + .skip((currPageInt - 1) * qnLimit) + .limit(qnLimit); + + res.status(200).json({ + message: QN_RETRIEVED, + pages: filteredTotalPages, + questions: filteredQuestions, + }); + } + } catch (error) { + res.status(500).json({ message: SERVER_ERROR, error }); + } +}; + +export const readQuestionIndiv = async ( + req: Request, + res: Response, +): Promise => { + try { + const { id } = req.params; + + const questionDetails = await Question.findById(id); + if (!questionDetails) { + res.status(404).json({ message: QN_NOT_FOUND }); + return; + } + res.status(200).json({ + message: QN_RETRIEVED, + question: questionDetails, + }); + } catch (error) { + res.status(500).json({ message: SERVER_ERROR, error }); } }; diff --git a/backend/question-service/src/routes/questionRoutes.ts b/backend/question-service/src/routes/questionRoutes.ts index 1d63c9824d..163a879cc2 100644 --- a/backend/question-service/src/routes/questionRoutes.ts +++ b/backend/question-service/src/routes/questionRoutes.ts @@ -2,6 +2,8 @@ import express from "express"; import { createQuestion, updateQuestion, + readQuestionsList, + readQuestionIndiv, } from "../controllers/questionController.ts"; const router = express.Router(); @@ -10,4 +12,8 @@ router.post("/questions", createQuestion); router.put("/questions/:id", updateQuestion); +router.post("/questionsquery/:currPage", readQuestionsList); + +router.get("/questions/:id", readQuestionIndiv); + export default router; diff --git a/backend/question-service/src/utils/constants.ts b/backend/question-service/src/utils/constants.ts index 36caf31102..3450bbaf93 100644 --- a/backend/question-service/src/utils/constants.ts +++ b/backend/question-service/src/utils/constants.ts @@ -5,3 +5,11 @@ export const QN_DESC_EXCEED_CHAR_LIMIT_RESPONSE_MESSAGE = export const DUPLICATE_QUESTION_RESPONSE_MESSAGE = "Duplicate question: A question with the same title already exists."; + +export const QN_CREATED = "Question created successfully."; + +export const QN_NOT_FOUND = "Question not found."; + +export const SERVER_ERROR = "Server error."; + +export const QN_RETRIEVED = "Question retrieved successfully."; From fc45f03c23dc20c9689e2d8b9dabbec719b1eb47 Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Mon, 23 Sep 2024 00:31:19 +0800 Subject: [PATCH 2/4] change read API request type - changed getting the question list from using POST request to using GET request - allow frontend to specify question limit per page through request query --- .../src/controllers/questionController.ts | 138 +++++++++--------- .../src/routes/questionRoutes.ts | 2 +- .../question-service/src/utils/constants.ts | 14 +- 3 files changed, 82 insertions(+), 72 deletions(-) diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index 6ae41343e7..96b9f62f61 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -5,10 +5,12 @@ import { DUPLICATE_QUESTION_RESPONSE_MESSAGE, QN_DESC_EXCEED_CHAR_LIMIT_RESPONSE_MESSAGE, QN_DESC_CHAR_LIMIT, - QN_CREATED, - QN_NOT_FOUND, - SERVER_ERROR, - QN_RETRIEVED, + QN_CREATED_MESSAGE, + QN_NOT_FOUND_MESSAGE, + SERVER_ERROR_MESSAGE, + QN_RETRIEVED_MESSAGE, + PAGE_LIMIT_REQUIRED_MESSAGE, + PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE, } from "../utils/constants.ts"; export const createQuestion = async ( @@ -43,11 +45,11 @@ export const createQuestion = async ( await newQuestion.save(); res.status(201).json({ - message: QN_CREATED, + message: QN_CREATED_MESSAGE, question: newQuestion, }); } catch (error) { - res.status(500).json({ message: SERVER_ERROR, error }); + res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); } }; @@ -61,7 +63,7 @@ export const updateQuestion = async ( const currentQuestion = await Question.findById(id); if (!currentQuestion) { - res.status(404).json({ message: QN_NOT_FOUND }); + res.status(404).json({ message: QN_NOT_FOUND_MESSAGE }); return; } @@ -89,72 +91,74 @@ export const updateQuestion = async ( question: updatedQuestion, }); } catch (error) { - res.status(500).json({ message: SERVER_ERROR, error }); + res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); } }; +interface QnListSearchFilterParams { + page: string; + qnLimit: string; + title?: string; + complexities?: string | string[]; + categories?: string | string[]; +} + export const readQuestionsList = async ( - req: Request, + req: Request, res: Response, ): Promise => { try { - const qnLimit = 10; - - const { currPage } = req.params; - const currPageInt = parseInt(currPage); - - if (!req.body || Object.keys(req.body).length == 0) { - const totalQuestions = await Question.countDocuments(); - if (totalQuestions == 0) { - res.status(404).json({ message: QN_NOT_FOUND }); - return; - } - const totalPages = Math.ceil(totalQuestions / qnLimit); - - const currPageQuestions = await Question.find() - .skip((currPageInt - 1) * qnLimit) - .limit(qnLimit); - - res.status(200).json({ - message: QN_RETRIEVED, - pages: totalPages, - questions: currPageQuestions, - }); - } else { - const { title, complexities, categories } = req.body; - const query: any = {}; - - if (title) { - query.title = { $regex: new RegExp(title, "i") }; - } - - if (complexities && complexities.length > 0) { - query.complexity = { $in: complexities }; - } - - if (categories && categories.length > 0) { - query.category = { $in: categories }; - } - - const filteredTotalQuestions = await Question.countDocuments(query); - if (filteredTotalQuestions == 0) { - res.status(404).json({ message: QN_NOT_FOUND }); - return; - } - const filteredTotalPages = Math.ceil(filteredTotalQuestions / qnLimit); - - const filteredQuestions = await Question.find(query) - .skip((currPageInt - 1) * qnLimit) - .limit(qnLimit); - - res.status(200).json({ - message: QN_RETRIEVED, - pages: filteredTotalPages, - questions: filteredQuestions, - }); + const { page, qnLimit, title, complexities, categories } = req.query; + + if (!page || !qnLimit) { + res.status(400).json({ message: PAGE_LIMIT_REQUIRED_MESSAGE }); + return; + } + + const pageInt = parseInt(page, 10); + const qnLimitInt = parseInt(qnLimit, 10); + + if (pageInt < 1 || qnLimitInt < 1) { + res.status(400).json({ message: PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE }); + return; + } + + const query: any = {}; + + if (title) { + query.title = { $regex: new RegExp(title, "i") }; + } + + if (complexities) { + query.complexity = { + $in: Array.isArray(complexities) ? complexities : [complexities], + }; + } + + if (categories) { + query.category = { + $in: Array.isArray(categories) ? categories : [categories], + }; + } + + const filteredTotalQuestions = await Question.countDocuments(query); + if (filteredTotalQuestions == 0) { + res.status(404).json({ message: QN_NOT_FOUND_MESSAGE }); + return; } + const filteredTotalPages = Math.ceil(filteredTotalQuestions / qnLimitInt); + + const filteredQuestions = await Question.find(query) + .skip((pageInt - 1) * qnLimitInt) + .limit(qnLimitInt); + + res.status(200).json({ + message: QN_RETRIEVED_MESSAGE, + pages: filteredTotalPages, + questions: filteredQuestions, + }); } catch (error) { - res.status(500).json({ message: SERVER_ERROR, error }); + res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); } }; @@ -167,14 +171,14 @@ export const readQuestionIndiv = async ( const questionDetails = await Question.findById(id); if (!questionDetails) { - res.status(404).json({ message: QN_NOT_FOUND }); + res.status(404).json({ message: QN_NOT_FOUND_MESSAGE }); return; } res.status(200).json({ - message: QN_RETRIEVED, + message: QN_RETRIEVED_MESSAGE, question: questionDetails, }); } catch (error) { - res.status(500).json({ message: SERVER_ERROR, error }); + res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); } }; diff --git a/backend/question-service/src/routes/questionRoutes.ts b/backend/question-service/src/routes/questionRoutes.ts index 163a879cc2..80820f4c9e 100644 --- a/backend/question-service/src/routes/questionRoutes.ts +++ b/backend/question-service/src/routes/questionRoutes.ts @@ -12,7 +12,7 @@ router.post("/questions", createQuestion); router.put("/questions/:id", updateQuestion); -router.post("/questionsquery/:currPage", readQuestionsList); +router.get("/questions", readQuestionsList); router.get("/questions/:id", readQuestionIndiv); diff --git a/backend/question-service/src/utils/constants.ts b/backend/question-service/src/utils/constants.ts index 3450bbaf93..8c5532616b 100644 --- a/backend/question-service/src/utils/constants.ts +++ b/backend/question-service/src/utils/constants.ts @@ -6,10 +6,16 @@ export const QN_DESC_EXCEED_CHAR_LIMIT_RESPONSE_MESSAGE = export const DUPLICATE_QUESTION_RESPONSE_MESSAGE = "Duplicate question: A question with the same title already exists."; -export const QN_CREATED = "Question created successfully."; +export const QN_CREATED_MESSAGE = "Question created successfully."; -export const QN_NOT_FOUND = "Question not found."; +export const QN_NOT_FOUND_MESSAGE = "Question not found."; -export const SERVER_ERROR = "Server error."; +export const SERVER_ERROR_MESSAGE = "Server error."; -export const QN_RETRIEVED = "Question retrieved successfully."; +export const QN_RETRIEVED_MESSAGE = "Question retrieved successfully."; + +export const PAGE_LIMIT_REQUIRED_MESSAGE = + "Page number and question limit per page should be provided."; + +export const PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE = + "Page number and question limit per page should be positive integers."; From 454ed1f6ea790dbf44da869274b28ceabb2779ee Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Tue, 24 Sep 2024 02:10:24 +0800 Subject: [PATCH 3/4] add read category endpoint - add endpoint for reading unique question categories for frontend filter component - update api documentation --- .../src/controllers/questionController.ts | 23 ++- .../src/routes/questionRoutes.ts | 3 + .../question-service/src/utils/constants.ts | 5 + backend/question-service/swagger.yml | 157 +++++++++++++++++- 4 files changed, 182 insertions(+), 6 deletions(-) diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index eacc0a8ede..bba96f0b13 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -12,6 +12,8 @@ import { QN_RETRIEVED_MESSAGE, PAGE_LIMIT_REQUIRED_MESSAGE, PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE, + CATEGORIES_NOT_FOUND_MESSAGE, + CATEGORIES_RETRIEVED_MESSAGE, } from "../utils/constants.ts"; import { upload } from "../../config/multer"; @@ -136,7 +138,7 @@ export const deleteQuestion = async ( const { id } = req.params; const currentQuestion = await Question.findById(id); if (!currentQuestion) { - res.status(400).json({ message: QN_NOT_FOUND_MESSAGE }); + res.status(404).json({ message: QN_NOT_FOUND_MESSAGE }); return; } @@ -235,3 +237,22 @@ export const readQuestionIndiv = async ( res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); } }; + +export const readCategories = async ( + req: Request, + res: Response, +): Promise => { + try { + const uniqueCats = await Question.distinct("category"); + if (!uniqueCats || uniqueCats.length == 0) { + res.status(404).json({ message: CATEGORIES_NOT_FOUND_MESSAGE }); + } + + res.status(200).json({ + message: CATEGORIES_RETRIEVED_MESSAGE, + categories: uniqueCats, + }); + } catch (error) { + res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); + } +}; diff --git a/backend/question-service/src/routes/questionRoutes.ts b/backend/question-service/src/routes/questionRoutes.ts index e0a5ffe6df..453f042d19 100644 --- a/backend/question-service/src/routes/questionRoutes.ts +++ b/backend/question-service/src/routes/questionRoutes.ts @@ -6,6 +6,7 @@ import { updateQuestion, readQuestionsList, readQuestionIndiv, + readCategories, } from "../controllers/questionController.ts"; const router = express.Router(); @@ -16,6 +17,8 @@ router.post("/questions/images", createImageLink); router.put("/questions/:id", updateQuestion); +router.get("/questions/categories", readCategories); + router.get("/questions", readQuestionsList); router.get("/questions/:id", readQuestionIndiv); diff --git a/backend/question-service/src/utils/constants.ts b/backend/question-service/src/utils/constants.ts index 6f942edaad..034edf847b 100644 --- a/backend/question-service/src/utils/constants.ts +++ b/backend/question-service/src/utils/constants.ts @@ -21,3 +21,8 @@ export const PAGE_LIMIT_REQUIRED_MESSAGE = export const PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE = "Page number and question limit per page should be positive integers."; + +export const CATEGORIES_NOT_FOUND_MESSAGE = "No categories found."; + +export const CATEGORIES_RETRIEVED_MESSAGE = + "Categories retrieved successfully."; diff --git a/backend/question-service/swagger.yml b/backend/question-service/swagger.yml index d9c0ca069a..b49cab2d66 100644 --- a/backend/question-service/swagger.yml +++ b/backend/question-service/swagger.yml @@ -58,7 +58,7 @@ definitions: properties: message: type: string - deescription: Message + description: Message ServerError: type: object properties: @@ -124,6 +124,83 @@ paths: application/json: schema: $ref: "#/definitions/ServerError" + get: + tags: + - questions + summary: Reads a list of questions + description: Reads a limited list of questions based on current page and question limit per page, taking into account search and filter conditions (if any). + parameters: + - in: query + name: page + type: integer + required: true + description: Page of questions to return + - in: query + name: qnLimit + type: integer + required: true + description: Limit on number of questions to return + - in: query + name: title + type: string + required: false + description: Question title search keywords + - in: query + name: complexities + schema: + oneOf: + - type: string + - type: array + items: + type: string + required: false + description: Question complexity filters + - in: query + name: categories + schema: + oneOf: + - type: string + - type: array + items: + type: string + required: false + description: Question category filters + responses: + 200: + description: Successful Response + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: Message + pages: + type: integer + description: Total number of pages of questions + questions: + type: array + items: + $ref: "#/definitions/Question" + 400: + description: Bad Request + content: + application/json: + schema: + $ref: "#/definitions/Error" + 404: + description: Question Not Found + content: + application/json: + schema: + $ref: "#/definitions/Error" + 500: + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/definitions/ServerError" /api/questions/{id}: put: tags: @@ -155,8 +232,8 @@ paths: description: Message question: $ref: "#/definitions/Question" - 400: - description: Bad Request + 404: + description: Question Not Found content: application/json: schema: @@ -189,8 +266,78 @@ paths: message: type: string description: Message - 400: - description: Bad Request + 404: + description: Question Not Found + content: + application/json: + schema: + $ref: "#/definitions/Error" + 500: + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/definitions/ServerError" + get: + tags: + - questions + summary: Reads a question + description: Reads a question + parameters: + - in: path + name: id + type: string + required: true + description: Question id + responses: + 200: + description: Successful Response + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: Message + question: + $ref: "#/definitions/Question" + 404: + description: Question Not Found + content: + application/json: + schema: + $ref: "#/definitions/Error" + 500: + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/definitions/ServerError" + /api/questions/categories: + get: + tags: + - questions + summary: Returns question categories + description: Returns list of unique question categories + responses: + 200: + description: Successful Response + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: Message + categories: + type: array + items: + type: string + description: Categories + 404: + description: Categories Not Found content: application/json: schema: From da0938fbaea3d018035c2dca2c410a37fdc00914 Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Tue, 24 Sep 2024 02:18:29 +0800 Subject: [PATCH 4/4] change readQuestionsList return field - return total number of questions instead of pages --- .../question-service/src/controllers/questionController.ts | 3 +-- backend/question-service/swagger.yml | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index bba96f0b13..67b8f36828 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -200,7 +200,6 @@ export const readQuestionsList = async ( res.status(404).json({ message: QN_NOT_FOUND_MESSAGE }); return; } - const filteredTotalPages = Math.ceil(filteredTotalQuestions / qnLimitInt); const filteredQuestions = await Question.find(query) .skip((pageInt - 1) * qnLimitInt) @@ -208,7 +207,7 @@ export const readQuestionsList = async ( res.status(200).json({ message: QN_RETRIEVED_MESSAGE, - pages: filteredTotalPages, + totalQns: filteredTotalQuestions, questions: filteredQuestions, }); } catch (error) { diff --git a/backend/question-service/swagger.yml b/backend/question-service/swagger.yml index b49cab2d66..b26186d1f0 100644 --- a/backend/question-service/swagger.yml +++ b/backend/question-service/swagger.yml @@ -176,9 +176,9 @@ paths: message: type: string description: Message - pages: + totalQns: type: integer - description: Total number of pages of questions + description: Total number of questions questions: type: array items: