NestJS: GraphQL Code First

Dung Tran
7 min readSep 3, 2019

--

Trong bài trước, tôi đã hướng dẫn cách cài đặt GraphQL Schema First trong NestJS. Tiếp tục nội dung về GraphQL, bài này sẽ nói về cách sử dụng Code First trong NestJS

Schema First thì rất dễ dàng trong việc setup một module GraphQL trong NestJS. Tuy nhiên, sẽ gặp một số vấn đề sau:

  1. Việc thay đổi file GraphQL SDL sẽ ảnh hưởng đến các thành phần khác của project. Ví dụ như model được tham chiếu trong các service, controller, hoặc resolver
  2. Phải thay đổi định nghĩa các entity hoặc map các entity cho phù hợp với model của GraphQL
  3. Sự tự động tạo code của NestJS có thể gây bất đồng bộ với các module khác.
  4. Việc sử dụng trong microservices sẽ không thể tái sử dụng các model từ GraphQL

Vì thế cần có một module để generate một file GraphQL schema từ các class của TypeScript trong NestJS. Cộng đồng dùng NestJS đã áp dụng Type-GraphQL cho mục đích trên. Và NestJS v5 đã hỗ trợ Type GraphQL thông qua issues 135. Hiện tại trong NestJS 6 gọi là code-first

Config

Tương tự như bài trước, tôi tiếp tục sử dụng repo bên dưới để hướng dẫn cho bài viết này.

Module GraphQL

Chúng ta dùng Nest CLI để tạo ra module hỗ trợ GraphQL. Sau khi dùng NestJS cli thì chỉ tạo ra thư mục và một file module. Cho nên tự thêm vào file package.json và tsconfig.json.

$ cd packages
$ nest generate mo module-graphql
$ cd module-graphql
$ mkdir src
$ cp module-graphql.module.ts src/

Cài đặt các packages cần thiết cho NestJS GraphQL

yarn add @nestjs/common @nestjs/core @nestjs/graphql apollo-server-express graphql-tools graphql class-validator class-transformer

Cấu hình pakage.json cho repo module-graphql

{
"name": "@mono/module-graphql",
"version": "0.1.0",
"main": "src/module-graphql.module.ts",
"license": "MIT",
"private": true,
"publishConfig": {},
"scripts": {
"generate-barrels": "barrelsby --delete -d src -l top"
},
"dependencies": {
"@mono/interfaces": "0.1.0",
"@nestjs/common": "^6.5.3",
"@nestjs/core": "^6.5.3",
"@nestjs/graphql": "^6.4.2",
"apollo-server-express": "^2.8.2",
"class-transformer": "^0.2.3",
"class-validator": "^0.10.0",
"graphql": "^14.5.0",
"graphql-tools": "^4.0.5"
},
"devDependencies": {
"@types/jest": "^24.0.18",
"barrelsby": "^2.1.1",
"jest": "^24.9.0",
"prettier": "^1.18.2",
"tslint": "^5.19.0",
"tslint-config-prettier": "^1.18.0",
"tslint-plugin-prettier": "^2.0.1",
"typescript": "^3.5.3"
}
}

Cấu hình tsconfig.json cho module-graphql

//tsconfig.json
{
"extends": "../../tsconfig.json",
"exclude": ["node_modules", "dist"]
}

Nest Application

Thêm repo module-graphql vào package.json của repo server

//server/package.json"dependencies": {
"@mono/entities": "0.1.0",
"@mono/interfaces": "0.1.0",
"@mono/models": "0.1.0",
"@mono/module-database": "0.1.0",
"@mono/module-graphql": "0.1.0", // add module-graphql
"@nestjs/common": "^6.5.3",
"@nestjs/core": "^6.5.3",
"@nestjs/platform-express": "^6.5.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.0",
"rxjs": "^6.5.2"
},

Cài đặt module-graphql vào repo server

$ lerna bootstrap

hoặc

$ yarn bootstrap

Cấu hình tsconfig.json trong repo server

//server/tsconfig.json
"references": [
{
"path": "../module-graphql/src"
}
]

Nhét module-graphql vào NestJS application

//packages/server/src/app.module.ts
import {ModuleGraphqlModule} from "@mono/module-graphql";
@Module({
imports: [ModuleGraphqlModule],
controllers: [],
providers: [],
})
export class AppModule {}

Coding

Định nghĩa những common object

  1. Interfaces

Chúng ta tạo repo interfaces để định nghĩa các interfaces cho model trong project

//interfaces/src/*export interface IAuthor {
id: string
name: string
}
export interface IPost {
id: string
title: string
content: string
}

2. Model

Tạo một repo định nghĩa model

//models/src/Authorimport {IAuthor} from "@mono/interfaces";
import {Post} from "./Post";

export abstract class Author implements IAuthor {
id: string;
name: string;
posts: Post[];

}
//models/src/Postimport {IPost} from "@mono/interfaces";
import {Author} from "./Author";

export abstract class Post implements IPost {

id: string;
title: string;
content: string;
author: Author;

}

3. Entities

Tạo một repo định nghĩa các entities, tương tự như các repo trên tạo các file entities trong thư mục src

//entities/src/AuthorEntityimport { Author} from "@mono/models";
import {Column, Entity, OneToMany, PrimaryGeneratedColumn} from 'typeorm'
import {PostEntity} from "./Post";

@Entity({name: "authors"})
export class AuthorEntity extends Author {
@PrimaryGeneratedColumn("increment")
id: string
@Column()
name: string
@OneToMany(type => PostEntity, (post: PostEntity) => post.author)
posts: PostEntity[]


}
//entities/src/PostEntityimport {Post} from '@mono/models';
import {Column, Entity, ManyToOne, PrimaryGeneratedColumn} from 'typeorm'
import {AuthorEntity} from "./Author";

@Entity({name: "posts"})
export class PostEntity extends Post {
@PrimaryGeneratedColumn("increment")
id: string;
@Column()
content: string;
@Column()
title: string;
@ManyToOne(type => AuthorEntity, (author: AuthorEntity) => author.posts)
author: AuthorEntity
}

Cấu hình các file package.json và tsconfig.json cho các repo trên

//package.json
{
"name": "@mono/entities", // repo name
"version": "0.1.0",
"main": "src/index.ts",
"license": "MIT",
"scripts": {
"generate-barrels": "barrelsby --delete -d src -l top"
},
"dependencies": {
"@mono/interfaces": "0.1.0",
"@mono/models": "0.1.0",// import repo cần thiết
"typeorm": "^0.2.18"
},
"devDependencies": {
"@types/jest": "^24.0.18",
"barrelsby": "^2.1.1",
"jest": "^24.9.0",
"prettier": "^1.18.2",
"tslint": "^5.19.0",
"tslint-config-prettier": "^1.18.0",
"tslint-plugin-prettier": "^2.0.1",
"typescript": "^3.5.3"
}
}
// tsconfig.json{
"extends": "../../tsconfig.json",
"exclude": [
"node_modules",
"dist"
],
"references": [
{
"path": "../interfaces" // import repo cần thiết
},
{
"path": "../models"
}
]
}

Cấu hình TypeORM cho project

Sau khi định nghĩa các entities thì cần khai báo trong TypeORMModule của NestJS

import {Module} from '@nestjs/common';
import {TypeOrmModule} from '@nestjs/typeorm';
import {PostEntity, AuthorEntity} from '@mono/entities'

@Module({
imports: [TypeOrmModule.forRoot({
...
entities: [PostEntity,AuthorEntity],
...
})]
})
export class ModuleDatabaseModule {
}

Chúng ta có thể import các tất cả entities bằng cách :

entities: [../entities/**/*.entity{ts|js}],

hoặc

import {Module} from '@nestjs/common';
import {TypeOrmModule} from '@nestjs/typeorm';
import * as Entities from '@mono/entities'

@Module({
imports: [TypeOrmModule.forRoot({
...
entities: [...Entities],
...
})]
})
export class ModuleDatabaseModule {
}

Định nghĩa GraphQL

Tạo một module cho Author để quản lý các vấn đề liên quan đến author, như trong bài GraphQL Schema First tôi tạo post-module trong repo module-graqhql. Nhưng trong bài này tôi sẽ tách nó ra thành repo riêng, điều này giúp có thể tái sử dụng repo trong nhiều NestJS Application

mkdir module-author

Chúng ta cấu hình module author như hình bên dưới

Module Author Graphql Code First

Cài đặt các packages cần thiết cho module-author

$ yarn add @mono/interfaces @mono/models @mono/entities @mono/repositories @mono/module-post @nestjs/typeorm typeorm @nestjs/common @nestjs/core @nestjs/graphql apollo-server-express class-transformer class-validator type-graphql reflect-metadata graphql typescript

Chúng ta cấu hình package.jsontsconfig.json cho module-author

{
"name": "@mono/module-author",
"main": "src/author.module.ts",
"dependencies": {
...
"type-graphql": "^0.17.5",
"graphql": "^14.5.0"
},
...
}

Cách cài đặt không khác gì so với GraphQL Schema First chỉ thêmtype-graphql”. Module này sẽ chuyên đổi các class định nghĩa ra GraphQL SDL.

Định nghĩa GraphQL SDL trong code, code bên dưới tôi tạo các class kế thừa từ interfaces, class từ models và định nghĩa các fieldstypes

//author.type.tsimport {Author as AuthorModel} from "@mono/models"
import {IAuthor} from "@mono/interfaces";
import {Field, ID, ObjectType, InterfaceType, InputType} from "type-graphql";
import {Post} from '@mono/module-post'

@InterfaceType()
export abstract class AAuthor implements IAuthor {
@Field(type => ID)
id: string
@Field()
name: string;
}

@ObjectType({implements: AAuthor})
export class Author extends AuthorModel {
@Field(type => Post)
posts: Post[]
}

@InputType()
export class CreateAuthorInput extends AuthorModel {
@Field()
name: string;
}

Trong đoạn code định nghĩa 3 loại type của graphql là input type, object typeinterface type. Cái class định nghĩa các field dùng decorator Field, thông thường thì các loại primitive types không cần khai báo trong Field, TypeGraphQL tự xác định được.

Tiếp theo sẽ tạo resolver cho GraphQL để định nghĩa 3 loại type: query type, mutation type subscription type.

//author.resolver.tsimport {Args, Mutation, Query, Resolver, Subscription, ResolveProperty} from '@nestjs/graphql';
import {PubSub} from 'apollo-server-express';
import {CreateAuthorInput, Author} from './author.type'
import {Inject} from "@nestjs/common";
import {AuthorService} from "./author.service";
import {FieldResolver, Root} from "type-graphql";

const pubSub = new PubSub();

@Resolver(of => Author)
export class AuthorResolver {

constructor(private readonly service: AuthorService) {
}

@Query(returns => [Author])
async getAuthors(): Promise<Author[]> {
const authors = await this.service.getAuthors()
return authors
}

@Mutation(returns => Author)
async createAuthor(@Args('input') args: CreateAuthorInput): Promise<Author> {
const author = await this.service.createAuthor(args);
pubSub.publish('createdAuthor', {createdAuthor:author})
return author
}

@Subscription(returns => Author)
async createdAuthor() {
return pubSub.asyncIterator('createdAuthor')
}

}

Tạo file service để tương tác với database. Trong service sẽ inject một repository để tương với database, thường class repositorry sẽ nằm chung module, nhưng tôi move ra một repo riêng để tái sử dụng cho các module khác.

//author.service.tsimport {Inject, Injectable} from '@nestjs/common';
import {AuthorRepository, PostRepository} from "@mono/repositories";
import {Author as AuthorModel} from "@mono/models"
import {InjectRepository} from '@nestjs/typeorm'
import {AuthorEntity, PostEntity} from "@mono/entities";
import {CreateAuthorInput} from './author.type';

@Injectable()
export class AuthorService {

constructor(
@InjectRepository(AuthorEntity)
private readonly repository: AuthorRepository,
@InjectRepository(PostEntity)
private readonly repositoryPost: PostRepository
) {
}

async getAuthors() {
return this.repository.find()
}

async createAuthor(arg: CreateAuthorInput) {
const author = this.repository.create(arg)
return this.repository.save(author)
}

async findPostsByAuthor(author: AuthorModel) {
return this.repositoryPost.find({relations: ['author'], where: {author: author}})
}
}

Như ta thấy thì trong file author.type.ts, Author có một liên kết với Post, để trả về giá trị của posts trong author ta có thể làm 2 cách sau:

Thêm relations trong find function của repository

async getAuthors() {
return this.repository.find({relations:['post']})
}

hoặc thêm vào resolve function cho field post trong GraphQL, trong nội dung bài này tôi sử dụng cách này


// author.resolver.ts
@ResolveProperty()
async posts(@Root() author: Author) {
return this.service.findPostsByAuthor(author)
}

Trong TypeGraphQL thì @FieldResolver() được dùng để custom một field trong graphQL

@FieldResolver()   
averageRating(@Root() recipe: Recipe) {
const ratingsSum = recipe.ratings.reduce((a, b) => a + b, 0); return recipe.ratings.length?ratingsSum/recipe.ratings.length: null;
}

Nhưng trong NestJS thì @FieldResolver() được chuyển thành @ResolveProperty()

Đến đây chúng ta hoàn tất việc implement cho module-author, bây giờ chúng ta khai báo cho module-graphql và server tương tự như trong GraphQL Schema First

import {AuthorModule} from "@mono/module-author";
@Module({
imports: [AuthorModule,
GraphQLModule.forRoot({
autoSchemaFile: 'schema.gql',
installSubscriptionHandlers: true
})]
})
export class ModuleGraphqlModule {
}

Bên dưới là link codesandbox.io để của tutorial, truy cập vào GraphQL playground bởi /graphql

GraphQL Code First on codesandbox.io

Testing

Các bạn tham khảo bài viết về GraphQL Schema First, cấu hình tương tự như bài viết trước.

Kết luận

Code-First khá rườm rà lúc ban đầu nhưng lại có ích cho việc mở rộng về sau. Các thay đổi các model không ảnh hưởng nhiều đến hệ thống. Các file GraphQL SDL độc lập với các class model và việc thay đổi GraphQL không ảnh hưởng đến entities hay models

Các module có share các file GraphQL SDL với nhau và tái sử dụng trong nhiều NestJS Application

--

--