npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

groq-builder

v0.9.1

Published

A **schema-aware**, strongly-typed GROQ query builder. It enables you to use **auto-completion** and **type-checking** for your GROQ queries.

Downloads

3,418

Readme

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 and price are strongly-typed based on the fields of product.
  • 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'; to from '@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;
}>();