Nest 공식문서 Pipes

송은우·2022년 3월 25일
0

Pipes!

기본적으로 클래스이고, PipeTransformer interface를 implements한다.
Nestjs Pipe 공식문서
파이프는 변환, 유효성 검증을 수행한다.
controller의 route handler에서 작동한다. @Params 부분
pipe는 메서드가 실행 되기 직전에 실행됨. 그 후 메서드는 변환 또는 검증된 변수를 이용해 작동함.

Pipe는 exception zone에서 돌아가기에, exception을 throw했을 때, 그 후에 controller method가 작동하지 않는다.

Built-in Pipes

ValidationPipe
ParseIntPipe
ParseFloatPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
ParseEnumPipe
DefaultValuePipe

예를 보면 ParseIntPipe같은 경우 JS 의 integer로 바꿀 수 있으면 바꾸고, 없으면 exception을 throw 함.

Binding Pipes

Pipe를 route handler method에 등록하는 방법은

@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
  return this.catsService.findOne(id);
}

같은 방법을 통하면 된다.


{
  "statusCode": 400,
  "message": "Validation failed (numeric string is expected)",
  "error": "Bad Request"
}

pipe에서 exception 발생시 이런 것을 return 한다.
이때 pipe에는 class를 전달했다. DI의 책임을 framework에 넘기고, 통과하지 못했을 경우 findOne method가 실행되지 않는다.

@Get(':id')
async findOne(
  @Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
  id: number,
) {
  return this.catsService.findOne(id);
}

여기서는 class가 아닌 instance를 전달했다.
ParseUUIDPipe()는 UUIDversion이 3,4,5인지에 따라 달라진다.

Custom Pipes

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

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value;
  }
}

간단한 검증 파이프. 그대로 받아서 그대로 뱉는 파이프이다.
항상 PipeTransform이라는 interface를 가져오는데, 이때 PipeTransform<T,R>의 형태로 만들 수 있다. 그렇게 된다면 T는 input,R은 return의 타입을 결정할 수 있다.

모든 Pipe는 transform(value:any,metadata:ArgumentMetadata)라고 하는 함수를 구현하는데, value는 메서드에 argument를 그대로 가져온 것이고,
metadata는


export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Type<unknown>;
  data?: string;
}

의 형태를 갖는다.
type은 어디서 쓰인건지(@Body, @Param, @Query, @Custom)
metaType은 타입의 타입이라고 할 수 있다.
조금 편하게 생각하면 instance 에 대한 정보만 있었을 때, static method를 호출하려면 어떻게 해야할까? 이럴 때 typeof instance를 통해 원래 클래스를 받고, 그 클래스의 메서드를 호출한다. 단순한 instance 의 타입이 아닌 인스턴스가 만들어진 상위 타입을 다루는데
당연하게도 vanilla javascript나, type이 선언되어있을 경우 undefined가 된다.
data 는 데코레이터 안에 있는 것. @Body('string")같은 것에서 'string'을 의미함
interface가 런타임에 사라지기에, method parameter의 타입이 클래스가 아닌 interface로 정의되어 있다면 metatype이 object라고 됨

Schema Based Validation

여기서 다시 DTO가 시작..

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}
@Post()
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

이렇게 검증할 수 있다
이런 DTO가 있을 때, route handler method에서 검증할 수도 있지만, 검증 + route handler 기능 으로 single responsibility rule이라고 하는 SOLID의 S원칙을 잃어버림.
검증 클래스를 앞부분에 달 수도 있지만, 직접 호출 과정을 다는 것이 불편함. middlewawre도 가능하지만, 모든 곳에서 동일하게 사용될 수 있는 그런 middleware를 제작하는 것이 불가능하다.

Object Schema Validation

DRY 원칙 : Don't Repeat Yourself 중복 함수가 하나도 없어야 한다.
여러 방법중 하나는 schema based validation이다.
JOI를 이용해서 검증하는 방법 npm i joi

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ObjectSchema } from 'joi';

@Injectable()
export class JoiValidationPipe implements PipeTransform {
  constructor(private schema: ObjectSchema) {}

  transform(value: any, metadata: ArgumentMetadata) {
    const { error } = this.schema.validate(value);
    if (error) {
      throw new BadRequestException('Validation failed');
    }
    return value;
  }
}

Object Schema를 constructor에서 가져오고, schema.validate()함수로 검증 과정을 한다.

Binding validation pipes

@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

같은 것을 통해 validate 검증도 가능 함. Schema를 를 넣어서, 그걸 통해서 검증하는 되게 당연한 방법

Class Validator

이거는 TS로만 가능함
npm i class-validator class-transformer

import { IsString, IsInt } from 'class-validator';

export class CreateCatDto {
  @IsString()
  name: string;

  @IsInt()
  age: number;

  @IsString()
  breed: string;
}

이게 된다.
그러면 당연히 pipe도 변경이 있어야 하는데

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    const object = plainToClass(metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {
      throw new BadRequestException('Validation failed');
    }
    return value;
  }

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

metatype이란 instance => class 라고 생각하면 편함. instance를 만들고 있는 type이 뭘까 라는 답변이 meta type

transform이 async 처리가 가능했던 것 처럼 async 지원이 확실함
metatype으로 ArgumentMetadata를 가져옴. 부가적인 statement를 가져올 수 있음

profile
학생의 마음가짐으로 최선을 다하자

0개의 댓글