From 8ac5ec18131ae4d270196dc7b1b472c01d025ff1 Mon Sep 17 00:00:00 2001 From: Artur Poffo Date: Fri, 23 Feb 2024 17:52:44 -0300 Subject: [PATCH] Feat(Course, Instructor, Route): Get course instructor details route --- README.md | 2 +- .../get-course-instructor-details.spec.ts | 69 +++++++++++++++++++ .../get-course-instructor-details.ts | 44 ++++++++++++ .../get-course-instructor-details.ts | 38 ++++++++++ src/infra/http/routes/course.ts | 2 + ...-get-course-instructor-details-use-case.ts | 15 ++++ 6 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 src/domain/course-management/application/use-cases/get-course-instructor-details.spec.ts create mode 100644 src/domain/course-management/application/use-cases/get-course-instructor-details.ts create mode 100644 src/infra/http/controllers/get-course-instructor-details.ts create mode 100644 src/infra/use-cases/factories/make-get-course-instructor-details-use-case.ts diff --git a/README.md b/README.md index 127ae74..8b3bc38 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,7 @@ - [x] GET /students/:studentId/enrollments - Get student courses with instructor and evaluations ### Instructors -- [ ] GET /courses/:courseId/instructor - Get course instructor details +- [x] GET /courses/:courseId/instructors - Get course instructor details - [ ] GET /instructors/:instructorId/courses - Get instructor courses with instructor and evaluations ### Courses diff --git a/src/domain/course-management/application/use-cases/get-course-instructor-details.spec.ts b/src/domain/course-management/application/use-cases/get-course-instructor-details.spec.ts new file mode 100644 index 0000000..f06c0d3 --- /dev/null +++ b/src/domain/course-management/application/use-cases/get-course-instructor-details.spec.ts @@ -0,0 +1,69 @@ +import { ResourceNotFoundError } from '@/core/errors/errors/resource-not-found-error' +import { makeCourse } from '../../../../../test/factories/make-course' +import { makeInstructor } from '../../../../../test/factories/make-instructor' +import { InMemoryClassesRepository } from '../../../../../test/repositories/in-memory-classes-repository' +import { InMemoryCourseTagsRepository } from '../../../../../test/repositories/in-memory-course-tags-repository' +import { InMemoryCoursesRepository } from '../../../../../test/repositories/in-memory-courses-repository' +import { InMemoryEnrollmentCompletedItemsRepository } from '../../../../../test/repositories/in-memory-enrollment-completed-items-repository' +import { InMemoryEnrollmentsRepository } from '../../../../../test/repositories/in-memory-enrollments-repository' +import { InMemoryInstructorRepository } from '../../../../../test/repositories/in-memory-instructors-repository' +import { InMemoryModulesRepository } from '../../../../../test/repositories/in-memory-modules-repository' +import { InMemoryStudentsRepository } from '../../../../../test/repositories/in-memory-students-repository' +import { GetCourseInstructorDetailsUseCase } from './get-course-instructor-details' + +let inMemoryEnrollmentCompletedItemsRepository: InMemoryEnrollmentCompletedItemsRepository +let inMemoryClassesRepository: InMemoryClassesRepository +let inMemoryCourseTagsRepository: InMemoryCourseTagsRepository +let inMemoryEnrollmentsRepository: InMemoryEnrollmentsRepository +let inMemoryStudentsRepository: InMemoryStudentsRepository +let inMemoryInstructorsRepository: InMemoryInstructorRepository +let inMemoryModulesRepository: InMemoryModulesRepository +let inMemoryCoursesRepository: InMemoryCoursesRepository +let sut: GetCourseInstructorDetailsUseCase + +describe('Get course instructor details use case', () => { + beforeEach(() => { + inMemoryEnrollmentCompletedItemsRepository = new InMemoryEnrollmentCompletedItemsRepository() + inMemoryClassesRepository = new InMemoryClassesRepository() + inMemoryCourseTagsRepository = new InMemoryCourseTagsRepository() + inMemoryStudentsRepository = new InMemoryStudentsRepository() + inMemoryInstructorsRepository = new InMemoryInstructorRepository() + + inMemoryModulesRepository = new InMemoryModulesRepository(inMemoryClassesRepository) + + inMemoryEnrollmentsRepository = new InMemoryEnrollmentsRepository( + inMemoryStudentsRepository, inMemoryEnrollmentCompletedItemsRepository + ) + inMemoryCoursesRepository = new InMemoryCoursesRepository(inMemoryModulesRepository, inMemoryInstructorsRepository, inMemoryEnrollmentsRepository, inMemoryStudentsRepository, inMemoryCourseTagsRepository) + + sut = new GetCourseInstructorDetailsUseCase(inMemoryCoursesRepository, inMemoryInstructorsRepository) + }) + + it('should be able to get course instructor details', async () => { + const instructor = makeInstructor({ name: 'John Doe' }) + await inMemoryInstructorsRepository.create(instructor) + + const course = makeCourse({ name: 'John Doe Course', instructorId: instructor.id }) + await inMemoryCoursesRepository.create(course) + + const result = await sut.exec({ + courseId: course.id.toString() + }) + + expect(result.isRight()).toBe(true) + expect(result.value).toMatchObject({ + instructor: expect.objectContaining({ + name: 'John Doe' + }) + }) + }) + + it('should not be able to get course details of a inexistent course', async () => { + const result = await sut.exec({ + courseId: 'inexistentCourseId' + }) + + expect(result.isLeft()).toBe(true) + expect(result.value).toBeInstanceOf(ResourceNotFoundError) + }) +}) diff --git a/src/domain/course-management/application/use-cases/get-course-instructor-details.ts b/src/domain/course-management/application/use-cases/get-course-instructor-details.ts new file mode 100644 index 0000000..0739873 --- /dev/null +++ b/src/domain/course-management/application/use-cases/get-course-instructor-details.ts @@ -0,0 +1,44 @@ +import { left, right, type Either } from '@/core/either' +import { ResourceNotFoundError } from '@/core/errors/errors/resource-not-found-error' +import { type UseCase } from '@/core/use-cases/use-case' +import { type Instructor } from '../../enterprise/entities/instructor' +import { type CoursesRepository } from '../repositories/courses-repository' +import { type InstructorsRepository } from '../repositories/instructors-repository' + +interface GetCourseInstructorDetailsUseCaseRequest { + courseId: string +} + +type GetCourseInstructorDetailsUseCaseResponse = Either< +ResourceNotFoundError, +{ + instructor: Instructor +} +> + +export class GetCourseInstructorDetailsUseCase implements UseCase { + constructor( + private readonly coursesRepository: CoursesRepository, + private readonly instructorsRepository: InstructorsRepository + ) { } + + async exec({ + courseId + }: GetCourseInstructorDetailsUseCaseRequest): Promise { + const course = await this.coursesRepository.findById(courseId) + + if (!course) { + return left(new ResourceNotFoundError()) + } + + const courseInstructor = await this.instructorsRepository.findById(course.instructorId.toString()) + + if (!courseInstructor) { + return left(new ResourceNotFoundError()) + } + + return right({ + instructor: courseInstructor + }) + } +} diff --git a/src/infra/http/controllers/get-course-instructor-details.ts b/src/infra/http/controllers/get-course-instructor-details.ts new file mode 100644 index 0000000..1aee5ff --- /dev/null +++ b/src/infra/http/controllers/get-course-instructor-details.ts @@ -0,0 +1,38 @@ +import { ResourceNotFoundError } from '@/core/errors/errors/resource-not-found-error' +import { makeInstructorMapper } from '@/infra/database/prisma/mappers/factories/make-instructor-mapper' +import { makeGetCourseInstructorDetailsUseCase } from '@/infra/use-cases/factories/make-get-course-instructor-details-use-case' +import { type FastifyReply, type FastifyRequest } from 'fastify' +import { z } from 'zod' +import { UserPresenter } from '../presenters/user-presenter' + +const getCourseInstructorDetailsParamsSchema = z.object({ + courseId: z.string().uuid() +}) + +export async function getCourseInstructorDetailsController(request: FastifyRequest, reply: FastifyReply) { + const { courseId } = getCourseInstructorDetailsParamsSchema.parse(request.params) + + const getCourseInstructorDetailsUseCase = makeGetCourseInstructorDetailsUseCase() + + const result = await getCourseInstructorDetailsUseCase.exec({ + courseId + }) + + 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 instructorMapper = makeInstructorMapper() + const instructor = await instructorMapper.toPrisma(result.value.instructor) + + return await reply.status(200).send({ + instructor: UserPresenter.toHTTP(instructor) + }) +} diff --git a/src/infra/http/routes/course.ts b/src/infra/http/routes/course.ts index a13b502..99d7a75 100644 --- a/src/infra/http/routes/course.ts +++ b/src/infra/http/routes/course.ts @@ -4,6 +4,7 @@ import { editCourseDetailsController } from '../controllers/edit-course-details' import { fetchCourseStudentsController } from '../controllers/fetch-course-students' import { fetchRecentCoursesController } from '../controllers/fetch-recent-courses' import { getCourseDetailsController } from '../controllers/get-course-details' +import { getCourseInstructorDetailsController } from '../controllers/get-course-instructor-details' import { getCourseMetricsController } from '../controllers/get-course-metrics' import { getCourseStatsController } from '../controllers/get-course-stats' import { queryCoursesByNameController } from '../controllers/query-courses-by-name' @@ -19,6 +20,7 @@ export async function courseRoutes(app: FastifyInstance) { app.get('/courses/filter/tags', queryCoursesByTagController) app.get('/courses/:courseId/stats', getCourseStatsController) app.get('/courses/:courseId/students', fetchCourseStudentsController) + app.get('/courses/:courseId/instructors', getCourseInstructorDetailsController) app.get('/courses/:courseId/metrics', { onRequest: [verifyJwt, verifyUserRole('INSTRUCTOR')] }, getCourseMetricsController) diff --git a/src/infra/use-cases/factories/make-get-course-instructor-details-use-case.ts b/src/infra/use-cases/factories/make-get-course-instructor-details-use-case.ts new file mode 100644 index 0000000..40991d0 --- /dev/null +++ b/src/infra/use-cases/factories/make-get-course-instructor-details-use-case.ts @@ -0,0 +1,15 @@ +import { GetCourseInstructorDetailsUseCase } from '@/domain/course-management/application/use-cases/get-course-instructor-details' +import { makePrismaCoursesRepository } from '@/infra/database/prisma/repositories/factories/make-prisma-courses-repository' +import { makePrismaInstructorsRepository } from '@/infra/database/prisma/repositories/factories/make-prisma-instructors-repository' + +export function makeGetCourseInstructorDetailsUseCase() { + const prismaCoursesRepository = makePrismaCoursesRepository() + const prismaInstructorsRepository = makePrismaInstructorsRepository() + + const getCourseDetailsUseCase = new GetCourseInstructorDetailsUseCase( + prismaCoursesRepository, + prismaInstructorsRepository + ) + + return getCourseDetailsUseCase +}