diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index f0debc75..6ce39154 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -18,6 +18,7 @@ import { StagingGuard } from "./common/guards/staging.guard"; import { HealthModule } from "./health/health.module"; import { ScheduleModule } from "@nestjs/schedule"; import { CoursesModule } from "./courses/courses.module"; +import { StudentFavouritedCoursesModule } from "./studentFavouritedCourses/studentFavouritedCourses.module"; @Module({ imports: [ @@ -62,6 +63,7 @@ import { CoursesModule } from "./courses/courses.module"; (env) => env.NODE_ENV !== "test", ), CoursesModule, + StudentFavouritedCoursesModule, ], controllers: [], providers: [ diff --git a/apps/api/src/courses/courses.service.ts b/apps/api/src/courses/courses.service.ts index 595481b5..f95f97bb 100644 --- a/apps/api/src/courses/courses.service.ts +++ b/apps/api/src/courses/courses.service.ts @@ -17,6 +17,7 @@ import { users, } from "../storage/schema"; import { getSortOptions } from "src/common/helpers/getSortOptions"; +import { Status } from "src/storage/schema/utils"; @Injectable() export class CoursesService { @@ -170,7 +171,7 @@ export class CoursesService { } private getFiltersConditions(filters: CoursesFilterSchema) { - const conditions = [eq(courses.state, "published")]; + const conditions = [eq(courses.state, Status.published.key)]; if (filters.title) { conditions.push( like(categories.title, `%${filters.title.toLowerCase()}%`), diff --git a/apps/api/src/storage/schema/utils.ts b/apps/api/src/storage/schema/utils.ts index cdb28cd6..638ae10f 100644 --- a/apps/api/src/storage/schema/utils.ts +++ b/apps/api/src/storage/schema/utils.ts @@ -29,6 +29,6 @@ export const timestamps = { export const archived = boolean("archived").default(false).notNull(); export const Status = { - draft: "Draft", - published: "Published", + draft: { key: "draft", value: "Draft" }, + published: { key: "published", value: "Published" }, } as const; diff --git a/apps/api/src/studentFavouritedCourses/api/studentFavouritedCourses.controller.ts b/apps/api/src/studentFavouritedCourses/api/studentFavouritedCourses.controller.ts new file mode 100644 index 00000000..bc9b5356 --- /dev/null +++ b/apps/api/src/studentFavouritedCourses/api/studentFavouritedCourses.controller.ts @@ -0,0 +1,53 @@ +import { Body, Controller, Delete, Post, Query } from "@nestjs/common"; +import { Validate } from "nestjs-typebox"; +import { BaseResponse, nullResponse, UUIDSchema } from "src/common"; +import { CurrentUser } from "src/common/decorators/user.decorator"; +import { StudentFavouritedCoursesService } from "../studentFavouritedCourses.service"; +import { + createFavouritedCourseSchema, + CreateFavouritedCourseSchema, +} from "../schemas/createFavouritedCourse.schema"; + +@Controller("studentFavouritedCourses") +export class StudentFavouritedCoursesController { + constructor( + private readonly studentFavouritedCoursesService: StudentFavouritedCoursesService, + ) {} + + @Post() + @Validate({ + request: [{ type: "body", schema: createFavouritedCourseSchema }], + }) + async createFavouritedCourse( + @Body() data: CreateFavouritedCourseSchema, + @CurrentUser() currentUser: { userId: string }, + ): Promise> { + await this.studentFavouritedCoursesService.createFavouritedCourseForUser( + data.courseId, + currentUser.userId, + ); + + return new BaseResponse({ + message: "Favourite course created successfully", + }); + } + + @Delete() + @Validate({ + response: nullResponse(), + request: [{ type: "query", name: "id", schema: UUIDSchema }], + }) + async deleteFavouritedCourseForUser( + @Query("id") id: string, + @CurrentUser() currentUser: { userId: string }, + ): Promise { + console.log(id, currentUser.userId); + + await this.studentFavouritedCoursesService.deleteFavouritedCourseForUser( + id, + currentUser.userId, + ); + + return null; + } +} diff --git a/apps/api/src/studentFavouritedCourses/schemas/course.schema.ts b/apps/api/src/studentFavouritedCourses/schemas/course.schema.ts new file mode 100644 index 00000000..3f7ba526 --- /dev/null +++ b/apps/api/src/studentFavouritedCourses/schemas/course.schema.ts @@ -0,0 +1,16 @@ +import { Type, Static } from "@sinclair/typebox"; +import { UUIDSchema } from "src/common"; + +export const allCoursesSchema = Type.Array( + Type.Object({ + id: UUIDSchema, + title: Type.String(), + imageUrl: Type.Union([Type.String(), Type.Null()]), + author: Type.String(), + category: Type.String(), + courseLessonCount: Type.Number(), + enrolledParticipantCount: Type.Number(), + }), +); + +export type AllCoursesResponse = Static; diff --git a/apps/api/src/studentFavouritedCourses/schemas/createFavouritedCourse.schema.ts b/apps/api/src/studentFavouritedCourses/schemas/createFavouritedCourse.schema.ts new file mode 100644 index 00000000..428a9450 --- /dev/null +++ b/apps/api/src/studentFavouritedCourses/schemas/createFavouritedCourse.schema.ts @@ -0,0 +1,10 @@ +import { Static, Type } from "@sinclair/typebox"; +import { UUIDSchema } from "src/common"; + +export const createFavouritedCourseSchema = Type.Object({ + courseId: UUIDSchema, +}); + +export type CreateFavouritedCourseSchema = Static< + typeof createFavouritedCourseSchema +>; diff --git a/apps/api/src/studentFavouritedCourses/studentFavouritedCourses.module.ts b/apps/api/src/studentFavouritedCourses/studentFavouritedCourses.module.ts new file mode 100644 index 00000000..c8f1d0bf --- /dev/null +++ b/apps/api/src/studentFavouritedCourses/studentFavouritedCourses.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; + +import { StudentFavouritedCoursesService } from "./studentFavouritedCourses.service"; +import { StudentFavouritedCoursesController } from "./api/studentFavouritedCourses.controller"; + +@Module({ + imports: [], + controllers: [StudentFavouritedCoursesController], + providers: [StudentFavouritedCoursesService], + exports: [], +}) +export class StudentFavouritedCoursesModule {} diff --git a/apps/api/src/studentFavouritedCourses/studentFavouritedCourses.service.ts b/apps/api/src/studentFavouritedCourses/studentFavouritedCourses.service.ts new file mode 100644 index 00000000..2781cabe --- /dev/null +++ b/apps/api/src/studentFavouritedCourses/studentFavouritedCourses.service.ts @@ -0,0 +1,61 @@ +import { + ConflictException, + Inject, + Injectable, + NotFoundException, +} from "@nestjs/common"; +import { and, eq } from "drizzle-orm"; +import { DatabasePg } from "src/common"; +import { courses, studentFavouritedCourses } from "src/storage/schema"; +import { Status } from "src/storage/schema/utils"; + +@Injectable() +export class StudentFavouritedCoursesService { + constructor(@Inject("DB") private readonly db: DatabasePg) {} + + async createFavouritedCourseForUser(courseId: string, userId: string) { + const [course] = await this.db + .select() + .from(courses) + .where( + and(eq(courses.id, courseId), eq(courses.state, Status.published.key)), + ); + if (!course) { + throw new NotFoundException("Course not found"); + } + + const [existingRecord] = await this.db + .select() + .from(studentFavouritedCourses) + .where( + and( + eq(studentFavouritedCourses.courseId, courseId), + eq(studentFavouritedCourses.studentId, userId), + ), + ); + if (existingRecord) { + throw new ConflictException("Favourite course already exists"); + } + + await this.db + .insert(studentFavouritedCourses) + .values({ courseId: courseId, studentId: userId }) + .returning(); + } + + async deleteFavouritedCourseForUser(id: string, userId: string) { + const [deletedFavouritedCourse] = await this.db + .delete(studentFavouritedCourses) + .where( + and( + eq(studentFavouritedCourses.id, id), + eq(studentFavouritedCourses.studentId, userId), + ), + ) + .returning(); + + if (!deletedFavouritedCourse) { + throw new NotFoundException("Favourite course not found"); + } + } +} diff --git a/apps/api/src/swagger/api-schema.json b/apps/api/src/swagger/api-schema.json index 7791a4ae..8f577f7f 100644 --- a/apps/api/src/swagger/api-schema.json +++ b/apps/api/src/swagger/api-schema.json @@ -836,6 +836,52 @@ } } } + }, + "/api/studentFavouritedCourses": { + "post": { + "operationId": "StudentFavouritedCoursesController_createFavouritedCourse", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateFavouritedCourseBody" + } + } + } + }, + "responses": { + "201": { + "description": "" + } + } + }, + "delete": { + "operationId": "StudentFavouritedCoursesController_deleteFavouritedCourseForUser", + "parameters": [ + { + "name": "id", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteFavouritedCourseForUserResponse" + } + } + } + } + } + } } }, "info": { @@ -1489,6 +1535,21 @@ "data", "pagination" ] + }, + "CreateFavouritedCourseBody": { + "type": "object", + "properties": { + "courseId": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "courseId" + ] + }, + "DeleteFavouritedCourseForUserResponse": { + "type": "null" } } }