我們想使用 創(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ù)組中也確實新增了一項:
訪問 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 中的攔截器