2017年12月12日

GraphQLスキーマスティッチング解説:スキーマデリゲーション

GraphQLスキーマスティッチングの理解(パートII)

GraphQL Schema Stitching explained

前回の記事では、リモート(実行可能)スキーマの詳しい内容について議論しました。これらのリモートスキーマは、スキーマスティッチングと呼ばれる一連のツールとテクニックの基盤となります。

スキーマスティッチングは、GraphQLコミュニティにおける全く新しいトピックです。一般的には、複数のGraphQLスキーマ(またはスキーマ定義)を組み合わせて接続し、単一のGraphQL APIを作成する行為を指します。

スキーマスティッチングには2つの主要な概念があります

  • スキーマデリゲーション:スキーマデリゲーションの核となる考え方は、特定のレゾルバの呼び出しを別のレゾルバに転送(デリゲート)することです。本質的に、スキーマ定義のそれぞれのフィールドが「再配線」されます。
  • スキーママージング:スキーママージングとは、既存の2つ(またはそれ以上)のGraphQL APIの結合を作成するという考え方です。関与するスキーマが完全に分離していれば問題ありませんが、そうでない場合は、名前の衝突を解決する方法が必要です。

ほとんどの場合、デリゲーションとマージングは実際に一緒に使用され、両方を使用するハイブリッドアプローチに行き着くでしょう。この連載記事では、各概念がそれ自体でよく理解できるように、これらを個別に説明します。

例:カスタムGitHub APIの構築

公開されているGitHub GraphQL APIを基にした例から始めましょう。Prisma GitHub組織に関する情報を提供する小さなアプリを構築したいと仮定します。

アプリに必要なAPIは、以下の機能を提供する必要があります

  • Prisma組織に関する情報(IDメールアドレスアバターURLピン留めされたリポジトリなど)を取得する
  • Prisma組織のリポジトリのリストを名前で取得する
  • アプリ自体の簡単な説明を取得する

要件をスキーマのルートフィールドにどのようにマッピングできるかを確認するために、GitHubのGraphQLスキーマ定義Query型を調べてみましょう。

要件1:Graphcool組織に関する情報を取得

最初の機能であるPrisma組織に関する情報の取得は、Query型のrepositoryOwnerルートフィールドを使用することで実現できます

Prisma組織に関する情報を要求するために、以下のクエリを送信できます

repositoryOwnerフィールドにloginとして"prismagraphql"を提供すると機能します。

ここでの問題は、RepositoryOwneremailフィールドを持たない単なるインターフェースであるため、emailを直接要求できないことです。しかし、Prisma組織の具体的な型が実際にOrganizationであることを知っているので、クエリ内でインラインフラグメントを使用することで、この問題を回避できます

さて、これは機能しますが、GitHub GraphQL APIをアプリの目的に直接使用するのを妨げるいくつかの摩擦点にすでにぶつかっています。

理想的には、私たちのAPIは、各クエリで引数を提供したり、Organization上のフィールドを直接要求したりすることなく、欲しい情報を直接要求できるルートフィールドを公開するだけでしょう。

要件2:Graphcoolリポジトリのリストを名前で取得

では、2番目の要件、Graphcoolリポジトリのリストを名前で取得するのはどうでしょうか。Query型をもう一度見ると、これは少し複雑になります。APIはリポジトリのリストを直接取得することを許可していません。代わりに、以下のルートフィールドを使用して、ownerとリポジトリのnameを提供することで、単一のリポジトリを要求できます。

対応するクエリはこちらです。

しかし、私たちのアプリで実際に望むもの(複数のリクエストを行う必要を避けるため)は、以下のようになるルートフィールドです。

要件3:アプリ自体の簡単な説明を取得

私たちのAPIは、「このアプリはPrisma GitHub組織に関する情報を提供します」のような、アプリを説明する文章を返すことができるべきです。

これはもちろん、GitHub APIに基づいては満たすことができない完全にカスタムの要件です。むしろ、おそらくこのようなシンプルなQueryルートフィールドで、自分たちで実装する必要があることは明らかです。

アプリケーションスキーマの定義

これで、APIの必要な機能と、スキーマに対して定義する必要がある理想的なQuery型を把握しました。

明らかに、このスキーマ定義自体は不完全です。OrganizationRepository型の定義が欠落しています。この問題を解決する簡単な方法の1つは、GitHubのスキーマ定義から手動で定義をコピー&ペーストすることです。

このアプローチはすぐに面倒になります。なぜなら、これらの型定義自体がスキーマ内の他の型に依存しているため(例えば、Repository型にはcodeOfconductフィールドがあり、その型はCodeOfConductです)、それらも手動でコピーする必要があるからです。この依存チェーンがスキーマのどこまで深く続くかには制限がなく、最終的にスキーマ定義全体を手作業でコピーすることになるかもしれません。

手動で型をコピーする場合、3つの方法があることに注意してください。

  • 型全体がコピーされ、追加のフィールドは追加されない
  • 型全体がコピーされ、追加のフィールドが追加される(または既存のフィールドが名前変更される)
  • 型のフィールドのサブセットのみがコピーされる

型全体を単にコピーする最初のアプローチが最も簡単です。これは次節で説明するように、graphql-importを使用して自動化できます。

型定義にフィールドが追加されたり、既存のフィールドが名前変更されたりする場合は、対応するレゾルバを実装する必要があります。なぜなら、基盤となるAPIはこれらの新しいフィールドの解決を処理できないからです。

最後に、型のフィールドのサブセットのみをコピーすることを選択するかもしれません。これは、型のすべてのフィールドを公開したくない場合に望ましいことです(基盤となるスキーマにはUser型にpasswordフィールドがあるかもしれませんが、それをアプリケーションスキーマで公開したくない場合など)。

GraphQL型定義のインポート

graphql-importパッケージは、異なる.graphqlファイル間で型定義を共有できるようにすることで、手作業を省いてくれます。別のGraphQLスキーマ定義から型をインポートするには、次のようにします。

JavaScriptコードでは、importSchema関数を使用できるようになり、それが依存関係を解決し、スキーマ定義が完全であることを保証します。

APIの実装

上記のスキーマ定義だけでは、まだ半分しか終わっていません。まだ欠けているのは、レゾルバ関数という形でのスキーマの実装です。

この時点で途方に暮れている場合は、GraphQLスキーマの基本的な仕組みと内部動作を紹介するこの記事を必ず読んでください。

これらのレゾルバをどのように実装するか考えてみましょう!最初のバージョンは次のようになります。

infoのレゾルバは些細なもので、アプリを説明する簡単な文字列を返せばよいです。しかし、GitHub GraphQL APIから情報を返す必要があるprismagraphqlprismagraphqlRepositoriesのレゾルバはどう扱えばよいでしょうか?

ここでこれを実装する素朴な方法は、info引数を見て、受信したクエリの選択セットを取得し、次に同じ選択セットを持つ別のGraphQLクエリをゼロから構築してGitHub APIに送信することです。これはGitHub GraphQL API用のリモートスキーマを作成することで促進することもできますが、全体としてはまだかなり冗長で面倒なプロセスです。

まさにここでスキーマデリゲーションが活躍します!以前に、GitHubのスキーマが私たちの要件(ある程度)を満たす2つのルートフィールド、repositoryOwnerとrepositoryを公開していることを見ました。これを利用して、完全に新しいクエリを作成する手間を省き、代わりに受信したクエリを転送することができます。

他のスキーマへのデリゲーション

したがって、新しいクエリ全体を構築しようとするのではなく、単に受信したクエリを取得し、その実行を別のスキーマに委任します。そのために使用するAPIは、graphql-toolsによって提供されるdelegateToSchemaと呼ばれます。

delegateToSchemaは7つの引数を受け取ります(以下の順序で)

  1. schema: GraphQLSchemaの実行可能インスタンス(これは実行を委任したい*ターゲットスキーマ*です)
  2. fragmentReplacements: インラインフラグメントを含むオブジェクト(これはこの記事では説明しないより高度なケース向けです)
  3. operation: 委任したいルート型を示す、3つの値("query", "mutation", "subscription")のいずれかを含む文字列
  4. fieldName: 委任したいルートフィールドの名前
  5. args: 委任先のルートフィールドの入力引数
  6. context: ターゲットスキーマのレゾルバチェーンを介して渡されるコンテキストオブジェクト
  7. info: 委任されるクエリに関する情報を含むオブジェクト

このアプローチを使用するには、まずGitHub GraphQL APIを表すGraphQLSchemaの実行可能インスタンスが必要です。これは、graphql-toolsのmakeRemoteExecutableSchemaを使用して取得できます。

GitHubのGraphQL APIは認証が必要なので、これを機能させるには認証トークンが必要です。取得方法はこのガイドを参照してください。

GitHub APIのリモートスキーマを作成するには、2つのものが必要です。

  • そのスキーマ定義GraphQLSchemaインスタンスの形式で)
  • そこからデータをフェッチする方法を知っているHttpLink

これは以下のコードで実現できます。

GitHubLinkHttpLinkのシンプルなラッパーであり、必要なLinkコンポーネントの作成に関して少し利便性を提供します。

素晴らしい!これで、レゾルバでデリゲートできるGitHub GraphQL APIの実行可能バージョンができました!🎉 まずはprismagraphqlレゾルバを実装してみましょう。

delegateToSchema関数が期待する7つの引数を渡しています。全体として驚きはありません。schemaはGitHub GraphQL APIのリモート実行可能スキーマです。その中で、自身のprismagraphqlクエリの実行を、GitHub APIのrepositoryOwnerクエリに委任したいと考えています。そのフィールドはlogin引数を期待するため、その値として"prismagraphql"を提供しています。最後に、infocontextオブジェクトをレゾルバチェーンに単純に渡しています。

prismagraphqlRepositoriesのレゾルバも同様にアプローチできますが、少し複雑です。以前の実装と異なる点は、私たちのprismagraphqlRepositories: [Repository!]!の型と、GitHubのスキーマ定義からの元のフィールドrepository: Repositoryの型が以前ほどきれいに一致しないことです。今度は、単一のリポジトリではなく、リポジトリの配列を返す必要があります。

そのため、Promise.allを使用して、複数のクエリを一度にデリゲートし、それらの実行結果をプロミスの配列にまとめることができるようにします。

これで完了です!カスタムGraphQL APIの3つのレゾルバすべてを実装しました。最初のもの(info用)は些細なもので、単純なカスタム文字列を返しますが、prismagraphqlprismagraphqlRepositoriesスキーマデリゲーションを使用して、クエリの実行を基盤となるGitHub APIに転送しています。

このコードの動作例を確認したい場合は、このリポジトリをご覧ください。

graphql-toolsによるスキーマデリゲーション

GitHubの上にカスタムGraphQL APIを構築する上記の例では、delegateToSchemaがクエリ実行のためのボイラープレートコードを書く手間をどのように省いてくれるかを見ました。ゼロから新しいクエリを構築してfetchやgraphql-request、その他のHTTPツールで送信する代わりに、graphql-toolsが提供するAPIを使用して、クエリの実行を別の(実行可能な)GraphQLSchemaインスタンスに委任できます。都合の良いことに、このインスタンスはリモートスキーマとして作成できます。

大まかに言えば、delegateToSchemaは、GraphQL.jsexecute関数の「プロキシ」として機能します。これは、内部的には渡された情報に基づいてGraphQLクエリ(またはミューテーション)を再構築することを意味します。クエリが構築されると、スキーマとクエリでexecuteを呼び出すだけです。

したがって、スキーマデリゲーションは、ターゲットスキーマが必ずしもリモートスキーマである必要はなく、ローカルスキーマでも実行できます。この点に関して、スキーマデリゲーションは非常に柔軟なツールです。同じスキーマ内でデリゲートしたい場合もあるでしょう。これは基本的に、graphql-toolsmergeSchemasで採用されているアプローチで、複数のスキーマがまず1つにマージされ、次にレゾルバが再配線されます。

要するに、スキーマデリゲーションとは、既存のGraphQL APIに簡単にクエリを転送できることなのです。

スキーマバインディング:GraphQL APIを簡単に再利用する方法

スキーマデリゲーションに関する新しい知識を身につけたことで、スキーマデリゲーションの上に薄い便利な層である「スキーマバインディング」と呼ばれる新しい概念を紹介できます。

公開GraphQL API用のバインディング

スキーマバインディングの核となるアイデアは、既存のGraphQL APIを再利用しやすくし、他の開発者がNPM経由で自分のプロジェクトに取り込めるようにすることです。これにより、複数のGraphQL APIの機能を非常に簡単に組み合わせることができるGraphQL「ゲートウェイ」を構築するための全く新しいアプローチが可能になります。

GitHub API専用のバインディングを使用すると、上記の例を簡素化できます。リモート実行可能スキーマを手動で作成する代わりに、この部分はgraphql-binding-githubパッケージによって行われます。GitHub APIにデリゲートするために以前必要だったすべての初期設定コードが削除された完全な実装は次のとおりです。

リモートスキーマを自分で作成する代わりに、graphql-binding-githubからインポートされたGitHubクラスをインスタンス化し、そのdelegate関数を使用するだけです。そうすると、内部でdelegateToSchemaを使用して実際にリクエストを実行します。

公開GraphQL API用のスキーマバインディングは開発者間で共有できます。graphql-binding-githubの他に、Yelp GraphQL API用のバインディングもすでに利用可能です。Devan Beitelによるgraphql-binding-yelpです。

自動生成されたデリゲート関数

このようなスキーマバインディングのAPIは、デリゲート関数が自動生成されるレベルまで改善できます。つまり、github.delegate('query', 'repository', ... )と書く代わりに、バインディングは対応するルートフィールド名にちなんだ関数、つまりgithub.query.repository( ... )を公開できるのです。

これらのデリゲート関数がビルドステップで生成され、強く型付けされた言語(TypeScriptやFlowなど)に基づいている場合、このアプローチは他のGraphQL APIとやり取りするためのコンパイル時の型安全性さえ提供します!

このアプローチがどのようなものか垣間見るには、Graphcoolサービス用のスキーマバインディングを簡単に生成でき、前述の自動生成デリゲート関数アプローチを使用しているprisma-bindingリポジトリをチェックしてください。

まとめ

これは「GraphQLスキーマスティッチングの理解」シリーズの2番目の記事です。最初の記事では、スキーマスティッチングのほとんどのシナリオの基盤となるリモート(実行可能)スキーマについて基礎知識を学びました。

この記事では、主にGitHub GraphQL APIを基にした包括的な例(コードはこちらで利用可能)を提供することで、スキーマデリゲーションの概念について議論しました。スキーマデリゲーションは、レゾルバ関数の実行を別の(または同じ)GraphQLスキーマ内の別のレゾルバに転送(委任)するメカニズムです。その主な利点は、完全に新しいクエリをゼロから構築する必要がなく、代わりに受信したクエリ(の一部)を再利用して転送できることです。

スキーマデリゲーションを基盤として使用することで、既存のGraphQL APIの再利用可能なスキーマバインディングを簡単に共有するための専用NPMパッケージを作成することが可能です。これらのバインディングがどのようなものかを知るには、GitHub API用のバインディングと、任意のGraphcoolサービス用のバインディングを簡単に生成できるprisma-bindingを確認してください。

次回の投稿もお見逃しなく!

Prismaニュースレターに登録

© . All rights reserved.