# 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](https://relay.dev/graphql/connections.htm): 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.

```graphql
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`:

```javascript
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.
