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 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 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.
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))
|