groq-js
A TypeScript implementation of GROQ, the query language for filtering, joining, and shaping JSON documents.
Repository Health
Technical Analysis
groq-js implements GROQ (Graph-Relational Object Queries), the JSON query language originally built for Sanity’s structured content platform, entirely in TypeScript. It parses GROQ query strings into an ESTree-inspired syntax tree, then evaluates that tree against an in-memory dataset of JSON documents, supporting filters, projections, joins via dereferencing, scoring, and dozens of built-in functions across namespaces like array, string, math, dateTime, geo, and pt (Portable Text).
Beyond evaluation, groq-js ships a standalone type evaluator that can statically infer the TypeScript-shaped result type of a GROQ query when given a Sanity schema, including narrowing through filters and joins, useful for generating typed query results without ever running the query.
What You Get
- A GROQ parser producing an ESTree-inspired syntax tree via parse()
- Synchronous and asynchronous evaluators (evaluate/evaluateSync) that run a parsed query against an in-memory dataset
- A type evaluator (typeEvaluate) that infers and narrows the TypeScript result type of a query from a Sanity schema
- An extensive standard library of GROQ functions across array, string, math, dateTime, geo, delta/diff, and Portable Text namespaces
- An unparse() utility to turn a syntax tree back into GROQ query text
Common Use Cases
- Testing Sanity Studio queries offline - run GROQ queries against fixture datasets in unit tests without hitting the Content Lake API
- Building GROQ-powered tools - editors, linters, and playgrounds that need to parse or evaluate GROQ outside of Sanity’s hosted query engine
- Generating typed query results - use typeEvaluate to derive TypeScript types for a query’s output ahead of time, for codegen or IDE tooling
- Implementing GROQ support in other datastores - reuse the parser/evaluator to run GROQ queries against non-Sanity JSON stores
Under The Hood
Architecture The library is organized as a pipeline: rawParser.js (a hand-rolled, dependency-free recursive-descent tokenizer/parser) feeds src/parser.ts, which builds a typed ESTree-inspired AST (shared/nodeTypes.ts) via createExpressionBuilder and validates custom function declarations with walk.ts; the AST is then handed to src/evaluator/evaluate.ts, which dispatches each node type to a per-type Executor (an EXECUTORS map keyed by node.type) exposing both executeSync and executeAsync paths, so the same tree can run synchronously for simple in-memory data or asynchronously when dereferencing/streaming is required. A Scope class (src/evaluator/scope.ts) threads params/source/value/context through nested and hidden scopes as traversal.ts walks arrays and projections, while a fully separate src/typeEvaluator/typeEvaluate.ts mirrors the same AST-driven dispatch pattern but statically narrows Sanity schema types instead of producing runtime values, sharing only the parser/AST layer with the evaluator. This split (parse-then-execute vs. parse-then-type-narrow) means the shared AST shape is the one place a change could ripple into both consumers, while the per-namespace function modules stay additive and isolated.
Tech Stack groq-js is authored in strict TypeScript (extending @sanity/tsconfig/strictest) targeting Node 22.12+, built to ESM-only output via tsdown, producing separate entry points for the current API, a frozen legacy v1-compatible surface, and an experimental surface. Its only runtime dependency is obug, a small debug-logging helper, keeping the parser and evaluator themselves dependency-free. Linting runs through ESLint with eslint-config-sanity and simple-import-sort, formatting via a shared Sanity Prettier config, and releases are fully automated with semantic-release driven by conventional commits, publishing through a dedicated CI & Release GitHub Actions workflow plus a separate canary-publish workflow on every push.
Code Quality Testing is extensive and runs on tap with direct TypeScript execution, spanning parser snapshot tests, evaluator behavior tests, function-level tests, and several dedicated type-evaluator test suites, plus a generator script that pulls the official cross-language GROQ test suite and turns it into local fixtures, meaning the library is validated against the canonical spec rather than only its own hand-written cases. Error handling is explicit and typed, with a dedicated syntax-error class carrying position information for parse failures, and executors raising explicit errors when a streamed value reaches a synchronous code path instead of silently returning the wrong result. TypeScript runs at its strictest configuration, CI covers all maintained Node LTS versions, and ESLint/Prettier enforce consistent style throughout.
API Design The public API is deliberately small and stable, with parse, evaluate/evaluateSync, and typeEvaluate covering the entire surface, and the README’s own quickstart mirrors the same call shape used throughout the test suite, so there is very little boilerplate to get a query running against an in-memory dataset. Namespaced exports let consumers pin to a frozen legacy contract while still reaching newer experimental functions, and typeEvaluate is notable developer-experience work: it gives tooling authors compile-time-accurate TypeScript types for a query’s result without ever running it, something most query-language implementations don’t attempt. Documentation is comparatively modest relative to the size of the function library, so discovering the full set of GROQ namespaces means reading source more than docs.