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:
- 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
- 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
- Sự tự động tạo code của NestJS có thể gây bất đồng bộ với các module khác.
- 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
- 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
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.json và tsconfig.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êm “type-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 fields và types
//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 type và interface 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 và 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
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