Skip to content

Commit

Permalink
Refactor(Video, Image, Upload): Refactor upload routes
Browse files Browse the repository at this point in the history
  • Loading branch information
Artur-Poffo committed Feb 23, 2024
1 parent bee7a15 commit 092c5a7
Show file tree
Hide file tree
Showing 24 changed files with 238 additions and 27 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"cSpell.words": [
"accesstoken",
"authtoken",
"codespark",
"dtos",
"fastify",
"originalname"
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,11 @@

### Video
- [x] POST /videos - Upload video
- [ ] GET /videos/:videoId - Get video details

### Image
- [x] POST /images - Upload image
- [ ] GET /images/:imageId - Get image details

## Potential Refactoring or Updates:

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@
"vite-tsconfig-paths": "^4.3.1",
"vitest": "^1.2.1"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "files" ALTER COLUMN "key" DROP NOT NULL;
13 changes: 13 additions & 0 deletions prisma/migrations/20240222232146_create_image_model/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "images" (
"id" TEXT NOT NULL,
"file_key" TEXT,

CONSTRAINT "images_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "images_file_key_key" ON "images"("file_key");

-- AddForeignKey
ALTER TABLE "images" ADD CONSTRAINT "images_file_key_fkey" FOREIGN KEY ("file_key") REFERENCES "files"("key") ON DELETE SET NULL ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:
- Made the column `key` on table `files` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "files" ALTER COLUMN "key" SET NOT NULL;
13 changes: 12 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,9 @@ model File {
key String @unique()
size Decimal
storedAt DateTime @default(now()) @map("stored_at")
video Video?
video Video?
Image Image?
@@map("files")
}
Expand All @@ -194,3 +196,12 @@ model Video {
@@map("videos")
}

model Image {
id String @id @default(uuid())
file File? @relation(fields: [fileKey], references: [key])
fileKey String? @unique() @map("file_key")
@@map("images")
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { right, type Either } from '@/core/either'
import { left, right, type Either } from '@/core/either'
import { InvalidMimeTypeError } from '@/core/errors/errors/invalid-mime-type-error'
import { type UseCase } from '@/core/use-cases/use-case'
import { Image } from '../../enterprise/entities/image'
import { type ImagesRepository } from './../repositories/images-repository'
Expand All @@ -11,7 +12,7 @@ interface UploadImageUseCaseRequest {
}

type UploadImageUseCaseResponse = Either<
null,
InvalidMimeTypeError,
{
image: Image
}
Expand All @@ -28,6 +29,10 @@ export class UploadImageUseCase implements UseCase<UploadImageUseCaseRequest, Up
body,
size
}: UploadImageUseCaseRequest): Promise<UploadImageUseCaseResponse> {
if (!/image\/(jpeg|png)/.test(imageType)) {
return left(new InvalidMimeTypeError(imageType))
}

const image = Image.create({
imageName,
imageType,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { right, type Either } from '@/core/either'
import { left, right, type Either } from '@/core/either'
import { InvalidMimeTypeError } from '@/core/errors/errors/invalid-mime-type-error'
import { type UseCase } from '@/core/use-cases/use-case'
import { Video } from '../../enterprise/entities/video'
import { type VideosRepository } from './../repositories/videos-repository'

interface UploadVideoUseCaseRequest {
videoName: string
videoType?: 'video/mp4' | 'video/avi'
videoType: 'video/mp4' | 'video/avi'
body: Buffer
duration: number
size: number
}

type UploadVideoUseCaseResponse = Either<
null,
InvalidMimeTypeError,
{
video: Video
}
Expand All @@ -30,6 +31,10 @@ export class UploadVideoUseCase implements UseCase<UploadVideoUseCaseRequest, Up
duration,
size
}: UploadVideoUseCaseRequest): Promise<UploadVideoUseCaseResponse> {
if (!/video\/(mp4|avi)/.test(videoType)) {
return left(new InvalidMimeTypeError(videoType))
}

const video = Video.create({
videoName,
videoType,
Expand Down
2 changes: 1 addition & 1 deletion src/domain/course-management/enterprise/entities/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface ImageProps {
imageType: 'image/jpeg' | 'image/png'
body: Buffer
size: number
imageKey?: string
imageKey?: string | null
storedAt: Date
}

Expand Down
2 changes: 1 addition & 1 deletion src/domain/storage/application/use-cases/upload-file.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { left, right, type Either } from '@/core/either'
import { InvalidMimeTypeError } from '@/core/errors/errors/invalid-mime-type-error'
import { type UseCase } from '@/core/use-cases/use-case'
import { File } from '../../enterprise/entities/file'
import { type FilesRepository } from '../repositories/files-repository'
import { type Uploader } from '../upload/uploader'
import { InvalidMimeTypeError } from './errors/invalid-mime-type-error'

interface UploadFileUseCaseRequest {
fileName: string
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { InMemoryImagesRepository } from '../../../../../../test/repositories/in-memory-images-repository'
import { PrismaImagesRepository } from '@/infra/database/prisma/repositories/prisma-images-repository'
import { PrismaStudentCertificatesRepository } from '../../repositories/prisma-student-certificates-repository'
import { CertificateMapper } from '../certificate-mapper'

export function makeCertificateMapper() {
const inMemoryImagesRepository = new InMemoryImagesRepository()
const prismaImagesRepository = new PrismaImagesRepository()
const prismaStudentCertificatesRepository = new PrismaStudentCertificatesRepository()

const certificateMapper = new CertificateMapper(
inMemoryImagesRepository,
prismaImagesRepository,
prismaStudentCertificatesRepository
)

Expand Down
34 changes: 34 additions & 0 deletions src/infra/database/prisma/mappers/image-mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { UniqueEntityID } from '@/core/entities/unique-entity-id'
import { Image } from '@/domain/course-management/enterprise/entities/image'
import { type Prisma } from '@prisma/client'

export class ImageMapper {
static toDomain(raw: Prisma.ImageGetPayload<{ include: { file: true } }>): Image | null {
if (!raw.file) {
return null
}

if (raw.file.type !== 'image/jpeg' && raw.file.type !== 'image/png') {
return null
}

return Image.create(
{
imageName: raw.file.name,
imageKey: raw.fileKey,
body: raw.file.body,
imageType: raw.file.type as 'image/jpeg' | 'image/png',
size: Number(raw.file.size),
storedAt: raw.file.storedAt
},
new UniqueEntityID(raw.id)
)
}

static toPrisma(image: Image): Prisma.ImageUncheckedCreateInput {
return {
id: image.id.toString(),
fileKey: image.imageKey
}
}
}
2 changes: 1 addition & 1 deletion src/infra/database/prisma/mappers/video-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class VideoMapper {
)
}

static toPrisma(video: Video): Prisma.VideoUncheckedCreateInput | null {
static toPrisma(video: Video): Prisma.VideoUncheckedCreateInput {
return {
id: video.id.toString(),
duration: video.duration,
Expand Down
83 changes: 83 additions & 0 deletions src/infra/database/prisma/repositories/prisma-images-repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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'
import { prisma } from '..'
import { ImageMapper } from '../mappers/image-mapper'

export class PrismaImagesRepository implements ImagesRepository {
async findById(id: string): Promise<Image | null> {
const image = await prisma.image.findUnique({
where: {
id
},
include: {
file: true
}
})

if (!image) {
return null
}

const domainImage = ImageMapper.toDomain(image)

return domainImage
}

async findByImageKey(key: string): Promise<Image | null> {
const image = await prisma.image.findUnique({
where: {
fileKey: key
},
include: {
file: true
}
})

if (!image) {
return null
}

const domainImage = ImageMapper.toDomain(image)

return domainImage
}

async appendImageKey(imageKey: string, imageId: string): Promise<Image | null> {
const image = await prisma.image.findUnique({
where: {
id: imageId
},
include: {
file: true
}
})

if (!image) {
return null
}

await prisma.image.update({
where: { id: imageId },
data: {
fileKey: imageKey
}
})

const domainImage = ImageMapper.toDomain(image)

return domainImage
}

async create(image: Image): Promise<Image> {
const infraImage = ImageMapper.toPrisma(image)

await prisma.image.create({
data: infraImage
})

DomainEvents.dispatchEventsForAggregate(image.id)

return image
}
}
6 changes: 3 additions & 3 deletions src/infra/events/factories/make-on-image-key-generated.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { InMemoryImagesRepository } from './../../../../test/repositories/in-memory-images-repository'
import { PrismaImagesRepository } from '@/infra/database/prisma/repositories/prisma-images-repository'
import { OnImageKeyGenerated } from './../../../domain/course-management/application/subscribers/on-image-key-generated'

export function makeOnImageKeyGenerated() {
const inMemoryImagesRepository = new InMemoryImagesRepository()
const prismaImagesRepository = new PrismaImagesRepository()

const onImageKeyGenerated = new OnImageKeyGenerated(
inMemoryImagesRepository
prismaImagesRepository
)

return onImageKeyGenerated
Expand Down
18 changes: 16 additions & 2 deletions src/infra/http/controllers/upload-image.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { InvalidMimeTypeError } from '@/core/errors/errors/invalid-mime-type-error'
import { makeUploadImageUseCase } from '@/infra/use-cases/factories/make-upload-image-use-case'
import { type FastifyReply, type FastifyRequest } from 'fastify'
import { ImagePresenter } from '../presenters/image-presenter'
import { ImageMapper } from './../../database/prisma/mappers/image-mapper'

interface MulterRequest extends FastifyRequest {
file?: {
Expand Down Expand Up @@ -27,8 +30,19 @@ export async function uploadImageController(request: MulterRequest, reply: Fasti
})

if (result.isLeft()) {
return await reply.status(500).send()
const error = result.value

switch (error.constructor) {
case InvalidMimeTypeError:
return await reply.status(415).send({ message: error.message })
default:
return await reply.status(500).send({ message: error.message })
}
}

return await reply.status(201).send()
const image = ImageMapper.toPrisma(result.value.image)

return await reply.status(201).send({
image: ImagePresenter.toHTTP(image)
})
}
18 changes: 16 additions & 2 deletions src/infra/http/controllers/upload-video.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { InvalidMimeTypeError } from '@/core/errors/errors/invalid-mime-type-error'
import { VideoMapper } from '@/infra/database/prisma/mappers/video-mapper'
import { GetVideoDuration } from '@/infra/storage/utils/get-video-duration'
import { makeUploadVideoUseCase } from '@/infra/use-cases/factories/make-upload-video-use-case'
import { type FastifyReply, type FastifyRequest } from 'fastify'
import { VideoPresenter } from '../presenters/video-presenter'

interface MulterRequest extends FastifyRequest {
file?: {
Expand Down Expand Up @@ -31,8 +34,19 @@ export async function uploadVideoController(request: MulterRequest, reply: Fasti
})

if (result.isLeft()) {
return await reply.status(500).send()
const error = result.value

switch (error.constructor) {
case InvalidMimeTypeError:
return await reply.status(415).send({ message: error.message })
default:
return await reply.status(500).send({ message: error.message })
}
}

return await reply.status(201).send()
const video = VideoMapper.toPrisma(result.value.video)

return await reply.status(201).send({
video: VideoPresenter.toHTTP(video)
})
}
10 changes: 10 additions & 0 deletions src/infra/http/presenters/image-presenter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { type Prisma } from '@prisma/client'

export class ImagePresenter {
static toHTTP(image: Prisma.ImageUncheckedCreateInput) {
return {
id: image.id,
imageKey: image.fileKey
}
}
}
Loading

0 comments on commit 092c5a7

Please sign in to comment.