Graphene-django: offset pagination support for relay connection

Created on 1 Nov 2019  路  7Comments  路  Source: graphql-python/graphene-django

Hi all, I have a use case to paginate results based on an offset. I figured I would be able to add "offset" into my graphQL query to achieve this. (source: https://graphql.org/learn/pagination/)

Specifically, I want to implement fetching page 3 results of a list, where each page has a size of 20. Doing this query throws "unknown argument "offset" on field allEntityA"

query {
  allEntityA(first: 20, offset: 40) {
     edges {
       node {
       }
     }
  }
}

I have already trasnformed my Django model into a type relay.Node)

class entityANode(DjangoObjectType):
  class Meta:
    model = models.EntityA
    interfaces = (relay.Node,)

How can I achieve jumping from page 1 to 3 without using cursor navigation?

鉁╡nhancement

Most helpful comment

Not stale! This is a very common requirement and @NateScarlet's PR provides it.

All 7 comments

I've archive this by #563, but no one come to review.
You can try my https://github.com/NateScarlet/graphene-django-tools

Our team would find #563 really helpful, is there anything we can do to help get it merged?

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Not stale! This is a very common requirement and @NateScarlet's PR provides it.

Hi all, I have a use case to paginate results based on an offset. I figured I would be able to add "offset" into my graphQL query to achieve this. (source: https://graphql.org/learn/pagination/)

Specifically, I want to implement fetching page 3 results of a list, where each page has a size of 20. Doing this query throws "unknown argument "offset" on field allEntityA"

query {
  allEntityA(first: 20, offset: 40) {
     edges {
       node {
       }
     }
  }
}

I have already trasnformed my Django model into a type relay.Node)

class entityANode(DjangoObjectType):
  class Meta:
    model = models.EntityA
    interfaces = (relay.Node,)

How can I achieve jumping from page 1 to 3 without using cursor navigation?

I have written a blog to address this issue. It can be achieved using "first" and "last" arguments only
read more here https://cloudworks.dukamneti.co.ke/blog/go-specific-page-graphene-django/

This is actually very easy to implement considering the underlying pagination in Graphene Django is an offset pagination.
Indeed the cursor contains simply an offset already so you can rely on that.
Here is a simple implementation that can be used in case it does not get incorporated in the project (it could be incorporated in the DjangoFilterConnectionField class as described here):

from graphql_relay.connection.arrayconnection import cursor_to_offset, offset_to_cursor

from graphene_django.filter import DjangoFilterConnectionField


class OffsetConnectionField(DjangoFilterConnectionField):

聽 聽 def __init__(self, *args, **kwargs):
聽 聽 聽 聽 kwargs.setdefault("offset", graphene.Int())
聽 聽 聽 聽 super().__init__(*args, **kwargs)

聽 聽 @classmethod
聽 聽 def connection_resolver(
聽 聽 聽 聽 聽 聽 cls,
聽 聽 聽 聽 聽 聽 resolver,
聽 聽 聽 聽 聽 聽 connection,
聽 聽 聽 聽 聽 聽 default_manager,
聽 聽 聽 聽 聽 聽 queryset_resolver,
聽 聽 聽 聽 聽 聽 max_limit,
聽 聽 聽 聽 聽 聽 enforce_first_or_last,
聽 聽 聽 聽 聽 聽 root,
聽 聽 聽 聽 聽 聽 info,
聽 聽 聽 聽 聽 聽 **args
聽 聽 ):
聽 聽 聽 聽 """
聽 聽 聽 聽 Check parameter compatibility for `offset`.
聽 聽 聽 聽 Using offset with before could lead to negative indexing which is not supported by Relay so we forbid it altogether.
聽 聽 聽 聽 """
聽 聽 聽 聽 offset = args.get("offset")
聽 聽 聽 聽 before = args.get("before")

聽 聽 聽 聽 if offset is not None:
聽 聽 聽 聽 聽 聽 assert before is None, (
聽 聽 聽 聽 聽 聽 聽 聽 "You can't provide a `before` at the same time as an `offset` value to properly paginate the `{}` connection."
聽 聽 聽 聽 聽 聽 ).format(info.field_name)

聽 聽 聽 聽 return super().connection_resolver(
聽 聽 聽 聽 聽 聽 resolver,
聽 聽 聽 聽 聽 聽 connection,
聽 聽 聽 聽 聽 聽 default_manager,
聽 聽 聽 聽 聽 聽 queryset_resolver,
聽 聽 聽 聽 聽 聽 max_limit,
聽 聽 聽 聽 聽 聽 enforce_first_or_last,
聽 聽 聽 聽 聽 聽 root,
聽 聽 聽 聽 聽 聽 info,
聽 聽 聽 聽 聽 聽 **args
聽 聽 聽 聽 )

聽 聽 @classmethod
聽 聽 def resolve_connection(cls, connection, args, iterable, max_limit=None):
聽 聽 聽 聽 """
聽 聽 聽 聽 Take the offset out of the argument and if it is not `None` convert it to a `after` cursor.
聽 聽 聽 聽 """
聽 聽 聽 聽 offset = args.pop("offset", None)
聽 聽 聽 聽 after = args.get("after")
聽 聽 聽 聽 if offset:
聽 聽 聽 聽 聽 聽 if after:
聽 聽 聽 聽 聽 聽 聽 聽 offset += cursor_to_offset(after) + 1
            # input offset starts at 1 while the graphene offset starts at 0
            args["after"] = offset_to_cursor(offset - 1)
聽 聽 聽 聽 return super().resolve_connection(connection, args, iterable, max_limit)

and you just need to use OffsetConnectionField instead to define your query.
For example in the cookbook example of the documentation:

# cookbook/graphql/category.py
import graphene

from cookbook.models import Category, Ingredient


class CategoryNode(DjangoObjectType):
    class Meta:
        model = Category
        interfaces = (graphene.relay.Node, )


class Query(graphene.ObjectType):
    category = graphene.relay.Node.Field(CategoryNode)
    all_categories = OffsetConnectionField(CategoryNode)

* Note *
@HenryKuria Your solution is not completely true. The combination of first and last does not completely cover the offset because of the RELAY_CONNECTION_MAX_LIMIT setting.
Indeed you can't set first: 310, last: 10 as it would raise an error.
While with the implementation that I proposed we use a cursor so we do not get this error when doing first: 10, offset: 300.

I think you can make a cursor by base64 encoding page offset like below

encode('arrayconnection:pageNumber')

It's not a robust solution but possible anyway.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

licx picture licx  路  3Comments

BrianChapman picture BrianChapman  路  3Comments

flame0 picture flame0  路  4Comments

Dawidpol picture Dawidpol  路  4Comments

StefanoSega picture StefanoSega  路  4Comments