gatsby-source-sanity
gatsby-source-sanity copied to clipboard
Create reverse relationships between documents
From the Gatsby docs:
It’s often convenient for querying to add to the schema backwards references. For example, you might want to query the author of a post, but you might also want to query all the posts an author has written.
This is exactly what I'm looking for. I'd like to see reverse relationships supported in this plugin, in particular for one-to-many relationships implemented using type: "reference"
in the Sanity schema, so that I can query references in both directions.
I'd file a PR, but it's a bit beyond me where to start; I don't have TypeScript experience, and I'm not familiar enough with the internals of this plugin.
Some potentially helpful resources:
- Creating a Source Plugin: Creating the reverse relationship
- Customizing the GraphQL Schema: Foreign-key fields
- This Gatsby issue comment - in short, there's an "easy" way to create back-references, but it only works for one-to-one relationships; one-to-many relationships require more verbose code
@aaronadamsCA I was able to do this on Gastby by using the following code on gastby-node
// gastby-node.js
// create a resolver to add the inverse relationship between Author and Pinsels
// Author has many Pinsels
// Pinsel belongs to Author
exports.createResolvers = ({ createResolvers }) => {
const resolvers = {
SanityAuthor: {
pinsels: {
type: ["SanityPinsel"],
resolve(source, args, context, info) {
return context.nodeModel.runQuery({
type: "SanityPinsel",
query: {
filter: {
author: {
elemMatch: {
_id: {
eq: source._id,
},
},
},
},
},
})
},
},
},
}
createResolvers(resolvers)
}
Then you can query like
export const query = graphql`
query AuthorPageTemplateQuery($id: String!) {
author: sanityAuthor(id: { eq: $id }) {
name
email
pinsels {
_id
}
}
}
`
Thanks @orlando, that is indeed what I'm doing now.
~~Note Gatsby still has a few core issues with using runQuery
in resolvers, but there have been major improvements and continue to be. Most remaining issues are around Gatsby failing to detect new node relationships, so you need to gatsby clean
between builds to pick up new relationships.~~ This is fixed now.
~~There's also a quirk where runQuery
returns null
instead of []
when there are no results; be sure to guard against this. It should be fixed in Gatsby v3, but it requires waiting for a major release since it's a significant API behaviour change.~~ This is also fixed now, sample code is updated.
Here is my approach. Note that I've found it's important to use createSchemaCustomization
to avoid encountering race conditions as you begin to define more and more custom relationships:
exports.createSchemaCustomization = ({ actions }) => {
actions.createTypes(`
type SanityNeighbourhood implements Node @infer {
stores: GatsbyStoreMiniConnection!
}
type GatsbyStoreMiniConnection @dontInfer {
totalCount: Int!
nodes: [SanityStore!]!
}
`);
};
exports.createResolvers = ({ createResolvers }) => {
createResolvers({
SanityNeighbourhood: {
stores: {
resolve: async ({ id }, _, { nodeModel }) =>
nodeModel.runQuery({
type: "SanityStore",
query: {
filter: {
neighbourhoods: { elemMatch: { id: { eq: id } } },
},
},
}),
},
},
GatsbyStoreMiniConnection: {
totalCount: {
resolve: async (parent) => (await parent).length,
},
nodes: {
resolve: (parent) => parent,
},
},
});
};
This is still something I think is worth implementing in the plugin, of course; Sanity knows the relationship model, and a reverse connection pattern like this would be immensely valuable to adoption of the platform.
I'm trying to do something similar with categories... (show other posts referencing the same category). I've generated category pages with all posts referencing the category… but really struggling from the other direction.
This post seemed like the answer, but a reference only contains the _key
, _ref
, and _type
...none of which matches the id. It may have worked in a past version of the API... https://nimblewebdeveloper.com/blog/gatsby-generate-related-posts-at-build-time
Question posted in Slack... https://sanity-io-land.slack.com/archives/CEMV34XAB/p1632951724115300
exports.createSchemaCustomization = ({ actions, schema }) => {
actions.createTypes([
schema.buildObjectType({
name: "SanityPost",
fields: {
related: {
type: "[SanityPost]",
resolve: async (source, args, context, info) => {
// the id of the referenced category is not available
const categories = source.categories.map((c) => c._ref);
if (!categories.length) return [];
const posts = await context.nodeModel.runQuery({
query: {
filter: {
categories: {
elemMatch: {
_id: { in: categories },
},
},
_id: { ne: source._id },
},
},
type: "SanityPost",
});
return posts && posts.length ? posts : [];
},
},
},
}),
]);
};
SOLVED
Turns out the magic 🪄 is using _rawDataCategories
rather than categories
in the source object!
hey @doublejosh I am also trying to achieve the same thing;
show other posts referencing the same category
but as you are aware, none of _key
, _ref
, _id
matches so I am pretty stuck. Can you share a snippet of what you mean by using _rawDataCategories
because I don‘t seem to have those in my GraphQL. Thank you!
In case this is useful for anyone else attempting to create a reverse relationship
/***** CUSTOM DATA TYPES *******************************************************
*
* FUTURE SELF
* Purpose of the code below: Establish a foreign key relationship.
*
* Articles and Pages reference Tags. Put another way, Pages and Article can have
* many tags.
*
* Sanity source plugin automatically resolves this parent child relationship into the
* Gatsby data layer. That enables you to query an Article or a Page and find out which
* tags have been referenced.
*
* However, you may want to do this in reverse. You may want to...
* Query a tag for all of the pages that reference it.
*
* This reverse reference is NOT automatically created by the Sanity Source Plugin.
* So, you will need to create it yourself. I've described this verbosely for my future self
* in the code below.
*
* High Level Steps
* 1. Creates a new field called "relatedContent" on the SanityTag type.
* 2. Explicitly define the shape of the relatedContent data.
* 3. Query all Articles and Pages that contain a reference to the Tag
* 4. Add the data from Articles and Pages to the relatedContent field and return it.
*
*********************************************************************************/
exports.createSchemaCustomization = ({ actions, schema }) => {
const {
createTypes,
printTypeDefinitions // uncomment this to print the existing type definitions in case you need to see them.
} = actions
// printTypeDefinitions({ path: "./typeDefs.txt" }) //uncomment to see type defs
// Array of Type Definitions that you would like to create
const typeDefs = [
// Create a "relatedContent" type on the root type of "SanityTag"
"type SanityTag implements Node { relatedContent: [RelatedContent] }",
// Define the "RelatedContent" type that was applied to the "relatedContent" key in the line above this one.
`type RelatedContent {
id: String!
name: String!
slug: String
}`,
// Now that you have the type, let's populate it with data, or "Resolve" the data.
schema.buildObjectType({
name: "SanityTag", // This is the "source" that is passed into the resolve function below
fields: {
relatedContent: { // This is the field that we will resolve the data for.
type: ["RelatedContent"], // As we established above it is of type "RelatedContent" and it is an array.
// The resolve function will return the data that populates the field
// in this case we are populating the "relatedContent" field
resolve: async (source, args, context, info) => {
/**
* context.nodeModel gives you access to query the data layer
* Below, I'm finding all articles and all pages where
* the SanityTag's "id" field value exists in the array of tags that
* referenced by the parent object.
*/
// Find all articles (SanityArticle) that contain a reference to the
// tag (SanityTag). Note that the ID field is used as the reference. Put more verbosely...
// When an article references a tag, is uses the ID field of the tag.
const articles = await context.nodeModel.findAll({
type: "SanityArticle", // Find All SanityArticle's
query: { // Where
filter: {
tags: { // The tags array
elemMatch: { // Has an element or object within the array
// who's id field matches the source id field.
// remember that the source is "SanityTag"
id: { eq: source.id },
},
},
},
},
})
// Same as above but find all pages
const pages = await context.nodeModel.findAll({
type: "SanityPage",
query: {
filter: {
tags: {
elemMatch: {
id: { eq: source.id },
},
},
},
},
})
/**
* Now that we have all of the related pages and articles
* let's create the data objects that will populate or resolve the data
*
* I've explicitly set the shape of the data for the type.
* Below we are creating the data objects for articles and pages separately
* because the shape of articles and pages data is different.
*
* pages and articles are arrays of objects.
* We are using the Array.map function to create and populate a new object
* in the shape of the RelatedContent type.
*/
// Create the relatedArticles array
const relatedArticles = articles.entries.map( entry => {
return {
id: entry?.id ?? 'no value',
name: entry?.meta?.name ?? 'no value',
slug: entry?.meta?.slug?.current ?? ''
}
})
// Create the relatedPages array
const relatedPages = pages.entries.map( entry => {
return {
id: entry.id,
name: entry.name,
slug: entry?.slug?.current ?? ''
}
})
/**
* We are finally ready to return the data that we would like
* to have populate our new relatedArticles field
*/
// Establish and empty entries array
let entries = []
// Use the Array.push method and the spread operator to add
// relatedArticles and relatedPages to the entries array.
entries.push(...relatedArticles, ...relatedPages)
// Return the entries
return entries
},
},
},
}),
]
/**
* The last step is to pass the typeDefs to the createTypes function
* And like magic...
* SanityTag now contains a relatedContent field which is an array of
* all Articles and Pages that reference the tag.
*/
createTypes(typeDefs)
}
Below is the query and result from the Graph QL Interface in Gatsby Dev. For sake of clarity. In the Sanity CMS...
- There is a tag named "One" with a slug of "one"
- Three articles reference the tag named "One"
The Query Get the tag named "One" and show me all of the pages and articles that reference this tag.
query MyQuery {
allSanityTag(filter: {slug: {current: {eq: "one"}}}) {
nodes {
name
id
relatedContent {
name
slug
}
}
}
}
The Response Here's the response showing the reverse relationship. Three content objects reference this tag.
{
"data": {
"allSanityTag": {
"nodes": [
{
"name": "One",
"relatedContent": [
{
"id": "-9dfe5f2e-4a10-5efe-8e9e-ede618873020",
"name": "Hello World",
"slug": "hello-world"
},
{
"id": "-200f023f-b8ff-511b-b05f-543870a447bf",
"name": "Extension BMI Calculator",
"slug": "extension-bmi-calculator"
},
{
"id": "-a8ad8fc7-d22e-5939-b110-f3ef337fd9f6",
"name": "Home",
"slug": "home"
}
]
}
]
}
},
"extensions": {}
}
Now you can return the your Gatsby code and make some magic happen...
Hope this helps someone save a few minutes in their day. :)
@ajmalafif
exports.createSchemaCustomization = ({ actions, schema }) => {
actions.createTypes([
// attach related posts to each post page
schema.buildObjectType({
interfaces: ["Node"],
name: "SanityPost",
fields: {
relatedPosts: {
type: "[SanityPost]",
resolve: async (source, args, context, info) => {
const categories = source._rawDataCategories
? source._rawDataCategories.map((c) => c._ref)
: [];
if (!categories.length) return [];
// deprecated in Gatsby 5
const posts = await context.nodeModel.runQuery({
query: {
filter: {
categories: {
elemMatch: {
_id: { in: categories },
},
},
status: { eq: "published" },
// exclude current node
_id: { ne: source._id },
},
sort: {
fields: ["publishedAt"],
order: ["DESC"],
},
// no way to limit results in runQuery
// see: https://github.com/gatsbyjs/gatsby/issues/15453
},
type: "SanityPost",
});
return posts && posts.length > 0 ? posts : [];
},
},
},
}),
]);
};
@doublejosh thank you so much for sharing this, really appreciate it!