2017年12月12日

GraphQL Schema Stitching 解释:Schema Delegation

理解 GraphQL schema stitching(第二部分)

GraphQL Schema Stitching explained

上一篇文章中,我们讨论了远程(可执行)schema的来龙去脉。这些远程schema是被称为schema stitching的工具集和技术的基础。

Schema stitching是 GraphQL 社区中的一个全新话题。通常,它指的是组合和连接多个 GraphQL schema(或schema 定义)以创建一个单一 GraphQL API 的行为。

Schema stitching 中有两个主要概念

  • Schema 委托 (Schema delegation):Schema 委托的核心思想是将特定解析器的调用转发(委托)给另一个解析器。实质上,schema 定义的相应字段正在被“重新连接”。
  • Schema 合并 (Schema merging):Schema 合并是创建两个(或更多)现有 GraphQL API 的联合的想法。如果涉及的 schema 完全不相交,则这不成问题——如果它们不相交,则需要一种方法来解决其命名冲突。

请注意,在大多数情况下,委托和合并会一起使用,最终我们将采用混合方法,同时使用两者。在本系列文章中,我们将分别介绍它们,以确保每个概念都能独立理解。

示例:构建一个自定义 GitHub API

让我们从一个基于公共 GitHub GraphQL API 的示例开始。假设我们想构建一个小型应用程序,提供关于 Prisma GitHub organization 的信息。

我们为应用程序需要的 API 应该具备以下能力

  • 检索 Prisma organization 的信息(例如其 ID电子邮件地址头像 URL置顶仓库
  • 按名称检索 Prisma organization 中的仓库列表
  • 检索关于应用程序本身的简短描述

让我们探索 GitHub 的 GraphQL schema 定义中的 Query 类型,看看如何将我们的需求映射到 schema 的根字段。

需求 1:检索 Graphcool organization 的信息

第一个功能,检索 Prisma organization 的信息,可以通过使用 Query 类型上的 repositoryOwner 根字段来实现

我们可以发送以下查询来询问有关 Prisma organization 的信息

当我们提供 "prismagraphql" 作为 loginrepositoryOwner 字段时,它会起作用。

这里的一个问题是,我们无法直接查询 email,因为 RepositoryOwner 只是一个没有 email 字段的接口。但是,由于我们知道 Prisma organization 的具体类型确实是 Organization,我们可以通过在查询中使用内联片段来解决这个问题

好的,这会起作用,但我们已经遇到了一些阻碍,使得 GitHub GraphQL API 无法直接用于我们的应用程序。

理想情况下,我们的 API 应该只暴露一个根字段,允许直接查询所需信息,而无需在每次查询时提供参数,并允许我们直接查询 Organization 上的字段

需求 2:按名称检索 Graphcool 仓库列表

第二个需求,按名称检索 Graphcool 仓库列表又如何?再次查看 Query 类型,这变得更加复杂。API 不允许直接检索仓库列表——相反,您可以使用以下根字段通过提供 owner 和仓库的 name 来查询单个仓库

这是一个对应的查询

然而,我们应用程序实际需要的(为了避免发出多个请求)是一个看起来像这样的根字段

需求 3:检索关于应用程序本身的简短描述

我们的 API 应该能够返回一个描述我们应用程序的句子,例如 "This app provides information about the Prisma GitHub organization"

这当然是一个完全自定义的需求,我们无法基于 GitHub API 来实现——相反,很明显我们需要自己实现它,可能通过一个简单的 Query 根字段来实现,就像这样

定义应用程序 schema

我们现在了解了 API 所需的功能以及我们需要为 schema 定义的理想 Query 类型

显然,这个 schema 定义本身是不完整的:它缺少 OrganizationRepository 类型的定义。解决这个问题的一种直接方法就是手动复制粘贴 GitHub 的 schema 定义中的定义。

这种方法很快就会变得繁琐,因为这些类型定义本身依赖于 schema 中的其他类型(例如,Repository 类型有一个 codeOfconduct 字段,类型是 CodeOfConduct),您也需要手动复制这些类型。这种依赖链在 schema 中可以深入到没有限制,您甚至可能最终手动复制整个 schema 定义。

请注意,手动复制类型时,有三种方法可以做到这一点

  • 复制整个类型,不添加额外字段
  • 复制整个类型并添加额外字段(或重命名现有字段)
  • 仅复制类型字段的子集

第一种方法,简单复制整个类型,是最直接的。可以使用 graphql-import 自动完成,如下一节所述。

如果在类型定义中添加了额外字段或重命名了现有字段,您需要确保实现相应的解析器,因为底层 API 当然无法处理这些新字段的解析。

最后,您可能决定只复制类型字段的子集。如果您不想暴露类型的所有字段,这可能是可取的(底层 schema 的 User 类型上可能有一个 password 字段,您不想在应用程序 schema 中暴露它)。

导入 GraphQL 类型定义

软件包 graphql-import 通过允许您在不同的 .graphql 文件之间共享类型定义来节省您的手动工作。您可以像这样从另一个 GraphQL schema 定义导入类型

在您的 JavaScript 代码中,您现在可以使用 importSchema 函数,它将为您解析依赖关系,确保您的 schema 定义完整。

实现 API

有了上面的 schema 定义,我们只完成了一半。仍然缺少的是以解析器函数形式存在的 schema 实现

如果您对此感到困惑,请务必阅读这篇文章,其中介绍了 GraphQL schema 的基本机制和内部工作原理。

让我们思考一下如何实现这些解析器!第一个版本可能如下所示

info 的解析器非常简单,我们可以返回一个描述我们应用程序的简单字符串。但是如何处理 prismagraphqlprismagraphqlRepositories 的解析器呢?在这里,我们实际上需要返回来自 GitHub GraphQL API 的信息。

在这里实现这种功能的幼稚方法是查看 info 参数以检索传入查询的选择集——然后从头构建另一个具有相同选择集的 GraphQL 查询,并将其发送到 GitHub API。这甚至可以通过为 GitHub GraphQL API 创建一个远程 schema 来实现,但总的来说仍然是一个相当冗长和繁琐的过程。

这正是schema 委托发挥作用的地方!我们之前看到 GitHub 的 schema 暴露了两个(某种程度上)满足我们需求的根字段:repositoryOwner 和 repository。我们现在可以利用这一点,节省创建全新查询的工作,而是转发传入的查询。

委托给其他 schema

因此,我们不是试图构建一个全新的查询,而是简单地获取传入的查询并将其执行委托给另一个 schema。我们将用于此目的的 API 是 graphql-tools 提供的 delegateToSchema

delegateToSchema 接收七个参数(顺序如下)

  1. schemaGraphQLSchema 的一个可执行实例(这是我们要委托执行的目标 schema
  2. fragmentReplacements:一个包含内联片段的对象(这适用于本文不讨论的更高级情况)
  3. operation:一个字符串,包含三个值中的一个( "query" , "mutation" 或 "subscription"),指示我们要委托给哪个根类型
  4. fieldName:我们要委托到的根字段名称
  5. args:我们要委托到的根字段的输入参数
  6. context:通过目标 schema 解析器链传递的上下文对象
  7. info:一个包含要委托的查询信息的对象

为了使用这种方法,我们首先需要一个表示 GitHub GraphQL API 的 GraphQLSchema 可执行实例。我们可以使用 graphql-tools 中的 makeRemoteExecutableSchema 来获取它。

请注意,GitHub 的 GraphQL API 需要认证,因此您需要一个认证令牌才能使其工作。您可以按照此指南获取一个。

为了创建 GitHub API 的远程 schema,我们需要两件事

  • schema 定义(以 GraphQLSchema 实例的形式)
  • 一个知道如何从中获取数据的 HttpLink

我们可以使用以下代码实现这一点

GitHubLink 只是 HttpLink 之上的一个简单包装器,为创建所需的 Link 组件提供了一些便利。

太棒了,我们现在有了一个可执行版本的 GitHub GraphQL API,我们可以在解析器中委托给它!🎉 让我们先实现 prismagraphql 解析器

我们正在传递 delegateToSchema 函数期望的七个参数。总的来说没有什么意外:schema 是 GitHub GraphQL API 的远程可执行 schema。在这里,我们希望将我们自己的 prismagraphql 查询的执行委托给 GitHub API 的 repositoryOwner 查询。由于该字段需要一个 login 参数,我们为其提供值 "prismagraphql"。最后,我们只是简单地通过解析器链传递 infocontext 对象。

prismagraphqlRepositories 的解析器可以以类似的方式处理,但它稍微有点棘手。与之前实现不同的是,我们的 prismagraphqlRepositories: [Repository!]! 类型与 GitHub schema 定义中的原始字段 repository: Repository 类型不太匹配。我们现在需要返回一个仓库数组,而不是单个仓库。

因此,我们继续使用 Promise.all 来确保我们可以一次委托多个查询,并将它们的执行结果打包成一个 promise 数组

就是这样!我们现在已经为我们的自定义 GraphQL API 实现了所有三个解析器。虽然第一个(用于 info)非常简单,只返回一个自定义字符串,但 prismagraphqlprismagraphqlRepositories 正在使用schema 委托将查询的执行转发到底层 GitHub API。

如果您想查看此代码的有效示例,请查看此仓库

使用 graphql-tools 进行 schema 委托

在上面基于 GitHub 构建自定义 GraphQL API 的示例中,我们看到了 delegateToSchema 如何让我们免于编写查询执行的样板代码。我们可以使用 graphql-tools 提供的 API 将查询的执行委托给 GraphQLSchema 的另一个(可执行)实例,而不是从头构建新查询并使用 fetch、graphql-request 或其他 HTTP 工具发送它。方便的是,这个实例可以作为远程 schema 创建。

从高层次来看,delegateToSchema 简单地充当 GraphQL.jsexecute 函数的“代理”。这意味着在底层,它将根据作为参数传递的信息重新组合一个 GraphQL 查询(或 mutation)。一旦查询构建完成,它所做的就是使用 schema 和查询调用 execute

因此,schema 委托不一定要求目标 schema 是远程 schema,它也可以与本地 schema 一起使用。在这方面,schema 委托是一个非常灵活的工具——您甚至可能想在同一个 schema 中进行委托。这基本上是 graphql-toolsmergeSchemas 采用的方法,其中多个 schema 首先合并成一个,然后重新连接解析器。

实质上,schema 委托就是能够轻松将查询转发到现有 GraphQL API。

Schema 绑定:重用 GraphQL API 的简便方法

掌握了关于 schema 委托的新知识后,我们可以引入一个新概念,它不过是 schema 委托之上的一个薄薄的便利层,称为schema 绑定

公共 GraphQL API 的绑定

schema 绑定的核心思想是提供一种简便方法,使现有 GraphQL API 可重用,以便其他开发者现在可以通过 NPM 将其引入到他们的项目中。这带来了一种全新的构建 GraphQL“网关”的方法,在这种方法中,组合多个 GraphQL API 的功能变得极其容易。

有了针对 GitHub API 的专用绑定,我们现在可以简化上述示例。创建远程可执行 schema 的部分现在由 graphql-binding-github 包完成,而不是手动完成。以下是完整的实现,其中所有之前委托给 GitHub API 所需的初始设置代码都已删除

我们不是自己创建远程 schema,而是简单地实例化从 graphql-binding-github 导入的 GitHub 类,并使用其 delegate 函数。然后它将在底层使用 delegateToSchema 实际执行请求。

公共 GraphQL API 的 schema 绑定可以在开发者之间共享。除了 graphql-binding-github 之外,还有 Yelp GraphQL API 的可用绑定:由 Devan Beitel 提供的 graphql-binding-yelp

自动生成的委托函数

这类 schema 绑定的 API 甚至可以改进到委托函数可以自动生成的程度。绑定可以暴露一个以相应根字段命名的函数:github.query.repository( ... ),而不是编写如下的 github.delegate('query', 'repository', ... )

当这些委托函数在构建步骤中生成并基于强类型语言(如 TypeScript 或 Flow)时,这种方法甚至可以为与其他 GraphQL API 交互提供编译时类型安全!

要大致了解这种方法,请查看 prisma-binding 仓库,它允许轻松为 Graphcool 服务生成 schema 绑定,并使用上述自动生成委托函数的方法。

总结

这是我们“理解 GraphQL schema stitching”系列的第二篇文章。在第一篇文章中,我们做了一些基础工作,学习了远程(可执行)schema,它们是大多数 schema stitching 场景的基础。

在本文中,我们主要通过基于GitHub GraphQL API 的综合示例(示例代码在此处提供)讨论了schema 委托的概念。Schema 委托是一种将解析器函数的执行转发(委托)到不同(甚至相同)GraphQL schema 中另一个解析器的机制。其主要优点是我们不必从头构建一个全新的查询,而是可以重用和转发(部分)传入的查询。

当使用 schema 委托作为基础时,可以创建专门的 NPM 包,以便轻松共享现有 GraphQL API 的可重用schema 绑定。要大致了解它们是什么样子,您可以查看GitHub API 的绑定以及允许轻松为任何 Graphcool 服务生成绑定的prisma-binding

不要错过下一篇文章!

订阅 Prisma Newsletter