Gatsby Data Relationships With Foreign-Key Fields

June 3rd 2020

If you have worked with Gatsby you know that Gatsby relies on GraphQL schema and generation for it’s data layer. If you are developer coming from a more traditional MERN stack than wrapping your head around GraphQL can be a little daunting. Thus, jumping into Gatsby’s data layer can be a little intimidating. Building a website and/or web application the data layer is going to be an important aspect of the development process, so understanding GraphQL in your Gatsby application is important. This tutorial walks through relating two different content types/models in our GraphQL schema.

Gatsby uses GraphQL to enable page and StaticQuery components to declare what data they and their sub-components need. Then, Gatsby makes that data available in the browser when needed by your components.

https://www.gatsbyjs.org/docs/graphql-concepts/

Gatsby Source JSON Data

We will walk through sourcing data from two files: author.json and post.json. These files will represent our data such data would look like the data you could get from a mongo database or another NoSQL database. Inside our files our data structure would look like:

author.json

[
  {
    "name": "John Doe",
    "email": "[email protected]",
    "username": "jdoe"
  }
]

This is a simple JSON Array which contains objects that hold data for each one of our authors. Inside our post.json file we will also have similar data structure for our post data.

post.json

[
  {
    "title": "Post One",
    "date": "2020-05-30",
    "content": "Post one lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
    "tags": [
      "gatsby", "react", "graphql"
    ],
    "author": "jdoe"
  },
  {
    "title": "Post Two",
    "date": "2020-05-31",
    "content": "Post two lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
    "tags": [
      "gatsby", "react", "graphql"
    ],
    "author": "jdoe"
  }
]

As you can see we have a field inside our post objects called author which is the username for the author of the post.

In order to source this data into our GraphQL data layer we will need to use two Gatsby plugins: gatsby-source-filesystem and gatsby-transform-json. Let’s add these plugins to our Gatsby project:

yarn add gatsby-source-filesystem gatsby-transform-json

With the two package dependencies installed we can add the plugins to our gatsby-config.js file such as:

module.exports = {
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `data`,
        path: `${__dirname}/src/data`
      }
    },
    {
      resolve: `gatsby-transformer-json`,
      options: {
        typeName: ({ node }) => node.name.charAt(0).toUpperCase() + node.name.slice(1)
      }
    }
  ]
}

We are adding options to each of our plugins. First, we’re adding path option to our gatsby-source-filesystem which specifies the path to our JSON files. Secondly, inside the options for our gatsby-transformer-json we are specifying an option for our typeName. The typeName option allows us to specify the GraphQL Object Type Name that will be generated for us. By default the gatsby-transformer-json generates the typeName based on the filename plus a Json extension. So for instance, if we did not provide the typeName option our GraphQL Schema for our author and post data would be authorJson and postJson to query single content objects or allAuthorJson and allPostJson to query array of data objects. Since we want to remove the Json addition of our typeName we are grabbing the filename from node.name and we’re capitalizing the first letter so our typeName will be Author and Post respectively – following GraphQL Type Name Conventions:

Type names should use PascalCase. This matches how classes are defined in the languages mentioned above.

www.apollographql.coms

Gatsby GraphiQL Playground

Now that we have our plugins installed we can start up our Gatsby development server and navigate to our GraphiQL Playground. In your project folder run yarn develop and when the development build is finished you can navigate to http://localhost:8000/___graphql

We can run a query to get our authors and posts from our GraphQL layer:

query {
  allAuthor {
    nodes {
      username
      name
      email
    }
  }
  allPost {
    nodes {
      title
      content
      date
      tags
    }
  }
}

We should be able to see the returned data generated from our query:

You can see that we got the expected result to return from both of our files: author.json and post.json. But what if we want to related these two pieces of content together? For instance, it would make sense to want to get all of the posts that an author has published such as:

query getAuthorPosts {
  allAuthor(filter: {username: {eq: "jdoe"}}) {
    nodes {
      name
      email
      username
      posts {
        title
        date
        content
        tags
      }
    }
  }
}

Likewise, it would be great if we could get all of the author’s information like the author’s name and email on each of the post objects when we query the posts:

query getPosts {
  allPost {
    nodes {
      title
      date
      content
      tags
      author {
        name
        username
        email
      }
    }
  }
}

In order to query for our author’s posts and also get our post by author we need to create a relationship in Gatsby so our GraphQL data layer can include the related content in our queries.

Gatsby GraphQL Schema Customization

Gatsby, out of the box, has type inference for data which automatically generates our GraphQL schema for us. We saw this by installing our gatsby-source-filesystem and gatsby-transformer-json plugins and then making the query for our allPost and allAuthor data. We didn’t have to write any GraphQL type definitions or define any field types in our schema objects. Gatsby did all of this for us with it’s automatic type inference!

Gatsby is able to automatically infer a GraphQL Schema from your data, and in many cases, this is really all you need. There are however situations when you either want to explicitly define the data shape, or add custom functionality to the query layer – this is what Gatsby’s Schema Customization API provides.

https://www.gatsbyjs.org/docs/schema-customization/

However, in our case we want to create a relationship between our author.json data and our post.json data so we need to be able to manipulate the GraphQL Schema that is created by Gatsby. Fortunately, Gatsby offers a hook for customizing Gatsby’s GraphQL data: createSchemaCustomization which can be called from gatsby-node.js

Actions to customize Gatsby’s schema generation are made available in the createSchemaCustomization (available in Gatsby v2.12 and above), and sourceNodes APIs.

https://www.gatsbyjs.org/docs/schema-customization/

Let’s take a look at how we can use the createSchemaCustomization hook to customize our GraphQL schema. First, we will need to define the hook in our gatsby-node.js file which expects a function that receives the actions argument such as:

exports.createSchemaCustomization = ({ actions }) => {
  { createTypes } = actions
}

As you can see inside our actions object we have a function called createTypes as the name implies can be used to create GraphQL schema type definitions or to customize schema type definitions that are already defined from plugins (such as our Author and Post object types). You are able to update types that are already defined because Gatsby merges type definitions with inferred field types:

by default, explicit type definitions from createTypes will be merged with inferred field types

What this means, is that if we already have a GraphQL type called Author and inside our createSchemaCustomization hook we create a type definition called Author the field definitions in our hook will be merged with the type that was created in our plugins.

Let’s continue inside our gatsby-node.js file with our exports.createSchemaCustomization hook definition by defining our GraphQL Schema Definition Language (SDL) which we can pass to the createTypes function.

Gatsby @link Directive

Gatsby has a few different ways to create our relationship between our Author and Post type, but one common way to do this is through the @link directive which we can define in our SDL (schema definition language) and pass to our createTypes action. Let’s look at what creating a relationship on our Post schema to include the author’s information in our GraphQL query would look like:

exports.createSchemaCustomization = ({ actions }) => {
  { createTypes } = actions
  const typeDefs = `
    type Post implements Node {
      author: Author @link(by: "username")
    }
    // implement this soon
    // Author implements Node {}
  `
  createTypes(typeDefs)
}

Gatsby provides a special directive @link which creates a custom field resolver for us. If we specify the by argument Gatsby will look for a field name on our source node (Post node) called author and pass the field value (for each node created) to the field name defined in by on our target node (Author node). If you remember in our source node Post data structure we specified the author field such as:

  {
    "title": "Post One",
    "date": "2020-05-30",
    "content": "Post one lorem ipsum dolor sit amet, consectetur ...",
    "tags": [
      "gatsby", "react", "graphql"
    ],
    "author": "jdoe"
  }

And in our Author data structure we have a field name called username:

  {
    "name": "John Doe",
    "email": "[email protected]",
    "username": "jdoe"
  }

Gatsby will use @link to create a foreign-key relationship from our source node to our target node using the by argument we defined.

Now we are able to restart our GraphiQL server by running yarn develop and we can navigate to http://localhost:8000/___graphql and run the query:

query getPosts {
  allPost {
    nodes {
      title
      date
      content
      tags
      author {
        name
        username
        email
      }
    }
  }
}

As you can see we are including our author field which is now a Node type and not a String and since it’s a Node type we can include the related fields such as name, username and email. This is a great step! But what if we wanted to create a back reference from our Author type to our Post type in order to get all posts for a certain author?

query getAuthorPosts {
  allAuthor(filter: {username: {eq: "jdoe"}}) {
    nodes {
      name
      email
      username
      posts {
        title
        date
        content
        tags
      }
    }
  }
}

This might be handy if you are on the author’s profile page and you want to see all of the post that they have written. We can easily do this by adding another type definition to our createTypes action and using the @link directive to create a back-link reference:

exports.createSchemaCustomization = ({ actions }) => {
  { createTypes } = actions
  const typeDefs = `
    type Post implements Node {
      author: Author @link(by: "username")
    }

    type Author implements Node {
      posts: [Post] @link(by: "author.username", from: "username")
    }
  `
  createTypes(typeDefs)
}

The from argument allows us to create a back-link reference for our Author nodes.

The optional from argument allows getting the field on the current type which acts as the foreign-key to the field specified in by. In other words, you link on from to by. This makes from especially helpful when adding a field for back-linking.

https://www.gatsbyjs.org/docs/schema-customization/#foreign-key-fields

Since our Post node field author is no longer a String and is now an object, we have to use by: "author.username" with object dot notation to get the value of our username argument. Now, with any luck we are able to restart GraphiQL server and run our query:

query getAuthorPosts {
  allAuthor(filter: {username: {eq: "jdoe"}}) {
    nodes {
      name
      email
      username
      posts {
        title
        date
        content
        tags
      }
    }
  }
}

And we should see the expected output with all of our post data included!

We have successfully created a two-way relationship between our Post nodes and our Author nodes by using the special @link directive in our createSchemaCustomization hook! This can open a whole new world of possibilities when shaping and forming our data structure in Gatsby. Until next time, stay curious, stay creative.