Skip to content

Pass arguments to callables for resolving StreamField block values #322

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 12, 2023

Conversation

bbliem
Copy link
Contributor

@bbliem bbliem commented Mar 21, 2023

Overview

Grapple already supports callables in StreamFields (#220). This allows us to dynamically compute what to return when querying a field of a StreamField block.

Unfortunately, Grapple does now allow us to pass arguments to those callables. Such a functionality would be useful, however, when we want make the computation of the query result depend on some parameters provided by the user.

This PR implements support for callables in StreamFields so that we can make queries like the following, where CustomBlock is a StreamField block for which we want to add a GraphQL field customizedField that takes a string argument extraArg.

query($id: Int) {
    page(id: $id) {
        ... on BlogPage {
            body {
                ... on CustomBlock {
                    customizedField(extraArg: "hello")
                }
            }
        }
    }
}

Implementation

When using graphql_fields for declaring the custom fields for a StreamField block, the field types are generated by the existing function get_field_type(item), where item can be a GraphQLField or a tuple (field, wrapper).

  • If item is a GraphQLField, then the field is generated as graphene.Field(field_type), where field_type is the appropriate type. Note that this does not declare any arguments.
  • If item is a tuple (field, wrapper), then the field is generated as wrapper(field_type). With this mechanism, we can specify a wrapper that generates something like graphene.Field(field_type, extra_arg=graphene.String()).

Unfortunately the following does not work:

@register_streamfield_block
class CustomBlock(blocks.StructBlock):
    graphql_fields = [
        (GraphQLString("customized_field", source="get_customized_field"),
         lambda field_type: graphene.Field(field_type, extra_arg=graphene.String())),
    ]

    def get_customized_field(self, values=None, extra_arg=None):
        return extra_arg  # or do something involving values

There are two reasons this does not work:

Firstly, Grapple does not take the field wrappers into account when generating resolvers. The resolvers are generated in grapple.actions.build_streamfield_type like this (abridged):

if hasattr(cls, "graphql_fields"):
    for item in cls.graphql_fields:
        field, field_type = get_field_type(item)
        methods["resolve_" + field.field_name] = (
            custom_cls_resolver(cls=cls, graphql_field=item) or streamfield_resolver
        )
        type_meta[field.field_name] = field_type

Note that graphql_field=item is passed to custom_cls_resolver. This works if item is a GraphQLField but not if it is a tuple (field, wrapper). (Grapple will fall back to the streamfield_resolver, which we do not want.) I suspect this is an oversight and fixed it by replacing graphql_field=item with graphql_field=field.

Secondly, when we define a field with custom arguments and implement the resolver with a callable, custom_cls_resolver does not supply the user-provided values for these arguments to the callable. This is easily fixed by passing kwargs along.

I added tests for this. Hopefully it's fine. Happy to improve further.

Copy link
Collaborator

@dopry dopry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bbliem Thanks so much for this contribution. From a pure code review perspective it looks pretty good, I'd just like to see some of the research you've done get memorialized into the code as comments and ensure we have tests covering all the changes. I can hopefully find time to test it in the coming weeks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants