Skip to content

Commit f2199ee

Browse files
authored
Merge pull request #27 from CS3219-AY2425S1/anun/question-get
get endpoints for questions
2 parents 997e5db + 8ba50b1 commit f2199ee

File tree

4 files changed

+206
-29
lines changed

4 files changed

+206
-29
lines changed

backend/question/src/controller/search-controller.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,73 @@
11
import { Request, Response } from 'express';
2-
import { searchQuestionsByTitleService } from '../services/get/index';
2+
import {
3+
getQuestionsService,
4+
getQuestionDetailsService,
5+
getRandomQuestionService,
6+
searchQuestionsByTitleService,
7+
} from '../services/get/';
8+
import {
9+
IGetQuestionsPayload,
10+
IGetQuestionPayload,
11+
IGetRandomQuestionPayload,
12+
} from '../services/get/types';
13+
14+
export const getQuestions = async (req: Request, res: Response): Promise<Response> => {
15+
const payload: IGetQuestionsPayload = {
16+
questionName: req.query.questionName as string,
17+
difficulty: req.query.difficulty as string,
18+
topic: req.query.topic as string[],
19+
pageNum: parseInt(req.query.pageNum as string) || 0,
20+
recordsPerPage: parseInt(req.query.recordsPerPage as string) || 20,
21+
};
22+
23+
try {
24+
const result = await getQuestionsService(payload);
25+
return res.status(result.code).json(result);
26+
} catch (error) {
27+
return res.status(500).json({ success: false, message: 'An error occurred', error });
28+
}
29+
};
30+
31+
export const getQuestionDetails = async (req: Request, res: Response): Promise<Response> => {
32+
const payload: IGetQuestionPayload = {
33+
questionId: parseInt(req.params.questionId),
34+
};
35+
36+
try {
37+
const result = await getQuestionDetailsService(payload);
38+
return res.status(result.code).json(result);
39+
} catch (error) {
40+
return res.status(500).json({ success: false, message: 'An error occurred', error });
41+
}
42+
};
43+
44+
export const getRandomQuestion = async (req: Request, res: Response): Promise<Response> => {
45+
const payload: IGetRandomQuestionPayload = {
46+
userId: parseInt(req.params.userId),
47+
difficulty: req.query.difficulty as string,
48+
topic: req.query.topic as string[],
49+
};
50+
51+
try {
52+
const result = await getRandomQuestionService(payload);
53+
return res.status(result.code).json(result);
54+
} catch (error) {
55+
return res.status(500).json({ success: false, message: 'An error occurred', error });
56+
}
57+
};
358

459
export const searchQuestionsByTitle = async (req: Request, res: Response): Promise<Response> => {
560
const { title } = req.query;
661
const page = parseInt(req.query.page as string) || 1;
762
const limit = parseInt(req.query.limit as string) || 10;
863

964
if (!title) {
10-
return res.status(200);
65+
return res.status(400).json({ success: false, message: 'Title is required' });
1166
}
1267

1368
try {
1469
const result = await searchQuestionsByTitleService(title.toString(), page, limit);
15-
return res.status(200).json(result);
70+
return res.status(result.code).json(result);
1671
} catch (error) {
1772
return res.status(500).json({ success: false, message: 'An error occurred', error });
1873
}
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
import { Router } from 'express';
2-
import { searchQuestionsByTitle } from '../controller/search-controller';
2+
import {
3+
searchQuestionsByTitle,
4+
getQuestions,
5+
getQuestionDetails,
6+
getRandomQuestion,
7+
} from '../controller/search-controller';
38

49
const router = Router();
510

611
router.get('/search', searchQuestionsByTitle);
712

13+
router.get('/', getQuestions);
14+
15+
router.get('/:questionId', getQuestionDetails);
16+
17+
router.get('/random', getRandomQuestion);
18+
819
export default router;

backend/question/src/services/get/index.ts

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,129 @@
11
import { db } from '../../lib/db/index';
2-
import { sql } from 'drizzle-orm';
2+
import { and, arrayOverlaps, eq, ilike, sql } from 'drizzle-orm';
33
import { questions } from '../../lib/db/schema';
4-
import { IGetQuestionsResponse } from '../get/types';
4+
import {
5+
IGetQuestionsPayload,
6+
IGetQuestionsResponse,
7+
IGetQuestionPayload,
8+
IGetQuestionResponse,
9+
IGetRandomQuestionPayload,
10+
IGetRandomQuestionResponse,
11+
} from '../get/types';
12+
13+
export const getQuestionsService = async (
14+
payload: IGetQuestionsPayload
15+
): Promise<IGetQuestionsResponse> => {
16+
const { questionName, difficulty, topic, pageNum = 0, recordsPerPage = 20 } = payload;
17+
const offset = pageNum * recordsPerPage;
18+
19+
const whereClause = [];
20+
21+
if (questionName) {
22+
whereClause.push(ilike(questions.title, `%${questionName}%`));
23+
}
24+
if (difficulty) {
25+
whereClause.push(eq(questions.difficulty, difficulty));
26+
}
27+
if (topic && topic.length > 0) {
28+
whereClause.push(arrayOverlaps(questions.topic, topic));
29+
}
30+
31+
const query = db
32+
.select()
33+
.from(questions)
34+
.where(and(...whereClause))
35+
.limit(recordsPerPage)
36+
.offset(offset);
37+
38+
const [results, totalCount] = await Promise.all([
39+
query,
40+
db
41+
.select({ count: questions.id })
42+
.from(questions)
43+
.where(and(...whereClause))
44+
.then((res) => res.length),
45+
]);
46+
47+
return {
48+
code: 200,
49+
data: {
50+
questions: results.map((q) => ({
51+
id: q.id,
52+
title: q.title,
53+
difficulty: q.difficulty,
54+
topic: q.topic,
55+
})),
56+
totalQuestions: totalCount,
57+
},
58+
};
59+
};
60+
61+
export const getQuestionDetailsService = async (
62+
payload: IGetQuestionPayload
63+
): Promise<IGetQuestionResponse> => {
64+
const { questionId } = payload;
65+
66+
const result = await db
67+
.select()
68+
.from(questions)
69+
.where(sql`${questions.id} = ${questionId}`)
70+
.limit(1);
71+
72+
if (result.length === 0) {
73+
return {
74+
code: 404,
75+
data: { question: null },
76+
error: {
77+
message: 'Question not found',
78+
},
79+
};
80+
}
81+
82+
return {
83+
code: 200,
84+
data: { question: result[0] },
85+
};
86+
};
87+
88+
export const getRandomQuestionService = async (
89+
payload: IGetRandomQuestionPayload
90+
): Promise<IGetRandomQuestionResponse> => {
91+
const { difficulty, topic } = payload;
92+
const whereClause = [];
93+
94+
if (difficulty) {
95+
whereClause.push(eq(questions.difficulty, difficulty));
96+
}
97+
98+
if (topic && topic.length > 0) {
99+
whereClause.push(arrayOverlaps(questions.topic, topic));
100+
}
101+
102+
// randomize the order of questions
103+
const query = db
104+
.select()
105+
.from(questions)
106+
.where(and(...whereClause))
107+
.orderBy(sql`RANDOM()`)
108+
.limit(1);
109+
110+
const result = await query;
111+
112+
if (result.length === 0) {
113+
return {
114+
code: 404,
115+
data: { question: null },
116+
error: {
117+
message: 'No matching questions found',
118+
},
119+
};
120+
}
121+
122+
return {
123+
code: 200,
124+
data: { question: result[0] },
125+
};
126+
};
5127

6128
export const searchQuestionsByTitleService = async (
7129
title: string,

backend/question/src/services/get/types.ts

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,17 @@ export type IGetQuestionsResponse = IServiceResponse<{
2828
// /details
2929
//=============================================================================
3030
export type IGetQuestionPayload = {
31-
questionNum: number;
31+
questionId: number;
3232
};
3333

3434
export type IGetQuestionResponse = IServiceResponse<{
3535
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
36-
question: {
37-
// TODO: Add schema from db
38-
/**
39-
* - name
40-
* - number
41-
* - description
42-
* - difficulty
43-
* - topic
44-
* - submissionHistory?: TBC
45-
*/
46-
};
36+
question?: {
37+
title: string; // name or title of the question
38+
description: string; // question description
39+
difficulty: string; // difficulty level (e.g., 'easy', 'medium', 'hard')
40+
topic: Array<string>; // array of topics the question belongs to
41+
} | null;
4742
}>;
4843

4944
//=============================================================================
@@ -56,16 +51,10 @@ export type IGetRandomQuestionPayload = {
5651
};
5752

5853
export type IGetRandomQuestionResponse = IServiceResponse<{
59-
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
6054
question: {
61-
// TODO: Add schema from db
62-
/**
63-
* - name
64-
* - number
65-
* - description
66-
* - difficulty
67-
* - topic
68-
* - submissionHistory?: TBC
69-
*/
70-
};
55+
title: string; // name or title of the question
56+
description: string; // question description
57+
difficulty: string; // difficulty level (e.g., 'easy', 'medium', 'hard')
58+
topic: Array<string>; // array of topics the question belongs to
59+
} | null;
7160
}>;

0 commit comments

Comments
 (0)