Implementing Offset-Based Pagination#
Make sure to check our introduction to pagination here!
Let us implement offset-based pagination in GraphQL. By the end of this tutorial, we should be able to return a sorted, filtered, and paginated list of users.
Let us model the User
type, which represents one user, with a name, occupation, and age.
# example.py
from typing import List, TypeVar, Dict, Any
import strawberry
@strawberry.type
class User:
name: str = strawberry.field(
description="The name of the user."
)
occupation: str = strawberry.field(
description="The occupation of the user."
)
age: int = strawberry.field(
description="The age of the user."
)
@staticmethod
def from_row(row: Dict[str, Any]):
return User(
name=row['name'],
occupation=row['occupation'],
age=row['age']
)
Let us now model the PaginationWindow
, which represents one “slice” of sorted, filtered, and paginated items.
GenericType = TypeVar("GenericType")
@strawberry.type
class PaginationWindow(List[GenericType]):
items: List[GenericType] = strawberry.field(
description="The list of items in this pagination window."
)
total_items_count: int = strawberry.field(
description="Total number of items in the filtered dataset."
)
Note that PaginationWindow
is generic - it can represent a slice of users, or a slice of any other type of
items that we might want to paginate.
PaginationWindow
also contains total_items_count
, which specifies how many items there are in total in the filtered
dataset, so that the client knows what the highest offset value can be.
Let’s define the query:
@strawberry.type
class Query:
@strawberry.field(description="Get a list of users.")
def users(self,
order_by: str,
limit: int,
offset: int = 0,
name: str | None = None,
occupation: str| None = None
) -> PaginationWindow[User]:
filters = {}
if name:
filters['name'] = name
if occupation:
filters['occupation'] = occupation
return get_pagination_window(
dataset=user_data,
ItemType=User,
order_by=order_by,
limit=limit,
offset=offset,
filters=filters
)
schema = strawberry.Schema(query=Query)
Now we’ll define a mock dataset and implement the get_pagination_window
function, which is used by the users
query.
For the sake of simplicity, our dataset will be an in-memory list containing four users:
user_data = [
{
"id": 1,
"name": "Norman Osborn",
"occupation": "Founder, Oscorp Industries",
"age": 42
},
{
"id": 2,
"name": "Peter Parker",
"occupation": "Freelance Photographer, The Daily Bugle",
"age": 20
},
{
"id": 3,
"name": "Harold Osborn",
"occupation": "President, Oscorp Industries",
"age": 19
},
{
"id": 4,
"name": "Eddie Brock",
"occupation": "Journalist, The Eddie Brock Report",
"age": 20
}
]
Here’s the implementation of the get_pagination_window
function. Note that it is generic and should work for all item types,
not only for the User
type.
def get_pagination_window(
dataset: List[GenericType],
ItemType: type,
order_by: str,
limit: int,
offset: int = 0,
filters: dict[str, str] = {}) -> PaginationWindow:
"""
Get one pagination window on the given dataset for the given limit
and offset, ordered by the given attribute and filtered using the
given filters
"""
if limit <= 0 or limit > 100:
raise Exception(f'limit ({limit}) must be between 0-100')
if filters:
dataset = list(filter(lambda x: matches(x, filters), dataset))
dataset.sort(key=lambda x: x[order_by])
if offset != 0 and not 0 <= offset < len(dataset):
raise Exception(f'offset ({offset}) is out of range '
f'(0-{len(dataset) - 1})')
total_items_count = len(dataset)
items = dataset[offset:offset + limit]
items = [ItemType.from_row(x) for x in items]
return PaginationWindow(
items=items,
total_items_count=total_items_count
)
def matches(item, filters):
"""
Test whether the item matches the given filters.
This demo only supports filtering by string fields.
"""
for attr_name, val in filters.items():
if val not in item[attr_name]:
return False
return True
The above code first filters the dataset according to the given filters, then sorts the dataset according to the
given order_by
field.
It then calculates total_items_count
(this must be done after filtering), and then slices the relevant items
according to offset
and limit
.
Finally, it converts the items to the given strawberry type, and returns a PaginationWindow
containing these items,
as well as the total_items_count
.
In a real project, you would probably replace this with code that fetches from a database using offset
and limit
.
If you’re using Strawberry with the Django web framework, you might want to make use of the Django pagination API. You can check it out here.
Running the Query#
Now, let us start the server and see offset-based pagination in action!
strawberry server example:schema
You will get the following message:
Running strawberry on http://0.0.0.0:8000/graphql 🍓
Go to http://0.0.0.0:8000/graphql to open GraphiQL, and run the following query to get first two users, ordered by name:
{
users(orderBy: "name", offset: 0, limit: 2) {
items {
name
age
occupation
}
totalItemsCount
}
}
The result should look like this:
{
"data": {
"users": {
"items": [
{
"name": "Eddie Brock",
"age": 20,
"occupation": "Journalist, The Eddie Brock Report"
},
{
"name": "Harold Osborn",
"age": 19,
"occupation": "President, Oscorp Industries"
}
],
"totalItemsCount": 4
}
}
}
The result contains:
items
- A list of the users in this pagination windowtotalItemsCount
- The total number of items in the filtered dataset. In this case, since no filter was given in the request,totalItemsCount
is 4, which is equal to the total number of users in the in-memory dataset.
Get the next page of users by running the same query, after incrementing
offset
by limit
.
Repeat until offset
reaches totalItemsCount
.
Running a Filtered Query#
Let’s run the query again, but this time we’ll filter out some users based on their occupation.
{
users(orderBy: "name", offset: 0, limit: 2, occupation: "ie") {
items {
name
age
occupation
}
totalItemsCount
}
}
By supplying occupation: "ie"
in the query, we are requesting only users whose occupation
contains the substring “ie”.
This is the result:
{
"data": {
"users": {
"items": [
{
"name": "Eddie Brock",
"age": 20,
"occupation": "Journalist, The Eddie Brock Report"
},
{
"name": "Harold Osborn",
"age": 19,
"occupation": "President, Oscorp Industries"
}
],
"totalItemsCount": 3
}
}
}
Note that totalItemsCount
is now 3 and not 4, because only 3 users in total
match the filter.