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サーバーは、接続のページングモデルをサポートするために、特定のタイプとタイプ名を予約する必要があります。特に、この仕様では、以下のタイプに関するガイドラインを作成します。
- 名前が「Connection」で終わる任意のオブジェクト。
PageInfo
という名前のオブジェクト。
2接続タイプ
名前が「Connection」で終わる任意のタイプは、この仕様では接続タイプとみなされます。接続タイプは、GraphQL仕様の「タイプシステム」セクションで定義されている「オブジェクト」である必要があります。
2.1フィールド
接続タイプは、edges
とpageInfo
という名前のフィールドを持っている必要があります。スキーマ設計者が適切と考える追加のフィールドを接続に含めることができます。
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フィールド
エッジタイプは、node
とcursor
という名前のフィールドを持っている必要があります。スキーマ設計者が適切と考える追加のフィールドをエッジに含めることができます。
3.1.1ノード
「エッジタイプ」には、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
エッジを返す必要があります。
一般的に、前のページの最後のエッジのcursor
をafter
に渡す必要があります。
4.2後方ページング引数
後方ページングを有効にするには、2つの引数が必要です。
last
は非負の整数を取得します。before
は、cursor
フィールドセクションで説明されているカーソルタイプを取得します。
サーバーは、これらの2つの引数を使用して、接続によって返されるエッジを変更し、before
カーソル以前のエッジを返し、最大でlast
エッジを返す必要があります。
一般的に、次のページの最初のエッジのcursor
をbefore
に渡す必要があります。
4.3エッジの順序
ビジネスロジックが指示する順序でエッジを並べ替えることができ、この仕様で網羅されていない追加の引数に基づいて順序を決定できます。ただし、順序はページ間で一貫している必要があり、重要なのは、first
/after
を使用する場合とlast
/before
を使用する場合で、他の引数がすべて同じであれば、エッジの順序は同じであるということです。last
/before
を使用する場合に反転してはなりません。より正式には
before: cursor
が使用される場合、cursor
に最も近いエッジは、結果edges
の最後に来なければなりません。after: cursor
が使用される場合、cursor
に最も近いエッジは、結果edges
の最初に来なければなりません。
4.4ページングアルゴリズム
返すエッジを決定するために、接続はbefore
とafter
カーソルを評価してエッジをフィルタリングし、次にfirst
を評価してエッジをスライスし、次にlast
を評価してエッジをスライスします。
より正式には
- edgesをApplyCursorsToEdges(allEdges, before, after)を呼び出した結果とします。
- firstが設定されている場合
- firstが0未満の場合
- エラーをスローします。
- edgesの長さがfirstより大きい場合
- edgesの末尾からエッジを削除することにより、edgesの長さをfirstになるようにスライスします。
- firstが0未満の場合
- lastが設定されている場合
- lastが0未満の場合
- エラーをスローします。
- edgesの長さがlastより大きい場合
- edgesの先頭からエッジを削除して、edgesの長さをlastにする。
- lastが0未満の場合
- edgesを返す。
- edgesをallEdgesに初期化する。
- afterが設定されている場合
- cursorがafter引数と等しいエッジをedgesからafterEdgeとする。
- afterEdgeが存在する場合
- afterEdge以前のエッジを全てedgesから削除する。
- beforeが設定されている場合
- cursorがbefore引数と等しいエッジをedgesからbeforeEdgeとする。
- beforeEdgeが存在する場合
- beforeEdge以降のエッジを全てedgesから削除する。
- edgesを返す。
5PageInfo
サーバはPageInfo
と呼ばれる型を提供しなければならない。
5.1フィールド
PageInfo
は、どちらもNULL以外のブール値を返すhasPreviousPage
とhasNextPage
フィールドを含まなければならない。また、どちらも不透明な文字列を返すstartCursor
とendCursor
フィールドも含まなければならない。結果がない場合は、startCursor
とendCursor
フィールドはNULLでもよい。
hasPreviousPage
は、クライアントの引数によって定義されたセットの前にさらにエッジが存在するかどうかを示すために使用される。クライアントがlast
/before
でページングしている場合、前のエッジが存在する場合はサーバはtrueを返し、存在しない場合はfalseを返さなければならない。クライアントがfirst
/after
でページングしている場合、クライアントはafter
の前にエッジが存在する場合はtrueを返すことができる(効率的に行える場合)。そうでない場合はfalseを返してもよい。より正式には
- lastが設定されている場合
- edgesをApplyCursorsToEdges(allEdges, before, after)を呼び出した結果とします。
- edgesがlast個以上の要素を含む場合、trueを返す。そうでない場合はfalseを返す。
- afterが設定されている場合
- afterの前に要素が存在することをサーバが効率的に判断できる場合、trueを返す。
- falseを返す。
hasNextPage
は、クライアントの引数によって定義されたセットの後にさらにエッジが存在するかどうかを示すために使用される。クライアントがfirst
/after
でページングしている場合、さらにエッジが存在する場合はサーバはtrueを返し、存在しない場合はfalseを返さなければならない。クライアントがlast
/before
でページングしている場合、クライアントはbefore
より後のエッジが存在する場合はtrueを返すことができる(効率的に行える場合)。そうでない場合はfalseを返してもよい。より正式には
- firstが設定されている場合
- edgesをApplyCursorsToEdges(allEdges, before, after)を呼び出した結果とします。
- edgesがfirst個以上の要素を含む場合、trueを返す。そうでない場合はfalseを返す。
- beforeが設定されている場合
- beforeの後に要素が存在することをサーバが効率的に判断できる場合、trueを返す。
- falseを返す。
first
とlast
の両方が含まれる場合、両方のフィールドは上記のアルゴリズムに従って設定する必要がありますが、ページングに関する意味は不明瞭になります。これは、first
とlast
の両方を使用したページングが推奨されない理由の1つです。startCursor
とendCursor
は、それぞれedges
の最初と最後のノードに対応するカーソルでなければならない。
startCursor
とendCursor
を定義しておらず、各エッジのcursor
を選択することに依存していたことに注意することが重要です。Relay Modernは帯域幅を節約するために(中間カーソルを使用しないため)、代わりにstartCursor
とendCursor
を選択するようになりました。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
}
}
]
}
}
}