Nestjs最佳实践教程:5自动验证,序列化与异常处理

智数探霄者
• 阅读 1371
有问题请扫描视频中qq群二维码交流
另,本人在找工作中,希望能有远程工作匹配(无法去外地),有需要的老板可以看一下我的个人介绍:pincman.com/about

学习目标

  • 全局自动数据验证管道
  • 全局数据序列化拦截器
  • 全局异常处理过滤器

文件结构

本节内容主要聚焦于CoreModule

src/core
├── constants.ts
├── core.module.ts
├── decorators
│   ├── dto-validation.decorator.ts
│   └── index.ts
├── helpers.ts
├── index.ts
├── providers
│   ├── app.filter.ts
│   ├── app.interceptor.ts
│   ├── app.pipe.ts
│   └── index.ts
└── types.ts

应用编码

本节中用到一个新的Typescript知识点-自定义装饰器和matedata,详细使用请查看我写的一篇相关文章

装饰器

添加一个用于为Dto构造metadata数据的装饰器

// src/core/decorators/dto-validation.decorator.ts
export const DtoValidation = (
    options?: ValidatorOptions & {
        transformOptions?: ClassTransformOptions;
    } & { type?: Paramtype },
) => SetMetadata(DTO_VALIDATION_OPTIONS, options ?? {});

验证管道

自定义一个全局的验证管道(继承自Nestjs自带的ValidationPipe管道)

代码: src/core/providers/app.pipe.ts

大致验证流程如下

  1. 获取要验证的dto类
  2. 获取Dto自定义的matadata数据(通过上面的装饰器定义)
  3. 合并默认验证选项(通过在CoreModule注册管道时定义)与matadata
  4. 根据DTO类上设置的type来设置当前的DTO请求类型('body' | 'query' | 'param' | 'custom')
  5. 如果被验证的DTO设置的请求类型与被验证的数据的请求类型不是同一种类型则跳过此管道
  6. 合并当前transform选项和自定义选项(验证后的数据使用class-transfomer`序列化)
  7. 如果dto类的中存在transform静态方法,则返回调用进一步transform之后的结果
  8. 重置验证选项和transform选项为默认

序列化拦截器

默认的序列化拦截器是无法对分页数据进行处理的,所以自定义的全局序列化拦截器类重写serialize方法,以便对分页数据进行拦截并序列化

// src/core/providers/app.interceptor.ts
serialize(
        response: PlainLiteralObject | Array<PlainLiteralObject>,
        options: ClassTransformOptions,
    ): PlainLiteralObject | PlainLiteralObject[] {
        const isArray = Array.isArray(response);
        if (!isObject(response) && !isArray) return response;
        // 如果是响应数据是数组,则遍历对每一项进行序列化
        if (isArray) {
            return (response as PlainLiteralObject[]).map((item) =>
                this.transformToPlain(item, options),
            );
        }
        // 如果是分页数据,则对items中的每一项进行序列化
        if (
            'meta' in response &&
            'items' in response &&
            Array.isArray(response.items)
        ) {
            return {
                ...response,
                items: (response.items as PlainLiteralObject[]).map((item) =>
                    this.transformToPlain(item, options),
                ),
            };
        }
        // 如果响应是个对象则直接序列化
        return this.transformToPlain(response, options);
    }

异常处理过滤器

Typeorm在找不到模型数据时会抛出EntityNotFound的异常,而此异常不会被捕获进行处理,以至于直接抛出500错误,一般在数据找不到时我们需要抛出的是404异常,所以需要定义一个全局异常处理的过滤器来进行捕获并处理.

全局的异常处理过滤器继承自Nestjs自带的BaseExceptionFilter,在自定义的类中定义一个对象属性,并复写catch方法以根据此属性中不同的异常进行判断处理

// src/core/providers/app.filter.ts
protected resExceptions: Array<
        { class: Type<Error>; status?: number } | Type<Error>
    > = [{ class: EntityNotFoundError, status: HttpStatus.NOT_FOUND }];
catch(exception: T, host: ArgumentsHost) {...}

注册全局

CoreModule中分别为全局的验证管道,序列化拦截器和异常处理过滤器进行注册

在注册全局管道验证时传入默认参数
// src/core/core.module.ts
providers: [
        {
            provide: APP_PIPE,
            useFactory: () =>
                new AppPipe({
                    transform: true,
                    forbidUnknownValues: true,
                    validationError: { target: false },
                }),
        },
        {
            provide: APP_FILTER,
            useClass: AppFilter,
        },
        {
            provide: APP_INTERCEPTOR,
            useClass: AppIntercepter,
        },
    ],
})

逻辑代码

  • 对于验证器需要修改DtoController
  • 对于拦截器需要修改EntityController
  • 对于过滤器需要修改Service

自动序列化

PostEntity为例,比如在显示文章列表数据的时候为了减少数据量不需要显示body内容,而单独访问一篇文章的时候则需要,这时候可以添加添加一个序列化组post-detail,而为了确定每个模型的字段在读取数据时只显示我们需要的,所以在类前添加一个@Exclude装饰器

对于对象类型需要通过@Type装饰器的字段转义

示例

// src/modules/content/entities/post.entity.ts
    ...
    @Expose()
    @Type(() => Date)
    @CreateDateColumn({
        comment: '创建时间',
    })
    createdAt!: Date;
    @Expose()
    @Type(() => CategoryEntity)
    @ManyToMany((type) => CategoryEntity, (category) => category.posts, {
        cascade: true,
    })
    @JoinTable()
    categories!: CategoryEntity[];
    @Expose({ groups: ['post-detail'] })
    @Column({ comment: '文章内容', type: 'longtext' })
    body!: string;

然后可以在在控制器中针对有特殊配置的序列化添加@SerializeOptions装饰器,如序列化组

示例

// src/modules/content/controllers/post.controller.ts
    ...
    @Get(':post')
    @SerializeOptions({ groups: ['post-detail'] })
    async show(
        @Param('post', new ParseUUIDEntityPipe(PostEntity))
        post: string,
    ) {
        return this.postService.detail(post);
    }

自动验证

为了代码简洁,把所有针对同一模型的DTO类全部放入一个文件,于是有了以下2个dto文件

  • src/modules/content/dtos/category.dto.ts
  • src/modules/content/dtos/post.dto.ts

dto文件中需要传入自定义验证参数的类添加@DtoValidation装饰器,比如@DtoValidation({ groups: ['create'] })

注意的是默认的paramTypebody,所以对于query,需要额外加上type: 'query'

示例

// src/modules/content/dtos/category.dto.ts
@Injectable()
@DtoValidation({ type: 'query' })
export class QueryCategoryDto implements PaginateDto {
...
}

现在可以在控制器中删除所有的new ValidatePipe(...)代码了,因为全局验证管道会自行处理

自动处理异常

现在把服务中的findOne等查询全部改成findOneOrFail等,把抛出的NotFoundError这些异常去除就可以在typeorm抛出默认的EntityNotFound异常时就会响应404

示例

// src/modules/content/services/post.service.ts
    async findOne(id: string) {
        const query = await this.getItemQuery();
        const item = await query.where('post.id = :id', { id }).getOne();
        if (!item)
            throw new EntityNotFoundError(PostEntity, `Post ${id} not exists!`);
        return item;
    }
点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
4年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Stella981 Stella981
4年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
4年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Easter79 Easter79
4年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Python进阶者 Python进阶者
2年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
智数探霄者
智数探霄者
Lv1
日落君山云起,春到沅湘草木,远思渺难收。
文章
2
粉丝
0
获赞
0