Querying nested draft documents in Sanity using GraphQL Apollo client

16 replies
Last updated: Jul 28, 2022
Hey ๐Ÿ™‚ Quick simple question:We are using Sanity in next.js project, and we are fetching data from sanity using graphql apollo client. I'm implementing now live-preview. And there is an question.
I've implemented fetching drafts documents in qgl query by using is_draft filter:


  allPage(limit: 1, where: {_: { is_draft: $isDraft }, slug: { current: { eq: $slug } } }) {
But, i have also nested documents in Page document in form:


Page {
  slug,
  title,
  contentPlaceholder[] {
    Banner (Document)
    Section (Document)
    Cards (object)
    ...others (Documents and objects
  }
}
And if i am fetching all page data (with objects/documents in contentPaceholder field) All of these nested
objects are properly fetched (in drafts) but all of nested documents are fetched in last published version. I need to know how to get also drafts of nested documents using sanity graphql api ๐Ÿ™‚
AI Update

I understand your issue - when querying drafts in GraphQL, your top-level Page document returns the draft version, but referenced documents in the contentPlaceholder array are returning their published versions instead of drafts.

Unfortunately, GraphQL's is_draft filter doesn't automatically cascade to referenced documents. This is a known limitation of Sanity's GraphQL API. The is_draft filter only affects the root-level query, not the references within those documents.

Here are your options to solve this:

The modern approach is to use Perspectives with the GraphQL API. Instead of using the is_draft filter, add a perspective parameter to your GraphQL endpoint:

https://<projectId>.api.sanity.io/v2023-08-01/graphql/<dataset>/<tag>?perspective=previewDrafts

The previewDrafts perspective automatically overlays drafts on top of published documents throughout your entire query, including all nested references. This means:

  • If a draft exists, you get the draft
  • If no draft exists, you get the published version
  • This applies recursively to all referenced documents

In your Apollo Client setup:

const client = new ApolloClient({
  uri: `https://${projectId}.api.sanity.io/v2023-08-01/graphql/${dataset}/${tag}?perspective=${isDraft ? 'previewDrafts' : 'published'}`,
  // ... other config
});

Then simplify your query to:

allPage(limit: 1, where: { slug: { current: { eq: $slug } } }) {
  slug
  title
  contentPlaceholder {
    ... on Banner {
      # Banner fields - will be draft if available
    }
    ... on Section {
      # Section fields - will be draft if available
    }
  }
}

Note: You'll need to use GraphQL API version v2023-08-01 or later for Perspectives support.

2. Switch to GROQ (Alternative)

If Perspectives don't work for your setup, consider switching from GraphQL to GROQ queries with the HTTP API. GROQ gives you much more control over draft handling and can explicitly dereference draft versions of nested documents.

With GROQ, you can write custom logic to overlay drafts on references, which GraphQL doesn't support natively.

Why This Happens

GraphQL's is_draft filter is essentially a shorthand for filtering by _id prefix (drafts.), but it doesn't affect how references are resolved. When GraphQL resolves a reference field, it looks up the referenced document by its canonical ID (without the drafts. prefix), which always returns the published version.

Perspectives solve this by changing the query execution context at the API level, so all document resolutionsโ€”including referencesโ€”check for drafts first.

The example query is:

query GetPageBySlug($slug: String, $isDraft: Boolean = false) {
  allPage(limit: 1, where: {_: { is_draft: $isDraft }, slug: { current: { eq: $slug } } }) {
    title,
    slug {
      current
    }  
    placeholderContent {
      __typename,
      ...BannerFragment,
      ...CardsFragment,
      ...[etc]
    }
  }
}
You can use filters for those in the same way you did in allPage.
https://www.sanity.io/docs/graphql#59b6c9e5ba43
Are you set on graphQL or maybe you would like to give GROQ a try ๐Ÿ˜‰
Thank you for response
user J
. unfortunately my project team has decided to use GQL in this project. I'm trying to build proper query with filter for nested value. (But i'm newbie in both gql and sanity). Basing on documentation i've created this:
query GetPageBySlug($slug: String, $idMatch: ID = "") {
  allPage(limit: 1, where: { _id: { matches: $idMatch }, slug: { current: { eq: $slug } } }) {
    _id
    slug {
      current
    }
    title
    placeholderContent(where: {
      _id: { matches: $idMatch }
    }) {
      __typename
      ...PostFragment
      ...TestimonialsFragment
      ...BannerFragment
[...]
    }
  }
} 
But i am receiving "Unknown argument \"where\" on field \"placeholderContent\" of type \"Page\"."" error.

This is schema for PlaceholderContent:

{
      name: 'placeholderContent',
      title: 'Content Placeholder',
      type: 'array',
      of: placeholderContentTypes,
    },
where placeholderContentTypes are:

  {
    type: 'reference',
    to: [
      { type: 'post' },
      { type: 'banner' },
      { type: 'testimonials' },
      { type: 'questionAnswers' },
      { type: 'slider' },
      { type: 'logotypes' },
    ],
  },
  { type: 'sidedSection' },
  { type: 'webinars' },,
  { type: 'contactForm' },
...
];
if i can't use Where filter for array or object and documents, how can i filter these items by ID's to match "draft"?
user J
Hey ๐Ÿ™‚Small update: I got permission to use groq for this query. Here's an example of query:

    const test = await sanityClient.fetch(groq`
      *[_type == "page" && slug.current == $slug && _id in path('drafts.**')][0] {
        _id,
        title,
        slug,
        metadata,
        'placeholderContent': placeholderContent[] {
          _type == 'reference' => @->, // I want to fetch draft
          _type != 'reference' => @,
        }
      }
    `, { slug: routes.privacy_policy })
have you tested your first ever groq query in your vision tool in the studio yet?
Nope, but i've tested ^that query in our project and it fetches data well, but still i'm boxing with fetching drafts for references ๐Ÿ™‚
user J
I almost got it:
 *[_type == "page" && slug.current == $slug && _id in path($idMatch)][0] {
        _id,
        title,
        slug,
        metadata,
        'placeholderContent': placeholderContent[] {
          ...*[_id in [^._ref, "drafts." + ^._ref]][0],
          _type != 'reference' => @,
        }
      }
But there is one last issue:

There is an example "placeholderContent" response:

[
  {
    "_createdAt": "2022-07-08T07:46:30Z",
    "_id": "drafts.fa61378c-20ed-4450-b0cd-e20c432b3456",
    "_rev": "7xboex-rf2-kvy-knu-ny0ltgjvb",
    "_type": "banner",

    "slug": {
      "_type": "slug",
      "current": "/privacy-policy"
    },
    "subtext": "test",
    "title": [
      {
        "_key": "071676100bbf",
        "_type": "block",
        "children": [...]
  },
  {
    "_createdAt": "2022-07-27T14:28:58Z",
    "_id": "68a88070-2cb4-4018-928e-3fbb8e975488",
    "_rev": "xrNOqW8MxhVZd5cuCRwmtb",
    "_type": "testimonials",
    "_updatedAt": "2022-07-27T14:28:58Z",
    "items": [
      {
        "_key": "0c041f845f96",
        "_type": "testimonial",
        "author": "test",
        "date": "2022-06-29",
        "opinion": "test"
      }
    ],
    "title": "test"
  }
]
And as you can see i have a part
...*[_id in [^._ref, "drafts." + ^._ref]][0]
in my query. And - if i select [0] in array -&gt; Only first document in placeholderContent array is fetched in draft version. If i pass [1] - only second document is fetched in draft (and first document is fetched in published version). This is probably the last thing to make it fully working
what do you want to get exactly from the placholder array?
I need to know how to get also drafts of nested documents
...*[_id in ^._ref && _id in path($"drafts.**")]
this should do the trick!
I have it implemented and fully working, huge thanks
user J
!
Can i resolve all images references in query?

The asset for image looks like that:

"asset": {
      "_ref": "image-df9f14aaebefb17704af30c01d6fcd7eca624afe-3000x4542-jpg",
      "_type": "reference"
    }
But there we have types autogenerated from graphql -&gt; There is a way to "autoresolve" image references and get full data like in graphql instead of asset reference?
Jup:
//get the whole image object with all metadata etc.
'img': img.asset->
//get only the image url 
'img': img.asset->url
I would highly reccomend
user B
โ€™s Learn GROQ in 45 minutes ๐Ÿ™‚ this is how I learnt the most and with nice examples to play with!
Okay ๐Ÿ™‚ And probably my last question:This is, how my query looks like:

*[_type == "page" && slug.current == $slug && _id in path($idMatch)][0] {
        _id,
        title,
        slug,
        metadata,
        'placeholderContent': placeholderContent[] {
          ...coalesce(
            *[_id in ["drafts." + ^._ref]][0],
            *[_id in [^._ref]][0]
          ) {
            ...,
            "__typename": _type,
            "titleRaw": title,
            "textRaw": text,
            "marketingAgreementRaw": marketingAgreement,
            "dataProcessingAgreementRaw": dataProcessingAgreement,
            "contentRaw": content,
            "bodyRaw": body,
            "image": {
              "alt": image.alt,
              "asset": image.asset->ur
            }
            
          },
          _type != 'reference' => {
            ...,
            "__typename": _type,
             "titleRaw": title,
            "textRaw": text,
            "marketingAgreementRaw": marketingAgreement,
            "dataProcessingAgreementRaw": dataProcessingAgreement,
            "contentRaw": content,
            "bodyRaw": body,
              
          },
        }
      }
And the question is:
I have many different Documents in placeholderContent. And we want to comply with the types autogenerated from gql queries. And for example: all of "blockContent" have added "Raw" by gql, and we have to rename "text" to "textRaw" - but only for documents having blockContent text. For now, "textRaw" is added to all of array items, no matter if "text" exists or not. Is there a way to make it better? Or maybe rename all blockContents automatically?
Hllo again
user J
๐Ÿ™‚ Hope that you have a nice day ๐Ÿ™‚ I've got a question. I'm looking for a feature to resolve all references in groq query, no matter how deep is it. I've found this topic: https://github.com/sanity-io/GROQ/issues/21 Is it implemented? Or there's something similar maybe?
Could you please post this as a separate thread in help? it is really hard to keep up with this thread now and it is a different issue ๐Ÿ™‚I need to go now, but I will be back tomorrow.
pink panther goodbye

Sanity โ€“ Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?