Comments (19)
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.
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.
After much work, here's a much nicer code snippet to get requested fields:
https://gist.github.com/mixxorz/dc36e180d1888629cf33
from graphene.
A better solution would only be possible if either
- the
ExecutionContext
becomes available insideResolveInfo
, or - the output of
ExecutionContext.collect_fields()
becomes available withinResolveInfo
from graphene.
@kolypto
can you add a license to your code?
from graphene.
from graphql.core.execution.base import collect_fields
fields = collect_fields(info.context, info.parent_type, info.field_asts[0], {}, set())
from graphene.
ahhh great, thanks @jhgg, will give that a try!
from graphene.
@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.
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.
Yep will do as soon as possible.
from graphene.
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.
@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.
@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.
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.
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.
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.
This appears resolved, per gist above / also from 2016 :p
from graphene.
@niwla23 added :)
from graphene.
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)
- Enum equality comparison is inconsistent
- http://www.graphene-python.org Has EXPIRED HOT 3
- Upload file HOT 2
- Unexpected argument in Pycharm 2023.2.1 HOT 1
- Default_value option in Arguments inside Mutation is not working as expected HOT 2
- Exception: NonNull could not have a mounted String() as inner type. Try with NonNull(String).
- Returning non fatal errors alongside data HOT 1
- Conversion to snake_case fails on nested inputs
- Depth limit validator key error if fragment undefined
- Tests in `graphene/relay/tests/test_custom_global_id.py` fail due incompatibility with pytest 8.x HOT 2
- Enum.from_enum() fails with Django IntegerChoices
- Do you support GQL over websocket? HOT 1
- More documentation for DisableIntrospection?
- Subclass built-in scalars without creating new GraphQL type
- In 3.1.0, `resolve_type` method on Interfaces and Unions no longer supports returning the GraphQL Type as a string HOT 1
- release 3.4.0 HOT 2
- grphene.List not work correctly HOT 1
- When passing the empty string to Date Type, it occurs `IndexError: string index out of range` HOT 2
- Schema execution timing out after upgrade from 2.1.9 to 3.3
- pregenerate schema and cache it for serverless cold start? HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from graphene.