diff --git a/.gitignore b/.gitignore index 963a057e75..f3ce38b554 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ - -.env +# Ignore IntelliJ IDEA project files +.idea/ +.env \ No newline at end of file diff --git a/README.md b/README.md index 5ddee1cbf1..1f1edee3a5 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ [![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/bzPrOe11) # CS3219 Project (PeerPrep) - AY2425S1 -## Group: Gxx +## Group: G03 -### Note: -- You can choose to develop individual microservices within separate folders within this repository **OR** use individual repositories (all public) for each microservice. -- In the latter scenario, you should enable sub-modules on this GitHub classroom repository to manage the development/deployment **AND** add your mentor to the individual repositories as a collaborator. -- The teaching team should be given access to the repositories as we may require viewing the history of the repository in case of any disputes or disagreements. +### Note: +- You can choose to develop individual microservices within separate folders within this repository **OR** use individual repositories (all public) for each microservice. +- In the latter scenario, you should enable sub-modules on this GitHub classroom repository to manage the development/deployment **AND** add your mentor to the individual repositories as a collaborator. +- The teaching team should be given access to the repositories as we may require viewing the history of the repository in case of any disputes or disagreements. diff --git a/docker-compose.yml b/docker-compose.yml index 4549daf7a7..f948f9242e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,8 +41,10 @@ services: MONGO_INITDB_ROOT_PASSWORD: ${QUESTION_DB_PASSWORD} volumes: - question-db:/data/db + - ./services/question/init-mongo/init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js networks: - question-db-network + restart: always volumes: question-db: diff --git a/services/question/README.md b/services/question/README.md new file mode 100644 index 0000000000..4989a60f12 --- /dev/null +++ b/services/question/README.md @@ -0,0 +1,434 @@ +# Question Service User Guide + +## Pre-requisites + +1. Run the following command to create the `.env` files at the root directory: + +```cmd +cp .env.sample .env +cp services/question/.env.sample services/question/.env +``` + +2. After setting up the .env files, build the Docker images and start the containers using the following command: + +```cmd +docker compose build +docker compose up -d +``` + +3. To stop and remove the containers and associated volumes, use the following command: + +```cmd +docker compose down -v +``` + +## Get Questions + +This endpoint allows the retrieval of all the questions in the database. If filter by (optional) parameters, questions +that matches with parameters will be returned; if no parameters are provided, all questions will be returned. + +- **HTTP Method**: `GET` +- **Endpoint**: `/questions` + +### Parameters: + +- `title` (Optional) - Filter by question title. +- `description` (Optional) - Filter by question description. +- `topics` (Optional) - Filter by topics associated with the questions. +- `difficulty` (Optional) - Filter by question difficulty. + +### Responses: + +| Response Code | Explanation | +|-----------------------------|-----------------------------------------------------------------------------------------------------------------| +| 200 (OK) | Success, all questions are returned. If no questions match the optional parameters, an empty array is returned. | +| 500 (Internal Server Error) | Unexpected error in the database or server. | + +### Command Line Example: + +``` +Retrieve all Questions: +curl -X GET http://localhost:8081/questions + +Retrieve Questions by Title: +curl -X GET "http://localhost:8081/questions?title=Reverse%20a%20String" + +Retrieve Questions by Description: +curl -X GET "http://localhost:8081/questions?description=string" + +Retrieve Questions by Topics: +curl -X GET "http://localhost:8081/questions?topics=Algorithms,Data%20Structures" + +Retrieve Questions by Difficulty: +curl -X GET "http://localhost:8081/questions?difficulty=Easy" + +Retrieve Questions by Title and Difficulty: +curl -X GET "http://localhost:8081/questions?title=Reverse%20a%20String&difficulty=Easy" + +Retrieve Questions by Title, Description, Topics, and Difficulty: +curl -X GET "http://localhost:8081/questions?title=Reverse%20a%20String&description=string&topics=Algorithms&difficulty=Easy" +``` + +### Parameter Format Details: + +The `topics` parameter must be passed as a comma-separated string in `GET` request because there is limitation with URL +encoding and readability concerns. + +### Example of Response Body for Success: + +```json +{ + "status": "Success", + "message": "Questions retrieved successfully", + "data": [ + { + "_id": "66ea6985cd34132719540c22", + "id": 1, + "description": "Write a function that reverses a string. The input string is given as an array of characters s.\n\nYou must do this by modifying the input array in-place with O(1) extra memory.\n\n\nExample 1:\n\nInput: s = [\"h\",\"e\",\"l\",\"l\",\"o\"]\nOutput: [\"o\",\"l\",\"l\",\"e\",\"h\"]\n\nExample 2:\nInput: s = [\"H\",\"a\",\"n\",\"n\",\"a\",\"h\"]\nOutput: [\"h\",\"a\",\"n\",\"n\",\"a\",\"H\"]\n\nConstraints:\n1 \u003C= s.length \u003C= 105 s[i] is a printable ascii character.", + "difficulty": "Easy", + "title": "Reverse a String", + "topics": [ + "Strings", + "Algorithms" + ] + }, + { + "_id": "66ea6985cd34132719540c23", + "id": 2, + "description": "Implement a function to detect if a linked list contains a cycle.", + "difficulty": "Easy", + "title": "Linked List Cycle Detection", + "topics": [ + "Data Structures", + "Algorithms" + ] + } + ] +} +``` + +--- + +## Get Question by ID + +This endpoint allows the retrieval of the question by using the question ID. + +- **HTTP Method**: `GET` +- **Endpoint**: `/questions/{id}` + +### Parameters: + +- `id` (Required) - The ID of the question to retrieve. + +### Responses: + +| Response Code | Explanation | +|-----------------------------|----------------------------------------------------------| +| 200 (OK) | Success, question corresponding to the `id` is returned. | +| 404 (Not Found) | Question with the specified `id` not found. | +| 500 (Internal Server Error) | Unexpected error in the database or server. | + +### Command Line Example: + +``` +Retrieve Question by ID: +curl -X GET http://localhost:8081/questions/1 +``` + +### Example of Response Body for Success: + +```json +{ + "status": "Success", + "message": "Question with ID retrieved successfully", + "data": { + "_id": "66ea6985cd34132719540c22", + "id": 1, + "description": "Write a function that reverses a string. The input string is given as an array of characters s.\n\nYou must do this by modifying the input array in-place with O(1) extra memory.\n\n\nExample 1:\n\nInput: s = [\"h\",\"e\",\"l\",\"l\",\"o\"]\nOutput: [\"o\",\"l\",\"l\",\"e\",\"h\"]\n\nExample 2:\nInput: s = [\"H\",\"a\",\"n\",\"n\",\"a\",\"h\"]\nOutput: [\"h\",\"a\",\"n\",\"n\",\"a\",\"H\"]\n\nConstraints:\n1 \u003C= s.length \u003C= 105 s[i] is a printable ascii character.", + "difficulty": "Easy", + "title": "Reverse a String", + "topics": [ + "Strings", + "Algorithms" + ] + } +} +``` + +--- + +## Get Question by Parameters (Random) + +This endpoint allows the retrieval of random questions that matches the parameters provided. + +- **HTTP Method**: `GET` +- **Endpoint**: `/questions/search` + +### Parameters: + +- `limit` (Optional) - The number of questions to be returned. If not provided, default limit is 1. +- `topics` (Required) - The topic of the question. +- `difficulty` (Required) - The difficulty of the question. + +### Responses: + +### Responses: + +| Response Code | Explanation | +|-----------------------------|-------------------------------------------------------------------------------------------------------------| +| 200 (OK) | Success, questions matching the parameters are returned. If no questions match, an empty array is returned. | +| 400 (Bad Request) | The request is missing required parameters or the parameters are invalid. | +| 500 (Internal Server Error) | Unexpected error in the database or server. | + +### Command Line Example: + +``` +Retrieve Random Question by Topics and Difficulty: +curl -X GET "http://localhost:8081/questions/search?topics=Algorithms&difficulty=Medium" + +Retrieve Random Question by Topics, Difficulty, and Limit: +curl -X GET "http://localhost:8081/questions/search?topics=Algorithms,Data%20Structures&difficulty=Easy&limit=5" +``` + +### Example of Response Body for Success: + +```json +{ + "status": "Success", + "message": "Questions with Parameters retrieved successfully", + "data": [ + { + "_id": "66ea6985cd34132719540c25", + "id": 4, + "description": "Given two binary strings a and b, return their sum as a binary string.", + "difficulty": "Easy", + "title": "Add Binary", + "topics": [ + "Bit Manipulation", + "Algorithms" + ] + }, + { + "_id": "66ea6985cd34132719540c22", + "id": 1, + "description": "Write a function that reverses a string. The input string is given as an array of characters s.\n\nYou must do this by modifying the input array in-place with O(1) extra memory.\n\n\nExample 1:\n\nInput: s = [\"h\",\"e\",\"l\",\"l\",\"o\"]\nOutput: [\"o\",\"l\",\"l\",\"e\",\"h\"]\n\nExample 2:\nInput: s = [\"H\",\"a\",\"n\",\"n\",\"a\",\"h\"]\nOutput: [\"h\",\"a\",\"n\",\"n\",\"a\",\"H\"]\n\nConstraints:\n1 \u003C= s.length \u003C= 105 s[i] is a printable ascii character.", + "difficulty": "Easy", + "title": "Reverse a String", + "topics": [ + "Strings", + "Algorithms" + ] + }, + { + "_id": "66ea6985cd34132719540c27", + "id": 6, + "description": "Implement a last-in first-out (LIFO) stack using only two queues. The implemented stack should support all the functions of a normal stack (push, top, pop, and empty).", + "difficulty": "Easy", + "title": "Implement Stack using Queues", + "topics": [ + "Data Structures" + ] + } + ] +} +``` + +--- + +## Get Topics + +This endpoint retrieves all unique topics in the database + +- **HTTP Method**: `GET` +- **Endpoint**: `/questions/topics` + +### Responses: + +| Response Code | Explanation | +|-----------------------------|---------------------------------------------------------------------| +| 200 (OK) | Success, all topics are returned. | +| 404 (Not Found) | No topic found. | +| 500 (Internal Server Error) | The server encountered an error and could not complete the request. | + +### Command Line Example: + +``` +Retrieve Topics: +curl -X GET http://localhost:8081/questions/topics +``` + +### Example of Response Body for Success: + +```json +{ + "status": "Success", + "message": "Topics retrieved successfully", + "data": [ + "Algorithms", + "Arrays", + "Bit Manipulation", + "Brainteaser", + "Data Structures", + "Databases", + "Recursion", + "Strings" + ] +} +``` + +--- + +## Add Question + +This endpoint allows the addition of a new question. The `id` is now automatically generated by the system to ensure +uniqueness. + +- **HTTP Method**: `POST` +- **Endpoint**: `/questions` + +### Parameters: + +- `title` (Required) - The title of the question. +- `description` (Required) - A description of the question. +- `topics` (Required) - The topics associated with the question. +- `difficulty` (Required) - The difficulty level of the question. + +### Responses: + +| Response Code | Explanation | +|-----------------------------|--------------------------------------------------------------------| +| 201 (Created) | The question is created successfully. | +| 400 (Bad Request) | Required fields are missing or invalid or question already exists. | +| 500 (Internal Server Error) | Unexpected error in the database or server. | + +### Command Line Example: + +``` +Add Question: +curl -X POST http://localhost:8081/questions -H "Content-Type: application/json" -d "{\"title\": \"New Question\", \"description\": \"This is a description for a new question.\", \"topics\": [\"Data Structures\", \"Algorithms\"], \"difficulty\": \"Medium\"}" +``` + +### Example of Response Body for Success: + +```json +{ + "status": "Success", + "message": "Question created successfully", + "data": { + "id": 21, + "title": "New Question", + "description": "This is a description for a new question.", + "topics": [ + "Data Structures", + "Algorithms" + ], + "difficulty": "Medium", + "_id": "66eedf739672ca081e9fd5ff" + } +} +``` + +--- + +## Update Question + +This endpoint allows updating an existing question. Only the title, description, topics, and difficulty can be updated. + +- **HTTP Method**: `PUT` +- **Endpoint**: `/questions/{id}` + +### Request Parameters: + +- `id` (Required) - The ID of the question to update. + +### Request Body: + +- `title` (Optional) - New title for the question. +- `description` (Optional) - New description for the question. +- `topics` (Optional) - New topics for the question. +- `difficulty` (Optional) - New difficulty level for the question. + +### Responses: + +| Response Code | Explanation | +|-----------------------------|------------------------------------------------| +| 200 (OK) | Success, the question is updated successfully. | +| 404 (Not Found) | Question with the specified `id` not found. | +| 500 (Internal Server Error) | Unexpected error in the database or server. | + +### Command Line Example: + +``` +Update Question: +curl -X PUT http://localhost:8081/questions/21 -H "Content-Type: application/json" -d "{\"title\": \"Updated Question Title\", \"description\": \"This is the updated description.\", \"topics\": [\"Updated Topic\"], \"difficulty\": \"Hard\"}" +``` + +### Example of Response Body for Success: + +```json +{ + "status": "Success", + "message": "Question updated successfully", + "data": { + "_id": "66eedf739672ca081e9fd5ff", + "id": 21, + "title": "Updated Title", + "description": "Updated description for the existing question.", + "topics": [ + "Data Structures", + "Algorithms" + ], + "difficulty": "Hard" + } +} +``` + +--- + +## Delete Question + +This endpoint allows the deletion of a question by the question ID. + +- **HTTP Method**: `DELETE` +- **Endpoint**: `/questions/{id}` + +### Parameters: + +- `id` (Required) - The ID of the question to delete. + +### Responses: + +| Response Code | Explanation | +|-----------------------------|------------------------------------------------| +| 200 (OK) | Success, the question is deleted successfully. | +| 404 (Not Found) | Question with the specified `id` not found. | +| 500 (Internal Server Error) | Unexpected error in the database or server. | + +### Command Line Example: + +``` +Delete Question: +curl -X DELETE http://localhost:8081/questions/21 +``` + +### Example of Response Body for Success: + +```json +{ + "status": "Success", + "message": "Question deleted successfully", + "data": { + "_id": "66eedf739672ca081e9fd5ff", + "id": 21, + "title": "Updated Title", + "description": "Updated description for the existing question.", + "topics": [ + "Data Structures", + "Algorithms" + ], + "difficulty": "Hard" + } +} +``` + +--- diff --git a/services/question/eslint.config.mjs b/services/question/eslint.config.mjs index 3f1c13ceb1..6873f57ac4 100644 --- a/services/question/eslint.config.mjs +++ b/services/question/eslint.config.mjs @@ -13,6 +13,8 @@ export default tseslint.config({ eslintPluginPrettierRecommended, ], rules: { + '@typescript-eslint/no-explicit-any': 'off', + // https://stackoverflow.com/questions/68816664/get-rid-of-error-delete-eslint-prettier-prettier-and-allow-use-double 'prettier/prettier': [ 'error', diff --git a/services/question/src/app.ts b/services/question/src/app.ts index ad51519cca..53a5b4acc0 100644 --- a/services/question/src/app.ts +++ b/services/question/src/app.ts @@ -9,7 +9,6 @@ const app: Express = express(); // Middleware app.use(morgan('dev')); - app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); @@ -23,6 +22,6 @@ app.use( // Routes app.use('/', router); -app.use('/', questionRouter); +app.use('/questions', questionRouter); export default app; diff --git a/services/question/src/controllers/questionController.ts b/services/question/src/controllers/questionController.ts index 915d7bdb97..e658e41c12 100644 --- a/services/question/src/controllers/questionController.ts +++ b/services/question/src/controllers/questionController.ts @@ -1,11 +1,237 @@ import { Request, Response } from 'express'; +import { handleError, handleNotFound, handleBadRequest, handleSuccess } from '../utils/helpers'; import { Question } from '../models/questionModel'; +import { getNextSequenceValue } from '../utils/sequence'; +/** + * This endpoint allows the retrieval of all the questions in the database (or can filter by optional parameters). + * @param req + * @param res + */ export const getQuestions = async (req: Request, res: Response) => { - const questions = await Question.find(); - const questionTitles = questions.map(q => q.title); - res.status(200).json({ - message: 'These are all the question titles:' + questionTitles, - }); - return; + try { + const { title, description, topics, difficulty } = req.query; + const query: any = {}; + + if (title) { + query.title = title as string; + } + if (description) { + const words = (description as string).split(' '); + query.description = { $regex: words.join('|'), $options: 'i' }; + } + if (topics) { + const topicsArray = (topics as string).split(','); + query.topics = { + $in: topicsArray.map(topic => new RegExp(topic, 'i')), + }; + } + if (difficulty) { + query.difficulty = difficulty as string; + } + + const questions = await Question.find(query); + + if ((title || description || topics || difficulty) && questions.length === 0) { + return handleSuccess(res, 200, 'No questions found matching the provided parameters.', []); + } + + handleSuccess(res, 200, 'Questions retrieved successfully', questions); + } catch (error) { + console.error('Error in getQuestions:', error); + handleError(res, error, 'Failed to retrieve questions'); + } +}; + +/** + * This endpoint allows the retrieval of the question by using the question ID. + * @param req + * @param res + */ +export const getQuestionById = async (req: Request, res: Response) => { + const { id } = req.params; + + const newId = parseInt(id, 10); + if (isNaN(parseInt(id, 10))) { + return handleBadRequest(res, 'Invalid question ID'); + } + + try { + const question = await Question.findOne({ id: newId }); + + if (!question) { + return handleNotFound(res, 'Question not found'); + } + + handleSuccess(res, 200, 'Question with ID retrieved successfully', question); + } catch (error) { + console.error('Error in getQuestionById:', error); + handleError(res, error, 'Failed to retrieve question'); + } +}; + +/** + * This endpoint allows the retrieval of random questions that matches the parameters provided. + * @param req + * @param res + */ +export const getQuestionByParameters = async (req: Request, res: Response) => { + const { limit, topics, difficulty } = req.query; + + if (!topics) { + return handleBadRequest(res, 'Topics are required'); + } + if (!difficulty) { + return handleBadRequest(res, 'Difficulty is required'); + } + + let newLimit; + if (limit) { + newLimit = parseInt(limit as string, 10); + } else { + newLimit = 1; + } + if (isNaN(newLimit)) { + return handleBadRequest(res, 'Limit must be a valid positive integer'); + } + if (newLimit <= 0) { + return handleBadRequest(res, 'Limit must be more than 0'); + } + + try { + const topicsArray = (topics as string).split(','); + const query = { + topics: { $in: topicsArray.map(topic => new RegExp(topic, 'i')) }, + difficulty: difficulty, + }; + const numOfQuestions = await Question.countDocuments(query); + + if (numOfQuestions === 0) { + return handleSuccess(res, 200, 'No questions found with the given parameters', []); + } + + const finalLimit = Math.min(newLimit, numOfQuestions); + const questions = await Question.aggregate([{ $match: query }, { $sample: { size: finalLimit } }]); + + handleSuccess(res, 200, 'Questions with Parameters retrieved successfully', questions); + } catch (error) { + console.error('Error in getQuestionByParameters:', error); + handleError(res, error, 'Failed to search for questions'); + } +}; + +/** + * This endpoint retrieves all unique topics in the database + * @param req + * @param res + */ +export const getTopics = async (req: Request, res: Response) => { + try { + const topics = await Question.distinct('topics'); + + if (!topics || topics.length === 0) { + return handleNotFound(res, 'No topics found'); + } + + handleSuccess(res, 200, 'Topics retrieved successfully', topics); + } catch (error) { + console.error('Error in getTopics:', error); + handleError(res, error, 'Failed to retrieve topics'); + } +}; + +/** + * This endpoint allows to add new question into the database + * @param req + * @param res + */ +export const addQuestion = async (req: Request, res: Response) => { + const { title, description, topics, difficulty } = req.body; + + if (!title) { + return handleBadRequest(res, 'Title is required'); + } + if (!description) { + return handleBadRequest(res, 'Description is required'); + } + if (!topics) { + return handleBadRequest(res, 'Topics are required'); + } + if (!difficulty) { + return handleBadRequest(res, 'Difficulty is required'); + } + + try { + const existingQuestion = await Question.findOne({ + $or: [{ title: title }, { description: description }], + }).collation({ locale: 'en', strength: 2 }); + if (existingQuestion) { + return handleBadRequest(res, `A question with the same title or description already exists.`); + } + + const newId = await getNextSequenceValue('questionId'); + const newQuestion = new Question({ + id: newId, + title, + description, + topics, + difficulty, + }); + + const savedQuestion = await newQuestion.save(); + handleSuccess(res, 201, 'Question created successfully', savedQuestion); + } catch (error) { + handleError(res, error, 'Failed to add question'); + } +}; + +/** + * This endpoint allows updating an existing question (only can update the title, description, topics and difficulty). + * @param req + * @param res + */ +export const updateQuestion = async (req: Request, res: Response) => { + const { id } = req.params; + const updates = { ...req.body }; + + if ('id' in updates) { + return handleBadRequest(res, 'ID cannot be updated'); + } + + try { + const updatedQuestion = await Question.findOneAndUpdate({ id: parseInt(id, 10) }, updates, { + new: true, + runValidators: true, + }); + + if (!updatedQuestion) { + return handleNotFound(res, 'Question not found'); + } + + handleSuccess(res, 200, 'Question updated successfully', updatedQuestion); + } catch (error) { + handleError(res, error, 'Failed to update question'); + } +}; + +/** + * This endpoint allows deletion of a question by the question ID. + * @param req + * @param res + */ +export const deleteQuestion = async (req: Request, res: Response) => { + const { id } = req.params; + + try { + const deletedID = parseInt(id, 10); + const deletedQuestion = await Question.findOneAndDelete({ id: deletedID }); + + if (!deletedQuestion) { + return handleNotFound(res, 'Question not found'); + } + + handleSuccess(res, 200, 'Question deleted successfully', deletedQuestion); + } catch (error) { + handleError(res, error, 'Failed to delete question'); + } }; diff --git a/services/question/src/index.ts b/services/question/src/index.ts index 99e6654e26..4802d8777b 100644 --- a/services/question/src/index.ts +++ b/services/question/src/index.ts @@ -1,6 +1,7 @@ import app from './app'; import { connectToDB, upsertManyQuestions } from './models'; import { getDemoQuestions } from './utils/data'; +import { initializeCounter } from './utils/sequence'; const port = process.env.PORT || 8081; @@ -12,6 +13,10 @@ connectToDB() .then(questions => upsertManyQuestions(questions)) .then(() => { console.log('Questions synced successfully'); + return initializeCounter(); + }) + .then(() => { + console.log('Question ID initialized successfully'); app.listen(port, () => console.log(`Question service is listening on port ${port}.`)); }) .catch(error => { diff --git a/services/question/src/init-mongo/init-mongo.js b/services/question/src/init-mongo/init-mongo.js new file mode 100644 index 0000000000..d429f2d60f --- /dev/null +++ b/services/question/src/init-mongo/init-mongo.js @@ -0,0 +1,5 @@ +db.createUser({ + user: "user", + pwd: "password", + roles: [{ role: "root", db: "admin" }] +}); \ No newline at end of file diff --git a/services/question/src/models/counterModel.ts b/services/question/src/models/counterModel.ts new file mode 100644 index 0000000000..3220f50ba9 --- /dev/null +++ b/services/question/src/models/counterModel.ts @@ -0,0 +1,19 @@ +import { Schema, model } from 'mongoose'; + +export interface ICounter { + _id: string; + sequence_value: number; +} + +const counterSchema = new Schema({ + _id: { + type: String, + required: true, + }, + sequence_value: { + type: Number, + required: true, + }, +}); + +export const Counter = model('Counter', counterSchema); diff --git a/services/question/src/models/index.ts b/services/question/src/models/index.ts index b78620a016..90596e07ce 100644 --- a/services/question/src/models/index.ts +++ b/services/question/src/models/index.ts @@ -4,8 +4,10 @@ import { IQuestion, Question } from './questionModel'; export async function connectToDB() { const mongoURI = process.env.NODE_ENV === 'production' ? process.env.DB_CLOUD_URI : process.env.DB_LOCAL_URI; + console.log('MongoDB URI:', mongoURI); + if (!mongoURI) { - throw Error('MongoDB URI not specified'); + throw new Error('MongoDB URI not specified'); } else if (!process.env.DB_USERNAME || !process.env.DB_PASSWORD) { throw Error('MongoDB credentials not specified'); } diff --git a/services/question/src/models/questionModel.ts b/services/question/src/models/questionModel.ts index 84de364fb1..c3905db976 100644 --- a/services/question/src/models/questionModel.ts +++ b/services/question/src/models/questionModel.ts @@ -20,6 +20,7 @@ const questionSchema = new Schema( type: Number, required: true, unique: true, + immutable: true, }, title: { type: String, @@ -37,7 +38,7 @@ const questionSchema = new Schema( difficulty: { type: String, required: true, - enum: ['Easy', 'Medium', 'Difficult'], + enum: ['Easy', 'Medium', 'Hard'], }, }, { versionKey: false }, diff --git a/services/question/src/routes/questionRoutes.ts b/services/question/src/routes/questionRoutes.ts index c7cb207f31..512f2aa733 100644 --- a/services/question/src/routes/questionRoutes.ts +++ b/services/question/src/routes/questionRoutes.ts @@ -1,7 +1,41 @@ import { Router } from 'express'; -import { getQuestions } from '../controllers/questionController'; -const router = Router(); +import { + getQuestions, + getQuestionById, + getQuestionByParameters, + getTopics, + addQuestion, + deleteQuestion, + updateQuestion, +} from '../controllers/questionController'; -router.get('/questions', getQuestions); +/** + * Router for question endpoints. + */ -export default router; +const questionRouter = Router(); + +/** + * Get questions (or anything related to questions) from the database. + */ +questionRouter.get('/search', getQuestionByParameters); +questionRouter.get('/topics', getTopics); +questionRouter.get('/', getQuestions); +questionRouter.get('/:id', getQuestionById); + +/** + * Add a new question to the database. + */ +questionRouter.post('/', addQuestion); + +/** + * Update a question in the database. + */ +questionRouter.put('/:id', updateQuestion); + +/** + * Delete a question from the database. + */ +questionRouter.delete('/:id', deleteQuestion); + +export default questionRouter; diff --git a/services/question/src/utils/data.ts b/services/question/src/utils/data.ts index 2755c73ebd..dd29e9da52 100644 --- a/services/question/src/utils/data.ts +++ b/services/question/src/utils/data.ts @@ -2,6 +2,11 @@ import fs from 'fs/promises'; import { IQuestion } from '../models/questionModel'; export async function getDemoQuestions(): Promise { - const data = await fs.readFile('./src/data/questions.json', { encoding: 'utf8' }); - return JSON.parse(data); + try { + const data = await fs.readFile('./src/data/questions.json', { encoding: 'utf8' }); + return JSON.parse(data); + } catch (error) { + console.error('Error reading questions from JSON:', error); + throw new Error('Failed to read demo questions'); + } } diff --git a/services/question/src/utils/helpers.ts b/services/question/src/utils/helpers.ts new file mode 100644 index 0000000000..91863ea8fe --- /dev/null +++ b/services/question/src/utils/helpers.ts @@ -0,0 +1,61 @@ +import { Response } from 'express'; + +/** + * Handles errors and sends a 500 response with the error message. + * @param res + * @param error + * @param message + */ +export const handleError = (res: Response, error: unknown, message = 'An unexpected error occurred') => { + console.error(error); + res.status(500).json({ + status: 'Error', + message, + error, + }); +}; + +/** + * Handles bad requests and sends a 400 response with a custom message. + * @param res + * @param error + * @param message + */ +export const handleBadRequest = (res: Response, error: unknown, message = 'Bad Request') => { + console.error(error); + res.status(400).json({ + status: 'Error', + message, + error, + }); +}; + +/** + * Handles not found errors and sends a 404 response with a custom message. + * @param res + * @param error + * @param message + */ +export const handleNotFound = (res: Response, error: unknown, message = 'Not Found') => { + console.log(error); + res.status(404).json({ + status: 'Error', + message, + error, + }); +}; + +/** + * Handles successful responses and sends a 200 response with the provided data. + * @param res + * @param data + * @param message + * @param statusCode - HTTP status code (default is 200) + */ +export const handleSuccess = (res: Response, statusCode = 200, message: string, data: unknown) => { + res.status(statusCode).json({ + status: 'Success', + message, + data, + }); +}; diff --git a/services/question/src/utils/sequence.ts b/services/question/src/utils/sequence.ts new file mode 100644 index 0000000000..ec39b0c5ef --- /dev/null +++ b/services/question/src/utils/sequence.ts @@ -0,0 +1,29 @@ +import { Question } from '../models/questionModel'; +import { Counter } from '../models/counterModel'; + +/** + * This function initializes the counter for the questions. + */ +export async function initializeCounter() { + const maxQuestion = await Question.findOne().sort('-id').exec(); + let maxId = 0; + if (maxQuestion) { + maxId = maxQuestion.id; + } + await Counter.findOneAndUpdate({ _id: 'questionId' }, { $set: { sequence_value: maxId } }, { upsert: true }); + console.log(`Question ID initialized to start from: ${maxId}`); +} + +/** + * This function retrieves the next sequence value. + * @param sequenceName + */ +export const getNextSequenceValue = async (sequenceName: string): Promise => { + const counter = await Counter.findByIdAndUpdate( + sequenceName, + { $inc: { sequence_value: 1 } }, + { new: true, upsert: true }, + ); + console.log(`Updated Question ID: ${counter.sequence_value}`); + return counter.sequence_value; +};