GraphQLResolveInfo type

This post will describe the GraphQLResolveInfo type which can help you to prepare and optimise your backend request when resolving a GraphQL request.

We will continue the discussion started in the previous post regarding the 3rd parameter passed to your resolve method:

source: any,
args: { [key: string]: any },
info: GraphQLResolveInfo

The structure of GraphQLResolveInfo is :

export type GraphQLResolveInfo = {
  fieldName: string,
  fieldASTs: Array<Field>,
  returnType: GraphQLOutputType,
  parentType: GraphQLCompositeType,
  schema: GraphQLSchema,
  fragments: { [fragmentName: string]: FragmentDefinition },
  rootValue: any,
  operation: OperationDefinition,
  variableValues: { [variableName: string]: any },
}

Simple query:

Let’s start with a simple query like:

{
  user(id: "1") {
    id
    name
  }
}

For a relatively simple schema, the JSON dump of the instance of GraphQLResolveInfo is more than 17.000 lines long!

It seems that the first 2 elements are where we should find what we need :

I removed a bunch of fields (alias and loc) which don’t appear to be important at this stage of the analysis.

An abbreviated version of fieldASTs looks like that:

"fieldASTs": [
  {
    "kind": "Field",
    "name": {
      "kind": "Name",
      "value": "user",
    },
    "arguments": [
      {
        "kind": "Argument",
        "name": {
          "kind": "Name",
          "value": "id",
        },
        "value": {
          "kind": "StringValue",
          "value": "1",
        },
      }
    ],
    "directives": [],
    "selectionSet": {
      "kind": "SelectionSet",
      "selections": [
        {
          "kind": "Field",
          "name": {
            "kind": "Name",
            "value": "id",
          },
          "arguments": [],
          "directives": [],
          "selectionSet": null,
        },
        {
          "kind": "Field",
          "name": {
            "kind": "Name",
            "value": "name",
          },
          "arguments": [],
          "directives": [],
          "selectionSet": null,
        }
      ],
    },
  }
]

Some guess work here :

Query with an Inline fragment

We could also do a request using an inline fragment (doesn’t really make sense here as there is no ambiguity regarding the nature of the object returned, but still):

{
  podcast(id: "1") {
    id
    title
    ...on Podcast {
      webUrl
      rssUrl
    }
  }
}

This time we have:

(extra set of unused fields removed for sake of readibility)

"fieldASTs": [
    "selectionSet": {
      "kind": "SelectionSet",
      "selections": [
        {
          "kind": "Field",
          "name": {
            "kind": "Name",
            "value": "id",
          },
        },
        {
          "kind": "Field",
          "name": {
            "kind": "Name",
            "value": "title",
          },
        },
        {
          "kind": "InlineFragment",
          "typeCondition": {
            "kind": "NamedType",
            "name": {
              "kind": "Name",
              "value": "Podcast",
            },
          },
          "selectionSet": {
            "kind": "SelectionSet",
            "selections": [
              {
                "kind": "Field",
                "name": {
                  "kind": "Name",
                  "value": "webUrl",
                },
              },
              {
                "kind": "Field",
                "name": {
                  "kind": "Name",
                  "value": "rssUrl",
                },
              }
            ],
          },
        }
      ],
    },
  }
],

This time we see that we have a new kind of element in the selections : InlineFragment.

The good news is that this structure is recursive and we just need to parse the selectionSet of this element to retrieve the extra fields we need (['webUrl','rssUrl']).

Request with a fragment spread

Another way to query the schema, is to use a fragment spread like here:

{
  podcast(id: "1") {
    id
    title
    ...podcastUrls
  }
}

fragment podcastUrls on Podcast {
  webUrl
  rssUrl
}

Let’s see how fieldASTs look like this time:

"fieldASTs": [
  {
    "selectionSet": {
      "kind": "SelectionSet",
      "selections": [
        {
          "kind": "Field",
          "name": {
            "kind": "Name",
            "value": "id",
          },
        },
        {
          "kind": "Field",
          "name": {
            "kind": "Name",
            "value": "title",
          },
        },
        {
          "kind": "FragmentSpread",
          "name": {
            "kind": "Name",
            "value": "podcastUrls",
          },
        }
      ],
    },
  }
]

No big surprise here, there is a new kind of element : FragmentSpread.

As expected, we can find the definition of this fragment spread in the fragments section of our GraphQLResolveInfo:

"fragments": {
    "podcastUrls": {
      "kind": "FragmentDefinition",
      "name": {
        "kind": "Name",
        "value": "podcastUrls",
      },
      "typeCondition": {
        "kind": "NamedType",
        "name": {
          "kind": "Name",
          "value": "Podcast",
        },
      },
      "selectionSet": {
        "kind": "SelectionSet",
        "selections": [
          {
            "kind": "Field",
            "name": {
              "kind": "Name",
              "value": "webUrl",
            },
          },
          {
            "kind": "Field",
            "name": {
              "kind": "Name",
              "value": "rssUrl",
            },
          }
        ],
      },
    }
  },

The good news is that we can parse the selectionSet of this object to get to the fields we need (['webUrl','rssUrl'])!

Code to parse

The code to parse the GraphQLResolveInfo to retrieve the list of fields that the request wants to fetch is finally relatively simple.

function astValueFromName(ast) {
  if (ast.name && ast.name.kind && ast.name.value) {
    if (ast.name.kind === 'Name') {
      return ast.name.value;
    }
  }
  throw new Error('unknown ast structure to extract name');
}

// this method will return the list of fields
// from a selection set
function listOfFieldsFromSelectionSet(selectionSet, fragments) {

  return selectionSet.selections.reduce((arr, selection) => {
    if (selection.kind === 'Field') {
      // we can directly get the field name
      arr.push(astValueFromName(selection));
    } else if (selection.kind === 'InlineFragment') {
      // we need to recurse into the definition of the inline fragment
      arr = arr.concat(listOfFieldsFromSelectionSet(selection.selectionSet,fragments));
    } else if (selection.kind === 'FragmentSpread') {
      // let's find the ast for this spread fragment
      var spreadName = astValueFromName(selection);
      var astSpread = fragments[spreadName];
      if (! astSpread) {
        throw new Error('can\'t find spread with name:' + spreadName);
      }
      // we need to recurse into the definition of the spread fragment
      arr = arr.concat(listOfFieldsFromSelectionSet(astSpread.selectionSet,fragments));
    }
    return arr;
  }, []);
}

and you call this method from your resolve method with:

listOfFieldsFromSelectionSet(ast.fieldASTs[0].selectionSet, ast.fragments)

Implementation note

GraphQL being pretty young and still much in flux, it is important to check the expectation as much as possible in order to catch any change in the ast structure as quickly as possible.

Conclusion

This GraphQLResolveInfo is complex but it contains all the information we can dream of in order to extract the intent of the query and then to perform all sort of optimizations to query the actual data.