2018 年 2 月 6 日

GraphQL 基础:解析 GraphQL 解析器中的 `info` 参数

GraphQL 服务器的结构和实现(第三部分)

GraphQL Basics

如果您之前编写过 GraphQL 服务器,那么您很有可能已经遇到过传递到解析器中的 info 对象。幸运的是,在大多数情况下,您并不真正需要了解它实际的作用以及它在查询解析过程中的作用。

然而,在一些边缘情况下,info 对象是造成许多困惑和误解的原因。本文的目标是深入了解 info 对象的内部结构,并阐明其在 GraphQL 执行过程中的作用。

本文假设您已经熟悉 GraphQL 查询和 mutation 如何解析的基础知识。如果您在这方面感到有些不确定,您绝对应该查看本系列文章的前几篇:第一部分:GraphQL 模式(必需)第二部分:网络层(可选)

info 对象的结构

回顾:GraphQL 解析器的签名

快速回顾一下,当使用 GraphQL.js 构建 GraphQL 服务器时,您有两个主要任务

  • 定义您的 GraphQL 模式(以 SDL 或普通 JS 对象的形式)
  • 对于模式中的每个字段,实现一个 解析器 函数,该函数知道如何返回该字段的值

解析器函数接受四个参数(按此顺序)

  1. parent:前一个解析器调用的结果(更多信息)。
  2. args:解析器字段的参数。
  3. context:每个解析器可以从中读取/写入的自定义对象。
  4. info这就是我们将在本文中讨论的内容。

这是一个简单 GraphQL 查询的执行过程以及相关解析器的调用概述。由于第二层解析器的解析是微不足道的,因此实际上没有必要实现这些解析器 - 它们的返回值由 GraphQL.js 自动推断。

GraphQL 解析器链中 parentargs 参数的概述

info 包含查询 AST 和更多执行信息

那些对 info 对象的结构和作用感到好奇的人仍然一头雾水。官方 规范文档都没有提到它。曾经有一个 GitHub 问题要求更好地记录它,但该问题在没有采取明显行动的情况下被关闭。因此,除了深入研究代码之外,别无他法。

从非常高的层面来说,可以说 info 对象包含传入的 GraphQL 查询的 AST。因此,解析器知道它们需要返回哪些字段。

要了解有关查询 AST 的更多信息,请务必查看 Christian Joudrey 的精彩文章 GraphQL 查询的生命周期 — 词法分析/解析 以及 Eric Baer 的精彩演讲 GraphQL 内幕

要了解 info 的结构,让我们看一下 它的 Flow 类型定义

以下是这些键的概述和快速说明

  • fieldName:如前所述,GraphQL 模式中的每个字段都需要由解析器支持。fieldName 包含属于当前解析器的字段的名称。
  • fieldNodes:一个数组,其中每个对象都表示剩余的选择集中的一个字段。
  • returnType:相应字段的 GraphQL 类型。
  • parentType:此字段所属的 GraphQL 类型。
  • path:跟踪直到到达当前字段(即解析器)所遍历的字段。
  • schema:表示您的可执行模式的 GraphQLSchema 实例。
  • fragments:查询文档中包含的 片段的映射。
  • rootValue:传递给执行的 rootValue 参数。
  • operation整个查询的 AST。
  • variableValues:与查询一起提供的任何变量的映射,对应于 variableValues 参数。

如果这仍然看起来很抽象,请不要担心,我们很快就会看到所有这些的示例。

特定字段与全局

关于上面的键有一个有趣的观察结果。 info 对象上的键要么是特定于字段的,要么是全局的。

特定于字段 仅仅意味着该键的值取决于传递 info 对象的字段(及其支持的解析器)。明显的例子是 fieldNamerootTypeparentType。考虑以下 GraphQL 类型的 author 字段

该字段的 fieldName 只是 authorreturnTypeUser!,而 parentTypeQuery

现在,对于 feed,这些值当然会有所不同:fieldNamefeedreturnType[Post!]!,而 parentType 也是 Query

所以,这三个键的值是特定于字段的。其他特定于字段的键有:fieldNodespath。实际上,上述 Flow 定义中的前五个键都是特定于字段的。

另一方面,全局意味着这些键的值不会改变——无论我们讨论的是哪个解析器。schemafragmentsrootValueoperationvariableValues 对于所有解析器都将始终携带相同的值。

一个简单的例子

现在让我们来看一个关于 info 对象内容的示例。为了做好准备,这里是我们用于此示例的 schema 定义

假设该 schema 的解析器实现如下

请注意,实际上不需要 Post.title 解析器,我们仍然在此处包含它,以查看调用解析器时 info 对象的外观。

现在考虑以下查询

为了简洁起见,我们仅讨论 Query.author 字段的解析器,而不是 Post.title 的解析器(当执行上述查询时仍会调用该解析器)。

如果您想使用此示例进行实验,我们准备了一个 存储库,其中包含上述 schema 的运行版本,以便您可以进行实验!

接下来,让我们看一下 info 对象中的每个键,并查看调用 Query.author 解析器时的外观(您可以在此处找到 info 对象的完整日志输出)。

fieldName

fieldName 只是 author

fieldNodes

请记住,fieldNodes 是特定于字段的。它实际上包含查询 AST 的一个摘录。此摘录从当前字段(即 author)开始,而不是从查询的开始。(从根开始的整个查询 AST 存储在 operation 中,请参见下文)。

returnType & parentType

如前所述,returnTypeparentType 相当简单

path

path 跟踪到当前字段为止所遍历的字段。对于 Query.author,它看起来很简单:"path": { "key": "author" }

为了进行比较,在 Post.title 解析器中,path 看起来如下

其余五个字段属于“全局”类别,因此对于 Post.title 解析器将是相同的。

schema

schema 是对可执行 schema 的引用。

fragments

fragments 包含片段定义,由于查询文档没有任何这些定义,因此它只是一个空映射:{}

rootValue

如前所述,rootValue 键的值对应于首先传递给 graphql 执行函数的 rootValue 参数。在本例中,它只是 null

operation

operation 包含传入查询的完整 查询 AST。回想一下,除其他信息外,这还包含我们在上面看到的 fieldNodes 的相同值

variableValues

此键表示已为查询传递的任何变量。由于我们的示例中没有变量,因此该值再次只是一个空映射:{}

如果查询是用变量编写的

variableValues 键将仅具有以下值

使用 GraphQL 绑定时 info 的作用

如本文开头所述,在大多数情况下,您根本不需要担心 info 对象。它只是您解析器签名的一部分,但您实际上并没有将其用于任何用途。那么,它何时变得相关呢?

info 传递给绑定函数

如果您以前使用过 GraphQL 绑定,那么您已经看到 info 对象是生成的绑定函数的一部分。考虑以下 schema

使用 graphql-binding,您现在可以通过调用专用的绑定函数而不是发送原始查询和突变来发送可用的查询和突变。

例如,考虑以下原始查询,检索特定的 User

使用绑定函数实现相同的功能如下所示

通过调用绑定实例上的 user 函数并传递相应的参数,我们传达的信息与上面的原始 GraphQL 查询完全相同。

graphql-binding 中的绑定函数接受三个参数

  1. args:包含字段的参数(例如,上面 createUser 突变的 username)。
  2. context:向下传递解析器链的 context 对象。
  3. infoinfo 对象。请注意,除了 GraphQLResolveInfo 的实例(info 的类型)之外,您还可以传递一个简单地定义选择集的字符串。

使用 Prisma 将应用程序 schema 映射到数据库 schema

info 对象可能引起混淆的另一个常见用例是基于 Prismaprisma-binding 实现 GraphQL 服务器。

在这种情况下,其思想是有两个 GraphQL 层

  • 数据库层由 Prisma 自动生成,并提供通用且强大的 CRUD API
  • 应用程序层* 定义暴露给客户端应用程序的 GraphQL API,并根据您的应用程序需求进行定制

作为后端开发人员,您有责任为应用程序层定义应用程序 schema 并实现其解析器。借助 prisma-binding,解析器的实现仅仅是一个将传入查询 委派 到基础数据库 API 的过程,而没有大的开销。

让我们考虑一个简单的例子——假设您从以下 Prisma 数据库服务的数据模型开始

Prisma 基于此数据模型生成的数据库 schema 类似于此

现在,假设您想构建一个类似于此的应用程序 schema

feed 查询不仅返回 Post 元素的列表,而且还能够返回列表的 count。请注意,它可选地采用一个 authorId,该 authorId 会过滤 feed 以仅返回特定 User 编写的 Post 元素。

实现此应用程序 schema 的第一个直觉可能如下所示。

实现 1:此实现看起来正确,但存在细微缺陷

此实现看起来足够合理。在 feed 解析器内部,我们正在根据可能传入的 authorId 构建 authorFilter。然后,使用 authorFilter 执行 posts 查询并检索 Post 元素,以及 postsConnection 查询,该查询可以访问列表的 count

也可以仅使用 postsConnection 查询来检索实际的 Post 元素。为了简单起见,我们仍然使用 posts 查询来实现此目的,并将另一种方法留给细心的读者练习。

实际上,当您以此实现启动 GraphQL 服务器时,乍一看一切都会很好。您会注意到简单的查询得到正确处理,例如,以下查询将成功

只有当您尝试检索 Post 元素的 author 时,您才会遇到问题

好吧!因此,由于某种原因,该实现没有返回 author,这会触发错误“不能为不可为空的 Post.author 返回 null。”,因为 Post.author 字段在应用程序 schema 中标记为必需。

让我们再次看一下实现的相关部分

这是我们检索 Post 元素的位置。但是,我们没有将选择集传递给 posts 绑定函数。如果未将第二个参数传递给 Prisma 绑定函数,则默认行为是查询该类型的所有标量字段。

这确实解释了这种行为。对 ctx.db.query.posts 的调用返回了正确的 Post 元素集,但仅返回了它们的 idtitle 值——没有关于 author 的关系数据。

那么,我们该如何解决这个问题呢?显然,我们需要一种方法来告诉 posts 绑定函数需要返回哪些字段。但是,在 feed 解析器的上下文中,这些信息在哪里呢?你能猜到吗?

正确:info 对象内部!因为 Prisma 绑定函数的第二个参数可以是字符串 info 对象,所以我们只需将传入 feed 解析器的 info 对象传递给 posts 绑定函数。

这个查询在实现 2 中会失败,错误信息为:“类型为 ‘Post’ 的字段 ‘posts’ 必须有一个子选择。”

然而,使用此实现,任何请求都无法正确处理。例如,考虑以下查询:

错误消息 “类型为 ‘Post’ 的字段 ‘posts’ 必须有一个子选择。” 是由上述实现的第 8 行产生的。

那么,这里发生了什么?失败的原因是 info 对象中特定字段的键与 posts 查询不匹配。

feed 解析器内部打印 info 对象可以更清楚地了解情况。让我们只考虑 fieldNodes 中的特定字段信息:

这个 JSON 对象也可以表示为一个字符串选择集:

现在一切都明白了!我们将上述选择集发送到 Prisma 数据库模式的 posts 查询,而该查询当然不知道 feedcount 字段。诚然,产生的错误消息不是特别有用,但至少我们现在明白了是怎么回事。

那么,解决这个问题的方法是什么?一种方法是手动解析出 fieldNodes 的选择集中正确的部分,并将其传递给 posts 绑定函数(例如,作为字符串)。

但是,有一个更优雅的解决方案,那就是为应用程序模式中的 Feed 类型实现专用的解析器。以下是正确的实现方式:

实现 3:此实现修复了上述问题

此实现修复了上面讨论的所有问题。有几点需要注意:

  • 第 8 行,我们现在传递一个字符串选择集({ id })作为第二个参数。这只是为了效率,否则将获取所有标量值(在我们的示例中不会有太大差异),而我们只需要 ID。
  • 我们不是从 Query.feed 解析器返回 posts,而是返回 postIds,它只是一个 ID 数组(表示为字符串)。
  • Feed.posts 解析器中,我们现在可以访问由解析器返回的 postIds。这次,我们可以利用传入的 info 对象,并简单地将其传递给 posts 绑定函数。

如果您想尝试这个例子,可以查看这个仓库,其中包含了上述示例的运行版本。您可以随意尝试本文中提到的不同实现,并自行观察其行为!

总结

在本文中,您深入了解了在基于 GraphQL.js 实现 GraphQL API 时使用的 info 对象。

info 对象没有官方文档——要了解更多关于它的信息,您需要深入研究代码。在本教程中,我们首先概述了其内部结构,并了解了它在 GraphQL 解析器函数中的作用。然后,我们介绍了一些边缘情况和潜在陷阱,在这些情况下,需要更深入地理解 info

本文中显示的所有代码都可以在相应的 GitHub 仓库中找到,因此您可以自己进行实验并观察 info 对象的行为。

不要错过下一篇文章!

注册 Prisma 新闻通讯