question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Get requested fields in resolve function

See original GitHub issue

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.

Issue Analytics

  • State:closed
  • Created 8 years ago
  • Reactions:3
  • Comments:18 (11 by maintainers)

github_iconTop GitHub Comments

12reactions
kolyptocommented, Aug 25, 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

9reactions
mixxorzcommented, Mar 28, 2016

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

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

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to get requested fields inside GraphQL resolver?
First, I need to get the requested fields. I can already get the whole query as a string. For example, in the resolver,...
Read more >
GraphQL performance tip: Select fields from requests all the ...
I came across graphql-fields which can return the requested fields based on the info param, it also works with advanced cases like union...
Read more >
Resolvers - Apollo GraphQL Docs
A resolver is a function that's responsible for populating the data for a single field in your schema. It can populate that data...
Read more >
Determining which fields were requested by a query - gqlgen
CollectAllFields is the simplest way to get the set of queried fields. It will return a slice of strings of the field names...
Read more >
Resolvers - graphql-compose
In terms of graphql-compose this field config is called as Resolver . ... wrap args , type , resolve (get resolver and create...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found