iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 7
1
Modern Web

Nest.js framework 30天初探系列 第 7

Nestjs framework 30天初探:Day07 Pipes

  • 分享至 

  • xImage
  •  

Pipes

https://ithelp.ithome.com.tw/upload/images/20171210/20107195Hkc12omP72.png

Pipes 是帶有@Pipe()裝飾器的class,Pipe必須實作PipeTransform介面。
Pipe可以將input data轉換成我們想要的output data,它也可以扛下參數資料驗證工作,在參數資料不正確時拋錯出來,這個錯誤會被上一章節提到的ExceptionsHandler或自定義的Exception Filter所捕捉。

  1. 請在專案根目錄下cmd指令,Shared資料夾底下創Pipes資料夾。
cd src/modules/Shared & mkdir Pipes
  1. 在Pipes資料夾底下新增一隻validation.pipe.ts,我們先寫簡單的Pipe,看一下Pipe的樣貌,後續再更深入探討。
    src/modules/Shared/Pipes/validation.pipe.ts
import { PipeTransform, Pipe, ArgumentMetadata } from '@nestjs/common';

//Pipe裝飾器,會告訴nestjs這是Pipe
@Pipe()
//要實作PipeTransform的介面
export class ValidationPipe implements PipeTransform<any>{
    //PipeTransform介面有transform(),這可以轉換input data並回傳
    transform(value: any, metadata: ArgumentMetadata) {
    //在這邊先不做任何轉換,只是要來看看Pipe的風貌,所以直接回傳value
        return value;
    }
}

注意:ValidationPipe 只能run在TypeScript,nestjs作者表示,如果是使用一般JavaScript的話,推薦使用Joi套件。

  1. 其中 ArgumentMetadata的底層如下:
    ArgumentMetadata
export interface ArgumentMetadata {
    type: Paramtype;
    metatype?: new (...args) => any;
    data?: string;
}

Paramtype

export declare type Paramtype = 'body' | 'query' | 'param' | 'custom';

官網說明:https://ithelp.ithome.com.tw/upload/images/20171210/20107195dSK3bDqskm.png

注意:因為TypeScript的Interface在轉譯過程會直接消失,所以如果是使用interface而不是class的話,metatype的型別會變成Object。

  1. 前面都是預告片,現在正式開演。在第二章節,addUser()需要傳遞參數,回顧一下方法的程式碼和該DTO的屬性。
    src/modules/Users/users.controller.ts
@Post('users')
    async addUser( @Response() res, @Body() createUserDTO: CreateUserDTO) {
        //使用Rx.js,所以回傳可以做更多資料流的處理
        await this.userService.addUser(createUserDTO).subscribe((users) => {
            res.status(HttpStatus.OK).json(users);
        })
    }

src/modules/Users/DTO/create-users.dto.ts

export class CreateUserDTO {
    readonly _id: number;
    readonly _name: string;
    readonly _age: number;
}
  1. 在CreateUserDTO,我們稍作改寫,使用class-validatorclass-validator提供了好幾種檢驗型別的裝飾器。
    src/modules/Users/DTO/create-users.dto.ts
import { IsString, IsInt } from 'class-validator';

export class CreateUserDTO {

    @IsInt()
    readonly _id: number;

    @IsString()
    readonly _name: string;

    @IsInt()
    readonly _age: number;
}
  1. 在Pipes資料夾底下新增validation.pipe.ts,並增加以下程式碼。
    src/modules/Shared/Pipes/validation.pipe.ts
import { HttpException } from '@nestjs/core';
import { PipeTransform, Pipe, ArgumentMetadata, HttpStatus } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';

//Pipe裝飾器,會告訴nestjs這是Pipe
@Pipe()
//要實作PipeTransform的介面
export class ValidationPipe implements PipeTransform<any>{
    //PipeTransform介面有transform(),這可以轉換input data並回傳
    async transform(value, metadata: ArgumentMetadata) {
        const { metatype } = metadata;
        /*不檢查原生JavaScript的型別,因為刻意寫ValidationPipe,就是要使用自定義的DTO class
        的屬性去做參數型別檢查,如果metatype是原生JavaScript的型別,就直接return 原始參數,
        不做ValidationPipe的檢查。
        */
        if (!metatype || !this.toValidate(metatype)) {
            return value;
        }
        //這裡使用class-transformer的方法,將plain javascript object(像是JSON object),轉換成一個class的object。        
        const object = plainToClass(metatype, value);
        //在CreateUserDTO,我們有使用class-validator的驗證裝飾器,validate()會回傳錯誤陣列。
        const errors = await validate(object);
        if (errors.length > 0) {
            console.log('validate errors',errors);
            throw new HttpException('檢驗錯誤', HttpStatus.BAD_REQUEST);
        }
        //ValidationPipe是特別打造出來檢驗型別用,最後要回傳原始參數,避免覆寫。
        return value;
    }
    //檢驗是否為原生JavaScript的型別
    private toValidate(metatype): boolean {
        const types = [String, Boolean, Number, Array, Object];
        return !types.find((type) => metatype === type);
    }
}

說明:請看程式碼裏頭的註解。

  1. Pipe其實也跟Exception Filter一樣,可以在Method作用、Controller作用、全域作用,甚至是參數區塊作用,所以我們將ValidationPipe寫在參數區塊來試試。
    src/modules/Users/users.controller.ts
    部分程式碼
@Post('users')
    async addUser( @Response() res, @Body(new ValidationPipe()) createUserDTO: CreateUserDTO) {
        //使用Rx.js,所以回傳可以做更多資料流的處理
        await this.userService.addUser(createUserDTO).subscribe((users) => {
            res.status(HttpStatus.OK).json(users);
        })
    }
  1. 因為我們在上一章節有使用Exception Filter,我們先註解掉他們的裝飾器,避免待會看不到ValidationPipe檢驗失敗的模樣,因為Exception Filter會去接住這個錯誤並做回應。
    src/app.module.ts
//@UseFilters(new HttpExceptionFilter())

src/modules/Users/users.controller.ts
//@UseFilters(new HttpExceptionFilter())
9. 來實際體驗一下ValidationPipe,打開Postman,對http://localhost:3000/users 進行POST請求,並觀察console畫面。
9.1 故意將_id傳一個字串。
https://ithelp.ithome.com.tw/upload/images/20171210/201071953vloDswnwE.png

說明:符合ValidationPipe所做的回應。

9.2 console結果
https://ithelp.ithome.com.tw/upload/images/20171210/20107195IbZPnOZHrC.png

class-validator所賜console有顯示詳細的型別錯誤資訊。

9.3 打開Postman,對http://localhost:3000/users 進行POST請求,傳入符合CreateUserDTO型別的json object。
https://ithelp.ithome.com.tw/upload/images/20171210/20107195879kOCAMbr.png

成功!

  1. 如一開始所說,Pipe可以將input data轉換成我們要的output data,所以來實作一下這部分,在Pipes資料夾新增parse-int.pipe.ts
    src/modules/Shared/Pipes/parse-int.pipe.ts
import { HttpException } from '@nestjs/core';
import { PipeTransform, Pipe, ArgumentMetadata, HttpStatus } from '@nestjs/common';

@Pipe()
export class ParseIntPipe implements PipeTransform<string>{
    async transform(value: string, metadata: ArgumentMetadata) {
        const val = parseInt(value, 10);
        if (isNaN(val)) {
            throw new HttpException('檢驗錯誤', HttpStatus.BAD_REQUEST);
        }
        return val;
    }
}
  1. 接著我們來實際使用一下ParseIntPipe,修改一下UsersController的getUser()。
    src/modules/Users/user.controller.ts
    部分程式碼
import { ParseIntPipe } from '../Shared/Pipes/parse-int.pipe';
@Get('users/:id')
    //使用Express的參數
    //@Param('id')可以直接抓id參數
    //使用ParseIntPipe
    async getUser( @Response() res, @Param('id', new ParseIntPipe()) id) {
        //id參數前面不帶加號,ParseIntPipe會將參數轉型為int型別
        await this.userService.getUser(id)
            .then((user) => {
                res.status(HttpStatus.OK).json(user);
            })
            .catch((error) => {
                console.error(error);
                res.status(HttpStatus.INTERNAL_SERVER_ERROR);
            })
    }
  1. 使用Postman,對http://localhost:3000/users/2 進行GET請求,結果如下。
    https://ithelp.ithome.com.tw/upload/images/20171210/20107195nJfM9Vphgk.png

正常顯示,所以ParseIntPipe轉換型別成功。

程式碼都在github


上一篇
Nestjs framework 30天初探:Day06 Exception Filters
下一篇
Nestjs framework 30天初探:Day08 Guards
系列文
Nest.js framework 30天初探30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
0
Jonny Huang
iT邦新手 5 級 ‧ 2017-12-16 08:23:40

"TypeScript的Interface在轉譯成JavaScript時會變成class"
這一點我倒沒有注意到,我以為 interface 會直接消失。

kk iT邦新手 5 級 ‧ 2017-12-17 15:55:04 檢舉

Jonny Huang您說的是對的,我的解釋不好,剛剛修改了文章。

1
raymonda
iT邦新手 5 級 ‧ 2018-03-22 15:26:57

妳好,請問如何使用Pipe進行部分參數驗證?
按照官方文檔的寫法,默認會對所有參數進行驗證,但是某些API不需要這樣做,例如更新和刪除API(只需要驗證部分參數)。
我有註意到 class-validator 自帶的 Skipping missing properties ,以及 typeormEntity Listeners and Subscribers ,但是我不知道怎麽用,請問能給我壹些幫助嗎,謝謝~!

抱歉來晚了xd
您當年要的資料如下~

另外在 dto 的 class-validaotr
IsOptional() 可以併用

    @IsOptional()
    @IsString()
    readonly _id?: string;

    @IsString()
    @IsNotEmpty()
    readonly content: string;

    @IsInt()
    readonly priority?: number;
0
jason19970210
iT邦新手 5 級 ‧ 2022-04-27 06:28:08

Updated Date: 2022/04/27

版本更新

// package.json [dependencies]
"@nestjs/common": "^8.0.0",

// cli
$ nest --version
8.2.5

Point 2.

import { PipeTransform, Pipe, ArgumentMetadata } from '@nestjs/common';
@Pipe()
export...

其中的 Pipe 改為 Injectable

import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export...

Point 6.
簡化

// before
async transform(value, metadata: ArgumentMetadata) {
    const { metatype } = metadata;
    if ...
        
// after
async transform(value, { metatype }: ArgumentMetadata) {
    if ...

補充:( 回應 raymonda
validate 可以帶入許多不同的 setting

// example
const errors = await validate(object, {
    skipMissingProperties: true,
    forbidUnknownValues: true, 
});

我要留言

立即登入留言