GraphQL カーソル接続仕様

この仕様は、GraphQL クライアントが関連するメタデータへのサーバーからのサポートを用いて、ページングのベストプラクティスを一貫して処理するためのオプションを提供することを目的としています。この仕様では、このパターンを「接続」と呼び、標準化された方法で公開することを提案します。

クエリにおいて、接続モデルは結果セットのスライスとページングのための標準的なメカニズムを提供します。

レスポンスにおいて、接続モデルはカーソルを提供する標準的な方法と、クライアントにより多くの結果が利用可能かどうかを伝える方法を提供します。

これらの4つの機能の例を以下に示すクエリです。

{
  user {
    id
    name
    friends(first: 10, after: "opaqueCursor") {
      edges {
        cursor
        node {
          id
          name
        }
      }
      pageInfo {
        hasNextPage
      }
    }
  }
}

この場合、friendsは接続です。このクエリは上記で説明した4つの機能を示しています。

  • スライスは、friendsへのfirst引数によって行われます。これは、接続に10人のフレンドを返すように要求します。
  • ページングは、friendsへのafter引数によって行われます。カーソルを渡したので、サーバーにそのカーソル以降のフレンドを返すように要求しました。
  • 接続内の各エッジについて、カーソルを要求しました。このカーソルは不透明な文字列であり、このエッジ以降から開始するページングにafter引数として渡すものです。
  • hasNextPageを要求しました。これにより、利用可能なエッジがさらに存在するかどうか、またはこの接続の終わりに達したかどうかがわかります。

この仕様のセクションでは、接続に関する正式な要件について説明します。

1予約済みタイプ

この仕様に準拠するGraphQLサーバーは、接続のページングモデルをサポートするために、特定のタイプとタイプ名を予約する必要があります。特に、この仕様では、以下のタイプに関するガイドラインを作成します。

2接続タイプ

名前が「Connection」で終わる任意のタイプは、この仕様では接続タイプとみなされます。接続タイプは、GraphQL仕様の「タイプシステム」セクションで定義されている「オブジェクト」である必要があります。

2.1フィールド

接続タイプは、edgespageInfoという名前のフィールドを持っている必要があります。スキーマ設計者が適切と考える追加のフィールドを接続に含めることができます。

2.1.1エッジ

「接続タイプ」には、edgesというフィールドが含まれている必要があります。このフィールドは、エッジタイプのラッパーであるリストタイプを返す必要があります。エッジタイプの要件は、以下の「エッジタイプ」セクションで定義されています。

2.1.2PageInfo

「接続タイプ」には、pageInfoというフィールドが含まれている必要があります。このフィールドは、以下の「PageInfo」セクションで定義されている非ヌルのPageInfoオブジェクトを返す必要があります。

2.2イントロスペクション

ExampleConnectionがタイプシステムに存在する場合、その名前が「Connection」で終わるため、接続になります。この接続のエッジタイプがExampleEdgeという名前の場合、上記の要件を正しく実装するサーバーは次のイントロスペクションクエリを受け入れ、提供された応答を返します。

{
  __type(name: "ExampleConnection") {
    fields {
      name
      type {
        name
        kind
        ofType {
          name
          kind
        }
      }
    }
  }
}

返します

{
  "data": {
    "__type": {
      "fields": [
        // May contain other items
        {
          "name": "pageInfo",
          "type": {
            "name": null,
            "kind": "NON_NULL",
            "ofType": {
              "name": "PageInfo",
              "kind": "OBJECT"
            }
          }
        },
        {
          "name": "edges",
          "type": {
            "name": null,
            "kind": "LIST",
            "ofType": {
              "name": "ExampleEdge",
              "kind": "OBJECT"
            }
          }
        }
      ]
    }
  }
}

3エッジタイプ

接続タイプのedgesフィールドによってリスト形式で返されるタイプは、この仕様ではエッジタイプとみなされます。エッジタイプは、GraphQL仕様の「タイプシステム」セクションで定義されている「オブジェクト」である必要があります。

3.1フィールド

エッジタイプは、nodecursorという名前のフィールドを持っている必要があります。スキーマ設計者が適切と考える追加のフィールドをエッジに含めることができます。

3.1.1ノード

「エッジタイプ」には、nodeというフィールドが含まれている必要があります。このフィールドは、スカラー、列挙型、オブジェクト、インターフェース、ユニオン、またはそれらのタイプのいずれかの非ヌルラッパーを返す必要があります。特に、このフィールドはリストを返すことはできません

注記 この命名は、この仕様の後のセクションで説明されている「Node」インターフェースと「node」ルートフィールドを反映しています。このフィールドがNodeを実装するオブジェクトを返す場合、仕様に準拠したクライアントは特定の最適化を実行できますが、これは準拠するための厳密な要件ではありません。

3.1.2カーソル

「エッジタイプ」には、cursorというフィールドが含まれている必要があります。このフィールドは、文字列としてシリアライズされるタイプを返す必要があります。これは、文字列、文字列の非ヌルラッパー、文字列としてシリアライズされるカスタムスカラー、または文字列としてシリアライズされるカスタムスカラーの非ヌルラッパーのいずれかです。

このフィールドが返すタイプは、この仕様の残りの部分ではカーソルタイプと呼ばれます。

このフィールドの結果は、クライアントによって不透明と見なされるべきですが、以下の「引数」セクションで説明されているように、サーバーに渡されます。

3.2イントロスペクション

ExampleEdgeがスキーマ内のエッジタイプであり、「Example」オブジェクトを返す場合、上記の要件を正しく実装するサーバーは次のイントロスペクションクエリを受け入れ、提供された応答を返します。

{
  __type(name: "ExampleEdge") {
    fields {
      name
      type {
        name
        kind
        ofType {
          name
          kind
        }
      }
    }
  }
}

返します

{
  "data": {
    "__type": {
      "fields": [
        // May contain other items
        {
          "name": "node",
          "type": {
            "name": "Example",
            "kind": "OBJECT",
            "ofType": null
          }
        },
        {
          "name": "cursor",
          "type": {
            // This shows the cursor type as String!, other types are possible
            "name": null,
            "kind": "NON_NULL",
            "ofType": {
              "name": "String",
              "kind": "SCALAR"
            }
          }
        }
      ]
    }
  }
}

4引数

接続タイプを返すフィールドには、前方ページング引数、後方ページング引数、またはその両方が含まれている必要があります。これらのページング引数は、クライアントが返される前にエッジのセットをスライスすることを可能にします。

4.1前方ページング引数

前方ページングを有効にするには、2つの引数が必要です。

  • firstは非負の整数を取得します。
  • afterは、cursorフィールドセクションで説明されているカーソルタイプを取得します。

サーバーは、これらの2つの引数を使用して、接続によって返されるエッジを変更し、afterカーソル以降のエッジを返し、最大でfirstエッジを返す必要があります。

一般的に、前のページの最後のエッジのcursorafterに渡す必要があります。

4.2後方ページング引数

後方ページングを有効にするには、2つの引数が必要です。

  • lastは非負の整数を取得します。
  • beforeは、cursorフィールドセクションで説明されているカーソルタイプを取得します。

サーバーは、これらの2つの引数を使用して、接続によって返されるエッジを変更し、beforeカーソル以前のエッジを返し、最大でlastエッジを返す必要があります。

一般的に、次のページの最初のエッジのcursorbeforeに渡す必要があります。

4.3エッジの順序

ビジネスロジックが指示する順序でエッジを並べ替えることができ、この仕様で網羅されていない追加の引数に基づいて順序を決定できます。ただし、順序はページ間で一貫している必要があり、重要なのは、first/afterを使用する場合とlast/beforeを使用する場合で、他の引数がすべて同じであれば、エッジの順序は同じであるということです。last/beforeを使用する場合に反転してはなりません。より正式には

  • before: cursorが使用される場合、cursorに最も近いエッジは、結果edges最後に来なければなりません。
  • after: cursorが使用される場合、cursorに最も近いエッジは、結果edges最初に来なければなりません。

4.4ページングアルゴリズム

返すエッジを決定するために、接続はbeforeafterカーソルを評価してエッジをフィルタリングし、次にfirstを評価してエッジをスライスし、次にlastを評価してエッジをスライスします。

注記 firstlastの両方に値を含めることは、混乱を招くクエリと結果につながる可能性が高いため、強くお勧めしません。「PageInfo」セクションでさらに詳しく説明します。

より正式には

EdgesToReturn(allEdges, before, after, first, last)
  1. edgesApplyCursorsToEdges(allEdges, before, after)を呼び出した結果とします。
  2. firstが設定されている場合
    1. firstが0未満の場合
      1. エラーをスローします。
    2. edgesの長さがfirstより大きい場合
      1. edgesの末尾からエッジを削除することにより、edgesの長さをfirstになるようにスライスします。
  3. lastが設定されている場合
    1. lastが0未満の場合
      1. エラーをスローします。
    2. edgesの長さがlastより大きい場合
      1. edgesの先頭からエッジを削除して、edgesの長さをlastにする。
  4. edgesを返す。
ApplyCursorsToEdges(allEdges, before, after)
  1. edgesallEdgesに初期化する。
  2. afterが設定されている場合
    1. cursorafter引数と等しいエッジをedgesからafterEdgeとする。
    2. afterEdgeが存在する場合
      1. afterEdge以前のエッジを全てedgesから削除する。
  3. beforeが設定されている場合
    1. cursorbefore引数と等しいエッジをedgesからbeforeEdgeとする。
    2. beforeEdgeが存在する場合
      1. beforeEdge以降のエッジを全てedgesから削除する。
  4. edgesを返す。

5PageInfo

サーバはPageInfoと呼ばれる型を提供しなければならない。

5.1フィールド

PageInfoは、どちらもNULL以外のブール値を返すhasPreviousPagehasNextPageフィールドを含まなければならない。また、どちらも不透明な文字列を返すstartCursorendCursorフィールドも含まなければならない。結果がない場合は、startCursorendCursorフィールドはNULLでもよい。

hasPreviousPageは、クライアントの引数によって定義されたセットの前にさらにエッジが存在するかどうかを示すために使用される。クライアントがlast/beforeでページングしている場合、前のエッジが存在する場合はサーバはtrueを返し、存在しない場合はfalseを返さなければならない。クライアントがfirst/afterでページングしている場合、クライアントはafterの前にエッジが存在する場合はtrueを返すことができる(効率的に行える場合)。そうでない場合はfalseを返してもよい。より正式には

HasPreviousPage(allEdges, before, after, first, last)
  1. lastが設定されている場合
    1. edgesApplyCursorsToEdges(allEdges, before, after)を呼び出した結果とします。
    2. edgeslast個以上の要素を含む場合、trueを返す。そうでない場合はfalseを返す。
  2. afterが設定されている場合
    1. afterの前に要素が存在することをサーバが効率的に判断できる場合、trueを返す。
  3. falseを返す。

hasNextPageは、クライアントの引数によって定義されたセットの後にさらにエッジが存在するかどうかを示すために使用される。クライアントがfirst/afterでページングしている場合、さらにエッジが存在する場合はサーバはtrueを返し、存在しない場合はfalseを返さなければならない。クライアントがlast/beforeでページングしている場合、クライアントはbeforeより後のエッジが存在する場合はtrueを返すことができる(効率的に行える場合)。そうでない場合はfalseを返してもよい。より正式には

HasNextPage(allEdges, before, after, first, last)
  1. firstが設定されている場合
    1. edgesApplyCursorsToEdges(allEdges, before, after)を呼び出した結果とします。
    2. edgesfirst個以上の要素を含む場合、trueを返す。そうでない場合はfalseを返す。
  2. beforeが設定されている場合
    1. beforeの後に要素が存在することをサーバが効率的に判断できる場合、trueを返す。
  3. falseを返す。
注記 firstlastの両方が含まれる場合、両方のフィールドは上記のアルゴリズムに従って設定する必要がありますが、ページングに関する意味は不明瞭になります。これは、firstlastの両方を使用したページングが推奨されない理由の1つです。

startCursorendCursorは、それぞれedgesの最初と最後のノードに対応するカーソルでなければならない。

注記 この仕様はRelay Classicを念頭に作成されたため、Relay LegacyはstartCursorendCursorを定義しておらず、各エッジのcursorを選択することに依存していたことに注意することが重要です。Relay Modernは帯域幅を節約するために(中間カーソルを使用しないため)、代わりにstartCursorendCursorを選択するようになりました。

5.2イントロスペクション

上記の要件を正しく実装したサーバは、次のイントロスペクションクエリを受け入れ、提供された応答を返す。

{
  __type(name: "PageInfo") {
    fields {
      name
      type {
        name
        kind
        ofType {
          name
          kind
        }
      }
    }
  }
}

返します

{
  "data": {
    "__type": {
      "fields": [
        // May contain other fields.
        {
          "name": "hasNextPage",
          "type": {
            "name": null,
            "kind": "NON_NULL",
            "ofType": {
              "name": "Boolean",
              "kind": "SCALAR"
            }
          }
        },
        {
          "name": "hasPreviousPage",
          "type": {
            "name": null,
            "kind": "NON_NULL",
            "ofType": {
              "name": "Boolean",
              "kind": "SCALAR"
            }
          }
        },
        {
          "name": "startCursor",
          "type": {
            "name": "String",
            "kind": "SCALAR",
            "ofType": null
          }
        },
        {
          "name": "endCursor",
          "type": {
            "name": "String",
            "kind": "SCALAR",
            "ofType": null
          }
        }
      ]
    }
  }
}

§索引

  1. ApplyCursorsToEdges
  2. EdgesToReturn
  3. HasNextPage
  4. HasPreviousPage
  1. 1予約済みタイプ
  2. 2接続タイプ
    1. 2.1フィールド
      1. 2.1.1エッジ
      2. 2.1.2PageInfo
    2. 2.2イントロスペクション
  3. 3エッジタイプ
    1. 3.1フィールド
      1. 3.1.1ノード
      2. 3.1.2カーソル
    2. 3.2イントロスペクション
  4. 4引数
    1. 4.1前方ページング引数
    2. 4.2後方ページング引数
    3. 4.3エッジの順序
    4. 4.4ページングアルゴリズム
  5. 5PageInfo
    1. 5.1フィールド
    2. 5.2イントロスペクション
  6. §索引