feat:自定义全局拦截器、过滤器、验证管道
This commit is contained in:
parent
781962dce0
commit
f4b38483d6
3
.gitignore
vendored
3
.gitignore
vendored
@ -32,4 +32,5 @@ lerna-debug.log*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/extensions.json
|
||||
.vercel
|
||||
|
@ -3,12 +3,33 @@ import { Module } from '@nestjs/common';
|
||||
import { database } from '@/config';
|
||||
import { ContentModule } from '@/modules/content/content.module';
|
||||
import { CoreModule } from '@/modules/core/core.module';
|
||||
import { AppFilter, AppIntercepter, AppPipe } from '@/modules/core/providers';
|
||||
import { DatabaseModule } from '@/modules/database/database.module';
|
||||
import { WelcomeModule } from '@/modules/welcome/welcome.module';
|
||||
import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule.forRoot(database), ContentModule, WelcomeModule, CoreModule.forRoot()],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_PIPE,
|
||||
useValue: new AppPipe({
|
||||
transform: true,
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
forbidUnknownValues: true,
|
||||
validationError: { target: false },
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: AppIntercepter,
|
||||
},
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: AppFilter,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
@ -9,15 +9,11 @@ import {
|
||||
Post,
|
||||
Query,
|
||||
SerializeOptions,
|
||||
UseInterceptors,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { CreateCategoryDto, QueryCategoryDto, UpdateCategoryDto } from '@/modules/content/dtos';
|
||||
import { CategoryService } from '@/modules/content/services';
|
||||
import { AppIntercepter } from '@/modules/core/providers';
|
||||
|
||||
@UseInterceptors(AppIntercepter)
|
||||
@Controller('categories')
|
||||
export class CategoryController {
|
||||
constructor(protected service: CategoryService) {}
|
||||
@ -31,15 +27,7 @@ export class CategoryController {
|
||||
@Get()
|
||||
@SerializeOptions({ groups: ['category-list'] })
|
||||
async list(
|
||||
@Query(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
forbidUnknownValues: true,
|
||||
validationError: { target: false },
|
||||
}),
|
||||
)
|
||||
@Query()
|
||||
options: QueryCategoryDto,
|
||||
) {
|
||||
return this.service.paginate(options);
|
||||
@ -54,16 +42,7 @@ export class CategoryController {
|
||||
@Post()
|
||||
@SerializeOptions({ groups: ['category-detail'] })
|
||||
async create(
|
||||
@Body(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
forbidUnknownValues: true,
|
||||
validationError: { target: false },
|
||||
groups: ['create'],
|
||||
}),
|
||||
)
|
||||
@Body()
|
||||
data: CreateCategoryDto,
|
||||
) {
|
||||
return this.service.create(data);
|
||||
@ -72,16 +51,7 @@ export class CategoryController {
|
||||
@Patch()
|
||||
@SerializeOptions({ groups: ['category-detail'] })
|
||||
async update(
|
||||
@Body(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
forbidUnknownValues: true,
|
||||
validationError: { target: false },
|
||||
groups: ['update'],
|
||||
}),
|
||||
)
|
||||
@Body()
|
||||
data: UpdateCategoryDto,
|
||||
) {
|
||||
return this.service.update(data);
|
||||
|
@ -8,15 +8,11 @@ import {
|
||||
Post,
|
||||
Query,
|
||||
SerializeOptions,
|
||||
UseInterceptors,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { CreateCommentDto, QueryCommentDto, QueryCommentTreeDto } from '@/modules/content/dtos';
|
||||
import { CommentService } from '@/modules/content/services';
|
||||
import { AppIntercepter } from '@/modules/core/providers';
|
||||
|
||||
@UseInterceptors(AppIntercepter)
|
||||
@Controller('comments')
|
||||
export class CommentController {
|
||||
constructor(protected service: CommentService) {}
|
||||
@ -24,15 +20,7 @@ export class CommentController {
|
||||
@Get('tree')
|
||||
@SerializeOptions({ groups: ['comment-tree'] })
|
||||
async tree(
|
||||
@Query(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
forbidUnknownValues: true,
|
||||
validationError: { target: false },
|
||||
}),
|
||||
)
|
||||
@Query()
|
||||
query: QueryCommentTreeDto,
|
||||
) {
|
||||
return this.service.findTrees(query);
|
||||
@ -41,13 +29,7 @@ export class CommentController {
|
||||
@Get()
|
||||
@SerializeOptions({ groups: ['comment-list'] })
|
||||
async list(
|
||||
@Query(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
forbidUnknownValues: true,
|
||||
validationError: { target: false },
|
||||
}),
|
||||
)
|
||||
@Query()
|
||||
query: QueryCommentDto,
|
||||
) {
|
||||
return this.service.paginate(query);
|
||||
@ -56,15 +38,7 @@ export class CommentController {
|
||||
@Post()
|
||||
@SerializeOptions({ groups: ['comment-detail'] })
|
||||
async store(
|
||||
@Body(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
forbidUnknownValues: true,
|
||||
validationError: { target: false },
|
||||
}),
|
||||
)
|
||||
@Body()
|
||||
data: CreateCommentDto,
|
||||
) {
|
||||
return this.service.create(data);
|
||||
|
@ -9,19 +9,16 @@ import {
|
||||
Post,
|
||||
Query,
|
||||
SerializeOptions,
|
||||
UseInterceptors,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dtos';
|
||||
import { PostService } from '@/modules/content/services';
|
||||
import { AppIntercepter } from '@/modules/core/providers';
|
||||
|
||||
/**
|
||||
* 文章控制器
|
||||
* 负责处理与文章相关的请求,如获取文章列表、创建新文章等。
|
||||
*/
|
||||
@UseInterceptors(AppIntercepter)
|
||||
|
||||
@Controller('posts')
|
||||
export class PostController {
|
||||
constructor(private postService: PostService) {}
|
||||
@ -29,15 +26,7 @@ export class PostController {
|
||||
@Get()
|
||||
@SerializeOptions({ groups: ['post-list'] })
|
||||
async list(
|
||||
@Query(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
forbidUnknownValues: true,
|
||||
validationError: { target: false },
|
||||
}),
|
||||
)
|
||||
@Query()
|
||||
options: QueryPostDto,
|
||||
) {
|
||||
return this.postService.paginate(options);
|
||||
@ -52,15 +41,7 @@ export class PostController {
|
||||
@Post()
|
||||
@SerializeOptions({ groups: ['post-detail'] })
|
||||
async store(
|
||||
@Body(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
forbidNonWhitelisted: true,
|
||||
forbidUnknownValues: true,
|
||||
validationError: { target: false },
|
||||
groups: ['create'],
|
||||
}),
|
||||
)
|
||||
@Body()
|
||||
data: CreatePostDto,
|
||||
) {
|
||||
return this.postService.create(data);
|
||||
@ -69,15 +50,7 @@ export class PostController {
|
||||
@Patch()
|
||||
@SerializeOptions({ groups: ['post-detail'] })
|
||||
async update(
|
||||
@Body(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
forbidNonWhitelisted: true,
|
||||
forbidUnknownValues: true,
|
||||
validationError: { target: false },
|
||||
groups: ['update'],
|
||||
}),
|
||||
)
|
||||
@Body()
|
||||
data: UpdatePostDto,
|
||||
) {
|
||||
return this.postService.update(data);
|
||||
|
@ -9,15 +9,11 @@ import {
|
||||
Post,
|
||||
Query,
|
||||
SerializeOptions,
|
||||
UseInterceptors,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { CreateTagDto, QueryCategoryDto, UpdateTagDto } from '@/modules/content/dtos';
|
||||
import { TagService } from '@/modules/content/services';
|
||||
import { AppIntercepter } from '@/modules/core/providers';
|
||||
|
||||
@UseInterceptors(AppIntercepter)
|
||||
@Controller('tags')
|
||||
export class TagController {
|
||||
constructor(protected service: TagService) {}
|
||||
@ -25,15 +21,7 @@ export class TagController {
|
||||
@Get()
|
||||
@SerializeOptions({})
|
||||
async list(
|
||||
@Query(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
forbidUnknownValues: true,
|
||||
validationError: { target: false },
|
||||
}),
|
||||
)
|
||||
@Query()
|
||||
options: QueryCategoryDto,
|
||||
) {
|
||||
return this.service.paginate(options);
|
||||
@ -51,16 +39,7 @@ export class TagController {
|
||||
@Post()
|
||||
@SerializeOptions({})
|
||||
async store(
|
||||
@Body(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
forbidUnknownValues: true,
|
||||
validationError: { target: false },
|
||||
groups: ['create'],
|
||||
}),
|
||||
)
|
||||
@Body()
|
||||
data: CreateTagDto,
|
||||
) {
|
||||
return this.service.create(data);
|
||||
@ -69,16 +48,7 @@ export class TagController {
|
||||
@Patch()
|
||||
@SerializeOptions({})
|
||||
async update(
|
||||
@Body(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
forbidUnknownValues: true,
|
||||
validationError: { target: false },
|
||||
groups: ['update'],
|
||||
}),
|
||||
)
|
||||
@Body()
|
||||
data: UpdateTagDto,
|
||||
) {
|
||||
return this.service.update(data);
|
||||
|
@ -12,8 +12,10 @@ import {
|
||||
} from 'class-validator';
|
||||
import { toNumber } from 'lodash';
|
||||
|
||||
import { DtoValidation } from '@/modules/core/decorators';
|
||||
import { PaginateOptions } from '@/modules/database/types';
|
||||
|
||||
@DtoValidation({ type: 'query' })
|
||||
export class QueryCategoryDto implements PaginateOptions {
|
||||
@Transform(({ value }) => toNumber(value))
|
||||
@Min(1, { message: '当前页数必须大于1' })
|
||||
@ -31,6 +33,7 @@ export class QueryCategoryDto implements PaginateOptions {
|
||||
/**
|
||||
* 分类新增验证
|
||||
*/
|
||||
@DtoValidation({ groups: ['create'] })
|
||||
export class CreateCategoryDto {
|
||||
@MaxLength(25, {
|
||||
always: true,
|
||||
@ -56,6 +59,7 @@ export class CreateCategoryDto {
|
||||
/**
|
||||
* 分类更新验证
|
||||
*/
|
||||
@DtoValidation({ groups: ['update'] })
|
||||
export class UpdateCategoryDto extends PartialType(CreateCategoryDto) {
|
||||
@IsUUID(undefined, { groups: ['update'], message: '分类ID格式不正确' })
|
||||
@IsDefined({ groups: ['update'], message: '分类ID必须指定' })
|
||||
|
@ -18,12 +18,14 @@ import {
|
||||
import { isNil, toNumber } from 'lodash';
|
||||
|
||||
import { PostOrderType } from '@/modules/content/constants';
|
||||
import { DtoValidation } from '@/modules/core/decorators';
|
||||
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()
|
||||
@ -59,6 +61,7 @@ export class QueryPostDto implements PaginateOptions {
|
||||
/**
|
||||
* 文章创建验证
|
||||
*/
|
||||
@DtoValidation({ groups: ['create'] })
|
||||
export class CreatePostDto {
|
||||
@MaxLength(255, {
|
||||
always: true,
|
||||
@ -119,6 +122,7 @@ export class CreatePostDto {
|
||||
/**
|
||||
* 文章更新验证
|
||||
*/
|
||||
@DtoValidation({ groups: ['update'] })
|
||||
export class UpdatePostDto extends PartialType(CreatePostDto) {
|
||||
@IsUUID(undefined, { groups: ['update'], message: '文章ID格式错误' })
|
||||
@IsDefined({ groups: ['update'], message: '文章ID必须指定' })
|
||||
|
@ -11,11 +11,13 @@ import {
|
||||
} from 'class-validator';
|
||||
import { toNumber } from 'lodash';
|
||||
|
||||
import { DtoValidation } from '@/modules/core/decorators';
|
||||
import { PaginateOptions } from '@/modules/database/types';
|
||||
|
||||
/**
|
||||
* 标签分页查询验证
|
||||
*/
|
||||
@DtoValidation({ type: 'query' })
|
||||
export class QueryTagsDto implements PaginateOptions {
|
||||
@Transform(({ value }) => toNumber(value))
|
||||
@Min(1, { message: '当前页数必须大于1' })
|
||||
@ -33,6 +35,7 @@ export class QueryTagsDto implements PaginateOptions {
|
||||
/**
|
||||
* 标签新增验证
|
||||
*/
|
||||
@DtoValidation({ groups: ['create'] })
|
||||
export class CreateTagDto {
|
||||
@MaxLength(25, {
|
||||
always: true,
|
||||
@ -53,6 +56,7 @@ export class CreateTagDto {
|
||||
/**
|
||||
* 标签更新验证
|
||||
*/
|
||||
@DtoValidation({ groups: ['update'] })
|
||||
export class UpdateTagDto extends PartialType(CreateTagDto) {
|
||||
@IsUUID(undefined, { groups: ['update'], message: '标签ID格式不正确' })
|
||||
@IsDefined({ groups: ['update'], message: '标签ID必须指定' })
|
||||
|
4
src/modules/core/constants.ts
Normal file
4
src/modules/core/constants.ts
Normal file
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* DTOValidation 装饰器选项
|
||||
*/
|
||||
export const DTO_VALIDATION_OPTIONS = 'dto_validation_options';
|
14
src/modules/core/decorators/dto-validation.decorator.ts
Normal file
14
src/modules/core/decorators/dto-validation.decorator.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { DTO_VALIDATION_OPTIONS } from '@/modules/core/constants';
|
||||
import { Paramtype, SetMetadata } from '@nestjs/common';
|
||||
import { ClassTransformOptions } from 'class-transformer';
|
||||
import { ValidatorOptions } from 'class-validator';
|
||||
|
||||
/**
|
||||
* 用于配置通过全局验证管道验证数据的DTO类装饰器
|
||||
* @params options
|
||||
*/
|
||||
export const DtoValidation = (
|
||||
options?: ValidatorOptions & { transformOptions?: ClassTransformOptions } & {
|
||||
type?: Paramtype;
|
||||
},
|
||||
) => SetMetadata(DTO_VALIDATION_OPTIONS, options ?? {});
|
1
src/modules/core/decorators/index.ts
Normal file
1
src/modules/core/decorators/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './dto-validation.decorator';
|
52
src/modules/core/providers/app.filter.ts
Normal file
52
src/modules/core/providers/app.filter.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { ArgumentsHost, Catch, HttpException, HttpStatus, Type } from '@nestjs/common';
|
||||
import { BaseExceptionFilter } from '@nestjs/core';
|
||||
import { isObject } from 'lodash';
|
||||
import { EntityNotFoundError, EntityPropertyNotFoundError, QueryFailedError } from 'typeorm';
|
||||
|
||||
/**
|
||||
* 全局过滤器,用于响应自定义异常
|
||||
*/
|
||||
@Catch()
|
||||
export class AppFilter<T = Error> extends BaseExceptionFilter<T> {
|
||||
protected resExceptions: Array<{ class: Type<Error>; status?: number } | Type<Error>> = [
|
||||
{ class: EntityNotFoundError, status: HttpStatus.NOT_FOUND },
|
||||
{ class: QueryFailedError, status: HttpStatus.BAD_REQUEST },
|
||||
{ class: EntityPropertyNotFoundError, status: HttpStatus.BAD_REQUEST },
|
||||
];
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
catch(exception: T, host: ArgumentsHost) {
|
||||
const applicationRef =
|
||||
this.applicationRef || (this.httpAdapterHost && this.httpAdapterHost.httpAdapter)!;
|
||||
// 是否在自定义的异常处理类列表中
|
||||
const resException = this.resExceptions.find((item) =>
|
||||
'class' in item ? exception instanceof item.class : exception instanceof item,
|
||||
);
|
||||
|
||||
// 如果不在自定义异常处理类列表也没有继承自HttpException
|
||||
if (!resException && !(exception instanceof HttpException)) {
|
||||
return this.handleUnknownError(exception, host, applicationRef);
|
||||
}
|
||||
let res: string | object = '';
|
||||
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
if (exception instanceof HttpException) {
|
||||
res = exception.getResponse();
|
||||
status = exception.getStatus();
|
||||
} else if (resException) {
|
||||
// 如果在自定义异常处理类列表中
|
||||
const e = exception as unknown as Error;
|
||||
res = e.message;
|
||||
if ('class' in resException && resException.status) {
|
||||
status = resException.status;
|
||||
}
|
||||
}
|
||||
const message = isObject(res)
|
||||
? res
|
||||
: {
|
||||
statusCode: status,
|
||||
message: res,
|
||||
};
|
||||
|
||||
applicationRef!.reply(host.getArgByIndex(1), message, status);
|
||||
}
|
||||
}
|
82
src/modules/core/providers/app.pipe.ts
Normal file
82
src/modules/core/providers/app.pipe.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { DTO_VALIDATION_OPTIONS } from '@/modules/core/constants';
|
||||
import { deepMerge } from '@/modules/core/helpers';
|
||||
import {
|
||||
ArgumentMetadata,
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
Paramtype,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { isObject, omit } from 'lodash';
|
||||
|
||||
/**
|
||||
* 全局管道,用于处理DTO验证
|
||||
*/
|
||||
@Injectable()
|
||||
export class AppPipe extends ValidationPipe {
|
||||
async transform(value: any, metadata: ArgumentMetadata) {
|
||||
const { metatype, type } = metadata;
|
||||
// 获取要验证的dto类
|
||||
const dto = metatype as any;
|
||||
// 获取dto类的装饰器元数据中的自定义验证选项
|
||||
const options = Reflect.getMetadata(DTO_VALIDATION_OPTIONS, dto) || {};
|
||||
// 把当前已设置的选项解构到备份对象
|
||||
const originOptions = { ...this.validatorOptions };
|
||||
// 把当前已设置的class-transform选项解构到备份对象
|
||||
const originTransform = { ...this.transformOptions };
|
||||
// 把自定义的class-transform和type选项解构
|
||||
const { transformOptions, type: optionsType, ...customOptions } = options;
|
||||
// 根据DTO类上设置的type来设置当前的DTO请求类型,默认为'body'
|
||||
const requestType: Paramtype = optionsType ?? 'body';
|
||||
|
||||
// 如果被验证的DTO设置的请求类型与被验证的数据的请求类型不是同一种类型 跳过此管道
|
||||
if (requestType !== type) return value;
|
||||
|
||||
// 合并当前transform选项和自定义选项
|
||||
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, v]) => {
|
||||
if (!isObject(v) || !('mimetype' in v)) return [key, v];
|
||||
return [key, omit(v, ['fields'])];
|
||||
}),
|
||||
)
|
||||
: value;
|
||||
|
||||
try {
|
||||
// 序列化并验证dto对象
|
||||
let result = await super.transform(toValidate, metadata);
|
||||
|
||||
// 如果dto类中存在transform静态方法,则返回调用进一步transform之后的结果
|
||||
if (typeof result.transform === 'function') {
|
||||
result === (await result.transform(result));
|
||||
const { transform, ...data } = result;
|
||||
result = data;
|
||||
}
|
||||
|
||||
// 重置验证选项
|
||||
this.validatorOptions = originOptions;
|
||||
// 重置transform选项
|
||||
this.transformOptions = originTransform;
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
// 重置验证选项
|
||||
this.validatorOptions = originOptions;
|
||||
// 重置transform选项
|
||||
this.transformOptions = originTransform;
|
||||
|
||||
if ('response' in error) throw new BadRequestException(error.response);
|
||||
throw new BadRequestException(error);
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +1,3 @@
|
||||
export * from './app.filter';
|
||||
export * from './app.interceptor';
|
||||
export * from './app.pipe';
|
||||
|
Loading…
Reference in New Issue
Block a user