当前位置 博文首页 > 文章内容

    Nest.js参数校验和自定义返回数据格式详解

    作者:shunshunshun18 栏目:未分类 时间:2021-03-28 14:44:05

    本站于2023年9月4日。收到“大连君*****咨询有限公司”通知
    说我们IIS7站长博客,有一篇博文用了他们的图片。
    要求我们给他们一张图片6000元。要不然法院告我们

    为避免不必要的麻烦,IIS7站长博客,全站内容图片下架、并积极应诉
    博文内容全部不再显示,请需要相关资讯的站长朋友到必应搜索。谢谢!

    另祝:版权碰瓷诈骗团伙,早日弃暗投明。

    相关新闻:借版权之名、行诈骗之实,周某因犯诈骗罪被判处有期徒刑十一年六个月

    叹!百花齐放的时代,渐行渐远!



    0x0 参数校验

    参数校验大部分业务是使用 Nest.js 中的管道 方法实现,具体可以查阅文档 。不过编写过程中遇到一些问题,虽然文档讲得比较晦涩。

    在做个查询接口,里面包含一些参数,做成 dto 结构数据:

    import { ApiProperty } from '@nestjs/swagger'
    
    export class QueryUserDto {
     @ApiProperty({
     required: false,
     description: '页码'
     })
     readonly currentPage: number
    
     @ApiProperty({
     required: false,
     description: '条数'
     })
     readonly pageSize: number
    
     @ApiProperty({
     required: false,
     description: '用户账号'
     })
     readonly username?: string
    
     @ApiProperty({
     required: false,
     description: '用户状态'
     })
     readonly activeStatus: number
    
     @ApiProperty({
     required: false,
     description: '排序的方式: ASC, DESC'
     })
     readonly order: 'DESC' | 'ASC'
    }
     TYPESCRIPT
    
    

    在 @Query 请求传入对应的参数,发现得到的数据类型都是 String ,然后查阅相关文档才明白还需要 class-transformer 的 Type 进行转换:

    import { ApiProperty } from '@nestjs/swagger'
    import { Type } from 'class-transformer'
    
    export class QueryUserDto {
     @ApiProperty({
     required: false,
     description: '页码'
     })
     @Type(() => Number)
     readonly currentPage: number = 1
    
     @ApiProperty({
     required: false,
     description: '条数'
     })
     @Type(() => Number)
     readonly pageSize: number = 10
    
     @ApiProperty({
     required: false,
     description: '用户账号'
     })
     readonly username?: string
    
     @ApiProperty({
     required: false,
     description: '用户状态'
     })
     @Type(() => Number)
     readonly activeStatus: number = 3
    
     @ApiProperty({
     required: false,
     description: '排序的方式: ASC, DESC'
     })
     readonly order: 'DESC' | 'ASC' = 'DESC'
    }
    
    

    然后在 ValidationPipe 管道方法里开启 transform 选项:

    app.useGlobalPipes(
     new ValidationPipe({
     transform: true
     })
    )
    

    或者在 app.modules.ts 注入:

    import { ValidationPipe } from '@nestjs/common'
    import { APP_PIPE } from '@nestjs/core'
    
    @Module({
     imports: [
     // ...
     ],
     controllers: [AppController],
     providers: [
     {
      provide: APP_PIPE,
      useValue: new ValidationPipe({
      transform: true
      })
     }
     ]
    })
    
    

    俩者使用方法区别于程序的是否混合应用类型。

    我这边为了省事直接写在全局方法里,最终到 service 拿到的数据就是经过管道业务处理过的数据,不需要在 service 层进行大量的数据类型判断。

    0x1 自定义返回数据格式

    在 controller 返回的数据都是从数据库表结构而来:

    {
     "id": "d8d5a56c-ee9f-4e41-be48-5414a7a5712c",
     "username": "Akeem.Cremin",
     "password": "$2b$10$kRcsmN6ewFC2GOs0TEg6TuvDbNzf1VGCbQf2fI1UeyPAiZCq9rMKm",
     "email": "Garrett87@hotmail.com",
     "nickname": "Wallace Nicolas",
     "role": "user",
     "isActive": true,
     "createdTime": "2021-03-24T15:24:26.806Z",
     "updatedTime": "2021-03-24T15:24:26.806Z"
    }
    

    如果需要定义最终返回接口的数据格式例如:

    {
     "statusCode": 200,
     "message": "获取成功",
     "data": {
      "id": "d8d5a56c-ee9f-4e41-be48-5414a7a5712c",
      "username": "Akeem.Cremin",
      "password": "$2b$10$kRcsmN6ewFC2GOs0TEg6TuvDbNzf1VGCbQf2fI1UeyPAiZCq9rMKm",
      "email": "Garrett87@hotmail.com",
      "nickname": "Wallace Nicolas",
      "role": "user",
      "isActive": true,
      "createdTime": "2021-03-24T15:24:26.806Z",
      "updatedTime": "2021-03-24T15:24:26.806Z"
     }
    }
    

    这里就需要做个自定义成功请求拦截器:

    nest g in shared/interceptor/transform
    
    import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common'
    import { Observable } from 'rxjs'
    import { map } from 'rxjs/operators'
    import { Request } from 'express'
    
    interface Response<T> {
     data: T
    }
    
    @Injectable()
    export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
     intercept(context: ExecutionContext, next: CallHandler<T>): Observable<any> {
     const request = context.switchToHttp().getRequest<Request>()
     Logger.log(request.url, '正常接口请求')
    
     return next.handle().pipe(
      map(data => {
      return {
       data: data,
       statusCode: 200,
       message: '请求成功'
      }
      })
     )
     }
    }
    
    

    然后在 app.module.ts 引入即可使用:

    import { ValidationPipe } from '@nestjs/common'
    import { APP_INTERCEPTOR } from '@nestjs/core'
    
    import { TransformInterceptor } from '@/shared/interceptor/transform.interceptor'
    
    @Module({
     imports: [
     // ...
     ],
     controllers: [AppController],
     providers: [
     {
      provide: APP_INTERCEPTOR,
      useClass: TransformInterceptor
     }
     ]
    })
    
    

    不过 APP_INTERCEPTOR 排序要注意,TransformInterceptor 最好放在第一个,否则会失效。

    错误过滤器:

    nest g f shared/filters/httpException
    
    import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from '@nestjs/common'
    import { Response, Request } from 'express'
    
    @Catch(HttpException)
    export class HttpExceptionFilter implements ExceptionFilter {
     catch(exception: HttpException, host: ArgumentsHost) {
     const context = host.switchToHttp()
     const response = context.getResponse<Response>()
     const request = context.getRequest<Request>()
     const status = exception.getStatus()
     const message = exception.message
    
     Logger.log(`${request.url} - ${message}`, '非正常接口请求')
    
     response.status(status).json({
      statusCode: status,
      message: message,
      path: request.url,
      timestamp: new Date().toISOString()
     })
     }
    }
    
    

    然后在 app.module.ts 引入即可使用:

    import { ValidationPipe } from '@nestjs/common'
    import { APP_FILTER } from '@nestjs/core'
    
    import { HttpExceptionFilter } from '@/shared/filters/http-exception.filter'
    
    @Module({
     imports: [
     // ...
     ],
     controllers: [AppController],
     providers: [
     {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter
     }
     ]
    })
    
    

    0x2 隐藏实体类中的某个字段

    本来想使用 @Exclude 属性来隐藏数据库中一些敏感的字段,但发现无法满足特殊的需求,如果是返回单条实例可以实现隐藏,但是我有个 findAll 就无法实现了,上面在 Serialization | NestJS - A progressive Node.js framework 文档里说的非常详细,不过这里还有个办法。首先在实力类敏感数据字段上添加属性:

    import { BaseEntity, Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
    
    @Entity('user')
    export class UserEntity extends BaseEntity {
     @PrimaryGeneratedColumn('uuid', {
      comment: '用户编号'
     })
     id: string
    
     @Column({
      type: 'varchar',
      length: 50,
      unique: true,
      comment: '登录用户'
     })
     username: string
    
     @Column({
      type: 'varchar',
      length: 200,
      select: false,
      comment: '密码'
     })
     password: string
    
    

    select: false 可以在返回查询结果隐藏这个字段,但所有涉及到这个字段查询必须添加这个字段,比如我在 user.service.ts 登录查询中:

    const user = await getRepository(UserEntity)
       .createQueryBuilder('user')
       .where('user.username = :username', { username })
       .addSelect('user.password')
       .getOne()
    

    .addSelect('user.password') 添加这个属性查询将会包括 password 这个字段,否则普通查询的方法不会包括这个字段。

    总结