From 2fa5244f137fb82ff9d117a277af22eb0dd4ad29 Mon Sep 17 00:00:00 2001 From: wielopolski Date: Wed, 11 Sep 2024 15:23:57 +0200 Subject: [PATCH 1/3] feat: add courses list endpoint --- apps/api/src/courses/helpers/index.ts | 10 ++++++++ apps/api/src/swagger/api-schema.json | 33 +++++++++++++++------------ 2 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 apps/api/src/courses/helpers/index.ts diff --git a/apps/api/src/courses/helpers/index.ts b/apps/api/src/courses/helpers/index.ts new file mode 100644 index 000000000..d9e6ec253 --- /dev/null +++ b/apps/api/src/courses/helpers/index.ts @@ -0,0 +1,10 @@ +import { asc, desc } from "drizzle-orm"; +import { + CourseSortField, + SortCourseFieldsOptions, +} from "src/courses/schemas/courseQuery"; + +export const getSortOptions = (sort: SortCourseFieldsOptions) => ({ + sortOrder: sort.startsWith("-") ? desc : asc, + sortedField: sort.replace(/^-/, "") as CourseSortField, +}); diff --git a/apps/api/src/swagger/api-schema.json b/apps/api/src/swagger/api-schema.json index 8f577f7fc..049b62e65 100644 --- a/apps/api/src/swagger/api-schema.json +++ b/apps/api/src/swagger/api-schema.json @@ -1334,17 +1334,7 @@ "title": { "type": "string" }, - "archived": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ] - }, - "createdAt": { + "imageUrl": { "anyOf": [ { "type": "string" @@ -1353,13 +1343,28 @@ "type": "null" } ] + }, + "author": { + "type": "string" + }, + "category": { + "type": "string" + }, + "courseLessonCount": { + "type": "number" + }, + "enrolledParticipantCount": { + "type": "number" } }, "required": [ "id", "title", - "archived", - "createdAt" + "imageUrl", + "author", + "category", + "courseLessonCount", + "enrolledParticipantCount" ] } }, @@ -1553,4 +1558,4 @@ } } } -} \ No newline at end of file +} From 7874d1976a65850902461afc5cc923729ce10e47 Mon Sep 17 00:00:00 2001 From: wielopolski Date: Wed, 11 Sep 2024 23:46:38 +0200 Subject: [PATCH 2/3] feat: add student favourtie course endpoint --- .../api/studentFavouriteCourses.controller.ts | 53 ++++++++++++++++ .../studentFavouriteCourses/helpers/index.ts | 10 +++ .../schemas/course.schema.ts | 16 +++++ .../schemas/createFavouriteCourse.schema.ts | 10 +++ .../studentFavouriteCourses.module.ts | 11 ++++ .../studentFavouriteCourses.service.ts | 61 +++++++++++++++++++ apps/api/src/swagger/api-schema.json | 46 ++++++++++++++ 7 files changed, 207 insertions(+) create mode 100644 apps/api/src/studentFavouriteCourses/api/studentFavouriteCourses.controller.ts create mode 100644 apps/api/src/studentFavouriteCourses/helpers/index.ts create mode 100644 apps/api/src/studentFavouriteCourses/schemas/course.schema.ts create mode 100644 apps/api/src/studentFavouriteCourses/schemas/createFavouriteCourse.schema.ts create mode 100644 apps/api/src/studentFavouriteCourses/studentFavouriteCourses.module.ts create mode 100644 apps/api/src/studentFavouriteCourses/studentFavouriteCourses.service.ts diff --git a/apps/api/src/studentFavouriteCourses/api/studentFavouriteCourses.controller.ts b/apps/api/src/studentFavouriteCourses/api/studentFavouriteCourses.controller.ts new file mode 100644 index 000000000..dbe744b8c --- /dev/null +++ b/apps/api/src/studentFavouriteCourses/api/studentFavouriteCourses.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 { StudentFavouriteCoursesService } from "../studentFavouriteCourses.service"; +import { + createFavouriteCourseSchema, + CreateFavouriteCourseSchema, +} from "../schemas/createFavouriteCourse.schema"; + +@Controller("studentFavouriteCourses") +export class StudentFavouriteCoursesController { + constructor( + private readonly studentFavouriteCoursesService: StudentFavouriteCoursesService, + ) {} + + @Post() + @Validate({ + request: [{ type: "body", schema: createFavouriteCourseSchema }], + }) + async createFavouriteCourse( + @Body() data: CreateFavouriteCourseSchema, + @CurrentUser() currentUser: { userId: string }, + ): Promise> { + await this.studentFavouriteCoursesService.createFavouriteCourseForUser( + data.courseId, + currentUser.userId, + ); + + return new BaseResponse({ + message: "Favourite course created successfully", + }); + } + + @Delete() + @Validate({ + response: nullResponse(), + request: [{ type: "query", name: "id", schema: UUIDSchema }], + }) + async deleteFavouriteCourseForUser( + @Query("id") id: string, + @CurrentUser() currentUser: { userId: string }, + ): Promise { + console.log(id, currentUser.userId); + + await this.studentFavouriteCoursesService.deleteFavouriteCourseForUser( + id, + currentUser.userId, + ); + + return null; + } +} diff --git a/apps/api/src/studentFavouriteCourses/helpers/index.ts b/apps/api/src/studentFavouriteCourses/helpers/index.ts new file mode 100644 index 000000000..d9e6ec253 --- /dev/null +++ b/apps/api/src/studentFavouriteCourses/helpers/index.ts @@ -0,0 +1,10 @@ +import { asc, desc } from "drizzle-orm"; +import { + CourseSortField, + SortCourseFieldsOptions, +} from "src/courses/schemas/courseQuery"; + +export const getSortOptions = (sort: SortCourseFieldsOptions) => ({ + sortOrder: sort.startsWith("-") ? desc : asc, + sortedField: sort.replace(/^-/, "") as CourseSortField, +}); diff --git a/apps/api/src/studentFavouriteCourses/schemas/course.schema.ts b/apps/api/src/studentFavouriteCourses/schemas/course.schema.ts new file mode 100644 index 000000000..3f7ba5262 --- /dev/null +++ b/apps/api/src/studentFavouriteCourses/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/studentFavouriteCourses/schemas/createFavouriteCourse.schema.ts b/apps/api/src/studentFavouriteCourses/schemas/createFavouriteCourse.schema.ts new file mode 100644 index 000000000..25bd34bf9 --- /dev/null +++ b/apps/api/src/studentFavouriteCourses/schemas/createFavouriteCourse.schema.ts @@ -0,0 +1,10 @@ +import { Static, Type } from "@sinclair/typebox"; +import { UUIDSchema } from "src/common"; + +export const createFavouriteCourseSchema = Type.Object({ + courseId: UUIDSchema, +}); + +export type CreateFavouriteCourseSchema = Static< + typeof createFavouriteCourseSchema +>; diff --git a/apps/api/src/studentFavouriteCourses/studentFavouriteCourses.module.ts b/apps/api/src/studentFavouriteCourses/studentFavouriteCourses.module.ts new file mode 100644 index 000000000..280b387e4 --- /dev/null +++ b/apps/api/src/studentFavouriteCourses/studentFavouriteCourses.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { StudentFavouriteCoursesController } from "./api/studentFavouriteCourses.controller"; +import { StudentFavouriteCoursesService } from "./studentFavouriteCourses.service"; + +@Module({ + imports: [], + controllers: [StudentFavouriteCoursesController], + providers: [StudentFavouriteCoursesService], + exports: [], +}) +export class StudentFavouriteCoursesModule {} diff --git a/apps/api/src/studentFavouriteCourses/studentFavouriteCourses.service.ts b/apps/api/src/studentFavouriteCourses/studentFavouriteCourses.service.ts new file mode 100644 index 000000000..d4773a731 --- /dev/null +++ b/apps/api/src/studentFavouriteCourses/studentFavouriteCourses.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, studentFavouriteCourses } from "src/storage/schema"; +import { Status } from "src/storage/schema/utils"; + +@Injectable() +export class StudentFavouriteCoursesService { + constructor(@Inject("DB") private readonly db: DatabasePg) {} + + async createFavouriteCourseForUser(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(studentFavouriteCourses) + .where( + and( + eq(studentFavouriteCourses.courseId, courseId), + eq(studentFavouriteCourses.studentId, userId), + ), + ); + if (existingRecord) { + throw new ConflictException("Favourite course already exists"); + } + + await this.db + .insert(studentFavouriteCourses) + .values({ courseId: courseId, studentId: userId }) + .returning(); + } + + async deleteFavouriteCourseForUser(id: string, userId: string) { + const [deletedFavouriteCourse] = await this.db + .delete(studentFavouriteCourses) + .where( + and( + eq(studentFavouriteCourses.id, id), + eq(studentFavouriteCourses.studentId, userId), + ), + ) + .returning(); + + if (!deletedFavouriteCourse) { + 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 049b62e65..34b82e629 100644 --- a/apps/api/src/swagger/api-schema.json +++ b/apps/api/src/swagger/api-schema.json @@ -882,6 +882,52 @@ } } } + }, + "/api/studentFavouriteCourses": { + "post": { + "operationId": "StudentFavouriteCoursesController_createFavouriteCourse", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateFavouriteCourseBody" + } + } + } + }, + "responses": { + "201": { + "description": "" + } + } + }, + "delete": { + "operationId": "StudentFavouriteCoursesController_deleteFavouriteCourseForUser", + "parameters": [ + { + "name": "id", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteFavouriteCourseForUserResponse" + } + } + } + } + } + } } }, "info": { From bcdb1fe0c72040d9e34c8262aceeaf018f4fdbe1 Mon Sep 17 00:00:00 2001 From: wielopolski Date: Thu, 12 Sep 2024 03:15:23 +0200 Subject: [PATCH 3/3] feat: add student favourtie course list for student endpoint --- apps/api/src/courses/helpers/index.ts | 10 -- .../api/studentFavouriteCourses.controller.ts | 53 -------- .../studentFavouriteCourses/helpers/index.ts | 10 -- .../schemas/course.schema.ts | 16 --- .../schemas/createFavouriteCourse.schema.ts | 10 -- .../studentFavouriteCourses.module.ts | 11 -- .../studentFavouriteCourses.service.ts | 61 ---------- .../studentFavouritedCourses.controller.ts | 42 ++++++- .../studentFavouritedCourses.service.ts | 115 +++++++++++++++++- apps/api/src/swagger/api-schema.json | 104 +++++++++++----- 10 files changed, 226 insertions(+), 206 deletions(-) delete mode 100644 apps/api/src/courses/helpers/index.ts delete mode 100644 apps/api/src/studentFavouriteCourses/api/studentFavouriteCourses.controller.ts delete mode 100644 apps/api/src/studentFavouriteCourses/helpers/index.ts delete mode 100644 apps/api/src/studentFavouriteCourses/schemas/course.schema.ts delete mode 100644 apps/api/src/studentFavouriteCourses/schemas/createFavouriteCourse.schema.ts delete mode 100644 apps/api/src/studentFavouriteCourses/studentFavouriteCourses.module.ts delete mode 100644 apps/api/src/studentFavouriteCourses/studentFavouriteCourses.service.ts diff --git a/apps/api/src/courses/helpers/index.ts b/apps/api/src/courses/helpers/index.ts deleted file mode 100644 index d9e6ec253..000000000 --- a/apps/api/src/courses/helpers/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { asc, desc } from "drizzle-orm"; -import { - CourseSortField, - SortCourseFieldsOptions, -} from "src/courses/schemas/courseQuery"; - -export const getSortOptions = (sort: SortCourseFieldsOptions) => ({ - sortOrder: sort.startsWith("-") ? desc : asc, - sortedField: sort.replace(/^-/, "") as CourseSortField, -}); diff --git a/apps/api/src/studentFavouriteCourses/api/studentFavouriteCourses.controller.ts b/apps/api/src/studentFavouriteCourses/api/studentFavouriteCourses.controller.ts deleted file mode 100644 index dbe744b8c..000000000 --- a/apps/api/src/studentFavouriteCourses/api/studentFavouriteCourses.controller.ts +++ /dev/null @@ -1,53 +0,0 @@ -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 { StudentFavouriteCoursesService } from "../studentFavouriteCourses.service"; -import { - createFavouriteCourseSchema, - CreateFavouriteCourseSchema, -} from "../schemas/createFavouriteCourse.schema"; - -@Controller("studentFavouriteCourses") -export class StudentFavouriteCoursesController { - constructor( - private readonly studentFavouriteCoursesService: StudentFavouriteCoursesService, - ) {} - - @Post() - @Validate({ - request: [{ type: "body", schema: createFavouriteCourseSchema }], - }) - async createFavouriteCourse( - @Body() data: CreateFavouriteCourseSchema, - @CurrentUser() currentUser: { userId: string }, - ): Promise> { - await this.studentFavouriteCoursesService.createFavouriteCourseForUser( - data.courseId, - currentUser.userId, - ); - - return new BaseResponse({ - message: "Favourite course created successfully", - }); - } - - @Delete() - @Validate({ - response: nullResponse(), - request: [{ type: "query", name: "id", schema: UUIDSchema }], - }) - async deleteFavouriteCourseForUser( - @Query("id") id: string, - @CurrentUser() currentUser: { userId: string }, - ): Promise { - console.log(id, currentUser.userId); - - await this.studentFavouriteCoursesService.deleteFavouriteCourseForUser( - id, - currentUser.userId, - ); - - return null; - } -} diff --git a/apps/api/src/studentFavouriteCourses/helpers/index.ts b/apps/api/src/studentFavouriteCourses/helpers/index.ts deleted file mode 100644 index d9e6ec253..000000000 --- a/apps/api/src/studentFavouriteCourses/helpers/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { asc, desc } from "drizzle-orm"; -import { - CourseSortField, - SortCourseFieldsOptions, -} from "src/courses/schemas/courseQuery"; - -export const getSortOptions = (sort: SortCourseFieldsOptions) => ({ - sortOrder: sort.startsWith("-") ? desc : asc, - sortedField: sort.replace(/^-/, "") as CourseSortField, -}); diff --git a/apps/api/src/studentFavouriteCourses/schemas/course.schema.ts b/apps/api/src/studentFavouriteCourses/schemas/course.schema.ts deleted file mode 100644 index 3f7ba5262..000000000 --- a/apps/api/src/studentFavouriteCourses/schemas/course.schema.ts +++ /dev/null @@ -1,16 +0,0 @@ -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/studentFavouriteCourses/schemas/createFavouriteCourse.schema.ts b/apps/api/src/studentFavouriteCourses/schemas/createFavouriteCourse.schema.ts deleted file mode 100644 index 25bd34bf9..000000000 --- a/apps/api/src/studentFavouriteCourses/schemas/createFavouriteCourse.schema.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Static, Type } from "@sinclair/typebox"; -import { UUIDSchema } from "src/common"; - -export const createFavouriteCourseSchema = Type.Object({ - courseId: UUIDSchema, -}); - -export type CreateFavouriteCourseSchema = Static< - typeof createFavouriteCourseSchema ->; diff --git a/apps/api/src/studentFavouriteCourses/studentFavouriteCourses.module.ts b/apps/api/src/studentFavouriteCourses/studentFavouriteCourses.module.ts deleted file mode 100644 index 280b387e4..000000000 --- a/apps/api/src/studentFavouriteCourses/studentFavouriteCourses.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from "@nestjs/common"; -import { StudentFavouriteCoursesController } from "./api/studentFavouriteCourses.controller"; -import { StudentFavouriteCoursesService } from "./studentFavouriteCourses.service"; - -@Module({ - imports: [], - controllers: [StudentFavouriteCoursesController], - providers: [StudentFavouriteCoursesService], - exports: [], -}) -export class StudentFavouriteCoursesModule {} diff --git a/apps/api/src/studentFavouriteCourses/studentFavouriteCourses.service.ts b/apps/api/src/studentFavouriteCourses/studentFavouriteCourses.service.ts deleted file mode 100644 index d4773a731..000000000 --- a/apps/api/src/studentFavouriteCourses/studentFavouriteCourses.service.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - ConflictException, - Inject, - Injectable, - NotFoundException, -} from "@nestjs/common"; -import { and, eq } from "drizzle-orm"; -import { DatabasePg } from "src/common"; -import { courses, studentFavouriteCourses } from "src/storage/schema"; -import { Status } from "src/storage/schema/utils"; - -@Injectable() -export class StudentFavouriteCoursesService { - constructor(@Inject("DB") private readonly db: DatabasePg) {} - - async createFavouriteCourseForUser(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(studentFavouriteCourses) - .where( - and( - eq(studentFavouriteCourses.courseId, courseId), - eq(studentFavouriteCourses.studentId, userId), - ), - ); - if (existingRecord) { - throw new ConflictException("Favourite course already exists"); - } - - await this.db - .insert(studentFavouriteCourses) - .values({ courseId: courseId, studentId: userId }) - .returning(); - } - - async deleteFavouriteCourseForUser(id: string, userId: string) { - const [deletedFavouriteCourse] = await this.db - .delete(studentFavouriteCourses) - .where( - and( - eq(studentFavouriteCourses.id, id), - eq(studentFavouriteCourses.studentId, userId), - ), - ) - .returning(); - - if (!deletedFavouriteCourse) { - throw new NotFoundException("Favourite course not found"); - } - } -} diff --git a/apps/api/src/studentFavouritedCourses/api/studentFavouritedCourses.controller.ts b/apps/api/src/studentFavouritedCourses/api/studentFavouritedCourses.controller.ts index bc9b53568..bf664e8de 100644 --- a/apps/api/src/studentFavouritedCourses/api/studentFavouritedCourses.controller.ts +++ b/apps/api/src/studentFavouritedCourses/api/studentFavouritedCourses.controller.ts @@ -1,12 +1,24 @@ -import { Body, Controller, Delete, Post, Query } from "@nestjs/common"; +import { Body, Controller, Delete, Get, Post, Query } from "@nestjs/common"; import { Validate } from "nestjs-typebox"; -import { BaseResponse, nullResponse, UUIDSchema } from "src/common"; +import { + BaseResponse, + nullResponse, + paginatedResponse, + PaginatedResponse, + UUIDSchema, +} from "src/common"; import { CurrentUser } from "src/common/decorators/user.decorator"; import { StudentFavouritedCoursesService } from "../studentFavouritedCourses.service"; import { createFavouritedCourseSchema, CreateFavouritedCourseSchema, } from "../schemas/createFavouritedCourse.schema"; +import { Type } from "@sinclair/typebox"; +import { + sortCourseFieldsOptions, + SortCourseFieldsOptions, +} from "src/courses/schemas/courseQuery"; +import { allCoursesSchema, AllCoursesResponse } from "../schemas/course.schema"; @Controller("studentFavouritedCourses") export class StudentFavouritedCoursesController { @@ -32,6 +44,32 @@ export class StudentFavouritedCoursesController { }); } + @Get() + @Validate({ + response: paginatedResponse(allCoursesSchema), + request: [ + { type: "query", name: "page", schema: Type.Number({ minimum: 1 }) }, + { type: "query", name: "perPage", schema: Type.Number() }, + { type: "query", name: "sort", schema: sortCourseFieldsOptions }, + ], + }) + async getAllCategories( + @Query("page") page: number, + @Query("perPage") perPage: number, + @Query("sort") sort: SortCourseFieldsOptions, + @CurrentUser() currentUser: { userId: string }, + ): Promise> { + const query = { page, perPage, sort }; + + const data = + await this.studentFavouritedCoursesService.getCoursesForStudents( + query, + currentUser.userId, + ); + + return new PaginatedResponse(data); + } + @Delete() @Validate({ response: nullResponse(), diff --git a/apps/api/src/studentFavouritedCourses/studentFavouritedCourses.service.ts b/apps/api/src/studentFavouritedCourses/studentFavouritedCourses.service.ts index 2781cabe9..a708991a0 100644 --- a/apps/api/src/studentFavouritedCourses/studentFavouritedCourses.service.ts +++ b/apps/api/src/studentFavouritedCourses/studentFavouritedCourses.service.ts @@ -4,10 +4,25 @@ import { Injectable, NotFoundException, } from "@nestjs/common"; -import { and, eq } from "drizzle-orm"; -import { DatabasePg } from "src/common"; -import { courses, studentFavouritedCourses } from "src/storage/schema"; +import { and, count, eq, sql } from "drizzle-orm"; +import { DatabasePg, Pagination } from "src/common"; +import { DEFAULT_PAGE_SIZE, addPagination } from "src/common/pagination"; +import { CoursesQuery } from "src/courses/api/courses.types"; +import { getSortOptions } from "src/common/helpers/getSortOptions"; +import { + CourseSortField, + CourseSortFields, +} from "src/courses/schemas/courseQuery"; +import { + categories, + courseLessons, + courses, + studentCourses, + studentFavouritedCourses, + users, +} from "src/storage/schema"; import { Status } from "src/storage/schema/utils"; +import { AllCoursesResponse } from "./schemas/course.schema"; @Injectable() export class StudentFavouritedCoursesService { @@ -43,6 +58,83 @@ export class StudentFavouritedCoursesService { .returning(); } + async getCoursesForStudents( + query: CoursesQuery, + userId: string, + ): Promise<{ data: AllCoursesResponse; pagination: Pagination }> { + const { + sort = CourseSortFields.title, + perPage = DEFAULT_PAGE_SIZE, + page = 1, + } = query; + + const { sortOrder, sortedField } = getSortOptions(sort); + + const selectedColumns = { + id: courses.id, + creationDate: courses.createdAt, + title: courses.title, + imageUrl: courses.imageUrl, + author: sql`CONCAT(${users.firstName} || ' ' || ${users.lastName})`, + category: categories.title, + courseLessonCount: sql`(SELECT COUNT(*) FROM ${courseLessons} WHERE ${courseLessons.courseId} = ${courses.id})::INTEGER`, + enrolledParticipantCount: count(studentCourses.courseId), + }; + + return this.db.transaction(async (tx) => { + const queryDB = tx + .select(selectedColumns) + .from(studentFavouritedCourses) + .innerJoin(courses, eq(studentFavouritedCourses.courseId, courses.id)) + .innerJoin(categories, eq(courses.categoryId, categories.id)) + .leftJoin(users, eq(courses.authorId, users.id)) + .leftJoin(studentCourses, eq(courses.id, studentCourses.courseId)) + .leftJoin(courseLessons, eq(courses.id, courseLessons.courseId)) + .where(and(eq(studentFavouritedCourses.studentId, userId))) + .orderBy( + sortOrder(this.getColumnToSortBy(sortedField as CourseSortField)), + ) + .groupBy( + courses.id, + courses.title, + courses.imageUrl, + users.firstName, + users.lastName, + categories.title, + ); + + const dynamicQuery = queryDB.$dynamic(); + const paginatedQuery = addPagination(dynamicQuery, page, perPage); + const data = await paginatedQuery; + const [totalItems] = await tx + .select({ count: count() }) + .from(studentFavouritedCourses) + .innerJoin(courses, eq(studentFavouritedCourses.courseId, courses.id)) + .innerJoin(categories, eq(courses.categoryId, categories.id)) + .leftJoin(users, eq(courses.authorId, users.id)) + .leftJoin(studentCourses, eq(courses.id, studentCourses.courseId)) + .leftJoin(courseLessons, eq(courses.id, courseLessons.courseId)) + .where(and(eq(studentFavouritedCourses.studentId, userId))) + .groupBy( + courses.id, + courses.title, + courses.imageUrl, + users.firstName, + users.lastName, + categories.title, + ); + + return { + data: data, + pagination: { + totalItems: totalItems?.count || 0, + page, + perPage, + }, + }; + }); + } + async deleteFavouritedCourseForUser(id: string, userId: string) { const [deletedFavouritedCourse] = await this.db .delete(studentFavouritedCourses) @@ -58,4 +150,21 @@ export class StudentFavouritedCoursesService { throw new NotFoundException("Favourite course not found"); } } + + private getColumnToSortBy(sort: CourseSortField) { + switch (sort) { + case CourseSortFields.author: + return sql`CONCAT(${users.firstName} || ' ' || ${users.lastName})`; + case CourseSortFields.category: + return categories.title; + case CourseSortFields.creationDate: + return courses.createdAt; + case CourseSortFields.lessonsCount: + return count(studentCourses.courseId); + case CourseSortFields.enrolledParticipantsCount: + return count(studentCourses.courseId); + default: + return courses.title; + } + } } diff --git a/apps/api/src/swagger/api-schema.json b/apps/api/src/swagger/api-schema.json index 34b82e629..8c6ba0e26 100644 --- a/apps/api/src/swagger/api-schema.json +++ b/apps/api/src/swagger/api-schema.json @@ -857,16 +857,81 @@ } } }, - "delete": { - "operationId": "StudentFavouritedCoursesController_deleteFavouritedCourseForUser", + "get": { + "operationId": "StudentFavouritedCoursesController_getAllCategories", "parameters": [ { - "name": "id", + "name": "page", "required": false, "in": "query", "schema": { - "format": "uuid", - "type": "string" + "minimum": 1, + "type": "number" + } + }, + { + "name": "perPage", + "required": false, + "in": "query", + "schema": { + "type": "number" + } + }, + { + "name": "sort", + "required": false, + "in": "query", + "schema": { + "anyOf": [ + { + "const": "title", + "type": "string" + }, + { + "const": "category", + "type": "string" + }, + { + "const": "creationDate", + "type": "string" + }, + { + "const": "author", + "type": "string" + }, + { + "const": "lessonsCount", + "type": "string" + }, + { + "const": "enrolledParticipantsCount", + "type": "string" + }, + { + "const": "-title", + "type": "string" + }, + { + "const": "-category", + "type": "string" + }, + { + "const": "-creationDate", + "type": "string" + }, + { + "const": "-author", + "type": "string" + }, + { + "const": "-lessonsCount", + "type": "string" + }, + { + "const": "-enrolledParticipantsCount", + "type": "string" + } + ] } } ], @@ -875,36 +940,15 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeleteFavouritedCourseForUserResponse" + "$ref": "#/components/schemas/GetAllCategoriesResponse" } } } } } - } - }, - "/api/studentFavouriteCourses": { - "post": { - "operationId": "StudentFavouriteCoursesController_createFavouriteCourse", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateFavouriteCourseBody" - } - } - } - }, - "responses": { - "201": { - "description": "" - } - } }, "delete": { - "operationId": "StudentFavouriteCoursesController_deleteFavouriteCourseForUser", + "operationId": "StudentFavouritedCoursesController_deleteFavouritedCourseForUser", "parameters": [ { "name": "id", @@ -921,7 +965,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeleteFavouriteCourseForUserResponse" + "$ref": "#/components/schemas/DeleteFavouritedCourseForUserResponse" } } } @@ -1604,4 +1648,4 @@ } } } -} +} \ No newline at end of file