When going through the docs, it's not clear to me how to represent nested structures in order to pass query arguments through to sub-structures. Admittedly, I'm no expert on GraphQL, so if there's some construct that helps fulfill this scenario, my hope is that the docs could be bolstered to make that more clear. Take this trite example:
query {
posts (tag: "science") {
id,
title,
user (username: "john_doe") {
username,
first_name
}
}
}
My expectation is that I'd get back posts with the tag "science" and whose username is "john_doe". Filtering posts by the tag "science" happens naturally, but how do we support using the arguments to user in order filter the posts? Examples I've seen using e.g. JS support this, so I assume it's part of the GraphQL specs, but I haven't seen any of the Graphene demos support it.
The only way I've been able to achieve that sub-filter is by inspecting the field ast's in the posts resolver and just doing a single DB query upfront to filter by "science" and "john_does" at the same time (below). This seems like the wrong approach, but again, I'm not sure if that's a shortcoming of the docs or my misunderstanding.
class QueryType(ObjectType):
...
def resolve_posts(self, args, context, info):
sub_fields = defaultdict(dict)
for field in info.field_asts:
for selection in field.selection_set.selections:
for argument in selection.arguments:
sub_fields[selection.name.value][argument.name.value] = argument.value.value
q = { args, sub_fields } # Essentially, { "tag": "science", {"username": "john_doe" }}
return mongo.db.analysis.find(q)
Sure, ok, that works for simple cases, but I'm not sure if that'll hold up. Can we avoid writing a resolver like above and work with something like below in order to have this executed as multiple queries (essentially first filter the Posts by tag, then pass those to User to filter by correct username)?
class UserSchema(ObjectType):
username = String()
first_name = String()
def resolve_username(self, args, context, info):
# At this point self is already {'username': 'timmy tom' }, there's no
# parent Posts structure, just the `username` of the current Post.
# I can choose to return a `null`username if it's not == "john_doe",
# but that doesn't solve how I'd remove the parent Post
# from the returned results.
return self.get('username')
def resolve_first_name(self, args, context, info):
return self.get('first_name')
class PostSchema(ObjectType):
id = ID()
title = String()
user = UserType
def resolve_id(self, args, context, info):
return self.get('id')
def resolve_title(self, args, context, info):
return self.get('title')
class QueryType(ObjectType):
posts = List(
PostSchema,
id=ID(),
title=String(),
metadata = Field(UserType, username=Argument(String)) # ??
)
def resolve_posts(self, args, context, info):
return mongo.db.posts.find(args)
Hi @nsh87,
As a recommendation, Is better to be explicit on the query, and adding arguments to a field when their attributes could change the resolution of that field.
In this case, would be better (and more scalable) to add a username argument to the posts field,
Note that Input and Output data have different types by design (as reflected in the GraphQL spec), so you can't use a ObjectType as input.
So you can query like:
query {
posts (tag: "science", user: {username: "john_doe"}) {
id,
title,
user {
# Note that I moved the username filter the posts field, as affects its resolution
username,
first_name
}
}
}
And the implementation:
from graphene import InputObjectType, ObjectType, String, ID, Field, List
class UserSchema(ObjectType):
username = String()
first_name = String()
def resolve_username(self, args, context, info):
return self.get('username')
def resolve_first_name(self, args, context, info):
return self.get('first_name')
class PostSchema(ObjectType):
id = ID()
title = String()
user = Field(UserSchema)
def resolve_id(self, args, context, info):
return self.get('id')
def resolve_title(self, args, context, info):
return self.get('title')
def resolve_user(self, args, context, info):
return self.get('user')
class UserInputSchema(InputObjectType):
username = String()
first_name = String()
class QueryType(ObjectType):
posts = List(
PostSchema,
id=ID(),
title=String(),
user=UserInputSchema()
)
def resolve_posts(self, args, context, info):
return mongo.db.posts.find(args)
(check also a similar example working in the playground)
Hope this helps!
Note that Input and Output data have different types by design (as reflected in the GraphQL spec), so you can't use a
ObjectTypeas input.
That is especially explanatory as to why my proposed method wouldn't work.
I suspected this was possible in some way. This does solve the nested queries quite well. I've looked through several Issues regarding nesting/subquerying; some playground examples in the docs (for this and other common scenarios) would be valuable, in my humble opinion. I saw the InputObjectType in the AbstractType docs page, but for me that more demonstrated type inheritance (which is, in fact, quite convenient in that example) than how to take additional input. It becomes more clear with more knowledge of GraphQL's specs and what that class aims to do. Awesome work, by the way, @syrusakbary. Thanks for the quick response and the library.
This method no longer seems to work on Graphene 2.0. What would be a good way to accomplish filtering under this new upgrade?
Specifically, 'args' is now deprecated, and when I tried passing in, say, 'title' in the resolve_
resolve_<field>() missing 1 required positional argument: 'title'
Most helpful comment
Hi @nsh87,
As a recommendation, Is better to be explicit on the query, and adding arguments to a field when their attributes could change the resolution of that field.
In this case, would be better (and more scalable) to add a username argument to the posts field,
Note that Input and Output data have different types by design (as reflected in the GraphQL spec), so you can't use a
ObjectTypeas input.Example
So you can query like:
And the implementation:
(check also a similar example working in the playground)
Hope this helps!