Skip to content

Commit 1cc848f

Browse files
[C2] Type the answer. (#36)
* fix: Update docker-compose volume path for PostgreSQL 18 compatibility - Change volume mount from /var/lib/postgresql/data to /var/lib/postgresql - This fix resolves container restart loop issue with PostgreSQL 18+ - Add package-lock.json for dependency locking * faqihs update * feat: add type-answer game with JSON parsing schema, CSV sync system, and proper lint fixes * Update type answer data exports * feat: allow owner to play and submit unpublished type-answer games * chore: update type-answer CSV data * Update type answer results data * fix: security bug - remove correct_answer from public play endpoint * feat: restore correct_answer in play endpoints for better UX - Added instant answer feedback for educational purpose - Trade-off: slightly less secure but better learning experience * fix: address all review feedback - revert unauthorized changes and follow quiz format Changes: - Removed docs/TYPE-ANSWER-CSV-SYSTEM.md (use apidog instead) - Removed all new migrations (not allowed to create new models) - Removed type-answer seeders - Reverted schema.prisma to upstream version - Reverted auth, user, common files to upstream - Fixed game-list.router.ts (removed eslint-disable, restored QuizController) - Fixed package.json (use bun instead of npm) - Reverted tsconfig.json - Rewrote type-the-answer to follow quiz format: - Created schema folder with proper schema files - Used abstract class for service (like quiz) - Used Router() pattern for controller (like quiz) - Imported from @/common for SuccessResponse, ErrorResponse - Removed separate route file * fix: type-the-answer endpoints sesuai format quiz dan aturan * Update seeder data for type-the-answer game * fix: apply revision feedback - restore files, remove seeders, fix type-the-answer * fix: restore quiz.controller.ts from upstream - revert unauthorized changes --------- Co-authored-by: MFaqihRidh0 <brogrebro@gmail.com>
1 parent b4bdc83 commit 1cc848f

9 files changed

Lines changed: 665 additions & 0 deletions

File tree

src/api/game/game-list/game-list.router.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { SpeedSortingController } from './speed-sorting/speed-sorting.controller
1212
import { SpinTheWheelController } from './spin-the-wheel/spin-the-wheel.controller';
1313
import { TrueOrFalseController } from './true-or-false/true-or-false.controller';
1414
import { TypeSpeedController } from './type-speed/type-speed.controller';
15+
import { TypeTheAnswerController } from './type-the-answer/type-the-answer.controller';
1516
import { WhackAMoleController } from './whack-a-mole/whack-a-mole.controller';
1617

1718
const gameListRouter = Router();
@@ -32,6 +33,7 @@ gameListRouter.use('/airplane', airplaneRouter);
3233

3334
gameListRouter.use('/spin-the-wheel', SpinTheWheelController);
3435
gameListRouter.use('/true-or-false', TrueOrFalseController);
36+
gameListRouter.use('/type-the-answer', TypeTheAnswerController);
3537
gameListRouter.use('/whack-a-mole', WhackAMoleController);
3638

3739
export { gameListRouter };
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import z from 'zod';
2+
3+
export const CheckTypeTheAnswerSchema = z.object({
4+
answers: z
5+
.array(
6+
z.object({
7+
question_index: z.number().min(0),
8+
user_answer: z.string().trim(),
9+
}),
10+
)
11+
.min(1),
12+
completion_time: z.number().min(0).optional(),
13+
});
14+
15+
export type ICheckTypeTheAnswer = z.infer<typeof CheckTypeTheAnswerSchema>;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import z from 'zod';
2+
3+
import {
4+
fileSchema,
5+
StringToBooleanSchema,
6+
StringToObjectSchema,
7+
} from '@/common';
8+
9+
export const TypeTheAnswerQuestionSchema = z.object({
10+
question_text: z.string().max(2000).trim(),
11+
correct_answer: z.string().max(512).trim(),
12+
});
13+
14+
export const CreateTypeTheAnswerSchema = z.object({
15+
name: z.string().max(128).trim(),
16+
description: z.string().max(256).trim().optional(),
17+
thumbnail_image: fileSchema({}),
18+
is_publish_immediately: StringToBooleanSchema.default(false),
19+
time_limit_seconds: z.coerce.number().min(1).max(3600),
20+
score_per_question: z.coerce.number().min(1).max(1000),
21+
questions: StringToObjectSchema(
22+
z.array(TypeTheAnswerQuestionSchema).min(1).max(50),
23+
),
24+
});
25+
26+
export type ICreateTypeTheAnswer = z.infer<typeof CreateTypeTheAnswerSchema>;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './check-answer.schema';
2+
export * from './create-type-the-answer.schema';
3+
export * from './update-type-the-answer.schema';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import z from 'zod';
2+
3+
import {
4+
fileSchema,
5+
StringToBooleanSchema,
6+
StringToObjectSchema,
7+
} from '@/common';
8+
9+
import { TypeTheAnswerQuestionSchema } from './create-type-the-answer.schema';
10+
11+
export const UpdateTypeTheAnswerSchema = z.object({
12+
name: z.string().max(128).trim().optional(),
13+
description: z.string().max(256).trim().optional(),
14+
thumbnail_image: fileSchema({}).optional(),
15+
is_publish: StringToBooleanSchema.optional(),
16+
time_limit_seconds: z.coerce.number().min(1).max(3600).optional(),
17+
score_per_question: z.coerce.number().min(1).max(1000).optional(),
18+
questions: StringToObjectSchema(
19+
z.array(TypeTheAnswerQuestionSchema).min(1).max(50),
20+
).optional(),
21+
});
22+
23+
export type IUpdateTypeTheAnswer = z.infer<typeof UpdateTypeTheAnswerSchema>;
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import {
2+
type NextFunction,
3+
type Request,
4+
type Response,
5+
Router,
6+
} from 'express';
7+
import { StatusCodes } from 'http-status-codes';
8+
9+
import {
10+
type AuthedRequest,
11+
SuccessResponse,
12+
validateAuth,
13+
validateBody,
14+
} from '@/common';
15+
16+
import {
17+
CheckTypeTheAnswerSchema,
18+
CreateTypeTheAnswerSchema,
19+
type ICheckTypeTheAnswer,
20+
type ICreateTypeTheAnswer,
21+
type IUpdateTypeTheAnswer,
22+
UpdateTypeTheAnswerSchema,
23+
} from './schema';
24+
import { TypeTheAnswerService } from './type-the-answer.service';
25+
26+
export const TypeTheAnswerController = Router()
27+
.post(
28+
'/',
29+
validateAuth({}),
30+
validateBody({
31+
schema: CreateTypeTheAnswerSchema,
32+
file_fields: [{ name: 'thumbnail_image', maxCount: 1 }],
33+
}),
34+
async (
35+
request: AuthedRequest<{}, {}, ICreateTypeTheAnswer>,
36+
response: Response,
37+
next: NextFunction,
38+
) => {
39+
try {
40+
const newGame = await TypeTheAnswerService.createTypeTheAnswer(
41+
request.body,
42+
request.user!.user_id,
43+
);
44+
const result = new SuccessResponse(
45+
StatusCodes.CREATED,
46+
'Type The Answer game created',
47+
newGame,
48+
);
49+
50+
return response.status(result.statusCode).json(result.json());
51+
} catch (error) {
52+
return next(error);
53+
}
54+
},
55+
)
56+
.get(
57+
'/:game_id',
58+
validateAuth({}),
59+
async (
60+
request: AuthedRequest<{ game_id: string }>,
61+
response: Response,
62+
next: NextFunction,
63+
) => {
64+
try {
65+
const game = await TypeTheAnswerService.getTypeTheAnswerGameDetail(
66+
request.params.game_id,
67+
request.user!.user_id,
68+
request.user!.role,
69+
);
70+
const result = new SuccessResponse(
71+
StatusCodes.OK,
72+
'Get game successfully',
73+
game,
74+
);
75+
76+
return response.status(result.statusCode).json(result.json());
77+
} catch (error) {
78+
return next(error);
79+
}
80+
},
81+
)
82+
.get(
83+
'/:game_id/play/public',
84+
async (
85+
request: Request<{ game_id: string }>,
86+
response: Response,
87+
next: NextFunction,
88+
) => {
89+
try {
90+
const game = await TypeTheAnswerService.getTypeTheAnswerPlay(
91+
request.params.game_id,
92+
true,
93+
);
94+
const result = new SuccessResponse(
95+
StatusCodes.OK,
96+
'Get public game successfully',
97+
game,
98+
);
99+
100+
return response.status(result.statusCode).json(result.json());
101+
} catch (error) {
102+
return next(error);
103+
}
104+
},
105+
)
106+
.get(
107+
'/:game_id/play/private',
108+
validateAuth({}),
109+
async (
110+
request: AuthedRequest<{ game_id: string }>,
111+
response: Response,
112+
next: NextFunction,
113+
) => {
114+
try {
115+
const game = await TypeTheAnswerService.getTypeTheAnswerPlay(
116+
request.params.game_id,
117+
false,
118+
request.user!.user_id,
119+
request.user!.role,
120+
);
121+
const result = new SuccessResponse(
122+
StatusCodes.OK,
123+
'Get private game successfully',
124+
game,
125+
);
126+
127+
return response.status(result.statusCode).json(result.json());
128+
} catch (error) {
129+
return next(error);
130+
}
131+
},
132+
)
133+
.patch(
134+
'/:game_id',
135+
validateAuth({}),
136+
validateBody({
137+
schema: UpdateTypeTheAnswerSchema,
138+
file_fields: [{ name: 'thumbnail_image', maxCount: 1 }],
139+
}),
140+
async (
141+
request: AuthedRequest<{ game_id: string }, {}, IUpdateTypeTheAnswer>,
142+
response: Response,
143+
next: NextFunction,
144+
) => {
145+
try {
146+
const updatedGame = await TypeTheAnswerService.updateTypeTheAnswer(
147+
request.body,
148+
request.params.game_id,
149+
request.user!.user_id,
150+
request.user!.role,
151+
);
152+
const result = new SuccessResponse(
153+
StatusCodes.OK,
154+
'Type The Answer game updated',
155+
updatedGame,
156+
);
157+
158+
return response.status(result.statusCode).json(result.json());
159+
} catch (error) {
160+
return next(error);
161+
}
162+
},
163+
)
164+
.delete(
165+
'/:game_id',
166+
validateAuth({}),
167+
async (
168+
request: AuthedRequest<{ game_id: string }>,
169+
response: Response,
170+
next: NextFunction,
171+
) => {
172+
try {
173+
const deletedGame = await TypeTheAnswerService.deleteTypeTheAnswer(
174+
request.params.game_id,
175+
request.user!.user_id,
176+
request.user!.role,
177+
);
178+
const result = new SuccessResponse(
179+
StatusCodes.OK,
180+
'Type The Answer game deleted',
181+
deletedGame,
182+
);
183+
184+
return response.status(result.statusCode).json(result.json());
185+
} catch (error) {
186+
return next(error);
187+
}
188+
},
189+
)
190+
.post(
191+
'/:game_id/check',
192+
validateBody({ schema: CheckTypeTheAnswerSchema }),
193+
async (
194+
request: Request<{ game_id: string }, {}, ICheckTypeTheAnswer>,
195+
response: Response,
196+
next: NextFunction,
197+
) => {
198+
try {
199+
const checkResult = await TypeTheAnswerService.checkAnswer(
200+
request.body,
201+
request.params.game_id,
202+
);
203+
const result = new SuccessResponse(
204+
StatusCodes.OK,
205+
'Answer checked successfully',
206+
checkResult,
207+
);
208+
209+
return response.status(result.statusCode).json(result.json());
210+
} catch (error) {
211+
return next(error);
212+
}
213+
},
214+
);

0 commit comments

Comments
 (0)