diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000..13566b81b0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml 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/services/question/.gitignore b/services/question/.gitignore index 931232e706..052504ff33 100644 --- a/services/question/.gitignore +++ b/services/question/.gitignore @@ -4,6 +4,7 @@ /node_modules /.pnp .pnp.js +/services/question/node_modules/ # testing /coverage diff --git a/services/question/README.md b/services/question/README.md new file mode 100644 index 0000000000..6d952159e9 --- /dev/null +++ b/services/question/README.md @@ -0,0 +1,103 @@ +# Question Service User Guide + +## Get All Questions +This endpoint allows the retrieval of all the questions in the database. + +- **HTTP Method**: `GET` +- **Endpoint**: `/questions` + +### Responses: + +| Response Code | Explanation | +|---------------|-----------------------------------------------------| +| 200 (OK) | Success, all questions are returned | +| 500 (Internal Server Error) | Unexpected error in the database or server | + +**Example of Response Body for Success**: +TBC + +## Get Question by ID +This endpoint allows the retrieval of the question by using the question ID. + +- **HTTP Method**: `GET` +- **Endpoint**: `/questions/{questionId}` + +### Parameters: + +- Required: `questionId` path parameter + +### Responses: + +| Response Code | Explanation | +|---------------|-----------------------------------------------------| +| 200 (OK) | Success, question corresponding to the questionID is returned | +| 404 (Not Found) | Question with the specified questionID not found | +| 500 (Internal Server Error) | Unexpected error in the database or server | + +**Example of Response Body for Success**: +TBC + +## Get Question by Parameters +This endpoint allows the retrieval of a random question that matches the parameters provided. + +- **HTTP Method**: `GET` +- **Endpoint**: `/questions/search` + +### Parameters: + +- `limit` - The number of questions to be returned (Required) +- `topics` - The topic of the question (Required) +- `languages` - The language of the question (Required) +- `difficulty` - The difficulty of question (Required) + +### Responses: + +| Response Code | Explanation | +|---------------|-----------------------------------------------------| +| 200 (OK) | Success, question corresponding to the limit, topics, languages and difficulty is returned | +| 400 (Bad Request) | Missing fields | +| 404 (Not Found) | Question with the specified parameter(s) not found | +| 500 (Internal Server Error) | Unexpected error in the database or server | + +**Example of Response Body for Success**: +TBC + +## Get Topics +This endpoint retrieves all unique topics in the database (e.g. “Sorting”, “OOP”, “DFS”, etc…) + +- **HTTP Method**: `GET` +- **Endpoint**: `/questions/topics` + +### Headers: + +- Required: - + +### Responses: + +| Response Code | Explanation | +|---------------|-----------------------------------------------------| +| 200 (OK) | Success, all topics are returned | +| 500 (Internal Server Error) | The server encountered an error and could not complete the request | + +**Example of Response Body for Success**: +TBC + +## Get Language +This endpoint retrieves all unique languages in the database (e.g, Java, C++, C, etc..) + +- **HTTP Method**: `GET` +- **Endpoint**: `/questions/languages` + +### Headers: + +- Required: - + +### Responses: + +| Response Code | Explanation | +|---------------|-----------------------------------------------------| +| 200 (OK) | Success, all languages are returned | +| 500 (Internal Server Error) | The server encountered an error and could not complete the request | + +**Example of Response Body for Success**: +TBC 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..e0e949d111 100644 --- a/services/question/src/controllers/questionController.ts +++ b/services/question/src/controllers/questionController.ts @@ -1,11 +1,108 @@ import { Request, Response } from 'express'; +import { handleError, handleNotFound, handleBadRequest, handleSuccess } from '../utils/helpers'; import { Question } from '../models/questionModel'; +/** + * This endpoint allows the retrieval of all the questions in the database. + * @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 questions = await Question.find(); + + handleSuccess(res, "All 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(newId)) { + return handleBadRequest(res, 'Invalid question ID'); + } + + try { + const question = await Question.findOne({ id: newId }); + + if (!question) { + return handleNotFound(res, 'Question not found'); + } + + handleSuccess(res, "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 a random question that matches the parameters provided. + * @param req + * @param res + */ +export const getQuestionByParameters = async (req: Request, res: Response) => { + const { limit, topics, difficulty } = req.query; + const stringLimit = limit as string + const newLimit = parseInt(stringLimit, 10); + + if (!topics) { + return handleBadRequest(res, 'Topics are required'); + } + if (!difficulty) { + return handleBadRequest(res, 'Difficulty is required'); + } + 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 }, + difficulty: difficulty, + }; + const questions = await Question.find(query).limit(newLimit); + + if (!questions || questions.length === 0) { + return handleNotFound(res, 'No questions found with the given parameters'); + } + + handleSuccess(res, "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 (e.g. “Sorting”, “OOP”, “DFS”, etc…) + * @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, "Topics retrieved successfully", topics); + } catch (error) { + console.error('Error in getTopics:', error); + handleError(res, error, 'Failed to retrieve topics'); + } +}; \ No newline at end of file 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..4e14460f10 100644 --- a/services/question/src/models/questionModel.ts +++ b/services/question/src/models/questionModel.ts @@ -37,7 +37,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..8896825ad4 100644 --- a/services/question/src/routes/questionRoutes.ts +++ b/services/question/src/routes/questionRoutes.ts @@ -1,7 +1,26 @@ import { Router } from 'express'; -import { getQuestions } from '../controllers/questionController'; -const router = Router(); +import { getQuestions, getQuestionById, getQuestionByParameters, getTopics } from '../controllers/questionController'; -router.get('/questions', getQuestions); +const questionRouter = Router(); -export default router; +/** + * To Test: curl -X GET "http://localhost:8081/questions/search?topics=Algorithms,Data%20Structures&difficulty=Easy&limit=5" + */ +questionRouter.get('/search', getQuestionByParameters); + +/** + * To Test: http://localhost:8081/topics + */ +questionRouter.get('/topics', getTopics); + +/** + * To Test: http://localhost:8081/questions/ + */ +questionRouter.get('/', getQuestions); + +/** + * To Test: http://localhost:8081/questions/1 + */ +questionRouter.get('/:id', getQuestionById); + +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..eb8bd85a61 --- /dev/null +++ b/services/question/src/utils/helpers.ts @@ -0,0 +1,51 @@ +import { Response } from 'express'; + +/** + * 500: Unexpected error in the database/server + * @param res + * @param error + * @param message + * @param statusCode + */ +export const handleError = (res: any, error: any, message = 'An unexpected error occurred', statusCode = 500) => { + console.error(error); + res.status(statusCode).json({ error }); +}; + +/** + * 400: Bad Request + * @param res + * @param error + * @param message + * @param statusCode + */ +export const handleBadRequest = (res: any, error: any, message = 'Bad Request', statusCode = 400) => { + console.error(error); + res.status(statusCode).json({ error }); +}; + +/** + * 404: Not Found + * @param res + * @param error + * @param message + * @param statusCode + */ +export const handleNotFound = (res: any, error: any, message = 'Not Found', statusCode = 404) => { + console.error(error); + res.status(statusCode).json({ error }); +}; + +/** + * 200: Success + * @param res + * @param message + * @param data + */ +export const handleSuccess = (res: Response, message: string, data: any) => { + res.status(200).json({ + status: 'success', + message, + data, + }); +}; \ No newline at end of file