Skip to content

Commit 8392bac

Browse files
authored
Merge pull request #110 from CS3219-AY2425S1/jehou/questiondb
Leetcode question filtering
2 parents 4bcec21 + b0d37be commit 8392bac

File tree

8 files changed

+638
-92
lines changed

8 files changed

+638
-92
lines changed

backend/question/docs/question.plantuml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,21 @@ entity Pagination {
2323
interface IQuestionServiceAPI {
2424
+ GET /
2525
+ POST /create [createQuestion: EditQuestion]
26-
+ POST /all [pagination: Pagination]
26+
+ POST /all [pagination: Pagination, title: string, complexity: string[], category: string[]]
2727
+ GET /{id}
2828
+ POST /{id}/update [updateQuestion: EditQuestion]
2929
+ POST /{id}/delete
30+
+ POST /pick-question [complexity: string[], category: string[]]
3031
}
3132

3233
note right of IQuestionServiceAPI
3334
- GET /: Check service status
3435
- POST /create: Create a new question
35-
- POST /all: Retrieve all questions with pagination
36+
- POST /all: Retrieve all questions with pagination (Supports filtering by title, complexiy and category)
3637
- GET /{id}: Retrieve a specific question by ID
3738
- POST /{id}/update: Update a specific question by ID
3839
- POST /{id}/delete: Delete a specific question by ID
40+
- POST /pick-question: Pick a random question given a list of complexities and categories
3941
end note
4042

4143
class QuestionService {

backend/question/docs/question.yaml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,39 @@ paths:
6666
type: integer
6767
default: 1
6868
description: Page number
69+
example: 1
6970
page_size:
7071
type: integer
7172
default: 10
7273
description: Number of questions per page
74+
example: 10
75+
title:
76+
type: string
77+
default: ""
78+
description: Filter by title
79+
example: "Two Sum"
80+
complexity:
81+
type: array
82+
items:
83+
type: string
84+
default: []
85+
description: Filter by complexities
86+
example: ["Easy", "Medium", "Hard"]
87+
category:
88+
type: array
89+
items:
90+
type: string
91+
default: []
92+
description: Filter by categories
93+
example:
94+
[
95+
"Hash Table",
96+
"Array",
97+
"Linked List",
98+
"Dynamic Programming",
99+
"Math",
100+
"String",
101+
]
73102
responses:
74103
"200":
75104
description: List of questions
@@ -155,6 +184,43 @@ paths:
155184
description: Question not found
156185
"500":
157186
description: Internal server error
187+
/pick-question:
188+
post:
189+
summary: Pick a random question given a list of complexities and categories
190+
requestBody:
191+
content:
192+
application/json:
193+
schema:
194+
type: object
195+
properties:
196+
complexity:
197+
type: array
198+
items:
199+
type: string
200+
default: []
201+
description: Filter by complexities
202+
example: ["Easy"]
203+
category:
204+
type: array
205+
items:
206+
type: string
207+
default: []
208+
description: Filter by categories
209+
example: ["Hash Table"]
210+
211+
responses:
212+
"200":
213+
description: Question picked
214+
content:
215+
application/json:
216+
schema:
217+
$ref: "#/components/schemas/Question"
218+
"400":
219+
description: Bad Request (validation error)
220+
"404":
221+
description: No Question found
222+
"500":
223+
description: Internal server error
158224

159225
components:
160226
schemas:

backend/question/src/routes/questionRoutes.ts

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Question, { TQuestion } from "../models/Question";
44
import {
55
createQuestionValidators,
66
idValidators,
7+
pickQuestionValidators,
78
updateQuestionValidators,
89
} from "./validators";
910

@@ -60,24 +61,30 @@ router.post("/all", async (req: Request, res: Response) => {
6061
let pagination = parseInt(req.body.pagination as string, 10) || 1; // Default page is 1
6162
const page_size = parseInt(req.body.page_size as string, 10) || 10; // Default limit is 10
6263
const skip = (pagination - 1) * page_size; // Calculate how many documents to skip
64+
65+
const { title, complexity, category } = req.body;
66+
67+
const query: any = { deleted: false };
68+
if (title && title !== "") query.title = { $regex: title, $options: "i" };
69+
if (complexity && complexity.length > 0)
70+
query.complexity = { $in: complexity };
71+
if (category && category.length > 0) query.category = { $in: category };
72+
6373
try {
64-
const questions = await Question.find(
65-
{ deleted: false },
66-
{
67-
questionid: 1,
68-
title: 1,
69-
description: 1,
70-
complexity: 1,
71-
category: 1,
72-
}
73-
)
74+
const questions = await Question.find(query, {
75+
questionid: 1,
76+
title: 1,
77+
description: 1,
78+
complexity: 1,
79+
category: 1,
80+
})
7481
.lean()
7582
.sort({ questionid: "ascending" })
7683
.skip(skip)
7784
.limit(page_size)
7885
.exec();
7986

80-
const total = await Question.countDocuments({ deleted: false }).exec();
87+
const total = await Question.countDocuments(query).exec();
8188
const totalPages = Math.ceil(total / page_size);
8289
if (totalPages < pagination) pagination = 1;
8390

@@ -123,6 +130,47 @@ router.get("/:id", [...idValidators], async (req: Request, res: Response) => {
123130
}
124131
});
125132

133+
// Retrieve a random question by complexity and category
134+
router.post(
135+
"/pick-question",
136+
[...pickQuestionValidators],
137+
async (req: Request, res: Response) => {
138+
const errors = validationResult(req);
139+
if (!errors.isEmpty()) {
140+
return res.status(400).json({ errors: errors.array() });
141+
}
142+
const { complexity, category } = req.body;
143+
144+
const query: any = { deleted: false };
145+
if (complexity) query.complexity = { $in: complexity };
146+
if (category) query.category = { $in: category };
147+
148+
try {
149+
const randomQuestion = await Question.aggregate([
150+
{ $match: query }, // Filter by complexity and category
151+
{ $sample: { size: 1 } }, // Randomly select one document
152+
{
153+
$project: {
154+
questionid: 1,
155+
title: 1,
156+
description: 1,
157+
complexity: 1,
158+
category: 1,
159+
},
160+
},
161+
]);
162+
163+
if (!randomQuestion.length) {
164+
return res.status(404).json({ message: "No questions found" });
165+
}
166+
167+
return res.json(randomQuestion[0]);
168+
} catch (error) {
169+
return res.status(500).send("Internal server error");
170+
}
171+
}
172+
);
173+
126174
// Update a specific question by id
127175
router.post(
128176
"/:id/update",
@@ -180,10 +228,6 @@ router.post(
180228
);
181229
return res.json(updatedQuestion);
182230
} catch (error) {
183-
//to catch pre-middleware defined error
184-
if (error instanceof Error) {
185-
return res.status(404).json(error.message);
186-
}
187231
return res.status(500).send("Internal server error");
188232
}
189233
}

backend/question/src/routes/validators.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,51 @@ export const createQuestionValidators = [
1010
throw new Error("Category must be a non-empty array");
1111
}
1212
//check if array contains only non-empty strings and trim whitespace
13-
if (!category.every((element) => typeof element === "string" && element.length > 0 && element.trim().length > 0)) {
13+
if (
14+
!category.every(
15+
(element) =>
16+
typeof element === "string" &&
17+
element.length > 0 &&
18+
element.trim().length > 0
19+
)
20+
) {
1421
throw new Error("Category must contain only non-empty strings");
1522
}
1623
return true;
17-
}
18-
),
24+
}),
1925
];
2026

2127
export const idValidators = [check("id").isInt({ min: 1 })];
2228

29+
export const pickQuestionValidators = [
30+
check("complexity").custom((complexity) => {
31+
if (!Array.isArray(complexity) || complexity.length === 0) {
32+
throw new Error("Complexity must be a non-empty array");
33+
}
34+
if (
35+
!complexity.every(
36+
(element) => typeof element === "string" && element.trim().length > 0
37+
)
38+
) {
39+
throw new Error("Complexity must contain only non-empty strings");
40+
}
41+
return true;
42+
}),
43+
check("category").custom((category) => {
44+
if (!Array.isArray(category) || category.length === 0) {
45+
throw new Error("Category must be a non-empty array");
46+
}
47+
if (
48+
!category.every(
49+
(element) => typeof element === "string" && element.trim().length > 0
50+
)
51+
) {
52+
throw new Error("Category must contain only non-empty strings");
53+
}
54+
return true;
55+
}),
56+
];
57+
2358
export const updateQuestionValidators = [
2459
check("id").isInt(),
2560
body().custom((body) => {
@@ -35,7 +70,14 @@ export const updateQuestionValidators = [
3570
throw new Error("Category must be a non-empty array");
3671
}
3772
//check if array contains only non-empty strings and trim whitespace
38-
if (!field.every((element) => typeof element === "string" && element.length > 0 && element.trim().length > 0)) {
73+
if (
74+
!field.every(
75+
(element) =>
76+
typeof element === "string" &&
77+
element.length > 0 &&
78+
element.trim().length > 0
79+
)
80+
) {
3981
throw new Error("Category must contain only non-empty strings");
4082
}
4183
} else {

0 commit comments

Comments
 (0)