Combined queries in graphql

Posted: 30 June, 2019 Category: code Tagged: graphqlgatsby

I recently ran into an issue where I needed to run 2 gql queries at the same time: one for products, and another for related product images. This came about because images were an entirely different part of the graph than the products themselves. So, I needed to fetch a product, and then also fetch its related images.

The Problem

First off, I didn't want to do 2 separate runs to graphql. Secondly, because of the way the images were stored (all together in one place, no folders, no inherent structure) I needed some way to filter for just the images related to the current product.

The Solution

Graphql lets you filter with regular expressions, so I decided on just imposing a naming convention on the images... <relatedProductId>-<imageRole>-.*. This was preferable to imposing a physical taxonomy on the images because one of the requirements was that images of any type and any size as well as any number could be arbitrarly dumped into the images folder, and they needed to simply, magically, "work" with the products.

After a bit of googling and trial and error, this is the graphql query I came up with. Note that its two top-level nodes can be labelled whatever you like.... I just chose labels that reflect the actual use case:

query productAndRelatedImagesQuery($productId: String!, $relatedImagesFilter: String!) {
  product: product(id: {eq: $productId}) {
    name
    description
    # ... other fields ...
  }

  relatedImages: allFile(filter: { name: { regex: $relatedImagesFilter } }) {
    nodes {
      name
      publicURL
    }
  }
}

In most cases relatedImagesFilter will be the exact same thing as the productId, but I had a bunch of scenarios on this project in which that was not necessarily the case, so I needed a separate discriminant for the images.

The Gatsby-specific thing to note here is that, the drilldown to the product will likely be coming from a product listing of sorts, so you could propagate this info down to the components that need it. I found it more expedient to pass these data as part of the page context, because in my case the final rendering component was a page template anyway. This meant that in the gatsby-node.js file, it was necessary to generate and propagate the key data elements - productId and relatedImagesFilter as part of the page context... leaving the resulting product page free to query what is specifically needed from graphql. This also keeps the "parent" product listing and its related query simple and uncluttered.

The following happens somewhere deep inside exports.createPages = ({ graphql, actions }) => {}

// a reminder as to where createPage comes from
const { createPage } = actions;

// ... interim code not shown ...

// ... at the point where product pages are being generated...
// cos remember - gatsby is BUILD-time, not runtime!
result.data.allProduct.edges.forEach(edge => {

  //... 

  const roleType = `...`;
  createPage({
    path: `...`,
    component: productPageComponent,
    context: {
      productId: edge.node.productId,
      // images regex filter goes here
      relatedImagesFilter: `/(?:${edge.node.productId.toLowerCase()}-${roleType})/`
    }
  });
});

So that's how the page context is populated, giving the page everything it needs to run the "combo" query in graphql and fetch two seemingly-separate things at once.

Also easy to miss, but note the lazy allFile query for the images implies that they are already being vacuumed up by gatsby-source-filesystem somewhere in your gatsby-config.

Hope the code snippets are useful!