---
title: 上下文(Context)
icon: Shuffle
file: ./content/docs/context.zh.mdx
---
在 Node.js 世界中,上下文(Context)允许我们在同一个请求中共享数据和状态。在 GraphQL 中,上下文允许在同一个请求的多个[解析函数](./resolver)和[中间件](./middleware)之间共享数据。
一个常见的用例是将当前访问者的身份信息存储在上下文中,以便在解析函数和中间件中访问。
## 访问上下文
在 `GQLoom` 中,我们通过 `useContext()` 函数访问上下文。
GQLoom 的 `useContext` 函数的设计参考了 [React](https://zh-hans.react.dev/) 的 `useContext` 函数。
你可以在[解析器](./resolver)内的任何地方调用 `useContext` 函数以访问当前请求的上下文,而不需要显式地传递 `context` 函数。
在幕后,`useContext` 使用 Node.js 的 [AsyncLocalStorage](https://nodejs.org/api/async_context.html#class-asynclocalstorage) 来隐式传递上下文。
### 启用上下文
对于不支持 `AsyncLocalStorage` 的环境,如浏览器或 Cloudflare Workers,可以使用 [resolverPayload](./context#直接访问解析器负载) 中的 `context` 属性。
我们通过在 weave 函数中传入 `asyncContextProvider` 来启用上下文。`asyncContextProvider` 实际上是一个全局中间件。
```ts
import { weave } from "@gqloom/core"
import { asyncContextProvider } from "@gqloom/core/context" // [!code hl]
const schema = weave(ValibotWeaver,asyncContextProvider, ...resolvers)
```
接下来,让我们尝试在各个地方访问上下文。
我们将使用 [graphql-yoga](https://the-guild.dev/graphql/yoga-server) 作为适配器。
### 在解析函数中访问上下文
```ts twoslash
import { query, resolver, weave } from "@gqloom/core"
import { useContext } from "@gqloom/core/context"
import { ValibotWeaver } from "@gqloom/valibot"
import * as v from "valibot"
import { type YogaInitialContext, createYoga } from "graphql-yoga"
import { createServer } from "http"
async function useUser() {
const authorization =
useContext().request.headers.get("Authorization")
const user = await UserService.getUserByAuthorization(authorization)
return user
}
const helloResolver = resolver({
hello: query(v.string(), {
input: {
name: v.pipeAsync(
v.nullish(v.string()),
v.transformAsync(async (value) => { // [!code hl]
if (value != null) return value // [!code hl]
const user = await useUser() // [!code hl]
return user.name // [!code hl]
}) // [!code hl]
),
},
resolve: ({ name }) => `Hello, ${name}`,
}),
})
const yoga = createYoga({ schema: weave(ValibotWeaver, helloResolver) })
createServer(yoga).listen(4000, () => {
console.info("Server is running on http://localhost:4000/graphql")
})
```
在上面的代码中,我们在 `v.transformAsync` 中使用 `useUser()` 来获取上下文中的用户信息,并将其作为 `name` 的值返回。
我们可以在 `zod` 中自定义验证或转换,并在其中直接访问上下文:
```ts twoslash
import { query, resolver, weave } from "@gqloom/core"
import { useContext } from "@gqloom/core/context"
import { ZodWeaver } from "@gqloom/zod"
import * as z from "zod"
import { type YogaInitialContext, createYoga } from "graphql-yoga"
import { createServer } from "http"
async function useUser() {
const authorization =
useContext().request.headers.get("Authorization")
const user = await UserService.getUserByAuthorization(authorization)
return user
}
const helloResolver = resolver({
hello: query(z.string(), {
input: {
name: z
.string()
.nullish()
.transform(async (value) => { // [!code hl]
if (value != null) return value // [!code hl]
const user = await useUser() // [!code hl]
return user.name // [!code hl]
}), // [!code hl]
},
resolve: ({ name }) => `Hello, ${name}`,
}),
})
const yoga = createYoga({ schema: weave(ZodWeaver, helloResolver) })
createServer(yoga).listen(4000, () => {
console.info("Server is running on http://localhost:4000/graphql")
})
```
在上面的代码中,我们在 `z.transform` 中使用 `useUser()` 来获取上下文中的用户信息,并将其作为 `name` 的值返回。
## 记忆化
考虑我们通过以下自定义函数来访问用户:
```ts twoslash
import { useContext } from "@gqloom/core/context"
import { type YogaInitialContext } from "graphql-yoga"
async function useUser() {
const authorization =
useContext().request.headers.get("Authorization")
const user = await UserService.getUserByAuthorization(authorization)
return user
}
```
我们可能在 `useUser()` 中执行一些昂贵的操作,例如从数据库中获取用户信息,并且我们还有可能在同一请求中多次调用它。
为了避免多次调用造成的额外开销,我们可以使用记忆化(Memoization)来缓存结果,并在后续调用中重用它们。
在 GQLoom 中,我们使用 `createMemoization` 函数来创建一个记忆化函数。
记忆化函数会在第一次被调用后,将其结果缓存在上下文中,并在后续调用中直接返回缓存的结果。
也就是说,在同一个请求中,记忆化函数只会被执行一次,无论它被调用多少次。
让我们将 `useUser()` 函数记忆化:
```ts twoslash
import { createMemoization, useContext } from "@gqloom/core/context"
const useUser = createMemoization(async () => {
const authorization =
useContext().request.headers.get("Authorization")
const user = await UserService.getUserByAuthorization(authorization)
return user
})
```
如你所见,我们只需要将函数包装在 `createMemoization` 函数中即可。
随后,我们可以在解析器内的任何地方调用 `useUser()`,而无需担心多次调用带来的开销。
## 注入上下文
`asyncContextProvider` 还允许我们注入上下文。这通常与 [执行器](./advanced/executor) 一起使用。
```ts
const giraffeExecutor = giraffeResolver.toExecutor(
asyncContextProvider.with(useCurrentUser.provide({ id: 9, roles: ["admin"] }))
)
```
## 访问解析器负载
除了 `useContext` 函数,GQLoom 还提供了 `useResolverPayload` 函数,用于访问解析器中的所有参数:
* root: 上一个对象,对于根查询类型上的字段来说,通常不会使用;
* args: 在 GraphQL 查询中为字段提供的参数;
* context: 在各个解析函数和中间件中共享的上下文对象,即 `useContext` 的返回值;
* info: 包含有关当前解析器调用的信息,例如 GraphQL 查询的路径、字段名称等;
* field: 当前解析器正在解析的字段定义;
### 直接访问解析器负载
对于不提供 `AsyncLocalStorage` 的环境,如浏览器或 Cloudflare Workers,我们可以直接在解析函数和中间件中访问解析器负载。
#### 解析函数
在解析函数中,`payload` 总是最后一个参数。
```ts
const helloResolver = resolver({
hello: query(v.string()).resolve((_input, payload) => {
const user = // [!code hl]
(payload!.context as YogaInitialContext).request.headers.get("Authorization") // [!code hl]
return `Hello, ${user ?? "World"}`
}),
})
```
```ts
const helloResolver = resolver({
hello: query(z.string()).resolve((_input, payload) => {
const user = // [!code hl]
(payload!.context as YogaInitialContext).request.headers.get("Authorization") // [!code hl]
return `Hello, ${user ?? "World"}`
}),
})
```
#### 中间件
```ts twoslash
import { Middleware, ResolverPayload } from "@gqloom/core"
import { type YogaInitialContext } from "graphql-yoga"
function getUser(payload: ResolverPayload) {
const user = (payload.context as YogaInitialContext).request.headers.get(
"Authorization"
)
return user
}
const authGuard: Middleware = ({ next, payload }) => {
const user = getUser(payload!)
if (!user) throw new Error("Please login first")
return next()
}
```
## 在各个适配器中使用上下文
在 GraphQL 生态中,每个适配器都提供了不同的上下文对象,你可以在适配器章节中了解如何使用:
* [Yoga](./advanced/adapters/yoga)
* [Apollo](./advanced/adapters/apollo)
* [Mercurius](./advanced/adapters/mercurius)
---
title: 数据加载器(Dataloader)
icon: HardDriveDownload
file: ./content/docs/dataloader.zh.mdx
---
由于 GraphQL 的灵活性,当我们加载某个对象的关联对象时,我们通常需要执行多个查询。
这就造成了著名的 N+1 查询问题。为了解决这个问题,我们可以使用 [DataLoader](https://github.com/graphql/dataloader)。
`DataLoader` 能够将多个请求合并为一个请求,从而减少数据库的查询次数,同时还能缓存查询结果,避免重复查询。
## 示例
### 表格定义
考虑我们有两张表 `users` 和 `posts`,其中 `posts` 通过 `posts.authorId` 关联到 `users` 的 `id`:
```ts twoslash
import { field, query, resolver, weave } from "@gqloom/core"
import { eq, inArray } from "drizzle-orm"
import { drizzle } from "drizzle-orm/node-postgres"
import { config } from "./env.config"
import * as tables from "./schema"
import { posts, users } from "./schema"
const db = drizzle(config.databaseUrl, { schema: tables, logger: true })
const userResolver = resolver.of(users, {
users: query(users.$list()).resolve(() => db.select().from(users)),
posts: field(posts.$list())
.derivedFrom("id")
.resolve((user) => db.select().from(posts).where(eq(posts.authorId, user.id))),
})
export const schema = weave(userResolver)
```
在上面的代码中,我们定义了一个用户解析器,它包含:
* `users` 查询:用于获取所有用户
* `posts` 字段:用于获取对应用户的所有帖子
下面是一个示例查询,它将返回所有用户的信息以及对应的帖子:
```graphql title="GraphQL Query"
query usersWithPosts {
users {
id
name
email
posts {
id
title
}
}
}
```
这个查询将为每个用户分别查询他们的帖子。我们在前一步在数据库里填充了 20 个用户,所以这个查询将引起 20 次对 `posts` 表的查询。\
这显然是一种非常低效的方式,让我们来看看如何使用 DataLoader 来减少查询次数。
### 使用 DataLoader
接下来,我们将使用 DataLoader 来优化我们的查询。
```ts twoslash
import { field, query, resolver, weave } from "@gqloom/core"
import { eq, inArray } from "drizzle-orm"
import { drizzle } from "drizzle-orm/node-postgres"
import { config } from "./env.config"
import * as tables from "./schema"
import { posts, users } from "./schema"
const db = drizzle(config.databaseUrl, { schema: tables, logger: true })
const userResolver = resolver.of(users, {
users: query(users.$list()).resolve(() => db.select().from(users)),
posts_: field(posts.$list()) // [!code --]
.derivedFrom("id") // [!code --]
.resolve((user) => db.select().from(posts).where(eq(posts.authorId, user.id))), // [!code --]
posts: field(posts.$list()) // [!code ++]
.derivedFrom("id") // [!code ++]
.load(async (userList) => { // [!code ++]
const postList = await db // [!code ++]
.select() // [!code ++]
.from(posts) // [!code ++]
.where( // [!code ++]
inArray( // [!code ++]
posts.authorId, // [!code ++]
userList.map((u) => u.id) // [!code ++]
) // [!code ++]
) // [!code ++]
const postMap = Map.groupBy(postList, (p) => p.authorId) // [!code ++]
return userList.map((u) => postMap.get(u.id) ?? []) // [!code ++]
}), // [!code ++]
})
export const schema = weave(userResolver)
```
在上面的代码中,我们使用 `field().load()` 来启用数据批量加载,在幕后这将使用 `DataLoader` 来批量加载数据。\
在 `load()` 内部,我们通过以下步骤实现数据批量加载:
1. 使用 `in` 操作从 `posts` 表中一次性获取所有当前加载的用户的帖子;
2. 使用 `Map.groupBy()` 将帖子列表按作者 ID 分组;
3. 将用户列表按顺序映射到帖子列表,如果某个用户没有帖子,则返回一个空数组。
如此一来,我们将原先的 20 次查询合并为 1 次查询,从而实现了性能优化。
必须保证查询函数的返回数组顺序与 `IDs` 数组顺序一致。`DataLoader` 依赖于此顺序来正确地合并结果。
---
title: 快速上手
icon: PencilRuler
file: ./content/docs/getting-started.zh.mdx
---
import { File, Folder, Files } from 'fumadocs-ui/components/files';
为了快速上手 GQLoom,我们将一起搭建一个简单的 GraphQL 后端应用。
我们将搭建一个猫舍应用,并为向外部提供 GraphQL API。
该应用将包含一些简单的功能:
* 猫基础信息管理:录入猫的的基本信息,包括名称、生日等,更新、删除和查询猫;
* 用户(猫主人)登记管理:录入用户信息,简单的登录功能,查看自己或其他用户的猫;
我们将使用以下技术:
* [TypeScript](https://www.typescriptlang.org/): 作为我们的开发语言;
* [Node.js](https://nodejs.org/): 作为我们应用的运行时;
* [graphql.js](https://github.com/graphql/graphql-js): GraphQL 的 JavaScript 实现;
* [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server): 功能全面的 GraphQL HTTP 适配器;
* [Drizzle ORM](https://orm.drizzle.team/): 一个快速且类型安全的 ORM,帮助我们操作数据库;
* [Valibot](https://valibot.dev/) 或者 [Zod](https://zod.dev/): 用于定义和验证输入;
* `GQLoom`: 让我们舒适且高效地定义 GraphQL Schema 并编写解析器(Resolver);
## 前提条件
我们只需要安装版本 20 以上的 [Node.js](https://nodejs.org/) 来运行我们的应用。
## 创建应用
### 项目结构
我们的应用将有以下的结构:
其中,`src` 目录下的各个文件夹或文件的职能如下:
* `contexts`: 存放上下文,如当前用户;
* `providers`: 存放需要与外部服务交互的功能,如数据库连接、Redis 连接;
* `resolvers`: 存放 GraphQL 解析器;
* `schema`: 存放 schema,主要是数据库表结构;
* `index.ts`: 用于以 HTTP 服务的形式运行 GraphQL 应用;
GQLoom 对项目的文件结构没有任何要求,这里只提供一个参考,在实践中你可以按照需求和喜好组织文件。
### 初始化项目
首先,让我们新建文件夹并初始化项目:
```sh
mkdir cattery
cd ./cattery
npm init -y
```
```sh
mkdir cattery
cd ./cattery
pnpm init
```
```sh
mkdir cattery
cd ./cattery
yarn init -y
```
然后,我们将安装一些必要的依赖来以便在 Node.js 运行中 TypeScript 应用:
```sh
npm i -D typescript @types/node tsx
npx tsc --init
```
```sh
pnpm add -D typescript @types/node tsx
pnpm exec tsc --init
```
```sh
yarn add -D typescript @types/node tsx
yarn dlx -q -p typescript tsc --init
```
接下来,我们将安装 GQLoom 以及相关依赖,我们可以选择 [Valibot](https://valibot.dev/) 或者 [Zod](https://zod.dev/) 来定义并验证输入:
```sh
# use Valibot
npm i graphql graphql-yoga @gqloom/core valibot @gqloom/valibot
# use Zod
npm i graphql graphql-yoga @gqloom/core zod @gqloom/zod
```
```sh
# use Valibot
pnpm add graphql graphql-yoga @gqloom/core valibot @gqloom/valibot
# use Zod
pnpm add graphql graphql-yoga @gqloom/core zod @gqloom/zod
```
```sh
# use Valibot
yarn add graphql graphql-yoga @gqloom/core valibot @gqloom/valibot
# use zod
yarn add graphql graphql-yoga @gqloom/core zod @gqloom/zod
```
### 你好 世界
让我们编写第一个[解析器](./resolver):
```ts twoslash
import { mutation, query, resolver } from "@gqloom/core"
import { eq } from "drizzle-orm"
import * as v from "valibot"
import { db } from "../providers"
import { users } from "../schema"
export const userResolver = resolver.of(users, {
usersByName: query(users.$list())
.input({ name: v.string() })
.resolve(({ name }) => {
return db.query.users.findMany({
where: eq(users.name, name),
})
}),
userByPhone: query(users.$nullable())
.input({ phone: v.string() })
.resolve(({ phone }) => {
return db.query.users.findFirst({
where: eq(users.phone, phone),
})
}),
createUser: mutation(users)
.input({
data: v.object({
name: v.string(),
phone: v.string(),
}),
})
.resolve(async ({ data }) => {
const [user] = await db.insert(users).values(data).returning()
return user
}),
})
```
```ts twoslash
// @filename: resolvers/user.ts
import { mutation, query, resolver } from "@gqloom/core"
import { eq } from "drizzle-orm"
import * as z from "zod"
import { db } from "../providers"
import { users } from "../schema"
export const userResolver = resolver.of(users, {
usersByName: query(users.$list())
.input({ name: z.string() })
.resolve(({ name }) => {
return db.query.users.findMany({
where: eq(users.name, name),
})
}),
userByPhone: query(users.$nullable())
.input({ phone: z.string() })
.resolve(({ phone }) => {
return db.query.users.findFirst({
where: eq(users.phone, phone),
})
}),
createUser: mutation(users)
.input({
data: z.object({
name: z.string(),
phone: z.string(),
}),
})
.resolve(async ({ data }) => {
const [user] = await db.insert(users).values(data).returning()
return user
}),
})
```
```ts title="src/resolvers/index.ts"
import { helloResolver } from "./hello"
import { userResolver } from "./user" // [!code ++]
export const resolvers = [helloResolver, userResolver] // [!code ++]
```
很好,现在让我们在演练场尝试一下:
```gql title="GraphQL Mutation" tab="Mutation"
mutation {
createUser(data: {name: "Bob", phone: "001"}) {
id
name
phone
}
}
```
```json tab="Response"
{
"data": {
"createUser": {
"id": 1,
"name": "Bob",
"phone": "001"
}
}
}
```
继续尝试找回刚刚创建的用户:
```gql title="GraphQL Query" tab="Query"
{
usersByName(name: "Bob") {
id
name
phone
}
}
```
```json tab="Response"
{
"data": {
"usersByName": [
{
"id": 1,
"name": "Bob",
"phone": "001"
}
]
}
}
```
### 当前用户上下文
首先让我们为应用添加 `asyncContextProvider` 中间件来启用异步上下文:
```ts title="src/index.ts"
import { createServer } from "node:http"
import { weave } from "@gqloom/core"
import { ValibotWeaver } from "@gqloom/valibot" // [!code ++]
import { createYoga } from "graphql-yoga"
import { resolvers } from "./resolvers"
const schema = weave(asyncContextProvider, ValibotWeaver, ...resolvers) // [!code ++]
const yoga = createYoga({ schema })
createServer(yoga).listen(4000, () => {
console.info("Server is running on http://localhost:4000/graphql")
})
```
```ts title="src/index.ts"
import { createServer } from "node:http"
import { weave } from "@gqloom/core"
import { ZodWeaver } from "@gqloom/zod" // [!code ++]
import { createYoga } from "graphql-yoga"
import { resolvers } from "./resolvers"
const schema = weave(asyncContextProvider, ZodWeaver, ...resolvers) // [!code ++]
const yoga = createYoga({ schema })
createServer(yoga).listen(4000, () => {
console.info("Server is running on http://localhost:4000/graphql")
})
```
接下来,让我们尝试添加一个简单的登录功能,再为用户解析器添加一个查询操作:
* `mine`: 返回当前用户信息
为了实现这个查询,首先得有登录功能,让我们来简单写一个:
```ts twoslash
import { createMemoization, useContext } from "@gqloom/core/context"
import { eq } from "drizzle-orm"
import { GraphQLError } from "graphql"
import type { YogaInitialContext } from "graphql-yoga"
import { db } from "../providers"
import { users } from "../schema"
export const useCurrentUser = createMemoization(async () => {
const phone =
useContext().request.headers.get("authorization")
if (phone == null) throw new GraphQLError("Unauthorized")
const user = await db.query.users.findFirst({ where: eq(users.phone, phone) })
if (user == null) throw new GraphQLError("Unauthorized")
return user
})
```
在上面的代码中,我们创建了一个用于获取当前用户的[上下文](./context)函数,它将返回当前用户的信息。我们使用 `createMemoization()` 将此函数记忆化,这确保在同一个请求内此函数仅执行一次,以避免多余的数据库查询。
我们使用 `useContext()` 获取了 Yoga 提供的上下文(Context),并从请求头中获取了用户的手机号码,并根据手机号码查找用户,如果用户不存在,则抛出 `GraphQLError`。
如你所见,这个登录功能非常简陋,仅作为演示使用,完全不保证安全性。在实践中通常推荐使用 `session` 或者 `jwt` 等方案。
现在,我们在解析器里添加新的查询操作:
```ts title="src/resolvers/user.ts"
import { mutation, query, resolver } from "@gqloom/core"
import { eq } from "drizzle-orm"
import * as v from "valibot"
import { useCurrentUser } from "../contexts" // [!code ++]
import { db } from "../providers"
import { users } from "../schema"
export const userResolver = resolver.of(users, {
mine: query(users).resolve(() => useCurrentUser()), // [!code ++]
usersByName: query(users.$list())
.input({ name: v.string() })
.resolve(({ name }) => {
return db.query.users.findMany({
where: eq(users.name, name),
})
}),
userByPhone: query(users.$nullable())
.input({ phone: v.string() })
.resolve(({ phone }) => {
return db.query.users.findFirst({
where: eq(users.phone, phone),
})
}),
createUser: mutation(users)
.input({
data: v.object({
name: v.string(),
phone: v.string(),
}),
})
.resolve(async ({ data }) => {
const [user] = await db.insert(users).values(data).returning()
return user
}),
})
```
```ts title="src/resolvers/user.ts"
import { mutation, query, resolver } from "@gqloom/core"
import { eq } from "drizzle-orm"
import * as z from "zod"
import { useCurrentUser } from "../contexts" // [!code ++]
import { db } from "../providers"
import { users } from "../schema"
export const userResolver = resolver.of(users, {
mine: query(users).resolve(() => useCurrentUser()), // [!code ++]
usersByName: query(users.$list())
.input({ name: z.string() })
.resolve(({ name }) => {
return db.query.users.findMany({
where: eq(users.name, name),
})
}),
userByPhone: query(users.$nullable())
.input({ phone: z.string() })
.resolve(({ phone }) => {
return db.query.users.findFirst({
where: eq(users.phone, phone),
})
}),
createUser: mutation(users)
.input({
data: z.object({
name: z.string(),
phone: z.string(),
}),
})
.resolve(async ({ data }) => {
const [user] = await db.insert(users).values(data).returning()
return user
}),
})
```
如果我们在演练场里之间调用这个新的查询,应用程序将给我们未认证的错误:
```gql title="Graphql Query" tab="Query"
{
mine {
id
name
phone
}
}
```
```json tab="Response"
{
"errors": [
{
"message": "Unauthorized",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"mine"
]
}
],
"data": null
}
```
点开演练场下方的 `Headers`,并在请求头里添加 `authorization` 字段,这里我们使用在上一步中创建的 `Bob` 的手机号码,这样我们就作为`Bob`登录了:
```json tab="Headers"
{
"authorization": "001"
}
```
```gql title="Graphql Query" tab="Query"
{
mine {
id
name
phone
}
}
```
```json tab="Response"
{
"data": {
"mine": {
"id": 1,
"name": "Bob",
"phone": "001"
}
}
}
```
### 解析器工厂
接下来,我们将添加与猫咪相关的业务逻辑。
我们使用[解析器工厂](./schema/drizzle#解析器工厂)来快速创建接口:
```ts twoslash
import { field, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle"
import * as z from "zod"
import { db } from "../providers"
import { cats } from "../schema"
const catResolverFactory = drizzleResolverFactory(db, "cats")
export const catResolver = resolver.of(cats, {
cats: catResolverFactory.selectArrayQuery(),
age: field(z.number().int())
.derivedFrom("birthday")
.input({
currentYear: z
.number()
.int()
.nullish()
.transform((x) => x ?? new Date().getFullYear()),
})
.resolve((cat, { currentYear }) => {
return currentYear - cat.birthday.getFullYear()
}),
})
```
```ts title="src/resolvers/index.ts"
import { catResolver } from "./cat" // [!code ++]
import { helloResolver } from "./hello"
import { userResolver } from "./user"
export const resolvers = [helloResolver, userResolver, catResolver] // [!code ++]
```
在上面的代码中,我们使用 `drizzleResolverFactory()` 创建了 `catResolverFactory`,用于快速构建解析器。
我们添加了一个使用 `catResolverFactory` 创建了一个选取数据的查询 ,并将它命名为 `cats`,这个查询将提供完全的对 `cats` 表的查询操作。\
此外,我们还为猫咪添加了额外的 `age` 字段,用以获取猫咪的年龄。
接下来,让我们尝试添加一个 `createCat` 的变更。我们希望只有登录用户能访问这个接口,并且被创建的猫咪将归属于当前用户:
```ts twoslash
import { field, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle"
import * as v from "valibot"
import { useCurrentUser } from "../contexts"
import { db } from "../providers"
import { cats } from "../schema"
const catResolverFactory = drizzleResolverFactory(db, "cats")
export const catResolver = resolver.of(cats, {
cats: catResolverFactory.selectArrayQuery(),
age: field(v.pipe(v.number()))
.derivedFrom("birthday")
.input({
currentYear: v.nullish(v.pipe(v.number(), v.integer()), () =>
new Date().getFullYear()
),
})
.resolve((cat, { currentYear }) => {
return currentYear - cat.birthday.getFullYear()
}),
createCats: catResolverFactory.insertArrayMutation({ // [!code ++]
input: v.pipeAsync( // [!code ++]
v.objectAsync({ // [!code ++]
values: v.arrayAsync( // [!code ++]
v.pipeAsync( // [!code ++]
v.object({ // [!code ++]
name: v.string(), // [!code ++]
birthday: v.pipe( // [!code ++]
v.string(), // [!code ++]
v.transform((x) => new Date(x)) // [!code ++]
), // [!code ++]
}), // [!code ++]
v.transformAsync(async ({ name, birthday }) => ({ // [!code ++]
name, // [!code ++]
birthday, // [!code ++]
ownerId: (await useCurrentUser()).id, // [!code ++]
})) // [!code ++]
) // [!code ++]
), // [!code ++]
}) // [!code ++]
), // [!code ++]
}), // [!code ++]
})
```
```ts twoslash
import { field, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle"
import * as z from "zod"
import { useCurrentUser } from "../contexts"
import { db } from "../providers"
import { cats } from "../schema"
const catResolverFactory = drizzleResolverFactory(db, "cats")
export const catResolver = resolver.of(cats, {
cats: catResolverFactory.selectArrayQuery(),
age: field(z.number().int())
.derivedFrom("birthday")
.input({
currentYear: z
.number()
.int()
.nullish()
.transform((x) => x ?? new Date().getFullYear()),
})
.resolve((cat, { currentYear }) => {
return currentYear - cat.birthday.getFullYear()
}),
createCats: catResolverFactory.insertArrayMutation({ // [!code ++]
input: z.object({ // [!code ++]
values: z // [!code ++]
.object({ // [!code ++]
name: z.string(), // [!code ++]
birthday: z.coerce.date(), // [!code ++]
}) // [!code ++]
.transform(async ({ name, birthday }) => ({ // [!code ++]
name, // [!code ++]
birthday, // [!code ++]
ownerId: (await useCurrentUser()).id, // [!code ++]
})) // [!code ++]
.array(), // [!code ++]
}), // [!code ++]
}), // [!code ++]
})
```
在上面的代码中,我们使用 `catResolverFactory` 创建了一个向 `cats` 表格添加更多数据的变更,并且我们重写了这个变更的输入。在验证输入时,我们使用 `useCurrentUser()` 获取当前登录用户的 ID,并将作为 `ownerId` 的值传递给 `cats` 表格。
现在让我们在演练场尝试添加几只猫咪:
```gql tab="mutation" title="GraphQL Mutation"
mutation {
createCats(values: [
{ name: "Mittens", birthday: "2021-01-01" },
{ name: "Fluffy", birthday: "2022-02-02" },
]) {
id
name
age
}
}
```
```json tab="Headers"
{
"authorization": "001"
}
```
```json tab="Response"
{
"data": {
"createCats": [
{
"id": 1,
"name": "Mittens",
"age": 4
},
{
"id": 2,
"name": "Fluffy",
"age": 3
}
]
}
}
```
让我们使用 `cats` 查询再确认一下数据库的数据:
```gql tab="query" title="GraphQL Query"
{
cats {
id
name
age
}
}
```
```json tab="Response"
{
"data": {
"cats": [
{
"id": 1,
"name": "Mittens",
"age": 4
},
{
"id": 2,
"name": "Fluffy",
"age": 3
}
]
}
}
```
### 关联对象
我们希望在查询猫咪的时候可以获取到猫咪的拥有者,并且在查询用户的时候也可以获取到他所有的猫咪。\
这在 GraphQL 中非常容易实现。\
让我们为 `cats` 添加额外的 `owner` 字段,并为 `users` 添加额外的 `cats` 字段:
```ts title="src/resolvers/cat.ts"
import { field, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle"
import * as v from "valibot"
import { useCurrentUser } from "../contexts"
import { db } from "../providers"
import { cats } from "../schema"
const catResolverFactory = drizzleResolverFactory(db, "cats")
export const catResolver = resolver.of(cats, {
cats: catResolverFactory.selectArrayQuery(),
age: field(v.pipe(v.number()))
.derivedFrom("birthday")
.input({
currentYear: v.nullish(v.pipe(v.number(), v.integer()), () =>
new Date().getFullYear()
),
})
.resolve((cat, { currentYear }) => {
return currentYear - cat.birthday.getFullYear()
}),
owner: catResolverFactory.relationField("owner"), // [!code ++]
createCats: catResolverFactory.insertArrayMutation({
input: v.pipeAsync(
v.objectAsync({
values: v.arrayAsync(
v.pipeAsync(
v.object({
name: v.string(),
birthday: v.pipe(
v.string(),
v.transform((x) => new Date(x))
),
}),
v.transformAsync(async ({ name, birthday }) => ({
name,
birthday,
ownerId: (await useCurrentUser()).id,
}))
)
),
})
),
}),
})
```
```ts title="src/resolvers/cat.ts"
import { field, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle"
import * as z from "zod"
import { useCurrentUser } from "../contexts"
import { db } from "../providers"
import { cats } from "../schema"
const catResolverFactory = drizzleResolverFactory(db, "cats")
export const catResolver = resolver.of(cats, {
cats: catResolverFactory.selectArrayQuery(),
age: field(z.number().int())
.derivedFrom("birthday")
.input({
currentYear: z
.number()
.int()
.nullish()
.transform((x) => x ?? new Date().getFullYear()),
})
.resolve((cat, { currentYear }) => {
return currentYear - cat.birthday.getFullYear()
}),
owner: catResolverFactory.relationField("owner"), // [!code ++]
createCats: catResolverFactory.insertArrayMutation({
input: z.object({
values: z
.object({
name: z.string(),
birthday: z.coerce.date(),
})
.transform(async ({ name, birthday }) => ({
name,
birthday,
ownerId: (await useCurrentUser()).id,
}))
.array(),
}),
}),
})
```
```ts title="src/resolvers/user.ts"
import { mutation, query, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle" // [!code ++]
import { eq } from "drizzle-orm"
import * as v from "valibot"
import { useCurrentUser } from "../contexts"
import { db } from "../providers"
import { users } from "../schema"
const userResolverFactory = drizzleResolverFactory(db, "users") // [!code ++]
export const userResolver = resolver.of(users, {
cats: userResolverFactory.relationField("cats"), // [!code ++]
mine: query(users).resolve(() => useCurrentUser()),
usersByName: query(users.$list())
.input({ name: v.string() })
.resolve(({ name }) => {
return db.query.users.findMany({
where: eq(users.name, name),
})
}),
userByPhone: query(users.$nullable())
.input({ phone: v.string() })
.resolve(({ phone }) => {
return db.query.users.findFirst({
where: eq(users.phone, phone),
})
}),
createUser: mutation(users)
.input({
data: v.object({
name: v.string(),
phone: v.string(),
}),
})
.resolve(async ({ data }) => {
const [user] = await db.insert(users).values(data).returning()
return user
}),
})
```
```ts title="src/resolvers/user.ts"
import { mutation, query, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle" // [!code ++]
import { eq } from "drizzle-orm"
import * as z from "zod"
import { useCurrentUser } from "../contexts"
import { db } from "../providers"
import { users } from "../schema"
const userResolverFactory = drizzleResolverFactory(db, "users") // [!code ++]
export const userResolver = resolver.of(users, {
cats: userResolverFactory.relationField("cats"), // [!code ++]
mine: query(users).resolve(() => useCurrentUser()),
usersByName: query(users.$list())
.input({ name: z.string() })
.resolve(({ name }) => {
return db.query.users.findMany({
where: eq(users.name, name),
})
}),
userByPhone: query(users.$nullable())
.input({ phone: z.string() })
.resolve(({ phone }) => {
return db.query.users.findFirst({
where: eq(users.phone, phone),
})
}),
createUser: mutation(users)
.input({
data: z.object({
name: z.string(),
phone: z.string(),
}),
})
.resolve(async ({ data }) => {
const [user] = await db.insert(users).values(data).returning()
return user
}),
})
```
在上面的代码中,我们使用解析器工厂为 `cats` 创建了 `owner` 字段;同样地,我们还为 `users` 创建了 `cats` 字段。\
在幕后,解析器工厂创建的关系字段将使用 `DataLoader` 从数据库查询以避免 N+1 问题。
让我们在演练场尝试一下查询猫的所有者:
```gql title="GraphQL Query" tab="query"
{
cats {
id
name
age
owner {
id
name
phone
}
}
}
```
```json tab="Response"
{
"data": {
"cats": [
{
"id": 1,
"name": "Mittens",
"age": 4,
"owner": {
"id": 1,
"name": "Bob",
"phone": "001"
}
},
{
"id": 2,
"name": "Fluffy",
"age": 3,
"owner": {
"id": 1,
"name": "Bob",
"phone": "001"
}
}
]
}
}
```
让我们尝试一下查询当前用户的猫咪:
```gql title="GraphQL Query" tab="query"
{
mine {
name
cats {
id
name
age
}
}
}
```
```json tab="Headers"
{
"authorization": "001"
}
```
```json tab="Response"
{
"data": {
"mine": {
"name": "Bob",
"cats": [
{
"id": 1,
"name": "Mittens",
"age": 4
},
{
"id": 2,
"name": "Fluffy",
"age": 3
}
]
}
}
}
```
## 总结
在本篇文章中,我们创建了一个简单的 GraphQL 服务端应用。我们使用了以下工具:
* `Valibot` 或者 `Zod`: 用于定义和验证输入;
* `Drizzle`: 用于操作数据库,并且直接使用 `Drizzle` 表格作为 `GraphQL` 输出类型;
* 上下文: 用于在程序的不同部分之间共享数据,这对于实现登录、追踪日志等场景非常有用;
* 解析器工厂: 用于快速创建解析器和操作;
* `GraphQL Yoga`: 用于创建 GraphQL HTTP 服务,并且提供了 GraphiQL 演练场;
我们的应用实现了添加和查询 `users` 和 `cats` 的功能,但限于篇幅没有实现更新和删除功能,可以通过解析器工厂来快速添加。
## 下一步
* 查看 GQLoom 的核心概念:[丝线](./silk)、[解析器](./resolver)、[编织](./weave);
* 了解常用功能:[上下文](./context)、[DataLoader](./dataloader)、[中间件](./middleware)
* 为前端项目添加 GraphQL 客户端:[gql.tada](https://gql-tada.0no.co/)、[Urql](https://commerce.nearform.com/open-source/urql/)、[Apollo Client](https://www.apollographql.com/docs/react)、[TanStack Query](https://tanstack.com/query/latest/docs/framework/react/graphql)、[Graffle](https://graffle.js.org/)
---
title: 介绍
icon: BookMarked
file: ./content/docs/index.zh.mdx
---
## GraphQL 是什么
GraphQL 是一种用于 API 的查询语言,由 Facebook 开发并开源。它允许客户端指定所需的数据结构,从而减少不必要的数据传输,提高 API 的性能和可维护性。
GraphQL 带来了以下优点:
* **类型安全**:强类型查询语言,可以确保从服务端到客户端数据的一致性和安全性。
* **灵活聚合** 自动聚合多个查询,既减少客户端的请求次数,也保证服务端 API 的简洁性。
* **高效查询**:客户端可以指定所需的数据结构,从而减少不必要的数据传输,提高 API 的性能和可维护性。
* **易于扩展**:通过添加新的字段和类型来扩展 API,而不需要修改现有的代码。
* **高效协作**:使用 Schema 作为文档,减少沟通成本,提高开发效率。
* **繁荣生态**: 各类工具与框架不断推陈出新,社区活跃且发展迅速,应用领域广泛且未来前景广阔。
## GQLoom 是什么
GQLoom 是一个 **代码优先(Code-First)** 的 GraphQL Schema 纺织器,用于将 **TypeScript/JavaScript** 生态中的**运行时类型**编织成 GraphQL Schema。
[Zod](https://zod.dev/)、[Valibot](https://valibot.dev/)、[Yup](https://github.com/jquense/yup) 等运行时验证库已经在后端应用开发中得到广泛的使用;同时在使用 [Prisma](https://www.prisma.io/) 、[MikroORM](https://mikro-orm.io/)、[Drizzle](https://orm.drizzle.team/) 等 ORM 库时,我们也会预先定义包含运行时类型的数据库表结构或实体模型。
GQLoom 的职责就是将这些运行时类型编织为 GraphQL Schema。
当使用 GQLoom 开发后端应用时,你只需要使用你熟悉的 Schema 库编写类型,现代的 Schema 库将为你推导 TypeScript 类型,而 GQLoom 将为你编织 GraphQL 类型。\
除此之外,GQLoom 的**解析器工厂**还可以为 [Prisma](./schema/prisma.mdx#解析器工厂)、[MikroORM](./schema/mikro-orm.mdx#解析器工厂)、[Drizzle](./schema/drizzle.mdx#解析器工厂) 生成 CRUD 接口,并支持自定义输入和添加中间件。
GQLoom 的设计受 [tRPC](https://trpc.io/)、[TypeGraphQL](https://typegraphql.com/)启发,在一些技术实现上参考了 [Pothos](https://pothos-graphql.dev/) 。
### 你好,世界
```ts twoslash tab="valibot"
import { resolver, query, weave } from "@gqloom/core"
import { ValibotWeaver } from "@gqloom/valibot"
import * as v from "valibot"
const helloResolver = resolver({
hello: query(v.string())
.input({ name: v.nullish(v.string(), "World") })
.resolve(({ name }) => `Hello, ${name}!`),
})
export const schema = weave(ValibotWeaver, helloResolver)
```
```ts twoslash tab="zod"
import { resolver, query, weave } from "@gqloom/core"
import { ZodWeaver } from "@gqloom/zod"
import * as z from "zod"
const helloResolver = resolver({
hello: query(z.string())
.input({ name: z.string().nullish() })
.resolve(({ name }) => `Hello, ${name ?? "World"}!`),
})
export const schema = weave(ZodWeaver, helloResolver)
```
### 亮点
* 🧑💻 **开发体验**:更少的样板代码、语义化的 API 设计、广泛的生态集成使开发愉快;
* 🔒 **类型安全**:从 Schema 自动推导类型,在开发时享受智能提示,在编译时发现潜在问题;
* 🎯 **接口工厂**:寻常的 CRUD 接口太简单又太繁琐了,交给解析器工厂来快速创建;
* 🔋 **整装待发**:中间件、上下文、订阅、联邦图已经准备就绪;
* 🔮 **抛却魔法**:没有装饰器、没有元数据和反射、没有代码生成,只需要 JavaScript/TypeScript 就可以在任何地方运行;
* 🧩 **丰富集成**:使用你最熟悉的验证库和 ORM 来建构你的下一个 GraphQL 应用;
---
title: 中间件(Middleware)
icon: Fence
file: ./content/docs/middleware.zh.mdx
---
中间件是一种函数,它介入了解析函数的处理流程。它提供了一种在请求和响应流程中插入逻辑的方式,以便在发送响应之前或在请求处理之前执行代码。
`GQLoom` 的中间件遵循了 [Koa](https://koajs.com/#application) 的洋葱式中间件模式。
## 定义中间件
中间件是一个函数,它将在调用时被注入 `options` 对象作为参数,`options` 包含以下属性:
* `outputSilk`: 输出丝线,包含了当前正在解析字段的输出类型;
* `parent`: 当前字段的父节点,相当于 `useResolverPayload().root`;
* `parseInput`: 用于获取或修改当前字段的输入;
* `type`: 当前字段的类型,其值为 `query`, `mutation`, `subscription` 或 `field`;
* `next`: 用于调用下一个中间件的函数;
`options` 还可以直接作为 `next` 函数使用。
另外,我们还可以通过 `useContext()` 和 `useResolverPayload()` 获取到当前解析函数的上下文以及更多信息。
一个最基础的中间件函数如下:
```ts twoslash
import { ValibotWeaver, weave, resolver, query } from "@gqloom/valibot"
import * as v from "valibot"
import { createServer } from "node:http"
import { createYoga } from "graphql-yoga"
import { outputValidator, valibotExceptionFilter } from "./middlewares"
const helloResolver = resolver({
hello: query(v.pipe(v.string(), v.minLength(10)))
.input({ name: v.string() })
.use(outputValidator) // [!code hl]
.resolve(({ name }) => `Hello, ${name}`),
})
export const schema = weave(ValibotWeaver, helloResolver, valibotExceptionFilter) // [!code hl]
const yoga = createYoga({ schema })
createServer(yoga).listen(4000, () => {
// eslint-disable-next-line no-console
console.info("Server is running on http://localhost:4000/graphql")
})
```
在上面的代码中,我们对 `hello` 查询的输出添加了 `v.minLength(10)` 的要求,并在解析函数中添加了 `outputValidator` 中间件。
我们还在 `weave` 中添加了一个全局中间件 `ValibotExceptionFilter`。
```ts twoslash
import { weave, resolver, query } from "@gqloom/zod"
import * as z from "zod"
import { createServer } from "node:http"
import { createYoga } from "graphql-yoga"
import { outputValidator, zodExceptionFilter } from "./middlewares"
const helloResolver = resolver({
hello: query(z.string().min(10))
.input({ name: z.string() })
.use(outputValidator) // [!code hl]
.resolve(({ name }) => `Hello, ${name}`),
})
export const schema = weave(helloResolver, zodExceptionFilter) // [!code hl]
const yoga = createYoga({ schema })
createServer(yoga).listen(4000, () => {
// eslint-disable-next-line no-console
console.info("Server is running on http://localhost:4000/graphql")
})
```
在上面的代码中,我们对 `hello` 查询的输出添加了 `z.string().min(10)` 的要求,并在解析函数中添加了 `outputValidator` 中间件。
我们还在 `weave` 中添加了一个全局中间件 `ValibotExceptionFilter`。
当我们进行以下查询时:
```graphql title="GraphQL Query"
{
hello(name: "W")
}
```
将会得到类似如下的结果:
```json
{
"errors": [
{
"message": "Invalid length: Expected >=10 but received 8",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"hello"
],
"extensions": {
"issues": [
{
"kind": "validation",
"type": "min_length",
"input": "Hello, W",
"expected": ">=10",
"received": "8",
"message": "Invalid length: Expected >=10 but received 8",
"requirement": 10
}
]
}
}
],
"data": null
}
```
```json
{
"errors": [
{
"message": "String must contain at least 10 character(s)",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"hello"
],
"extensions": {
"issues": [
{
"code": "too_small",
"minimum": 10,
"type": "string",
"inclusive": true,
"exact": false,
"message": "String must contain at least 10 character(s)",
"path": []
}
]
}
}
],
"data": null
}
```
如果我们调整输入,使返回的字符串长度符合要求:
```graphql title="GraphQL Query"
{
hello(name: "World")
}
```
将会得到没有异常的响应:
```json
{
"data": {
"hello": "Hello, World"
}
}
```
### 鉴权
对用户的权限进行校验是一个常见的需求,我们可以通过中间件来轻易实现。
考虑我们的用户有 `"admin"` 和 `"editor"` 两种角色,我们希望管理员和编辑员分别可以访问自己的操作。
首先,我们实现一个 `authGuard` 中间件,用于校验用户的角色:
```ts twoslash
import { type Middleware } from "@gqloom/core"
import { useUser } from "./context"
import { GraphQLError } from "graphql"
export function authGuard(role: "admin" | "editor"): Middleware {
return async (next) => {
const user = await useUser()
if (user == null) throw new GraphQLError("Not authenticated")
if (!user.roles.includes(role)) throw new GraphQLError("Not authorized")
return next()
}
}
```
在上面的代码中,我们声明了一个 `authGuard` 中间件,它接受一个角色参数,并返回一个中间件函数。
中间件函数会检查用户是否已经认证,并且是否具有指定的角色,如果不符合要求,则抛出一个 `GraphQLError` 异常。
我们可以为不同的解析器应用不同的中间件:
```ts twoslash
import { resolver, mutation } from "@gqloom/core"
import * as v from "valibot"
import { authGuard } from "./middlewares"
const adminResolver = resolver(
{
deleteArticle: mutation(v.boolean(), () => true),
},
{
middlewares: [authGuard("admin")], // [!code hl]
}
)
const editorResolver = resolver(
{
createArticle: mutation(v.boolean(), () => true),
updateArticle: mutation(v.boolean(), () => true),
},
{ middlewares: [authGuard("editor")] } // [!code hl]
)
```
```ts twoslash
import { resolver, mutation } from "@gqloom/zod"
import * as z from "zod"
import { authGuard } from "./middlewares"
const adminResolver = resolver(
{
deleteArticle: mutation(z.boolean(), () => true),
},
{
middlewares: [authGuard("admin")], // [!code hl]
}
)
const editorResolver = resolver(
{
createArticle: mutation(z.boolean(), () => true),
updateArticle: mutation(z.boolean(), () => true),
},
{ middlewares: [authGuard("editor")] } // [!code hl]
)
```
在上面的代码中,我们为 `adminResolver` 和 `editorResolver` 分别应用了 `authGuard` 中间件,并指定了不同的角色。这样,只有具有相应角色的用户才能访问对应解析器内的操作。
### 日志
我们也可以通过中间件来实现日志记录功能。例如,我们可以创建一个 `logger` 中间件,用于记录每个字段解析函数的执行时间:
```ts twoslash
import { mutation, resolver } from "@gqloom/core"
import * as v from "valibot"
const Post = v.object({
__typename: v.nullish(v.literal("Post")),
id: v.number(),
title: v.string(),
content: v.string(),
authorId: v.number(),
})
interface IPost extends v.InferOutput {}
const posts: IPost[] = []
export const postsResolver = resolver({
createPost: mutation(Post)
.input(
v.object({
title: v.string(),
content: v.string(),
authorId: v.number(),
})
)
.use(async ({ next, parseInput }) => { // [!code hl]
const result = await parseInput.getResult() // [!code hl]
result.authorId = (await useUser()).id // [!code hl]
parseInput.setResult(result) // [!code hl]
return next() // [!code hl]
})
.resolve(({ title, content, authorId }) => {
const post = {
id: Math.random(),
title,
content,
authorId,
}
posts.push(post)
return post
}),
})
```
```ts twoslash
import { mutation, resolver } from "@gqloom/core"
import { z } from "zod"
const Post = z.object({
__typename: z.literal("Post").nullish(),
id: z.number(),
title: z.string(),
content: z.string(),
authorId: z.number(),
})
interface IPost extends z.output {}
const posts: IPost[] = []
export const postsResolver = resolver({
createPost: mutation(Post)
.input(
z.object({
title: z.string(),
content: z.string(),
authorId: z.number(),
})
)
.use(async ({ next, parseInput }) => { // [!code hl]
const result = await parseInput.getResult() // [!code hl]
result.authorId = (await useUser()).id // [!code hl]
parseInput.setResult(result) // [!code hl]
return next() // [!code hl]
})
.resolve(({ title, content, authorId }) => {
const post = {
id: Math.random(),
title,
content,
authorId,
}
posts.push(post)
return post
}),
})
```
## 使用中间件
GQLoom 能够在各种范围内应用中间件,包括解析函数、解析器局部中间件和全局中间件。
### 解析函数中间件
我们可以在解析函数中直接使用中间件,只需要在构造时使用 `use` 方法,例如:
```ts twoslash
import { resolver, query } from "@gqloom/core"
import * as v from "valibot"
import { outputValidator } from "./middlewares"
const helloResolver = resolver({
hello: query(v.pipe(v.string(), v.minLength(10)))
.input({ name: v.string() })
.use(outputValidator) // [!code hl]
.resolve(({ name }) => `Hello, ${name}`),
})
```
```ts twoslash
import { resolver, query } from "@gqloom/zod"
import * as z from "zod"
import { outputValidator } from "./middlewares"
const helloResolver = resolver({
hello: query(z.string().min(10))
.input({ name: z.string() })
.use(outputValidator) // [!code hl]
.resolve(({ name }) => `Hello, ${name}`),
})
```
### 解析器局部中间件
我们也可以在解析器级别应用中间件,这样中间件将对解析器内的所有操作生效。
只需要使用 `use` 方法为解析器构添加 `middlewares`:
```ts twoslash
import { resolver, mutation } from "@gqloom/core"
import * as v from "valibot"
import { authGuard } from "./middlewares"
const adminResolver = resolver({
deleteArticle: mutation(v.boolean(), () => true),
}).use(authGuard("admin")) // [!code hl]
const editorResolver = resolver({
createArticle: mutation(v.boolean(), () => true),
updateArticle: mutation(v.boolean(), () => true),
}).use(authGuard("editor")) // [!code hl]
```
```ts twoslash
import { resolver, mutation } from "@gqloom/zod"
import * as z from "zod"
import { authGuard } from "./middlewares"
const adminResolver = resolver({
deleteArticle: mutation(z.boolean(), () => true),
}).use(authGuard("admin")) // [!code hl]
const editorResolver = resolver({
createArticle: mutation(z.boolean(), () => true),
updateArticle: mutation(z.boolean(), () => true),
}).use(authGuard("editor")) // [!code hl]
```
### 全局中间件
为了应用全局中间件,我们需要在 `weave` 函数中传入中间件字段,例如:
```ts
import { weave } from "@gqloom/core"
import { exceptionFilter } from "./middlewares"
export const schema = weave(helloResolver, exceptionFilter) // [!code hl]
```
### 根据操作类型应用中间件
我们可以为中间件指定在哪些操作类型上生效。
```ts twoslash
import type { Middleware } from "@gqloom/core"
import { GraphQLError } from "graphql"
export const transaction: Middleware = async ({ next }) => {
try {
await db.beginTransaction()
const result = await next()
await db.commit()
return result
} catch (error) {
await db.rollback()
throw new GraphQLError("Transaction failed", {
extensions: { originalError: error },
})
}
}
transaction.operations = ["mutation"]
```
`Middleware.operations` 是一个字符串数组,用于指定中间件在哪些操作类型上生效,可选值为:
* `"query"`;
* `"mutation"`;
* `"subscription"`;
* `"field"`;
* `"subscription.resolve"`;
* `"subscription.subscribe"`;
`Middleware.operations` 的默认值为 `["field", "query", "mutation", "subscription.subscribe"]`。
---
title: 解析器(Resolver)
icon: RadioTower
file: ./content/docs/resolver.zh.mdx
---
解析器(Resolver)是用来放着 GraphQL 操作(`query`、`mutation`、`subscription`)的地方。
通常我们将业务相近的操作放在同一个解析器中,比如将用户相关的操作放在一个名为 `userResolver` 的解析器中。
## 区分操作
首先,我们简单地了解一下 GraphQL 的基本操作和应该在什么时候使用它们:
* **查询(Query)** 是用来获取数据的操作,比如获取用户信息、获取商品列表等。查询操作通常不会改变服务的持久化数据。
* **变更(Mutation)** 是用来修改数据的操作,比如创建用户、更新用户信息、删除用户等。变更操作通常会改变服务的持久化数据。
* **订阅(Subscription)** 是服务端主动推送数据给客户端的操作,订阅操作通常不会改变服务的持久化数据。或者说,订阅是实时的查询(Query)。
## 定义解析器
我们使用 `resolver` 函数来定义解析器:
```ts twoslash
const userMap: Map = new Map(
[
{ id: 1, name: "Cao Xueqin" },
{ id: 2, name: "Wu Chengen" },
].map((user) => [user.id, user])
)
const bookMap: Map = 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`:
```ts twoslash
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())),
})
```
```ts twoslash
import { resolver, query } from '@gqloom/zod'
import * as z from "zod"
const bookResolver = resolver.of(Book, {
books: query(z.array(Book)).resolve(() => Array.from(bookMap.values())),
})
```
在上面的代码中,我们使用 `resolver.of` 函数来定义 `bookResolver`,它是一个对象解析器,用于解析 `Book` 对象。
在 `bookResolver` 中,我们定义了一个 `books` 字段,它是一个查询操作,用于获取所有的书籍。
接下来,我们将为 `Book` 对象添加一个名为 `author` 的额外字段用于获取书籍的作者:
```ts twoslash
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)), // [!code hl]
})
```
```ts twoslash
import { resolver, query, field } from '@gqloom/zod'
import * as z from "zod"
const bookResolver = resolver.of(Book, {
books: query(z.array(Book)).resolve(() => Array.from(bookMap.values())),
author: field(User.nullish()).resolve((book) => userMap.get(book.authorID)), // [!code hl]
})
```
在上面的代码中,我们使用 `field` 函数来定义 `author` 字段。
`field` 函数接受两个参数:
* 第一个参数是字段的返回类型;
* 第二个参数是解析函数或选项,在这里我们使用了一个解析函数:我们从解析函数的第一个参数获取 `Book` 实例,然后根据 `authorID` 字段从 `userMap` 中获取对应的 `User` 实例。
### 定义字段输入
在 GraphQL 中,我们可以为字段定义输入参数,以便在查询时传递额外的数据。
在 `GQLoom` 中,我们可以使用 `field` 函数的第二个参数来定义字段的输入参数。
```ts twoslash
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()) // [!code hl]
.input({ name: v.string() }) // [!code hl]
.resolve((book, { name }) => { // [!code hl]
return `The book ${book.title} is in ${name}'s collection.` // [!code hl]
}), // [!code hl]
})
```
```ts twoslash
import { resolver, query, field } from '@gqloom/zod'
import * as z from "zod"
const bookResolver = resolver.of(Book, {
books: query(z.array(Book)).resolve(() => Array.from(bookMap.values())),
author: field(User.nullish()).resolve((book) => userMap.get(book.authorID)),
signature: field(z.string()) // [!code hl]
.input({ name: z.string() }) // [!code hl]
.resolve((book, { name }) => { // [!code hl]
return `The book ${book.title} is in ${name}'s collection.` // [!code hl]
}), // [!code hl]
})
```
在上面的代码中,我们使用 `field` 函数来定义 `signature` 字段。
`field` 函数的第二个参数是一个对象,它包含两个字段:
* `input`:字段的输入参数,它是一个对象,包含一个 `name` 字段,它的类型是 `string`;
* `resolve`:字段的解析函数,它接受两个参数:第一个参数由 `resolver.of` 构造的解析器的源对象,即 `Book` 实例;第二个参数是字段的输入参数,即包含 `name` 字段的输入。
刚刚我们定义的 `bookResolver` 对象可以通过 [weave](../weave) 函数编织成 GraphQL schema:
```ts
import { weave } from '@gqloom/core'
import { ValibotWeaver } from '@gqloom/valibot'
export const schema = weave(ValibotWeaver, bookResolver)
```
```ts
import { weave } from '@gqloom/core'
import { ZodWeaver } from '@gqloom/zod'
export const schema = weave(ZodWeaver, bookResolver)
```
最终得到的 GraphQL schema 如下:
```graphql title="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()` 使用,这个函数用于精确获取当前查询所需要的字段。
```ts
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
}),
})
```
```ts
import { field, resolver } from "@gqloom/core"
import * as z from "zod"
import { giraffes } from "./table"
export const giraffeResolver = resolver.of(giraffes, {
age: field(z.number())
.derivedFrom("birthDate")
.resolve((giraffe) => {
const today = new Date()
const age = today.getFullYear() - giraffe.birthDate.getFullYear()
return age
}),
})
```
```ts
import * as sqlite from "drizzle-orm/sqlite-core"
import { drizzleSilk } from "@gqloom/drizzle"
export const giraffes = drizzleSilk(
sqlite.sqliteTable("giraffes", {
id: sqlite.integer("id").primaryKey(),
name: sqlite.text("name").notNull(),
birthDate: sqlite.integer({ mode: "timestamp" }).notNull(),
})
)
```
---
title: 丝线(Silk)
icon: Volleyball
file: ./content/docs/silk.zh.mdx
---
`GQLoom` 的全称是 GraphQL Loom,即 GraphQL 纺织机。
丝线(Silk)是纺织机的基本原料,它同时反应 GraphQL 类型和 TypeScript 类型。
在开发时,我们使用现有的模式库的 Schema 作为丝线,最终 `GQLoom` 会将丝线编织进 GraphQL Schema。
## 简单的标量丝线
我们可以使用 `silk` 函数创建一个简单的标量丝线:
```ts twoslash
import { silk } from "@gqloom/core"
import { GraphQLString, GraphQLInt, GraphQLNonNull } from "graphql"
const StringSilk = silk(GraphQLString)
const IntSilk = silk(GraphQLInt)
const NonNullStringSilk = silk(new GraphQLNonNull(GraphQLString))
const NonNullStringSilk1 = silk.nonNull(StringSilk)
```
## 对象丝线
我们可以直接使用 [graphql.js](https://graphql.org/graphql-js/constructing-types/) 构造 GraphQL 对象:
```ts twoslash
import { silk } from "@gqloom/core"
import {
GraphQLObjectType,
GraphQLNonNull,
GraphQLString,
GraphQLInt,
} from "graphql"
interface ICat {
name: string
age: number
}
const Cat = silk(
new GraphQLObjectType({
name: "Cat",
fields: {
name: { type: new GraphQLNonNull(GraphQLString) },
age: { type: new GraphQLNonNull(GraphQLInt) },
},
})
)
```
在上面的代码中:我们定义了一个 `ICat` 接口,并使用 `silk` 函数定义了一个名为 `Cat` 的丝线。
其中,`silk` 函数接受 `ICat` 作为泛型参数,还接受一个 `GraphQLObjectType` 实例用来阐述 `Cat` 在 GraphQL 中的详细结构。
`Cat` 在 GraphQL 中将呈现为:
```graphql title="GraphQL Schema"
type Cat {
name: String!
age: Int!
}
```
你可能注意到了,使用 `graphql.js` 来创建丝线需要同时声明 `ICat` 接口和 `GraphQLObjectType`,也就是说,我们为 `Cat` 创建了两份定义。
重复的定义让代码损失了简洁性,也增加了维护成本。
## 使用模式库创建丝线
好在,我们有像 [Valibot](https://valibot.dev/)、[Zod](https://zod.dev/) 这样的模式库,它们创建的 Schema 将携带 TypeScript 类型,并在运行时仍然携带类型。
`GQLoom` 可以直接使用这些 Schema 作为丝线,而不需要重复定义。
`GQLoom` 目前已经集成来自以下库的 Schema:
* [Valibot](../schema/valibot)
* [Zod](../schema/zod)
* [Yup](../schema/yup)
* [Mikro ORM](../schema/mikro-orm)
* [Prisma](../schema/prisma)
* [Drizzle](../schema/drizzle)
### 使用 Valibot 创建丝线
```ts twoslash
import * as v from "valibot"
const StringSilk = v.string()
const BooleanSilk = v.boolean()
const Cat = v.object({
__typename: v.literal("Cat"),
name: v.string(),
age: v.number(),
})
```
在上面的代码中,我们使用 [Valibot](https://valibot.dev/) 创建了一些简单的 Schema 作为丝线,你可以在[Valibot 集成](../schema/valibot)章节中了解如何使用 [Valibot](https://valibot.dev/) 创建更复杂的类型。
### 使用 Zod 创建丝线
```ts twoslash
import * as z from "zod"
const StringSilk = z.string()
const BooleanSilk = z.boolean()
const Cat = z.object({
__typename: z.literal("Cat"),
name: z.string(),
age: z.number(),
})
```
在上面的代码中,我们使用 [Zod](https://zod.dev/) 创建了一些简单的 Schema 作为丝线,你可以在[Zod 集成](../schema/zod)章节中了解如何使用 [Zod](https://zod.dev/) 创建更复杂的类型。
`GQLoom` 核心库遵循了 [标准 Schema 规范](https://github.com/standard-schema/standard-schema),得益于 `Valibot`、`Zod` 同样遵循此规范,我们不需要使用额外的包装函数就可以将来自 Valibot、Zod 的 Schema 作为丝线使用。
---
title: 编织(Weave )
icon: Waves
file: ./content/docs/weave.zh.mdx
---
在 GQLoom 中,`weave` 函数用于将多个解析器(Resolver)或丝线(Silk)编织到一张 GraphQL Schema 中。
`weave` 函数可以接收[解析器](./resolver)、[丝线](./silk)、编织器配置、全局[中间件](./middleware)
## 编织解析器
最常见的用法是将多个解析器编织到一起,例如:
```ts
import { weave } from '@gqloom/core';
export const schema = weave(helloResolver, catResolver);
```
## 编织单独丝线
有时候,我们需要将一个单独的[丝线](../silk)编织到 GraphQL Schema 中,例如:
```ts twoslash
import { weave } from '@gqloom/core'
import { ValibotWeaver } from '@gqloom/valibot'
import * as v from "valibot"
const Dog = v.object({
__typename: v.nullish(v.literal("Dog")),
name: v.string(),
age: v.number(),
})
export const schema = weave(ValibotWeaver, helloResolver, catResolver, Dog);
```
```ts twoslash
import { weave } from '@gqloom/core'
import { ZodWeaver } from '@gqloom/zod'
import * as z from "zod"
const Dog = z.object({
__typename: z.literal("Dog").nullish(),
name: z.string(),
age: z.number(),
})
export const schema = weave(ZodWeaver, helloResolver, catResolver, Dog);
```
## 编织器配置
### 输入类型命名转换
在 GraphQL 中,对象分为 [type](https://graphql.org/graphql-js/object-types/) 和 [input](https://graphql.org/graphql-js/mutations-and-input-types/) 类型。
而使用 `GQLoom` 时,我们一般只使用 `object` 类型,在幕后 `GQLoom` 会自动将 `object` 类型转换为 `input` 类型。
这样做的好处是我们可以直接使用 `object` 类型来定义输入参数,而无需手动定义 `input` 类型。
但是当我们将同一个 `object` 类型同时用于 `type` 和 `input` 时,将因为命名冲突而无法编织成 GraphQL Schema。
让我们来看一个例子:
```ts twoslash
import { resolver, mutation, weave } from '@gqloom/core'
import { ValibotWeaver } from '@gqloom/valibot'
import * as v from "valibot"
const Cat = v.object({
__typename: v.nullish(v.literal("Cat")),
name: v.string(),
birthDate: v.string(),
})
const catResolver = resolver({
createCat: mutation(Cat, {
input: {
data: Cat,
},
resolve: ({ data }) => data,
}),
})
export const schema = weave(ValibotWeaver, catResolver);
```
```ts twoslash
import { resolver, mutation, weave } from '@gqloom/zod'
import { ZodWeaver } from '@gqloom/zod'
import * as z from "zod"
const Cat = z.object({
__typename: z.literal("Cat").nullish(),
name: z.string(),
birthDate: z.string(),
})
const catResolver = resolver({
createCat: mutation(Cat, {
input: {
data: Cat,
},
resolve: ({ data }) => data,
}),
})
export const schema = weave(ZodWeaver, catResolver);
```
在上面的代码中,我们定义了一个 `Cat` 对象,并将其用于 `type` 和 `input`。但是当我们尝试将 `catResolver` 编织到 GraphQL Schema 中时,会抛出一个错误,提示 `Cat` 名称重复:
```bash
Error: Schema must contain uniquely named types but contains multiple types named "Cat".
```
要解决这个问题,我们需要为 `input` 类型指定一个不同的名称。我们可以使用 `SchemaWeaver.config` 配置中的 `getInputObjectName` 选项来实现这一点:
```ts twoslash
import { resolver, mutation, weave, GraphQLSchemaLoom } from '@gqloom/core'
import { ValibotWeaver } from '@gqloom/valibot'
import * as v from "valibot"
const Cat = v.object({
__typename: v.nullish(v.literal("Cat")),
name: v.string(),
birthDate: v.string(),
})
const catResolver = resolver({
createCat: mutation(Cat, {
input: {
data: Cat,
},
resolve: ({ data }) => data,
}),
})
export const schema = weave(
catResolver,
ValibotWeaver,
GraphQLSchemaLoom.config({ getInputObjectName: (name) => `${name}Input` }) // [!code hl]
)
```
```ts twoslash
import { resolver, mutation, weave, GraphQLSchemaLoom } from '@gqloom/zod'
import * as z from "zod"
const Cat = z.object({
__typename: z.literal("Cat").nullish(),
name: z.string(),
birthDate: z.string(),
})
const catResolver = resolver({
createCat: mutation(Cat, {
input: {
data: Cat,
},
resolve: ({ data }) => data,
}),
})
export const schema = weave(
catResolver,
GraphQLSchemaLoom.config({ getInputObjectName: (name) => `${name}Input` }) // [!code hl]
)
```
如此一来,`Cat` 对象将被转换为 `CatInput` 类型,从而避免了命名冲突。
以上的 `catResolver` 将编织得到如下 GraphQL Schema:
```graphql title="GraphQL Schema"
type Mutation {
createCat(data: CatInput!): Cat!
}
type Cat {
name: String!
birthDate: String!
}
input CatInput {
name: String!
birthDate: String!
}
```
## 全局中间件
```ts
import { weave } from '@gqloom/core';
import { logger } from './middlewares';
export const schema = weave(helloResolver, catResolver, logger)
```
在[中间件章节](./middleware)中查看更多关于中间件的用法。