GraphQL - Nested resolvers

Enable nested resolvers for our graphql backend
This post is part of a Series of posts, and may not make a lot of sense without reading the previous installments.

Introduction

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.

Nested query/resolver

I’ll start with showing we can already query for a pokemon’s evolutionBranch, and then show a new field evolvesTo.

Current evolutionBranch

Certain pokemon can evolve, and we already have access to this information using the pokemon query:

Let’s see the data for bulbasaur

bulbasaur-evobranch

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).

New evolvesTo

Starter preparation

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.

A nested Pokemon field

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.

Imagination

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
      }
    }
  }
}

evolvesTo-qry

derp-squirtle

pogchain

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.

charm-family

{
  "id": "CHARMANDER",
  ... the rest of the object
  "evolvesTo": [
    {
      "id": "CHARMELEON",
      ... the rest of the object
      "evolvesTo": [
        "id": "CHARIZARD",
        ... the rest of the object
      ]
    }
  ]
},
... etcetera...

Enter Nested queries

Instead of the “Easy & Basic” approach described above, we will instead go with the “Proper & Scalable” approach: Nesting queries.

cle

Nested Resolvers

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.

Pokemon.evolvesTo

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

The resolver code

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;

Summarizing

So we added a new field to our type Pokemon, and this field is of type Pokemon.

graphql uses SUPER_POWER!

superpower

So type Pokemon is referencing itself (albeit an array of itself), and is enabling a type of recursion for accessing our data.

Conclusion

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.



Back to Post List
ok!