Stop me if you've heard this one...
A developer is trying to connect Drupal to Gatsby. Drupal says, "I've got a Related Content field that allows SEVEN different entity types to be referenced, but only THREE slots for content. What do you think?"
Developer says, "Sure, no problem, Drupal!"
Gatsby jumps in and says, "You better make sure that all seven of those entity types are in those three slots."
Developer and Drupal look at each other in confusion. Developer says, "Gatsby, that's not GREAT"
** Laughs go here **
What happened?
It turns out the way that Gatsby builds what it calls "nodes" is by querying the data available. This usually works perfectly, but there are occasions where all the data might not be present, so Gatsby doesn't build the nodes, or it builds the nodes, but doesn't create all of the fields. Any optional field can fall into this if there is no content in that field on a Drupal entity, and if you're building templates for your decoupled site, then you might just want some content in that field.
NOTE: A Drupal "node" and a GraphQL "node" are not the same thing. This may take some time to sink in. It definitely did for me! For the remainder of this post I will refer to GraphQL nodes as "nodes" and Drupal nodes as "entities". Everybody got that? Good, moving on.
Because the nodes with your hypothetical content have not yet been created, or the fields on your entity don't have anything in them, Gatsby just passes right over them and acts like they don't even exist.
Commonly, you'll see something like this pop up during gatsby build
or gatsby develop
:
Generating development JavaScript bundle failed
/var/www/gatsby/src/components/templates/homepage/index.js
56:9 error Cannot query field "field_background_media" on type "node__homepageRelationships".
This can be frustrating. And when I say "frustrating" I mean that it's table-flipping, head slamming into desk, begging for help from strangers on the street frustrating. The quick fix for this one is simple: ADD SOMETHING TO THE FIELD. In fact, you don't even have to add it to every instance of that field. You can get away with only putting some dummy content in a single entity and you'll have access to that field in Gatsby.
You could also make it a required field so that there MUST be something in it or else the author can't save. This is valid, but in the event that your project's powers-that-be decide that it should be optional, you may need another alternative. This is where GraphQL schema customization comes in.
GraphQL schema and Gatsby
First, a little primer on GraphQL schema. Outside of Gatsby, a system using GraphQL usually needs to define the schema for GraphQL nodes. Gatsby takes this out of the developer's hands and builds the schema dynamically based on the content available to it from its source, in this case gatsby-source-drupal
. However, empty data in the source results in no schema in Gatsby.
Let's look at the fix for the optional image above and why it works:
In gatsby-node.js
we build our schema through createPages
, createPage
, and GraphQL queries. This is also where we're going to place our code to help GraphQL along a little bit. This will live in a function called createSchemaCustomization
that we can use to customize our GraphQL schema to prevent empty field errors from ruining our day.
The Gatsby docs outline this in the Schema Customization section, but it didn't really click there for me because the examples are based on sourcing from Markdown as opposed to Drupal or another CMS. Things finally clicked a little when I found this issue on Github that showed how to work around a similar problem when sourcing from WordPress.
Three scenarios and three solutions
Scenario 1: Empty field
This is by far the simplest scenario, but that doesn't make it simple. If you have a basic text field on a Drupal entity that does not yet have any content or at some point may not have any content, you don't want your queries to break your site. You need to tell GraphQL that the field exists and it needs to create the matching schema for it.
Here's how it's done:
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions
const typeDefs = `
type node__homepage implements Node {
field_optional_text: String
`
createTypes(typeDefs)
}
Now, to walk through it a bit. We're using createSchemaCustomization
here and passing in actions
. The actions
variable is an object that contains several functions so we destructure it to only use the one we need here: createTypes
.
We define our types in a GraphQL query string that we lovingly call typeDefs.
The first line of the query tells which type we're going to be modifying, in this case node__homepage
, and then what interface we're implementing, in this case Node
.
NOTE: If you're looking at GraphiQL to get your field and node names it may be a bit misleading. For example, in GraphiQL and in my other queries node__homepage
is called as nodeHomepage
. You can find out what the type name is by running a query in GraphiQL that looks like:
query MyQuery {
nodeHomepage {
internal {
type
}
}
}
or by exploring your definitions in GraphiQL.
Next, we add the line field_optional_text: String
which is where we define our schema. This is telling GraphQL that this field should exist and it should be of the type String
. If there are additional fields, Gatsby and GraphQL are smart enough to infer them so you don't have to redefine every other field on your node.
Finally, we pass our customizations into createTypes and restart our development server. No more errors!
Scenario 2: Entity reference field
Suppose we have a field for media that may not always have content. We still want our queries to work in Gatsby, but if there's only one Drupal entity of this content type and it happens to not have an image attached, then our queries are going to break, and so will our hearts. This requires a foreign key relationship because the entity itself is a node to GraphQL.
Here's how it's done:
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions
const typeDefs = `
type node__homepage implements Node {
relationships: node__homepageRelationships
}
type node__homepageRelationships {
field_background_media: media__image @link(from: "field_background_media___NODE")
}
`
createTypes(typeDefs)
Looks a little similar to the previous scenario, but there's a bit more to it. Let's look at the changes.
Instead of defining our custom schema under the type node__homepage implements Node {
line, we reference our relationships
field which is where all referenced nodes live. In this case, we tell GraphQL that the relationships
field is of type node__homepageRelationships
. This is another thing you can find using GraphiQL.
The next section further defines our relationships field. We declare that node__homepageRelationships
should have the field field_background_media
of type media__image
, but what's that next part? That's where our foreign key link is happening. Since field_background_image
is an entity reference, it becomes a node. This line links the field to the node created from the field. On a field with only one entity type referenced, it may not be necessary to include this line, but if your media field starts allowing images AND videos, you're going to want it and you're going to eventually need to make some other updates.
Scenario 3: Related content field with multiple content types
This is the one that had me crying. I mean scream-crying. This is the widowmaker, the noisy killer, the hill to die upon. At least it WAS, but I am triumphant!
Like my horrible joke from the beginning of this post, I have a related content field that is supposed to allow pretty much any content type from Drupal, but only has three slots. I need to have queries available for each content type, but if one doesn't exist on the field, then GraphQL makes a fuss. I can't say for sure, but I've got a feeling that it also subtly insulted my upbringing somewhere behind the scenes.
Now, I'm here to save you some time and potentially broken keyboards and/or monitors.
Here's how it's done:
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions
const typeDefs = `
union relatedContentUnion =
node__article
| node__basic_page
| node__blog_post
| node__cat_food_reviews
| node__modern_art_showcase
| node__simpsons_references
type node__homepage implements Node {
relationships: node__homepageRelationships
}
type node__homepageRelationships {
field_related_content: [relatedContentUnion] @link(from: "field_related_content___NODE")
}
`
createTypes(typeDefs)
}
The biggest changes here are the union and the brackets around the type. From the GraphQL docs:
Union types are very similar to interfaces, but they don't get to specify any common fields between the types.
This means that any type in the union can be returned and they don't need to have the same fields. Sounds like exactly what we need, right?
So we define our union of our six content types and create the relationship using a foreign key link. We use the node created from our field, field_related_content___NODE
as our foreign reference, and we get results. However, the brackets around the union are there, and they must do something. In fact they do. They indicate that what will be returned is an array of content in this union, meaning that there will be multiple values instead of one single value returned.
Summary
This seriously took me much longer than it should have to figure out. Hopefully, you don't suffer my same fate and find this post useful. One thing to note that almost destroyed me is that there are THREE, count them 1, 2, 3, underscores between the field name and NODE
in the link. _ _ _ NODE
. I only put two when I was fighting this and it cost me more time than I'm willing to admit on a public forum.
Special thanks to Shane Thomas (AKA Codekarate) for helping me sort through this and being able to count to 3 better than me.