Pagination
Every list in the Kavitro API is paginated. You never receive an unbounded list: you ask for a page, and you get that page along with the information needed to fetch the next one.
The shape of a list
List fields return a connection. A connection has two parts: edges, which carries the records, and pageInfo, which describes the page you just received.
query Contacts($first: Int!) {
contacts(first: $first) {
edges {
cursor
node {
id
name
displayName
}
}
pageInfo {
hasNextPage
endCursor
total
}
}
}
{
"first": 25
}
Each entry in edges wraps a single record: node is the record itself, and cursor is an opaque marker for its position in the list. Most of the time you only need node.
Always pass first
first sets the page size. Every list has a default, but those defaults are not the same everywhere - some lists default to 10, others to 50 - so a query that omits first silently gets a page size you did not choose.
Pass it explicitly, every time:
contacts(first: 25) {
edges {
node {
id
}
}
}
The maximum page size is 50 on effectively every list; asking for more is rejected.
first is also the single biggest driver of query cost, because everything you select inside a list is multiplied by the number of records you request. Asking for 50 records costs roughly ten times what asking for 5 does, for the same selection.
Paging forward
To fetch the next page, pass the previous page's endCursor as after.
query Contacts($first: Int!, $after: String) {
contacts(first: $first, after: $after) {
edges {
node {
id
name
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
Send after: null (or leave it out) for the first page. After that, keep feeding back the endCursor you were given, and stop when hasNextPage is false:
page 1 first: 25, after: null -> endCursor "eyJpZCI6MjV9", hasNextPage true
page 2 first: 25, after: "eyJpZCI6MjV9" -> endCursor "eyJpZCI6NTB9", hasNextPage true
page 3 first: 25, after: "eyJpZCI6NTB9" -> hasNextPage false, stop
Cursors are opaque. Don't decode them, build them, or store them as if they were record IDs - treat them as a token you received and hand back.
Loop on hasNextPage, not on a page count you worked out in advance. Records can be created or deleted while you are paging, and hasNextPage accounts for that where an arithmetic guess does not.
What pageInfo gives you
PageInfo carries both cursor-style fields for iterating and page-number fields for displaying progress:
| Field | Type | Meaning |
|---|---|---|
hasNextPage | Boolean! | Another page follows this one. |
hasPreviousPage | Boolean! | A page precedes this one. |
startCursor | String | Cursor of the first record on this page. |
endCursor | String | Cursor of the last record on this page. |
count | Int! | Number of records on this page. |
total | Int! | Number of records across every page. |
currentPage | Int! | Which page this is, counting from 1. |
lastPage | Int! | The number of the final page. |
Use hasNextPage and endCursor to walk the list. Use total, count, currentPage and lastPage when you need to show a user where they are ("showing 25 of 812"), not to drive the loop.
Request only the pageInfo fields you actually use. total in particular has to count the whole result set, so leave it out of queries that just page through everything.
Nested lists paginate too
A list inside another type is a connection as well, and takes its own first:
query Contact($id: ID!) {
contact(id: $id) {
id
name
emails(first: 10) {
edges {
node {
id
value
isDefault
}
}
}
}
}
Nested lists tend to default to smaller pages than root lists, which is another reason to be explicit. They also compound query cost: a nested list multiplies against the outer one, so a page of 50 contacts each selecting 10 emails is 500 records' worth of cost. Keep both numbers small, or split the work into two queries.
Not every list-shaped field is a connection. A few return a plain list and take no first at all - Contact.comments is one - in which case there is nothing to page through and you receive all of them.