Gatsby GraphQL Custom Field Resolver Related Types

March 8th 2020

We ran into a scenario recently where we wanted to relate an image file to a field in our JSON object definition. We were using Gatsby Transformer JSON to transform our JSON files into GraphQL Types that we can query using the Gatsby GraphQL and since we were using the Gatsby Source Filesystem we had access to the source image files but we needed a way to relate those to a field in our Type definitions. Today we’re going to do a quick walk through using a Gatsby’s createResolver API to add custom resolvers to fields to related an images field in our JSON file to the appropriate file found in the File definition type.

First we’re going to setup a few plugins to help us create the type definitions. First, Gatsby Source Filesystem for sourcing both our .json files and .jpg files. Second, we’re using Gatsby Transformer JSON to create our type definitions for our Project type. We’re going to store the .json files we want to reference in /src/data folder and the images that we’re associating with our project in the /src/data/images folder. We install the two plugins gatsby-source-filesystem and gatsby-transformer-json:

yarn add gatsby-source-filesystem gatsby-transformer-json

And configure them in our gatsby-config.js file:

module.exports = {
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `data`,
        path: `src/data`,
        ignore: [`**/\.*`], // ignore files starting with a dot
      },
    },
    {
      resolve: `gatsby-transformer-json`,
      options: {
        typeName: ({ node }) => node.name 
      },
    },
  ]
}

Let’s walk through the plugin configuration options quick. For the Gatsby Source Filesystem we’re setting the path to look for files in src/data folder to source the files. There will be a GraphQL type created called File and allFile for traversing file nodes. As for the Gatsby Transformer Json plugin we are setting the typeName field to be the name of the “node” which the source file plugin creates and in our example the file: Project.json the typeName would resolve to Project

With those two plugins setup we can turn our attention back to the solution. Our problem is that we need to create a custom resolver for an image field on our Project type to relate the File type to our project. Let’s take a look at what our Project.json file looks like:

[
  {
    "name": "Project One",
    "url": "https://example.com",
    "technologies": ["Nuxt.js", "Bootstrap CSS", "GSAP"],
    "start": "2018-10-15",
    "end": "2020-01-15",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
  },
  {
    "name": "Project Two",
    "url": "https://example.com",
    "technologies": ["Nuxt.js", "Firebase", "Bulma CSS"],
    "start": "2018-10-15",
    "end": "2020-01-15",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
  }
]

As you can see our Project.json nodes do not contain a filed called image as we will dynamically create this later using Gatsby’s createTypes action. We can use the file gatsby-node.js and the sourceNodes API to further define our Schema types for our Project GraphQL type. The sourceNodes API exposes an action called createTypes that we can use to extend, overwrite or define GraphQL types. In our case we’re going to overwrite the Project type created by the Gatsby Transformer Json plugin so we have full control over the Type definition:

exports.sourceNodes = ({ actions }) => {
  actions.createTypes(`
    type Project implements Node @dontInfer {
      id: ID!
      name: String!
      technologies: [String!]!
      start: Date! @dateformat
      end: Date! @dateformat
      url: String!
      slug: String!
      image: File
      description: String!
    }`
  )
}

Opting out of type inference

There are however advantages to providing full definitions for a node type, and bypassing the type inference mechanism altogether. With smaller scale projects inference is usually not a performance problem, but as projects grow the performance penalty of having to check each field type will become noticeable.

Gatsby allows to opt out of inference with the @dontInfer type directive – which in turn requires that you explicitly provide type definitions for all fields.

gatsby.org/docs

We decided to use @dontInfer to control the Type definitions of our Project type. Note that we defined a new field in our Project type called image and set the type definition to File. Now that we created our field definition we can use Gatsby’s createResolvers API to create a custom resolver for our image field. We’re going to create a helper function that “slugifies” the name of the project and returns a string (which we will use to name the image we want to relate to our image field).

exports.createResolvers = ({ createResolvers }) => {
  //custom helper function to slugify a string
  const slugify = str => {
    return str
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, "-")
      .replace(/(^-|-$)+/g, "")
  }
}

Now that we have our helper function we can work on the createResolvers definition:

exports.createResolvers = ({ createResolvers }) => {
  //custom helper function to slugify a string
  const slugify = str => {
    return str
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, "-")
      .replace(/(^-|-$)+/g, "")
  }
  createResolvers({
    Project: {
      image: {
        type: "File", 
        resolve: (source, args, context) => {
          return context.nodeModel.runQuery({
            query: {
              filter: {
                name: {
                  eq: slugify(source.name)
                }
              }
            },
            type: "File",
            firstOnly: true,
          })
        }
      }
    },
  })
}

There’s a lot going on here but let’s walk through it. We are using the createResolvers action to create a resolver for our Project Type image field and we’re using the runQuery to run a GraphQL query to search for File types which name equals the slugified string of the name of our project. The resolve callback receives the source object in which we can get the Project’s name and pass that to our slugify function in order to use that as a filter in our runQuery function. Let’s say we have a project named: “My Special Project” our runQuery would look like:

return context.nodeModel.runQuery({
  query: {
    filter: {
      name: {
        eq: `my-special-project`
      }
    }
  },
  type: "File",
  firstOnly: true,
 })

Now all we have to do is put an image file named my-special-project.jpg in our /src/data/images directory and our runQuery will attach our resolver field allowing us to include our image in a GraphQL query such as:

query ($projectID: String!) {
    project(id: { eq: $projectID }) {
      name
      description
      technologies
      url
      id
      image {
        publicURL
      }
      start(formatString: "MMM Do YYYY")
      end(formatString: "MMM Do YYYY")
    }
  }

And our data returned will look like:

{
  "data": {
    "project": {
      "id": "e3182852-50f7-5a98-afab-0b2858a6b859",
      "url": "https://myspecialproject.com",
      "technologies": [
        "Nuxt.js",
        "Bootstrap CSS",
        "GSAP"
      ],
      "start": "2018-10-15",
      "slug": "/project/my-special-project",
      "name": "My Special Project",
      "image": {
        "publicURL": "/static/my-special-project-55083ac97b3a49cc4a60442c1bc5253b.jpg"
      },
      "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Felis bibendum ut tristique et. Mi tempus imperdiet nulla malesuada pellentesque elit eget gravida cum. Ut sem viverra aliquet eget. At varius vel pharetra vel turpis nunc eget. Tristique senectus et netus et malesuada fames. Semper risus in hendrerit gravida rutrum quisque non tellus orci. Pulvinar etiam non quam lacus suspendisse faucibus. Posuere urna nec tincidunt praesent semper feugiat. Egestas dui id ornare arcu odio ut sem nulla. Porttitor eget dolor morbi non arcu risus. Lacus sed viverra tellus in hac habitasse platea. Augue eget arcu dictum varius duis. Nisi lacus sed viverra tellus. Suspendisse ultrices gravida dictum fusce ut placerat orci. Blandit turpis cursus in hac habitasse. Et ultrices neque ornare aenean.",
      "end": "2020-01-15"
    }
  }
}

Note the image: { projectURL: "/static/my-special-project-55083ac97b3a49cc4a60442c1bc5253b.jpg" } field and you can see we’ve successfully attached our image to our project!

We have successfully created a field resolver that uses runQuery which executes a GraphQL query to resolve the image field to filter type File to the slugified Project name in order to create a relationship between our Project schema and our Files schema.

Hopefully this will help in your next Gatsby project! Until next time, stay curious, stay creative!