Code for this post is in branch nested-queries.
In this post, I am going to feature a graphql SUPER_POWER.
I enhanced the readResolvers
function in ./utils.js
to enable implementations for nested resolvers.
In this case, what we want to achieve, is the ability to “drill-down” on the pokemon.json
data and kind of apply the same query
in a recursive way.
I’ll start with showing we can already query for a pokemon’s evolutionBranch
, and then show a new field evolvesTo
.
Certain pokemon can evolve, and we already have access to this information using the pokemon
query:
Let’s see the data for bulbasaur
Through the existing evolutionBranch
field, we can ask our query to give us the evolutions for the pokemon; There could be multiple evolutions (like in Eevee’s case), or none (like an already fully-evolved Charizard).
For this post’s changes branch nested-queries, I did some “low-level changes” to ./utils.js
to enable nested resolvers for the starter repo.
I also did “fixes” on the pokemon.json
data that you can see on the commit details.
I added a new field to our type Pokemon
:
type Pokemon {
id: String!
name: String!
names: String!
type1: String!
type2: String
family: String!
kmBuddyDistance: Int!
stats: Stats!
moves: MoveSet!
height: Float!
weight: Float!
evolutionBranch: [Evolution]!
isTransferable: Boolean!
isDeployable: Boolean!
isLegend: Boolean!
isMythic: Boolean!
parentId: String
+ evolvesTo: [Pokemon]!
}
The new field is an array of type Pokemon
. So it is the same type as the containing type.
Up to this point, all fields we have used in our types
have been either properties or objects with more properties in them, and they have all been very symmetrical to the underlying data.
For instance, this query
{
pokemon(id:"bulbasaur") {
name
type1
type2
evolutionBranch {
evolution
form
}
}
}
Follows a symmetry to the object in the data:
{
"name": "BULBASAUR",
"type1": "GRASS",
"type2": "POISON",
"evolutionBranch": [
{
"evolution": "IVYSAUR",
"form": "IVYSAUR",
}
],
And this falls into graphql
’s basic implementation.
And we are going to do something more “advanced”, or “less basic”.
We are adding a new field Pokemon.evolvesTo
, and the data does not contain the relevant information as we want to expose it in our query.
Imagine achieving what is proposed here… think about it for a few seconds before scrolling down too much…
pokemon(id:"charmander") {
name
evolvesTo {
name {
evolvesTo {
name
}
}
}
}
One possible way would be to hammer our data to follow this layout… Grab/copy the pokemon object and paste it inside a new evolvesTo
property, and keep doing this until we end up with our pokemon.json
data-file with all the nesting necessary to achieve what the query is asking for.
{
"id": "CHARMANDER",
... the rest of the object
"evolvesTo": [
{
"id": "CHARMELEON",
... the rest of the object
"evolvesTo": [
"id": "CHARIZARD",
... the rest of the object
]
}
]
},
... etcetera...
Instead of the “Easy & Basic” approach described above, we will instead go with the “Proper & Scalable” approach: Nesting queries.
So we will need to create a resolver
for Pokemon.evolvesTo
.
Up to now, we know how to create resolvers for queries, and put them inside /g/resolvers/Query/
.
We also know that any mutation resolvers we would put in /g/resolvers/Mutation/
.
I have also created a new folder /g/resolvers/zNested/
to hold these nested query resolvers.
I updated the code so that any .js
files within this new folder are attached to our resolversMap
so graphql can find them and process our more complex queries.
We will create the following file:
/g/resolvers/zNested/
Pokemon/evolvesTo.js
So inside the new folder zNested
, we will create folder Pokemon
. And then inside, file evolvesTo.js
.
Following this convention, our graphql processor will be able to find a resolver for Pokemon.evolvesTo
.
These nested resolvers use the same function signature:
function resolver (obj, args, ctx, info): type as object | Promise
The resolver
should return an object or a Promise
that will resolve an object.
But for nested resolvers
, we will typically use the obj
argument, and in fewer cases, also the args
argument.
obj
will be the returned object from the parent query. So it will be a type Pokemon
let pokemon = require('./../../../../data/pokemon.json');
var resolver = (obj, args, ctx, info) => {
// obj, the parent Pokemon
let ary = [];
// loop each evolutionBranch...
obj.evolutionBranch.forEach((evo) => {
let evoId = evo.form || evo.evolution;
// find the pokemon object based on the evolution data...
let p = findPokemon(evoId);
// add it to the resulting list/array
if (p) ary.push(p);
});
return ary;
};
function findPokemon (id) {
let p = false;
for (let k = 0; k <= pokemon.length; k++) {
if (pokemon[k].id === id.toUpperCase()) {
p = pokemon[k];
break;
}
}
return p;
}
module.exports = resolver;
So we added a new field to our type Pokemon
, and this field is of type Pokemon
.
graphql uses SUPER_POWER!
So type Pokemon
is referencing itself (albeit an array of itself), and is enabling a type of recursion for accessing our data.
The changes have already been deployed to the running endpoint at
https://rj07ty7re4.execute-api.us-east-1.amazonaws.com/dev/gql
and have been pushed to branch nested-queries.