> Backend/NestJS

230105목_공부일지

Janku 2023. 1. 5. 17:36

1. 파이프와 유효성 검사: 요청이 제대로 전달되었는가 (p 113~)

   => 정의: 요청이 라우터 핸들러로 전달되기 전에 요청 객체를 변환할 수 있는 기회를 제공

   => 미들웨어와 유사하나, 미들웨어는 app의 모든 컨텍스트에서 사용하도록 할 수 없음. 

   => 목적: 변환(입력 데이터를 원하는 형시으로 변환) && 유효성 검사 (입력 데이터가 사용자가 정한 기준에 유효하지 않은 경우 예외)

   => 사용방법: @Param decorator의 두번째 인수로 파이프를 넘겨 현재 실행 콘텍스트 (ExecutionContext에 바인딩)

 

findOne(@Param('id', new ParseIntPipe({errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE})) id: number) {

 

=> 숫자일때는 정상
// CMD:: curl -X GET http://localhost:3000/origin/22
// RESULT:: This action returns a #22 user%

=> 문자일때는 400에러
// CMD:: curl -X GET http://localhost:3000/users/origin/ww
// RESULT:: {"statusCode":400,"message":"Validation failed (numeric string is expected)","error":"Bad Request"}%

 

2. DefaultValuePipe는 인수의 값에 기본값을 설정할 때 사용. 

// CMD:: curl -X GET http://localhost:3000/origin
// RESULT:: This action returns all users offset:: 0 & limit:: 10%
@Get('/origin')
findAll(
    @Query('offset', new DefaultValuePipe(0), ParseIntPipe) offset: number,
    @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number
) {
    return this.usersService.findAll(offset, limit);
}

 

3. 파이프의 내부 구현 및 이해 

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

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
    // 구현되어야 하는 transform은 두개의 parameter 있음.
    // 1. value: 현재 pipe로 전달되는 인수
    // 2. metadata: 현재 파이프에 전달되는 인수의 메타데이터
      transform(value: any, metadata: ArgumentMetadata): any {
    
         return value;
     }
     
        // ArgumentMetadata  정의
        // type: 파이프에 전달된 param의 종류 (Body, Param, Query)
        // metatype:: 라우트 핸들러에 정의된 인수의 타입/ 핸들러에서 타입을 생략하거나 바닐라 사용시 undefined
        // data:: decorator에 전달된 문자열 => 매개변수의 이름


}

 

// CMD:: curl -X GET http://localhost:3000/origin/pipe/23
// RESULT:: This action returns a #22 user%
// CONSOLE:: { metatype: [Function: Number], type: 'param', data: 'id' }
@Get('/origin/pipe/:id')
findOne_validationPipe(@Param('id', ValidationPipe) id: number) {
    return this.usersService.findOne(+id);
}

 

4. 유효성 검사 파이프 만들기 (using class-validator && class-transformer)

   => npm i --save class-validator class-transformer

 

=> create-user-dto.ts

// NestJS에서 payload 를 처리하기 위해서는 데이터 전송 객체 (DTO)를 구현하면 된다.
import {IsString, MaxLength, MinLength, IsEmail } from "class-validator";


export class CreateUserDto {
    @IsString()
    @MinLength(1)
    @MaxLength(20)
    readonly name: string;


    @IsEmail()
    readonly email: string;

    readonly password: string;

}

 

=> validation.pipe.ts

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

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
    // 구현되어야 하는 transform은 두개의 parameter 있음.
    // 1. value: 현재 pipe로 전달되는 인수
    // 2. metadata: 현재 파이프에 전달되는 인수의 메타데이터
    // before::
    // transform(value: any, metadata: ArgumentMetadata): any {
    //
    //     return value;
    // }

    // after::
    // CMD :: http://localhost:3000/origin/create -H "Content-Type: application/json" -d '{"name": "name_example", "email":"email@example.com"}'
    // RESULT:: {"name":"name_example","email":"email@example.com"}%
    async transform(value: any, { metatype }: ArgumentMetadata) {
        // ArgumentMetadata  정의
        // type: 파이프에 전달된 param의 종류 (Body, Param, Query)
        // metatype:: 라우트 핸들러에 정의된 인수의 타입/ 핸들러에서 타입을 생략하거나 바닐라 사용시 undefined
        // data:: decorator에 전달된 문자열 => 매개변수의 이름

        if (!metatype || !this.toValidate(metatype)) {
            return value;
        }
        //take  entity and turn it into  instant
        const object = plainToClass(metatype, value);
        const errors = await validate(object)
        if (errors.length > 0) {
            throw new BadRequestException('Validation failed');
        }
        return value
    }
    // 설명::
    // 전달된 meta-type 이 파이프가 지원하는 타입인지 검사. (toValidate)
    // class-transformer  plainToClass 함수를 통해 순수 자바스크립트 객체를 클래스의 객체로 변경 => 캡슐화 진행
    // (더이상 리터럴 객체를 다룰 필요 없이 값과 행위가 한곳에 모여있는 클래스 인스턴스 단위로 다룰 수 있게 됩니다.)
    // class-validator 유효성 검사 데커레이터는 타입이 필요. (이를 위해서, plainToClass 사용 )
    // 네트워크 요청을 통해 들어온 데이터는 역직렬화 과정에서 본문의 객체가 아무런 타입 정보도 가지고 있지 않기 떄문에, 타입을 지정하는 변환과정을 plainToClass로 수행
    // 마지막으로 유효성 검사에 통과했다면, 원래 값 그대로 전달  || 실패시 400 에러

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


}

 

 

=> ValidationPipe 적용하기 위해 1) main.ts에 추가 및 2) create핸들러에 적용 

 

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from "./pipe/validation/validation.pipe";
import * as dotenv from 'dotenv';
import * as path from 'path';

// dotenv.config({
//   path: path.resolve(
//       (process.env.NODE_ENV === 'production') ? './env/.production.env'
//           :(process.env.NODE_ENV === 'stage') ? './env/.stage.env' : './env/.development.env'
//   )
// })


async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // ValidationPipe를 모든 핸들러에 일일이 지정하지 않고, 전역으로 설정하려면 부트스트랩 과정에서 적용 => nest에 이미 validation-pipe가 있기 때문에 직접 만들필요 X
     app.useGlobalPipes(new ValidationPipe())

  await app.listen(3000);
}
bootstrap();
@Post('/origin/create')
create(@Body(ValidationPipe) createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto)
}

 

 

5. 유저 서비스에 유효성 검사 적용

   => 기존에 생성한 ValidationPipe은 이해를 위해서 만들고 사용했기 때문에, 이제는 @nestjs/common 에 있는 ValidationPipe사용

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
// import { ValidationPipe } from "./pipe/validation/validation.pipe";
import { ValidationPipe } from "@nestjs/common";
import * as dotenv from 'dotenv';
import * as path from 'path';

// dotenv.config({
//   path: path.resolve(
//       (process.env.NODE_ENV === 'production') ? './env/.production.env'
//           :(process.env.NODE_ENV === 'stage') ? './env/.stage.env' : './env/.development.env'
//   )
// })


async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // ValidationPipe를 모든 핸들러에 일일이 지정하지 않고, 전역으로 설정하려면 부트스트랩 과정에서 적용 => nest에 이미 validation-pipe가 있기 때문에 직접 만들필요 X
  // app.useGlobalPipes(new ValidationPipe())

  // class-transformer를 사용하기 위해서는 transform: true값 지정
  app.useGlobalPipes(new ValidationPipe({
    transform: true
  }))
  await app.listen(3000);
}
bootstrap();

 

6. Class-Transformer 활용.

   => Transform decorator는 transform 함수를 인수로 받고, 여기서 속성의 값과, 그 속성을 가지고 있는 객체를 인수로 받아, 속성 변화하는 함수. 

 

// NestJS에서 payload 를 처리하기 위해서는 데이터 전송 객체 (DTO)를 구현하면 된다.
import {IsString, MaxLength, MinLength, IsEmail } from "class-validator";
import {Expose, Transform} from 'class-transformer'


export class CreateUserDto {
    @Transform(params => {
        // name의 받은 인수가 양옆으로 띄어쓰기 되어있는 경우에 줄여주는 transform 
        return params.value.trim()
    })
    @IsString()
    @MinLength(1)
    @MaxLength(20)
    readonly name: string;


    @IsEmail()
    readonly email: string;

    readonly password: string;

}

 

7. @Transform decorator를 통해 들어오는 param을 변형하여,  조건이 유효하지 않는 경우, throw error를 할 수 있지만, 직접 필요한 검사를 수행하는 decorator를 만들어 사용할 수 있다. 

import {registerDecorator, ValidationArguments, ValidationOptions} from "class-validator";

const NotIn = (property: string, validationOptions?: ValidationOptions) => { //데커레이터의 인수는 객체에서 참조하려고하는 다른 속성의 이름과 ValidationOptions 를 받는다
    return (object: Object, propertyName: string) => { // registerDecorator를 호출하는 함수 리턴 : 인수로 데커레이터가 선언될 객체와 속성이름 받음
        registerDecorator({ // registerDecorator 함수는 ValidationDecoratorOptions 객체를 인수로 받음
            name: 'NotIn', // Decorator 이름
            target: object.constructor, // Decorator는 객체가 생성될때 적용
            propertyName,
            options: validationOptions, // 유효성 옵션은 Decorator의 인수로 전달받은 것을 사용
            constraints: [property],
            validator: { // 유효성 검사 규칙 (id와 pw가 비슷한 경우, ERROR)
                validate(value: any, args: ValidationArguments){
                    const [relatedPropertyName] = args.constraints;
                    const relatedValue = (args.object)[relatedPropertyName]
                    // console.log(args.object) => createUserDTO 값을 음

                    return typeof value === 'string' && typeof relatedValue === 'string' && !relatedValue.includes(value)
                }
            }
        })
    }
}


export default NotIn;

 

 

=> create-user-dto.ts 수정

@Transform(params => {
    return params.value.trim()
})
@NotIn('password', {message: 'password 체크'})
@IsString()
@MinLength(1)
@MaxLength(20)
readonly name: string;

 

 

1. 
cmd:: curl -X POST  http://localhost:3000/origin/create -H "Content-Type: application/json" -d '{"name": "name_example  ", "email":"email@example.com","password":"1234"}'
res:: {"name":"name_example","email":"email@example.com","password":"1234"}%     

2. 
cmd:: curl -X POST  http://localhost:3000/origin/create -H "Content-Type: application/json" -d '{"name": "name_example  ", "email":"email@example.com","password":"name_example_pw"}'
res:: {"statusCode":400,"message":["password 체크"],"error":"Bad Request"}%