HomeAbout

Pagination

What is Pagination

Pagination is about receiving data in chunk rather than as a whole.

This can help to improve performance, especially when the list of items is large or when the data is being retrieved over a slow connection.

In Graphql, pagination concerns with connection between objects.

  • connection between objects are represented as a list.

This relationship can be best understood as:

  • "Molly" has friends who are "Sam", "Dan", and "Cam".
{ "person": { "friends": [ { "name": "Sam" }, { "name": "Dan" }, { "name": "Cam" } ] } }

Slicing

Client may specify how many records to be fetched.

Client may ask for first two records to be fetched, but may request for the next two records to be fetched after in a separate request.

query { hero { name friends(first: 2) { name } } }

Offset Pagination

Classic pagination approach.

Offset pagination is most appropriate for page-based pagination feature.

  • e.g. page 1 has 1-10 records, page 2 has 11-20 records.s
entity_name(first:2 offset:2)

offset is a start index.

  • similar to after, returning records after the offset.

first is a count of items to be retrieved (limit).

  • returning at most first records.
first:4 first:3 (after offset) v v v v v v v [][][][][offset:4][][][][][][][] ^[zero-based-index]

Downsides

Performance and security downside when dealing with high volume/frequency data.

If new records are added after user had made a request, then offset calculation for subsequent pages may become ambiguous.

You typically query until an empty list comes back (to know that the pagination has reached the end).

  • This is an additional call to verify the next page.

Requesting a specific chunk of the list by providing the number of items to be retrieved.

Connection-based (e.g. raw id value or offset)

Characterized by use of nodes (optionally, edges).

entity_name(first:2 after:$entityId)

Why connection?

Why name a list of edges a connection?

Why not just call it list?

  • A connection is a way to get all of the nodes that are connected to another node in a specific way.

How Connection works

The connection evaluates:

  • first and after arguments to filter the edges in offset based pagination.
  • first, last, before, and after arguments to filter the edges in cursor based pagination.

So - connection is about efficiently fetching the nodes (records).

In this case we want to get all of the nodes connected to our users that are friends.

  • Another connection might be between a user node to all of the posts that they liked which would be its own query.
# Offset Based Connection Pagination { user { id name friends(first: 10, after: 10) { # shortcut to get the nodes (records) nodes { id name } } } }

In above case, friends is a connection type.

Slicing is done with the first argument to friends.

  • This asks for the connection to return 10 friends.

Pagination is done with the after argument to friends by passing a cursor or an offset value.

  • server to return friends after the cursor or new offset value.

Availability of neighboring edges are sufaced under pageInfo which has fields like hasNextPage.

  • that will tell us if there are more edges available, or if we’ve reached the end of this connection.

Field Requirements

Connection types node must have fields named edges and/or nodes and pageInfo.

Edges

edges field must return a list type that wraps an edge type.

  • edge type must be an object with fields node.

pageInfo

pageInfo must have non-null boolean fields of:

  • hasPreviousPage
    • Indicates whether more edges exist prior to the set defined by client arguments (boolean).
  • hasNextPage
    • Indicates whether more edges exist following the set defined by client arguments (boolean).

Should also have fields like:

  • Offset value (current page)

This example queries for next 3 records after the provided entityId.

  • Provided entityId corresponds to the id of the last record in the previous result fetched.

For offset, this would be a number of records to skip.

first:4 first:3 (after entityId) v v v v v v v [][][][entityId][][][][][][][][]

Using PageInfo pagination allows the client to retrieve the data in chunks in both directions.

  • This makes cursor-based approach appropriate for infinite-scrolling.

Result

{ "data": { "hero": { "name": "R2-D2", "friendsConnection": { "totalCount": 3, "nodes": [ { "name": "Han Solo" }, { "name": "Leia Organa" } ], "pageInfo": { "hasNextPage": false } } } } }

Cursor-based

Cursor is a modified version of Connected pagination concept.

Cursor is an opaque identifier corresponding to a record.

  • base64 encoding is recommended for making the record opaque.

Every record in the list is associated with a cursor.

  • cursor by default represents the last item that was fetched in the previous query.

Using cursor gives flexibility if the pagination model changes in the future in any arbitrary way.

Clients paginating through the list then provide the cursor of the starting record as well as a count of items to be retrieved.

type User { id: ID! name: String friends( # friends connection field first: Int, after: String, last: Int, before: String ): UserFriendsConnection } type UserFriendsConnection { edges: [UserEdge!]! pageInfo: PageInfo! } type UserEdge { node: User # actual item in the list cursor: String! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String }

edges (connection)

For each edge in the connection, we retrieve a cursor.

  • cursor is an opaque string, and is precisely what we would pass to the after arg to paginate starting after the specified edge.

opaque means the inner structure exists, but is unknown.

  • e.g. base64 translation of a string of Id.

We modify the connection field friends to add edges field.

  • now, the entity should give a list of edges that has both cursor and the underlying node (record).

Because cursor is a connection property, not the Entity object itself (unrelated to the entity object itself), we need a new layer of indirection.

  • cursor should not be a field on the entity directly.

We introduce the edges field to the connection.

type UserFriendsConnection { pageInfo: PageInfo! edges: [UserFriendsEdge] # optional, can directly reference nodes } type UserFriendsEdge { node: User }

edge can have additionalproperties of its own which act effectively as metadata.

type UserFriendsEdge { cursor: String! node: User # metadata fields friendedAt: DateTime anotherMetadataField: String }
{ user { id name friends(first: 10, after: "opaqueCursor") { edges { # always a list as a connection cursor node { # actual record id name } } pageInfo { hasNextPage } } } }

pageInfo

Has all the fields of connection based pagination:

  • hasPreviousPage
  • hasNextPage
  • startCursor
  • endCursor

Both startCursor and endCursor can be null when if there are no results.

Arguments and Pagination Direction

Applies to both connection and cursor based pagination.

Forward Pagination (next n-records):

first which is a non-negative integer.

  • server should return at most first edge.
  • edges(records) <= first returned.

after which is a cursor type.

  • server should return edges after the after cursor.

If you have a query that only uses first and after, you have a forward-only pagination.

Offset based pagination does not require backwards pagination.

  • Offset is a single directional lookahead.
query { users(first:5, after:$cursor) { edges { ... } pageInfo { ... } } }

Backwards Pagination (n-records before cursor):

last which is a non-negative integer

  • server should return at most last edges.
  • edges(records) <= last returned.

before which is a cursor type

  • server should return edges before the before cursor.

node

Each node returned within edges list corresponds to the records.

edge

Each edge represents a connection.

  • connection represents a relationship between two objects (nodes).

edges is not a boilerplate field, it should contain additional metadata fields that accurately represents a relationship.

  • edges can be used for exposing other things like time_entity_spent_with_another_entity (edge relationship metric).
query { hero { name friends(first: 2) { # this line is the entity edges { # always a list as a connection node { # actual record name } cursor # edges hold the cursor metadata_with_node # natural place for this field } } } }

pageInfo

type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String }

If you need to get other cursor positions from the object, you can reduce the need for querying in either direction by adding endCursor or startCursor to the pageInfo field.

query { hero { name friends(first: 2) { # this line is the entity pageInfo { # added to not query for edges endCursor hasPreviousPage hasNextPage } totalCount # added to get total count of records edges { # always a list as a connection node { # actual record name } cursor # edges have the cursor time_with_another_entity # natural place for this field } } } }

Pagination Algorithm

EdgesToReturn(allEdges, before, after, first, last) - Let edges be the result of calling ApplyCursorsToEdges(allEdges, before, after). - If first is set: - If first is less than 0: - Throw an error. - If edges has length greater than than first: - Slice edges to be of length first by removing edges from the end of edges. - If last is set: - If last is less than 0: - Throw an error. - If edges has length greater than than last: - Slice edges to be of length last by removing edges from the start of edges. - Return edges. ApplyCursorsToEdges(allEdges, before, after) - Initialize edges to be allEdges. - If after is set: - Let afterEdge be the edge in edges whose cursor is equal to the after argument. - If afterEdge exists: - Remove all elements of edges before and including afterEdge. - If before is set: - Let beforeEdge be the edge in edges whose cursor is equal to the before argument. - If beforeEdge exists: - Remove all elements of edges after and including beforeEdge. - Return edges.
AboutContact