Skip to content

Commit

Permalink
Merge pull request #16 from Artur-Poffo/feat-implement-upload-class-v…
Browse files Browse the repository at this point in the history
…ideo-use-case

Feat(Class, Video, Events, Storage Domain): Implement upload video use case
  • Loading branch information
Artur-Poffo committed Feb 1, 2024
2 parents 7207214 + d9ad2ef commit d9ff9dc
Show file tree
Hide file tree
Showing 40 changed files with 725 additions and 78 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@typescript-eslint/space-before-function-paren": "off",
"@typescript-eslint/no-extraneous-class": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/no-misused-promises": "off"
"@typescript-eslint/no-misused-promises": "off",
"no-new": "off"
}
}
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
- [x] Instructors can add classes to modules.
- [x] Should be able to register tags
- [x] Responsible instructor can add "tags" to their course to inform students about technologies present in the course.
- [ ] Instructors can upload videos.
- [x] Instructors can create classes and upload videos to them.
- [ ] Instructor can upload a certificate template for students upon course completion.
- [ ] Return information of an instructor with their courses.

Expand Down Expand Up @@ -50,7 +50,7 @@
- [x] Should not be able to register a module to a specific course with same name twice.
- [ ] A student can only issue one certificate per course.
- [ ] A student can only enroll for a particular course once.
- [ ] There should not be repeated tags in a course.
- [x] There should not be repeated tags in a course.

## Non-Functional Requirements

Expand Down Expand Up @@ -139,19 +139,29 @@
- studentId: string
- issuedAt: date

- [x] Video
- - id: string
- videoName: string
- videoType: 'video/mp4' | 'video/avi'
- body: Buffer
- duration: number
- size: number
- storedAt: Date

- [x] Class
- - id: string
- name: string
- description: string
- duration: number
- videoKey: string
- videoId: string
- classNumber: number
- moduleId: string

## Storage Domain

- [ ] File
- [x] File
- - id: string
- fileName: string
- filType: string
- body: buffer
- fileKey: string
- size: number
- storedAt: Date
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import { type Class } from '../../enterprise/entities/class'
export interface ClassesRepository {
findById: (id: string) => Promise<Class | null>
findManyByCourseId: (courseId: string) => Promise<Class[]>
findManyByModuleId: (moduleId: string) => Promise<Class[]>
create: (classToAdd: Class) => Promise<Class>
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { type Module } from '../../enterprise/entities/module'
import { type ModuleWithClassesDTO } from './../../enterprise/entities/dtos/module-with-classes'

export interface ModulesRepository {
findById: (id: string) => Promise<Module | null>
findManyByCourseId: (id: string) => Promise<Module[]>
findModuleWithClassesById: (id: string) => Promise<ModuleWithClassesDTO | null>
create: (module: Module) => Promise<Module>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { type Video } from '../../enterprise/entities/video'

export interface VideosRepository {
findById: (id: string) => Promise<Video | null>
appendVideoKey: (videoKey: string, videoId: string) => Promise<Video | null>
create: (video: Video) => Promise<Video>
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,39 @@ import { ResourceNotFoundError } from '@/core/errors/errors/resource-not-found-e
import { makeCourse } from '../../../../../test/factories/make-course'
import { makeInstructor } from '../../../../../test/factories/make-instructor'
import { makeModule } from '../../../../../test/factories/make-module'
import { makeVideo } from '../../../../../test/factories/make-video'
import { InMemoryClassesRepository } from '../../../../../test/repositories/in-memory-classes-repository'
import { InMemoryCoursesRepository } from '../../../../../test/repositories/in-memory-courses-repository'
import { InMemoryVideosRepository } from '../../../../../test/repositories/in-memory-videos-repository'
import { InMemoryInstructorRepository } from './../../../../../test/repositories/in-memory-instructors-repository'
import { InMemoryModulesRepository } from './../../../../../test/repositories/in-memory-modules-repository'
import { AddClassToModuleUseCase } from './add-class-to-module'
import { ClassAlreadyExistsInThisModuleError } from './errors/class-already-exists-in-this-module-error'
import { ClassNumberIsAlreadyInUseError } from './errors/class-number-is-already-in-use-error'
import { ClassVideoRequiredError } from './errors/class-video-required-error'

let inMemoryVideosRepository: InMemoryVideosRepository
let inMemoryClassesRepository: InMemoryClassesRepository
let inMemoryModulesRepository: InMemoryModulesRepository
let inMemoryCoursesRepository: InMemoryCoursesRepository
let inMemoryInstructorsRepository: InMemoryInstructorRepository
let inMemoryClassesRepository: InMemoryClassesRepository
let sut: AddClassToModuleUseCase

describe('Add class to a module use case', () => {
beforeEach(() => {
inMemoryModulesRepository = new InMemoryModulesRepository()
inMemoryVideosRepository = new InMemoryVideosRepository()

inMemoryClassesRepository = new InMemoryClassesRepository(inMemoryModulesRepository)
inMemoryModulesRepository = new InMemoryModulesRepository(inMemoryClassesRepository)
inMemoryInstructorsRepository = new InMemoryInstructorRepository(inMemoryCoursesRepository)
inMemoryCoursesRepository = new InMemoryCoursesRepository(inMemoryModulesRepository, inMemoryClassesRepository, inMemoryInstructorsRepository)
sut = new AddClassToModuleUseCase(inMemoryClassesRepository, inMemoryModulesRepository, inMemoryCoursesRepository)

inMemoryCoursesRepository = new InMemoryCoursesRepository(
inMemoryModulesRepository, inMemoryInstructorsRepository
)

sut = new AddClassToModuleUseCase(
inMemoryClassesRepository, inMemoryModulesRepository, inMemoryCoursesRepository, inMemoryVideosRepository
)
})

it('should be able to add a new class to a module', async () => {
Expand All @@ -33,6 +45,9 @@ describe('Add class to a module use case', () => {
const course = makeCourse({ instructorId: instructor.id })
await inMemoryCoursesRepository.create(course)

const video = makeVideo()
await inMemoryVideosRepository.create(video)

const module = makeModule({
courseId: course.id,
moduleNumber: 1
Expand All @@ -43,8 +58,7 @@ describe('Add class to a module use case', () => {
name: 'John Doe Class',
description: 'Class description',
classNumber: 1,
duration: 600,
videoKey: 'video-key',
videoId: video.id.toString(),
instructorId: course.instructorId.toString(),
moduleId: module.id.toString()
})
Expand All @@ -57,19 +71,47 @@ describe('Add class to a module use case', () => {
})
})

it('should not be able to add a new class to a module with a inexistent video', async () => {
const instructor = makeInstructor()
await inMemoryInstructorsRepository.create(instructor)

const course = makeCourse({ instructorId: instructor.id })
await inMemoryCoursesRepository.create(course)

const module = makeModule({
courseId: course.id,
moduleNumber: 1
})
await inMemoryModulesRepository.create(module)

const result = await sut.exec({
name: 'John Doe Class',
description: 'Class description',
classNumber: 1,
videoId: 'inexistentVideoId',
instructorId: course.instructorId.toString(),
moduleId: module.id.toString()
})

expect(result.isLeft()).toBe(true)
expect(result.value).toBeInstanceOf(ClassVideoRequiredError)
})

it('should not be able to add a class for a inexistent module', async () => {
const instructor = makeInstructor()
await inMemoryInstructorsRepository.create(instructor)

const video = makeVideo()
await inMemoryVideosRepository.create(video)

const course = makeCourse({ instructorId: instructor.id })
await inMemoryCoursesRepository.create(course)

const result = await sut.exec({
name: 'John Doe Class',
description: 'Class description',
classNumber: 1,
duration: 600,
videoKey: 'video-key',
videoId: video.id.toString(),
instructorId: course.instructorId.toString(),
moduleId: 'inexistentModuleId'
})
Expand All @@ -89,6 +131,9 @@ describe('Add class to a module use case', () => {
const course = makeCourse({ instructorId: sponsor.id })
await inMemoryCoursesRepository.create(course)

const video = makeVideo()
await inMemoryVideosRepository.create(video)

const module = makeModule({
courseId: course.id,
moduleNumber: 1
Expand All @@ -99,8 +144,7 @@ describe('Add class to a module use case', () => {
name: 'John Doe Class',
description: 'Class description',
classNumber: 1,
duration: 600,
videoKey: 'video-key',
videoId: video.id.toString(),
instructorId: wrongInstructor.id.toString(),
moduleId: module.id.toString()
})
Expand All @@ -116,6 +160,9 @@ describe('Add class to a module use case', () => {
const course = makeCourse({ instructorId: instructor.id })
await inMemoryCoursesRepository.create(course)

const video = makeVideo()
await inMemoryVideosRepository.create(video)

const module = makeModule({
courseId: course.id,
moduleNumber: 1
Expand All @@ -126,8 +173,7 @@ describe('Add class to a module use case', () => {
name: 'John Doe Class',
description: 'Class description',
classNumber: 1,
duration: 600,
videoKey: 'video-key',
videoId: video.id.toString(),
instructorId: course.instructorId.toString(),
moduleId: module.id.toString()
})
Expand All @@ -136,8 +182,7 @@ describe('Add class to a module use case', () => {
name: 'John Doe Class',
description: 'Class description',
classNumber: 2,
duration: 600,
videoKey: 'video-key',
videoId: video.id.toString(),
instructorId: course.instructorId.toString(),
moduleId: module.id.toString()
})
Expand All @@ -153,6 +198,9 @@ describe('Add class to a module use case', () => {
const course = makeCourse({ instructorId: instructor.id })
await inMemoryCoursesRepository.create(course)

const video = makeVideo()
await inMemoryVideosRepository.create(video)

const module = makeModule({
courseId: course.id,
moduleNumber: 1
Expand All @@ -163,8 +211,7 @@ describe('Add class to a module use case', () => {
name: 'John Doe Class 1',
description: 'Class description',
classNumber: 1, // Add a class to the first position
duration: 600,
videoKey: 'video-key',
videoId: video.id.toString(),
instructorId: course.instructorId.toString(),
moduleId: module.id.toString()
})
Expand All @@ -173,8 +220,7 @@ describe('Add class to a module use case', () => {
name: 'John Doe Class 2',
description: 'Class description',
classNumber: 1, // trying to add a new class for the same position, as the first class
duration: 600,
videoKey: 'video-key',
videoId: video.id.toString(),
instructorId: course.instructorId.toString(),
moduleId: module.id.toString()
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { left, right, type Either } from '@/core/either'
import { UniqueEntityID } from '@/core/entities/unique-entity-id'
import { NotAllowedError } from '@/core/errors/errors/not-allowed-error'
import { ResourceNotFoundError } from '@/core/errors/errors/resource-not-found-error'
import { type UseCase } from '@/core/use-cases/use-case'
import { Class } from '../../enterprise/entities/class'
import { type ClassesRepository } from '../repositories/classes-repository'
import { type CoursesRepository } from '../repositories/courses-repository'
import { type ModulesRepository } from '../repositories/modules-repository'
import { type VideosRepository } from './../repositories/videos-repository'
import { ClassAlreadyExistsInThisModuleError } from './errors/class-already-exists-in-this-module-error'
import { ClassNumberIsAlreadyInUseError } from './errors/class-number-is-already-in-use-error'
import { ClassVideoRequiredError } from './errors/class-video-required-error'

interface AddClassToModuleUseCaseRequest {
name: string
description: string
duration: number
videoKey: string
videoId: string
classNumber: number
moduleId: string
instructorId: string
Expand All @@ -31,15 +31,15 @@ export class AddClassToModuleUseCase implements UseCase<AddClassToModuleUseCaseR
constructor(
private readonly classesRepository: ClassesRepository,
private readonly modulesRepository: ModulesRepository,
private readonly coursesRepository: CoursesRepository
private readonly coursesRepository: CoursesRepository,
private readonly videosRepository: VideosRepository
) {}

async exec({
name,
description,
duration,
videoId,
classNumber,
videoKey,
moduleId,
instructorId
}: AddClassToModuleUseCaseRequest): Promise<AddClassToModuleUseCaseResponse> {
Expand All @@ -61,7 +61,13 @@ export class AddClassToModuleUseCase implements UseCase<AddClassToModuleUseCaseR
return left(new NotAllowedError())
}

const classesInThisModule = completeCourse.classes.filter(classToCompare => classToCompare.moduleId.toString() === moduleId)
const courseClasses: Class[] = []

completeCourse.modules.forEach(moduleToExtract => {
courseClasses.push(...moduleToExtract.classes)
})

const classesInThisModule = courseClasses.filter(classToCompare => classToCompare.moduleId.toString() === moduleId)
const classWithSameNameInSameModule = classesInThisModule.find(classToCompare => classToCompare.name === name)

if (classWithSameNameInSameModule) {
Expand All @@ -74,13 +80,18 @@ export class AddClassToModuleUseCase implements UseCase<AddClassToModuleUseCaseR
return left(new ClassNumberIsAlreadyInUseError(classNumber))
}

const video = await this.videosRepository.findById(videoId)

if (!video) {
return left(new ClassVideoRequiredError())
}

const classToAdd = Class.create({
name,
description,
duration,
videoKey,
videoId: video.id,
classNumber,
moduleId: new UniqueEntityID(moduleId)
moduleId: module.id
})

await this.classesRepository.create(classToAdd)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ describe('Attach tag to course use case', () => {
beforeEach(() => {
inMemoryCourseTagsRepository = new InMemoryCourseTagsRepository()
inMemoryTagsRepository = new InMemoryTagsRepository()
inMemoryModulesRepository = new InMemoryModulesRepository()
inMemoryModulesRepository = new InMemoryModulesRepository(inMemoryClassesRepository)
inMemoryClassesRepository = new InMemoryClassesRepository(inMemoryModulesRepository)
inMemoryInstructorsRepository = new InMemoryInstructorRepository(inMemoryCoursesRepository)
inMemoryCoursesRepository = new InMemoryCoursesRepository(inMemoryModulesRepository, inMemoryClassesRepository, inMemoryInstructorsRepository)
inMemoryCoursesRepository = new InMemoryCoursesRepository(inMemoryModulesRepository, inMemoryInstructorsRepository)
sut = new AttachTagToCourseUseCase(inMemoryCourseTagsRepository, inMemoryTagsRepository, inMemoryCoursesRepository)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ let sut: AuthenticateUserUseCase

describe('Authenticate user use case', () => {
beforeEach(() => {
inMemoryModulesRepository = new InMemoryModulesRepository()
inMemoryModulesRepository = new InMemoryModulesRepository(inMemoryClassesRepository)
inMemoryClassesRepository = new InMemoryClassesRepository(inMemoryModulesRepository)
inMemoryCoursesRepository = new InMemoryCoursesRepository(inMemoryModulesRepository, inMemoryClassesRepository, inMemoryInstructorsRepository)
inMemoryCoursesRepository = new InMemoryCoursesRepository(inMemoryModulesRepository, inMemoryInstructorsRepository)
inMemoryInstructorsRepository = new InMemoryInstructorRepository(inMemoryCoursesRepository)
inMemoryStudentsRepository = new InMemoryStudentsRepository()
inMemoryUsersRepository = new InMemoryUsersRepository(inMemoryInstructorsRepository, inMemoryStudentsRepository)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { type UseCaseError } from '@/core/errors/use-case-error'

export class ClassVideoRequiredError extends Error implements UseCaseError {
constructor() {
super('Class video is required.')
}
}
Loading

0 comments on commit d9ff9dc

Please sign in to comment.