Cảnh báo Chương này chỉ áp dụng cho cách tiếp cận code first.
Hệ thống metadata reflection của TypeScript có một số giới hạn khiến nó không thể, ví dụ, xác định một class gồm những property nào hoặc nhận biết một property có phải là optional hay required hay không. Tuy nhiên, một số giới hạn này có thể được giải quyết tại thời điểm biên dịch. Nest cung cấp một plugin giúp tăng cường quá trình biên dịch TypeScript để giảm lượng code khung (boilerplate) cần viết.
Gợi ý Plugin này là tùy chọn (opt-in). Nếu muốn, bạn vẫn có thể tự khai báo tất cả decorator thủ công, hoặc chỉ khai báo một số decorator cụ thể khi cần.
Ghi chú người dịch Đây chính là cơ chế tương tự “code generation” mà nhiều framework Go dùng (ví dụ đọc struct tag rồi sinh code) — chỉ khác là thay vì chạy một bước generate riêng, plugin này gắn thẳng vào compiler TypeScript, chạy tại thời điểm biên dịch (build time) để phân tích AST và tự thêm decorator vào, thay vì bạn phải viết tay từng cái một.
Tổng quan
Plugin GraphQL sẽ tự động:
- gắn
@Fieldvào tất cả property của class input object, object type và args, trừ khi dùng@HideField - set property
nullabledựa trên dấu hỏi chấm (ví dụname?: stringsẽ setnullable: true) - set property
typedựa trên kiểu dữ liệu (hỗ trợ cả mảng) - sinh mô tả (description) cho property dựa trên comment (nếu
introspectCommentsđược set làtrue)
Lưu ý rằng tên file của bạn phải có một trong các hậu tố sau để được plugin phân tích: ['.input.ts', '.args.ts', '.entity.ts', '.model.ts'] (ví dụ, author.entity.ts). Nếu bạn dùng hậu tố khác, bạn có thể điều chỉnh hành vi của plugin bằng option typeFileNameSuffix (xem bên dưới).
Với những gì ta đã học được từ trước tới giờ, bạn phải viết trùng lặp khá nhiều code để package biết cách type của bạn nên được khai báo trong GraphQL như thế nào. Ví dụ, bạn có thể định nghĩa một class Author đơn giản như sau:
@ObjectType()
export class Author {
@Field(type => ID)
id: number;
@Field({ nullable: true })
firstName?: string;
@Field({ nullable: true })
lastName?: string;
@Field(type => [Post])
posts: Post[];
}
Điều này chưa hẳn là vấn đề lớn với các dự án cỡ vừa, nhưng nó sẽ trở nên dài dòng và khó bảo trì khi bạn có một tập lớn các class.
Bằng cách bật plugin GraphQL, định nghĩa class ở trên có thể được khai báo đơn giản như sau:
@ObjectType()
export class Author {
@Field(type => ID)
id: number;
firstName?: string;
lastName?: string;
posts: Post[];
}
Plugin sẽ tự động thêm các decorator phù hợp ngay khi biên dịch, dựa trên cây cú pháp trừu tượng (Abstract Syntax Tree). Nhờ đó, bạn sẽ không phải vật lộn với các decorator @Field rải rác khắp code.
Gợi ý Plugin sẽ tự động sinh các property GraphQL còn thiếu, nhưng nếu bạn cần ghi đè chúng, chỉ cần set tường minh qua @Field().
Introspection comment
Khi bật tính năng introspection comment, CLI plugin sẽ sinh description cho các field dựa trên comment.
Ví dụ, với một property roles như sau:
/**
* Danh sách các role của user
*/
@Field(() => [String], {
description: `A list of user's roles`
})
roles: string[];
Bạn phải viết trùng lặp giá trị description. Khi bật introspectComments, CLI plugin có thể trích xuất các comment này và tự động cung cấp description cho property. Giờ đây, field ở trên có thể được khai báo đơn giản như sau:
/**
* Danh sách các role của user
*/
roles: string[];
Sử dụng CLI plugin
Để bật plugin, mở nest-cli.json (nếu bạn dùng Nest CLI) và thêm cấu hình plugins sau:
{
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"plugins": ["@nestjs/graphql"]
}
}
Bạn có thể dùng property options để tùy chỉnh hành vi của plugin.
{
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"plugins": [
{
"name": "@nestjs/graphql",
"options": {
"typeFileNameSuffix": [".input.ts", ".args.ts"],
"introspectComments": true
}
}
]
}
}
Property options phải tuân theo interface sau:
export interface PluginOptions {
typeFileNameSuffix?: string[];
introspectComments?: boolean;
}
| Option | Mặc định | Mô tả |
|---|---|---|
typeFileNameSuffix |
['.input.ts', '.args.ts', '.entity.ts', '.model.ts'] |
Hậu tố file type GraphQL |
introspectComments |
false |
Nếu set true, plugin sẽ sinh description cho property dựa trên comment |
Nếu bạn không dùng CLI mà có cấu hình webpack riêng, bạn có thể dùng plugin này kết hợp với ts-loader:
getCustomTransformers: (program: any) => ({
before: [require('@nestjs/graphql/plugin').before({}, program)]
}),
SWC builder
Với setup tiêu chuẩn (không phải monorepo), để dùng CLI Plugin với SWC builder, bạn cần bật type checking, như mô tả tại đây.
$ nest start -b swc --type-check
Với setup monorepo, làm theo hướng dẫn tại đây.
$ npx ts-node src/generate-metadata.ts
# HOẶC npx ts-node apps/{YOUR_APP}/src/generate-metadata.ts
Giờ đây, file metadata đã được tuần tự hóa (serialize) phải được load bởi method của GraphQLModule, như dưới đây:
import metadata from './metadata.js'; // <-- file được tự động sinh bởi "PluginMetadataGenerator"
GraphQLModule.forRoot<...>({
..., // các option khác
metadata,
}),
Ghi chú người dịch Vấn đề mà SWC gặp phải ở đây là do bản chất: SWC compile file TypeScript sang JavaScript rất nhanh nhưng bỏ qua bước type-check đầy đủ, trong khi plugin GraphQL cần đọc AST kèm thông tin kiểu (type information) để suy ra nullable, type cho field. Vì vậy Nest phải tách riêng một bước sinh metadata (generate-metadata.ts) chạy bằng ts-node (có type-check đầy đủ) rồi nạp kết quả đó vào lúc khởi động ứng dụng.
Tích hợp với ts-jest (test e2e)
Khi chạy test e2e với plugin này được bật, bạn có thể gặp vấn đề khi biên dịch schema. Ví dụ, một trong những lỗi thường gặp nhất là:
Object type <name> must define one or more fields.
Điều này xảy ra vì cấu hình jest không import plugin @nestjs/graphql/plugin ở đâu cả.
Để khắc phục, tạo file sau trong thư mục test e2e của bạn:
const transformer = require('@nestjs/graphql/plugin');
module.exports.name = 'nestjs-graphql-transformer';
// bạn nên thay đổi số version mỗi khi bạn thay đổi cấu hình bên dưới - nếu không, jest sẽ không phát hiện thay đổi
module.exports.version = 1;
module.exports.factory = (cs) => {
return transformer.before(
{
// các option của @nestjs/graphql/plugin (có thể để trống)
},
cs.program, // "cs.tsCompiler.program" cho các phiên bản Jest cũ hơn (<= v27)
);
};
Với cấu hình này, import AST transformer vào file cấu hình jest của bạn. Mặc định (trong ứng dụng starter), file cấu hình test e2e nằm trong thư mục test và có tên jest-e2e.json.
{
... // các cấu hình khác
"globals": {
"ts-jest": {
"astTransformers": {
"before": ["<đường dẫn đến file vừa tạo ở trên>"]
}
}
}
}
Nếu bạn dùng jest@^29, hãy dùng đoạn code dưới đây, vì cách tiếp cận trước đó đã bị deprecated.
{
... // các cấu hình khác
"transform": {
"^.+\\.(t|j)s$": [
"ts-jest",
{
"astTransformers": {
"before": ["<đường dẫn đến file vừa tạo ở trên>"]
}
}
]
}
}