Pipes

Pipes are really useful feature. You should think about them as a streams of data. They're called immediately before route handlers.

Pipes是非常有用的功能。可以将pipes看作是数据流。在路由控制器处理完程序之后立即调用pipes。

Pipes是什么?

Pipe is a simple class, which is decorated by @Pipe() and implements PipeTransform interface.

Pipe是一个简单的带有@Pipe装饰器的类,它可以实现PipeTransform 界面。

import { PipeTransform, Pipe, ArgumentMetadata } from '@nestjs/common';

@Pipe()
export class CustomPipe implements PipeTransform {
    public transform(value, metadata: ArgumentMetadata) {
        return value;
    }
}

It has a one method, which receives 2 arguments:

  • value (any)
  • metadata (ArgumentMetadata) metadata is the metadata of an argument for which the pipe is being processed, and the value is... just its value. The metadata holds few properties:

它有一个方法,该方法接收两个参数

  • value (any)
  • metadata (ArgumentMetadata)

    metadata指的是pipe正在处理的参数的元数据,元数据有以下属性:

export interface ArgumentMetadata {
    type: 'body' | 'query' | 'param';
    metatype?: any;
    data?: any;
}

I'll tell you about them later.

稍后再详细讲解。

运行原理

Let's imagine we have a route handler:

让我们想象一下我们有如下路由处理程序:

@Post()
public async addUser(@Res() res: Response, @Body('user') user: UserDto) {
    const msg = await this.usersService.addUser(user);
    res.status(HttpStatus.CREATED).json(msg);
}

There is a request body parameter user. The type is UserDto:

有一个请求体参数用户。属性为UserDto

export class UserDto {
    public readonly name: string;
    public readonly age: number;
}

This object always has to be correct, so we have to validate those two fields. We could do it in the route handler method, but we will break the single responsibility rule. The second idea is to create validator class and delegate the task there, but we will have to call this validator at the beginning of the method every time. So maybe should we create a validation middleware? It's good idea, but it's almost impossible to create a generic middleware, which might by used along entire application. This is the first use-case, when you should consider to use a Pipe.

该对象必须校正,所以我们需要验证这两个字段。我们可以在路由处理程序方法中进行验证,但是这违反了单一职责原则。

第二种方法是我们可以创建一个验证类,由这个验证类去执行验证,但是这样的话,我们每次在执行方法的时候都必须调用该验证器。

那么我们是不是可以创建一个验证中间件呢?但是要创建一个在整个应用程序中通用的中间件几乎是不可能的。

所以这个时候我们就可以考虑使用Pipe了。

ValidatorPipe with Joi

There is an amazing library, which name is Joi. It's commonly used with hapi. Let's create a schema:

有一个超级好用的库,叫做Joi,它通常跟hapi搭配使用。我们来创建一个模式:

const userSchema = Joi.object().keys({
    name: Joi.string().alphanum().min(3).max(30).required(),
    age: Joi.number().min(1).required(),
}).required();
And the appropriate Pipe:
@Pipe()
export class JoiValidatorPipe implements PipeTransform {
    constructor(private readonly schema: Joi.ObjectSchema) {}

    public transform(value, metadata: ArgumentMetadata) {
        const { error } = Joi.validate(value, this.schema);
        if (error) {
            throw new HttpException('Validation failed', HttpStatus.BAD_REQUEST);
        }
        return value;
    }
}

Notice! If you want to use your domain exception instead of built-in HttpException, you have to set-up Exception Filter. Now, we only have to bind JoiValidatorPipe to our method.

注意!如果你想使用局域异常来代替built-in HttpException,必须设置异常过滤器。 现在,我们只需要将将JoiValidatorPipe与我们的方法相结合,就可以进行验证了。

@Post()
@UsePipes(new JoiValidatorPipe(userSchema))
public async addUser(@Res() res: Response, @Body('user') user: UserDto) {
    const msg = await this.usersService.addUser(user);
    res.status(HttpStatus.CREATED).json(msg);
}

The transform() function will be evaluated for each @Body(), @Param() and @Query() argument of the route handler. If you want to run validation only for @Body() arguments - use metadata properties. Let's create pipe with predicate:

路由处理程序的每个 @Body(), @Param() and @Query()参数,都要进行transform()功能评估。如果你只需要针对@Body()参数执行验证,那么你可以使用元数据属性。让我们使用谓词创建Pipe:

@Pipe()
export class JoiValidatorPipe implements PipeTransform {
    constructor(
        private readonly schema: Joi.ObjectSchema,
        private readonly toValidate = (metadata: ArgumentMetadata) => true) {}

    public transform(value, metadata: ArgumentMetadata) {
        if (!this.toValidate(metadata)) {
            return value;
        }
        const { error } = Joi.validate(value, this.schema);
        if (error) {
            throw new HttpException('Validation failed', HttpStatus.BAD_REQUEST);
        }
        return value;
    }
}

And the usage example:

看一个有用的示例:

@Post()
@UsePipes(new JoiValidatorPipe(userSchema, ({ type }) => type === 'body'))
public async addUser(@Res() res: Response, @Body('user') user: UserDto) {
    const msg = await this.usersService.addUser(user);
    res.status(HttpStatus.CREATED).json(msg);
}
Also, you can directly bind pipe to chosen argument:
@Post()
public async addUser(
    @Res() res: Response,
    @Body('user', new JoiValidatorPipe(userSchema)) user: UserDto) {

    const msg = await this.usersService.addUser(user);
    res.status(HttpStatus.CREATED).json(msg);
}

That's all.

Joi is a great library, but this solution is not generic. We always have to create a schema, and the pipe instance. Can we make it better? Sure, we can.

Joi是一个强大的库。但是这个解决办法并不能解决通用问题。我们总是要创建一个模式和pipe实例。

ValidatorPipe with class-validator

There is an amazing library, which name is class-validator (great library @pleerock!). It allows to use decorator-based validation. Let's create a generic validation pipe:

还有一个名为class-validator的强大的库。使用这个库可帮助我们进行基于装饰器的验证。

import { validate } from 'class-validator';

@Pipe()
export class ValidatorPipe implements PipeTransform {
    public async transform(value, metadata: ArgumentMetadata) {
        const { metatype } = metadata;
        if (!this.toValidate(metatype)) {
            return value;
        }
        const object = Object.assign(new metatype(), value);
        const errors = await validate(object);
        if (errors.length > 0) {
            throw new HttpException('Validation failed', HttpStatus.BAD_REQUEST);
        }
        return value;
    }

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

IMPORTANT! It works only with TypeScript.

注意!只用该库时只能使用TypeScript。

As you can see, the metatype property of ArgumentMetadata holds type of the value. Furthermore, the pipes can by asynchronous. When we would use ValidatorPipe there:

ArgumentMetadatametatype属性包含值类型。此外,Pipes可进行异步操作。我们可以这样使用ValidatorPipe

@Post()
@UsePipes(new ValidatorPipe())
public async addUser(@Res() res: Response, @Body('user') user: UserDto) {
    const msg = await this.usersService.addUser(user);
    res.status(HttpStatus.CREATED).json(msg);
}

The metatype will be UserDto. That's it. We have a generic solution now. Notice! It doesn't work with TypeScript interfaces - you have to use classes instead.

metatype为UserDto。 就是如此,我们现在找到了一个方案可以解决通用性问题。 注意!它无法在TypeScript接口中运行 - 所以必须使用类代替。

作用域

You already know that pipes can be argument-scoped and method-scoped. It's not everything! We can set-up pipe for each route handler in the Controller (controller-scoped pipes):

你已经了解到pipes可以是argument-scopedmethod-scoped。不仅仅如此!我们也可以在控制器(controller-scoped pipes)中为每个路由处理程序设置pipe。

@Controller('users')
@UsePipes(new ValidatorPipe())
export class UsersController {}

Moreover, you can set-up global pipe for each route handler in the Nest application!

此外,你还可以在Nest应用程序中为每个路由处理程序设置global pipe

const app = NestFactory.create(ApplicationModule);
app.useGlobalPipes(new ValidatorPipe());

This is how you can e.g. enable request properties auto-validation.

results matching ""

    No results matching ""