Skip to content

Commit c109833

Browse files
[Questions] CRUD endpoints (#3)
This PR adds API endpoints for questions-service: `cmd`: -`get_questions` -`get_question` -`create_question` -`update_question` -`delete_question` Postman Collection: https://www.postman.com/gold-station-786386/workspace/peerprep/collection/27342778-6fbf20dc-66a8-4c5a-b43d-a59d1efe03c7?action=share&creator=27342778&active-environment=27342778-f05ea77e-113b-46ee-a7e3-348077563858 Note: Need to add new env vars - check `README.md` Supabase URL and key can be found [here](https://supabase.com/dashboard/project/ncxgumrscmhhksignpks/settings/api) --------- Co-authored-by: javinchua <[email protected]>
1 parent bcc66ab commit c109833

File tree

11 files changed

+362
-58
lines changed

11 files changed

+362
-58
lines changed
Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,64 @@
11
// apps/backend/api-gateway/src/questions/questions.controller.ts
22

3-
import { Controller, Get, Param, Inject, Body, Post } from '@nestjs/common';
3+
import {
4+
Controller,
5+
Get,
6+
Param,
7+
Inject,
8+
Body,
9+
Post,
10+
Put,
11+
Delete,
12+
Query,
13+
} from '@nestjs/common';
414
import { ClientProxy } from '@nestjs/microservices';
15+
16+
import { CreateQuestionDto, UpdateQuestionDto } from '@repo/dtos/questions';
17+
518
@Controller('questions')
619
export class QuestionsController {
720
constructor(
821
@Inject('QUESTIONS_SERVICE')
922
private readonly questionsServiceClient: ClientProxy,
1023
) {}
1124

25+
@Get()
26+
async getQuestions(@Query('includeDeleted') includeDeleted: boolean = false) {
27+
return this.questionsServiceClient.send(
28+
{ cmd: 'get_questions' },
29+
includeDeleted,
30+
);
31+
}
32+
1233
@Get(':id')
1334
async getQuestionById(@Param('id') id: string) {
1435
return this.questionsServiceClient.send({ cmd: 'get_question' }, id);
1536
}
1637

1738
@Post()
18-
async createQuestion(@Body() createQuestionDto: any) {
39+
async createQuestion(@Body() createQuestionDto: CreateQuestionDto) {
1940
return this.questionsServiceClient.send(
2041
{ cmd: 'create_question' },
2142
createQuestionDto,
2243
);
2344
}
45+
46+
@Put(':id')
47+
async updateQuestion(
48+
@Param('id') id: string,
49+
@Body() updateQuestionDto: UpdateQuestionDto,
50+
) {
51+
if (id != updateQuestionDto.id) {
52+
throw new Error('ID in URL does not match ID in request body');
53+
}
54+
return this.questionsServiceClient.send(
55+
{ cmd: 'update_question' },
56+
updateQuestionDto,
57+
);
58+
}
59+
60+
@Delete(':id')
61+
async deleteQuestion(@Param('id') id: string) {
62+
return this.questionsServiceClient.send({ cmd: 'delete_question' }, id);
63+
}
2464
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
SUPABASE_URL=your-supabase-url
2+
SUPABASE_KEY=your-supabase-key

project/apps/questions-service/README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,20 @@
2828

2929
## Project setup
3030

31-
```bash
32-
$ pnpm install
33-
```
31+
1. Install dependencies:
32+
33+
```bash
34+
$ pnpm install
35+
```
36+
37+
2. Copy the `.env.example` file to create a new `.env` file:
38+
39+
```bash
40+
$ cp .env.example .env
41+
```
42+
43+
Modify the `.env` file with your environment-specific configuration.
44+
3445

3546
## Compile and run the project
3647

project/apps/questions-service/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,14 @@
2525
"@repo/dtos": "workspace:*",
2626
"@repo/eslint-config": "workspace:*",
2727
"@nestjs/common": "^10.0.0",
28+
"@nestjs/config": "^3.2.3",
2829
"@nestjs/core": "^10.0.0",
2930
"@nestjs/microservices": "^10.4.3",
3031
"@nestjs/platform-express": "^10.0.0",
32+
"@repo/dtos": "workspace:*",
33+
"@repo/pipes": "workspace:*",
34+
"@supabase/supabase-js": "^2.45.4",
35+
"dotenv": "^16.4.5",
3136
"reflect-metadata": "^0.2.0",
3237
"rxjs": "^7.8.1"
3338
},

project/apps/questions-service/src/questions.controller.spec.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

project/apps/questions-service/src/questions.controller.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,40 @@ import { Controller, UsePipes } from '@nestjs/common';
22
import { MessagePattern, Payload } from '@nestjs/microservices';
33
import { QuestionsService } from './questions.service';
44
import { ZodValidationPipe } from '@repo/pipes/zod-validation-pipe.pipe';
5-
import { CreateQuestionDto, createQuestionSchema } from '@repo/dtos/questions';
5+
import {
6+
CreateQuestionDto,
7+
createQuestionSchema,
8+
UpdateQuestionDto,
9+
updateQuestionSchema,
10+
} from '@repo/dtos/questions';
611
@Controller()
712
export class QuestionsController {
813
constructor(private readonly questionsService: QuestionsService) {}
914

15+
@MessagePattern({ cmd: 'get_questions' })
16+
async getQuestions(includeDeleted: boolean) {
17+
return await this.questionsService.findAll(includeDeleted);
18+
}
19+
1020
@MessagePattern({ cmd: 'get_question' })
11-
getQuestion(id: string) {
12-
return this.questionsService.findById(id);
21+
async getQuestionById(id: string) {
22+
return await this.questionsService.findById(id);
1323
}
1424

1525
@MessagePattern({ cmd: 'create_question' })
1626
@UsePipes(new ZodValidationPipe(createQuestionSchema))
17-
async create(@Payload() createQuestionDto: CreateQuestionDto) {
18-
return createQuestionDto;
27+
async createQuestion(@Payload() createQuestionDto: CreateQuestionDto) {
28+
return await this.questionsService.create(createQuestionDto);
29+
}
30+
31+
@MessagePattern({ cmd: 'update_question' })
32+
@UsePipes(new ZodValidationPipe(updateQuestionSchema))
33+
async updateQuestion(@Payload() createQuestionDto: UpdateQuestionDto) {
34+
return await this.questionsService.update(createQuestionDto);
35+
}
36+
37+
@MessagePattern({ cmd: 'delete_question' })
38+
async deleteQuestionById(id: string) {
39+
return await this.questionsService.deleteById(id);
1940
}
2041
}

project/apps/questions-service/src/questions.module.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { Module } from '@nestjs/common';
22
import { QuestionsController } from './questions.controller';
33
import { QuestionsService } from './questions.service';
4+
import { ConfigModule } from '@nestjs/config';
45

56
@Module({
6-
imports: [],
7+
imports: [
8+
ConfigModule.forRoot({
9+
isGlobal: true,
10+
}),
11+
],
712
controllers: [QuestionsController],
813
providers: [QuestionsService],
914
})
Lines changed: 113 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,118 @@
1-
import { Injectable } from '@nestjs/common';
1+
import { Injectable, Logger } from '@nestjs/common';
2+
import { ConfigService } from '@nestjs/config';
3+
import {
4+
CreateQuestionDto,
5+
QuestionDto,
6+
UpdateQuestionDto,
7+
} from '@repo/dtos/questions';
8+
import { createClient, SupabaseClient } from '@supabase/supabase-js';
29

310
@Injectable()
411
export class QuestionsService {
5-
private readonly questions = [
6-
{ id: '1', title: 'What is NestJS?', userId: '1' },
7-
// ...other questions
8-
];
9-
10-
findById(id: string) {
11-
console.log('hello');
12-
return this.questions.find((question) => question.id === id);
12+
private supabase: SupabaseClient;
13+
private readonly logger = new Logger(QuestionsService.name);
14+
15+
private readonly QUESTIONS_TABLE = 'question_bank';
16+
17+
constructor(private configService: ConfigService) {
18+
const supabaseUrl = this.configService.get<string>('SUPABASE_URL');
19+
const supabaseKey = this.configService.get<string>('SUPABASE_KEY');
20+
21+
if (!supabaseUrl || !supabaseKey) {
22+
throw new Error('Supabase URL and key must be provided');
23+
}
24+
25+
this.supabase = createClient(supabaseUrl, supabaseKey);
26+
}
27+
28+
private handleError(operation: string, error: unknown): never {
29+
this.logger.error(`Error during ${operation}:`, error);
30+
throw error;
31+
}
32+
33+
async findAll(includeDeleted: boolean = false): Promise<QuestionDto[]> {
34+
const query = this.supabase.from(this.QUESTIONS_TABLE).select();
35+
36+
if (!includeDeleted) {
37+
query.is('deleted_at', null);
38+
}
39+
40+
const { data, error } = await query;
41+
42+
if (error) {
43+
this.handleError('fetch questions', error);
44+
}
45+
46+
this.logger.log(
47+
`fetched ${data.length} questions, includeDeleted: ${includeDeleted}`,
48+
);
49+
return data;
50+
}
51+
52+
async findById(id: string): Promise<QuestionDto> {
53+
const { data, error } = await this.supabase
54+
.from(this.QUESTIONS_TABLE)
55+
.select()
56+
.eq('id', id)
57+
.single();
58+
59+
if (error) {
60+
this.handleError('fetch question by id', error);
61+
}
62+
63+
this.logger.log(`fetched question with id ${id}`);
64+
return data;
65+
}
66+
67+
async create(question: CreateQuestionDto): Promise<QuestionDto> {
68+
const { data, error } = await this.supabase
69+
.from(this.QUESTIONS_TABLE)
70+
.insert(question)
71+
.select()
72+
.single<QuestionDto>();
73+
74+
if (error) {
75+
this.handleError('create question', error);
76+
}
77+
78+
this.logger.log(`created question ${data.id}`);
79+
return data;
80+
}
81+
82+
async update(question: UpdateQuestionDto): Promise<QuestionDto> {
83+
const updatedQuestion = {
84+
...question,
85+
updated_at: new Date(),
86+
};
87+
88+
const { data, error } = await this.supabase
89+
.from(this.QUESTIONS_TABLE)
90+
.update(updatedQuestion)
91+
.eq('id', question.id)
92+
.select()
93+
.single();
94+
95+
if (error) {
96+
this.handleError('update question', error);
97+
}
98+
99+
this.logger.log(`updated question with id ${question.id}`);
100+
return data;
101+
}
102+
103+
async deleteById(id: string): Promise<QuestionDto> {
104+
const { data, error } = await this.supabase
105+
.from(this.QUESTIONS_TABLE)
106+
.update({ deleted_at: new Date() })
107+
.eq('id', id)
108+
.select()
109+
.single();
110+
111+
if (error) {
112+
this.handleError('delete question', error);
113+
}
114+
115+
this.logger.log(`deleted question with id ${id}`);
116+
return data;
13117
}
14118
}
Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,45 @@
11
import { z } from "zod";
22

3-
export const createQuestionSchema = z
4-
.object({
5-
title: z.string().min(1),
6-
description: z.string().min(1),
7-
category: z.string().min(1),
8-
complexity: z.number().min(1).max(5),
9-
})
10-
.required();
3+
export enum Complexity {
4+
Easy = "Easy",
5+
Medium = "Medium",
6+
Hard = "Hard",
7+
}
118

9+
export enum Category {
10+
Strings = "Strings",
11+
Algorithms = "Algorithms",
12+
DataStructures = "Data Structures",
13+
BitManipulation = "Bit Manipulation",
14+
Recursion = "Recursion",
15+
Databases = "Databases",
16+
BrainTeaser = "Brain Teaser",
17+
Arrays = "Arrays",
18+
}
19+
20+
const ComplexitySchema = z.nativeEnum(Complexity);
21+
const CategorySchema = z.nativeEnum(Category);
22+
23+
const commonQuestionFields = z.object({
24+
q_title: z.string().min(1),
25+
q_desc: z.string().min(1),
26+
q_category: z.array(CategorySchema),
27+
q_complexity: ComplexitySchema,
28+
});
29+
30+
export const questionSchema = commonQuestionFields.extend({
31+
id: z.string().uuid(),
32+
created_at: z.date(),
33+
updated_at: z.date(),
34+
deleted_at: z.date().nullable(),
35+
});
36+
37+
export const createQuestionSchema = commonQuestionFields;
38+
39+
export const updateQuestionSchema = commonQuestionFields.extend({
40+
id: z.string().uuid(),
41+
});
42+
43+
export type QuestionDto = z.infer<typeof questionSchema>;
1244
export type CreateQuestionDto = z.infer<typeof createQuestionSchema>;
45+
export type UpdateQuestionDto = z.infer<typeof updateQuestionSchema>;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"root":["./src/zod-validation-pipe.pipe.ts"],"errors":true,"version":"5.6.2"}
1+
{"root":["./src/zod-validation-pipe.pipe.ts"],"version":"5.6.2"}

0 commit comments

Comments
 (0)