Querying an array with multiple document types in Sanity.io

25 replies
Last updated: Dec 22, 2021
Hi everyone, I've got another question for the groq masters.
I'm not sure how to query an array that contains more than one document type. My schema is as follows:

export default {
  title: "Resources",
  name: "resourcesPage",
  type: "document",
  __experimental_actions: [/*'create',*/ "update", /*'delete',*/ "publish"],
  fields: [
    mainHeading(),
    subHeading(),
    bodyTextNormal(),
    {
      title: "Resource Items",
      name: "resourceItems",
      type: "array",
      of: [
        {
          type: "reference",
          name: "youTube",
          title: "YouTube Video",
          to: {
            type: "youTube",
          },
        },
        {
          type: "reference",
          name: "blogPost",
          title: "Blog Post",
          to: {
            type: "blogPost",
          },
        },
      ],
    },
  ],
};
When I query the array:

*[_type == "resourcesPage" ][0] {
      resourceItems[]->
}
I get (cleaned up and commented):

{
  "resourceItems": [
    // First array item - a summary of a YouTube video
    {
      "_createdAt": "2021-11-06T03:52:16Z",
      "_id": "57b5415a-c03f-43b8-9f8d-e9e494740e91",
      "_rev": "65Id4bytofoxKzz3lRg5RP",
      "_type": "youTube",
      "_updatedAt": "2021-11-06T03:52:16Z",
      "description": "The description text",
      "thumbnail": "<https://i.ytimg.com/vi/k2Vyjj1j_eY/maxresdefault.jpg>",
      "title": "The title",
      "url": "<https://www.youtube.com/watch?v=xxxxxxxx>"
    },

    // Second array item - a blog post
    {
      "_createdAt": "2021-11-08T00:55:59Z",
      "_id": "e3c0f198-60cd-4613-8fdb-4735115bbaaa",
      "_rev": "yuZxWYwFNB6KJB4TNpS46C",
      "_type": "blogPost",
      "_updatedAt": "2021-11-08T02:17:31Z",
      "author": {
        "_ref": "5f618def-ec87-4dad-adfa-0a704a7a48d4",
        "_type": "reference"
      },
      "body": [
        // Portable text here
      ],
      "mainImage": {
        "_type": "image",
        "altText": "The alt text",
        "asset": {
          "_ref": "image-XXXXXXXXXX-5760x3840-jpg",
          "_type": "reference"
        }
      },
      "publishedDate": "2021-11-08T00:51:00.000Z",
      "slug": {
        "_type": "slug",
        "current": "test-blog-post"
      },
      "subtitle": "This Is A Test Blog Post",
      "tags": [
        {
          "_key": "d14e12e4c705",
          "_ref": "994dea5e-08be-4d85-9cf1-443f03191cc5",
          "_type": "reference"
        },
        {
          "_key": "45fcec2de5d0",
          "_ref": "489d2487-ffa3-48f4-a98a-b3ead02ff912",
          "_type": "reference"
        }
      ],
      "title": "Test Blog Post"
    }
  ]
}
I have no problem dealing with an array of one document type, but I can't figure out the groq syntax to add the two different projections to pull what I need from the different document types in the array.
Nov 8, 2021, 2:46 PM
Hi (Removed Name), one way would be to create two named subqueries (I don't know if that is the exact term) ...this should return two named arrays, one of videos, and one of blog posts, each with different projections.
*[_type == "resourcesPage"][0]{
  "videos": resourceItems[_type == "youTube"]->{ title, thumbnail },
  "posts": resourceItems[_type == "blogPost"]->{ title, mainImage }
}
Nov 9, 2021, 12:07 AM
Hey (Removed Name), yeah that does work. Thanks!
Any idea if it's possible to preserve the ordering in the original array?
Nov 9, 2021, 1:30 AM
I figured out a way that would work. If there's a better way I'd love to know.
I queried the
_id
for each array item. That tells me the order they should be in. I can then map over the
ids
and match it up with the correct video or blog post.
*[_type == "resourcesPage" ][0] {
  "ids": resourceItems[]-> _id,
  "videos": resourceItems[_type == "youTube"]-> { _id, title },
  "blogPosts": resourceItems[_type == "blogPost"]-> { _id, title }
}
result:

{
  "blogPosts": [
    {
      "_id": "e3c0f198-60cd-4613-8fdb-4735115bbaaa",
      "title": "Test Blog Post"
    }
  ],
  "ids": [
    "e3c0f198-60cd-4613-8fdb-4735115bbaaa",
    "65491ceb-770a-44e7-a762-85289bb4ba35",
    "57b5415a-c03f-43b8-9f8d-e9e494740e91"
  ],
  "videos": [
    {
      "_id": "65491ceb-770a-44e7-a762-85289bb4ba35",
      "title": "First Video Title"
    },
    {
      "_id": "57b5415a-c03f-43b8-9f8d-e9e494740e91",
      "title": "Second Video Title"
    }
  ]
}
Nov 9, 2021, 1:51 AM
If there's a way to structure the query so I end up with an array of
resourceItems
that are in the same order as the stored array, that would be awesome. If not, I can work with the solution above.
Thanks for the help as well,
user U
. I learn a little more about qroq every time I ask a question.
Nov 9, 2021, 1:57 AM
Ok so this gives me an array of
resourceItems
in order:
*[_type == "resourcesPage" ][0] {
  "resourceItems": resourceItems[
    _type == "youTube" || 
    _type == "blogPost"
  ]-> 
}
now if I can figure out a projection that can conditionally extract what I need from each document type in the array, that will do the trick. Is it possible to do this type of conditional logic in a projection?

It's challenging because the two document typed have different schemas.
Nov 9, 2021, 2:08 AM
Alright, so I know I'm talking to myself here but I added a projection that has all the fields I need from each document type. Some of them overlap, like
_type
and
title
. For the fields that don't overlap, they end up as
NULL
in the document that doesn't have them. Is there a way to add another projection to filter out the nulls to avoid sending extra useless bytes over the wire? As is, it'll work perfectly with my front end code, since it will render based on
_type
and won't even try to read the null values anyway. It would just be nice to avoid transmitting useless extra data.

*[_type == "resourcesPage" ][0] {
  "resourceItems": resourceItems[
    _type == "youTube" || 
    _type == "blogPost"
  ]-> {
    _type,
    url,
    title,
    description,
    thumbnail,
    subtitle,
    publishedDate,
    body,
    mainImage,
    author->{ name, avatar },
    "tags": tags[]-> tagName,
    "slug": slug.current,
  }
}
for example, here's the returned blog post with the extra fields as null:

{
  "_type": "blogPost",
  "author": {
    "avatar": { ... },
    "name": "John Doe"
  },
  "body": [ // Portable text here ],
  "description": null,
  "mainImage": { ... },
  "publishedDate": "2021-11-08T00:51:00.000Z",
  "slug": "test-blog-post",
  "subtitle": "This Is A Test Blog Post",
  "tags": [
    "Tag 1",
    "Tag 2"
  ],
  "thumbnail": null,
  "title": "Test Blog Post",
  "url": null
}
Nov 9, 2021, 2:32 AM
What if you create an order field, or use this plugin which looks to do that. Then, you could order your subqueries (each with their own projection, like in the example above) https://www.sanity.io/plugins/order-documents
Nov 9, 2021, 2:36 AM
You mean an order field in the schema? I was thinking of that, but I think it's easier for the site owner to just drag them into the order they want in the studio. Or am I misunderstanding you?
Nov 9, 2021, 2:41 AM
You mean an order field in the schema? I was thinking of that, but I think it's easier for the site owner to just drag them into the order they want in the studio. Or am I misunderstanding you?
Nov 9, 2021, 2:41 AM
ah - yes you are right - but the plugin above looks to add that drag and drop functionality
Nov 9, 2021, 2:42 AM
Oh hey, that's a pretty sweet plugin. Just looked at it.
Nov 9, 2021, 2:42 AM
It looks like that plugin would end up adding an order field to the documents. I could then concatenate the two arrays and sort on the front end.
Nov 9, 2021, 2:44 AM
I am guessing that then you could keep your projections matching what you want them to look like, something like this:
*[_type == "resourcesPage"][0]{
  "videos": resourceItems[_type == "youTube"]->{ title, thumbnail } | order (order asc),
  "posts": resourceItems[_type == "blogPost"]->{ title, mainImage, other, things } | order (order asc),
}
Nov 9, 2021, 2:44 AM
Yeah definitely. That plugin is probably the cleanest solution since it adds the order field. Thanks again (Removed Name). Learning a ton on this forum.
Nov 9, 2021, 2:47 AM
Oh wait, are you saying you don't want your posts and videos parsed out into separate groups on the front end, you just want to preserve the order but maintain different projections. In that case, yeah I guess I'd concat the two arrays above on the front end, or do the thing of ignoring the nulls.
Nov 9, 2021, 2:47 AM
Yeah that's correct. It'll be a mix of the two types. I think that plugin plus the front end concat will be the simplest because it's not returning all the null values in the JSON.
Nov 9, 2021, 2:49 AM
Cool, I hope it works out!
Nov 9, 2021, 2:50 AM
Cool, I hope it works out!
Nov 9, 2021, 2:50 AM
user U
I've learned quite a bit about groq since we were discussing this. The correct way to do it is using conditional projections. https://www.sanity.io/docs/query-cheat-sheet#text=//%20Specify%20sets%20of%20projections%20f[…]ata%20of%20referenced%20document%0A%20%20%7D%0A%7D

*[_type == "resourcesPage" ][0] {
  mainHeading,
  subHeading,
  bodyText,
  resourceItems[]-> {
    _type == "youTube" => {
      _type,
      title,
      url,
      description,
      thumbnail
    },
    _type == "blogPost" => {
      _type,
      title,
      summary,
      mainImage,
      "slug": slug.current
    }
  }
}
Dec 21, 2021, 5:56 PM
Hi
user H
oh, thanks for posting back - that is much nicer than concatenating on the front end.
Dec 21, 2021, 7:10 PM
Definitely! It's amazing how powerful groq is!
Dec 21, 2021, 7:14 PM
...a few hours later... ended up down a rabbit hole on this, and have emerged slightly enlightened but still scratching my head. My syntax (way above) no longer works (with latest API version) for the case where you do want to separate out the referenced documents into arrays by
_type
. So I went in a number of circles seeing if I could achieve it with the conditional thing you linked to. I didn't get it but I did end up with this, which does work.
*[_type == 'project']{
   title,
   "tasks": *[_type == 'task' && _id in ^.relatedItems[]._ref].title,
   "records": *[_type == 'record' && _id in ^.relatedItems[]._ref].title
} 
I am just mildly curious now to know if I am still going in a small circle here and there is another way to achieve this. Below is my schema:
project
is a document type that has an array of references to
task
and
record
document types.
export default {
    name: "project",
    title: "Project",
    type: "document",
    fields: [
        {
            title: 'Title',
            name: 'title',
            type: 'string'
        },
        {
            title: 'Tasks & Records',
            name: 'relatedItems',
            type: 'array',
            of: [
                { type: 'reference', to: [
                    {type: 'task'},
                    {type: 'record'}
                ]}
            ]
        }
    ]
}
Dec 21, 2021, 10:53 PM
I'm not sure actually. For anything remotely complex I need to use the Vision plugin in the studio to build my queries.
Dec 22, 2021, 3:38 PM
I'm not sure actually. For anything remotely complex I need to use the Vision plugin in the studio to build my queries.
Dec 22, 2021, 3:38 PM

Sanity– build remarkable experiences at scale

Sanity is a modern headless CMS that treats content as data to power your digital business. Free to get started, and pay-as-you-go on all plans.

Was this answer helpful?