Skip to content

Commit a3a05cd

Browse files
authored
feat(api): add questions voting (#409)
* feat(api): add questions voting * refactor(api): refactor questions upvoting * feat(openapi-types): generate new openapi types
1 parent 03b6dd4 commit a3a05cd

File tree

3 files changed

+149
-2
lines changed

3 files changed

+149
-2
lines changed

apps/api/modules/questions/questions.routes.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
2-
import { FastifyInstance, FastifyPluginAsync } from "fastify";
2+
import { FastifyPluginAsync } from "fastify";
3+
import { isPrismaError } from "../db/prismaErrors.util.js";
4+
import { PrismaErrorCode } from "../db/prismaErrors.js";
35
import {
46
deleteQuestionByIdSchema,
57
generateGetQuestionsSchema,
68
generatePatchQuestionByIdSchema,
79
generatePostQuestionsSchema,
810
generateGetQuestionByIdSchema,
11+
upvoteQuestionSchema,
12+
downvoteQuestionSchema,
913
} from "./questions.schemas.js";
1014

1115
const questionsPlugin: FastifyPluginAsync = async (fastify) => {
@@ -251,6 +255,89 @@ const questionsPlugin: FastifyPluginAsync = async (fastify) => {
251255
return reply.status(204);
252256
},
253257
});
258+
259+
fastify.withTypeProvider<TypeBoxTypeProvider>().route({
260+
url: "/questions/:id/votes",
261+
method: "POST",
262+
schema: upvoteQuestionSchema,
263+
async handler(request, reply) {
264+
const {
265+
params: { id },
266+
session: { data: sessionData },
267+
} = request;
268+
269+
if (!sessionData) {
270+
throw fastify.httpErrors.unauthorized();
271+
}
272+
273+
try {
274+
const questionVote = await fastify.db.questionVote.upsert({
275+
where: {
276+
userId_questionId: {
277+
userId: sessionData._user.id,
278+
questionId: id,
279+
},
280+
},
281+
update: {},
282+
create: {
283+
userId: sessionData._user.id,
284+
questionId: id,
285+
},
286+
});
287+
288+
return {
289+
data: {
290+
userId: questionVote.userId,
291+
questionId: questionVote.questionId,
292+
},
293+
};
294+
} catch (err) {
295+
if (isPrismaError(err)) {
296+
switch (err.code) {
297+
case PrismaErrorCode.ForeignKeyViolation:
298+
throw fastify.httpErrors.notFound(`Question with id: ${id} not found!`);
299+
}
300+
}
301+
302+
throw err;
303+
}
304+
},
305+
});
306+
307+
fastify.withTypeProvider<TypeBoxTypeProvider>().route({
308+
url: "/questions/:id/votes",
309+
method: "DELETE",
310+
schema: downvoteQuestionSchema,
311+
async handler(request, reply) {
312+
const {
313+
params: { id },
314+
session: { data: sessionData },
315+
} = request;
316+
317+
if (!sessionData) {
318+
throw fastify.httpErrors.unauthorized();
319+
}
320+
321+
const question = await fastify.db.question.findFirst({
322+
where: {
323+
id,
324+
},
325+
});
326+
327+
if (!question) {
328+
throw fastify.httpErrors.notFound(`Question with id: ${id} not found!`);
329+
}
330+
331+
await fastify.db.questionVote.deleteMany({
332+
where: {
333+
userId: sessionData._user.id,
334+
questionId: id,
335+
},
336+
});
337+
338+
return reply.status(204).send();
339+
},
340+
});
254341
};
255342

256343
export default questionsPlugin;

apps/api/modules/questions/questions.schemas.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,26 @@ export const deleteQuestionByIdSchema = {
166166
id: Type.Integer(),
167167
}),
168168
};
169+
170+
export const upvoteQuestionSchema = {
171+
params: Type.Object({
172+
id: Type.Integer(),
173+
}),
174+
response: {
175+
200: Type.Object({
176+
data: Type.Object({
177+
userId: Type.Integer(),
178+
questionId: Type.Integer(),
179+
}),
180+
}),
181+
},
182+
};
183+
184+
export const downvoteQuestionSchema = {
185+
params: Type.Object({
186+
id: Type.Integer(),
187+
}),
188+
response: {
189+
204: Type.Never(),
190+
},
191+
};

packages/openapi-types/types.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,43 @@ export interface paths {
216216
};
217217
};
218218
};
219+
"/questions/{id}/votes": {
220+
post: {
221+
parameters: {
222+
path: {
223+
id: number;
224+
};
225+
};
226+
responses: {
227+
/** @description Default Response */
228+
200: {
229+
content: {
230+
"application/json": {
231+
data: {
232+
userId: number;
233+
questionId: number;
234+
};
235+
};
236+
};
237+
};
238+
};
239+
};
240+
delete: {
241+
parameters: {
242+
path: {
243+
id: number;
244+
};
245+
};
246+
responses: {
247+
/** @description Default Response */
248+
204: {
249+
content: {
250+
"application/json": boolean & true;
251+
};
252+
};
253+
};
254+
};
255+
};
219256
"/": {
220257
get: {
221258
responses: {
@@ -233,7 +270,7 @@ export interface paths {
233270
export type webhooks = Record<string, never>;
234271

235272
export interface components {
236-
schemas: {};
273+
schemas: never;
237274
responses: never;
238275
parameters: never;
239276
requestBodies: never;

0 commit comments

Comments
 (0)