70 %
Chris Biscardi

Constructing Query Types in Themes

Previously, we covered how seemingly innocuous queries can prevent your themes from being composable when used with other themes and even sites you didn't plan for. In this post, we'll cover the core issue a bit more directly.

Querying Formats and Sources

To refresh your memory, we explored a query for Mdx nodes and a similar query for File nodes, shown here.

GraphQL
{
allMdx(
sort: { fields: [frontmatter___date], order: DESC }
filter: { frontmatter: { draft: { ne: true } } }
) { ... }
}
GraphQL
allFile(filter: { sourceInstanceName: { eq: "posts" } }) {
edges {
node {
childMdx {
...
}
}
}
}

The core problem with both of these queries is that we have a conceptual content type that these represent (BlogPost, Note, AuthorBio, etc) and we're querying a format (Mdx) or a source (File) and not a content type (BlogPost). Mdx is how our files are written while File is where our files come from. Neither accurately represents a conceptual content type accurately, ie: what the content means to us.

Creating new types

So to start, we need to create a new type in the GraphQL system called BlogPost. To do this, we'll use the sourceNodes lifecycle method and the schema customization APIs. Specifically we're using buildObjectType to create a new type with a set of resolvers at the same time.

Most of our fields have no resolvers defined because they can be infered by Gatsby itself. There are roughly two types of fields here:

  1. fields whose values exist on the source object itself
  2. fields that only return values after using the resolver to compute the value

For the second type of field we need to pass through our resolver to the source type's resolver to get the final value (note: this resolver code will be simplified by additional future Gatsby APIs). We take this approach for excerpt and body.

JS
// gatsby-node.js
exports.sourceNodes = ({ actions, schema }) => {
const { createTypes } = actions;
createTypes(
schema.buildObjectType({
name: `BlogPost`,
fields: {
id: { type: `ID!` },
title: {
type: "String!"
},
tags: { type: `[String]!` }
excerpt: {
type: "String!",
resolve: async (source, args, context, info) => {
const type = info.schema.getType(`Mdx`);
const mdxNode = context.nodeModel.getNodeById({
id: source.parent
});
const resolver = type.getFields()["excerpt"].resolve;
const excerpt = await resolver(
mdxNode,
{ pruneLength: 140 },
context,
{
fieldName: "excerpt"
}
);
return excerpt;
}
},
body: {
type: "String!",
resolve(source, args, context, info) {
const type = info.schema.getType(`Mdx`);
const mdxNode = context.nodeModel.getNodeById({
id: source.parent
});
const resolver = type.getFields()["body"].resolve;
return resolver(mdxNode, {}, context, {
fieldName: "body"
});
}
},
},
interfaces: [`Node`]
})
);
};

So now we have our BlogPost type, but we still need some nodes using that type. To do this, we'll use some boilerplate createNode calls that translates fairly directly from MDX frontmatter to BlogPost fields. We only want to create BlogPost nodes from Mdx nodes with File parents from the posts filesystem source in this case. We could, in the future, source BlogPosts from different places as well.

JS
// gatsby-node.js
const crypto = require("crypto");
exports.onCreateNode = ({
node,
actions,
getNode,
createNodeId
}) => {
const {
createNodeField,
createNode,
createParentChildLink
} = actions;
if (node.internal.type === `Mdx`) {
const { frontmatter } = node;
const parent = getNode(node.parent);
if (
parent.internal.type === "File" &&
parent.sourceInstanceName === "posts"
) {
const fieldData = {
title: node.frontmatter.title,
tags: node.frontmatter.tags || []
};
createNode({
...fieldData,
// Required fields.
id: createNodeId(`${node.id} >>> BlogPost`),
parent: node.id,
children: [],
internal: {
type: `BlogPost`,
contentDigest: crypto
.createHash(`md5`)
.update(JSON.stringify(fieldData))
.digest(`hex`),
content: JSON.stringify(fieldData),
description: `Blog Posts`
}
});
createParentChildLink({
parent: parent,
child: node
});
}
}
};

Once we've done this, the following query will always work regardless of if we have any BlogPosts in the node system. Note that even though we haven't coded any explicit support for filtering or sorting, we can still use them because Gatsby handles that for us.

Note also that because we created a parent/child relationship for the BlogPost nodes, we can reach up into any of the parents to access interesting information, so no user of our theme will be overly restricted if they want to do something outside of the 80% use case.

GraphQL
{
allBlogPost(sort: { fields: title, order: ASC }) {
totalCount
nodes {
title
tags
body
parent {
... on Mdx {
parent {
... on File {
relativePath
}
}
}
}
}
}
}

With no posts, the result is:

JS
{
"data": {
"allBlogPost": {
"totalCount": 0,
"nodes": []
}
}
}

With results, we get the following instead (note: I've removed the body value for brevity as it can get quite long).

JS
{
"data": {
"allBlogPost": {
"totalCount": 2,
"nodes": [
{
"title": "Something Else",
"tags": [],
"body": "",
"parent": {
"parent": {
"relativePath": "page-2.mdx"
}
}
},
{
"title": "my post",
"tags": [],
"body": "",
"parent": {
"parent": {
"relativePath": "index.mdx"
}
}
}
]
}
}
}

Fin

Now we've gotten ourselves to a point where we can use a BlogPost type instead of Mdx, simplifying our code base and also ensuring that when users of our theme want to query for blog posts on their pages, they don't have to worry about filtering all of the other Mdx or File results out.