当我尝试将模式合并在GraphQL Mesh中时,遇到了很大的困难
GraphQL Mesh是什么?
可以将多个WebAPI集成为一个GraphQL服务的库。
WebAPI可以使用GraphQL,当然也可以使用REST等各种方式。
例如
※虽然我想表达一种中继的感觉,但为了方便省事,我就省略了全部。
提供具有以下模式的StudentAPI
type Student {
id: ID!
name: String!
schoolId: ID!
}
type StudentConnection {
edges: [StudentNode!]
}
type StudentNode {
node: Student!
}
type Query {
student(id: ID!): Student
studentsById(
ids: [ID!]!
after: ID
first: Int
): StudentConnection!
students(
schoolId: ID
# なんかフィールド増えるでしょ…
after: ID,
first: Int
): StudentConnection!
}
假设SchoolAPI提供以下方案,并分别在不同的服务器上运行。
type School {
id: ID!
name: String!
}
type SchoolConnection {
edges: [SchoolNode!]
}
type SchoolNode {
node: School!
}
type Query {
school(id: ID!): School
schoolsById(
ids: [ID!]!
after: ID
first: Int
): SchoolConnection!
}
Mesh的安装和配置
由于Mesh是使用JS制作的,所以我们可以使用一个随意的包管理器进行安装。
我建议使用yarn来进行安装。
yarn init
yarn add graphql @graphql-mesh/cli
接下来,在项目根目录中写一个名为 .meshrc.yaml 的配置文件。
sources:
- name: Student
handler:
graphql:
endpoint: <student APIのエンドポイント>
- name: School
handler:
graphql:
endpoint: <school APIのエンドポイント>
这个阶段的项目结构。
- project/
- node_modules/
- .meshrc.yaml
- package.json
- yarn.lock
在这种状态下
yarn mesh dev
在点击它时,服务器和GraphiQL都会启动。在GraphiQL中,可以同时执行学生 API 和学校 API 的查询!太厉害了!
顺便提一下
※在正式场合下运行时,应如下所示。
yarn mesh build # .mesh/ 下に成果物ができる
yarn mesh start # .mesh/ 下を使ってプロダクションモードで起動
哎呀,也不是特别了不起嘛?
不同来源之间的关系定义
只要能够调用两个API,就只需要调用原始服务即可。
然而,在 Mesh 中,您可以进行模式扩展并连接模式!
做法 (yà fǎ)
我想暂时给学生分配所属学校,并给学校分配学生名单。
我会准备以下的GraphQL文件。
因为它是对现有类型的扩展,所以我们将使用extend进行定义。
extend type Student {
school: School
}
extend type School {
students(
after: ID
first: Int
): StudentConnection
}
我先试着写一个随意的解析器来读取这个。
import { Resolvers } from "./.mesh";
const resolvers: Resolvers = {
Student: {
school: (root, _args, context, info) => context.School.Query.school(
{
args: { id: root.schoolId }.
context,
info,
},
),
},
School: {
students: (root, args, context, info) => context.Student.studentsByIds(
{
args: { schoolId: root.id, after: args.after, first: args.first },
context,
info,
},
},
};
Mesh的特点是,添加到sources的API访问器会在上下文中生长。
context.<source名称>.(Query|Mutation|Subscription).<执行的查询或操作>只需传递参数、上下文和信息,它便会帮你调用原来的API。
随意放置两个文件。
- project/
- node_modules/
- .meshrc.yaml
- package.json
- resolvers.ts
- student-and-school.graphql
- yarn.lock
再加上定义和解析器,以使它们读取到Mesh中,需要进行配置的追加。
sources:
- name: Student
...
- name: School
- ...
+additionalTypeDefs:
+- ./student-and-school.graphql
+additionalResolvers:
+- ./resolvers.ts
在这个状态下,执行`yarn mesh dev`命令,会为Student和School添加额外的解析器。
只需执行这个就可以了。
query {
student(id: "hoge") {
school {
name
}
}
}
不动!为什么呢。
只写解析器是无法正常运行的吗?
嗯,首先请看一下这个查询。
真不敢相信它真的可以运行。
query {
student(id: "hoge") {
schoolId
school {
name
}
}
}
你是否能理解呢?
在Student.school的解析器中,使用root.schoolId。
但是,由于最初的查询没有获取schoolId,所以root.schoolId变为未定义。
真是糟糕啊。获取学校时每次都必须获取schoolId吗?
那种事情并没有发生。
resolver的重新定义
目前看来,如果加上正确答案,就会变成这样。
import { Resolvers } from "./.mesh";
const resolvers: Resolvers = {
Student: {
school: {
selectionSet: "{ schoolId }",
resolver: (root, _args, context, info) => context.School.Query.school(
{
args: { id: root.schoolId },
context,
info,
},
),
},
},
School: {
students: {
selectionSet: "{ id }",
resolver: (root, args, context, info) => context.Student.studentsByIds(
{
args: { schoolId: root.id, after: args.after, first: args.first },
context,
info,
},
},
],
},
};
我认为你可以看到 resolve 函数被移动到 resolver 字段中,并且新增了一个叫做 selectionSet 的属性。
selectionSet 是用来编写查询根属性以解析指定字段所需的选择器的。请注意,如果不使用”{}”括起来,将会导致解析错误。
因为有类型提示,所以我能够通过尝试和错误找到答案,但是理解这个属性的含义花了相当长的时间。
在文档中,只是简单地在代码中写了selectionSet,没有写明它的含义。真该死!
还可以继续吗?解决N+1问题!
你也许注意到了这段代码存在n+1问题,如果你细心地阅读了前面的文章。
Student.school。就是你。
/* 前略 */
Student: {
school: {
selectionSet: "{ schoolId }",
resolver: (root, _args, context, info) => context.School.Query.school(
{
args: { id: root.schoolId },
context,
info,
},
),
},
},
/* 後略 */
这段代码正在调用SchoolAPI的school。
每当Student被resolve时,school就会被调用一次!
例如,如果编写以下查询,就会调用school最多100次,即每次返回一个Student。
query {
students(after: "???", first: 100) {
edges {
node {
name
school {
name
}
}
}
}
如果是与数据库交互的情况,我会使用dataloader之类的工具进行整理。
但是因为对方是API,所以有些困难。
哦,dataloader真是太棒了,我们应该使用它。
然而,Mesh内置了批处理功能。
为了解决问题,让你尝试一点点地狱。
首先,这样吧。
Student: {
school: {
selectionSet: "{ schoolId }",
- resolver: (root, _args, context, info) => context.School.Query.school(
+ resolver: (root, _args, context, info) => context.School.Query.schools(
{
- args: { id: root.schoolId },
+ key: root.schoolId,
+ argsFromKeys: (keys: string[]) => ({
+ ids: keys,
+ }),
+ valueFromResults: (results, keys: string[]) =>
+ keys.map(key => results.edges
+ .map(e => e.node)
+ .find(n => n.id === key) ?? null
+ ),
context,
info,
},
),
},
},
将API更改为schools,并添加批处理设置。
key: argsFromKeys にわたるキー値
argsFromKeys: 複数のキー値から、叩くAPIのargsを作る関数。関数なので、キー値以外の引数も返り値に加えれば引数として成立します。
valueFromResults: APIの返り値から、キーの順番に取得結果を取り出します。
undefinedを返すと型チェックにひっかかるので注意(任意パラメータが | null で定義されているため)
只是,只有这样的话不能动。
希望能够回忆起先前的查询。
query {
students(after: "???", first: 100) {
edges {
node {
name
school {
name
}
}
}
}
在学校中,所写的选择器是{ name }。
在默认情况下,会将其直接连接到schools选择器上。
但是,由于schools是返回Connection的,实际上我们希望做的是这样的。
-school {
+edges {
+ node {
name
+ }
}
总之,我们只需用 edges { node { } } 来包围原始选择器,是吗?嗯嗯。
那么,你是怎么做的呢?
这样吧。
Student: {
school: {
selectionSet: "{ schoolId }",
resolver: (root, _args, context, info) => context.School.Query.schools(
{
key: root.schoolId,
argsFromKeys: (keys: string[]) => ({
ids: keys,
}),
valueFromResults: (results, keys: string[]) =>
keys.map(key => results.edges
.map(e => e.node)
.find(n => n.id === key) ?? null
),
context,
info,
+ selectionSet: [
+ {
+ kind: Kind.FIELD,
+ name: { kind: Kind.NAME, value: "edges" },
+ selectionSet: {
+ kind: Kind.SELECTION_SET,
+ selections: info.fieldNodes.map(f => ({
+ ...f,
+ name: { kind: Kind.NAME, value: "node" },
+ selectionSet: mergeSelectionSetNodes(
+ f.selectionSet,
+ parseSelectionSetNode(
+ gql`
+ {
+ id
+ }
+ `
+ )
+ )
+ }),
+ },
+ },
+ ],
},
),
},
},
是的,我已经到了对于graphql.js中DocumentNode如何构建的不太清楚的领域。
重点有四个。
-
- 通过selectionSet属性覆盖选择器
-
- 如果你对SelectionSetNode类型感兴趣的话,可以去看一下
-
- 对于原始的school,可以通过info.fieldNodes获取选择器
-
- 然而,这只是针对school的选择器,我们需要将其更改为针对node的选择器。
-
- 关于这个选择器是针对哪个字段的,可以通过name属性获取并进行更改。
-
- 基本上,DocumentNode是只读的,不能直接修改其值。
-
- 所以我们需要使用扩展运算符之类的方法来创建一个新的对象。
虽然在valueFromResults中使用了id,但它不一定包含在选择器中,所以需要将其添加到选择器中。
找不到类似mergeSelectionSetNodes和parseSelectionSetNode的库,所以我自己创建了一个。
如果整理好了的话可能会公开。
如果已经有类似的库,请告诉我!
根据以下这篇文章的说法,用指令似乎也可以做到同样的事情。
-
- たぶん { edge { node {} } で囲むの無理だよなあと思っている
- 完全に実装の話であり、仕様を書くべきスキーマ定義に書くのはなんか違和感がある
在这方面我没有太深入探讨。
顺便提一下,我结合了这篇文章和类型提示进行推测,找到了正确答案。文档啊!
总结
我尝试使用GraphQL Mesh来整合多个API并实现API之间的关联关系,但是缺少的提示太少了,让我感到非常困惑。
希望能对某人提供帮助,会实现吗?