- Rest API = Moudule + Controller + Service
- GraphQL API = Moudule + Resolver + Service + (Controller)
이게 일반적이지만, gql은 /graphql 경로 하나만 사용하므로 다른 경로를 사용하고 싶다면 Controller도 같이 사용하곤 합니다. 의외로 흔한 구성입니다.
Nest + gql + typeORM(@nestjs/typeorm) 프로젝트 플로우
1. 프로젝트 전체의 내용물을 분석하여 Module로 쪼갠다.
2. Module 별로 다음과 작은 작업을 진행한다.
0. ERM을 짠다. NoSQL이라도 ERM은 필요하다. 완벽할 필요는 없고 가안이라도 짜자.
1. code first 방식으로 사용할 gql schema/entity를 짠다. data mapping 방식으로 짜자. @Entity(), @ObjectType()
2. resolver를 짠다.
3. service에 entity를 inject하여 사용한다.
이 과정에서 특히 신경써야할 부분, 혹은 헷갈릴 수 있는 부분은
- gql schema/entity를 짬에 있어 각 데코레이터들이 어디에서 온 것이가를 확실히 알아두라는 것 (@Field는 gql의 문법, @Column은 typeORM의 문법)
- Resolver 를 작성함에 있어 dto를 만들어 활용할 것. nest에서 제공해주는 Mapped Type을 사용하면 좋음
- resolver의 인자에 @InputType를 쓴다면 @Args에 이름이 있어야 한다. 반면 @ArgsType을 쓴다면 @Args에 이름이 없어도 된다
- 각 entity들은 scope 상의 Module에도 주입해야하지만 AppModule에도 따로 주입해야 한다는 점
- validator들이 작동하기 위해서는 GlobalValidationPipe를 AppModule에 넣어줘야 한다는 점
1~3을 실행하는 구체적인 코드는 아래와 같다.
1. entity 정의, validation 사용하기
import { Field, ObjectType } from '@nestjs/graphql';
import { IsString } from 'class-validator';
import { CoreEntity } from 'src/common/entities/core.entity';
import { Column, Entity } from 'typeorm';
@ObjectType()
@Entity()
export class User extends CoreEntity {
@Field(() => String) // gql
@Column() // typeorm
@IsString() // class-validatior
email: string;
@Field(() => String)
@Column()
@IsString()
password: string;
}
2. resolver에서 @Query, @Mutation 등 정의
import { Query, Resolver } from '@nestjs/graphql';
import { User } from './entities/user.entity';
import { UserService } from './users.service';
@Resolver((of) => User)
export class UserResolver {
constructor(private readonly userService: UserService) {}
@Query()
...
}
3. Service에서 비즈니스 로직 작성, Repository를 Inject하여 사용
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private readonly user: Repository<User>,
) {}
}
1~3을 한 후 해당 scope의 Module에서 하나의 응집하기
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UserResolver } from './users.resolvers';
import { UserService } from './users.service';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UserResolver, UserService],
})
export class UsersModule {}
이렇게 하나의 Module은 의미론적인 단위가 된다. 해당 Module을 모아서 AppModule에 모으는 것이 Nest의 동작 방식이다.
=======
ArgsType => 수작업으로 arguments를 만들고자할 때
import { ArgsType, Field } from '@nestjs/graphql';
import { IsBoolean, IsString, Length } from 'class-validator';
@ArgsType()
export class CreateRestaurantDto {
@Field(() => String)
@IsString()
@Length(5, 10)
name: string;
}
InputType => MappedType을 이용해 DTO 만들 때 좋음
@InputType()
export class CreateRestaurantDto extends OmitType(Restaurant, ['id'], InputType) {}
====
resolver에 @InputType을 args로 쓴다면 @Args에 이름이 있어야 한다. DTO 때문에 다들 이 방식을 사용한다.
gql에서 input: {name: ..., age: ...} 이런 꼴로 사용해야해서 조금 귀찮지만 수작업으로 input을 @ArgsType으로 만드는 것보다는 빠르다.
@Mutation(() => Boolean)
async createRestaurant(@Args('input') createRestaurantDto: CreateRestaurantDto): Promise<boolean> {
try {
await this.restaurantService.createRestaurant(createRestaurantDto);
return true;
} catch (error) {
console.log(error.message);
return false;
}
}
반면 @ArgsType을 args로 쓴다면 @Args에 이름이 없어도 된다. 곧바로 {name:..., age:...} 꼴로 사용할 수 있어서 편하긴하다. 그러나 @ArgsType을 수작업할 생각을하니...
@Mutation(() => Boolean)
createRestaurant(@Args() createRestaurantDto: CreateRestaurantDto): boolean {
console.log(createRestaurantDto);
return true;
}
Rest API > Graphql migration
1. GraphqlModule을 appModule에 import한다
2. 기존 entity 정의를 InputType, ObjectType, @Field를 이용하여 정의하여 준다
(* Mapped Type 이용을 위해 InputType에 isAbstract를 true로 놓고, 이름을 정해주자)
3. Controller를 Resolver로 바꿔준다
하면서 배운 것
(1) Query, PickType 류는 전부 @nestjs/graphql 에서 export되어야 한다. dns, @nestjs/common 에서 export되면 의도대로 동작하지 않으니 주의하자
(2) @InputType으로 만들어 놓고서 막상 Resolver에서 넣으려고 할 때 @Args('input') 이걸 자꾸 까먹는다... @ArgsType도 아닌데...
Jest Unit Test
모듈이 사용하는 부분을 그대로 테스트 환경 조성해야 한다.
사용하는 DB, 외부 라이브러리를 모두 Mocking + spy func 해야 한다.
mocking (1) : darrengwon.tistory.com/1004?category=915252
mocking (2) : darrengwon.tistory.com/1046?category=915252
spy : darrengwon.tistory.com/1047
이 때, mocking한 값들을 테스트 모듈 내에 넣어줄때, 같은 mocking을 공유하지 않도록 함수 형태로 전달해서 리턴값을 사용하도록 만들자
const mockRepository = () => ({
findOne: jest.fn(),
save: jest.fn(),
create: jest.fn(),
findOneOrFail: jest.fn(),
delete: jest.fn(),
});
... test module
{ provide: getRepositoryToken(User), useValue: mockRepository() },
{ provide: getRepositoryToken(Verification), useValue: mockRepository() },
... spy
jest
.spyOn(service, 'getPodcast')
.mockImplementation(async id => ({ ok: true, error: null }));
이후에는 유닛 테스팅을 진행해주면 된다. 주로 다음 메서드를 사용하면서 진행한다.
1. 특정 메서드가 불렸는지, 인자는 뭐가 들어왔는지 체킹한다.
toHaveBeenCalledTimes, toHaveBeenCalledWith
expect(userRepository.create).toHaveBeenCalledTimes(1); // 함수가 1번 불릴 것으로 예상함 (mockRepository를 공유하지 않아야 함)
expect(userRepository.create).toHaveBeenCalledWith(createAccountArgs); // create 메서드일 때 들어올 인자 체킹
2. DB는 다음 메서드를 통해 실 DB를 건드리지 않고 반환값을 조작해야 한다.
mockResolvedValue, mockRejectedValue,
mockReturnValue(Promise가 아닐 경우에 사용. 이름이 비슷해서 종종 잊곤 하지만 중요함.)
// mockResolvedValue
const mockedUser = { id: 1, checkPassword: jest.fn(() => Promise.resolve(true)) };
userRepository.findOne.mockResolvedValue(mockedUser);
// mockRejectedValue
userRepository.findOneOrFail.mockRejectedValue(new Error());
// createPodcastInput
podcastRepository.create.mockReturnValue(createPodcastInput);
3. 반환한 값에 와야할 값을 expect 하면서 실제 서비스 코드와 테스트 코드를 교차 확인한다.
toEqual, toMatchObject
expect(result).toEqual({ ok: false, error: 'User Not Found' });
'Node, Nest, Deno > 🦁 Nest.js' 카테고리의 다른 글
Task Scheduling(Cron, interval, timeout) in Nest (0) | 2021.01.06 |
---|---|
Custom param decorators (0) | 2020.11.24 |
Guard + MetaData (0) | 2020.11.24 |
Nest Middleware (0) | 2020.11.24 |
dynamic-modules 만들기 (0) | 2020.11.24 |