Pagination

Pagination uses relay style, supporting forward and backward navigation.

Overview

Pagination is implemented in the relay style. Reference documentation can be found here. Both forward and backward pagination are supported:

  • the arguments for forward pagination are first (specifing the maximum number of items to return) and after (which is an opaque cursor, typically pointing to the last item in the previous page)
  • the arguments for backward pagination are last (specifying the maximum number of items to return) and before (which is an opaque cursor, typically pointing to the first item in the next page)
  • a skip argument is available for both forward and backward pagination and specifies a number of records to be skipped (from the given cursor) before retrieving the requested number of records.

Note

The values for first, last, and skip must be positive integers.

Warning

Pagination does not work with grouping (groupBy and having arguments).

Information about a page is returned in the pageInfo node. This contains the following fields:

Field Description
hasNextPage Indicates whether there are more records to fetch after the current page.
hasPreviousPage Indicates whether there are more records to fetch before the current page.
startCursor An opaque cursor pointing to the first record in the current page.
endCursor An opaque cursor pointing to the last record in the current page.

When forward pagination is performed, the hasNextPage field is true if there are more records to fetch after the current page. When backward pagination is performed, the hasNextPage field is true if there are more records to fetch before the current page.

Similarly, when forward pagination is performed, the hasPreviousPage field is true if there are more records to fetch before the current page. When backward pagination is performed, the hasPreviousPage field is always false.

Understanding pagination

In the following example, we fetch the first 5 associtates. This being the first page, there is no argument for the after parameter. Passing a null value for the after parameter is equivalent to not passing it at all.

Query

query read($cid : Int!)
{
  useCompany(no: $cid)
  {
    associate(first: 5)
    {
      pageInfo
      {
        hasNextPage
        hasPreviousPage
        startCursor
        endCursor
      }
      items
      {
        associateNo
        name
      }
    }
  }
}
Result

{
  "data": {
    "useCompany": {
      "associate": {
        "pageInfo": {
          "hasNextPage": true,
          "hasPreviousPage": false,
          "startCursor": "MQ==",
          "endCursor": "NQ=="
        },
        "items": [
          {
            "associateNo": 1,
            "name": "Et Cetera Solutions"
          },
          {
            "associateNo": 2,
            "name": "Rock And Random"
          },
          {
            "associateNo": 3,
            "name": "Handy Help AS"
          },
          {
            "associateNo": 4,
            "name": "EasyWay Crafting"
          },
          {
            "associateNo": 5,
            "name": "Zen Services AS"
          }
        ]
      }
    }
  }
}

For a following page, we need to take the value of the endCursor return from a query and supply it as the argument to after. This is shown in the following example, where "NQ==" from the query above was supplied for the after parameter:

Query

query read($cid : Int!)
{
  useCompany(no: $cid)
  {
    associate(first: 5, after: "NQ==")
    {
      pageInfo
      {
        hasNextPage
        hasPreviousPage
        startCursor
        endCursor
      }
      items
      {
        associateNo
        name
      }
    }
  }
}
Result

{
  "data": {
    "useCompany": {
      "associate": {
        "pageInfo": {
          "hasNextPage": true,
          "hasPreviousPage": true,
          "startCursor": "Ng==",
          "endCursor": "MTA="
        },
        "items": [
          {
            "associateNo": 6,
            "name": "Smart Vita AS"
          },
          {
            "associateNo": 7,
            "name": "Full Force Services"
          },
          {
            "associateNo": 8,
            "name": "HomeRun Auto AS"
          },
          {
            "associateNo": 9,
            "name": "Trade Kraft AS"
          },
          {
            "associateNo": 10,
            "name": "Nodic Cool Sports AS"
          }
        ]
      }
    }
  }
}

Requests with backwards pagination are performed similarly, exept that last and before are used instead of first and after. In the following example, the value of before is taken from the value of startCursor returned by the previous query that returned the second page of associates (with five records per page). As a result, the data returned from this new query is actually the first page of associates.

Query

query read($cid : Int!)
{
  useCompany(no: $cid)
  {
    associate(last: 5, before: "Ng==")
    {
      pageInfo
      {
        hasNextPage
        hasPreviousPage
        startCursor
        endCursor
      }
      items
      {
        associateNo
        name
      }
    }
  }
}
Result

{
  "data": {
    "useCompany": {
      "associate": {
        "pageInfo": {
          "hasNextPage": true,
          "hasPreviousPage": false,
          "startCursor": "MQ==",
          "endCursor": "NQ=="
        },
        "items": [
          {
            "associateNo": 1,
            "name": "Et Cetera Solutions"
          },
          {
            "associateNo": 2,
            "name": "Rock And Random"
          },
          {
            "associateNo": 3,
            "name": "Handy Help AS"
          },
          {
            "associateNo": 4,
            "name": "EasyWay Crafting"
          },
          {
            "associateNo": 5,
            "name": "Zen Services AS"
          }
        ]
      }
    }
  }
}

Providing both forward and backward pagination arguments (first/after and last/before) is illegal and the query will fail with an error.

If pagination arguments are not supplied a default page size of 5000 records is used.

When you fetch data in pages (which is recommended for most scenarious) you can fetch the total number of objects in the table with disregard to the page that is fetched or the size of the page. This is possible with the totalCount. This will return the number of records that match the filter (if any) but ignoring paginagion (if any).

This feature is exemplified below. We, again, fetch the first 5 associates that have a customer number greater or equal than 11000 but also request the total number of records that match this filter.

Query

query read($cid : Int!)
{
  useCompany(no: $cid)
  {
    associate(
      first: 5,
      filter: {customerNo :{_gte: 11000}})
    {
      totalCount
      pageInfo
      {
        hasNextPage
        hasPreviousPage
        startCursor
        endCursor
      }
      items
      {
        associateNo
        customerNo
        name
      }
    }
  }
}
Result

{
  "data": {
    "useCompany": {
      "associate": {
        "totalCount": 29,
        "pageInfo": {
          "hasNextPage": true,
          "hasPreviousPage": false,
          "startCursor": "MQ==",
          "endCursor": "NQ=="
        },
        "items": [
          {
            "associateNo": 308,
            "customerNo": 11000,
            "name": "Auto Glory AS"
          },
          {
            "associateNo": 309,
            "customerNo": 11001,
            "name": "Home Team Business"
          },
          {
            "associateNo": 310,
            "customerNo": 11003,
            "name": "Corpus Systems"
          },
          {
            "associateNo": 311,
            "customerNo": 11004,
            "name": "Dash Credit AS"
          },
          {
            "associateNo": 312,
            "customerNo": 11007,
            "name": "Smart Help Services AS"
          }
        ]
      }
    }
  }
}

You can skip a number of records before feetching a page (with the size indicated by either the first or the last arguments) using the argument skip. This is an optional argument. When present, it indicates the number of records to be skipped. Here is an example:

Forward

query read($cid : Int!)
{
  useCompany(no: $cid)
  {
    generalLedgerAccount(skip : 5,
                         first: 10)
    {
      totalCount
      items
      {
        accountNo
      }
    }
  }
}
Backward

query read($cid : Int!)
{
  useCompany(no: $cid)
  {
    generalLedgerAccount(skip : 5,
                         last : 10,
                         before: "MTE=")
    {
      totalCount
      items
      {
        accountNo
      }
    }
  }
}

Warning

Because the execution of queries for joined tables is optimized for performance, pagination does not work as described in this document. To understand the problem and the workaround see Unoptimized queries.

Pagination in depth

To understand how pagination works, we will consider the following example, where the general ledger account table contains 25 records. It could look like this:

        page 1 (10 records)                    page 2 (10 records)            page 3 (5 records)
|---------------------------------|---------------------------------|---------------------------------|

| 1000 | 1020 | ... | 1100 | 1120 | 1130 | 1140 | ... | 1240 | 1250 | 1260 | 1270 | 1280 | 1300 | 1310 |

Forward pagination

To fetch the records from the beginning, we perform a query like this:

Query

query read($cid : Int!)
{
  useCompany(no: $cid)
  {
    generalLedgerAccount(first : 10,
                         after: null)
    {
      pageInfo
      {
        hasNextPage
        hasPreviousPage
        startCursor
        endCursor
      }
      items
      {
        accountNo
      }
    }
  }
}
Result

{
  "data": {
    "useCompany": {
      "generalLedgerAccount": {
        "pageInfo": {
          "hasNextPage": true,
          "hasPreviousPage": false,
          "startCursor": "MQ==",
          "endCursor": "MTA="
        },
        "items": [
          {
            "accountNo": 1000
          },
          {
            "accountNo": 1020
          },
          ...
          {
            "accountNo": 1100
          },
          {
            "accountNo": 1120
          }
        ]
      }
    }
  }
}

This returns the first ten records, from index 1 to 10, and the startCursor and endCursor are pointing to the first and last elements in the set.

        page 1 (10 records)
|---------------------------------|

| 1000 | 1020 | ... | 1100 | 1120 | 1130 | 1140 | ... | 1240 | 1250 | 1260 | 1270 | 1280 | 1300 | 1310 |
   ^                          ^
   |                          |
 startCursor                endCursor

To fetch the next page of 10 records, we provide the value of endCursor as the argument for after:

Query

query read($cid : Int!)
{
  useCompany(no: $cid)
  {
    generalLedgerAccount(first : 10,
                         after: "MTA=")
    {
      pageInfo
      {
        hasNextPage
        hasPreviousPage
        startCursor
        endCursor
      }
      items
      {
        accountNo
      }
    }
  }
}
Result

{
  "data": {
    "useCompany": {
      "generalLedgerAccount": {
        "pageInfo": {
          "hasNextPage": true,
          "hasPreviousPage": true,
          "startCursor": "MTE=",
          "endCursor": "MjA="
        },
        "items": [
          {
            "accountNo": 1130
          },
          {
            "accountNo": 1140
          },
          ...
          {
            "accountNo": 1240
          },
          {
            "accountNo": 1250
          }
        ]
      }
    }
  }
}

This returns the second page of ten records, from index 11 to 20, and the startCursor and endCursor are pointing to the first and last elements in the set.

                           after            page 2 (10 records)
                             |    |---------------------------------|
                             v
| 1000 | 1020 | ... | 1100 | 1120 | 1130 | 1140 | ... | 1240 | 1250 | 1260 | 1270 | 1280 | 1300 | 1310 |
                                     ^                          ^
                                     |                          |
                                   startCursor                endCursor

To fetch the next page of 10 records, we repeat the operation, again, using the endCursor for the value of the after argument:

Query

query read($cid : Int!)
{
  useCompany(no: $cid)
  {
    generalLedgerAccount(first : 10,
                         after: "MjA=")
    {
      pageInfo
      {
        hasNextPage
        hasPreviousPage
        startCursor
        endCursor
      }
      items
      {
        accountNo
      }
    }
  }
}
Result

{
  "data": {
    "useCompany": {
      "generalLedgerAccount": {
        "pageInfo": {
          "hasNextPage": false,
          "hasPreviousPage": true,
          "startCursor": "MjE=",
          "endCursor": "MjU="
        },
        "items": [
          {
            "accountNo": 1260
          },
          {
            "accountNo": 1270
          },
          {
            "accountNo": 1280
          },
          {
            "accountNo": 1300
          },
          {
            "accountNo": 1310
          },
        ]
      }
    }
  }
}

This returns a page of only 5 records, from index 21 to 25, and the startCursor and endCursor are pointing to the first and last elements in the set.

                                                              after          page 3 (5 records)
                                                                |   |----------------------------------|
                                                                v
| 1000 | 1020 | ... | 1100 | 1120 | 1130 | 1140 | ... | 1240 | 1250 | 1260 | 1270 | 1280 | 1300 | 1310 |
                                                                       ^                           ^
                                                                       |                           |
                                                                     startCursor                 endCursor

The value of the hasNextPage field is false because there are no more records to fetch.

Note

The value used for the after argument should be the value of the endCursor from the previous page (unless it’s null, in which case it means the fetching should start from the beginning). The after cursor identifies the position of the last record in a (previous) page. The record at the position represented by the cursor passed to the after argument is not included in the result set. A page starts with the record following the one identified by after.

It is also possible to skip records before fetching a page. This is done by providing the skip argument. The following example skips the first 3 records before fetching a page of 10 records. We don’t start from the beginning, but at the end of the first page of 10 records, as exemplify below:

                           after     skip 3 records            page of 10 records
                             |    >-------------------<|-----------------------------------------------|
                             v
| 1000 | 1020 | ... | 1100 | 1120 | 1130 | 1140 | 1150 | 1160 | ... | 1240 | 1250 | 1260 | 1270 | 1280 | 1300 | 1310 |
                                                          ^                                        ^
                                                          |                                        |
                                                        startCursor                              endCursor
Query

query read($cid : Int!)
{
  useCompany(no: $cid)
  {
    generalLedgerAccount(first : 10,
                         skip : 3,
                         after: "MTA=")
    {
      pageInfo
      {
        hasNextPage
        hasPreviousPage
        startCursor
        endCursor
      }
      items
      {
        accountNo
      }
    }
  }
}
Result

{
  "data": {
    "useCompany": {
      "generalLedgerAccount": {
        "pageInfo": {
          "hasNextPage": true,
          "hasPreviousPage": true,
          "startCursor": "MTQ=",
          "endCursor": "MjM="
        },
        "items": [
          {
            "accountNo": 1160
          },
          {
            "accountNo": 1200
          },
          ...
          {
            "accountNo": 1270
          },
          {
            "accountNo": 1280
          }
        ]
      }
    }
  }
}

Backward pagination

Backward pagination works similarly, except that the last and before arguments are used instead of first and after. The argument last indicates how many records the page should contain. The argument before represents the position of the record before which the page is located. The record at the before position is not included in the result set.

Therefore, if in the preceding example we fetched all the records in the table, and now want to move backwards, but use the value of the endCursor as the arguement for before, then this last record will not be included in the returned page.

                                                page of 10 records                                 before
                           |---------------------------------------------------------------------| |
                                                                                                   v
| 1000 | 1020 | ... | 1160 | 1200 | 1210 | 1220 | 1230 | 1240 | 1250 | 1260 | 1270 | 1280 | 1300 | 1310 |
                              ^                                                              ^
                              |                                                              |
                            startCursor                                                    endCursor
Query

query read($cid : Int!)
{
  useCompany(no: $cid)
  {
    generalLedgerAccount(last : 10,
                         before: "MjU=")
    {
      pageInfo
      {
        hasNextPage
        hasPreviousPage
        startCursor
        endCursor
      }
      items
      {
        accountNo
      }
    }
  }
}
Result

{
  "data": {
    "useCompany": {
      "generalLedgerAccount": {
        "pageInfo": {
          "hasNextPage": true,
          "hasPreviousPage": false,
          "startCursor": "MTU=",
          "endCursor": "MjQ="
        },
        "items": [
          {
            "accountNo": 1200
          },
          {
            "accountNo": 1210
          },
          ...
          {
            "accountNo": 1280
          },
          {
            "accountNo": 1300
          }
        ]
      }
    }
  }
}
Last modified September 24, 2024