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

@dangreaves/groq-query-builder

v0.19.3

Published

<h1 align="center">groq-query-builder</h1>

Downloads

889

Readme

Example

This package allows you to build GROQ queries using TypeBox schemas.

TypeBox is high-performance runtime validation package, which allows you to define "schemas" which can be runtime validated, and inferred as TypeScript types.

import { Type } from "@sinclair/typebox";
import { createClient } from "@sanity/client";

import {
  makeQueryClient,
  TypedProjection,
  InferFromSchema,
  filterProjection,
} from "@dangreaves/groq-query-builder";

// Get a Sanity client like usual.
const client = createClient({
  /** ... */
});

// Make a "safe" client which accepts a schema as the query, and will runtime validate responses.
const queryClient = makeQueryClient((...args) => client.fetch(...args));

// Define a projection schema for the product.
const ProductSchema = TypedProjection({
  _type: Type.Literal("product"),
  title: Type.String(),
  price: Type.Number(),
  description: Type.String(),
});

// Optionally infer a type from the schema.
type ProductType = InferFromSchema<typeof ProductSchema>;

// Filter the projection using a GROQ string.
const query = filterProjection(
  ProductSchema,
  `"product" == _type && "tshirt" == handle`,
);

// Send Sanity the query, and receive a fully typed and validated response.
const products = await queryClient(query);

Motivation

Sanity CMS is a content management system consisting of two parts: Sanity Studio and Content Lake.

  • Sanity Studio is a self-hostable GUI for content editors. It validates that data entered by editors matches the Sanity Schema before posting it to the Content Lake.
  • Content Lake is essentially a giant JSON database hosted by Sanity. It has no concept of a schema.

Fetching data from Sanity

To get data out of Sanity, you will likely use GROQ, a query language designed to filter, slice and project the JSON data contained in the Content Lake.

A GROQ query might look like this...

*["product" == _type && "tshirt" == handle]{title,price,description}

Using the @sanity/client package, you might send it like this...

import { createClient } from "@sanity/client";

const client = createClient({
  /** ... */
});

const product = await client.fetch(
  '*["product" == _type && "tshirt" == handle]{title,price,description}',
);

This works fine for small projects, but it has the following problems.

  1. The product variable is untyped, we don't know what shape the data will come out as.
  2. The query itself is just a string. There are heaps of things which GROQ can do, leading to sharp increase in query complexity. Managing it as a string quickly becomes painful.

Sanity TypeGen

For the lack of types, there is an offical solution: Sanity TypeGen.

  • This tool works by converting your Sanity schema into TypeScript types.
  • From your GROQ query strings, it is able to infer an expected response type.

👍 For the vast majority of users, this is enough, and I recommend you go and use it.

👎 However, the limitations of Sanity TypeGen are...

  1. The types are inferred from your Sanity schema.There is no guarantee that the data in the Content Lake actually adheres to this schema. If you create data using one schema, and then change the schema, then the underlying data will not change. Therefore, it's posible to receive data in the query response which does not match the inferred type.
  2. Complex Sanity schemas may break the tool.Some users have very complicated Sanity schemas, with recursive references, union types and other complexities. The Sanity TypeGen tool may struggle to create TypeScript types from these schemas. In these cases, it's often better to type the GROQ query, rather than the underlying schema.
  3. GROQ queries still written as a string.With this tool, you still write your GROQ queries as a string. This works for small queries, but when you get into the realm of deep expanding references, recursive objects and other complexities, managing the query as a string becomes very difficult.
  4. No runtime validation.This tool will only generate types at build time. There is no runtime validation that checks the data coming out of Sanity actually matches the type.

groqd

The groqd package addresses all of these limitations.

It allows you to write a GROQ query using a fluent interface complete with an expected response schema based on Zod. This schema is used to runtime validate the response from Sanity, and throw an error if the response does not match the expected schema. Becuase the query is constructed with a Zod schema, the response type can be inferred, and completely trusted.

Here is that same GROQ example, but with groqd...

import { q } from "groqd";
import { createClient } from "@sanity/client";

const client = createClient({
  /** ... */
});

const { query, schema } = q("*")
  .filter(`"product" == _type && "tshirt" == handle`)
  .grabOne({
    title: q.string(),
    price: q.number(),
    description: q.string(),
  });

// Product is fully typed as { title: string; price: number; description: string; }
const product = schema.parse(await client.fetch(query));

👎 The limitation for groqd is that due to it's use of Zod, it gets slower and slower the more complex your schema becomes. In my own case, when my schema in groqd ended up as a few hundred deeply nested entities, TypeScript simply could not keep up with the type generation. There is an issue (https://github.com/FormidableLabs/groqd/issues/261) which addresses this.