Pagination๐Ÿ”—

In this section, we'll cover the everything necessary for adding pagination to your GraphQL schema. Undine supports both offset and cursor based pagination.

Offset pagination๐Ÿ”—

Offset pagination is the simplest pagination method. It allows paginating a list by specifying an offset from the first item, and a limit for the number of items to return.

Offset pagination works well for lists where each item's index never changes, e.g., a list sorted by a timestamp or an auto-incrementing primary key. If this is not the case, you should use cursor based pagination instead, because changes in the middle of the list between page queries can cause items to be skipped or duplicated.

To add offset pagination to a QueryType Entrypoint, you need to wrap with the OffsetPagination class.

from undine import Entrypoint, QueryType, RootType
from undine.pagination import OffsetPagination

from .models import Task


class TaskType(QueryType[Task]): ...


class Query(RootType):
    paged_tasks = Entrypoint(OffsetPagination(TaskType))

This creates the following GraphQL types.

type TaskType {
  pk: Int!
  name: String!
  done: Boolean!
  createdAt: DateTime!
}

type Query {
  pagedTasks(
    offset: Int
    limit: Int
  ): [TaskType!]!
}

Offset pagination can also be used with many-related Fields.

from undine import Entrypoint, Field, QueryType, RootType
from undine.pagination import OffsetPagination

from .models import Person, Task


class PersonType(QueryType[Person]): ...


class TaskType(QueryType[Task]):
    assignees = Field(OffsetPagination(PersonType))


class Query(RootType):
    paged_tasks = Entrypoint(OffsetPagination(TaskType))

This creates the following GraphQL types.

type PersonType {
  pk: Int!
  name: String!
  email: Email!
  tasks: [TaskType!]!
}

type TaskType {
  pk: Int!
  name: String!
  done: Boolean!
  createdAt: DateTime!
  assignees(
    offset: Int
    limit: Int
  ): [PersonType!]!
}

type Query {
  pagedTasks(
    offset: Int
    limit: Int
  ): [TaskType!]!
}

Cursor pagination๐Ÿ”—

Cursor based pagination works by assigning items an opaque unique identifier called a "cursor". Pages can then be defined as starting before or after a given cursor. This makes cursor based pagination more resilient to changes in the paginated list, since the cursors themselves do not change when items are added or removed.

Additionally, cursor based pagination wraps the paginated items as Edge objects inside a Connection object. These objects contain additional information about the pagination state, such as the total count of items, cursor values, or whether a next or previous page exists. For more information on cursor pagination, see the GraphQL Cursor Connections Specification.

To add cursor pagination to a QueryType Entrypoint, you need to wrap with the Connection class.

from undine import Entrypoint, QueryType, RootType
from undine.relay import Connection

from .models import Task


class TaskType(QueryType[Task]): ...


class Query(RootType):
    paged_tasks = Entrypoint(Connection(TaskType))

This creates the following GraphQL types.

type TaskType {
  pk: Int!
  name: String!
  done: Boolean!
  createdAt: DateTime!
}

type TaskTypeEdge {
  cursor: String!
  node: TaskType
}

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

type TaskTypeConnection {
  totalCount: Int!
  pageInfo: PageInfo!
  edges: [TaskTypeEdge!]!
}

type Query {
  pagedTasks(
    after: String
    before: String
    first: Int
    last: Int
  ): TaskTypeConnection!
}

Querying this Entrypoint will return a response like this:

{
  "data": {
    "pagedTasks": {
      "totalCount": 3,
      "pageInfo": {
        "hasNextPage": false,
        "hasPreviousPage": false,
        "startCursor": "YXJyYXljb25uZWN0aW9uOjA=",
        "endCursor": "YXJyYXljb25uZWN0aW9uOjI="
      },
      "edges": [
        {
          "cursor": "YXJyYXljb25uZWN0aW9uOjA=",
          "node": {
            "pk": 1,
            "name": "Task 1",
            "done": false
          }
        },
        {
          "cursor": "YXJyYXljb25uZWN0aW9uOjE=",
          "node": {
            "pk": 2,
            "name": "Task 2",
            "done": true
          }
        },
        {
          "cursor": "YXJyYXljb25uZWN0aW9uOjI=",
          "node": {
            "pk": 3,
            "name": "Task 3",
            "done": false
          }
        }
      ]
    }
  }
}

Similarly, cursor pagination can also be used with many-related Fields.

from undine import Entrypoint, Field, QueryType, RootType
from undine.relay import Connection

from .models import Person, Task


class PersonType(QueryType[Person]): ...


class TaskType(QueryType[Task]):
    assignees = Field(Connection(PersonType))


class Query(RootType):
    paged_tasks = Entrypoint(Connection(TaskType))

For Relay-compliant clients, see the Global Object IDs section for adding support for the Node interface.

Filtering and ordering๐Ÿ”—

If a FilterSet or an OrderSet has been added to a QueryType, their arguments will be added to the Entrypoint along with the pagination arguments for the specific pagination method. For example, for a Connection Entrypoint:

type Query {
  pagedTasks(
    after: String
    before: String
    first: Int
    last: Int
    filter: TaskFilterSet
    orderBy: [TaskOrderSet!]
  ): TaskTypeConnection!
}

Page size๐Ÿ”—

The default page size for all pagination methods is set by the PAGINATION_PAGE_SIZE setting. You can also use a different page size by using the page_size argument.

from undine import Entrypoint, QueryType, RootType
from undine.relay import Connection

from .models import Task


class TaskType(QueryType[Task]): ...


class Query(RootType):
    paged_tasks = Entrypoint(Connection(TaskType, page_size=20))

Setting page size to None will return all items in a single page.

Custom pagination strategies๐Ÿ”—

The default pagination strategies are accurate and performant for both top-level and nested fields (although calculating totalCount for nested Connections can be slow, since it requires a subquery for each parent item). Still, if you need to modify the pagination behavior, you can do so by providing a custom PaginationHandler class.

from undine import Entrypoint, QueryType, RootType
from undine.pagination import PaginationHandler
from undine.relay import Connection

from .models import Task


class CustomPaginationHandler(PaginationHandler):
    """Custom pagination logic."""


class TaskType(QueryType[Task]): ...


class Query(RootType):
    paged_tasks = Entrypoint(Connection(TaskType, pagination_handler=CustomPaginationHandler))