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.
from typing import List, TypeVar, Dict, Any, Genericimport strawberry
@strawberry.typeclass 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.
Item = TypeVar("Item")
@strawberry.typeclass PaginationWindow(Generic[Item]): items: List[Item] = 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.typeclass 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 window -
totalItemsCount
- 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.