diff --git a/.gitignore b/.gitignore index 78cff21..7f2ffd5 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ pnpm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* +yarn.lock # OS .DS_Store diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 32f6885..c145361 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,7 +14,7 @@ model videos { datetime_download DateTime? @db.Timestamp(6) datetime_convert DateTime? @db.Timestamp(6) url String @db.VarChar(244) - situation EVideoSituation + situation video_situation error_message String? @db.VarChar(244) clips_quantity Int? times Json? @@ -25,7 +25,7 @@ model videos { datetime_posted DateTime? @db.Timestamp(6) } -enum EVideoSituation { +enum video_situation { FILA PROCESSANDO ERRO diff --git a/src/main.ts b/src/main.ts index f76bc8d..b629495 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,13 @@ -import { NestFactory } from '@nestjs/core'; +import { ClassSerializerInterceptor } from '@nestjs/common'; +import { NestFactory, Reflector } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); + const reflector = app.get(Reflector); + + app.useGlobalInterceptors(new ClassSerializerInterceptor(reflector)); + await app.listen(process.env.PORT ?? 3000); } -bootstrap(); +void bootstrap(); diff --git a/src/videos/dto/create-video-dto.ts b/src/videos/dto/create-video-dto.ts index d506586..eb453fa 100644 --- a/src/videos/dto/create-video-dto.ts +++ b/src/videos/dto/create-video-dto.ts @@ -7,7 +7,7 @@ import { IsUrl, MaxLength, } from 'class-validator'; -import { EVideoSituation } from '../../../generated/prisma'; +import { video_situation } from '../../../generated/prisma'; export class CreateVideoDto { @IsUrl() @@ -15,8 +15,8 @@ export class CreateVideoDto { url!: string; @IsOptional() - @IsEnum(EVideoSituation) - situation?: EVideoSituation; + @IsEnum(video_situation) + situation?: video_situation; @IsOptional() @IsString() diff --git a/src/videos/dto/paginated.dto.ts b/src/videos/dto/paginated.dto.ts index f1c97ec..c6bfa4f 100644 --- a/src/videos/dto/paginated.dto.ts +++ b/src/videos/dto/paginated.dto.ts @@ -1,5 +1,5 @@ import { Type } from 'class-transformer'; -import { IsInt, IsOptional, Max, Min } from 'class-validator'; +import { IsIn, IsInt, IsOptional, IsString, Max, Min } from 'class-validator'; export class PaginatedQueryDto { @IsOptional() @@ -14,12 +14,19 @@ export class PaginatedQueryDto { @Min(1) @Max(100) perPage: number = 20; + + @IsOptional() + @Type(() => String) + @IsString() + @IsIn(['asc', 'desc']) + direction: string = 'desc'; } export type PaginatedResponse = { content: T[]; pagination: { page: number; + direction: string; perPage: number; total: number; totalPages: number; diff --git a/src/videos/dto/video-response.dto.ts b/src/videos/dto/video-response.dto.ts index 05ed8a0..13dbe02 100644 --- a/src/videos/dto/video-response.dto.ts +++ b/src/videos/dto/video-response.dto.ts @@ -1,12 +1,41 @@ -import { EVideoSituation } from 'generated/prisma'; +import { Expose, Transform, TransformFnParams } from 'class-transformer'; +import dayjs from 'dayjs'; +import { video_situation } from 'generated/prisma'; -export interface VideoResponseDto { - id: number; - title: string | null; - url: string; - situation: EVideoSituation; - clips_quantity: number; - videoid: string; - filename: string; - datetime_download: Date | string; +export class VideoResponseDto { + @Expose() + id!: number; + + @Expose() + title!: string | null; + + @Expose() + url!: string; + + @Expose() + situation!: video_situation; + + @Expose() + @Transform(({ value }: TransformFnParams) => + typeof value === 'number' ? value : 0, + ) + clips_quantity!: number; + + @Expose() + @Transform(({ value }: TransformFnParams) => + typeof value === 'string' ? value : '', + ) + videoid!: string; + + @Expose() + @Transform(({ value }: TransformFnParams) => + typeof value === 'string' ? value : '', + ) + filename!: string; + + @Expose() + @Transform(({ value }: TransformFnParams) => + value ? dayjs(value as Date | string).format('DD/MM/YYYY HH:mm:ss') : '', + ) + datetime_download!: string; } diff --git a/src/videos/videos.controller.ts b/src/videos/videos.controller.ts index 876aa0e..9d8c715 100644 --- a/src/videos/videos.controller.ts +++ b/src/videos/videos.controller.ts @@ -7,11 +7,12 @@ import { Body, Query, } from '@nestjs/common'; -import { videos, Prisma, EVideoSituation } from 'generated/prisma'; +import { videos, Prisma, video_situation } from 'generated/prisma'; import { VideosService } from './videos.service'; import { VideoResponseDto } from './dto/video-response.dto'; import { PaginatedQueryDto, PaginatedResponse } from './dto/paginated.dto'; +import { EBooleanPipe } from './videos.pipe'; @Controller('videos') export class VideosController { @@ -20,13 +21,21 @@ export class VideosController { @Get() async list( @Query() query: PaginatedQueryDto, - @Query('situation') situation?: EVideoSituation, - ): Promise> { - return this.videosService.listPaginated( - query.page, - query.perPage, - situation, - ); + @Query('situation') situation?: video_situation, + @Query('pageable', new EBooleanPipe(true)) pageable: boolean = true, + ): Promise | VideoResponseDto[]> { + const situacao = situation?.toLocaleUpperCase() as video_situation; + + if (pageable || query.page || query.perPage) { + return this.videosService.listPaginated( + Number(query.page ?? 1), + Number(query.perPage ?? 10), + query.direction as 'asc' | 'desc', + situacao, + ); + } + + return this.videosService.list(situacao); } @Get(':id') @@ -46,9 +55,4 @@ export class VideosController { async delete(@Param('id') id: string): Promise { return this.videosService.delete(Number(id)); } - - @Get('situation/:s') - async listBySituation(@Param('s') s: EVideoSituation): Promise { - return this.videosService.listBySituation(s); - } } diff --git a/src/videos/videos.pipe.ts b/src/videos/videos.pipe.ts new file mode 100644 index 0000000..1bdf49a --- /dev/null +++ b/src/videos/videos.pipe.ts @@ -0,0 +1,22 @@ +import { Injectable, PipeTransform, BadRequestException } from '@nestjs/common'; + +@Injectable() +export class EBooleanPipe + implements PipeTransform +{ + constructor(private readonly defaultValue?: boolean) {} + + transform(value: string | undefined): boolean { + if (value === undefined || value === null || value === '') { + if (this.defaultValue !== undefined) return this.defaultValue; + + throw new BadRequestException('Parâmetro esperado: "V" ou "F"'); + } + + const normalized = value.toString().trim().toUpperCase(); + if (normalized === 'V') return true; + if (normalized === 'F') return false; + + throw new BadRequestException('Valor inválido. Use "V" ou "F"'); + } +} diff --git a/src/videos/videos.service.ts b/src/videos/videos.service.ts index 8a9fca3..e3ee61a 100644 --- a/src/videos/videos.service.ts +++ b/src/videos/videos.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; -import dayjs from 'dayjs'; +import { plainToInstance } from 'class-transformer'; -import { Prisma, videos, EVideoSituation } from 'generated/prisma'; +import { Prisma, videos, video_situation } from 'generated/prisma'; import { PrismaService } from '../prisma/prisma.service'; import { VideoResponseDto } from './dto/video-response.dto'; @@ -11,37 +11,42 @@ import { PaginatedResponse } from './dto/paginated.dto'; export class VideosService { constructor(private readonly prisma: PrismaService) {} - async list(): Promise { - const data = await this.prisma.videos.findMany({ orderBy: { id: 'desc' } }); + async list(situation?: video_situation): Promise { + const data = await this.prisma.videos.findMany({ + where: situation ? { situation } : {}, + orderBy: { id: 'desc' }, + select: { + id: true, + title: true, + url: true, + situation: true, + clips_quantity: true, + videoid: true, + filename: true, + datetime_download: true, + }, + }); - return data.map((video) => ({ - id: video.id, - title: video.title, - url: video.url, - situation: video.situation, - clips_quantity: video.clips_quantity ?? 0, - videoid: video.videoid!, - filename: video.filename || '', - datetime_download: video.datetime_download - ? dayjs(video.datetime_download).format('DD/MM/YYYY HH:mm:ss') - : '', - })); + return plainToInstance(VideoResponseDto, data, { + excludeExtraneousValues: true, + }); } async listPaginated( page: number, perPage: number, - situation?: EVideoSituation, + direction: 'asc' | 'desc' = 'desc', + situation?: video_situation, ): Promise> { - const skip = (page - 1) * perPage; - const where = situation ? { situation } : undefined; + const skip = page >= 1 ? page * perPage : 0; + const where = situation ? { situation } : {}; const [rows, total] = await Promise.all([ this.prisma.videos.findMany({ where, - orderBy: { id: 'asc' }, + orderBy: { id: direction }, skip, - take: Number(perPage), + take: perPage ?? 1, select: { id: true, title: true, @@ -56,16 +61,11 @@ export class VideosService { this.prisma.videos.count({ where }), ]); - const content: VideoResponseDto[] = rows.map((row) => ({ - id: row.id, - title: row.title ?? null, - url: row.url, - situation: row.situation, - clips_quantity: row.clips_quantity ?? 0, - videoid: row.videoid ?? '', - filename: row.filename ?? '', - datetime_download: row.datetime_download ?? '', - })); + const content: VideoResponseDto[] = plainToInstance( + VideoResponseDto, + rows, + { excludeExtraneousValues: true }, + ); const totalPages = Math.max(1, Math.ceil(total / perPage)); @@ -73,6 +73,7 @@ export class VideosService { content, pagination: { page, + direction, perPage, total, totalPages, @@ -100,11 +101,4 @@ export class VideosService { where: { id }, }); } - - async listBySituation(situation: EVideoSituation): Promise { - return this.prisma.videos.findMany({ - where: { situation }, - orderBy: { id: 'desc' }, - }); - } }