7、Nest.js 中的類驗證器

我們想使用 創(chuàng)建用戶 這個功能來說明類驗證器舔痕,首先來完善我們的 user 接口:

src/users/interfaces/user.interface.ts

import { User } from './user.interface';

export interface IUserService {

   findAll(): Promise<User[]>;

   findOne(id: number): Promise<User>;

   create(User): Promise<User>;

   edit(User): Promise<User>;

   remove(id: number): Promise<boolean>;

}

然后改寫一下我們的 UsersService魏滚,讓它含有一個靜態(tài)的 User 數(shù)組镀首,并且 fineOne 和 fineAll 都從這個靜態(tài)變量中獲取數(shù)據(jù),create鼠次、edit更哄、remove也都有相應的實現(xiàn):

src/users/services/user.service.ts

import { Injectable } from '@nestjs/common';
import { User } from '../interfaces/user.interface';
import { IUserService } from '../interfaces/user-service.interface';

@Injectable()
export class UsersService implements IUserService {

    private static users: User[] = [
        { id: 1, name: '小明', age: 18 },
        { id: 2, name: '小紅', age: 16 },
        { id: 3, name: '小壯', age: 20 },
    ];

    async findAll(): Promise<User[]> {
        return UsersService.users;
    }

    async findOne(id: number): Promise<User> {
        return UsersService.users.find(user => user.id == id)
    }

    async create(user: User): Promise<User> {
        UsersService.users.push(user);
        return user;
    }

    async edit(user: User): Promise<User> {
        let index = UsersService.users.findIndex(item => item.id == user.id)

        if(index >= 0) {
            UsersService.users[index] = user;
        }

        return UsersService.users[index];
    }

    async remove(id: number): Promise<boolean> {
        let index = UsersService.users.findIndex(item => item.id == id)

        if(index >= 0) {
            UsersService.users.splice(index, 1);
        }
        
        return index >= 0;
    }
}

現(xiàn)在訪問 http://127.0.0.1:3000/users/3,看到下面的輸出:

{"id":3,"name":"小壯","age":20}

發(fā)送一個 post 請求后腥寇,users 數(shù)組中也確實新增了一項:


image.png

訪問 http://127.0.0.1:3000/users

[
    {"id":1,"name":"小明","age":18},
    {"id":2,"name":"小紅","age":16},
    {"id":3,"name":"小壯","age":20},
    {"id":"4","name":"小李","age":"17"}
]

我們沒有得到預期的效果成翩,心細的同學可能已經(jīng)發(fā)現(xiàn)了, 新增加的用戶的 id 和 age 值的類型是 string 而不是 number花颗,這樣的程序是很不健壯的捕传,我們已經(jīng)知道該如何使用管道來驗證客戶端提交的參數(shù)了惠拭,那么如何保證參數(shù)有且僅有一個 User 類型呢扩劝?
在 Nest 中 類驗證器 可以很好的解決這個問題庸论,在使用類驗證器之前,我們需要先安裝兩個npm包:

$ npm install --save class-validator class-transformer

有關這兩個包的更多用法可以到 GitHub 上搜索棒呛。
為我們的 ApiErrorCode 定義更多的業(yè)務狀態(tài)碼:

export enum ApiErrorCode {
    TIMEOUT = -1, // 系統(tǒng)繁忙
    SUCCESS = 0, // 成功

    USER_ID_INVALID = 10001, // 用戶 ID 無效
    USER_NAME_INVALID = 10002, // 用戶 姓名 無效
    USER_AGE_INVALID = 10003, // 用戶 年齡 無效
}

我們還需要一個叫做 DTO(數(shù)據(jù)傳輸對象)的文件聂示,他就是一個普通的類,用來替換 UsersController 中 create 方法的參數(shù)類型簇秒,目前我們 create 方法使用的是 User 接口類型鱼喉,TypeScript 接口在編譯過程中被刪除,這樣會導致我們無法在管道中獲取參數(shù)的元數(shù)據(jù)趋观。

src/users/dtos/create-user.dto.ts

import { User } from "../interfaces/user.interface";
import { IsString, IsInt, IsNotEmpty, Min, Max } from 'class-validator';
import { ApiErrorCode } from "common/enums/api-error-code.enum";

export class CreateUserDto implements User {
        
    @IsInt({ message: '用戶ID必須是整數(shù)', context: { errorCode: ApiErrorCode.USER_ID_INVALID } })
    @Min(1, { message: '用戶ID必須大于等于1', context: { errorCode: ApiErrorCode.USER_ID_INVALID } })
    readonly id: number;

    @IsNotEmpty({ message: '用戶姓名是必不可少的', context: { errorCode: ApiErrorCode.USER_NAME_INVALID } })
    @IsString({ message: '用戶姓名是必不可少的', context: { errorCode: ApiErrorCode.USER_NAME_INVALID } })
    readonly name: string;
    
    @IsInt({ message: '用戶年齡必須是整數(shù)', context: { errorCode: ApiErrorCode.USER_AGE_INVALID } })
    @Min(1, { message: '用戶年齡必須大于1', context: { errorCode: ApiErrorCode.USER_AGE_INVALID } })
    @Max(200, { message: '用戶年齡必須小于200', context: { errorCode: ApiErrorCode.USER_AGE_INVALID } })
    readonly age: number;
}

在 UsersController 中使用我們的 DTO 對象:

src/users/users.controller.ts

import { Controller, Param, Get, Post, Delete, Put, Body } from '@nestjs/common';
import { User } from './interfaces/user.interface';
import { UsersService } from './services/users.service';
import { UserIdPipe } from './pipes/user-id.pipe';
import { CreateUserDto } from './dtos/create-user.dto';

@Controller('users')
export class UsersController {

    constructor(private readonly usersService: UsersService) { }

    @Get()
    async findAll(): Promise<User[]> {

        return await this.usersService.findAll();
    }

    @Get(':id')
    async findOne(@Param('id', new UserIdPipe()) id): Promise<User> {

        return await this.usersService.findOne(id);
    }

    @Post()
    async create(@Body() user: CreateUserDto): Promise<User> {

        return await this.usersService.create(user);
    }

    @Put()
    async edit(@Body() user: CreateUserDto): Promise<User> {

        return await this.usersService.edit(user);
    }

    @Delete(':id')
    async remove(@Param('id', new UserIdPipe()) id): Promise<boolean> {

        return await this.usersService.remove(id);
    }
}

現(xiàn)在我們的控制器看起來非常簡潔扛禽,它只做了它該做的事情——分發(fā)請求, 業(yè)務邏輯的實現(xiàn)交給 service 層皱坛, 參數(shù)的驗證交給 驗證層编曼。
讓我們來實現(xiàn)我們的類驗證器,我們打算寫一個全局的DTO驗證層剩辟, 如果你沒有忘記 面向切面編程 那么你應該知道DTO驗證層在我們系統(tǒng)的各個模塊中也是一個橫切面:

src/common/pipes/api-params-validation.pipe.ts

import { ArgumentMetadata, PipeTransform, Injectable, HttpStatus } from '@nestjs/common';
import { ApiException } from '../exceptions/api.exception';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';

@Injectable()
export class ApiParamsValidationPipe implements PipeTransform {
  async transform(value: any, metadata: ArgumentMetadata) {

    const { metatype } = metadata;

    // 如果參數(shù)不是 類 而是普通的 JavaScript 對象則不進行驗證
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    
    // 通過元數(shù)據(jù)和對象實例掐场,去構建原有類型
    const object = plainToClass(metatype, value);
    const errors = await validate(object);

    if (errors.length > 0) {
      // 獲取到第一個沒有通過驗證的錯誤對象
      let error = errors.shift();
      let constraints = error.constraints
      let contexts = error.contexts
      
      // 將未通過驗證的字段的錯誤信息和狀態(tài)碼,以ApiException的形式拋給我們的全局異常過濾器
      for (let key in constraints) {
        throw new ApiException(constraints[key], contexts[key].errorCode, HttpStatus.BAD_REQUEST);
      }

    }

    return value;
  }

  private toValidate(metatype): boolean {
    const types = [String, Boolean, Number, Array, Object];
    return !types.find((type) => metatype === type);
  }
}

值得注意的一點是贩猎,要想讓 TypeScript 將客戶提交上來的數(shù)據(jù)轉(zhuǎn)換成正確的類型熊户,我們需要手動指定類型元數(shù)據(jù):

src/users/dtos/create-user.dto.ts

import { Type } from "class-transformer";
import { IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
import { ApiErrorCode } from "common/enums/api-error-code.enum";
import { User } from "../interfaces/user.interface";

export class CreateUserDto implements User {
        
    @Type(() => Number)    
    @Min(1, { message: '用戶ID必須大于等于1', context: { errorCode: ApiErrorCode.USER_ID_INVALID } })
    @IsInt({ message: '用戶ID必須是整數(shù)', context: { errorCode: ApiErrorCode.USER_ID_INVALID } })
    readonly id: number;

    @IsString({ message: '用戶姓名必須是字符串', context: { errorCode: ApiErrorCode.USER_NAME_INVALID } })
    @IsNotEmpty({ message: '用戶姓名是必不可少的', context: { errorCode: ApiErrorCode.USER_NAME_INVALID } })
    readonly name: string;
        
    @Type(() => Number)    
    @Min(1, { message: '用戶年齡必須大于1', context: { errorCode: ApiErrorCode.USER_AGE_INVALID } })
    @Max(200, { message: '用戶年齡必須小于200', context: { errorCode: ApiErrorCode.USER_AGE_INVALID } })
    @IsInt({ message: '用戶年齡必須是整數(shù)', context: { errorCode: ApiErrorCode.USER_AGE_INVALID } })
    readonly age: number;
}

最后一步,在 main.ts 中使用我們的全局類驗證器:

src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from 'app.module';
import { HttpExceptionFilter } from 'common/filters/http-exception.filter';
import { ApiParamsValidationPipe } from 'common/pipes/api-params-validation.pipe';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  app.useGlobalPipes(new ApiParamsValidationPipe());

  await app.listen(3000);
}
bootstrap();

現(xiàn)在我們對客戶端輸入的參數(shù)做了非常嚴格的校驗吭服!

上一篇:6嚷堡、Nest.js 中的管道與驗證器
下一篇:8、Nest.js 中的攔截器

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末艇棕,一起剝皮案震驚了整個濱河市麦到,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌欠肾,老刑警劉巖瓶颠,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異刺桃,居然都是意外死亡粹淋,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門瑟慈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來桃移,“玉大人,你說我怎么就攤上這事葛碧〗杞埽” “怎么了?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵进泼,是天一觀的道長蔗衡。 經(jīng)常有香客問我纤虽,道長,這世上最難降的妖魔是什么绞惦? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任逼纸,我火速辦了婚禮,結(jié)果婚禮上济蝉,老公的妹妹穿的比我還像新娘杰刽。我一直安慰自己,他們只是感情好王滤,可當我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布贺嫂。 她就那樣靜靜地躺著,像睡著了一般雁乡。 火紅的嫁衣襯著肌膚如雪涝婉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天蔗怠,我揣著相機與錄音墩弯,去河邊找鬼。 笑死寞射,一個胖子當著我的面吹牛渔工,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播桥温,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼引矩,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了侵浸?” 一聲冷哼從身側(cè)響起旺韭,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎掏觉,沒想到半個月后区端,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡澳腹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年织盼,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片酱塔。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡沥邻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出羊娃,到底是詐尸還是另有隱情唐全,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布蕊玷,位于F島的核電站邮利,受9級特大地震影響弥雹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜近弟,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望挺智。 院中可真熱鬧祷愉,春花似錦、人聲如沸赦颇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽媒怯。三九已至订讼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扇苞,已是汗流浹背欺殿。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鳖敷,地道東北人脖苏。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像定踱,于是被迫代替她去往敵國和親棍潘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,514評論 2 348