GraphQL Meshでスキーマを合体させようとしたら大変だった

# GraphQL Meshとは

https://www.graphql-mesh.com/

複数のWebAPIを結合して、1つのGraphQLサービスにすることができるライブラリ。
WebAPIにはGraphQLはもちろん、RESTなどいろんなものが使える。

## 例えば

※relayっぽい感じにしていますが、全部書くの面倒なので省略

以下のスキーマを提供するStudentAPIと
```graphql:student.graphql
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が別々のサーバで立っているとします。
```graphql:school.graphql
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ですすめます。

```bash
yarn init
yarn add graphql @graphql-mesh/cli
```

次に、プロジェクトルートに `.meshrc.yaml` という設定ファイルを書きます。

```yaml:.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
```

この状態で

```bash
yarn mesh dev
```

を叩くと、サーバとGraphiQLが立ち上がります。
GraphiQLではstudent apiのクエリとschool apiの両方が叩けます！スゴイ！

### ちなみに

※本番で動かすときは以下のようになります。
```bash
yarn mesh build # .mesh/ 下に成果物ができる
yarn mesh start # .mesh/ 下を使ってプロダクションモードで起動
```

…いや、別にすごくなくね？

# 異なるソース間のリレーション定義

両方のAPIが叩けるだけなら元サービスを叩けばいいだけの話です。

しかし、Meshではスキーマ拡張をして、スキーマ同士をつなぎ合わせることができます！

## やり方

とりあえず、学生に所属学校を、学校に所属生徒リストをそれぞれつけてみようと思います。

以下のgraphqlファイルを用意します。
既存型の拡張になるので、extendを使って定義します。
```graphql:student-and-school.graphql
extend type Student {
  school: School
}

extend type School {
  students(
    after: ID
    first: Int
  ): StudentConnection
}
```

これを読み込ませるResolverをとりあえず適当に書いてみます。
```ts:resolvers.ts
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に生えることです。
`context.<source名>.(Query|Mutation|Subscription).<実行するクエリとか>` に
とりあえず引数と、contextとinfoを渡せば、元のAPIを叩いてくれます。

2つのファイルをテキトーにおいて、
```
- project/
  - node_modules/
  - .meshrc.yaml
  - package.json
  - resolvers.ts
  - student-and-school.graphql
  - yarn.lock
```

あとは定義とresolverをMeshに読ませるために設定の追加です。
```diff_yaml:.meshrc.yaml
sources:
 - name: Student
     ...
 - name: School
 -   ...
+additionalTypeDefs:
+- ./student-and-school.graphql
+additionalResolvers:
+- ./resolvers.ts
```

この状態で `yarn mesh dev` を叩けば、StudentとSchoolに追加のresolverが生えています。

あとはこれを実行すれば、
```graphql
query {
  student(id: "hoge") {
    school {
      name
    }
  }
}
```
**…動かない！**
なんでやねん。

## 普通にリゾルバだけ書いても動かない！？

まぁまずはこのクエリを見てくれ。
なんとこれは動くんだ。
```graphql
query {
  student(id: "hoge") {
    schoolId
    school {
      name
    }
  }
}
```

…おわかりいただけただろうか。
`Student.school` のリゾルバーでは、 `root.schoolId` を使用しています。
が、最初のクエリは **`schoolId`を取得してない** ので、`root.schoolId`が`undefined` になるんですね。
なんてこった。`school`取得する時は`schoolId`も毎回取らないとだめなのかよ。

…そんなことはなかった。

## resolverの再定義

とりあえず正解を乗せるとこうなのである。
```typescript:resolvers.ts
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,
        },
      },
    ],
  },
};
```

リゾルバー関数が `resolver` フィールドに移動して、新たに `selectionSet` というプロパティが生えたのが分かると思います。
この`selectionSet` は、 **指定したフィールドをresolveするのに必要なrootのプロパティをクエリするセレクタを書きます** 。
(`{}`で囲わないとパースエラーになるので注意)

型ヒントがあったからトライ＆エラーできたけど、このプロパティがこういう意味を持つと理解するまでかなりかかりました。
ドキュメントにはなんかしれっとコードに `selectionSet` が書いてあるだけで、それが持つ意味までは書いてませんでした。チクショー！

# まだいけるよなぁ？n+1問題の解決だァ！

ここまでの記事を読んで、勘のいい方は、このコードがn+1問題をはらんでいることにお気づきになられたかもしれません。
Student.school。お前だよ。

```typescript:resolver.ts
  /* 前略 */
  Student: {
    school: {
      selectionSet: "{ schoolId }",
      resolver: (root, _args, context, info) => context.School.Query.school(
        {
          args: { id: root.schoolId },
          context,
          info,
        },
      ),
    },
  },
  /* 後略 */
```

このコードは、SchoolAPIのschoolを叩いています。
**Studentがresolveされる度にな！**
例えば、以下のようなクエリを書けば、出てきたStudentの分、つまり最大100回schoolが叩かれるわけです。
```graphql
query {
  students(after: "???", first: 100) {
    edges {
      node {
        name
        school {
          name
        }
      }
    }
  }
```

こういうのはDB相手だったらdataloaderとか使ってまとめるんですが、
いかんせん相手がAPIなのでつらいものがある。
あ、dataloaderは神なので使いましょう。

https://github.com/graphql/dataloader

ただ、Meshにはビルトインでバッチングする機能がございます。

## 問題を解決するために、ちょっとした地獄を味わってもらう

まずはこうじゃ。

```diff_typescript:resolver.ts
  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を返すと型チェックにひっかかるので注意(任意パラメータが `<type> | null` で定義されているため)

ただ、 **こんだけだと動きません** 。
先ほどのクエリを思い出してほしいのですが
```graphql
query {
  students(after: "???", first: 100) {
    edges {
      node {
        name
        school {
          name
        }
      }
    }
  }
```
schoolに書かれているセレクタは `{ name }` です。
デフォルトの挙動では、これを直接 `schools` のセレクタとしてつけようとします。
しかし `schools` は `Connection` を返すため、実際にはこうしたいわけです。

```diff_graph_ql
-school {
+edges {
+  node {
      name
+  }
}
```

要は、元のセレクタを `edges { node { } }` で囲めばいいんですね。ふむふむ。
…どうやんの？

こうじゃ。
```diff_typescript:resolver.ts
  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` の作りが分からないとよくわからん領域まで来ました。
要点は4つです。

1. selectionSetプロパティでセレクタを上書き
   SelectionSetNodeという型なので興味のある人は見てみよう
2. 元の `school` に対するセレクタは `info.fieldNodes` でとれる
   ただし、これは `school` に対するセレクタなので、 `node` に対するセレクタに変えてやる必要がある。
   何のフィールドに対してのセレクタだったかは `name` プロパティが持っているので、これを書き換えてやる。
3. 基本的に `DocumentNode` はreadonlyであり、値の書き換えを直接行うことができない。
   なのでスプレッド演算子とか駆使して新しいオブジェクトを作る必要がある。
4. `valueFromResults` で `id` を使用しているが、セレクタに含まれるとは限らないので、セレクタに足してやる必要がある。
   `mergeSelectionSetNodes` と `parseSelectionSetNode` っぽいライブラリはみつからなかったので自分で作りました。
    なんかまとまったら公開するかも。
    すでにあるなら教えてくれえ！

一応以下の記事によれば同じことをディレクティブでできそうな感じではあるのですが

https://www.graphql-mesh.com/docs/guides/performances-best-practices

* たぶん `{ edge { node {} }` で囲むの無理だよなあと思っている
* 完全に実装の話であり、仕様を書くべきスキーマ定義に書くのはなんか違和感がある

というところであんまり掘り下げていません。
ちなみにこの記事と型ヒントを組み合わせて推定しながら正解にたどり着きました。ドキュメントー！

# まとめ

GraphQL Meshを使って、複数のAPIを統合し、API間のリレーションを実装するのをやってみました。
ヒントが少なすぎて泣きそうでした。

誰かの助けになればいいんですが。なるのかな？