nestjs之pipe是如何从dto校验的?

发布时间:2024年01月19日

NestJS中的Pipe主要用于处理输入数据的验证和转换。当你使用类验证器(Class Validator)和数据传输对象(DTO)时,Pipe会根据DTO定义的装饰器来校验数据。让我们从源码的角度仔细分析这个过程。

1. 创建DTO和使用装饰器

首先,你会创建一个DTO类,并使用Class Validator提供的装饰器来注明验证规则。例如:

// create-user.dto.ts
import { IsString, Length, IsNotEmpty } from 'class-validator';

export class CreateUserDto {
  @IsString({ message: '用户名必须是一个字符串' })
  @Length(3, 20, { message: '用户名长度必须在3到20个字符之间' })
  @IsNotEmpty({ message: '账户不能为空' })
  username: string;

  @IsString({ message: '密码必须是一个字符串' })
  @IsNotEmpty({ message: '密码不能为空' })
  @Length(3, 20,{ message: '密码长度必须在3到20个字符之间' })
  password: string;
}

2. 在控制器中使用Pipe

在控制器中,你会使用@Body()装饰器结合ValidationPipe来应用这些验证规则:

import { Body, Controller, Post } from '@nestjs/common';
import { CreateCatDto } from './create-cat.dto';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post('register')
  async register(@Body(ValidationPipe) createUserDto: CreateUserDto) {
    return this.authService.register(createUserDto.username, createUserDto.password);
  }
}

3. ValidationPipe的工作方式

ValidationPipe是NestJS中的一个内置管道,它会自动实例化你的DTO,并使用Class Validator库来验证DTO属性。

从源码角度来看,当请求到达带有@Body(ValidationPipe)的控制器方法时,以下步骤会发生:

  1. 实例化DTO: NestJS会将请求体(request body)中的数据映射到DTO类的实例中。
// @nestjs\core\router\router-execution-context.js
class RouterExecutionContext {
    ...
    create(instance, callback, methodName, moduleKey, requestMethod, contextId = constants_2.STATIC_CONTEXT, inquirerId) {
        ...
        // 获取函数的参数类型并保存
        const { argsLength, fnHandleResponse, paramtypes, getParamsMetadata, httpStatusCode, responseHeaders, hasCustomHeaders, } = this.getMetadata(instance, callback, methodName, moduleKey, requestMethod, contextType);
        ...
    }
    getMetadata(instance, callback, methodName, moduleKey, requestMethod, contextType) {
        const cacheMetadata = this.handlerMetadataStorage.get(instance, methodName);
        if (cacheMetadata) {
            return cacheMetadata;
        }
        const metadata = this.contextUtils.reflectCallbackMetadata(instance, methodName, constants_1.ROUTE_ARGS_METADATA) || {};
        const keys = Object.keys(metadata);
        const argsLength = this.contextUtils.getArgumentsLength(keys, metadata);
        const paramtypes = this.contextUtils.reflectCallbackParamtypes(instance, methodName);
        const contextFactory = this.contextUtils.getContextFactory(contextType, instance, callback);
        const getParamsMetadata = (moduleKey, contextId = constants_2.STATIC_CONTEXT, inquirerId) => this.exchangeKeysForValues(keys, metadata, moduleKey, contextId, inquirerId, contextFactory);
        const paramsMetadata = getParamsMetadata(moduleKey);
        const isResponseHandled = this.isResponseHandled(instance, methodName, paramsMetadata);
        const httpRedirectResponse = this.reflectRedirect(callback);
        const fnHandleResponse = this.createHandleResponseFn(callback, isResponseHandled, httpRedirectResponse);
        const httpCode = this.reflectHttpStatusCode(callback);
        const httpStatusCode = httpCode
            ? httpCode
            : this.responseController.getStatusByMethod(requestMethod);
        const responseHeaders = this.reflectResponseHeaders(callback);
        const hasCustomHeaders = !(0, shared_utils_1.isEmpty)(responseHeaders);
        const handlerMetadata = {
            argsLength,
            fnHandleResponse,
            paramtypes,
            getParamsMetadata,
            httpStatusCode,
            hasCustomHeaders,
            responseHeaders,
        };
        // 保存函数的参数类型
        this.handlerMetadataStorage.set(instance, methodName, handlerMetadata);
        return handlerMetadata;
    }
    ...
}
exports.RouterExecutionContext = RouterExecutionContext;

  1. 执行验证: ValidationPipe使用Class Validator库对DTO实例进行验证。这包括检查是否所有装饰器定义的规则都被满足。
	// @nestjs\core\router\router-execution-context.js
	...
	// 路由执行pipe,并传入参数类型和req的值
    async getParamValue(value, { metatype, type, data, }, pipes) {
        if (!(0, shared_utils_1.isEmpty)(pipes)) {
            return this.pipesConsumer.apply(value, { metatype, type, data }, pipes);
        }
        return value;
    }
   ...

PipesConsumer 执行pipetransform方法。

// 执行applyPipes方法调用pipe的transform
class PipesConsumer {
    constructor() {
        this.paramsTokenFactory = new params_token_factory_1.ParamsTokenFactory();
    }
    async apply(value, { metatype, type, data }, pipes) {
        const token = this.paramsTokenFactory.exchangeEnumForString(type);
        return this.applyPipes(value, { metatype, type: token, data }, pipes);
    }
    async applyPipes(value, { metatype, type, data }, transforms) {
        return transforms.reduce(async (deferredValue, pipe) => {
            const val = await deferredValue;
            const result = pipe.transform(val, { metatype, type, data });
            return result;
        }, Promise.resolve(value));
    }
}
  1. 处理结果:
    • 如果验证通过,控制器方法会接收到验证后的DTO实例。
    • 如果验证失败,NestJS会抛出一个异常,通常返回一个400 Bad Request响应。
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ValidationPipe = void 0;
const tslib_1 = require("tslib");
const iterare_1 = require("iterare");
const util_1 = require("util");
const decorators_1 = require("../decorators");
const core_1 = require("../decorators/core");
const http_status_enum_1 = require("../enums/http-status.enum");
const http_error_by_code_util_1 = require("../utils/http-error-by-code.util");
const load_package_util_1 = require("../utils/load-package.util");
const shared_utils_1 = require("../utils/shared.utils");
let classValidator = {};
let classTransformer = {};
/**
 * @see [Validation](https://docs.nestjs.com/techniques/validation)
 *
 * @publicApi
 */
let ValidationPipe = class ValidationPipe {
    constructor(options) {
        options = options || {};
        const { transform, disableErrorMessages, errorHttpStatusCode, expectedType, transformOptions, validateCustomDecorators, ...validatorOptions } = options;
        // @see https://github.com/nestjs/nest/issues/10683#issuecomment-1413690508
        this.validatorOptions = { forbidUnknownValues: false, ...validatorOptions };
        this.isTransformEnabled = !!transform;
        this.transformOptions = transformOptions;
        this.isDetailedOutputDisabled = disableErrorMessages;
        this.validateCustomDecorators = validateCustomDecorators || false;
        this.errorHttpStatusCode = errorHttpStatusCode || http_status_enum_1.HttpStatus.BAD_REQUEST;
        this.expectedType = expectedType;
        this.exceptionFactory =
            options.exceptionFactory || this.createExceptionFactory();
        classValidator = this.loadValidator(options.validatorPackage);
        classTransformer = this.loadTransformer(options.transformerPackage);
    }
    loadValidator(validatorPackage) {
        return (validatorPackage ??
            (0, load_package_util_1.loadPackage)('class-validator', 'ValidationPipe', () => require('class-validator')));
    }
    loadTransformer(transformerPackage) {
        return (transformerPackage ??
            (0, load_package_util_1.loadPackage)('class-transformer', 'ValidationPipe', () => require('class-transformer')));
    }
    async transform(value, metadata) {
        if (this.expectedType) {
            metadata = { ...metadata, metatype: this.expectedType };
        }
        const metatype = metadata.metatype;
        if (!metatype || !this.toValidate(metadata)) {
            return this.isTransformEnabled
                ? this.transformPrimitive(value, metadata)
                : value;
        }
        const originalValue = value;
        value = this.toEmptyIfNil(value);
        const isNil = value !== originalValue;
        const isPrimitive = this.isPrimitive(value);
        this.stripProtoKeys(value);
        // 使用我们定义的的dto将value的值{xxx:xxx,xxx:xxx}实例化
        let entity = classTransformer.plainToClass(metatype, value, this.transformOptions);
        const originalEntity = entity;
        const isCtorNotEqual = entity.constructor !== metatype;
        if (isCtorNotEqual && !isPrimitive) {
            entity.constructor = metatype;
        }
        else if (isCtorNotEqual) {
            // when "entity" is a primitive value, we have to temporarily
            // replace the entity to perform the validation against the original
            // metatype defined inside the handler
            entity = { constructor: metatype };
        }
        // 调用class-validator去验证并返回errors
        const errors = await this.validate(entity, this.validatorOptions);
        if (errors.length > 0) {
            throw await this.exceptionFactory(errors);
        }
        ...
    }
    validate(object, validatorOptions) {
        return classValidator.validate(object, validatorOptions);
    }
    ...
};
exports.ValidationPipe = ValidationPipe;

4. 源码中的关键部分

在NestJS的源码中,ValidationPipe的实现可以在@nestjs/common包中找到。关键的部分是transform方法,它负责处理传入的数据,并应用Class Validator的验证逻辑。

5. 扩展和自定义

NestJS允许你自定义ValidationPipe的行为,例如自定义错误响应或启用更复杂的验证规则。你可以通过传递选项到ValidationPipe的构造函数来实现这些自定义行为。

通过这种方式,NestJS中的Pipes提供了一种强大且灵活的方法来确保控制器接收到有效且正确类型的数据。

文章来源:https://blog.csdn.net/qq_35094120/article/details/135688701
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。