Auto Dereferencing
Projections resolver for auto dereferencing
One of the page builder sections - section-text-media.ts
import { defineType, defineField } from '@sanity/types';
export default defineType(
{
name: 'sectionTextMedia',
title: 'Text + Media',
type: 'object',
fields: [
defineField({
name: 'title',
type: 'string',
}),
defineField({
name: 'image',
type: 'image',
}),
defineField({
name: 'link',
type: 'reference',
}),
],
preview: {
select: {
subtitle: 'title',
},
prepare: ({ subtitle }) => ({ title: 'Text + Media', subtitle }),
},
},
);Group all page builder sections in one index file - sections.ts
import sectionTextMedia from '@/sanity/schemas/sections/section-text-media';
import sectionCta from '@/sanity/schemas/sections/sectionCta';
const sections = [sectionTextMedia, sectionCta];
export default sections;Custom field for the page builder - page-builder.ts
import { defineType } from '@sanity/types';
import sections from '@/sanity/schemas/sections';
export default defineType(
{
name: 'pageBuilder',
type: 'array',
of: [...sections.map((section) => ({ type: section.name }))],
}
);A document with a title, slug, image, and page builder fields - page.ts
import { defineType, defineField } from '@sanity/types';
export default defineType(
{
name: 'page',
title: 'Pages',
type: 'document',
fields: [
defineField({
name: 'title',
type: 'string',
}),
defineField({
name: 'slug',
type: 'slug',
options: {
source: 'title',
},
}),
defineField({
name: 'image',
type: 'image',
}),
defineField({
name: 'sections',
type: 'pageBuilder',
}),
}
)Combine all documents in index file - documents.ts
import page from '@/sanity/schemas/documents/page';
import posts from '@/sanity/schemas/documents/posts';
const documents = [page, posts];
export default documents;A function to parse all fields within given schema type - utils.ts
import documents from '@/sanity/schemas/documents';
import sections from '@/sanity/schemas/sections';
export const projectionsResolver = (type, toDeep = false) => {
const schema = [...documents, ...sections].find((d) => d.name === type);
let query = '...,';
const getFieldQuery = (field) => {
switch (field.type) {
case 'slug':
return `"${field.name}": ${field.name}.current,`;
case 'pageBuilder':
return `${field.name}[] {
${sections.map(
(section) => `_type == "${section.name}" => {
${toDeep ? '...,' : projectionsResolver(section.name)}
}`
)}
},`;
case 'image':
return `${field.name} { ..., asset-> },`;
case 'object':
return `${field.name} {
${field.fields.map((f) => getFieldQuery(f)).join('')}
},`;
case 'array':
// in case array of references
if (field.of[0].type === 'reference') {
return `${field.name}[]-> {
${field.of[0].to.map(
(to) => `_type=="${to.type}" => { ${toDeep ? '...,' : projectionsResolver(getDoc(to.type), true)} }`
)}
},`;
}
// in case array of objects
if (field.of[0].type === 'object') {
return `${field.name}[] {
...,
${
field.of[0].name
? field.of.map(
// if object has name attribute filter by type
(of) => `_type == "${of.name}" => {
${of.fields.map((f) => getFieldQuery(f)).join('')}
}`
)
: // if object has no name reference all its types
field.of.map((of) => `${of.fields.map((f) => getFieldQuery(f)).join('')}`)
}
},`;
}
// in case array of images
if (field.of[0].type === 'image') {
return `${field.name}[] {
..., asset->
},`;
}
return `${field.name}[],`;
case 'reference':
if (toDeep) {
return `${field.name}->,`;
}
return `${field.name}-> {
${field.to.map((to) => `_type=="${to.type}" => { ${projectionsResolver(to.type, true)} }`)}
},`;
default:
return `${field.name},`;
}
};
if (schema.fields) {
schema.fields.forEach((field) => {
query += getFieldQuery(field);
});
}
return query;
};When request is made, we generate the query string based on the document type - queries.ts
import { client, projectionsResolver } from '@/sanity/utils';
export async function getDocument(type, slug) {
return client.fetch(
`*[_type=="${type}" && slug.current=="${slug}"][0] {
${projectionsResolver(type)}
}`,
);
}As an example, the next request returns all dereferenced data for a homepage - page.tsx
import { getDocument } from '@/sanity/queries';
export default async function Home() {
const page = await getDocument({
type: 'page',
slug: 'homepage',
});
return <pre>{JSON.stringify(page, null, 4)}</pre>;
}Auto dereferencing for schemas is quite a topic. I want to share my solution for such an issue.
When building small or medium size projects we often do not have much data and usually, we need all fields within one query.
Let's say we have an array of objects like Page Builder for example, inside any of those objects, we could have an array of references and inside that reference a link to another document. To get this last document we would have to build a long GROQ query. So, to do this task manually would require lots of copypasting, keeping in mind that usually projects have around 3-6 documents and 10-20 page builder sections. This snippet will generate a GROQ query based on field types for a given schema, including deeper levels.
Contributor

Artem Komarov
Web dev + a bit of everything
New Zealand