-
Notifications
You must be signed in to change notification settings - Fork 822
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
Comments
from graphql.core.execution.base import collect_fields
fields = collect_fields(info.context, info.parent_type, info.field_asts[0], {}, set()) |
ahhh great, thanks @jhgg, will give that a try! |
@jhgg if I change your code example to:
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 |
Weird. Can you post you code somewhere. I'll mess with it when I've got some time! Sent from my iPhone
|
Yep will do as soon as possible. |
@jhgg for seeing the print output just open the browser console. |
@syrusakbary yeah thanks for the docs, they are great! And the playground also works well, especially knowing the prints are showing in the console. |
I think this thing will be improved in the next version of Relay, so the might send the flattened query However, no idea why |
This code snippet successfully extracts all fields from 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. |
After much work, here's a much nicer code snippet to get requested fields: |
Thanks for that work @mixxorz Also it seems that graphql itself might get that feature anyway. See: graphql/graphql-js#304 Looks pretty great :) |
This appears resolved, per gist above / also from 2016 :p |
I would like to reopen this, as all proposed solutions do not work in graphene 3 anymore. Namely, the context does not provide |
Okay, I have a solution here :) A function that gives you a list of selected fields. GoalOur goal is to have a query like this:
and from within a resolver we want to produce the following list of selected fields:
notice that nested fields are not included: Basic implementationThis 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:
Usage: def resolve_field(_, info: graphql.GraphQLResolveInfo):
selected_field_names_naive(info.field_nodes[0].selection_set) A feature-complete implementationThis implementation has support for everything GraphQL itself supports because it relies on 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:
Usage: def resolve_field(_, info: graphql.GraphQLResolveInfo):
selected_field_names_naive(info.field_nodes[0].selection_set, info, 'Droid') The Combination of the TwoSince 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 |
A better solution would only be possible if either
|
@kolypto |
@niwla23 added :) |
for those coming here and realizing this is broken because
|
I have seen that the
info
parameter in theresolve
function provides ainfo.field_asts
, which provides information about the selected fields, but when fragments are provided you get something like: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 theCustomer
node?Also happy to provide an example of that would make it easier.
The text was updated successfully, but these errors were encountered: