Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update QuestionService Endpoints and Documentation #33

Merged
merged 20 commits into from
Sep 25, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: Update and add new endpoints for question service
- Update `getQuestions`: Include optional parameters for filtering.
- Update `getQuestionsByParameters`: Add randomisation.
- Add `addQuestion`: New endpoint to add a question.
- Add `updateQuestion`: New Endpoint to update an existing question.
- Add `deleteQuestion`: New Endpoint to delete a question.
  • Loading branch information
KhoonSun47 committed Sep 21, 2024
commit 5c6968588078e177320fd7af43fae4bef3f087dd
1 change: 0 additions & 1 deletion services/question/.gitignore
Original file line number Diff line number Diff line change
@@ -4,7 +4,6 @@
/node_modules
/.pnp
.pnp.js
/services/question/node_modules/

# testing
/coverage
162 changes: 142 additions & 20 deletions services/question/src/controllers/questionController.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,39 @@
import { Request, Response } from 'express';
import { handleError, handleNotFound, handleBadRequest, handleSuccess } from '../utils/helpers';
import { Question } from '../models/questionModel';
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.
* 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) => {
try {
const questions = await Question.find();
const {title, description, topics, difficulty} = req.query;
const query: any = {};

handleSuccess(res, "All questions retrieved successfully", questions);
if (title) {
query.title = {$regex: `^${title as string}$`, $options: 'i'};
}
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, $options: 'i'};
}
if (difficulty) {
query.difficulty = difficulty as string;
}

const questions = await Question.find(query);

if ((title || description || topics || difficulty) && questions.length === 0) {
return handleNotFound(res, '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');
@@ -24,43 +46,48 @@ export const getQuestions = async (req: Request, res: Response) => {
* @param res
*/
export const getQuestionById = async (req: Request, res: Response) => {
const { id } = req.params;
const {id} = req.params;

const newId = parseInt(id, 10);
if (isNaN(newId)) {
if (isNaN(parseInt(id, 10))) {
return handleBadRequest(res, 'Invalid question ID');
}

try {
const question = await Question.findOne({ id: newId });
const question = await Question.findOne({id: newId});

if (!question) {
return handleNotFound(res, 'Question not found');
}

handleSuccess(res, "Question with ID retrieved successfully", question);
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 a random question that matches the parameters provided.
* 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;
const stringLimit = limit as string
const newLimit = parseInt(stringLimit, 10);
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');
}
@@ -71,24 +98,30 @@ export const getQuestionByParameters = async (req: Request, res: Response) => {
try {
const topicsArray = (topics as string).split(',');
const query = {
topics: { $in: topicsArray },
topics: {$in: topicsArray, $options: 'i'},
difficulty: difficulty,
};
const questions = await Question.find(query).limit(newLimit);
const numOfQuestions = await Question.countDocuments(query);

if (!questions || questions.length === 0) {
if (numOfQuestions === 0) {
return handleNotFound(res, 'No questions found with the given parameters');
}

handleSuccess(res, "Questions with Parameters retrieved successfully", questions);
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 (e.g. “Sorting”, “OOP”, “DFS”, etc…)
* This endpoint retrieves all unique topics in the database
* @param req
* @param res
*/
@@ -100,9 +133,98 @@ export const getTopics = async (req: Request, res: Response) => {
return handleNotFound(res, 'No topics found');
}

handleSuccess(res, "Topics retrieved successfully", topics);
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 {id, title, description, topics, difficulty} = req.body;

if (!id) {
return handleBadRequest(res, 'ID is required');
}
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({id});
if (existingQuestion) {
return handleBadRequest(res, `A question with ID ${id} already exists`);
}

const newQuestion = new Question({
id,
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};

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');
}
};
1 change: 1 addition & 0 deletions services/question/src/models/questionModel.ts
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ const questionSchema = new Schema<IQuestion>(
type: Number,
required: true,
unique: true,
immutable: true,
},
title: {
type: String,
63 changes: 57 additions & 6 deletions services/question/src/routes/questionRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,77 @@
import { Router } from 'express';
import { getQuestions, getQuestionById, getQuestionByParameters, getTopics } from '../controllers/questionController';
import {Router} from 'express';
import {
getQuestions,
getQuestionById,
getQuestionByParameters,
getTopics,
addQuestion,
deleteQuestion,
updateQuestion
} from '../controllers/questionController';

/**
* All the routes related to questions.
* All the comments are happy scenarios.
*/

const questionRouter = Router();

/**
* To Test: curl -X GET "http://localhost:8081/questions/search?topics=Algorithms,Data%20Structures&difficulty=Easy&limit=5"
* curl -X GET "http://localhost:8081/questions/search?topics=Algorithms&difficulty=Medium"
* 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
* curl -X GET http://localhost:8081/topics
*/
questionRouter.get('/topics', getTopics);

/**
* To Test: http://localhost:8081/questions/
* curl -X GET http://localhost:8081/questions
* curl -X GET "http://localhost:8081/questions?title=Reverse%20a%20String"
* curl -X GET "http://localhost:8081/questions?description=string"
* curl -X GET "http://localhost:8081/questions?topics=Algorithms,Data%20Structures"
* curl -X GET "http://localhost:8081/questions?difficulty=Easy"
* curl -X GET "http://localhost:8081/questions?title=Reverse%20a%20String&difficulty=Easy"
* curl -X GET "http://localhost:8081/questions?description=string&topics=Algorithms"
* curl -X GET "http://localhost:8081/questions?title=Reverse%20a%20String&description=string&topics=Algorithms&difficulty=Easy"
*/
questionRouter.get('/', getQuestions);

/**
* To Test: http://localhost:8081/questions/1
* curl -X GET http://localhost:8081/questions/1
*/
questionRouter.get('/:id', getQuestionById);

/**
* curl -X POST http://localhost:8081/questions \
* -H "Content-Type: application/json" \
* -d '{
* "id": 21,
* "title": "New Question",
* "description": "This is a description for a new question.",
* "topics": ["Data Structures", "Algorithms"],
* "difficulty": "Medium"
* }'
*/
questionRouter.post('/', addQuestion);

/**
* 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"
* }'
*/
questionRouter.put('/:id', updateQuestion);

/**
* curl -X DELETE http://localhost:8081/questions/21
*/
questionRouter.delete('/:id', deleteQuestion);

export default questionRouter;
17 changes: 9 additions & 8 deletions services/question/src/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Response } from 'express';
import {Response} from 'express';

/**
* 500: Unexpected error in the database/server
@@ -9,7 +9,7 @@ import { Response } from 'express';
*/
export const handleError = (res: any, error: any, message = 'An unexpected error occurred', statusCode = 500) => {
console.error(error);
res.status(statusCode).json({ error });
res.status(statusCode).json({error});
};

/**
@@ -21,7 +21,7 @@ export const handleError = (res: any, error: any, message = 'An unexpected error
*/
export const handleBadRequest = (res: any, error: any, message = 'Bad Request', statusCode = 400) => {
console.error(error);
res.status(statusCode).json({ error });
res.status(statusCode).json({error});
};

/**
@@ -33,18 +33,19 @@ export const handleBadRequest = (res: any, error: any, message = 'Bad Request',
*/
export const handleNotFound = (res: any, error: any, message = 'Not Found', statusCode = 404) => {
console.error(error);
res.status(statusCode).json({ error });
res.status(statusCode).json({error});
};

/**
* 200: Success
* Handles successful responses.
* @param res
* @param statusCode - Default is 200
* @param message
* @param data
*/
export const handleSuccess = (res: Response, message: string, data: any) => {
res.status(200).json({
status: 'success',
export const handleSuccess = (res: Response, statusCode: number = 200, message: string, data: any) => {
res.status(statusCode).json({
status: 'Success',
message,
data,
});