当我尝试将模式合并在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如何构建的不太清楚的领域。
重点有四个。

    1. 通过selectionSet属性覆盖选择器

 

    1. 如果你对SelectionSetNode类型感兴趣的话,可以去看一下

 

    1. 对于原始的school,可以通过info.fieldNodes获取选择器

 

    1. 然而,这只是针对school的选择器,我们需要将其更改为针对node的选择器。

 

    1. 关于这个选择器是针对哪个字段的,可以通过name属性获取并进行更改。

 

    1. 基本上,DocumentNode是只读的,不能直接修改其值。

 

    1. 所以我们需要使用扩展运算符之类的方法来创建一个新的对象。

虽然在valueFromResults中使用了id,但它不一定包含在选择器中,所以需要将其添加到选择器中。
找不到类似mergeSelectionSetNodes和parseSelectionSetNode的库,所以我自己创建了一个。
如果整理好了的话可能会公开。
如果已经有类似的库,请告诉我!

根据以下这篇文章的说法,用指令似乎也可以做到同样的事情。

 

    • たぶん { edge { node {} } で囲むの無理だよなあと思っている

 

    完全に実装の話であり、仕様を書くべきスキーマ定義に書くのはなんか違和感がある

在这方面我没有太深入探讨。
顺便提一下,我结合了这篇文章和类型提示进行推测,找到了正确答案。文档啊!

总结

我尝试使用GraphQL Mesh来整合多个API并实现API之间的关联关系,但是缺少的提示太少了,让我感到非常困惑。

希望能对某人提供帮助,会实现吗?

广告
将在 10 秒后关闭
bannerAds