--- 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)中查看更多关于中间件的用法。