Cursor pagination
As mentioned in the previous section we can use cursor pagination when we have a lot of data as using an offset based pagination might not be performant.
In this section we'll see how we can implement a cursor based pagination for our
API. We'll allow our users to paginate the podcasts by passing a cursor
and
we'll be using the
relay specification to do so.
Relay specification
The relay specification defines how we can implement a cursor based pagination
for our API. It defines a Connection
type that contains a list of nodes and
some metadata about the pagination.
The Connection
type also defines a PageInfo
type that contains information
about the current page and if there are more pages.
The PageInfo
type contains the following fields: hasNextPage
,
hasPreviousPage
, startCursor
and endCursor
.
The startCursor
and endCursor
fields are the cursors for the first and last
node in the list of nodes.
An example query
{
podcasts(first: 2) {
pageInfo {
hasNextPage
}
edges {
cursor
node {
title
}
}
}
}
This query is more verbose than the previous one, but it provides us with more information about the pagination.
Implementing the cursor pagination
As with all our previous examples we also have function inside the data module
that will allow us to paginate data using cursors. The function is called
find_podcasts
and allows to pass a first
and after
argument.
Note: we are not implementing
before
andlast
but they can be implemented in a similar way.
Under the hood find_podcasts
uses django-cursor-pagination
to paginate the
data using performant cursors.
Before being able to implement the resolver, we need to implement the types for our pagination. The types we need to create are the following:
PodcastConnection
which is what will be returned by thepodcasts
field.PageInfo
which is the type for thepageInfo
field.PodcastEdge
which is the type for theedges
field.
Using python's generic types
Implementing these types all by hand can be tedious and error prone. Luckily python has a feature called generic types that allows us to create types that can be reused.
Strawberry is able to use Python's generic types to create types for us. Our project has these generic types already implemented for us, so we can use them to create our types.
They are defined inside api/pagination/types.py
and look like this:
from typing import Generic, TypeVar, Optional
import strawberry
from db.pagination import PageInfo as DBPageInfo
Node = TypeVar("Node")
@strawberry.type
class PageInfo:
has_next_page: bool
has_previous_page: bool
start_cursor: Optional[str]
end_cursor: Optional[str]
@strawberry.type
class Edge(Generic[Node]):
node: Node
cursor: str
@strawberry.type
class Connection(Generic[Node]):
edges: list[Edge[Node]]
page_info: PageInfo
We have defined the 3 types we need to implement our pagination, two of them use
python's generic types, Edge
and Connection
. We can use these types to
create our types for the pagination.
Using the generic types
We can use the generic types to create our types for the pagination. We can
immediately use them when defining our field and resolver for the podcasts
field.
import strawberry
from typing import Optional
from api.pagination.types import Connection, Edge, PageInfo
from db import data
from .types import Podcast
@strawberry.type
class PodcastsQuery:
# ... keep the other fields
@strawberry.field
async def podcasts(
self,
first: int = 10,
after: Optional[strawberry.ID] = None,
) -> Connection[Podcast]:
if first > 50:
raise ValueError("per page must be less than 50")
paginated_cursors = await data.find_podcasts(
first=first,
after=str(after) if after is not None else None,
)
page_info = PageInfo.from_db(paginated_cursors.page_info)
return Connection(
page_info=page_info,
edges=[
Edge(node=Podcast.from_db(edge.node), cursor=edge.cursor)
for edge in paginated_cursors.edges
],
)
We can see that we are using the Connection
type to define the return type of
our field by doing Connection[Podcast]
. Strawberry is able to use the generic
type to create the type for us, it will automatically create a type called
PodcastConnection
that will be added to our schema. This is done recursively
for all types that use the TypeVar, so we'll also have a PodcastEdge
type
defined for us.
If you want to learn more about pagination feel free to read this article: https://www.apollographql.com/blog/graphql/pagination/understanding-pagination-rest-graphql-and-relay/