groq-builder v0.9.1
groq-builder
A schema-aware, strongly-typed GROQ query builder.
It enables you to use auto-completion and type-checking for your GROQ queries.
In case you're wondering "What is GROQ?"
From https://www.sanity.io/docs/groq:
"GROQ is Sanity's open-source query language. It's a powerful and intuitive language that's easy to learn. With GROQ you can describe exactly what information your application needs, join information from several sets of documents, and stitch together a very specific response with only the exact fields you need."
Features
- Schema-aware - Use your
sanity.config.ts
for auto-completion and type-checking - Strongly-typed - Query results are strongly typed, based on the schema
- Optional runtime validation - Validate or transform query results at run-time, with broad or granular levels
Example
import { createGroqBuilder } from 'groq-builder';
import type { SchemaConfig } from './schema-config';
// ☝️ Note:
// Please see the "Schema Configuration" docs
// for an overview of this SchemaConfig type
const q = createGroqBuilder<SchemaConfig>()
const productsQuery = (
q.star
.filterByType('products')
.order('price desc')
.slice(0, 10)
.project(q => ({
name: true,
price: true,
slug: q.field("slug.current"),
imageUrls: q.field("images[]").deref().field("url")
}))
);
In the above query, ALL fields are strongly-typed, according to the Sanity schema defined in sanity.config.ts
!
- All strings like
'products'
,'price desc'
, and'images[]'
are strongly-typed, based on the matching field definitions. - In the projection,
name
andprice
are strongly-typed based on the fields ofproduct
. - In the projection, sub-queries are strongly typed too.
This example generates the following GROQ query:
*[_type == "products"] | order(price desc)[0...10] {
name,
price,
"slug": slug.current,
"imageUrls": images[]->url
}
Query Result Types
The above productsQuery
example generates the following results type:
import type { InferResultType } from 'groq-builder';
type ProductsQueryResult = InferResultType<typeof productsQuery>;
// 👆 Evaluates to the following:
type ProductsQueryResult = Array<{
name: string,
price: number,
slug: string,
imageUrls: Array<string>,
}>;
Optional Runtime Validation and Custom Parsing
You can add custom runtime validation and/or parsing logic into your queries, using the validate
method.
The validate
function accepts a simple function:
const products = q.star.filterByType('products').project(q => ({
name: true,
price: true,
priceFormatted: q.field("price").validate(price => formatCurrency(price)),
}));
It is also compatible with Zod, and can take any Zod parser or validation logic:
const products = q.star.filterByType('products').project(q => ({
name: z.string(),
slug: ["slug.current", z.string().optional()],
price: q.field("price").validate(z.number().nonnegative()),
}));
Schema Configuration
The entry-point to this library is the createGroqBuilder<SchemaConfig>()
function, which returns a strongly-typed q
object. You must supply the SchemaConfig
type parameter, which lists all document types from your Sanity Schema.
There are 2 approaches for creating this Schema. You can specify the Schema manually, or you can auto-generate the types based on your sanity.config.ts
.
Manually typing your Sanity Schema
The simplest way to create a Sanity Schema is to manually specify the document types. Here's a working example:
import { createGroqBuilder } from './index';
declare const references: unique symbol;
type Product = {
_type: "product";
_id: string;
name: string;
price: number;
images: Array<{ width: number; height: number; url: string; }>;
category: { _type: "reference"; _ref: string; [references]: "category"; };
}
type Category = {
_type: "category";
_id: string;
name: string;
products: Array<{ _type: "reference"; _ref: string; [references]: "product"; }>;
}
export type SchemaConfig = {
documentTypes: Product | Category;
referenceSymbol: typeof references;
}
export const q = createGroqBuilder<SchemaConfig>();
The only complexity is how references are handled. In the Sanity data, the reference
object doesn't say what kind of document it's referencing. We have to add this type information, using a unique symbol. So above, we added [references]: "category"
to capture the reference type. This information is used by the .deref()
method to ensure we follow references correctly.
Automatically generating your Sanity Schema
Fortunately, there is a way to automatically generate the Sanity Schema, using the Sanity configuration itself (sanity.config.ts
). This workflow has 2 steps: inferring types from the config, then copying the compiled types to your application.
Augment your sanity.config.ts
to infer the types
In the repo with your Sanity configuration (sanity.config.ts
), use the @sanity-typed/types
library to augment your configuration code.
This is pretty easy, and involves:
- Changing your imports
from 'sanity';
tofrom '@sanity-typed/types'
- Adding
as const
in a few places (according to the docs)
Then, in your schema.config.ts
, you infer all document types by adding:
import { InferSchemaValues } from '@sanity-typed/types';
export type SanityValues = InferSchemaValues<typeof config>;
Compile the types and copy to your application
Now that you've got the SanityValues
type, you'll need to compile the types, and copy them to your application (where you're using groq-builder
).
Normally you could use tsc
to compile the types, and copy them over. However, there is a far better approach: use the ts-simplify
CLI tool to compile and simplify the types.
From your Sanity repo, run:
npx ts-simplify ./sanity.config.ts ./sanity-schema.ts
This generates a ./sanity-schema.ts
file that has no dependencies, just the Sanity types!
Move this file to your application (where you're using groq-builder
), and finally, glue it all together like so:
./q.ts
import { createGroqBuilder, ExtractDocumentTypes } from 'groq-builder';
import { referenced, SanityValues } from './sanity-schema'; // This is the generated file
export const q = createGroqBuilder<{
documentTypes: ExtractDocumentTypes<SanityValues>;
referenceSymbol: typeof referenced;
}>();