230105목_공부일지
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"}%