Skip to content

Commit

Permalink
Squashed commit of the following:
Browse files Browse the repository at this point in the history
commit 26ac65f
Author: samuelim01 <[email protected]>
Date:   Mon Oct 7 00:29:03 2024 +0800

    Dockerize user-db locally (#53)

commit c9cec7b
Author: samuelim01 <[email protected]>
Date:   Thu Oct 3 15:26:13 2024 +0800

    Improve questions (#49)

    * Wrap delete questions in single API call

    * Add new POST `questions/delete` endpoint
    * Update question service README
    * Update frontend to use new endpoint

    * Ensure question table is responsive

    to fit smaller viewports

    * Fix question README

    * Fix comments

    * Update return type of getQuestionByID

    * Revert "Update return type of getQuestionByID"

    This reverts commit cb11b5a.

    * Update return type of getQuestionByID

    ---------

    Co-authored-by: limcaaarl <[email protected]>
  • Loading branch information
samuelim01 committed Oct 8, 2024
1 parent e61aa7e commit 7bbf91c
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 50 deletions.
9 changes: 4 additions & 5 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ QUESTION_DB_USERNAME=user
QUESTION_DB_PASSWORD=password

# User Service
USER_SERVICE_CLOUD_URI=mongodb+srv://admin:<db_password>@cluster0.uo0vu.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0
USER_SERVICE_LOCAL_URI=mongodb://127.0.0.1:27017/peerprepUserServiceDB

# Will use cloud MongoDB Atlas database
ENV=PROD
USER_DB_CLOUD_URI=<FILL-THIS-IN>
USER_DB_LOCAL_URI=mongodb://user-db:27017/user
USER_DB_USERNAME=user
USER_DB_PASSWORD=password

# Secret for creating JWT signature
JWT_SECRET=you-can-replace-this-with-your-own-secret
Expand Down
6 changes: 5 additions & 1 deletion compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ services:
command: npm run dev
volumes:
- /app/node_modules
- ./services/user:/app
- ./services/user:/app

user-db:
ports:
- 27018:27017
25 changes: 22 additions & 3 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,34 @@ services:
ports:
- 8082:8082
environment:
USER_SERVICE_CLOUD_URI: ${USER_SERVICE_CLOUD_URI}
USER_SERVICE_LOCAL_URI: ${USER_SERVICE_LOCAL_URI}
ENV: ${ENV}
DB_CLOUD_URI: ${USER_DB_CLOUD_URI}
DB_LOCAL_URI: ${USER_DB_LOCAL_URI}
DB_USERNAME: ${USER_DB_USERNAME}
DB_PASSWORD: ${USER_DB_PASSWORD}
JWT_SECRET: ${JWT_SECRET}
networks:
- user-db-network
restart: always

user-db:
container_name: user-db
image: mongo:7.0.14
environment:
MONGO_INITDB_ROOT_USERNAME: ${USER_DB_USERNAME}
MONGO_INITDB_ROOT_PASSWORD: ${USER_DB_PASSWORD}
volumes:
- user-db:/data/db
networks:
- user-db-network
command: --quiet
restart: always

volumes:
question-db:
user-db:

networks:
question-db-network:
driver: bridge
user-db-network:
driver: bridge
23 changes: 14 additions & 9 deletions frontend/src/_services/question.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import {
QuestionResponse,
QuestionBody,
UploadQuestionsResponse,
MessageOnlyResponse,
} from '../app/questions/question.model';
import { TopicResponse } from '../app/questions/topic.model';

@Injectable({
providedIn: 'root',
})
export class QuestionService {
private baseUrl = API_CONFIG.baseUrl;
private baseUrl = API_CONFIG.baseUrl + '/questions';

private httpOptions = {
headers: new HttpHeaders({
Expand Down Expand Up @@ -46,11 +47,11 @@ export class QuestionService {
}

// send request
return this.http.get<QuestionResponse>(this.baseUrl + '/questions', { params });
return this.http.get<QuestionResponse>(this.baseUrl, { params });
}

getQuestionByID(id: number): Observable<QuestionResponse> {
return this.http.get<QuestionResponse>(this.baseUrl + '/questions/' + id);
getQuestionByID(id: number): Observable<SingleQuestionResponse> {
return this.http.get<SingleQuestionResponse>(this.baseUrl + '/' + id);
}

getQuestionByParam(topics: string[], difficulty: string, limit?: number): Observable<QuestionResponse> {
Expand All @@ -61,16 +62,16 @@ export class QuestionService {
}
params = params.append('topics', topics.join(',')).append('difficulty', difficulty);

return this.http.get<QuestionResponse>(this.baseUrl + '/questions/search', { params });
return this.http.get<QuestionResponse>(this.baseUrl + '/search', { params });
}

getTopics(): Observable<TopicResponse> {
return this.http.get<TopicResponse>(this.baseUrl + '/questions/topics');
return this.http.get<TopicResponse>(this.baseUrl + '/topics');
}

addQuestion(question: QuestionBody): Observable<SingleQuestionResponse> {
return this.http
.post<SingleQuestionResponse>(this.baseUrl + '/questions', question, this.httpOptions)
.post<SingleQuestionResponse>(this.baseUrl, question, this.httpOptions)
.pipe(catchError(this.handleError));
}

Expand All @@ -82,13 +83,17 @@ export class QuestionService {

updateQuestion(id: number, question: QuestionBody): Observable<SingleQuestionResponse> {
return this.http
.put<SingleQuestionResponse>(this.baseUrl + '/questions/' + id, question, this.httpOptions)
.put<SingleQuestionResponse>(this.baseUrl + '/' + id, question, this.httpOptions)
.pipe(catchError(this.handleError));
}

deleteQuestion(id: number): Observable<SingleQuestionResponse> {
return this.http.delete<SingleQuestionResponse>(this.baseUrl + '/' + id).pipe(catchError(this.handleError));
}

deleteQuestions(ids: number[]): Observable<MessageOnlyResponse> {
return this.http
.delete<SingleQuestionResponse>(this.baseUrl + '/questions/' + id)
.post<MessageOnlyResponse>(this.baseUrl + '/delete', { ids }, this.httpOptions)
.pipe(catchError(this.handleError));
}

Expand Down
13 changes: 9 additions & 4 deletions frontend/src/app/questions/question.model.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
export interface QuestionResponse {
export interface BaseResponse {
status: string;
message: string;
}

export interface MessageOnlyResponse extends BaseResponse {
data: null;
}

export interface QuestionResponse extends BaseResponse {
data?: Question[] | null;
}

export interface SingleQuestionResponse {
status: string;
message: string;
export interface SingleQuestionResponse extends BaseResponse {
data: Question;
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/questions/questions.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
[value]="questions"
[(selection)]="selectedQuestions"
datakey="id"
[tableStyle]="{ 'table-layout': 'fixed' }"
[tableStyle]="{ 'min-width': '50rem' }"
[paginator]="true"
[rows]="5"
[rowsPerPageOptions]="[5, 10, 20]"
Expand Down
23 changes: 7 additions & 16 deletions frontend/src/app/questions/questions.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { DropdownModule } from 'primeng/dropdown';
import { ProgressSpinnerModule } from 'primeng/progressspinner';
import { Question } from './question.model';
import { QuestionService } from '../../_services/question.service';
import { forkJoin } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
import { QuestionDialogComponent } from './question-dialog.component';
import { Column } from './column.model';
Expand Down Expand Up @@ -87,9 +86,7 @@ export class QuestionsComponent implements OnInit {
message: 'Are you sure you want to delete the selected questions?',
header: 'Delete Confirmation',
icon: 'pi pi-exclamation-triangle',
accept: () => {
this.handleDeleteQuestionResponse();
},
accept: () => this.handleDeleteQuestionsResponse(),
});
}

Expand All @@ -103,21 +100,15 @@ export class QuestionsComponent implements OnInit {
};
}

handleDeleteQuestionResponse() {
const deleteRequests = this.selectedQuestions?.map(q => this.questionService.deleteQuestion(q.id));

forkJoin(deleteRequests!).subscribe({
handleDeleteQuestionsResponse() {
const ids = this.selectedQuestions?.map(q => q.id) || [];
this.questionService.deleteQuestions(ids).subscribe({
next: () => {
// delete locally
this.questions = this.questions?.filter(val => !this.selectedQuestions?.includes(val));
this.questions = this.questions?.filter(q => !ids.includes(q.id));
this.selectedQuestions = null;
},
error: (error: HttpErrorResponse) => {
this.onErrorReceive('Some questions could not be deleted. ' + error.error.message);
},
complete: () => {
this.onSuccessfulRequest('Question(s) Deleted');
},
error: (error: HttpErrorResponse) => this.onErrorReceive(error.error.message),
complete: () => this.onSuccessfulRequest('Question(s) Deleted'),
});
}

Expand Down
38 changes: 38 additions & 0 deletions services/question/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,3 +461,41 @@ curl -X DELETE http://localhost:8081/questions/21
```

---

## Delete Questions

This endpoint allows the deletion of multiple questions by their question IDs.

- **HTTP Method**: `POST`
- **Endpoint**: `/questions/delete`

### Parameters:

- `ids` (Required) - An array of integers representing the IDs of the questions to delete, e.g. `[1, 2, 3]`.

### Responses:

| Response Code | Explanation |
|-----------------------------|------------------------------------------------------|
| 200 (OK) | Success, the question is deleted successfully. |
| 400 (Bad Request) | The `ids` parameter was not specified or is invalid. |
| 404 (Not Found) | A question with the specified id not found. |
| 500 (Internal Server Error) | Unexpected error in the database or server. |

### Command Line Example:

```
curl -X POST http://localhost:8081/questions/delete -H "Content-Type: application/json" -d '{"ids": [21, 22]}'
```

### Example of Response Body for Success:

```json
{
"status": "Success",
"message": "Questions deleted successfully",
"data": null
}
```

---
29 changes: 29 additions & 0 deletions services/question/src/controllers/questionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,3 +303,32 @@ export const uploadQuestions = async (req: Request, res: Response) => {
}
}
};

/*
* This endpoint allows deletion of multiple questions by the question ID.
* @param req
* @param res
*/
export const deleteQuestions = async (req: Request, res: Response) => {
const { ids } = req.body;

if (!ids || !Array.isArray(ids)) {
return handleBadRequest(res, 'IDs are missing or not specified as an array');
}

const deletedIDs = ids.map((id: any) => parseInt(id, 10));
if (deletedIDs.some((id: any) => isNaN(id))) {
return handleBadRequest(res, 'Invalid question ID');
}

try {
const count = await Question.countDocuments({ id: { $in: deletedIDs } });

if (count !== ids.length) return handleNotFound(res, 'Question not found');
await Question.deleteMany({ id: { $in: deletedIDs } });
handleSuccess(res, 200, 'Questions deleted successfully', null);
} catch (error) {
console.log('Error in deleteQuestions:', error);
handleError(res, 'Failed to delete questions');
}
};
6 changes: 6 additions & 0 deletions services/question/src/routes/questionRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
deleteQuestion,
updateQuestion,
uploadQuestions,
deleteQuestions,
} from '../controllers/questionController';
import { upload } from '../utils/multer';

Expand Down Expand Up @@ -45,4 +46,9 @@ questionRouter.put('/:id', updateQuestion);
*/
questionRouter.delete('/:id', deleteQuestion);

/**
* Delete questions from the database.
*/
questionRouter.post('/delete', deleteQuestions);

export default questionRouter;
12 changes: 5 additions & 7 deletions services/user/.env.sample
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# User Service
USER_SERVICE_CLOUD_URI=<cloud_uri>
USER_SERVICE_LOCAL_URI=mongodb://127.0.0.1:27017/peerprepUserServiceDB
PORT=8082

# Will use cloud MongoDB Atlas database
ENV=PROD
# This is a sample environment configuration file.
# Copy this file to .env and replace the placeholder values with your own.
DB_CLOUD_URI=<FILL-THIS-IN>
DB_LOCAL_URI=mongodb://user-db:27017/user
PORT=8083

# Secret for creating JWT signature
JWT_SECRET=you-can-replace-this-with-your-own-secret
Expand Down
11 changes: 7 additions & 4 deletions services/user/src/model/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import 'dotenv/config';
import { connect } from 'mongoose';

export async function connectToDB() {
const mongoDBUri =
process.env.ENV === 'PROD' ? process.env.USER_SERVICE_CLOUD_URI : process.env.USER_SERVICE_LOCAL_URI;
const mongoUri = process.env.NODE_ENV === 'production' ? process.env.DB_CLOUD_URI : process.env.DB_LOCAL_URI;

if (!mongoDBUri) {
if (!mongoUri) {
throw new Error('MongoDB URI not specified');
}

await connect(mongoDBUri);
await connect(mongoUri, {
authSource: 'admin',
user: process.env.DB_USERNAME,
pass: process.env.DB_PASSWORD,
});
}

export async function createUser(username: string, email: string, password: string) {
Expand Down

0 comments on commit 7bbf91c

Please sign in to comment.