☰ Mục lục series
Tài liệu NestJS tiếng Việt
  1. Nhập môn (Overview)
    1. Giới thiệu
    2. First steps
    3. Controllers
    4. Providers
    5. Modules
    6. Middleware
    7. Exception filters
    8. Pipes
    9. Guards
    10. Interceptors
    11. Custom decorators
  2. Nền tảng (Fundamentals)
    1. Custom providers
    2. Asynchronous providers
    3. Dynamic modules
    4. Injection scopes
    5. Circular dependency
    6. Module reference
    7. Lazy-loading modules
    8. Execution context
    9. Lifecycle events
    10. Discovery service
    11. Platform agnosticism
    12. Testing
  3. Kỹ thuật (Techniques)
    1. Configuration
    2. Database (SQL)
    3. MongoDB
    4. Validation
    5. Caching
    6. Serialization
    7. Versioning
    8. Task scheduling
    9. Queues
    10. Logging
    11. Cookies
    12. Events
    13. Compression
    14. File upload
    15. Streaming files
    16. HTTP module
    17. Session
    18. MVC
    19. Performance (Fastify)
    20. Server-Sent Events
  4. Bảo mật (Security)
    1. Authentication
    2. Authorization
    3. Encryption & Hashing
    4. Helmet
    5. CORS
    6. CSRF
    7. Rate limiting
    8. Vòng đời request
  5. OpenAPI (Swagger)
    1. Introduction
    2. Types and parameters
    3. Operations
    4. Security
    5. Mapped types
    6. Decorators
    7. CLI plugin
    8. Other features
  6. Recipes
    1. REPL
    2. CRUD generator
    3. SWC (fast compiler)
    4. Passport (auth)
    5. Hot reload
    6. MikroORM
    7. Router module
    8. Health checks (Terminus)
    9. CQRS
    10. Prisma
    11. Serve static
    12. Commander
    13. Async local storage
  7. WebSockets
    1. Gateways
    2. Exception filters
    3. Pipes
    4. Guards
    5. Interceptors
    6. Adapters
  8. Microservices
    1. Overview
    2. Redis
    3. MQTT
    4. NATS
    5. RabbitMQ
    6. Kafka
    7. gRPC
    8. Custom transporters
    9. Exception filters
    10. Pipes
    11. Pre-request hooks
    12. Guards
    13. Interceptors
  9. GraphQL
    1. Quick start
    2. Resolvers
    3. Mutations
    4. Subscriptions
    5. Scalars
    6. Directives
    7. Interfaces
    8. Unions and enums
    9. Field middleware
    10. Mapped types
    11. Plugins
    12. Complexity
    13. Extensions
    14. CLI plugin
    15. Generating SDL
    16. Sharing models
    17. Other features
    18. Federation
  10. CLI & triển khai
    1. CLI overview
    2. Workspaces
    3. Libraries
    4. Usage
    5. Scripts
    6. Deployment
    7. Standalone apps
  11. FAQ
    1. Serverless
    2. HTTP adapter
    3. Keep-Alive connections
    4. Global path prefix
    5. Raw body
    6. Hybrid application
    7. HTTPS & multiple servers
    8. Common errors
  12. Phụ lục
    1. Bảng thuật ngữ
GraphQL

Scalars

Bản dịch từ docs.nestjs.com/graphql/scalars

Một GraphQL object type có tên và các field, nhưng đến một lúc nào đó các field này phải resolve về dữ liệu cụ thể. Đó là lúc scalar type xuất hiện: chúng đại diện cho các “lá” (leaves) của query (đọc thêm tại đây). GraphQL bao gồm các kiểu mặc định sau: Int, Float, String, Boolean và ID. Bên cạnh các kiểu dựng sẵn này, bạn có thể cần hỗ trợ các kiểu dữ liệu nguyên tử (atomic) tùy chỉnh (ví dụ: Date).

Ghi chú người dịch Có thể hình dung scalar giống enum/kiểu nguyên thủy trong schema Protobuf hay JSON Schema: nó là “điểm dừng” cuối cùng của cây truy vấn, nơi GraphQL chuyển từ object type (còn field con) sang một giá trị cụ thể (string, number…). Khi bạn cần một kiểu không có sẵn (ví dụ Date, UUID, BigInt), bạn phải tự định nghĩa cách serialize/parse nó, tương tự việc viết custom marshaller/unmarshaller cho một type trong Go hay custom Eloquent cast trong Laravel.

Code first

Cách tiếp cận code first đi kèm sẵn năm scalar, trong đó ba scalar chỉ là alias cho các kiểu GraphQL đã có sẵn.

  • ID (alias của GraphQLID) - đại diện cho một định danh duy nhất, thường dùng để fetch lại một object hoặc làm key cho cache
  • Int (alias của GraphQLInt) - số nguyên có dấu 32-bit
  • Float (alias của GraphQLFloat) - giá trị số thực dấu phẩy động (floating-point), độ chính xác kép (double-precision), có dấu
  • GraphQLISODateTime - chuỗi date-time theo UTC (mặc định dùng để đại diện cho kiểu Date)
  • GraphQLTimestamp - số nguyên có dấu đại diện cho ngày giờ dưới dạng số mili-giây tính từ đầu UNIX epoch

GraphQLISODateTime (ví dụ 2019-12-03T09:54:33Z) được dùng mặc định để đại diện cho kiểu Date. Để dùng GraphQLTimestamp thay thế, đặt dateScalarMode trong object buildSchemaOptions thành 'timestamp' như sau:

GraphQLModule.forRoot({
  buildSchemaOptions: {
    dateScalarMode: 'timestamp',
  }
}),

Tương tự, GraphQLFloat được dùng mặc định để đại diện cho kiểu number. Để dùng GraphQLInt thay thế, đặt numberScalarMode trong object buildSchemaOptions thành 'integer' như sau:

GraphQLModule.forRoot({
  buildSchemaOptions: {
    numberScalarMode: 'integer',
  }
}),

Ngoài ra, bạn có thể tạo scalar tùy chỉnh (custom scalar).

Ghi đè một scalar mặc định (Override a default scalar)

Để tạo một implementation tùy chỉnh cho scalar Date, chỉ cần tạo một class mới.

import { Scalar, CustomScalar } from '@nestjs/graphql';
import { Kind, ValueNode } from 'graphql';

@Scalar('Date', () => Date)
export class DateScalar implements CustomScalar<number, Date> {
  description = 'Date custom scalar type';

  parseValue(value: number): Date {
    return new Date(value); // giá trị nhận từ client
  }

  serialize(value: Date): number {
    return value.getTime(); // giá trị gửi cho client
  }

  parseLiteral(ast: ValueNode): Date {
    if (ast.kind === Kind.INT) {
      return new Date(ast.value);
    }
    return null;
  }
}

Sau khi có class này, đăng ký DateScalar như một provider.

@Module({
  providers: [DateScalar],
})
export class CommonModule {}

Giờ chúng ta có thể dùng kiểu Date trong các class của mình.

@Field()
creationDate: Date;

Import một scalar tùy chỉnh (Import a custom scalar)

Để dùng một scalar tùy chỉnh, import và đăng ký nó như một resolver. Chúng ta sẽ dùng package graphql-type-json để minh họa. Package npm này định nghĩa một scalar type GraphQL tên là JSON.

Bắt đầu bằng cách cài package:

$ npm i --save graphql-type-json

Sau khi cài xong, chúng ta truyền một custom resolver vào phương thức forRoot():

import GraphQLJSON from 'graphql-type-json';

@Module({
  imports: [
    GraphQLModule.forRoot({
      resolvers: { JSON: GraphQLJSON },
    }),
  ],
})
export class AppModule {}

Giờ chúng ta có thể dùng kiểu JSON trong các class của mình.

@Field(() => GraphQLJSON)
info: JSON;

Để xem một bộ scalar hữu ích khác, tham khảo package graphql-scalars.

Tạo một scalar tùy chỉnh (Create a custom scalar)

Để định nghĩa một scalar tùy chỉnh, tạo một instance GraphQLScalarType mới. Chúng ta sẽ tạo một scalar UUID tùy chỉnh.

const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

function validate(uuid: unknown): string | never {
  if (typeof uuid !== 'string' || !regex.test(uuid)) {
    throw new Error('invalid uuid');
  }
  return uuid;
}

export const CustomUuidScalar = new GraphQLScalarType({
  name: 'UUID',
  description: 'A simple UUID parser',
  serialize: (value) => validate(value),
  parseValue: (value) => validate(value),
  parseLiteral: (ast) => validate(ast.value),
});

Chúng ta truyền một custom resolver vào phương thức forRoot():

@Module({
  imports: [
    GraphQLModule.forRoot({
      resolvers: { UUID: CustomUuidScalar },
    }),
  ],
})
export class AppModule {}

Giờ chúng ta có thể dùng kiểu UUID trong các class của mình.

@Field(() => CustomUuidScalar)
uuid: string;

Schema first

Để định nghĩa một scalar tùy chỉnh (đọc thêm về scalar tại đây), tạo một type definition và một resolver riêng. Ở đây (giống tài liệu chính thức), chúng ta sẽ dùng package graphql-type-json để minh họa. Package npm này định nghĩa một scalar type GraphQL tên là JSON.

Bắt đầu bằng cách cài package:

$ npm i --save graphql-type-json

Sau khi cài xong, chúng ta truyền một custom resolver vào phương thức forRoot():

import GraphQLJSON from 'graphql-type-json';

@Module({
  imports: [
    GraphQLModule.forRoot({
      typePaths: ['./**/*.graphql'],
      resolvers: { JSON: GraphQLJSON },
    }),
  ],
})
export class AppModule {}

Giờ chúng ta có thể dùng scalar JSON trong type definitions của mình:

scalar JSON

type Foo {
  field: JSON
}

Một cách khác để định nghĩa một scalar type là tạo một class đơn giản. Giả sử chúng ta muốn bổ sung kiểu Date vào schema.

import { Scalar, CustomScalar } from '@nestjs/graphql';
import { Kind, ValueNode } from 'graphql';

@Scalar('Date')
export class DateScalar implements CustomScalar<number, Date> {
  description = 'Date custom scalar type';

  parseValue(value: number): Date {
    return new Date(value); // giá trị nhận từ client
  }

  serialize(value: Date): number {
    return value.getTime(); // giá trị gửi cho client
  }

  parseLiteral(ast: ValueNode): Date {
    if (ast.kind === Kind.INT) {
      return new Date(ast.value);
    }
    return null;
  }
}

Sau khi có class này, đăng ký DateScalar như một provider.

@Module({
  providers: [DateScalar],
})
export class CommonModule {}

Giờ chúng ta có thể dùng scalar Date trong type definitions.

scalar Date

Theo mặc định, TypeScript definition được sinh ra cho tất cả scalar là any - kiểu này không đảm bảo an toàn kiểu (type-safe) lắm. Nhưng bạn có thể cấu hình cách Nest sinh ra typing cho các scalar tùy chỉnh của mình bằng cách chỉ định cách sinh type:

import { GraphQLDefinitionsFactory } from '@nestjs/graphql';
import { join } from 'node:path';

const definitionsFactory = new GraphQLDefinitionsFactory();

definitionsFactory.generate({
  typePaths: ['./src/**/*.graphql'],
  path: join(process.cwd(), 'src/graphql.ts'),
  outputAs: 'class',
  defaultScalarType: 'unknown',
  customScalarTypeMapping: {
    DateTime: 'Date',
    BigNumber: '_BigNumber',
  },
  additionalHeader: "import _BigNumber from 'bignumber.js'",
});

Gợi ý Ngoài ra, bạn có thể dùng một type reference thay thế, ví dụ: DateTime: Date. Trong trường hợp này, GraphQLDefinitionsFactory sẽ lấy thuộc tính name của type được chỉ định (Date.name) để sinh TS definition. Lưu ý: cần thêm câu lệnh import cho các kiểu không dựng sẵn (custom type).

Giờ, với các custom scalar type GraphQL sau:

scalar DateTime
scalar BigNumber
scalar Payload

Chúng ta sẽ thấy các TypeScript definition được sinh ra sau đây trong src/graphql.ts:

import _BigNumber from 'bignumber.js';

export type DateTime = Date;
export type BigNumber = _BigNumber;
export type Payload = unknown;

Ở đây, chúng ta đã dùng thuộc tính customScalarTypeMapping để cung cấp một map các kiểu mà chúng ta muốn khai báo cho các custom scalar của mình. Chúng ta cũng đã cung cấp thuộc tính additionalHeader để có thể thêm bất kỳ import nào cần thiết cho các type definition này. Cuối cùng, chúng ta đã thêm một defaultScalarType là 'unknown', để bất kỳ custom scalar nào không được chỉ định trong customScalarTypeMapping sẽ được alias thành unknown thay vì any (điều mà TypeScript khuyến nghị sử dụng kể từ bản 3.0 để tăng độ an toàn kiểu).

Ghi chú người dịch unknown khác any ở chỗ TypeScript buộc bạn phải kiểm tra kiểu (narrow) trước khi dùng giá trị đó, giống việc Go bắt bạn type-assert một interface{} trước khi thao tác. Vì bản chất của custom scalar là “không biết trước hình dạng dữ liệu thật”, unknown an toàn hơn any rất nhiều vì compiler sẽ chặn bạn nếu quên kiểm tra.

Gợi ý Lưu ý rằng chúng ta đã import _BigNumber từ bignumber.js; điều này để tránh tham chiếu kiểu vòng (circular type reference).

Nội dung gốc: NestJS Documentation © Kamil Myśliwiec, giấy phép MIT. Bản dịch và ghi chú: Lê Văn Dũng.