GraphQL ResolveInfo Deep Dive

Building Efficient GraphQL Resolvers By Generating Database Queries

--

Inefficient data fetching and writing lots of boilerplate are two common problems to overcome when building GraphQL APIs. In this post, I want to show how both of these issues can be addressed by generating database queries inside GraphQL resolvers using the GraphQLResolveInfo object.

The GraphQLResolveInfo object is an extremely useful part of the GraphQL implementation that contains information about our GraphQL schema and the current operation and is passed to every resolver at query time. In this article, we’ll use the Neo4j GraphQL neo4j-graphql.js integration as an example, but the concepts can be used to generate your own database queries as well as gain an understanding of how GraphQL database integrations are built.

I gave a talk at GraphQL Summit SF 2019 about this topic. And yes, that is my Halloween costume 🤠

Neo4j GraphQL Integration

neo4j-graphql.js is a GraphQL integration for the Neo4j graph database. It has two main features:

The two main features of neo4j-graphql.js. In this article we focus on the second feature, “GraphQL Transpilation”.
  1. Schema Augmentation/Generation: Given a set of GraphQL type definitions (or an existing GraphQL schema) the library will generate Query and Mutation fields, add filtering and pagination fields to provide a full CRUD GraphQL API from these type definitions. Custom logic can be defined using @cypher schema directives that map Cypher queries to GraphQL fields.
  2. GraphQL Transpilation: Given an arbitrary GraphQL request, the library generates a single database query to resolve the request in one round trip to the database. Generating a single database query is important for performance considerations and addresses the n+1 query problem common in naive GraphQL API implementations.
neo4j-graphql.js helps with building GraphQL APIs backed by Neo4j.

It’s important to point out that the Neo4j GraphQL integration is about building the API layer that sits between the database and the client. GraphQL is not a database query language — it’s an API query language and neo4j-graphql.js is focused on helping to build that API layer.

A Motivating Example: The GraphQL Summit Graph

The GraphQL Summit Graph: A GraphQL API of the GraphQL Summit SF schedule, backed by Neo4j.

To see how this works, let’s consider an example. I thought it would be fun to build a GraphQL API of the GraphQL Summit schedule. So I wrote a Python script to scrape the schedule website and loaded the data into Neo4j.

The property graph model for the GraphQL Summit Graph

Since Neo4j is a graph database, we model the schedule data as a series of nodes, connected by relationships. To the left you can see the data model used, we have Session nodes connected to the speakers giving the sessions, the theme of the sessions, which room the session is in, as well as the company the speaker works for.

We store the attributes of each node as properties, such as the title of the session, the name of the speaker, etc.

Now we’re ready to query the data in Neo4j. We use the Cypher query language to query data with Neo4j. If you’re not familiar with Cypher, you can think of it as like SQL but for graphs. Here’s an example Cypher query to search for sessions containing the word “resolver” as well as the themes, speaker and the company connected to the speaker:

Neo4j Browser is a query workbench for Neo4j. You can access this database here, logging in with the username and password “summit”.

But our goal is to expose this data via a GraphQL API — we don’t want to just expose our database, we want a GraphQL API that a client application could use to search for sessions by topic, and include some custom logic, like generating recommended sessions for a user. To do this we’ll use neo4j-graphql.js to create a GraphQL API and autogenerate resolvers capable of fetching data from Neo4j based on our schema.

To build our GraphQL API with neo4j-graphql.js the first step is to define GraphQL type definitions that describe our data.

GraphQL type definitions for our GraphQL API.

Next, we import the makeAugmentedSchema function from neo4j-graphql.js and pass in the GraphQL type definitions we just created. This function will return a GraphQL executable schema object, which we can pass to Apollo Server to serve our GraphQL API. We just need to create a Neo4j JavaScript driver instance with a connection to our database and inject that into the context object so that it will be available inside the generated resolvers for fetching data from Neo4j, using the autogenerated Cypher queries.

Creating a Neo4j driver instance and serving our GraphQL API with Apollo Server.

Note that we haven’t defined any resolvers or for that matter, the Query or Mutation types — all of this is added for us by makeAugmentedSchema. Now we can query our API, here we’re taking advantage of the autogenerated filtering to search for sessions that contain the word “resolver” and associated data, including recommended sessions.

Querying our GraphQL API in GraphQL Playground. You can try it here.

This data is requested from Neo4j — our database query is generated at query time and requests from Neo4j only what is specified in the GraphQL request, including our computed field for recommended sessions.

So how does this query auto-generation work? Basically, the autogenerated resolver is inspecting the GraphQLResolveInfo object, checking for what fields are requested in the selection set, and generating a single database query.

But before we get into too much detail, let’s take a step back and talk about the GraphQL Resolver.

What’s A Resolver?

In GraphQL, resolvers are functions that contain the logic for resolving GraphQL requests — essentially turning a GraphQL operation into data. In the case of GraphQL queries, this typically means querying some data layer, like a database. Consider the follow GraphQL operation, a query to search for sessions matching some search string, then find the room, themes, the presenter of the session, who they work for, and finally recommended sessions.

To resolve this request, the resolver functions are called in a nested manner, starting with the Session resolver on the Query type. Each resolver function has the same signature and is passed four arguments, one of which is the GraphQL resolve info object.

The signature of a GraphQL resolver function.

Each resolver might fetch data from a database, check some authorization rule, and validate and format the response. Using an imaginary ORM type example, here’s what the resolvers necessary for finding our session information might look like:

Nested resolvers using an imaginary ORM-type example.

Two main problems with this approach are the performance implications of making multiple roundtrips to the database (each ORM call here would result in at least one request to the database), as well as the developer having to write all this boilerplate data fetching code.

Instead of manually implementing resolvers we can use the GraphQLResolveInfo object to generate a single database query to resolve our request from the root resolver.

Now that we’re clear about GraphQL resolvers, let’s talk about the GraphQLResolveInfo object.

What’s A GraphQLResolveInfo?

And more importantly, how can it help me build efficient GraphQL APIs?

The GraphQLResolveInfo type.

Remember that a GraphQL resolve info object exists in the context of a single GraphQL request and contains data specific to the current GraphQL operation and resolver.

The GraphQLResolveInfo object contains not only information about the current GraphQL operation but also global data that applies to the entire GraphQL schema.

Inside a GraphQLResolveInfo object you can find the following values:

  • fieldNamethe name of the field being resolved for the current resolver
  • fieldNodesa representation of the selection set of the GraphQL operation
  • returnTypewhat is the type of the field currently being resolved?
  • parentTypewhat is the type of the parent object?
  • pathwhat fields were traversed to reach the current field being resolved
  • schemathe executable GraphQLSchema object, for the entire GraphQL schema, not just the type being resolved
  • fragmentsany fragments used in the query
  • rootValuethe rootValue argument passed to the GraphQL execution function
  • operationthe Abstract Syntax Tree (AST) of the GraphQL operation
  • variableValuesthe values of any GraphQL variables used in the operation

Now that we know what data is in a GraphQLResolveInfo object, let’s see how we can use it in our resolvers.

Ways To Use GraphQLResolveInfo

We’ll cover three ways to use a GraphQLResolveInfo object:

  1. Finding the nested selection set of a GraphQL operation
  2. Generating a single database query from the root resolver
  3. Implementing schema directives

Finding The Nested Selection Set Of A GraphQL Operation

The graphql-parse-resolve-info package can be used to find the nested selection set.

The first step is to determine the nested selection set of the GraphQL operation. We can do this by iterating through the fieldNodes in the GraphQLResolveInfo object or through the AST.

Benjie from PostGraphile has created the graphql-parse-resolve-info package which makes it easy to find the nested selection set from within a resolver.

Just pass it a GraphQLResolveInfo object and we get back a single object with all of the fields and their types as well as any aliases used. For our session search query it would look something like this:

Generate A Single Database Query From The Root Resolver

Once we’ve determined the nested selection set we can iterate through it to build up the database query, including the fields specified in the selection set in our database query. For example, the Cypher query to resolve our GraphQL request, searching for sessions and associated data would look something like this:

Cypher query to search for Sessions and associated data.

And if we ran this query in Neo4j Browser we would see a graph representation of the results:

This is essentially how the GraphQL transpilation feature of neo4j-graphql.js works: inspect the GraphQLResolveInfo object to determine the nested selection set then iterate over that, building up the Cypher database query. However, we also need to handle GraphQL schema directives at query generation time.

Implement Schema Directives

Schema directives are GraphQL’s built-in extension mechanism and are used to indicate that some custom logic should take place on the server. Two common GraphQL schema directives used in Neo4j GraphQL are @relation and @cypher

Using schema directives with neo4j-graphql.js

The @relation schema directive is used to specify the direction and type of a relationship in the property graph model when defining a relationship field in the GraphQL schema.

The @cypher schema directive is used to bind a GraphQL field to the result of Cypher query — a way of defining custom logic in the GraphQL schema.

The @cypherSchema Directive

Often we need to define custom logic in our GraphQL API — CRUD is simply not enough. One way to do this is by specifying a Cypher query in a @cypher schema directive when creating the GraphQL type definitions. This allows us to define computed fields in our GraphQL type definitions by mapping GraphQL fields to a Cypher query by annotating the field with a schema directive.

Here we define a simple recommendation query to find similar sessions to the one being resolved:

When this recommended field is requested in a GraphQL operation, the Cypher query specified in the schema directive is included in the single generated Cypher query as a sub-query.

And the results of our GraphQL query:

Using apollo-tools tooling to create a simple GraphQL Schema directive.

This approach of implementing GraphQL schema directives (inspecting the GraphQLResolveInfo when building the database query to search for schema directives) allows us to handle query generation logic dependent on the presence of these schema directives.

A more traditional way to implement GraphQL schema directives would be to use the apollo-tools tooling.

This approach is used in other GraphQL schema directive implementations used throughout neo4j-graphql.js, such as the graphql-auth-directives package which allows for defining authorization rules in GraphQL type definitions through the use of schema directives.

In this post, we explored how to build efficient GraphQL resolvers using the GraphQLResolveInfo object and we saw how this is accomplished in the neo4j-graphql.js library by generating Cypher database queries from arbitrary GraphQL operations.

If you’d like to learn more about any of these topics some resources are list below or feel free to reach out on Twitter.

Resources

Learn more about using GraphQL, React, Apollo, and Neo4j Database for building fullstack applications.

--

--