GraphQL Server Structure and Implementation (Part 3)

If you’ve written a GraphQL server before, chances are you’ve come across info objects passed into the resolver. Fortunately, in most cases, you don’t really need to know what it actually does and does in query resolution.

In many extreme cases, however, info objects are the cause of much confusion and misunderstanding. The purpose of this article is to demystify the Info object and clarify its role in GraphQL execution.

This article assumes that you are already familiar with the parsing basics of GraphQL queries and variations. If you’re not sure about this, you can check out the previous articles in this series: Part 1: GraphQL Schema (required) Part 2: Network Layer (optional)

infoObject structure

Review: The signature of the GraphQL resolver function

To quickly review, you have two main tasks when building a GraphQL server using graphqL.js:

  • Define your GraphQL Schema (using SDL or as a pure JS object)
  • For each field in the schema, implement a resolver function that knows how to return the value of that field

The resolver function takes four arguments (in that order) :

  1. parent: Result of the last resolver call (For more information).
  2. args: Parameter of the resolver field
  3. context: a custom object that each resolver function can read and write to
  4. info: This is what we will discuss in this article

Below is an overview of the execution of a simple GraphQL query and the calls to its splitter. Since the parsing of the second-level resolver is simple, there is no need to actually implement these resolvers — graphQL.js automatically deduces their return value:

GraphQL resolver chainparentandargsAn overview of parameters

Info contains query AST and more execution information

Questions about the structure and function of info objects are forgotten. It is not mentioned in official specifications or documentation. There was a GitHub issue asking for better documentation, but it was shut down without apparent action. Therefore, there is no choice but to delve into the source code.

At a very high level, you can say that the INFO object contains the AST of the GraphQL query passed in. Therefore, the resolver knows which fields need to be returned.

To learn more about AST, be sure to check out Christian Joudrey’s excellent article On The Life of GraphQL Queries — Lexing/Parsing, and Eric Baer’s excellent talk on Parsing GraphQL in depth.

To understand the structure of info, let’s look at its Flow Type definition:

Flow is a Javascript static type checker.

/* @flow */

export type GraphQLResolveInfo = {
  fieldName: string,
  fieldNodes: Array<FieldNode>,
  returnType: GraphQLOutputType,
  parentType: GraphQLCompositeType,
  path: ResponsePath,
  schema: GraphQLSchema,
  fragments: { [fragmentName: string]: FragmentDefinitionNode },
  rootValue: mixed,
  operation: OperationDefinitionNode,
  variableValues: { [variableName: string]: mixed },
}
Copy the code

Here is an overview and brief description of each key value:

  • fieldName: As mentioned earlier, every field in your GraphQL Schema needs resolver support.fieldNameContains the name of the field that belongs to the current resolver.
  • fieldNodes: an array in which each object represents a field in the remaining selection set.
  • returnType: GraphQL type of the corresponding field.
  • parentType: The GraphQL type of the field’s parent field.
  • path: Tracks the fields traversed until the current field (the resolver) is reached.
  • schema: GraphQLSchema instance representing your executable schema.
  • fragments: Mapping of the shard that belongs to the query document.
  • rootValue: passed to the current executionrootValueParameters.
  • operation: AST of the entire query
  • variableValuesThat corresponds to thevariableValuesParameter, a mapping of any variables supplied with the query.

If it still seems abstract, don’t worry. We’ll see all of these examples shortly.

Field-specific and Global (Field-specific vs Global)

There is an interesting discovery about the key above. The keys on the INFO object are either field-specific or global.

For a particular field simply the value of the key depends on the field passed to the info object (and the shitter behind it). Obvious examples are fieldName, returnType, and parentType. Consider the following author field of type GraphQL:

typeQuery { author: User! feed: [Post!] ! }Copy the code

The fieldName for this field is just author, and the returnType is User! And parentType is Query.

For the FEED field, of course, these values are different: fieldName is feed, and returnType is [Post! ! ParentType is also Query.

Therefore, the values of these three keys are specific to a particular field. Other keys for specific fields are fieldNodes and PATH. In fact, the first five keys defined by the above “flow” are for specific fields.

Global, on the other hand, means that the values of those keys don’t change — no matter which resolver we’re talking about. Schema, fragments, rootValue, Operation, and variableValues will always have the same value for all resolvers.

A simple example

Let’s now look at an example of the contents of an INFO object. First, we will use the following schema definitions in this example:

typeQuery { author(id: ID!) : User! feed: [Post!] ! }typeUser { id: ID! username: String! posts: [Post!] ! }type Post {
  id: ID!
  title: String!
  author: User!
}
Copy the code

Assume the schema resolver implementation is as follows:

const resolvers = {
  Query: {
    author: (root, { id }, context, info) => {
      console.log(`Query.author - info: `, JSON.stringify(info))
      return users.find(u => u.id === id)
    },
    feed: (root, args, context, info) => {
      console.log(`Query.feed - info: `, JSON.stringify(info))
      return posts
    },
  },
  Post: {
    title: (root, args, context, info) => {
      console.log(`Post.title - info: `, JSON.stringify(info))
      return root.title
    },
  },
}
Copy the code

Note that the resolver for post.title is not actually needed here, but we put its implementation here to see more information about the info object when the resolver is called.

Now consider the following query:

query AuthorWithPosts {
  author(id: "user-1") {
    username
    posts {
      id
      title
    }
  }
}
Copy the code

For brevity, we only discuss the resolver for the Query.author field, not the resolver for post.title (which is still called when the Query above is executed).

If you want to use this example, we have prepared a repository with a working version of the above schema, so you can give it a try!

Next, let’s take a look at each key in the INFO object and see what they are when the Query.author resolver is called (you can find the entire log output of the INFO object here).

fieldName

FieldName as an author.

fieldNodes

Remember that fieldNodes are field-specific. It actually contains an excerpt of the AST of the query. This excerpt starts at the current field (that is, author), not at the root of the query. (The entire query AST from the root is stored in operation, see below).

{
  "fieldNodes": [{"kind": "Field"."name": {
        "kind": "Name"."value": "author"."loc": { "start": 27, "end": 33}},"arguments": [{"kind": "Argument"."name": {
            "kind": "Name"."value": "id"."loc": { "start": 34."end": 36}},"value": {
            "kind": "StringValue"."value": "user-1"."block": false."loc": { "start": 38."end": 46}},"loc": { "start": 34."end": 46}}],"directives": []."selectionSet": {
        "kind": "SelectionSet"."selections": [{"kind": "Field"."name": {
              "kind": "Name"."value": "username"."loc": { "start": 54."end": 62}}."arguments": []."directives": []."loc": { "start": 54."end": 62}}, {"kind": "Field"."name": {
              "kind": "Name"."value": "posts"."loc": { "start": 67, "end": 72}}."arguments": []."directives": []."selectionSet": {
              "kind": "SelectionSet"."selections": [{"kind": "Field"."name": {
                    "kind": "Name"."value": "id"."loc": { "start": 81, "end": 83}}."arguments": []."directives": []."loc": { "start": 81, "end": 83}}, {"kind": "Field"."name": {
                    "kind": "Name"."value": "title"."loc": { "start": 90, "end": 95}}."arguments": []."directives": []."loc": { "start": 90, "end": 95}}],"loc": { "start": 73, "end": 101}}."loc": { "start": 67, "end": 101}}],"loc": { "start": 48."end": 105}}."loc": { "start": 27, "end": 105}}]}Copy the code

returnType & parentType

As noted earlier, returnType and parentType are fairly trivial:

{
  "returnType": "User!"."parentType": "Query"
}
Copy the code

path

Path tracks fields that have been traversed up to the current field. For query.author, it is only “path”: {“key”: “author”}.

{
  "path": { "key": "author"}}Copy the code

For comparison, in the post-. title resolver, path is as follows:

{
  "path": {
    "prev": {
      "prev": { "prev": { "key": "author" }, "key": "posts" },
      "key": 0}."key": "title"}}Copy the code

The remaining five fields are in the “global” category, so they are the same for the Post.title resolver.

schema

Schema is a reference to an executable schema.

fragments

Fragments contain sharding definitions, because there are no sharding definitions in the query document, so it is just an empty mapping: {}.

rootValue

As mentioned earlier, the value of the rootValue key first corresponds to the rootValue parameter passed to the GraphQL execution function. In this example, the field is NULL.

operation

Operation contains the complete query AST of the incoming query. Recall that this information contains the same values as the fieldNodes above, among other things:

{
  "operation": {
    "kind": "OperationDefinition"."operation": "query"."name": {
      "kind": "Name"."value": "AuthorWithPosts"
    },
    "selectionSet": {
      "kind": "SelectionSet"."selections": [{"kind": "Field"."name": {
            "kind": "Name"."value": "author"
          },
          "arguments": [{"kind": "Argument"."name": {
                "kind": "Name"."value": "id"
              },
              "value": {
                "kind": "StringValue"."value": "user-1"}}]."selectionSet": {
            "kind": "SelectionSet"."selections": [{"kind": "Field"."name": {
                  "kind": "Name"."value": "username"}}, {"kind": "Field"."name": {
                  "kind": "Name"."value": "posts"
                },
                "selectionSet": {
                  "kind": "SelectionSet"."selections": [{"kind": "Field"."name": {
                        "kind": "Name"."value": "id"}}, {"kind": "Field"."name": {
                        "kind": "Name"."value": "title"}}]}}]}}}Copy the code

variableValues

This key represents all variables that have been passed to the query. Since there are no variables in our example, its value is once again just an empty map: {}.

If the query is written using variables:

query AuthorWithPosts($userId: ID!) {
  author(id: $userId) {
    username
    posts {
      id
      title
    }
  }
}
Copy the code

The variableValues key will have the following values:

{
  "variableValues": { "userId": "user-1"}}Copy the code

When using the GraphQL bindinginfoThe role of

As mentioned at the beginning of this article, in most cases, you don’t need to worry about info objects at all. It’s part of the resolver’s signature, but it’s not really used for anything. So, when does it make sense?

willinfoPass to the binding function

If you’ve used GraphQL Bindings before, you’ve seen the INFO object as part of the generated binding function. Consider the following schema:

typeQuery { users(): [User]! user(id: ID!) : User }typeMutation { createUser(username: String!) : User! deleteUser(id: ID!!) : User }type User {
  id: ID!
  username: String!
}
Copy the code

With GraphQL-Binding, you can now send available queries and variations by calling specialized binding functions instead of sending them through raw queries and variations.

For example, consider the following raw query to retrieve a specific user:

query {
  user(id: "user-100") {
    id
    username
  }
}
Copy the code

Using a binding function to achieve the same effect is shown below:

binding.query.user({ id: 'user-100' }, null, '{ id username }')
Copy the code

By calling the user function on the binding instance and passing the corresponding parameters, we convey exactly the same information as the original GraphQL query above.

The binding function in graphQL-Binding takes three arguments:

  1. args: Contains the parameters of the field (for example, abovecreateUserMutations in theusername).
  2. context: Context objects passed along the chain of resolvers.
  3. info: infoObject. Note that in addition to passingGraphQLResolveInfoIn addition to the instance (type of information) of, you can also pass strings that define only the selection set.

Use Prisma to map application Schemas to database schemas

Another common use case where info objects can cause confusion is the implementation of the GraphQL server based on Prisma and prisMA-Binding.

In this case, the implementation consists of two GraphQL layers:

  • The database layerIt is automatically generated by Prisma and provides a common and powerful CRUD API
  • Application layerDefines the GraphQL API, which is exposed to client applications and tailored to the needs of your application

As the back-end developer, it is your responsibility to define the application Schema for the application layer and implement its shredder. Thanks to prisMA-Binding, the implementation of the shredder is simply the process of delegating incoming queries to the underlying database API without much overhead.

Let’s consider a simple example — suppose you start with the following data model for the Prisma database service:

type Post {
  id: ID! @unique
  title: String!
  author: User!
}

typeUser { id: ID! @uniqe name: String! posts: [Post!] ! }Copy the code

Prisma generates a database schema based on this data model similar to the following:

type Query {
  posts(
    where: PostWhereInput
    orderBy: PostOrderByInput
    skip: Int
    after: String
    before: String
    first: Int
    last: Int
  ): [Post]!
  postsConnection(
    where: PostWhereInput
    orderBy: PostOrderByInput
    skip: Int
    after: String
    before: String
    first: Int
    last: Int
  ): PostConnection!
  post(where: PostWhereUniqueInput!) : Post users(where: UserWhereInput
    orderBy: UserOrderByInput
    skip: Int
    after: String
    before: String
    first: Int
    last: Int
  ): [User]!
  usersConnection(
    where: UserWhereInput
    orderBy: UserOrderByInput
    skip: Int
    after: String
    before: String
    first: Int
    last: Int
  ): UserConnection!
  user(where: UserWhereUniqueInput!) : User }typeMutation { createPost(data: PostCreateInput!) : Post! updatePost(data: PostUpdateInput! .where: PostWhereUniqueInput!) : Post deletePost(where: PostWhereUniqueInput!) : Post createUser(data: UserCreateInput!) : User! updateUser(data: UserUpdateInput! .where: UserWhereUniqueInput!) : User deleteUser(where: UserWhereUniqueInput!) : User }Copy the code

Now, suppose you want to build an application schema that looks like the following:

type Query {
  feed(authorId: ID): Feed!
}

typeFeed { posts: [Post!] ! count: Int! }Copy the code

A FEED query returns not only a list of Post elements, but also the count of the list. Note that it has the option of using authorId to filter information to return only Post elements written by a particular User.

Your first instinct for implementing this application, SCEHMA, might look something like this.

Implementation 1: This implementation appears to be correct, but has a slight flaw:

const resolvers = {
  Query: {
    async feed(parent, { authorId }, ctx, info) {
      // build filter
      const authorFilter = authorId ? { author: { id: authorId } } : {}

      // retrieve (potentially filtered) posts
      const posts = await ctx.db.query.posts({ where: authorFilter })

      // retrieve (potentially filtered) element count
      const postsConnection = await ctx.db.query.postsConnection({ where: authorFilter }, `{ aggregate { count } }`)
      return {
        count: postsConnection.aggregate.count,
        posts: posts,
      }
    },
  },
}
Copy the code

This implementation seems reasonable. Inside the Feed resolver, we construct an authorFilter based on the authorId that might be passed in, and then use the authorFilter to perform the Posts query and retrieve the Post element, as well as the postsConnection query, which provides access to the list count.

You can also use just a postsConnection query to retrieve the actual Post elements. For the sake of simplicity, we’ll stick with Post queries and leave the other method as an exercise for attentive readers.

In fact, when you start the GraphQL server with this implementation, at first glance it looks good. You will notice that simple queries are handled correctly, such as the following query will succeed:

query {
  feed(authorId: "cjdbbsepg0wp70144svbwqmtt") {
    count
    posts {
      id
      title
    }
  }
}
Copy the code

When you try to query the author of Post, the problem starts:

query {
  feed(authorId: "cjdbbsepg0wp70144svbwqmtt") {
    count
    posts {
      id
      title
      author {
        id
        name
      }
    }
  }
}
Copy the code

Well, for some reason, this implementation Cannot return author, so the error “Cannot return null for non-nullable post.author.” is emitted. The Post. Author field is marked as required in the application schema.

Let’s look at the relevant parts of the implementation again:

// retrieve (potentially filtered) posts
const posts = await ctx.db.query.posts({ where: authorFilter })
Copy the code

This is where we retrieve the Post element. However, we did not pass the selection set to the Posts binding function. If the second parameter is not passed to the Prisma binding function, the default behavior is to query all scalar fields of that type.

That does explain the behavior. The call to ctx.db.query.posts returns the correct set of Post elements, but only their ID and title values — no author data.

So how do we solve it? Obviously, you need a way to tell the Posts binding function which fields to return. But where does this information go in the context of the feed resolver? Can you guess?

Correct: Inside the INFO object! Since the second argument to the Prisma binding function can be either a string or an INFO object, we simply pass the INFO object passed to the Feed resolver to the Posts binding function.

“Field ‘posts’ of type’ Post ‘must have a sub selection.”

const resolvers = {
  Query: {
    async feed(parent, { authorId }, ctx, info) {
      // build filter
      const authorFilter = authorId ? { author: { id: authorId } } : {}

      // retrieve (potentially filtered) posts
      const posts = await ctx.db.query.posts({ where: authorFilter }, info) // pass `info`

      // retrieve (potentially filtered) element count
      const postsConnection = await ctx.db.query.postsConnection({ where: authorFilter }, `{ aggregate { count } }`)
      return {
        count: postsConnection.aggregate.count,
        posts: posts,
      }
    },
  },
}
Copy the code

However, with this implementation, no request will be handled correctly. For example, consider the following query:

query {
  feed {
    count
    posts {
      title
    }
  }
}
Copy the code

Error message “Field ‘posts’ of type’ Post ‘must have a sub selection.” Generated by line 8 of the above implementation.

So what’s going on here? It failed because the key in the information object for a particular field did not match the posts query.

Printing information objects inside the feed resolver makes you more informed. Let’s just consider the information for a specific field in fieldNodes:

{
  "fieldNodes": [{"kind": "Field"."name": {
        "kind": "Name"."value": "feed"
      },
      "arguments": []."directives": []."selectionSet": {
        "kind": "SelectionSet"."selections": [{"kind": "Field"."name": {
              "kind": "Name"."value": "count"
            },
            "arguments": []."directives": []}, {"kind": "Field"."name": {
              "kind": "Name"."value": "posts"
            },
            "arguments": []."directives": []."selectionSet": {
              "kind": "SelectionSet"."selections": [{"kind": "Field"."name": {
                    "kind": "Name"."value": "title"
                  },
                  "arguments": []."directives": []}]}}]}}Copy the code

This JSON object can also be represented as a selection set in string form:

{
  feed {
    count
    posts {
      title
    }
  }
}
Copy the code

It all makes sense now! We will send the above selection set to the Posts query in the Prisma database Schema, which of course does not know the feed and count fields. Granted, the resulting error messages aren’t very helpful, but at least we know what’s happening now.

So what is the solution to this problem? One way to solve this problem is to manually parse out the correct portion of the fieldNodes selection set and pass it to the Posts binding function (for example, as a string).

A more elegant solution to this problem is to implement a dedicated resolver for the Feed type from the application Schema. This is the right way to do it.

Implementation 3: This implementation addresses the above issues

const resolvers = {
  Query: {
    async feed(parent, { authorId }, ctx, info) {
      // build filter
      const authorFilter = authorId ? { author: { id: authorId } } : {}

      // retrieve (potentially filtered) posts
      const posts = await ctx.db.query.posts({ where: authorFilter }, `{ id }`) // second argument can also be omitted

      // retrieve (potentially filtered) element count
      const postsConnection = await ctx.db.query.postsConnection({ where: authorFilter }, `{ aggregate { count } }`)
      return {
        count: postsConnection.aggregate.count,
        postIds: posts.map(post => post.id), // only pass the `postIds` down to the `Feed.posts` resolver
      }
    },
  },
  Feed: {
    posts({ postIds }, args, ctx, info) {
      const postIdsFilter = { id_in: postIds }
      return ctx.db.query.posts({ where: postIdsFilter }, info)
    },
  },
}
Copy the code

This implementation solves all the problems discussed above. A few things to note:

  • In line 8, we now pass a string selection set ({id}) as the second parameter. This is just for efficiency, otherwise all scalar values will be taken with only the ID we need (it won’t be much different in our example).
  • We are not returning from the Query.feed resolverpostsInstead, returnpostIds, which is just an array of ids (represented as strings).
  • Now, inFeed.postsIn the resolver, we can access what the parent resolver returnspostId. This time, we can use the incominginfoObject, and simply pass it topostsBind functions.

If you want to use this example, you can look at the repository, which contains a running version of the above example. Feel free to try the different implementations mentioned in this article, and observe the behavior for yourself!

conclusion

In this article, you took an in-depth look at the Info objects used when implementing the GraphQL API based on graphqL.js.

The INFO object is not well documented officially — to learn more about it, you need to dig into the source code. In this tutorial, we’ll start with an overview of its internal structure and its role in the GraphQL resolver function. Then, we covered some of the extremes and potential pitfalls of info that you need to dig into.

All of the code shown in this article can be found in the appropriate GitHub repository, so you can experiment and observe the behavior of info objects yourself.