ethe / pygraphy

A modern Pythonic GraphQL implementation.
MIT License
92 stars 13 forks source link

Suggestion: Paginated Field #11

Open grvhi opened 5 years ago

grvhi commented 5 years ago

I'm not sure if this will be useful to the library, but I've created a paginated_field decorator for my project to make it easier to create fields which support pagination in a consistent yet DRY manner. Essentially, the decorator wraps an Object inside a dynamically-generated Object class; the original Object becomes the data field and a page_info field is added.

Before:

{"myQuery": {"someField": "Foo", "someOtherField": "Bar"}}

After:

{"myQuery": {"data": {"someField": "Foo", "someOtherField": "Bar"}, "pageInfo": {"cursor": "some-cursor"}}}

Usage:

@paginated_field(MyObjectSchema)
def pwnd_emails(self, count: int = 10, cursor: str = None) -> Tuple[List[MyObjectSchema], str]:
        user_id = context.get().variables['user_id']
        data, cursor = get_my_objects(
            user_id, as_dicts=True, 
            limit=count, cursor=cursor
        )
        return [MyObjectSchema(**d) for d in data], cursor

Note that the method's return annotation is Tuple[List[MyObjectSchema], str]: this allows IDEs to correctly type hint and accept the method's return type. The paginated_field decorator (see below) changes the field's return type to reflect the correct GraphQL schema when the schema is generated.

So far, this is working well for me (although it is currently un-tested). If @ethe thinks it would be a useful addition to the library, I can update the code below to be more generic and to add support for more fields on PageInfo.

class PageInfo(Object):
    cursor: Optional[str]

class PaginatedObject(Object):
    page_info: PageInfo

PAGINATED_CLASSES = {}

def paginated_field(schema_type):
    new_name = f'Paginated{schema_type.__name__}'
    new_class = PAGINATED_CLASSES.get(new_name)
    if not new_class:
        new_class = make_dataclass(
            new_name,
            fields=[('data', List[schema_type])],
            bases=(PaginatedObject, )
        )
        PAGINATED_CLASSES[new_name] = new_class

    def decorator(f):
        f.__is_field__ = True
        f.__annotations__['return'] = PaginatedObject

        @wraps(f)
        def wrapper(*args, **kwargs):
            data, cursor = f(*args, **kwargs)
            # noinspection PyArgumentList
            return new_class(data=data, page_info=PageInfo(cursor=cursor))

        return wrapper

    return decorator

Suggestions welcome!

ethe commented 5 years ago

GraphQL provides pagination support in Relay but does not give an independent component. Pygraphy may take a long time to support Relay, and I also do not want to extend GraphQL. So it is sorry for supporting the pagination query now, you can still make some custom tools to support it.