diff --git a/README.md b/README.md index 8b3bc38..8de0036 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ ### Instructors - [x] GET /courses/:courseId/instructors - Get course instructor details -- [ ] GET /instructors/:instructorId/courses - Get instructor courses with instructor and evaluations +- [x] GET /instructors/:instructorId/courses - Get instructor courses with instructor and evaluations ### Courses - [x] GET /courses/:courseId - Get course details diff --git a/src/domain/course-management/application/subscribers/on-file-uploaded.spec.ts b/src/domain/course-management/application/subscribers/on-file-uploaded.spec.ts index 8428ba6..dc79154 100644 --- a/src/domain/course-management/application/subscribers/on-file-uploaded.spec.ts +++ b/src/domain/course-management/application/subscribers/on-file-uploaded.spec.ts @@ -1,4 +1,5 @@ import { OnFileUploaded } from '@/domain/course-management/application/subscribers/on-file-uploaded' +import { GetVideoDuration } from '@/infra/storage/utils/get-video-duration' import { InMemoryVideosRepository } from '../../../../../test/repositories/in-memory-videos-repository' import { waitFor } from '../../../../../test/utils/wait-for' import { InMemoryFilesRepository } from './../../../../../test/repositories/in-memory-files-repository' @@ -10,6 +11,7 @@ let inMemoryFilesRepository: InMemoryFilesRepository let inMemoryImagesRepository: InMemoryImagesRepository let inMemoryVideosRepository: InMemoryVideosRepository let fakeUploader: FakeUploader +let getVideoDuration: GetVideoDuration let uploadFileUseCase: UploadFileUseCase describe('On file uploaded event', () => { @@ -18,6 +20,7 @@ describe('On file uploaded event', () => { inMemoryImagesRepository = new InMemoryImagesRepository() inMemoryVideosRepository = new InMemoryVideosRepository() fakeUploader = new FakeUploader() + getVideoDuration = new GetVideoDuration() uploadFileUseCase = new UploadFileUseCase( inMemoryFilesRepository, fakeUploader @@ -25,7 +28,8 @@ describe('On file uploaded event', () => { new OnFileUploaded( inMemoryImagesRepository, - inMemoryVideosRepository + inMemoryVideosRepository, + getVideoDuration ) }) diff --git a/src/domain/course-management/application/subscribers/on-file-uploaded.ts b/src/domain/course-management/application/subscribers/on-file-uploaded.ts index 0d22b1b..5530aea 100644 --- a/src/domain/course-management/application/subscribers/on-file-uploaded.ts +++ b/src/domain/course-management/application/subscribers/on-file-uploaded.ts @@ -5,11 +5,13 @@ import { Image } from '../../enterprise/entities/image' import { Video } from '../../enterprise/entities/video' import { type ImagesRepository } from '../repositories/images-repository' import { type VideosRepository } from '../repositories/videos-repository' +import { type GetVideoDuration } from './../../../../infra/storage/utils/get-video-duration' export class OnFileUploaded implements EventHandler { constructor( private readonly imagesRepository: ImagesRepository, - private readonly videosRepository: VideosRepository + private readonly videosRepository: VideosRepository, + private readonly getVideoDuration: GetVideoDuration ) { this.setupSubscriptions() } @@ -34,13 +36,15 @@ export class OnFileUploaded implements EventHandler { await this.imagesRepository.create(image) } else if (/video\/(mp4|avi)/.test(file.fileType)) { + const duration = await this.getVideoDuration.getInSecondsByBuffer(file.body) + const video = Video.create({ body: file.body, videoName: file.fileName, size: file.size, videoKey: file.fileKey, videoType: file.fileType as 'video/mp4' | 'video/avi', - duration: 24, + duration: Math.round(duration), storedAt: file.storedAt }) diff --git a/src/infra/app.ts b/src/infra/app.ts index 9c5c16f..fdbb4ce 100644 --- a/src/infra/app.ts +++ b/src/infra/app.ts @@ -14,6 +14,7 @@ import { enrollmentRoutes } from './http/routes/enrollment' import { evaluationRoutes } from './http/routes/evaluation' import { fileRoutes } from './http/routes/file' import { imageRoutes } from './http/routes/image' +import { instructorRoutes } from './http/routes/instructor' import { moduleRoutes } from './http/routes/module' import { studentRoutes } from './http/routes/student' import { studentCertificateRoutes } from './http/routes/student-certificate' @@ -48,13 +49,13 @@ app.register(fastifyJwt, { app.register(fastifyCookie) export const upload = multer() - app.register(multer.contentParser) // API Routes app.register(userRoutes) app.register(studentRoutes) +app.register(instructorRoutes) app.register(courseRoutes) app.register(fileRoutes) app.register(imageRoutes) diff --git a/src/infra/events/factories/make-on-file-uploaded.ts b/src/infra/events/factories/make-on-file-uploaded.ts index 4825940..859777d 100644 --- a/src/infra/events/factories/make-on-file-uploaded.ts +++ b/src/infra/events/factories/make-on-file-uploaded.ts @@ -1,14 +1,17 @@ import { OnFileUploaded } from '@/domain/course-management/application/subscribers/on-file-uploaded' import { PrismaImagesRepository } from '@/infra/database/prisma/repositories/prisma-images-repository' import { PrismaVideosRepository } from '@/infra/database/prisma/repositories/prisma-videos-repository' +import { GetVideoDuration } from './../../storage/utils/get-video-duration' export function makeOnFileUploaded() { const prismaImagesRepository = new PrismaImagesRepository() const prismaVideosRepository = new PrismaVideosRepository() + const getVideoDuration = new GetVideoDuration() const onVideoKeyGenerated = new OnFileUploaded( prismaImagesRepository, - prismaVideosRepository + prismaVideosRepository, + getVideoDuration ) return onVideoKeyGenerated diff --git a/src/infra/http/controllers/fetch-instructor-courses.ts b/src/infra/http/controllers/fetch-instructor-courses.ts new file mode 100644 index 0000000..506e335 --- /dev/null +++ b/src/infra/http/controllers/fetch-instructor-courses.ts @@ -0,0 +1,93 @@ +import { ResourceNotFoundError } from '@/core/errors/errors/resource-not-found-error' +import { type CourseWithInstructorAndEvaluationDTO } from '@/domain/course-management/enterprise/entities/dtos/course-with-instructor-and-evaluation' +import { CourseDtoMapper } from '@/domain/course-management/enterprise/entities/dtos/mappers/course-dto-mapper' +import { makeGetCourseEvaluationsAverageUseCase } from '@/infra/use-cases/factories/make-get-course-evaluations-average-use-case' +import { makeGetInstructorWithCoursesUseCase } from '@/infra/use-cases/factories/make-get-instructor-with-courses-use-case' +import { makeGetUserInfoUseCase } from '@/infra/use-cases/factories/make-get-user-info-use-case' +import { type FastifyReply, type FastifyRequest } from 'fastify' +import { z } from 'zod' +import { CoursesWithInstructorAndEvaluationPresenter } from '../presenters/courses-with-instructor-and-evaluation-presenter' + +const fetchInstructorCoursesParamsSchema = z.object({ + instructorId: z.string().uuid() +}) + +export async function fetchInstructorCoursesController(request: FastifyRequest, reply: FastifyReply) { + const { instructorId } = fetchInstructorCoursesParamsSchema.parse(request.params) + + const fetchInstructorCoursesUseCase = makeGetInstructorWithCoursesUseCase() + const getUserInfoUseCase = makeGetUserInfoUseCase() + const getCourseEvaluationsAverageUseCase = makeGetCourseEvaluationsAverageUseCase() + + const result = await fetchInstructorCoursesUseCase.exec({ + instructorId + }) + + if (result.isLeft()) { + const error = result.value + + switch (error.constructor) { + case ResourceNotFoundError: + return await reply.status(404).send({ message: error.message }) + default: + return await reply.status(500).send({ message: error.message }) + } + } + + const courses = result.value.instructorWithCourses.courses + + const coursesWithInstructorAndEvaluation: CourseWithInstructorAndEvaluationDTO[] = [] + + await Promise.all( + courses.map(async (course) => { + const instructorResult = await getUserInfoUseCase.exec({ + id: course.instructorId.toString() + }) + const courseEvaluationAverageResult = await getCourseEvaluationsAverageUseCase.exec({ + courseId: course.id.toString() + }) + + if (instructorResult.isLeft()) { + const error = instructorResult.value + + switch (error.constructor) { + case ResourceNotFoundError: + return await reply.status(404).send({ message: error.message }) + default: + return await reply.status(500).send({ message: error.message }) + } + } + + if (courseEvaluationAverageResult.isLeft()) { + return await reply.status(500).send() + } + + const { user } = instructorResult.value + const { evaluationsAverage } = courseEvaluationAverageResult.value + + const courseWithInstructorAndEvaluation: CourseWithInstructorAndEvaluationDTO = { + course: CourseDtoMapper.toDTO(course), + instructor: { + id: user.id, + name: user.name, + email: user.email, + age: user.age, + registeredAt: user.registeredAt, + summary: user.summary, + bannerImageKey: user.bannerImageKey, + profileImageKey: user.profileImageKey + }, + evaluationsAverage + } + + coursesWithInstructorAndEvaluation.push(courseWithInstructorAndEvaluation) + }) + ) + + return await reply.status(200).send({ + courses: coursesWithInstructorAndEvaluation.map(courseWithInstructorAndEvaluation => + CoursesWithInstructorAndEvaluationPresenter.toHTTP(courseWithInstructorAndEvaluation + ) + ) + }) +} diff --git a/src/infra/http/routes/instructor.ts b/src/infra/http/routes/instructor.ts new file mode 100644 index 0000000..4560a30 --- /dev/null +++ b/src/infra/http/routes/instructor.ts @@ -0,0 +1,6 @@ +import { type FastifyInstance } from 'fastify' +import { fetchInstructorCoursesController } from '../controllers/fetch-instructor-courses' + +export async function instructorRoutes(app: FastifyInstance) { + app.get('/instructors/:instructorId/courses', fetchInstructorCoursesController) +} diff --git a/src/infra/http/routes/student.ts b/src/infra/http/routes/student.ts index 09a9ad1..8c95031 100644 --- a/src/infra/http/routes/student.ts +++ b/src/infra/http/routes/student.ts @@ -1,7 +1,6 @@ import { type FastifyInstance } from 'fastify' import { fetchStudentCoursesController } from '../controllers/fetch-student-courses' -import { verifyJwt } from '../middlewares/verify-jwt' export async function studentRoutes(app: FastifyInstance) { - app.get('/students/:studentId/enrollments', { onRequest: [verifyJwt] }, fetchStudentCoursesController) + app.get('/students/:studentId/enrollments', fetchStudentCoursesController) }