解析器(Resolver)
解析器(Resolver)是用来放着 GraphQL 操作(query
、mutation
、subscription
)的地方。 通常我们将业务相近的操作放在同一个解析器中,比如将用户相关的操作放在一个名为 userResolver
的解析器中。
区分操作
首先,我们简单地了解一下 GraphQL 的基本操作和应该在什么时候使用它们:
查询(Query) 是用来获取数据的操作,比如获取用户信息、获取商品列表等。查询操作通常不会改变服务的持久化数据。
变更(Mutation) 是用来修改数据的操作,比如创建用户、更新用户信息、删除用户等。变更操作通常会改变服务的持久化数据。
订阅(Subscription) 是服务端主动推送数据给客户端的操作,订阅操作通常不会改变服务的持久化数据。或者说,订阅是实时的查询(Query)。
定义解析器
我们使用 resolver
函数来定义解析器:
import { resolver } from "@gqloom/core"
const helloResolver = resolver({})
在上面的代码中,我们定义了一个名为 helloResolver
的解析器,它暂时没有任何操作。
定义操作
让我们尝试使用 query
函数来定义操作:
import { resolver, query, silk } from "@gqloom/core"
import { GraphQLNonNull, GraphQLString } from "graphql"
const helloResolver = resolver({
hello: query(silk<string>(new GraphQLNonNull(GraphQLString))).resolve(
() => "Hello, World"
),
})
在上面的代码中,我们定义了一个名为 hello
的 query
操作,它返回一个非空字符串。 在这里,我们直接使用 graphql.js
提供的类型定义,如你所见,这可能略显啰嗦,我们可以选择使用模式库来简化代码:
我们可以使用 valibot 来定义 hello
操作的返回类型:
import { resolver, query } from "@gqloom/core"
import * as v from "valibot"
const helloResolver = resolver({
hello: query(v.string()).resolve(() => "Hello, World"),
})
在上面的代码中,我们使用 v.sting()
来定义 hello
操作的返回类型,我们可以直接把 valibot
的 Schema 作为丝线
使用。
定义操作的输入
query
、mutation
、subscription
操作都可以接受输入参数。
让我们为 hello
操作添加一个输入参数 name
:
import { resolver, query } from '@gqloom/core'
import * as v from "valibot"
const helloResolver = resolver({
hello: query(v.string())
.input({
name: v.nullish(v.string(), "World"),
})
.resolve(({ name }) => `Hello, ${name}`),
})
在上面的代码中,我们在 query
函数的第二个参数中传入了 input
属性来定义输入参数:input
属性是一个对象,它的键是输入参数的名称,值是输入参数的类型定义。
在这里,我们使用 v.nullish(v.string(), "World")
来定义 name
参数,它是一个可选的字符串,默认值为 "World"
。 在 resolve
函数中,我们可以通过第一个参数来获取输入参数的值,TypeScript 将会为我们推导其类型,在这里,我们直接解构得到 name
参数的值。
为操作添加更多信息
我们还可以为操作添加更多信息,比如 description
、deprecationReason
和 extensions
:
import { resolver, query } from '@gqloom/core'
import * as v from "valibot"
const helloResolver = resolver({
hello: query(v.string())
.description("Say hello to someone")
.input({ name: v.nullish(v.string(), "World") })
.resolve(({ name }) => `Hello, ${name}!`),
})
对象解析器
在 GraphQL 中,我们可以为对象上的字段定义解析函数,以此为对象添加额外的属性以及创建对象之间的关系。 这使得 GraphQL 能够构建非常灵活同时保持简洁的 API。
当使用 GQLoom
时,我们可以使用 resolver.of
函数来定义对象解析器。
我们首先定义两个简单对象 User
和 Book
:
import * as v from "valibot"
const User = v.object({
__typename: v.nullish(v.literal("User")),
id: v.number(),
name: v.string(),
})
interface IUser extends v.InferOutput<typeof User> {}
const Book = v.object({
__typename: v.nullish(v.literal("Book")),
id: v.number(),
title: v.string(),
authorID: v.number(),
})
interface IBook extends v.InferOutput<typeof Book> {}
在上面的代码中,我们定义了两个对象 User
和 Book
,它们分别表示用户和书籍。 在 Book
中,我们定义了一个 authorID
字段,它表示书籍的作者 ID。
另外我们定义两个简单的 Map 对象来存储一些预设数据:
const userMap: Map<number, IUser> = new Map(
[
{ id: 1, name: "Cao Xueqin" },
{ id: 2, name: "Wu Chengen" },
].map((user) => [user.id, user])
)
const bookMap: Map<number, IBook> = new Map(
[
{ id: 1, title: "Dream of Red Mansions", authorID: 1 },
{ id: 2, title: "Journey to the West", authorID: 2 },
].map((book) => [book.id, book])
)
接下来,我们定义一个 bookResolver
:
import { resolver, query } from '@gqloom/core'
import * as v from "valibot"
const bookResolver = resolver.of(Book, {
books: query(v.array(Book)).resolve(() => Array.from(bookMap.values())),
})
在上面的代码中,我们使用 resolver.of
函数来定义 bookResolver
,它是一个对象解析器,用于解析 Book
对象。 在 bookResolver
中,我们定义了一个 books
字段,它是一个查询操作,用于获取所有的书籍。
接下来,我们将为 Book
对象添加一个名为 author
的额外字段用于获取书籍的作者:
import { resolver, query, field } from '@gqloom/core'
import * as v from "valibot"
const bookResolver = resolver.of(Book, {
books: query(v.array(Book)).resolve(() => Array.from(bookMap.values())),
author: field(v.nullish(User)).resolve((book) => userMap.get(book.authorID)),
})
在上面的代码中,我们使用 field
函数来定义 author
字段。 field
函数接受两个参数:
- 第一个参数是字段的返回类型;
- 第二个参数是解析函数或选项,在这里我们使用了一个解析函数:我们从解析函数的第一个参数获取
Book
实例,然后根据authorID
字段从userMap
中获取对应的User
实例。
定义字段输入
在 GraphQL 中,我们可以为字段定义输入参数,以便在查询时传递额外的数据。
在 GQLoom
中,我们可以使用 field
函数的第二个参数来定义字段的输入参数。
import { resolver, query, field } from '@gqloom/core'
import * as v from "valibot"
const bookResolver = resolver.of(Book, {
books: query(v.array(Book)).resolve(() => Array.from(bookMap.values())),
author: field(v.nullish(User)).resolve((book) => userMap.get(book.authorID)),
signature: field(v.string())
.input({ name: v.string() })
.resolve((book, { name }) => {
return `The book ${book.title} is in ${name}'s collection.`
}),
})
在上面的代码中,我们使用 field
函数来定义 signature
字段。 field
函数的第二个参数是一个对象,它包含两个字段:
input
:字段的输入参数,它是一个对象,包含一个name
字段,它的类型是string
;resolve
:字段的解析函数,它接受两个参数:第一个参数由resolver.of
构造的解析器的源对象,即Book
实例;第二个参数是字段的输入参数,即包含name
字段的输入。
刚刚我们定义的 bookResolver
对象可以通过 weave 函数编织成 GraphQL schema:
import { weave } from '@gqloom/core'
import { ValibotWeaver } from '@gqloom/valibot'
export const schema = weave(ValibotWeaver, bookResolver)
最终得到的 GraphQL schema 如下:
type Book {
id: ID!
title: String!
authorID: ID!
author: User
signature(name: String!): String!
}
type User {
id: ID!
name: String!
}
type Query {
books: [Book!]!
}
定义派生属性
在为数据库表或其他持久化数据定义编写解析器时,我们通常需要根据表中的数据计算出新的属性,也就是派生属性。
派生属性要求在获取数据时选取其依赖的数据,我们可以使用 field().derivedFrom()
来声明所依赖的数据。 派生依赖将被 useResolvingFields()
使用,这个函数用于精确获取当前查询所需要的字段。
import { field, resolver } from "@gqloom/core"
import * as v from "valibot"
import { giraffes } from "./table"
export const giraffeResolver = resolver.of(giraffes, {
age: field(v.number())
.derivedFrom("birthDate")
.resolve((giraffe) => {
const today = new Date()
const age = today.getFullYear() - giraffe.birthDate.getFullYear()
return age
}),
})