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ủaGraphQLID) - đạ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 cacheInt(alias củaGraphQLInt) - số nguyên có dấu 32-bitFloat(alias củaGraphQLFloat) - giá trị số thực dấu phẩy động (floating-point), độ chính xác kép (double-precision), có dấuGraphQLISODateTime- chuỗi date-time theo UTC (mặc định dùng để đại diện cho kiểuDate)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).