GraphQL 游标连接规范
本规范旨在为 GraphQL 客户端提供一种选择,使其能够通过 GraphQL 服务器一致地处理 分页最佳实践,并支持相关元数据。本规范建议将此模式称为“连接”,并以标准化的方式公开它们。
在查询中,连接模型提供了一种标准机制来切片和分页结果集。
在响应中,连接模型提供了一种标准方法来提供游标,以及一种告知客户端何时有更多结果的方法。
以下查询是所有四种方法的示例。
{
user {
id
name
friends(first: 10, after: "opaqueCursor") {
edges {
cursor
node {
id
name
}
}
pageInfo {
hasNextPage
}
}
}
}
在本例中,friends
是一个连接。该查询演示了上述四个功能。
- 使用
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正向分页参数
要启用正向分页,需要两个参数。
first
接受一个非负整数。after
接受游标类型,如cursor
字段部分中所述。
服务器应该使用这两个参数来修改连接返回的边,返回 after
游标之后的边,并且最多返回 first
个边。
您通常应该为 after
传递上一页最后一个边的 cursor
。
4.2反向分页参数
要启用反向分页,需要两个参数。
last
接受一个非负整数。before
接受游标类型,如cursor
字段部分中所述。
服务器应该使用这两个参数来修改连接返回的边,返回 before
游标之前的边,并且最多返回 last
个边。
您通常应该为 before
传递下一页第一个边的 cursor
。
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
- 令 afterEdge 为 edges 中其 cursor 等于
after
参数的边。 - 如果存在 afterEdge
- 删除 edges 中在 afterEdge 之前和包括 afterEdge 在内的所有元素。
- 令 afterEdge 为 edges 中其 cursor 等于
- 如果设置了 before
- 令 beforeEdge 为 edges 中其 cursor 等于
before
参数的边。 - 如果存在 beforeEdge
- 删除 edges 中在 beforeEdge 之后和包括 beforeEdge 在内的所有元素。
- 令 beforeEdge 为 edges 中其 cursor 等于
- 返回 edges。
5PageInfo
服务器必须提供一个名为 PageInfo
的类型。
5.1字段
PageInfo
必须包含字段 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。
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
}
}
]
}
}
}