Skip to content

Commit fef3f1e

Browse files
authored
Merge pull request #27 from CS3219-AY2425S1/feat/question-service
Feat/question service
2 parents 5cd96ae + 53521d9 commit fef3f1e

File tree

11 files changed

+297
-41
lines changed

11 files changed

+297
-41
lines changed

docs/peerprep.postman_collection.json

Lines changed: 148 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
"_postman_id": "2b9fc136-67a7-4241-a816-a4cb71927c39",
44
"name": "peerprep",
55
"description": "# 🚀 Get started here\n\nThis template guides you through CRUD operations (GET, POST, PUT, DELETE), variables, and tests.\n\n## 🔖 **How to use this template**\n\n#### **Step 1: Send requests**\n\nRESTful APIs allow you to perform CRUD operations using the POST, GET, PUT, and DELETE HTTP methods.\n\nThis collection contains each of these [request](https://learning.postman.com/docs/sending-requests/requests/) types. Open each request and click \"Send\" to see what happens.\n\n#### **Step 2: View responses**\n\nObserve the response tab for status code (200 OK), response time, and size.\n\n#### **Step 3: Send new Body data**\n\nUpdate or add new data in \"Body\" in the POST request. Typically, Body data is also used in PUT request.\n\n```\n{\n \"name\": \"Add your name in the body\"\n}\n\n ```\n\n#### **Step 4: Update the variable**\n\nVariables enable you to store and reuse values in Postman. We have created a [variable](https://learning.postman.com/docs/sending-requests/variables/) called `base_url` with the sample request [https://postman-api-learner.glitch.me](https://postman-api-learner.glitch.me). Replace it with your API endpoint to customize this collection.\n\n#### **Step 5: Add tests in the \"Scripts\" tab**\n\nAdding tests to your requests can help you confirm that your API is working as expected. You can write test scripts in JavaScript and view the output in the \"Test Results\" tab.\n\n<img src=\"https://content.pstmn.io/fa30ea0a-373d-4545-a668-e7b283cca343/aW1hZ2UucG5n\" width=\"2162\" height=\"1530\">\n\n## 💪 Pro tips\n\n- Use folders to group related requests and organize the collection.\n \n- Add more [scripts](https://learning.postman.com/docs/writing-scripts/intro-to-scripts/) to verify if the API works as expected and execute workflows.\n \n\n## 💡Related templates\n\n[API testing basics](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=e9a37a28-055b-49cd-8c7e-97494a21eb54&sourceTemplateId=ddb19591-3097-41cf-82af-c84273e56719) \n[API documentation](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=e9c28f47-1253-44af-a2f3-20dce4da1f18&sourceTemplateId=ddb19591-3097-41cf-82af-c84273e56719) \n[Authorization methods](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=31a9a6ed-4cdf-4ced-984c-d12c9aec1c27&sourceTemplateId=ddb19591-3097-41cf-82af-c84273e56719)",
6-
"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json",
7-
"_exporter_id": "27257144",
8-
"_collection_link": "https://cs3219-g11.postman.co/workspace/CS3219-G11~db882cad-4ce9-4503-8852-cda2be0074c7/collection/24939823-2b9fc136-67a7-4241-a816-a4cb71927c39?action=share&source=collection_link&creator=27257144"
6+
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
7+
"_exporter_id": "24939823",
8+
"_collection_link": "https://cs3219-g11.postman.co/workspace/CS3219-G11~db882cad-4ce9-4503-8852-cda2be0074c7/collection/24939823-2b9fc136-67a7-4241-a816-a4cb71927c39?action=share&source=collection_link&creator=24939823"
99
},
1010
"item": [
1111
{
@@ -30,13 +30,22 @@
3030
"request": {
3131
"method": "GET",
3232
"header": [],
33-
"url": "{{question_base_url}}/questions/",
33+
"url": {
34+
"raw": "{{question_base_url}}/questions/",
35+
"host": [
36+
"{{question_base_url}}"
37+
],
38+
"path": [
39+
"questions",
40+
""
41+
]
42+
},
3443
"description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data."
3544
},
3645
"response": []
3746
},
3847
{
39-
"name": "Delete data",
48+
"name": "user questions",
4049
"event": [
4150
{
4251
"listen": "test",
@@ -67,10 +76,91 @@
6776
}
6877
}
6978
},
70-
"url": "{{question_base_url}}/userquestions/",
79+
"url": {
80+
"raw": "{{question_base_url}}/userquestions/",
81+
"host": [
82+
"{{question_base_url}}"
83+
],
84+
"path": [
85+
"userquestions",
86+
""
87+
]
88+
},
7189
"description": "This is a DELETE request, and it is used to delete data that was previously created via a POST request. You typically identify the entity being updated by including an identifier in the URL (eg. `id=1`).\n\nA successful DELETE request typically returns a `200 OK`, `202 Accepted`, or `204 No Content` response code."
7290
},
7391
"response": []
92+
},
93+
{
94+
"name": "create question",
95+
"request": {
96+
"method": "POST",
97+
"header": [],
98+
"body": {
99+
"mode": "raw",
100+
"raw": "{\r\n \"difficulty\": 3,\r\n \"description\": \"What is the capital of Singapore?\",\r\n \"examples\": [\"The capital might be Yishun\", \"The capital might not be Yishun?\"],\r\n \"constraints\": \"None\",\r\n \"tags\": [\"geography\", \"capital\"],\r\n \"title_slug\": \"what-is-the-capital-of-singapore\",\r\n \"title\": \"Capital of Singapore\",\r\n \"pictures\": []\r\n}",
101+
"options": {
102+
"raw": {
103+
"language": "json"
104+
}
105+
}
106+
},
107+
"url": {
108+
"raw": "{{question_base_url}}/questions/",
109+
"host": [
110+
"{{question_base_url}}"
111+
],
112+
"path": [
113+
"questions",
114+
""
115+
]
116+
}
117+
},
118+
"response": []
119+
},
120+
{
121+
"name": "update question",
122+
"request": {
123+
"method": "PUT",
124+
"header": [],
125+
"body": {
126+
"mode": "raw",
127+
"raw": "{\r\n \"difficulty\": 3,\r\n \"description\": \"What is the capital city of France?\",\r\n \"examples\": [\"Paris is known as the capital of France.\", \"Paris kinda stinky maybe\"],\r\n \"constraints\": \"None\",\r\n \"tags\": [\"geography\", \"capital\", \"France\"],\r\n \"title_slug\": \"capital-of-france\",\r\n \"title\": \"Capital of France Updated\",\r\n \"pictures\": []\r\n}",
128+
"options": {
129+
"raw": {
130+
"language": "json"
131+
}
132+
}
133+
},
134+
"url": {
135+
"raw": "{{question_base_url}}/questions/66f77d44d9fc757910483d70",
136+
"host": [
137+
"{{question_base_url}}"
138+
],
139+
"path": [
140+
"questions",
141+
"66f77d44d9fc757910483d70"
142+
]
143+
}
144+
},
145+
"response": []
146+
},
147+
{
148+
"name": "delete question",
149+
"request": {
150+
"method": "DELETE",
151+
"header": [],
152+
"url": {
153+
"raw": "{{question_base_url}}/questions/66f77d44d9fc757910483d70",
154+
"host": [
155+
"{{question_base_url}}"
156+
],
157+
"path": [
158+
"questions",
159+
"66f77d44d9fc757910483d70"
160+
]
161+
}
162+
},
163+
"response": []
74164
}
75165
]
76166
},
@@ -91,7 +181,57 @@
91181
}
92182
}
93183
},
94-
"url": "{{account_base_url}}/register"
184+
"url": {
185+
"raw": "{{account_base_url}}/register",
186+
"host": [
187+
"{{account_base_url}}"
188+
],
189+
"path": [
190+
"register"
191+
]
192+
}
193+
},
194+
"response": []
195+
},
196+
{
197+
"name": "manual login",
198+
"request": {
199+
"method": "POST",
200+
"header": [],
201+
"body": {
202+
"mode": "raw",
203+
"raw": "{\n \"email\": \"[email protected]\",\n \"password\": \"securePassword123\"\n}",
204+
"options": {
205+
"raw": {
206+
"language": "json"
207+
}
208+
}
209+
},
210+
"url": {
211+
"raw": "{{account_base_url}}/login",
212+
"host": [
213+
"{{account_base_url}}"
214+
],
215+
"path": [
216+
"login"
217+
]
218+
}
219+
},
220+
"response": []
221+
},
222+
{
223+
"name": "token login",
224+
"request": {
225+
"method": "GET",
226+
"header": []
227+
},
228+
"response": []
229+
},
230+
{
231+
"name": "Github OAuth",
232+
"request": {
233+
"method": "GET",
234+
"header": []
95235
},
96236
"response": []
97237
}
@@ -125,7 +265,7 @@
125265
},
126266
{
127267
"key": "question_base_url",
128-
"value": "http://34.124.133.152:4001/api/v1",
268+
"value": "localhost:4001/api/v1",
129269
"type": "string"
130270
},
131271
{

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"scripts": {
33
"pre-commit": "npm run --workspaces pre-commit",
4-
"prepare": "husky"
4+
"prepare": "husky prepare"
55
},
66
"devDependencies": {
77
"husky": "^9.1.6"

peerprep-fe/src/app/(main)/components/problems/ProblemRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default function ProblemRow({ problem }: { problem: Problem }) {
2121
const handleProblemClick = async () => {
2222
// Fetch problem details here (replace with actual API call)
2323
const details: ProblemDialogData = {
24-
question_id: problem.question_id,
24+
_id: problem._id,
2525
title: problem.title,
2626
difficulty: problem.difficulty,
2727
description: problem.description,

peerprep-fe/src/app/(main)/components/problems/ProblemTable.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ export default function ProblemTable({ problems }: ProblemTableProps) {
2929
</tr>
3030
</thead>
3131
<tbody>
32-
{filteredProblems.map((problem, index) => (
33-
<ProblemRow key={index} problem={problem} />
32+
{filteredProblems.map((problem) => (
33+
// should not use index as key
34+
<ProblemRow key={problem._id} problem={problem} />
3435
))}
3536
</tbody>
3637
</table>

peerprep-fe/src/types/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
interface Problem {
2-
question_id: number;
2+
_id: number;
33
title: string;
44
difficulty: number;
55
description: string;
@@ -11,7 +11,7 @@ interface Problem {
1111
}
1212

1313
interface ProblemDialogData {
14-
question_id: number;
14+
_id: number;
1515
title: string;
1616
difficulty: number;
1717
description: string;

question-service/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,8 @@ docker run -p 4001:4001 --env-file .env.dev question-service
1111

1212
## Routes Endpoint
1313

14-
- GET /questions/ - get all coding questions
15-
- GET /userQuestions/ - get all user questions
14+
- GET /api/v1/questions/ - get all coding questions
15+
- PUT /api/v1/questions/:id - update a coding question
16+
- POST /api/v1/questions/ - create a coding question
17+
- DELETE /api/v1/questions/:id - delete a coding question
18+
- GET /api/v1/userQuestions/ - get all user questions

question-service/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"express": "^4.21.0",
2323
"mongodb": "^6.9.0",
2424
"mongoose": "^8.6.3",
25-
"prettier": "^3.3.3"
25+
"prettier": "^3.3.3",
26+
"zod": "^3.23.8"
2627
},
2728
"devDependencies": {
2829
"@types/cors": "^2.8.17",

question-service/pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,45 @@
1-
import { UUID } from 'crypto';
1+
import { z } from 'zod';
2+
import { ObjectId } from 'mongodb';
23

3-
export interface Questions {
4-
_question_id: number;
5-
difficulty: number; // 1, 2, 3
6-
description: string;
7-
examples: string[];
8-
constraints: string;
9-
tags: string[];
10-
title_slug: string;
11-
title: string;
12-
pictures?: File[];
13-
}
4+
// object id schema
5+
const objectIdSchema = z.instanceof(ObjectId);
6+
// image file schema
7+
const MAX_FILE_SIZE = 5000000
8+
const ACCEPTED_IMAGE_TYPES = [
9+
'image/jpeg',
10+
'image/jpg',
11+
'image/png',
12+
'image/webp',
13+
]
1414

15-
export interface UserQuestions {
16-
_user_id: UUID;
17-
_question_id: UUID;
18-
status: string; // 'completed', 'in-progress', 'not-started'
19-
}
15+
const imageSchema = z
16+
.any()
17+
// To not allow empty files
18+
.refine((files) => files?.length >= 1, { message: 'Image is required.' })
19+
// To not allow files other than images
20+
.refine((files) => ACCEPTED_IMAGE_TYPES.includes(files?.[0]?.type), {
21+
message: '.jpg, .jpeg, .png and .webp files are accepted.',
22+
})
23+
// To not allow files larger than 5MB
24+
.refine((files) => files?.[0]?.size <= MAX_FILE_SIZE, {
25+
message: `Max file size is 5MB.`,
26+
})
27+
28+
export const QuestionsSchema = z.object({
29+
_id: objectIdSchema,
30+
difficulty: z.number(),
31+
description: z.string(),
32+
examples: z.array(z.string()),
33+
constraints: z.string(),
34+
tags: z.array(z.string()),
35+
title_slug: z.string(),
36+
title: z.string(),
37+
pictures: z.array(imageSchema).optional(),
38+
});
39+
40+
export const UserQuestionsSchema = z.object({
41+
_id: objectIdSchema,
42+
_user_id: objectIdSchema,
43+
_question_id: objectIdSchema,
44+
status: z.enum(['completed', 'in-progress', 'not-started']),
45+
});

0 commit comments

Comments
 (0)