From 778248b16f87b7eaec14aefcb586a7b8ec9cb3c5 Mon Sep 17 00:00:00 2001 From: liuyi Date: Fri, 23 May 2025 15:16:28 +0800 Subject: [PATCH] add app validation --- src/app.module.ts | 10 ++++ .../controllers/category.controller.ts | 18 +----- .../content/controllers/comment.controller.ts | 14 +---- .../content/controllers/post.controller.ts | 19 +----- .../content/controllers/tag.controller.ts | 8 +-- src/modules/content/dtos/category.dto.ts | 4 ++ src/modules/content/dtos/comment.dto.ts | 4 ++ src/modules/content/dtos/post.dto.ts | 4 ++ src/modules/content/dtos/tag.dto.ts | 4 ++ src/modules/core/contants.ts | 1 + .../decorator/dto.validation.decorator.ts | 11 ++++ src/modules/core/providers/app.pipe.ts | 59 +++++++++++++++++++ 12 files changed, 109 insertions(+), 47 deletions(-) create mode 100644 src/modules/core/contants.ts create mode 100644 src/modules/core/decorator/dto.validation.decorator.ts create mode 100644 src/modules/core/providers/app.pipe.ts diff --git a/src/app.module.ts b/src/app.module.ts index 30b07a0..e30327f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,12 +1,22 @@ import { Module } from '@nestjs/common'; +import { APP_PIPE } from '@nestjs/core'; + import { database } from './config'; +import { DEFAULT_VALIDATION_CONFIG } from './modules/content/constants'; import { ContentModule } from './modules/content/content.module'; import { CoreModule } from './modules/core/core.module'; +import { AppPipe } from './modules/core/providers/app.pipe'; import { DatabaseModule } from './modules/database/database.module'; @Module({ imports: [ContentModule, CoreModule.forRoot(), DatabaseModule.forRoot(database)], + providers: [ + { + provide: APP_PIPE, + useValue: new AppPipe(DEFAULT_VALIDATION_CONFIG), + }, + ], }) export class AppModule {} diff --git a/src/modules/content/controllers/category.controller.ts b/src/modules/content/controllers/category.controller.ts index 6c0f3c5..8179d18 100644 --- a/src/modules/content/controllers/category.controller.ts +++ b/src/modules/content/controllers/category.controller.ts @@ -10,12 +10,10 @@ import { Query, SerializeOptions, UseInterceptors, - ValidationPipe, } from '@nestjs/common'; import { AppInterceptor } from '@/modules/core/providers/app.interceptor'; -import { DEFAULT_VALIDATION_CONFIG } from '../constants'; import { CreateCategoryDto, QueryCategoryDto, UpdateCategoryDto } from '../dtos/category.dto'; import { CategoryService } from '../services'; @@ -33,7 +31,7 @@ export class CategoryController { @Get() @SerializeOptions({ groups: ['category-list'] }) async list( - @Query(new ValidationPipe(DEFAULT_VALIDATION_CONFIG)) + @Query() options: QueryCategoryDto, ) { return this.service.paginate(options); @@ -48,12 +46,7 @@ export class CategoryController { @Post() @SerializeOptions({ groups: ['category-detail'] }) async store( - @Body( - new ValidationPipe({ - ...DEFAULT_VALIDATION_CONFIG, - groups: ['create'], - }), - ) + @Body() data: CreateCategoryDto, ) { return this.service.create(data); @@ -62,12 +55,7 @@ export class CategoryController { @Patch() @SerializeOptions({ groups: ['category-detail'] }) async update( - @Body( - new ValidationPipe({ - ...DEFAULT_VALIDATION_CONFIG, - groups: ['update'], - }), - ) + @Body() data: UpdateCategoryDto, ) { return this.service.update(data); diff --git a/src/modules/content/controllers/comment.controller.ts b/src/modules/content/controllers/comment.controller.ts index f96246e..2a75a0d 100644 --- a/src/modules/content/controllers/comment.controller.ts +++ b/src/modules/content/controllers/comment.controller.ts @@ -9,14 +9,10 @@ import { Query, SerializeOptions, UseInterceptors, - ValidationPipe, } from '@nestjs/common'; -import { pick } from 'lodash'; - import { AppInterceptor } from '@/modules/core/providers/app.interceptor'; -import { DEFAULT_VALIDATION_CONFIG } from '../constants'; import { CreateCommentDto, QueryCommentDto, QueryCommentTreeDto } from '../dtos/comment.dto'; import { CommentService } from '../services'; @@ -27,18 +23,14 @@ export class CommentController { @Get('tree') @SerializeOptions({ groups: ['comment-tree'] }) - async tree(@Query(new ValidationPipe(DEFAULT_VALIDATION_CONFIG)) options: QueryCommentTreeDto) { + async tree(@Query() options: QueryCommentTreeDto) { return this.service.findTrees(options); } @Get() @SerializeOptions({ groups: ['comment-list'] }) async list( - @Query( - new ValidationPipe({ - ...pick(DEFAULT_VALIDATION_CONFIG, ['forbidNonWhitelisted', 'whitelist']), - }), - ) + @Query() options: QueryCommentDto, ) { return this.service.paginate(options); @@ -46,7 +38,7 @@ export class CommentController { @Post() @SerializeOptions({ groups: ['comment-detail'] }) - async store(@Body(new ValidationPipe(DEFAULT_VALIDATION_CONFIG)) data: CreateCommentDto) { + async store(@Body() data: CreateCommentDto) { return this.service.create(data); } diff --git a/src/modules/content/controllers/post.controller.ts b/src/modules/content/controllers/post.controller.ts index a2cd49e..c1733e8 100644 --- a/src/modules/content/controllers/post.controller.ts +++ b/src/modules/content/controllers/post.controller.ts @@ -10,15 +10,12 @@ import { Query, SerializeOptions, UseInterceptors, - ValidationPipe, } from '@nestjs/common'; import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dtos/post.dto'; import { PostService } from '@/modules/content/services/post.service'; import { AppInterceptor } from '@/modules/core/providers/app.interceptor'; -import { DEFAULT_VALIDATION_CONFIG } from '../constants'; - @UseInterceptors(AppInterceptor) @Controller('posts') export class PostController { @@ -27,7 +24,7 @@ export class PostController { @Get() @SerializeOptions({ groups: ['post-list'] }) async list( - @Query(new ValidationPipe(DEFAULT_VALIDATION_CONFIG)) + @Query() options: QueryPostDto, ) { return this.postService.paginate(options); @@ -42,12 +39,7 @@ export class PostController { @Post() @SerializeOptions({ groups: ['post-detail'] }) async store( - @Body( - new ValidationPipe({ - ...DEFAULT_VALIDATION_CONFIG, - groups: ['create'], - }), - ) + @Body() data: CreatePostDto, ) { return this.postService.create(data); @@ -56,12 +48,7 @@ export class PostController { @Patch() @SerializeOptions({ groups: ['post-detail'] }) async update( - @Body( - new ValidationPipe({ - ...DEFAULT_VALIDATION_CONFIG, - groups: ['update'], - }), - ) + @Body() data: UpdatePostDto, ) { return this.postService.update(data); diff --git a/src/modules/content/controllers/tag.controller.ts b/src/modules/content/controllers/tag.controller.ts index 5280d2a..b4f6594 100644 --- a/src/modules/content/controllers/tag.controller.ts +++ b/src/modules/content/controllers/tag.controller.ts @@ -10,12 +10,10 @@ import { Query, SerializeOptions, UseInterceptors, - ValidationPipe, } from '@nestjs/common'; import { AppInterceptor } from '@/modules/core/providers/app.interceptor'; -import { DEFAULT_VALIDATION_CONFIG } from '../constants'; import { CreateTagDto, QueryTagDto, UpdateTagDto } from '../dtos/tag.dto'; import { TagService } from '../services'; @@ -27,7 +25,7 @@ export class TagController { @Get() @SerializeOptions({}) async list( - @Query(new ValidationPipe(DEFAULT_VALIDATION_CONFIG)) + @Query() options: QueryTagDto, ) { return this.service.paginate(options); @@ -42,7 +40,7 @@ export class TagController { @Post() @SerializeOptions({}) async store( - @Body(new ValidationPipe({ ...DEFAULT_VALIDATION_CONFIG, groups: ['create'] })) + @Body() data: CreateTagDto, ) { return this.service.create(data); @@ -51,7 +49,7 @@ export class TagController { @Patch() @SerializeOptions({}) async update( - @Body(new ValidationPipe({ ...DEFAULT_VALIDATION_CONFIG, groups: ['update'] })) + @Body() date: UpdateTagDto, ) { return this.service.update(date); diff --git a/src/modules/content/dtos/category.dto.ts b/src/modules/content/dtos/category.dto.ts index 6a7722f..c0f9997 100644 --- a/src/modules/content/dtos/category.dto.ts +++ b/src/modules/content/dtos/category.dto.ts @@ -12,8 +12,10 @@ import { } from 'class-validator'; import { toNumber } from 'lodash'; +import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator'; import { PaginateOptions } from '@/modules/database/types'; +@DtoValidation({ type: 'query' }) export class QueryCategoryDto implements PaginateOptions { @Transform(({ value }) => toNumber(value)) @Min(1, { message: 'The current page must be greater than 1.' }) @@ -28,6 +30,7 @@ export class QueryCategoryDto implements PaginateOptions { limit = 10; } +@DtoValidation({ groups: ['create'] }) export class CreateCategoryDto { @MaxLength(25, { always: true, @@ -53,6 +56,7 @@ export class CreateCategoryDto { customOrder?: number = 0; } +@DtoValidation({ groups: ['update'] }) export class UpdateCategoryDto extends PartialType(CreateCategoryDto) { @IsUUID(undefined, { message: 'The ID format is incorrect', groups: ['update'] }) @IsDefined({ groups: ['update'], message: 'The ID must be specified' }) diff --git a/src/modules/content/dtos/comment.dto.ts b/src/modules/content/dtos/comment.dto.ts index daeafa8..61c447f 100644 --- a/src/modules/content/dtos/comment.dto.ts +++ b/src/modules/content/dtos/comment.dto.ts @@ -12,8 +12,10 @@ import { } from 'class-validator'; import { toNumber } from 'lodash'; +import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator'; import { PaginateOptions } from '@/modules/database/types'; +@DtoValidation({ type: 'query' }) export class QueryCommentDto implements PaginateOptions { @Transform(({ value }) => toNumber(value)) @Min(1, { message: 'The current page must be greater than 1.' }) @@ -32,8 +34,10 @@ export class QueryCommentDto implements PaginateOptions { post?: string; } +@DtoValidation({ type: 'query' }) export class QueryCommentTreeDto extends PickType(QueryCommentDto, ['post']) {} +@DtoValidation() export class CreateCommentDto { @MaxLength(1000, { message: '' }) @IsNotEmpty({ message: '' }) diff --git a/src/modules/content/dtos/post.dto.ts b/src/modules/content/dtos/post.dto.ts index 66cb838..b91f8cd 100644 --- a/src/modules/content/dtos/post.dto.ts +++ b/src/modules/content/dtos/post.dto.ts @@ -17,9 +17,11 @@ import { import { isNil, toNumber } from 'lodash'; import { PostOrder } from '@/modules/content/constants'; +import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator'; import { toBoolean } from '@/modules/core/helpers'; import { PaginateOptions } from '@/modules/database/types'; +@DtoValidation({ type: 'query' }) export class QueryPostDto implements PaginateOptions { @Transform(({ value }) => toBoolean(value)) @IsBoolean() @@ -53,6 +55,7 @@ export class QueryPostDto implements PaginateOptions { tag?: string; } +@DtoValidation({ groups: ['create'] }) export class CreatePostDto { @MaxLength(255, { always: true, @@ -109,6 +112,7 @@ export class CreatePostDto { tags?: string[]; } +@DtoValidation({ groups: ['update'] }) export class UpdatePostDto extends PartialType(CreatePostDto) { @IsUUID(undefined, { groups: ['update'], diff --git a/src/modules/content/dtos/tag.dto.ts b/src/modules/content/dtos/tag.dto.ts index c1468f7..8ce1032 100644 --- a/src/modules/content/dtos/tag.dto.ts +++ b/src/modules/content/dtos/tag.dto.ts @@ -11,8 +11,10 @@ import { } from 'class-validator'; import { toNumber } from 'lodash'; +import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator'; import { PaginateOptions } from '@/modules/database/types'; +@DtoValidation({ type: 'query' }) export class QueryTagDto implements PaginateOptions { @Transform(({ value }) => toNumber(value)) @Min(1, { message: 'The current page must be greater than 1.' }) @@ -27,6 +29,7 @@ export class QueryTagDto implements PaginateOptions { limit = 10; } +@DtoValidation({ groups: ['create'] }) export class CreateTagDto { @MaxLength(255, { always: true, @@ -44,6 +47,7 @@ export class CreateTagDto { desc?: string; } +@DtoValidation({ groups: ['update'] }) export class UpdateTagDto extends PartialType(CreateTagDto) { @IsUUID(undefined, { message: 'The ID format is incorrect', groups: ['update'] }) @IsDefined({ groups: ['update'], message: 'The ID must be specified' }) diff --git a/src/modules/core/contants.ts b/src/modules/core/contants.ts new file mode 100644 index 0000000..eafa87d --- /dev/null +++ b/src/modules/core/contants.ts @@ -0,0 +1 @@ +export const DTO_VALIDATION_OPTIONS = 'dto_validation_options'; diff --git a/src/modules/core/decorator/dto.validation.decorator.ts b/src/modules/core/decorator/dto.validation.decorator.ts new file mode 100644 index 0000000..04e43ab --- /dev/null +++ b/src/modules/core/decorator/dto.validation.decorator.ts @@ -0,0 +1,11 @@ +import { Paramtype, SetMetadata } from '@nestjs/common'; +import { ClassTransformOptions } from 'class-transformer'; +import { ValidationOptions } from 'class-validator'; + +import { DTO_VALIDATION_OPTIONS } from '../contants'; + +export const DtoValidation = ( + options?: ValidationOptions & { transformOptions?: ClassTransformOptions } & { + type?: Paramtype; + }, +) => SetMetadata(DTO_VALIDATION_OPTIONS, options ?? {}); diff --git a/src/modules/core/providers/app.pipe.ts b/src/modules/core/providers/app.pipe.ts new file mode 100644 index 0000000..55ca16d --- /dev/null +++ b/src/modules/core/providers/app.pipe.ts @@ -0,0 +1,59 @@ +import { ArgumentMetadata, BadRequestException, Paramtype, ValidationPipe } from '@nestjs/common'; + +import { isObject, omit } from 'lodash'; + +import { DTO_VALIDATION_OPTIONS } from '../contants'; +import { deepMerge } from '../helpers'; + +export class AppPipe extends ValidationPipe { + async transform(value: any, metadata: ArgumentMetadata): Promise { + const { metatype, type } = metadata; + const dto = metatype as any; + const options = Reflect.getMetadata(DTO_VALIDATION_OPTIONS, dto) || {}; + const originOptions = { ...this.validatorOptions }; + const originTransform = { ...this.transformOptions }; + const { transformOptions, type: optionsType, ...customOptions } = options; + const requestBody: Paramtype = optionsType ?? 'body'; + if (requestBody !== type) { + return value; + } + if (transformOptions) { + this.transformOptions = deepMerge( + this.transformOptions, + transformOptions ?? {}, + 'replace', + ); + } + this.validatorOptions = deepMerge(this.validatorOptions, customOptions ?? {}, 'replace'); + const toValidate = isObject(value) + ? Object.fromEntries( + Object.entries(value as RecordAny).map(([key, val]) => { + if (isObject(val) && 'mimetype' in val) { + return [key, omit(val, ['fields'])]; + } + return [key, val]; + }), + ) + : value; + console.log(value); + console.log(toValidate); + try { + let result = await super.transform(toValidate, metadata); + if (typeof result.transform === 'function') { + result = await result.transform(result); + const { transform, ...data } = result; + result = data; + } + this.validatorOptions = originOptions; + this.transformOptions = originTransform; + return result; + } catch (error: any) { + this.validatorOptions = originOptions; + this.transformOptions = originTransform; + if ('response' in error) { + throw new BadRequestException(error.response); + } + throw new BadRequestException(error); + } + } +}