ArchipadDevelopers

Pagination

The lists of a GraphQL API that may grow, such as the projects you can reach or the issues of a project, are returned page by page. Every one of them follows the GraphQL Cursor Connections Specification: the field returns a connection, and you ask for the next page with the cursor of the last item you received.

Reading a page

A connection has two fields:

  • edges: the items of the page, in order. Each edge holds the item, node, and the cursor that points at it.
  • pageInfo: where the page stands in the list. hasNextPage tells whether items follow, and endCursor is the cursor of the last item of the page, null when the page is empty.

Two arguments choose the page:

  • first: how many items the page holds, from 1 to 100. Omitted, a page holds 50 items.
  • after: the endCursor of the previous page. Omitted, you get the first page.
query Projects($after: String) {
	projects(first: 100, after: $after) {
		edges {
			node {
				id
				name
			}
		}
		pageInfo {
			hasNextPage
			endCursor
		}
	}
}

Reading a whole list

Ask for the first page without after, then pass the endCursor of each page as the after of the next, until hasNextPage is false:

let after = null;
do {
	const { data } = await graphql(PROJECTS_QUERY, { after });
	for (const { node } of data.projects.edges) {
		// ...
	}
	after = data.projects.pageInfo.hasNextPage ? data.projects.pageInfo.endCursor : null;
} while (after);

A list is always given in the same order, so paging through it neither skips nor repeats an item that exists from the first page to the last. An item added or deleted while you page may or may not be on the pages that follow.

Paging only goes forward: last and before are not supported, and hasPreviousPage is always false.

Cursors

A cursor is opaque: pass it back as it was given, without reading it or building one. Its content may change without notice.

A cursor is valid for the list that returned it and the arguments that list was given. To read the next page, send the same arguments as for the first page, with after and, if you like, another first. A cursor sent with other arguments, such as another filter, is refused: the items it skips were chosen with the arguments of the first page. To change the arguments, start again from the first page.

As arguments go, a filter given as null is the same as a filter left out, and the order of the values of a list does not matter.

Filters

The arguments that narrow a list, such as filter.typeKeys of regions or phases of ProjectRegion.issues, all mean the same:

Value given The list keeps
left out every item
null every item, as when left out
[] no item: none is in an empty list
["a", "b"] the items that match one of the values

An empty list returns an empty page rather than every item: when you build the list of values from data you read, such as the projects of a buyer, having none gives you nothing instead of everything.

Errors

These answer an error in errors, with extensions.code set to BAD_REQUEST and extensions.status set to 400:

  • first out of 1 to 100: "first must be from 1 to 100."
  • A cursor this list did not return: "after is not a cursor this list returned."
  • A cursor sent with other arguments than the page that returned it: "after is a cursor this list returned with other arguments", followed by what to do.

The last one also happens when a page you started before an update of the API is read after it: start again from the first page.