Skip to content

Commit

Permalink
Feat(Image, File, Upload Events): Image entity and image upload
Browse files Browse the repository at this point in the history
  • Loading branch information
Artur-Poffo committed Feb 3, 2024
1 parent d9ff9dc commit 3e49ea5
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 4 deletions.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,17 @@
- moduleNumber: number
- courseId: string

- [x] Image
- - id: string
- imageName: string
- imageType: 'image/jpeg' | 'image/png'
- body: Buffer
- size: number
- storedAt: Date

- [x] Certificate
- - id: string
- imageKey: string
- imageId: string
- courseId: string

- [x] StudentCertificate
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { type Image } from '../../enterprise/entities/image'

export interface ImagesRepository {
findById: (id: string) => Promise<Image | null>
appendImageKey: (imageKey: string, imageId: string) => Promise<Image | null>
create: (image: Image) => Promise<Image>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { InMemoryImagesRepository } from './../../../../../test/repositories/in-memory-images-repository'
import { UploadImageUseCase } from './upload-image'

let inMemoryImagesRepository: InMemoryImagesRepository
let sut: UploadImageUseCase

describe('Add image to class use case', () => {
beforeEach(() => {
inMemoryImagesRepository = new InMemoryImagesRepository()
sut = new UploadImageUseCase(inMemoryImagesRepository)
})

it('should be able to upload a image', async () => {
const result = await sut.exec({
imageName: 'imageName',
imageType: 'image/jpeg',
body: Buffer.from('imageBody'),
duration: 60 * 10, // Ten minutes,
size: 1024 * 1024 // 1MB
})

expect(result.isRight()).toBe(true)
})
})
47 changes: 47 additions & 0 deletions src/domain/course-management/application/use-cases/upload-image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { right, type Either } from '@/core/either'
import { type ResourceNotFoundError } from '@/core/errors/errors/resource-not-found-error'
import { type UseCase } from '@/core/use-cases/use-case'
import { Image } from '../../enterprise/entities/image'
import { type ImagesRepository } from './../repositories/images-repository'

interface UploadImageUseCaseRequest {
imageName: string
imageType?: 'image/jpeg' | 'image/png'
body: Buffer
duration: number
size: number
}

type UploadImageUseCaseResponse = Either<
ResourceNotFoundError,
{
image: Image
}
>

export class UploadImageUseCase implements UseCase<UploadImageUseCaseRequest, UploadImageUseCaseResponse> {
constructor(
private readonly imagesRepository: ImagesRepository
) {}

async exec({
imageName,
imageType,
body,
duration,
size
}: UploadImageUseCaseRequest): Promise<UploadImageUseCaseResponse> {
const image = Image.create({
imageName,
imageType,
body,
size
})

await this.imagesRepository.create(image)

return right({
image
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { Entity } from '@/core/entities/entity'
import { type UniqueEntityID } from '@/core/entities/unique-entity-id'

export interface CertificateProps {
imageKey: string
imageId: UniqueEntityID
courseId: UniqueEntityID
}

export class Certificate extends Entity<CertificateProps> {
get imageKey() {
return this.props.imageKey
get imageId() {
return this.props.imageId
}

get courseId() {
Expand Down
65 changes: 65 additions & 0 deletions src/domain/course-management/enterprise/entities/image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { AggregateRoot } from '@/core/entities/aggregate-root'
import { type UniqueEntityID } from '@/core/entities/unique-entity-id'
import { type Optional } from '@/core/types/optional'
import { ImageUploadedEvent } from '../events/image-uploaded'

export interface ImageProps {
imageName: string
imageType: 'image/jpeg' | 'image/png'
body: Buffer
size: number
imageKey?: string
storedAt: Date
}

export class Image extends AggregateRoot<ImageProps> {
get imageName() {
return this.props.imageName
}

get imageType() {
return this.props.imageType
}

get body() {
return this.props.body
}

get size() {
return this.props.size
}

get imageKey() {
return this.props.imageKey
}

set imageKey(imageKeyToAppend) {
this.props.imageKey = imageKeyToAppend
}

get storedAt() {
return this.props.storedAt
}

static create(
props: Optional<ImageProps, 'storedAt' | 'imageType'>,
id?: UniqueEntityID
) {
const image = new Image(
{
imageType: props.imageType ?? 'image/jpeg',
storedAt: props.storedAt ?? new Date(),
...props
},
id
)

const isNewImage = !id

if (isNewImage) {
image.addDomainEvent(new ImageUploadedEvent(image))
}

return image
}
}
17 changes: 17 additions & 0 deletions src/domain/course-management/enterprise/events/image-uploaded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { type UniqueEntityID } from '@/core/entities/unique-entity-id'
import { type DomainEvent } from '@/core/events/domain-event'
import { type Image } from '../entities/image'

export class ImageUploadedEvent implements DomainEvent {
public image: Image
public ocurredAt: Date

constructor(image: Image) {
this.image = image
this.ocurredAt = new Date()
}

getAggregateId(): UniqueEntityID {
return this.image.id
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { makeImage } from '../../../../../test/factories/make-image'
import { InMemoryFilesRepository } from '../../../../../test/repositories/in-memory-files-repository'
import { InMemoryImagesRepository } from '../../../../../test/repositories/in-memory-images-repository'
import { FakeUploader } from '../../../../../test/storage/fake-uploader'
import { waitFor } from '../../../../../test/utils/wait-for'
import { UploadFileUseCase } from './../use-cases/upload-file'
import { OnImageUploaded } from './on-image-uploaded'

let inMemoryFilesRepository: InMemoryFilesRepository
let inMemoryImagesRepository: InMemoryImagesRepository
let fakeUploader: FakeUploader
let uploadFileUseCase: UploadFileUseCase

describe('On image uploaded', () => {
beforeEach(() => {
inMemoryFilesRepository = new InMemoryFilesRepository()
inMemoryImagesRepository = new InMemoryImagesRepository()
fakeUploader = new FakeUploader()
uploadFileUseCase = new UploadFileUseCase(inMemoryFilesRepository, fakeUploader)

new OnImageUploaded(uploadFileUseCase, inMemoryImagesRepository)
})

it('should be able to upload a image', async () => {
const image = makeImage()
await inMemoryImagesRepository.create(image)

expect(inMemoryFilesRepository.items[0]).toMatchObject({
fileName: image.imageName
})
expect(fakeUploader.files[0]).toMatchObject({
fileName: image.imageName
})
})

it('should append generated fileKey to pattern file on courses domain', async () => {
const image = makeImage()
await inMemoryImagesRepository.create(image)

await waitFor(() => {
expect(inMemoryImagesRepository.items[0]).toMatchObject({
imageKey: fakeUploader.files[0].fileKey
})
})
})
})
37 changes: 37 additions & 0 deletions src/domain/storage/application/subscribers/on-image-uploaded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { DomainEvents } from '@/core/events/domain-events'
import { type EventHandler } from '@/core/events/event-handler'
import { type ImagesRepository } from '@/domain/course-management/application/repositories/images-repository'
import { ImageUploadedEvent } from '@/domain/course-management/enterprise/events/image-uploaded'
import { type UploadFileUseCase } from '../use-cases/upload-file'

export class OnImageUploaded implements EventHandler {
constructor(
private readonly uploadFileUseCase: UploadFileUseCase,
private readonly imagesRepository: ImagesRepository
) {
this.setupSubscriptions()
}

setupSubscriptions(): void {
DomainEvents.register(
this.uploadImage.bind(this) as (event: unknown) => void,
ImageUploadedEvent.name
)
}

private async uploadImage({ image }: ImageUploadedEvent) {
const result = await this.uploadFileUseCase.exec({
fileName: image.imageName,
fileType: image.imageType,
body: image.body,
size: image.size,
storedAt: image.storedAt
})

if (result.isRight()) {
const { fileKey } = result.value.file

await this.imagesRepository.appendImageKey(fileKey, image.id.toString())
}
}
}
21 changes: 21 additions & 0 deletions test/factories/make-image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { type UniqueEntityID } from '@/core/entities/unique-entity-id'
import { Image, type ImageProps } from '@/domain/course-management/enterprise/entities/image'
import { faker } from '@faker-js/faker'

export function makeImage(
override: Partial<ImageProps> = {},
id?: UniqueEntityID
) {
const image = Image.create(
{
imageName: faker.company.name(),
imageType: 'image/jpeg',
body: Buffer.from(faker.lorem.slug()),
size: faker.number.int(),
...override
},
id
)

return image
}
39 changes: 39 additions & 0 deletions test/repositories/in-memory-images-repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { DomainEvents } from '@/core/events/domain-events'
import { type ImagesRepository } from '@/domain/course-management/application/repositories/images-repository'
import { type Image } from '@/domain/course-management/enterprise/entities/image'

export class InMemoryImagesRepository implements ImagesRepository {
items: Image[] = []

async findById(id: string): Promise<Image | null> {
const image = this.items.find(imageToCompare => imageToCompare.id.toString() === id)

if (!image) {
return null
}

return image
}

async appendImageKey(imageKey: string, id: string): Promise<Image | null> {
const imageToAppendKey = this.items.find(imageToCompare => imageToCompare.id.toString() === id)

if (!imageToAppendKey) {
return null
}

if (!imageToAppendKey.imageKey) {
imageToAppendKey.imageKey = imageKey
}

return imageToAppendKey
}

async create(image: Image): Promise<Image> {
this.items.push(image)

DomainEvents.dispatchEventsForAggregate(image.id)

return image
}
}

0 comments on commit 3e49ea5

Please sign in to comment.