Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

groq-builder: compatibility with GroqD #248

Merged
merged 53 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
1ce12b1
groq-builder: feature(compat): export an untyped query builder
Nov 28, 2023
cbdf3e9
groq-builder: feature(compat): renamed `projection` to `grab`
Nov 28, 2023
3a2e9a2
groq-builder: feature(compat): change `executeBuilder` to use `makeSa…
Nov 29, 2023
1ca5916
groq-builder: feature(compat): added simple `validate` functions
Dec 1, 2023
5f70b04
groq-builder: feature(compat): renamed `parse` to `validate`
Dec 1, 2023
3bd7213
groq-builder: feature(compat): added validation methods to the `q` ob…
Dec 1, 2023
0de26e7
groq-builder: feature(compat): added aggregate validation errors
Dec 1, 2023
59c47d7
groq-builder: feature(compat): improved error messages
Dec 1, 2023
21067a2
groq-builder: feature(compat): improved error messages
Dec 1, 2023
0081fdf
groq-builder: feature(compat): improved error messages
Dec 2, 2023
7c34f56
groq-builder: feature(compat): simplified errors
Dec 3, 2023
e6d73ef
groq-builder: feature(compat): added more nested projection tests
Dec 4, 2023
6eac82b
groq-builder: feature(compat): added initial migration doc
Dec 4, 2023
b72c7ad
groq-builder: feature(compat): renamed `grab` to `project`
Dec 5, 2023
c989782
groq-builder: feature(compat): minor code improvements
Dec 6, 2023
5b7fa84
groq-builder: feature(compat): renamed again to `projectField` for a …
Dec 6, 2023
cf2aaac
groq-builder: feature(compat): handle projection arrays correctly
Dec 6, 2023
1dfef7a
groq-builder: feature(compat): extracted `projectField` to separate file
Dec 6, 2023
b3919f3
groq-builder: feature(compat): extracted `projectField` tests to sepa…
Dec 6, 2023
82f5a07
groq-builder: feature(compat): improved ProjectionKey and added tests
Dec 6, 2023
7f42e76
groq-builder: feature(compat): tests for ProjectionKeyValue
Dec 6, 2023
60c2d02
groq-builder: feature(compat): renamed to `compat`
Dec 6, 2023
b4f0802
groq-builder: feature(compat): added `compat` entrypoint
Dec 6, 2023
a4b52dc
groq-builder: feature(compat): moved docs to top-level
Dec 6, 2023
ed5aa38
groq-builder: feature(compat): added "beta" disclaimer
Dec 6, 2023
9e20b8a
groq-builder: feature(compat): removed `filterBy`
Dec 7, 2023
83b77b9
groq-builder: feature(compat): fixed exports
Dec 7, 2023
25f6043
groq-builder: feature(compat): added `groq-builder/validation` entryp…
Dec 8, 2023
1894509
groq-builder: feature(compat): added the untyped `filter` method
Dec 8, 2023
7f791b1
groq-builder: feature(compat): support parser functions
Dec 11, 2023
6054525
groq-builder: feature(compat): added `q.slug` method
Dec 11, 2023
af3911f
groq-builder: feature(compat): implemented `q.object` and `q.array` v…
Dec 12, 2023
a295dd8
groq-builder: feature(compat): simplified `validate.object` and `vali…
Dec 13, 2023
8f875f6
groq-builder: feature(compat): extracted primitives to separate file
Dec 13, 2023
1a6f18b
groq-builder: feature(compat): added simple `q.contentBlocks` impleme…
Dec 13, 2023
3b8332a
groq-builder: feature(compat): added more Migration docs
Dec 15, 2023
2e5edcc
groq-builder: feature(compat): renamed `projectField` to simply `field`
Dec 15, 2023
1a62f61
groq-builder: feature(compat): removed accidental self-dependency
Dec 15, 2023
fa24a73
groq-builder: feature(compat): added some jsdocs
Dec 15, 2023
517f6c1
groq-builder: feature(compat): massive update to MIGRATION guide
Dec 16, 2023
e61fcc1
groq-builder: feature(compat): added "sideEffects" configuration for …
Dec 16, 2023
0517e0e
groq-builder: feature(compat): moved all exports to the main index, r…
Dec 16, 2023
878103e
groq-builder: feature(compat): added `InferResultItem` type helper
Dec 18, 2023
65f0861
groq-builder: feature(compat): updated README with recent changes
Dec 18, 2023
771915d
groq-builder: feature(compat): made the `chain` method public
Dec 18, 2023
edcb563
groq-builder: feature(compat): fixed error message tests
Dec 18, 2023
ea4e3d7
groq-builder: feature(compat): added the `raw` command
Dec 18, 2023
977f897
groq-builder: feature(compat): improved "grab" documentation
Dec 18, 2023
640ba24
groq-builder: feature(compat): fixed @deprecated notices for grab
Dec 18, 2023
b3e0154
groq-builder: feature(compat): removed outdated `projectField` reference
Dec 18, 2023
b3bc16c
groq-builder: feature(compat): improved documentation around using Zod
Dec 18, 2023
e8eef13
groq-builder: feature(compat): unused import
Dec 18, 2023
732fd24
groq-builder: feature(compat): changeset
Dec 21, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/brown-ducks-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"groq-builder": minor
---

- Added backwards compatibility with GroqD v0.x
- Implemented validation methods like `q.string()`
- Renamed `grab -> project`, `grabOne -> field`
- Fixed build issues and deployment files
36 changes: 23 additions & 13 deletions packages/groq-builder/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
# WARNING: this package is in "beta" state; feel free to use at your own risk

> If you're looking for a feature-complete, strongly-typed Groq utility, please use [GroqD](https://formidable.com/open-source/groqd/).
> This package aims to be a successor to GroqD, but is not yet feature-complete. Please use at your own risk.

# `groq-builder`

A **schema-aware**, strongly-typed GROQ query builder.
It enables you to use **auto-completion** and **type-checking** for your GROQ queries.
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

Expand All @@ -13,23 +22,23 @@ It enables you to use **auto-completion** and **type-checking** for your GROQ qu

```ts
import { createGroqBuilder } from 'groq-builder';
import type { MySchemaConfig } from './my-schema-config';
import type { SchemaConfig } from './schema-config';
// ☝️ Note:
// Please see the "Schema Configuration" docs
// for an overview of this SchemaConfig type

const q = createGroqBuilder<MySchemaConfig>()
const q = createGroqBuilder<SchemaConfig>()

const productsQuery = (
q.star
.filterByType('products')
.order('price desc')
.slice(0, 10)
.projection(q => ({
.project(q => ({
name: true,
price: true,
slug: q.projection('slug.current'),
imageUrls: q.projection('images[]').deref().projection('url')
slug: q.field("slug.current"),
imageUrls: q.field("images[]").deref().field("url")
}))
);
```
Expand Down Expand Up @@ -69,23 +78,24 @@ type ProductsQueryResult = Array<{

## Optional Runtime Validation and Custom Parsing

You can add custom runtime validation and/or parsing logic into your queries, using the `parse` method.
You can add custom runtime validation and/or parsing logic into your queries, using the `validate` method.

The `parse` function accepts a simple function:
The `validate` function accepts a simple function:

```ts
const products = q.star.filterByType('products').projection(q => ({
const products = q.star.filterByType('products').project(q => ({
name: true,
price: true,
priceFormatted: q.projection("price").parse(price => formatCurrency(price)),
priceFormatted: q.field("price").validate(price => formatCurrency(price)),
}));
```

It is also compatible with [Zod](https://zod.dev/), and can take any Zod parser or validation logic:
```ts
const products = q.star.filterByType('products').projection(q => ({
name: true,
price: q.projection("price").parse(z.number().nonnegative()),
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()),
}));
```

Expand Down
159 changes: 159 additions & 0 deletions packages/groq-builder/docs/MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Migrating from GroqD v0.x to Groq-Builder v0.x
<!-- TODO: rename `Groq-Builder v0.x` to `groqd v1` throughout this document -->

## Minimal Migration Example

Migrating from `groqd` to `groq-builder` is straightforward, since there are few API changes.
Here's an example of a simple `groqd` query, and the **minimum** changes required to migrate to `groq-builder`:

#### Before, with `groqd`

```ts
import { q } from "groqd";

const productsQuery = q("*")
.filterByType("product")
.order('price asc')
.slice(0, 10)
.grab({
name: q.string(),
price: q.number(),
slug: ["slug.current", q.string().optional()],
image: q("image").deref(),
});
```

#### After, with `groq-builder`

```ts
import { createGroqBuilderWithValidation } from "groq-builder";
const q = createGroqBuilderWithValidation<any>(); // Using 'any' makes the query schema-unaware
carbonrobot marked this conversation as resolved.
Show resolved Hide resolved

const productsQuery = q.star
.filterByType("product")
.order('price asc')
.slice(0, 10)
.grab({
name: q.string(),
price: q.number(),
slug: ["slug.current", q.string().optional()],
image: q.field("image").deref(),
});
```

In this minimal example, we made 3 changes:
1. We created the root `q` object, binding it to a schema (or `any` to keep it schema-unaware).
2. We changed `q("*")` to `q.star`
3. We changed `q("image")` to `q.field("image")`

Keep reading for a deeper explanation of these changes.

## Step 1: Creating the root `q` object

```ts
// src/queries/q.ts
import { createGroqBuilder } from 'groq-builder';
type SchemaConfig = any;
export const q = createGroqBuilder<SchemaConfig>();
```

By creating the root `q` this way, we're able to bind it to our `SchemaConfig`.
By using `any` for now, our `q` will be schema-unaware (same as `groqd`).
Later, we'll show you how to change this to a strongly-typed schema.


## Step 2: Replacing the `q("...")` method

This is the biggest API change.
With `groqd`, the root `q` was a function that allowed any Groq string to be passed.
With `groq-builder`, all queries must be chained, using the type-safe methods.

The 2 most common changes needed will be changing all `q("*")` into `q.star`, and changing projections from `q("name")` to `q.field("name")`.

For example:
```ts
// Before:
q("*").grab({
imageUrl: q("image"),
});

// After:
q.star.grab({
imageUrl: q.field("image"),
})
```

If you do have more complex query logic inside a `q("...")` function, you should refactor to use chainable methods.
However, if you cannot refactor at this time, you can use the `raw` method instead:

## Step 3. An escape hatch: the `raw` method

Not all Groq queries can be strongly-typed. Sometimes you need an escape hatch; a way to write a query, and manually specify the result type.
The `raw` method does this by accepting any Groq string. It requires you to specify the result type. For example:

```ts
q.project({
itemCount: q.raw<number>(`count(*[_type === "item")`)
});
```
carbonrobot marked this conversation as resolved.
Show resolved Hide resolved

Ideally, you could refactor this to be strongly-typed, but you might use the escape hatch for unsupported features, or for difficult-to-type queries.


## Adding a Strongly Typed Schema

With `GroqD v0.x`, we use Zod to define the shape of our queries, and validate this shape at runtime.

With `groq-builder`, by [adding a strongly-typed Sanity schema](./README.md#schema-configuration), we can validate our queries at compile-time too. This makes our queries:

- Easier to write (provides auto-complete)
- Safer to write (all commands are type-checked, all fields are verified)
- Faster to execute (because runtime validation can be skipped)

In a projection, we can skip runtime validation by simply using `true` instead of a validation method (like `q.string()`). For example:
```ts
const productsQuery = q.star
.filterByType("product")
.project({
name: true, // 👈 'true' will bypass runtime validation
price: true, // 👈 and we still get strong result types from our schema
slug: "slug.current", // 👈 a naked projection string works too!
});
carbonrobot marked this conversation as resolved.
Show resolved Hide resolved
```

Since `q` is strongly-typed to our Sanity schema, it knows the types of the product's `name`, `price`, and `slug`, so it outputs a strongly-typed result. And assuming we trust our Sanity schema, we can skip the overhead of runtime checks.


## Additional Improvements

### Migrating from `grab -> project` and `grabOne-> field`

The `grab`, `grabOne`, `grab$`, and `grabOne$` methods still exist, but have been deprecated, and should be replaced with the `project` and `field` methods.

Sanity's documentation uses the word "projection" to refer to grabbing specific fields, so we have renamed the `grab` method to `project` (pronounced pruh-JEKT, if that helps). It also uses the phrase "naked projection" to refer to grabbing a single field, but to keep things terse, we've renamed `grabOne` to `field`. So we recommend migrating from `grab` to `project`, and from `grabOne` to `field`.

Regarding `grab$` and `grabOne$`, these 2 variants were needed to improve compatibility with Zod's `.optional()` utility. But the `project` and `field` methods work just fine with the built-in validation functions (like `q.string().optional()`).


### `q.select(...)`
This is not yet supported by `groq-builder`.

### Validation methods

Most validation methods, like `q.string()` or `q.number()`, are built-in now, and are no longer powered by Zod. These validation methods work mostly the same, but are simplified and more specialized to work with a strongly-typed schema.

Some of the built-in validation methods, like `q.object()` and `q.array()`, are much simpler than the previous Zod version.
These check that the data is an `object` or an `array`, but do NOT check the shape of the data.

Please use Zod if you need to validate an object's shape, validate items inside an Array, or you'd like more powerful runtime validation logic. For example:

```ts
import { z } from 'zod';

q.star.filterByType("user").project({
email: z.coerce.string().email().min(5),
createdAt: z.string().datetime().optional(),
});
```

carbonrobot marked this conversation as resolved.
Show resolved Hide resolved

6 changes: 4 additions & 2 deletions packages/groq-builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
"query",
"typescript"
],
"main": "dist/index.js",
"main": "./dist/index.js",
"sideEffects": [
"./dist/commands/**"
],
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": [
{
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
Expand Down
12 changes: 6 additions & 6 deletions packages/groq-builder/src/commands/deref.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ const data = mock.generateSeedData({});

describe("deref", () => {
const qProduct = q.star.filterByType("product").slice(0);
const qCategoryRef = qProduct.projection("categories[]").slice(0);
const qCategoryRef = qProduct.field("categories[]").slice(0);
const qCategory = qCategoryRef.deref();
const qVariantsRefs = qProduct.projection("variants[]");
const qVariantsRefs = qProduct.field("variants[]");
const qVariants = qVariantsRefs.deref();

it("should deref a single item", () => {
Expand All @@ -35,7 +35,7 @@ describe("deref", () => {
});

it("should be an error if the item is not a reference", () => {
const notAReference = qProduct.projection("slug");
const notAReference = qProduct.field("slug");
expectType<InferResultType<typeof notAReference>>().toStrictEqual<{
_type: "slug";
current: string;
Expand All @@ -45,15 +45,15 @@ describe("deref", () => {
type ErrorResult = InferResultType<typeof res>;
expectType<
ErrorResult["error"]
>().toStrictEqual<"Expected the object to be a reference type">();
>().toStrictEqual<"⛔️ Expected the object to be a reference type ⛔️">();
});

it("should execute correctly (single)", async () => {
const results = await executeBuilder(data.datalake, qCategory);
const results = await executeBuilder(qCategory, data.datalake);
expect(results).toEqual(data.categories[0]);
});
it("should execute correctly (multiple)", async () => {
const results = await executeBuilder(data.datalake, qVariants);
const results = await executeBuilder(qVariants, data.datalake);
expect(results).toEqual(data.variants);
});
});
2 changes: 1 addition & 1 deletion packages/groq-builder/src/commands/deref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ declare module "../groq-builder" {
}

GroqBuilder.implement({
deref(this: GroqBuilder<any, RootConfig>): any {
deref(this: GroqBuilder<any, RootConfig>) {
return this.chain("->", null);
},
});
32 changes: 4 additions & 28 deletions packages/groq-builder/src/commands/filter.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,14 @@
import { GroqBuilder } from "../groq-builder";
import { StringKeys } from "../types/utils";
import { ResultItem, ResultOverride } from "../types/result-types";
import { RootConfig } from "../types/schema-types";

declare module "../groq-builder" {
export interface GroqBuilder<TResult, TRootConfig> {
filterBy<
TKey extends StringKeys<keyof ResultItem<TResult>>,
TValue extends Extract<ResultItem<TResult>[TKey], string>
>(
filterString: `${TKey} == "${TValue}"`
): GroqBuilder<
ResultOverride<
TResult,
Extract<ResultItem<TResult>, { [P in TKey]: TValue }>
>,
TRootConfig
>;

filterByType<
TType extends Extract<ResultItem<TResult>, { _type: string }>["_type"]
>(
type: TType
): GroqBuilder<
ResultOverride<TResult, Extract<ResultItem<TResult>, { _type: TType }>>,
TRootConfig
>;
filter(filterExpression: string): GroqBuilder<TResult, TRootConfig>;
}
}

GroqBuilder.implement({
filterBy(this: GroqBuilder, filterString) {
return this.chain(`[${filterString}]`, null);
},
filterByType(this: GroqBuilder, type) {
return this.chain(`[_type == "${type}"]`, null);
filter(this: GroqBuilder<any, RootConfig>, filterExpression) {
return this.chain(`[${filterExpression}]`, null);
},
});
Loading
Loading