It’s funny the sort of problems one encounters while working on real-world projects. Problems that seem simple in theory but don’t seem to have been mentioned (even in passing) in the documentation of whatever package you’re using. More than that, you can’t seem to find any question asked by a stranger online along the same lines.
Before we get too far, I will assume that you have a working knowledge of GraphQL as well as Graphene-Django. As such, I won’t go into the details of setting up the codebase or explaining such terms as query, arguments etc.
Some context: I was building a GraphQL service - built with Django using Graphene-Django - and I was looking to define arguments that could be used across different GraphQL queries. (Remember, keep your code DRY.) Sounds simple enough, right?
A quick scan of the documentation didn’t reveal any solution to my problem. So I did what any developer these days does: I searched the web. The top results from StackOverflow didn’t reveal anything that matched my specific need so I decided to try and figure things out on my own; this actually proved to be simpler than I thought it would be.
The Problem
In practical terms, let’s say that I wish to have the search_keyword
argument across a number of queries. Applied to one query, this would look something like:
1
2
3
4
5
6
7
8
9
10
11
class MyQuery(graphene.ObjectType):
"""
`MyQuery` docstring.
"""
records = graphene.Field(
graphene.String,
search_keyword=graphene.String(
required=False, description="Search phrase."
),
)
But say I wish to have this argument included in 20 or 30 queries. You can see how tiring it would be to have to keep defining the argument. And what if I wish to change the name of the argument somewhere down the road?
“There has to be a better way,” I thought. As it turns out, there is.
The Solution
Our solution is pretty simple:
- Create a custom class that inherits from
graphene.Field
and declare our common fields. - Use this derived class when defining our queries.
The entire codebase is available on GitHub. The following sections only touch on the relevant blocks of code.
Step 1: Create a custom class
As mentioned above, our custom class inherits from the graphene.Field
class.
In the supplied code, we see this in action in the file reusable_args/utils/object_fields.py
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# reusable_args/utils/object_fields.py
import graphene
class FieldWithArgs(graphene.Field):
"""
Class with common (reusable) arguments.
"""
def __init__(
self,
type_,
args=None,
resolver=None,
source=None,
deprecation_reason=None,
name=None,
description=None,
required=False,
_creation_counter=None,
default_value=None,
**extra_args
):
"""
Field that adds the `search_keyword` argument to a query.
"""
extra_args["search_keyword"] = graphene.String(
required=False,
default_value=None,
description="Search phrase."
)
# Truncated for brevity
super().__init__(...)
As seen above, we override the __init__
method and pass our common arguments to extra_args
before calling super().__init__()
. This achieves two things:
It utilises the mechanism Graphene uses internally to pass arguments that are to be mounted to the query.
What this means is that if you were to declare thesearch_keyword
argument at the query level, it would be passed in theextra_keywords
dictionary.1It allows us to declare other arguments at the query level without resorting to unsightly monkey-patching, just like we would if we were to use the regular
graphene.Field
class.2
Step 2: Use the derived class
We can use our derived class like we would the graphene.Field
class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# reusable_args/schema.py
...
class QueryWithReusableArgs(graphene.ObjectType):
records_reusable = FieldWithArgs(
graphene.List(FruitType),
)
def resolve_records_reusable(self, info, search_keyword):
records = FRUITS
if search_keyword:
records = [
fruit
for fruit in FRUITS
if search_keyword.lower() in fruit.get("name").lower()
or search_keyword.lower() == fruit.get("name").lower()
]
return records
...
It is important to note that we will still need to include the search_keyword
argument in the resolver method, much like we would need to if we were to use the graphene.Field
class.
Conclusion
And there we have it. While I can be a bit verbose at times, I hope this post has not only highlighted a useful trick but also clearly explained how this trick works.
Until the next time.
Footnotes
To see this in action for yourself, edit the
__init__
method inFieldWithArgs
and addprint
extra_args
:1 2 3 4 5 6 7 8 9 10
records = FieldWithArgs( graphene.String, another_field=graphene.String( required=False ), ) class FieldWithArgs(graphene.Field): def __init__(): print(extra_args)
In the included snippet, the two query definitions would work the same way:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
records = FieldWithArgs( graphene.String, another_field=graphene.String( required=False ), ) records = graphene.Field( graphene.String, search_keyword= graphene.String( required=False, default_value=None, description="Search phrase." ), another_field=graphene.String( required=False ), )