NestJs 복습 및 정리

Harry·2020년 11월 12일
7

NestJs

목록 보기
1/2

GraphQL

Query, Mutation Type

  • 어떠한 로직이 있는 게 아니라, 타입 을 선언해주는 것

resolver

  • 어떠한 타입이 선언되었으니, 해당 선언에 대한 로직을 만들어주는 곳

DTO

controller 단에서 request 들어오는 데이터에 대한 타입을 지정하는 데에 사용
→ 물론 유효성 검사까지 진행 가능

  • DTO 에서 유효성 검사를 진행해도 되지만 Entity 파일에서도 가능하다.
  • class 상단에 @InputType() 혹은 @ArgsType() 를 사용

@InputType() / 클래스 상단에 사용

import { InputType } from '@nestjs/graphql';
  • 해당 DTO를 사용하는 Resolver 에서는 @Args("apple") 해당 객체에 대한 이름을 반드시 넣어주어야 한다.
  • 해당 Resolver@Args("apple") 명시해준 이름을 통해 하나의 객체가 들어오게 된다.
    • apple이라는 object가 들어오게 되는 것. 그 내부의 데이터는 DTO로 명시해준 타입에 맞게끔 들어오게 된다.

@ArgsType() / 클래스 상단에 사용

import { ArgsType } from '@nestjs/graphql';
  • 해당 DTO을 사용하는 Resolver 에서 @Args() 이름을 넣어줄 필요가 없다.
  • 해당 Resolver 는 여러 개의 인자를 받게 된다. ( 단 하나의 객체만 받는 게 아니라는 것 )
    • DTO에 명시해준 key값이 결국엔 인자들의 이름이다.

@Field(type ⇒ ) / 클래스 내부 변수에 사용

import { Field } from '@nestjs/graphql';
  • 우리는 GraphQL을 사용하기 때문에 GraphQL 로부터 Field 를 가져와 해당 key값이 어떤 타입인 지를 지정해준다.

Example Code)

// 밑의 클래스에 하나의 필드에 대해 정의하기 위한 Interface
@InputType()
class UpdateRestaurantInputType extends PartialType(CreateRestaurantDto) {}

// 실제로 요청들어오는 데이터에 대한 Interface
@ArgsType()
export class UpdateRestaurantDto {
  @Field(type => Number)
  id: number;
  @Field(type => UpdateRestaurantInputType)
  data: UpdateRestaurantInputType;
}

Entity를 통해 DTO 만들기

Documentation | NestJS - A progressive Node.js framework

  • DTO를 하나씩 다 짜줄 필요는 없다 → 기존에 존재하는 Entity 를 통해 DTO를 만들 수 있다.
  • Entity 를 통해 DTO 를 만들 때에는 Nestjs가 제공해주는 Type class를 상속받으면 된다.

Example Code)

// 밑의 클래스에 하나의 필드에 대해 정의하기 위한 Interface
@InputType()
class UpdateRestaurantInputType extends PartialType(CreateRestaurantDto) {}

// 실제로 요청들어오는 데이터에 대한 Interface
@ArgsType()
export class UpdateRestaurantDto {
  @Field(type => Number)
  id: number;
  @Field(type => UpdateRestaurantInputType)
  data: UpdateRestaurantInputType;
}

Entity

  1. GraphQL에서 Type을 지정하기 위함
  2. DB에서 Table Schema 를 지정하기 위함
  3. 또한, Entity를 통해서 유효성 검사 가능
  • 총 3가지를 위해서 Entity 를 이용하지만, 기본적으로 DB Table Schema를 위해서 만드는 위함이 제일 큰 듯 싶다 ( DB Table Schema 위주로 Entity 를 짜는 듯 )
  • 3가지를 모두 Entity 에서 사용 가능하다.

Exmaple Code)

import { ObjectType, Field, InputType, ID } from '@nestjs/graphql';
import { IsString, IsBoolean, Length, IsOptional } from 'class-validator';
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@InputType({ isAbstract: true })
@ObjectType() // for GraphQL
@Entity() // for TypeORM
export class Restaurant {
  @Field(type => ID)
  @PrimaryGeneratedColumn()
  id: number;
  @Field(type => String) // for GraphQL
  @Column() // for typeORM
  @IsString()
  @Length(5)
  name: string;
  @Field(type => Boolean, { defaultValue: true }) // for GraphQL
  @Column({ default: true }) // for typeORM
  @IsBoolean()
  @IsOptional()
  isVegan: boolean;
  @Field(type => String) // for GraphQL
  @Column() // for typeORM
  @IsString()
  address: string;
}

Entity를 언제 graphql type으로 생성해줄까?

  • Entity 에서 GraphQL을 위한 decorator를 사용했다고 Type을 생성해주는 것은 아니다.
  • resolver에서 해당 Entity Class 를 사용해야 type으로 생성해준다
  • 즉, 사용을 할 경우에 type으로 만들어준다.

GraphQL에서의 defaultValue

  • 해당 Entity를 통해 DTO를 만들고 이 DTO를 어떠한 resolver 의 요청 데이터로 사용하게 될 때, defaultValue로 설정해놓은 값이 해당 변수에 들어가진다.
  • 이렇게 되면, 내가 값을 넣어주지 않아도 알아서 defaultValue에 적혀진 값을 할당해주고, 이 값을 DB를 통해 사용한다
    • DB에 default를 쓸 필요가 없다라는 뜻

DB에서의 default

  • DB에 저장할 때 값을 지정해주지 않을 경우, default 에 넣어준 값이 할당된다.
  • GraphQL 에 defaultValue를 설정해주지 않을 경우, undefined 가 들어오게 될텐데 이럴 때 DB의 default로 설정한 값이 들어가게 된다.

Resolver

  • 기존에는 Query Type, Mutation Type 을 선언해준 다음에 resolver를 만들어 주었지만 NestJs 같은 경우 resolver.ts 파일 하나로 이 모든 걸 해줄 수 있다 → schema.graphql 을 알아서 만들어주기 떄문!
@Resolver(of => Restaurant)
export class RestaurantResolver {
	// service를 사용하기 위해선 construcor 함수의 매개변수로 넣어주어야 합니다!
	// 이후 이 class 내부의 모든 함수에선 this 키워드를 통해 service에 접근이 가능합니다.
  constructor(private readonly restaurantService: RestaurantService) {}
  @Query(returns => [Restaurant])
  restaurants(): Promise<Restaurant[]> {
    return this.restaurantService.getAll();
  }
  @Mutation(returns => Boolean)
  async createRestaurant(
    @Args('input') createRestaurantDto: CreateRestaurantDto,
  ): Promise<boolean> {
    try {
      await this.restaurantService.createRestaurant(createRestaurantDto);
      return true;
    } catch (error) {
      console.log(error);
      return false;
    }
  }
  @Mutation(returns => Boolean)
  async updateRestaurant(
    @Args() updateRestaurantDto: UpdateRestaurantDto,
  ): Promise<boolean> {
    try {
      await this.restaurantService.updateRestaurant(updateRestaurantDto);
      return true;
    } catch (error) {
      console.log(error);
      return false;
    }
  }
}
  • Controller 와 같은 역할
  • Resolver class 에서 Query 혹은 Mutation 을 데코레이터로 지정하여 resolver 를 만들어준다.
    • Nest가 Query OR Mutation 에 대한 schema는 자동으로 생성해줌
  • @Resolver(of => Restaurant) 에서 of는 void의 타입 ( 아무런 뜻이 없음 → 가독성을 위해 적는다. ) 또한, return type의 경우 Function 의 타입인데, 해당 함수도 별 다른 의미가 존재하지 않음

@Query

  • @Query(returns => [Restaurant]) 를 통해 schema를 생성해준다.
    • @Query() 데코레이터를 손 대지 않고, 아래의 함수 return type을 boolean으로 바꿨을 경우에도 schema는 데코레이터에 작성해준 return type으로 생성된다.
  • Query에 대한 이름은 함수의 이름으로 생성된다.
  • 생성되는 Query Schema는 아래와 같다.
type Restaurant {
  id: ID!
  name: String!
  isVegan: Boolean!
  address: String!
}

type Query{
	restaurants => [Restaurant]
}

@Mutation

  • @Mutation(returns => Boolean) 을 통해 schema를 생성한다.
  • 위와 동일

validate object

npm i joi

Example code)

import * as Joi from 'joi'
ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: process.env.NODE_ENV === 'dev' ? '.env.dev' : '.env.test',
      // if NODE_ENV is prod, set environement variable elsewhere
      ignoreEnvFile: process.env.NODE_ENV === 'prod',
      validationSchema: Joi.object({
        NODE_ENV: Joi.valid('dev', 'test', 'prod').required(),
        TYPEORM_CONNECTION: Joi.string().required(),
        TYPEORM_HOST: Joi.string().required(),
        TYPEORM_USERNAME: Joi.string().required(),
        TYPEORM_PASSWORD: Joi.string().required(),
        TYPEORM_DATABASE: Joi.string().required(),
        TYPEORM_PORT: Joi.number().required(),
        TYPEORM_SYNCHRONIZE: Joi.boolean().required(),
        TYPEORM_LOGGING: Joi.boolean().required(),
      }),
    }),

Service

  • 항상 모든 비즈니스 로직은 Service 파일의 클래스 내부에 선언한다.
  • 사용하고자 하는 DB Table 을 import 한다.

DB Repository 사용 방법

  1. DB Table 을 이용하기 위해선 module 파일에서 해당 Repository를 import 해주어야 한다.

    // ~.module.ts
    @Module({
      **imports: [TypeOrmModule.forFeature([Restaurant])],**
      providers: [RestaurantResolver, RestaurantService],
    })
  2. 이제 Repository를 import 했으니, 이걸 Service 에서 사용할 수 있게 providers 에 넣어준다.

    // ~.module.ts
    @Module({
      imports: [TypeOrmModule.forFeature([Restaurant])],
      **providers: [RestaurantResolver, RestaurantService],**
    })
    export class RestaurantsModule {}
  3. 사용하고자 하는 Service의 constructor 함수에서 해당 Repository를 Inject 해주자!

    import { Injectable } from '@nestjs/common';
    import { Restaurant } from './entities/restaurant.entity';
    import { InjectRepository } from '@nestjs/typeorm';
    import { Repository } from 'typeorm';
    
    @Injectable()
    export class RestaurantService {
      constructor(
        @InjectRepository(Restaurant)
        private readonly restaurants: Repository<Restaurant>,
      ) {}
    }
  4. 이제 Service 내부의 모든 함수에서 this 키워드를 통해 해당 Repository에 접근이 가능하다.

  5. Resolver 에서 Service 에 접근하기 위해서는 Resolverconstructor 함수 매개변수로 해당 Service 를 적어주면 된다.

    import { Resolver } from '@nestjs/graphql';
    import { Restaurant } from './entities/restaurant.entity';
    import { RestaurantService } from './restaurants.service';
    
    @Resolver(of => Restaurant)
    export class RestaurantResolver {
      constructor(private readonly restaurantService: RestaurantService) {}
    
    }

TypeORM 에서 save 와 create 의 차이점

// just create class not access to db
const user = repository.create(); 
// same as
const user = new User();
const user = repository.create({
    id: 1,
    firstName: "Timber",
    lastName: "Saw"
}); 
// same as
const user = new User();
user.firstName = "Timber"; user.lastName = "Saw";
  • 위의 코드만 작성했을 경우, 해당 User에 대한 데이터들은 DB에 저장되지 않는다. → 그냥 단지 class만 만들어 줬으니까
  • 이를 저장하기 위해선 마지막에 save 함수를 사용해주어야 한다.
const user = repository.create({
    id: 1,
    firstName: "Timber",
    lastName: "Saw"
});
await repository.save(user)
// same as
const user = new User();
user.firstName = "Timber"; user.lastName = "Saw";
await repository.save(user)

Entity를 이용하여 DTO를 생성하는 법

첫 번째 방법

Nest Documentation link

import { InputType, OmitType } from '@nestjs/graphql';
import { Restaurant } from '../entities/restaurant.entity';

@InputType()
export class CreateRestaurantDto extends OmitType(
  Restaurant,
  ['id'] as const,
  InputType,
) {}
  • 3번째로 넘기는 인자는 해당 class의 타입을 지정해주는 것.
  • 인자를 넘기지 않으면 기본적으로 parent class 의 type 을 따라간다
    • Restaurant의 타입은 ObjectType 이기 때문에, 현재 상속받은 child class는 ObjectType 을 따라가게 된다.
  • 하지만 resolver단에서 Dto를 사용하기 위해선 InputType 이여야 하기 때문에, InputType 으로 바꿔준 것.

두 번째 방법

import { InputType, OmitType } from '@nestjs/graphql';
import { Restaurant } from '../entities/restaurant.entity';

@InputType()
export class CreateRestaurantDto extends OmitType(
  Restaurant,
  ['id'] as const,
) {}
import { ObjectType, Field, InputType, ID } from '@nestjs/graphql';
import { IsString, IsBoolean, Length, IsOptional } from 'class-validator';
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@InputType({ isAbstract: true })
@ObjectType() // for GraphQL
@Entity() // for TypeORM
export class Restaurant {
  @Field(type => ID)
  @PrimaryGeneratedColumn()
  id: number;
  @Field(type => String) // for GraphQL
  @Column() // for typeORM
  @IsString()
  @Length(5)
  name: string;
  @Field(type => Boolean, { defaultValue: true }) // for GraphQL
  @Column({ default: true }) // for typeORM
  @IsBoolean()
  @IsOptional()
  isVegan: boolean;
  @Field(type => String) // for GraphQL
  @Column() // for typeORM
  @IsString()
  address: string;
}
  • InputType을 schema에 적용시키는 것은 원하지 않는다.
  • 해당 클래스를 사용하거나, 이것을 상속받거나 직접 사용할 경우에 InputType으로 적용된다(?)
  • 고로, OmitType 세번째 인자로 InputType 을 넘기지 않아도 된다.

Resolver Args

  • 여러 개의 @Args 를 쓰는 것보단 한 개의 @Args 만 사용해서 가독성을 높이자.
  • 여러 개의 @Args 보단
// resolver
@Mutation(returns => Boolean)
  async updateRestaurant(
    @Args('id') id: number,
    @Args('input') data: UpdateRestaurantDto,
  ): Promise<boolean> {
    return true;
  }
  • 단 한 개의 @Args 만 사용
// resolver
@Mutation(returns => Boolean)
async updateRestaurant(
  @Args() updateRestaurant: UpdateRestaurantDto,
): Promise<boolean> {
  return true;
}
// dto
import { InputType, PartialType, Field, ArgsType } from '@nestjs/graphql';
import { CreateRestaurantDto } from './create-restaurant.dto';

// 밑의 클래스에 하나의 필드에 대해 정의하기 위한 Interface
@InputType()
class UpdateRestaurantInputType extends PartialType(CreateRestaurantDto) {}

// 실제로 요청들어오는 데이터에 대한 Interface
@ArgsType()
export class UpdateRestaurantDto {
  @Field(type => Number)
  id: number;
  @Field(type => UpdateRestaurantInputType)
  data: UpdateRestaurantInputType;
}

@InputType 을 사용할 경우, resolver에서 반드시 @Args(' ... ') 이름을 넣어주자!

0개의 댓글