Hi!
I would like to bring to your attention a problem I'm facing at my company. The tl;dr is that there's no clean way to do non relay based pagination and that multiple features seem to be tightly coupled with relay. I get into details below.
Graphql seems to be very opinionated about what's the best way to do pagination, you can see this here: https://graphql.org/learn/pagination/#pagination-and-edges. That is fine if you're Facebook and you're doing infinitely scrolling pages of spam and ads. That's not what I've found myself doing in the past 14 years of my programming experience so far. At my current company we have 2 good use cases for relay based pagination: support messaging (like a chat app) and an activity stream log. That's 2 cases out of dozens where relay is NOT a good candidate. What we need is:
This seemingly simple pagination approach which has been used for decades is really hard to do with strawberry graphql django. We cannot use the paginated field as we wouldn't know the total count. We can't use relay connection and ListConnectionWithTotalCount
as we lose the ability to jump to a given page. We cannot use an offset based cursor as that would make it non opaque - the API client would have to know what the cursor is and how to calculate it in order to jump to a specific page. Relay spec is pretty clear about the fact that cursors should be opaque:
As a reminder that the cursors are opaque and that their format should not be relied upon, we suggest base64 encoding them.
So what does that leave us with? Well, we could implement our custom extension and pagination logic right? RIGHT?
Too many feautres of strawberry-graphql-django are tightly coupled with relay spec. First and foremost the optimizer. If we don't inherit from the Connection
class the optimizer will never kick in. Furthermore, from our testing, it seems that more functionalities are tightly coupled with the base Connection
class, e.g. see references to is_connection
for instance. There could be more things that fall apart without using Connection
class, I just don't know.
Are there any other options that I haven't mentioned? Maybe I'm missing something obvious here? For the time being, we are doing a total hack to do this kind of pagination, see for yourself:
@strawberry.type
class PaginatedConnection(strawberry_django.relay.ListConnectionWithTotalCount[relay_types.NodeType]):
@classmethod
def resolve_connection(
cls,
nodes: relay_types.NodeIterableType[relay_types.NodeType],
*,
info: Optional[types.Info] = None,
offset: int = 0,
limit: int = 1000,
**kwargs: Any,
) -> Self:
nodes_queryset = cast(django_models.QuerySet[django_models.Model], nodes)
sliced_nodes = nodes_queryset[offset : offset + limit]
edges = [
strawberry.relay.Edge[relay_types.NodeType](cursor="", node=cast(relay_types.NodeType, v))
for v in sliced_nodes
]
page_info = strawberry.relay.PageInfo(
start_cursor=None,
end_cursor=None,
has_previous_page=False,
has_next_page=False,
)
return cls(page_info=page_info, edges=edges, nodes=nodes)
class PaginatedConnectionExtension(strawberry_django_field.StrawberryDjangoConnectionExtension):
connection_type: Type[PaginatedConnection[relay_types.Node]]
def apply(self, field: strawberry_django_field.StrawberryDjangoField) -> None:
super().apply(field)
# Drop relay arguments
field.arguments = [a for a in field.arguments if a.python_name not in ["after", "before", "first", "last"]]
# Add offset/limit arguments
field.arguments = [
*field.arguments,
arguments.StrawberryArgument(
python_name="offset",
graphql_name=None,
type_annotation=annotation.StrawberryAnnotation(int),
description=("Number of rows that are omitted before the beginning of the result set"),
default=0,
),
arguments.StrawberryArgument(
python_name="limit",
graphql_name=None,
type_annotation=annotation.StrawberryAnnotation(int),
description=(
"Limits the number of values returned from the result set. Negative values imply no limit."
),
default=1000,
),
]
def resolve(
self,
next_: field_extension.SyncExtensionResolver,
source: Any,
info: types.Info,
*,
limit: int,
offset: int,
**kwargs: Any,
) -> Any:
# Copy pasted from base class with adjusted signature for limit/offset
assert self.connection_type is not None
nodes = cast(Iterable[relay_types.Node], next_(source, info, **kwargs))
# We have a single resolver for both sync and async, so we need to check if
# nodes is awaitable or not and resolve it accordingly
if inspect.isawaitable(nodes):
async def async_resolver():
resolved = self.connection_type.resolve_connection(
await nodes,
info=info,
limit=limit,
offset=offset,
)
if inspect.isawaitable(resolved):
resolved = await resolved
return resolved
return async_resolver()
return self.connection_type.resolve_connection(
nodes,
info=info,
limit=limit,
offset=offset,
)
To sum up, how could this pagination mechanism be best achieved with strawberry django? Looking forward, what kind of improvements could we do not to overly rely on the Relay spec? Probably the hardest thing to get right (and also the most important) would be the optimizer.
I hope we can have a fruitful discussion!
Pay now to fund the work behind this issue.
Get updates on progress being made.
Maintainer is rewarded once the issue is completed.
You're funding impactful open source efforts
You want to contribute to this effort
You want to get funding like this too