diff --git a/peerprep/backend/question-service/src/controllers/questionController.ts b/peerprep/backend/question-service/src/controllers/questionController.ts index 5884d079e3..99e06aae36 100644 --- a/peerprep/backend/question-service/src/controllers/questionController.ts +++ b/peerprep/backend/question-service/src/controllers/questionController.ts @@ -55,7 +55,16 @@ export const createQuestion = async (req: Request, res: Response): Promise // Update a question export const updateQuestion = async (req: Request, res: Response): Promise => { + const { title } = req.body; + try { + // Check if another question with the same title exists (excluding the current question by ID) + const duplicateQuestion = await Question.findOne({ title, _id: { $ne: req.params.id } }); + if (duplicateQuestion) { + res.status(400).json({ message: 'A question with this title already exists' }); + return; + } + const updatedQuestion = await Question.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true @@ -154,3 +163,40 @@ export const getRandomQuestionByTopicAndDifficultyOld = async (topic: string, di throw new Error("Failed to retrieve a random question"); } }; + +// Get all unique categories +export const getAllCategories = async (req: Request, res: Response): Promise => { + try { + const categories = await Question.distinct('categories'); + res.status(200).json(categories); + } catch (error) { + res.status(500).json({ message: 'Error fetching categories', error }); + } +}; + +// Get all unique difficulties +export const getAllDifficulties = async (req: Request, res: Response): Promise => { + try { + const difficulties = await Question.distinct('difficulty'); + res.status(200).json(difficulties); + } catch (error) { + res.status(500).json({ message: 'Error fetching difficulties', error }); + } +}; + +// Check if a specific category and difficulty combination exists +export const checkCategoryDifficultyAvailability = async (req: Request, res: Response): Promise => { + const { category, difficulty } = req.query; + + try { + const question = await Question.findOne({ categories: category, difficulty: difficulty }); + if (question) { + res.status(200).json({ available: true }); + } else { + res.status(404).json({ available: false, message: 'No question found for the specified category and difficulty.' }); + } + } catch (error) { + res.status(500).json({ message: 'Error checking category and difficulty availability', error }); + } +}; + diff --git a/peerprep/backend/question-service/src/middleware/normalizationMiddleware.ts b/peerprep/backend/question-service/src/middleware/normalizationMiddleware.ts new file mode 100644 index 0000000000..1c98ece1d1 --- /dev/null +++ b/peerprep/backend/question-service/src/middleware/normalizationMiddleware.ts @@ -0,0 +1,13 @@ +import { Request, Response, NextFunction } from 'express'; + +export const normalizeQuestionData = (req: Request, res: Response, next: NextFunction): void => { + if (req.body) { + if (Array.isArray(req.body.categories)) { + req.body.categories = req.body.categories.map((cat: string) => cat.toLowerCase()); + } + if (typeof req.body.difficulty === 'string') { + req.body.difficulty = req.body.difficulty.toLowerCase(); + } + } + next(); +}; diff --git a/peerprep/backend/question-service/src/routes/questionRoutes.ts b/peerprep/backend/question-service/src/routes/questionRoutes.ts index 9484740d3b..c8e8bb01d7 100644 --- a/peerprep/backend/question-service/src/routes/questionRoutes.ts +++ b/peerprep/backend/question-service/src/routes/questionRoutes.ts @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { getQuestions, getQuestionById, createQuestion, updateQuestion, deleteQuestion, getRandomQuestionEndpoint } from '../controllers/questionController'; +import { getQuestions, getQuestionById, createQuestion, updateQuestion, deleteQuestion, getRandomQuestionEndpoint, getAllCategories, getAllDifficulties, checkCategoryDifficultyAvailability } from '../controllers/questionController'; const router: Router = Router(); @@ -7,9 +7,13 @@ const router: Router = Router(); router.get('/questions/random-question', getRandomQuestionEndpoint); router.get('/questions', getQuestions); router.get('/questions/:id', getQuestionById); +router.get('/categories', getAllCategories); +router.get('/difficulties', getAllDifficulties); +router.get('/availability', checkCategoryDifficultyAvailability); router.post('/questions', createQuestion); router.put('/questions/:id', updateQuestion); router.delete('/questions/:id', deleteQuestion); + export default router; diff --git a/peerprep/backend/question-service/src/sampleData.ts b/peerprep/backend/question-service/src/sampleData.ts index fb75a9f571..115c443620 100644 --- a/peerprep/backend/question-service/src/sampleData.ts +++ b/peerprep/backend/question-service/src/sampleData.ts @@ -1,54 +1,6 @@ import { Request, Response } from 'express'; import { createQuestion } from './controllers/questionController'; import Question from './models/questionModel'; -/* - { - questionId: 6, - title: 'Implement Stack using Queues', - 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).`, - categories: 'data-structures', - difficulty: 'easy', - }, - { - questionId: 7, - title: 'Combine Two Tables', - description: `Given table Person with the following columns: personId, lastName, firstName. And table Address with the following columns: addressId, personId, city, state. Write a solution to report the first name, last name, city, and state of each person.`, - categories: 'Databases', - difficulty: 'easy', - }, - { - questionId: 12, - title: 'Rotate Image', - description: `You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise).`, - categories: 'Arrays, algorithms', - difficulty: 'medium', - }, - { - questionId: 13, - title: 'Airplane Seat Assignment Probability', - description: `n passengers board an airplane with exactly n seats. The first passenger has lost the ticket and picks a seat randomly. After that, the rest of the passengers will: take their own seat if it is still available, or pick other seats randomly if their seat is taken. Return the probability that the nth person gets their own seat.`, - categories: 'Brainteaser', - difficulty: 'medium', - }, - { - questionId: 19, - title: 'Chalkboard XOR Game', - description: `You are given an array of integers representing numbers written on a chalkboard. Alice and Bob take turns erasing exactly one number from the chalkboard. Return true if and only if Alice wins the game, assuming both players play optimally.`, - categories: 'Brainteaser', - difficulty: 'hard', - }, - { - questionId: 18, - title: 'Wildcard Matching', - description: `Given an input string (s) and a pattern (p), implement wildcard pattern matching with support for '?' and '*' where: - - '?' matches any single character. - - '*' matches any sequence of characters (including the empty sequence). - - The matching should cover the entire input string (not partial).`, - categories: 'Strings, algorithms', - difficulty: 'hard', - } -*/ const sampleQuestions = [ { @@ -83,41 +35,69 @@ const sampleQuestions = [ { title: 'Add Binary', description: `Given two binary strings a and b, return their sum as a binary string.`, - categories: 'algorithms', + categories: 'bit-manipulation, algorithms', difficulty: 'easy', }, { title: 'Fibonacci Number', description: `The Fibonacci numbers, commonly denoted F(n) form a sequence, such that each number is the sum of the two preceding ones, starting from 0 and 1. That is, F(0) = 0, F(1) = 1 F(n) = F(n - 1) + F(n - 2), for n > 1. Given n, calculate F(n).`, - categories: 'algorithms', + categories: 'recursion, algorithms', + difficulty: 'easy', + }, + { + title: 'Implement Stack using Queues', + 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).`, + categories: 'data-structures', difficulty: 'easy', }, { title: 'Repeated DNA Sequences', - description: `Given a string s that represents a DNA sequence, return all the 10-letter-long sequences (substrings) that occur more than once in a DNA molecule.`, - categories: 'algorithms, graphs', + description: `The DNA sequence is composed of a series of nucleotides abbreviated as 'A', 'C', 'G', and 'T'. + + For example, "ACGAATTCCG" is a DNA sequence. When studying DNA, it is useful to identify repeated sequences within the DNA. + + Given a string s that represents a DNA sequence, return all the 10-letter-long sequences (substrings) that occur more than once in a DNA molecule. You may return the answer in any order.`, + categories: 'algorithms, bit-manipulation', difficulty: 'medium', }, { title: 'Course Schedule', - description: `There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai. Return true if you can finish all courses.`, + description: `There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai. + + For example, the pair [0, 1], indicates that to take course 0 you have to first take course 1. Return true if you can finish all courses. Otherwise, return false.`, categories: 'data-structures, algorithms', difficulty: 'medium', }, { title: 'LRU Cache Design', description: `Design and implement an LRU (Least Recently Used) cache.`, - categories: 'data-structures, dynamic-programming', + categories: 'data-structures', difficulty: 'medium', }, { title: 'Longest Common Subsequence', description: `Given two strings text1 and text2, return the length of their longest common subsequence. If there is no common subsequence, return 0. - A subsequence of a string is a new string generated from the original string with some characters deleted without changing the relative order of the remaining characters.`, + A subsequence of a string is a new string generated from the original string with some characters deleted without changing the relative order of the remaining characters. + + For example, "ace" is a subsequence of "abcde". A common subsequence of two strings is a subsequence that is common to both strings.`, categories: 'strings, algorithms', difficulty: 'medium', }, + { + title: 'Rotate Image', + description: `You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise).`, + categories: 'arrays, algorithms', + difficulty: 'medium', + }, + { + title: 'Airplane Seat Assignment Probability', + description: `n passengers board an airplane with exactly n seats. The first passenger has lost the ticket and picks a seat randomly. But after that, the rest of the passengers will: Take their own seat if it is still available, and Pick other seats randomly when they find their seat occupied + + Return the probability that the nth person gets his own seat.`, + categories: 'brainteaser', + difficulty: 'medium', + }, { title: 'Validate Binary Search Tree', description: `Given the root of a binary tree, determine if it is a valid binary search tree (BST).`, @@ -127,7 +107,7 @@ const sampleQuestions = [ { title: 'Sliding Window Maximum', description: `You are given an array of integers nums, there is a sliding window of size k which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves right by one position, return the maximum sliding window.`, - categories: 'graphs, algorithms', + categories: 'arrays, algorithms', difficulty: 'hard', }, { @@ -141,6 +121,26 @@ const sampleQuestions = [ description: `Serialization is the process of converting a data structure or object into a sequence of bits so that it can be stored in a file or memory buffer, or transmitted across a network connection. Design an algorithm to serialize and deserialize a binary tree.`, categories: 'data-structures, algorithms', difficulty: 'hard', + }, + { + title: 'Wildcard Matching', + description: `Given an input string (s) and a pattern (p), implement wildcard pattern matching with support for '?' and '*' where: + + '?' Matches any single character. '*' Matches any sequence of characters (including the empty sequence). The matching should cover the entire input string (not partial).`, + categories: 'strings, algorithms', + difficulty: 'hard', + }, + { + title: 'Chalkboard XOR Game', + description: `You are given an array of integers nums represents the numbers written on a chalkboard. + + Alice and Bob take turns erasing exactly one number from the chalkboard, with Alice starting first. If erasing a number causes the bitwise XOR of all the elements of the chalkboard to become 0, then that player loses. The bitwise XOR of one element is that element itself, and the bitwise XOR of no elements is 0. + + Also, if any player starts their turn with the bitwise XOR of all the elements of the chalkboard equal to 0, then that player wins. + + Return true if and only if Alice wins the game, assuming both players play optimally.`, + categories: 'brainteaser', + difficulty: 'hard', } ]; diff --git a/peerprep/backend/question-service/src/server.ts b/peerprep/backend/question-service/src/server.ts index c8155d59d8..72b8f8fbe0 100644 --- a/peerprep/backend/question-service/src/server.ts +++ b/peerprep/backend/question-service/src/server.ts @@ -9,6 +9,7 @@ import databaseRoutes from './routes/databaseRoutes'; import gptRoutes from './routes/gptRoutes'; import testcaseRoutes from './routes/testcaseRoutes'; import loadSampleData from './sampleData'; +import { normalizeQuestionData } from './middleware/normalizationMiddleware'; connectDB() // Initialize MongoDB connection .then(() => { @@ -44,6 +45,11 @@ app.use(cors({ } as CorsOptions)); app.use(express.json()); +// Apply normalization middleware to specific routes +// This middleware will normalize `categories` and `difficulty` fields to lowercase +app.use('/api/questions', normalizeQuestionData); + + // API routes app.use('/api', questionRoutes); diff --git a/peerprep/frontend/src/api/questionApi.ts b/peerprep/frontend/src/api/questionApi.ts index 99b24a8db2..dd8188bd9c 100644 --- a/peerprep/frontend/src/api/questionApi.ts +++ b/peerprep/frontend/src/api/questionApi.ts @@ -3,7 +3,7 @@ import { Question } from '../models/Question'; const API_URL = 'http://localhost:8080/api/questions'; -class ApiError extends Error { +export class ApiError extends Error { constructor(message: string, public statusCode?: number) { super(message); this.name = 'ApiError'; @@ -13,7 +13,14 @@ class ApiError extends Error { const handleApiError = (error: unknown): never => { if (axios.isAxiosError(error)) { const axiosError = error as AxiosError; + + // Explicitly cast response data as an object with a `message` string field if expected + const responseData = axiosError.response?.data as { message?: string } | undefined; + if (axiosError.response) { + if (axiosError.response.status === 400 && responseData?.message?.includes('title already exists')) { + throw new ApiError('A question with this title already exists.', 400); + } throw new ApiError(`API error: ${axiosError.response.statusText}`, axiosError.response.status); } else if (axiosError.request) { throw new ApiError('API error: No response received from the server'); @@ -21,10 +28,11 @@ const handleApiError = (error: unknown): never => { throw new ApiError(`API error: ${axiosError.message}`); } } else { - throw new ApiError(`API error: An unexpected error occurred ${error}`); + throw new ApiError(`API error: An unexpected error occurred ${String(error)}`); } }; + const validateQuestionData = (data: any): data is Question => { return ( typeof data === 'object' && @@ -38,6 +46,14 @@ const validateQuestionData = (data: any): data is Question => { ); }; +const normalizeQuestionData = (data: Omit): Omit => { + return { + ...data, + categories: data.categories.map(cat => cat.toLowerCase()), + difficulty: data.difficulty.toLowerCase() + }; +}; + export const fetchQuestions = async (): Promise => { try { const response = await axios.get(API_URL); @@ -53,7 +69,8 @@ export const fetchQuestions = async (): Promise => { export const createQuestion = async (questionData: Omit): Promise => { try { - const response = await axios.post(API_URL, questionData); + const normalizedData = normalizeQuestionData(questionData); + const response = await axios.post(API_URL, normalizedData); if (!validateQuestionData(response.data)) { throw new Error('Invalid question data received from server'); } @@ -65,7 +82,8 @@ export const createQuestion = async (questionData: Omit): Promi export const updateQuestion = async (id: string, questionData: Omit): Promise => { try { - const response = await axios.put(`${API_URL}/${id}`, questionData); + const normalizedData = normalizeQuestionData(questionData); + const response = await axios.put(`${API_URL}/${id}`, normalizedData); if (!validateQuestionData(response.data)) { throw new Error('Invalid question data received from server'); } diff --git a/peerprep/frontend/src/controllers/QuestionController.tsx b/peerprep/frontend/src/controllers/QuestionController.tsx index 224ea078d5..f635d79576 100644 --- a/peerprep/frontend/src/controllers/QuestionController.tsx +++ b/peerprep/frontend/src/controllers/QuestionController.tsx @@ -1,4 +1,5 @@ import * as api from '../api/questionApi'; +import { ApiError } from '../api/questionApi'; import { Question } from '../models/Question'; class QuestionController { @@ -40,16 +41,23 @@ class QuestionController { } static async createQuestion(questionData: Omit): Promise { - console.log('Creating question:', questionData); const error = QuestionController.validateQuestion(questionData); if (error) { - throw new Error(`Invalid question data: ${error}`); + throw new Error(error.message); // Throw validation error message directly } try { return await api.createQuestion(questionData); } catch (error) { - console.error('Error creating question:', error); - throw new Error('Failed to create question. Please check your input and try again.'); + if (error instanceof ApiError && error.statusCode === 400) { + console.error('Duplicate title error:', error.message); + throw new Error(error.message); + } else if (error instanceof Error) { + console.error('Error creating question:', error.message); + throw new Error('Failed to create question. Please check your input and try again.'); + } else { + console.error('An unknown error occurred'); + throw new Error('Failed to create question due to an unknown error.'); + } } } @@ -64,11 +72,20 @@ class QuestionController { } try { return await api.updateQuestion(id, questionData); - } catch (error) { - console.error('Error updating question:', error); - throw new Error('Failed to update question. Please check your input and try again.'); + } catch (error: unknown) { // Specify `unknown` type for error + if (error instanceof ApiError && error.statusCode === 400) { + console.error('Duplicate title error:', error.message); + throw new Error(error.message); // Show specific message for duplicate title + } else if (error instanceof Error) { // Check if error is a general Error instance + console.error('Error updating question:', error.message); + throw new Error('Failed to update question. Please check your input and try again.'); + } else { + console.error('An unknown error occurred'); + throw new Error('Failed to update question due to an unknown error.'); + } } } + static async deleteQuestion(id: string): Promise { if (!id || typeof id !== 'string') { diff --git a/peerprep/frontend/src/views/MatchingServiceViews/MatchingServiceMainView.tsx b/peerprep/frontend/src/views/MatchingServiceViews/MatchingServiceMainView.tsx index afbff9e483..176dff723a 100644 --- a/peerprep/frontend/src/views/MatchingServiceViews/MatchingServiceMainView.tsx +++ b/peerprep/frontend/src/views/MatchingServiceViews/MatchingServiceMainView.tsx @@ -158,9 +158,13 @@ const MatchingServiceMainView: React.FC = () => { diff --git a/peerprep/frontend/src/views/QuestionServiceViews/QuestionForm.tsx b/peerprep/frontend/src/views/QuestionServiceViews/QuestionForm.tsx index 7798850988..805f6902fe 100644 --- a/peerprep/frontend/src/views/QuestionServiceViews/QuestionForm.tsx +++ b/peerprep/frontend/src/views/QuestionServiceViews/QuestionForm.tsx @@ -73,7 +73,7 @@ const QuestionForm: React.FC = ({ onSubmit, initialData }) =>
- {['algorithms', 'data-structures', 'dynamic-programming', 'graphs', 'strings'].map(category => ( + {['algorithms', 'arrays', 'bit-manipulation', 'brainteaser', 'data-structures', 'dynamic-programming', 'graphs', 'recursion', 'strings'].map(category => (