Skip to content

Get requested fields in resolve function #57

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

Closed
Globegitter opened this issue Nov 30, 2015 · 19 comments
Closed

Get requested fields in resolve function #57

Globegitter opened this issue Nov 30, 2015 · 19 comments
Labels

Comments

@Globegitter
Copy link

I have seen that the info parameter in the resolve function provides a info.field_asts, which provides information about the selected fields, but when fragments are provided you get something like:

[Field(alias=None, name=Name(value=u'customer'), arguments=[], directives=[], selection_set=SelectionSet(selections=[Field(alias=None, name=Name(value=u'id'), arguments=[], directives=[], selection_set=None), FragmentSpread(name=Name(value=u'__RelayQueryFragment0wau8gf'), directives=[])]))]

which means for fragments we can't really figure out which fields are selected at runtime.

Our use-case for knowing the fields in the resolve functions is,that we only want to calculate the fields that are actually requested because some of the fields are expensive to calculate.

Edit: Or are the resolve methods for specific fields meant to be used for that? E.g. resolve_full_name on the Customer node?

Also happy to provide an example of that would make it easier.

@jhgg
Copy link
Member

jhgg commented Nov 30, 2015

from graphql.core.execution.base import collect_fields
fields = collect_fields(info.context, info.parent_type, info.field_asts[0], {}, set())

@Globegitter
Copy link
Author

ahhh great, thanks @jhgg, will give that a try!

@Globegitter
Copy link
Author

@jhgg if I change your code example to:

fields = collections.defaultdict(list) #can not just be dict because of 'id' from relay
fields = collect_fields(info.context, info.parent_type, info.field_asts[0].selection_set, fields, set())

I am getting to print all the field names in the 'collect_fields' function, but it just returns before it has traversed the whole ast. So the actual retyurn of fields is just defaultdict(<type 'list'>, {u'id': [Field(alias=None, name=Name(value=u'id'), arguments=[], directives=[], selection_set=None)]})

@jhgg
Copy link
Member

jhgg commented Nov 30, 2015

Weird. Can you post you code somewhere. I'll mess with it when I've got some time!

Sent from my iPhone

On Nov 30, 2015, at 9:50 AM, Markus Padourek [email protected] wrote:

@jhgg if I change your code example to:

fields = collections.defaultdict(list) #can not just be dict because of 'id' from relay
fields = collect_fields(info.context, info.parent_type, info.field_asts[0].selection_set, fields, set())
I am getting to print all the field names in the 'collect_fields' function, but it just returns before it has traversed the whole ast. So the actual retyurn of fields is just defaultdict(<type 'list'>, {u'id': [Field(alias=None, name=Name(value=u'id'), arguments=[], directives=[], selection_set=None)]})


Reply to this email directly or view it on GitHub.

@Globegitter
Copy link
Author

Yep will do as soon as possible.

@Globegitter
Copy link
Author

Here we go @jhgg http://graphene-python.org/playground/?schema=import%2520collections%250A%250Aimport%2520graphene%250Afrom%2520graphene%2520import%2520relay%250Afrom%2520graphql.core.execution.base%2520import%2520collect_fields%250A%250Aclass%2520Customer(relay.Node)%253A%250A%2520%2520%2520%2520first_name%2520%253D%2520graphene.String()%250A%2520%2520%2520%2520middle_name%2520%253D%2520graphene.String()%250A%2520%2520%2520%2520last_name%2520%253D%2520graphene.String()%250A%2520%2520%2520%2520expensive_field%2520%253D%2520graphene.String()%250A%2520%2520%2520%2520%250A%2520%2520%2520%2520%2540classmethod%250A%2520%2520%2520%2520def%2520get_node(cls%252C%2520customer_id%252C%2520info)%253A%250A%2520%2520%2520%2520%2520%2520%2520%2520return%2520Customer()%250A%2520%2520%2520%2520%2520%2520%2520%2520%250Aclass%2520Query(graphene.ObjectType)%253A%250A%2520%2520%2520%2520hello%2520%253D%2520graphene.String()%250A%2520%2520%2520%2520ping%2520%253D%2520graphene.String(to%253Dgraphene.String())%250A%2520%2520%2520%2520customer%2520%253D%2520graphene.Field(Customer)%250A%250A%2520%2520%2520%2520def%2520resolve_hello(self%252C%2520args%252C%2520info)%253A%250A%2520%2520%2520%2520%2520%2520%2520%2520return%2520%27World%27%250A%250A%2520%2520%2520%2520def%2520resolve_ping(self%252C%2520args%252C%2520info)%253A%250A%2520%2520%2520%2520%2520%2520%2520%2520return%2520%27Pinging%2520%257B%257D%27.format(args.get(%27to%27))%250A%2520%2520%2520%2520%250A%2520%2520%2520%2520def%2520resolve_customer(self%252C%2520_%252C%2520info)%253A%250A%2520%2520%2520%2520%2520%2520%2520%2520fields%2520%253D%2520collections.defaultdict(list)%250A%2520%2520%2520%2520%2520%2520%2520%2520fields%2520%253D%2520collect_fields(info.context%252C%2520info.parent_type%252C%2520info.field_asts%255B0%255D.selection_set%252C%2520fields%252C%2520set())%250A%2520%2520%2520%2520%2520%2520%2520%2520print(fields)%250A%2520%2520%2520%2520%2520%2520%2520%2520%2523%2520only%2520calculate%2520expensive_field%2520here%2520if%2520it%2520is%2520in%2520fields%250A%2520%2520%2520%2520%2520%2520%2520%2520return%2520Customer(id%253D%271%27%252C%2520first_name%253D%27Test%27%252C%2520last_name%253D%27customer%27)%250A%250Aschema%2520%253D%2520graphene.Schema(query%253DQuery)%250A&query=query%2520GetCustomer%257B%250A%2520%2520customer%257B%250A%2520%2520%2520%2520id%252C...__RelayQueryFragment0wau8gf%250A%2520%2520%257D%250A%257D%2520%250Afragment%2520__RelayQueryFragment0wau8gf%2520on%2520Customer%257B%250A%2520%2520firstName%250A%2520%2520lastName%250A%257D

That should be pretty similar to what I am currently working with

@syrusakbary
Copy link
Member

@jhgg for seeing the print output just open the browser console.
@Globegitter I'm happy to see you started using the playground, let me know if any suggestions there!

@Globegitter
Copy link
Author

@syrusakbary yeah thanks for the docs, they are great! And the playground also works well, especially knowing the prints are showing in the console.

@syrusakbary
Copy link
Member

I think this thing will be improved in the next version of Relay, so the might send the flattened query
facebook/relay@ca0afe1

However, no idea why graphql-core (and probably graphql-js) doesn't get the fields from fragments... any suggestion @jhgg?

@mixxorz
Copy link

mixxorz commented Mar 28, 2016

This code snippet successfully extracts all fields from info:

def get_fields(info):
    prev_fragment_names = set()
    params = collections.defaultdict(list)
    params = collect_fields(info.context,
                            info.parent_type,
                            info.field_asts[0].selection_set,
                            params,
                            prev_fragment_names)

    for fragment_name in prev_fragment_names:
        params = collect_fields(info.context,
                                info.parent_type,
                                info.fragments[fragment_name].selection_set,
                                params,
                                prev_fragment_names)

    return set(params.keys())

This hasn't been tested for all edge cases though.

@mixxorz
Copy link

mixxorz commented Mar 28, 2016

After much work, here's a much nicer code snippet to get requested fields:

https://gist.github.com/mixxorz/dc36e180d1888629cf33

@Globegitter
Copy link
Author

Thanks for that work @mixxorz Also it seems that graphql itself might get that feature anyway. See: graphql/graphql-js#304

Looks pretty great :)

@ProjectCheshire
Copy link
Member

This appears resolved, per gist above / also from 2016 :p

@fhennig
Copy link

fhennig commented Oct 24, 2020

I would like to reopen this, as all proposed solutions do not work in graphene 3 anymore. Namely, the context does not provide field_asts property anymore.

@kolypto
Copy link

kolypto commented Feb 5, 2021

Okay, I have a solution here :) A function that gives you a list of selected fields.

Goal

Our goal is to have a query like this:

query {
    id
    object { id name }
    field(arg: "value")
}

and from within a resolver we want to produce the following list of selected fields:

['id', 'object', 'field']

notice that nested fields are not included: object { id name } is provided, but only object is mentioned.

Basic implementation

This function simply goes through the AST at the current level, picks up all the fields, and returns their names as a list.

import graphql
from collections import abc

def selected_field_names_naive(selection_set: graphql.SelectionSetNode) -> abc.Iterator[str]:
    """ Get the list of field names that are selected at the current level. Does not include nested names.

    Limitations:
    * Does not resolve fragments; throws RuntimeError
    * Does not take directives into account. A field might be disabled, and this function wouldn't know

    As a result:
    * It will give a RuntimeError if a fragment is provided
    * It may give false positives in case directives are used
    * It is 20x faster than the alternative

    Benefits:
    * Fast!

    Args:
        selection_set: the selected fields
    """
    assert isinstance(selection_set, graphql.SelectionSetNode)

    for node in selection_set.selections:
        # Field
        if isinstance(node, graphql.FieldNode):
            yield node.name.value
        # Fragment spread (`... fragmentName`)
        elif isinstance(node, (graphql.FragmentSpreadNode, graphql.InlineFragmentNode)):
            raise NotImplementedError('Fragments are not supported by this simplistic function')
        # Something new
        else:
            raise NotImplementedError(str(type(node)))

It can be used only in the most basic cases because:

  • It does not support directives that might exclude a field
  • It does not support fragment spread (... fragmentName)
  • It does not support inline fragments (... on Droid { })

Usage:

def resolve_field(_, info: graphql.GraphQLResolveInfo):
    selected_field_names_naive(info.field_nodes[0].selection_set)

A feature-complete implementation

This implementation has support for everything GraphQL itself supports because it relies on context.collect_fields(), but it's also the slowest one, and it requires you to provide the runtime type in order to resolve fragments.

import graphql
from collections import abc
from typing import Union

def selected_field_names(selection_set: graphql.SelectionSetNode,
                         info: graphql.GraphQLResolveInfo,
                         runtime_type: Union[str, graphql.GraphQLObjectType] = None) -> abc.Iterator[str]:
    """ Get the list of field names that are selected at the current level. Does not include nested names.

    This function re-evaluates the AST, but gives a complete list of included fields.
    It is 25x slower than `selected_field_names_naive()`, but still, it completes in 7ns or so. Not bad.

    Args:
        selection_set: the selected fields
        info: GraphQL resolve info
        runtime_type: The type of the object you resolve to. Either its string name, or its ObjectType.
            If none is provided, this function will fail with a RuntimeError() when resolving fragments
    """
    # Create a temporary execution context. This operation is quite cheap, actually.
    execution_context = graphql.ExecutionContext(
        schema=info.schema,
        fragments=info.fragments,
        root_value=info.root_value,
        operation=info.operation,
        variable_values=info.variable_values,
        # The only purpose of this context is to be able to run the collect_fields() method.
        # Therefore, many parameters are actually irrelevant
        context_value=None,
        field_resolver=None,
        type_resolver=None,
        errors=[],
        middleware_manager=None,
    )

    # Use it
    return selected_field_names_from_context(selection_set, execution_context, runtime_type)


def selected_field_names_from_context(
        selection_set: graphql.SelectionSetNode,
        context: graphql.ExecutionContext,
        runtime_type: Union[str, graphql.GraphQLObjectType] = None) -> abc.Iterator[str]:
    """ Get the list of field names that are selected at the current level.

    This function is useless because `graphql.ExecutionContext` is not available at all inside resolvers.
    Therefore, `selected_field_names()` wraps it and provides one.
    """
    assert isinstance(selection_set, graphql.SelectionSetNode)

    # Resolve `runtime_type`
    if isinstance(runtime_type, str):
        runtime_type = context.schema.type_map[runtime_type]  # raises: KeyError

    # Resolve all fields
    fields_map = context.collect_fields(
        # Use the provided Object type, or use a dummy object that fails all tests
        runtime_type=runtime_type or None,
        # runtime_type=runtime_type or graphql.GraphQLObjectType('<temp>', []),
        selection_set=selection_set,
        fields={},  # out
        visited_fragment_names=(visited_fragment_names := set()),  # out
    )

    # Test fragment resolution
    if visited_fragment_names and not runtime_type:
        raise RuntimeError('The query contains fragments which cannot be resolved '
                           'because `runtime_type` is not provided by the lazy developer')

    # Results!
    return (
        field.name.value
        for fields_list in fields_map.values()
        for field in fields_list
    )

Drawbacks:

  • Slower than the first one
  • It re-evaluates the AST. graphql has already evaluated it, but sadly, we don't have access to that information

Usage:

def resolve_field(_, info: graphql.GraphQLResolveInfo):
    selected_field_names_naive(info.field_nodes[0].selection_set, info, 'Droid')

The Combination of the Two

Since both functions are quite useful, here's a function that combines the best of both:

def selected_field_names_fast(selection_set: graphql.SelectionSetNode,
                              context: graphql.GraphQLResolveInfo,
                              runtime_type: Union[str, graphql.GraphQLObjectType] = None) -> abc.Iterator[str]:
    """ Use the fastest available function to provide the list of selected field names

    Note that this function may give false positives because in the absence of fragments it ignores directives.
    """
    # Any fragments?
    no_fragments = all(isinstance(node, graphql.FieldNode) for node in selection_set.selections)

    # Choose the function to execute
    if no_fragments:
        return selected_field_names_naive(selection_set)
    else:
        return selected_field_names(selection_set, context, runtime_type)

License: MIT, or Beerware

@kolypto
Copy link

kolypto commented Feb 5, 2021

A better solution would only be possible if either

  • the ExecutionContext becomes available inside ResolveInfo, or
  • the output of ExecutionContext.collect_fields() becomes available within ResolveInfo

@niwla23
Copy link

niwla23 commented Aug 25, 2021

@kolypto
can you add a license to your code?

@kolypto
Copy link

kolypto commented Aug 25, 2021

@niwla23 added :)

@george-activision
Copy link

for those coming here and realizing this is broken because fields_map = context.collect_fields( collect_fields is not an attribute of context anymore, here's the fixed code.

def selected_field_names_from_context(
        selection_set: graphql.SelectionSetNode,
        context: graphql.ExecutionContext,
        runtime_type: Union[str, graphql.GraphQLObjectType] = None) -> abc.Iterator[str]:
    """ Get the list of field names that are selected at the current level.

    This function is useless because `graphql.ExecutionContext` is not available at all inside resolvers.
    Therefore, `selected_field_names()` wraps it and provides one.
    """
    assert isinstance(selection_set, graphql.SelectionSetNode)

    # Resolve `runtime_type`
    if isinstance(runtime_type, str):
        runtime_type = context.schema.type_map[runtime_type]  # raises: KeyError

    # Resolve all fields
    fields_map = graphql.execution.collect_fields.collect_fields(
        context.schema,
        context.fragments,
        context.variable_values,
        runtime_type or None,
        context.operation.selection_set
    )

    # Results!
    return (
        field.name.value
        for fields_list in fields_map.values()
        for field in fields_list
    )


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

No branches or pull requests

9 participants