strawberry-graphql / strawberry

A GraphQL library for Python that leverages type annotations 🍓
https://strawberry.rocks
MIT License
3.94k stars 520 forks source link

Add offset argument to relay connection #3053

Open mattflorence opened 1 year ago

mattflorence commented 1 year ago

It would be very useful to add an offset argument to relay connection. It's a feature that is implemented in graphene. See https://github.com/graphql-python/graphene-django/blob/v3.1.5/graphene_django/fields.py#L139-L145

Feature Request Type

Description

When using this library's limit/offset pagination, pageInfo doesn't update hasPreviousPage and hasNextPage, they are always false. Currently, I am using a custom connection as a workaround:

import strawberry
from strawberry.relay.utils import from_base64, to_base64

import strawberry_django
from strawberry_django import relay

@strawberry.type
class CustomConnection(relay.ListConnectionWithTotalCount[strawberry.relay.NodeType]):
    """
    A strawberry connection to count the number of query results
    """

    # Adding offset argument to custom connection
    @classmethod
    def resolve_connection(
        cls,
        nodes: NodeIterableType[strawberry.relay.NodeType],
        *,
        info: Info,
        before: Optional[str] = None,
        after: Optional[str] = None,
        first: Optional[int] = None,
        last: Optional[int] = None,
        offset: Optional[int] = None,
        **kwargs: Any,
    ) -> AwaitableOrValue[Self]:

        # This implemntation is based on the graphene
        # implementation of first/offset pagination
        if offset:
            if after:
                offset += from_base64(after) + 1
            # input offset starts at 1 while the offset starts at 0
            after = to_base64("arrayconnection", offset - 1)

        conn = super().resolve_connection(
            nodes,
            info=info,
            before=before,
            after=after,
            first=first,
            last=last,
            **kwargs,
        )

        if inspect.isawaitable(conn):

            async def wrapper():
                resolved = await conn
                resolved.nodes = nodes
                return resolved

            return wrapper()

        conn = cast(Self, conn)
        conn.nodes = nodes
        return conn

But that means a custom resolver would have to look like this:

    @strawberry.field
    def foos(
        self,
        info: strawberry.types.Info,
        before: Optional[str] = None,
        after: Optional[str] = None,
        first: Optional[int] = None,
        last: Optional[int] = None,
        offset: Optional[int] = None,
    ) -> Optional[CustomConnection[FooType]]:
        # Resolver logic here returning queryset qs
        return CustomConnection[FooType].resolve_connection(info=info, nodes=qs, offset=offset, first=first, last=last, after=after, before=before)

Upvote & Fund

Fund with Polar

bellini666 commented 1 year ago

For curiosity, what would be an use case for an offset parameter?

I mean, after exists because of that, and even in the graphene link you shared has a # Remove the offset parameter and convert it to an after cursor. comment, meaning they actually want to remove it at some point.

mattflorence commented 1 year ago

It would reduce some complexity using the GraphQL API in frontend for pagination, we wouldn't need to store the cursor returned by a previous query and use it as after argument in the next. If we know we have 5 items per page for example, than offset is always a multiple of 5, we can easily jump to any page we want. Given this it would be more convenient to use offset rather than after. I think what they meant by this : # Remove the offset parameter and convert it to an after cursor is to take the argument offset and evaluate what after would be in that case inside the resolve_connection method and not remove it from the library.

bellini666 commented 1 year ago

It would reduce some complexity using the GraphQL API in frontend for pagination, we wouldn't need to store the cursor returned by a previous query and use it as after argument in the next. If we know we have 5 items per page for example, than offset is always a multiple of 5, we can easily jump to any page we want. Given this it would be more convenient to use offset rather than after.

Hrm, I see.

Actually, it is already possible for you to do that without needing an offset argument. As long as you are using the ListConnection, you can pass any cursor you want to it. I described that solution in this comment

Regarding changing the API, that would need to be done on strawberry core itself because the replay implementation is there. I'll move this issue to that repo so we can properly discuss this with other people.