Giter VIP home page Giter VIP logo

Comments (19)

kolypto avatar kolypto commented on August 19, 2024 13

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

from graphene.

fhennig avatar fhennig commented on August 19, 2024 11

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.

from graphene.

mixxorz avatar mixxorz commented on August 19, 2024 10

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

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

from graphene.

kolypto avatar kolypto commented on August 19, 2024 4

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

from graphene.

niwla23 avatar niwla23 commented on August 19, 2024 1

@kolypto
can you add a license to your code?

from graphene.

jhgg avatar jhgg commented on August 19, 2024
from graphql.core.execution.base import collect_fields
fields = collect_fields(info.context, info.parent_type, info.field_asts[0], {}, set())

from graphene.

Globegitter avatar Globegitter commented on August 19, 2024

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

from graphene.

Globegitter avatar Globegitter commented on August 19, 2024

@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)]})

from graphene.

jhgg avatar jhgg commented on August 19, 2024

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.

from graphene.

Globegitter avatar Globegitter commented on August 19, 2024

Yep will do as soon as possible.

from graphene.

Globegitter avatar Globegitter commented on August 19, 2024

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

from graphene.

syrusakbary avatar syrusakbary commented on August 19, 2024

@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!

from graphene.

Globegitter avatar Globegitter commented on August 19, 2024

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

from graphene.

syrusakbary avatar syrusakbary commented on August 19, 2024

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?

from graphene.

mixxorz avatar mixxorz commented on August 19, 2024

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.

from graphene.

Globegitter avatar Globegitter commented on August 19, 2024

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

Looks pretty great :)

from graphene.

ProjectCheshire avatar ProjectCheshire commented on August 19, 2024

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

from graphene.

kolypto avatar kolypto commented on August 19, 2024

@niwla23 added :)

from graphene.

george-activision avatar george-activision commented on August 19, 2024

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
    )


from graphene.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.