-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
groq-builder: Added
q.fragment
implementation (#250)
* feature(fragment): added `q.fragment` implementation * feature(fragment): added tests for fragment queries * feature(fragment): ensure we export `Fragment` and `InferFragmentType` types * feature(fragment): added docs * feature(validation): fixed broken import --------- Co-authored-by: scottrippey <scott.william.rippey@gmail.com>
- Loading branch information
1 parent
175913b
commit 2b53b9f
Showing
9 changed files
with
309 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"groq-builder": patch | ||
--- | ||
|
||
Added support for Fragments via `q.fragment` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
# Fragments | ||
|
||
A "fragment" is a reusable projection. It is just a `groq-builder` concept, not a part of the Groq language. | ||
|
||
Fragments can be reused across multiple queries, and they can be easily extended or combined. | ||
|
||
## Defining a Fragment | ||
|
||
To create a fragment, you specify the "input type" for the fragment, then define the projection. For example: | ||
|
||
```ts | ||
const productFragment = q.fragment<SanitySchema.Product>().project({ | ||
name: q.string(), | ||
price: q.number(), | ||
slug: ["slug.current", q.string()], | ||
}); | ||
``` | ||
|
||
You can easily extract a type from this fragment too: | ||
|
||
```ts | ||
type ProductFragment = InferFragmentType<typeof productFragment>; | ||
``` | ||
|
||
## Using a Fragment | ||
|
||
To use this fragment in a query, you can pass it directly to the `.project` method: | ||
|
||
```ts | ||
const productQuery = q.star.filterByType("product").project(productFragment); | ||
``` | ||
|
||
You can also spread the fragment into a projection: | ||
```ts | ||
const productQuery = q.star.filterByType("product").project({ | ||
...productFragment, | ||
description: q.string(), | ||
images: "images[]", | ||
}); | ||
``` | ||
|
||
## Extending and combining Fragments | ||
|
||
Fragments are just plain objects, with extra type information. This makes it easy to extend and combine your fragments. | ||
|
||
To extend a fragment: | ||
|
||
```ts | ||
const productDetailsFragment = q.fragment<SanitySchema.Product>().project({ | ||
...productFragment, | ||
description: q.string(), | ||
msrp: q.number(), | ||
slug: q.slug("slug"), | ||
}); | ||
``` | ||
|
||
To combine fragments: | ||
|
||
```ts | ||
const productDetailsFragment = q.fragment<SanitySchema.Product>().project({ | ||
...productFragment, | ||
...productDescriptionFragment, | ||
...productImagesFragment, | ||
}); | ||
``` | ||
|
||
To infer the "result type" of any of these fragments, use `InferFragmentType`: | ||
|
||
```ts | ||
import { InferFragmentType } from './public-types'; | ||
|
||
type ProductFragment = InferFragmentType<typeof productFragment>; | ||
type ProductDetailsFragment = InferFragmentType<typeof productDetailsFragment>; | ||
type ProductDescriptionFragment = InferFragmentType<typeof productDescriptionFragment>; | ||
type ProductImagesFragment = InferFragmentType<typeof productImagesFragment>; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import { describe, it, expect } from "vitest"; | ||
import { SanitySchema, SchemaConfig } from "../tests/schemas/nextjs-sanity-fe"; | ||
import { expectType } from "../tests/expectType"; | ||
import { InferFragmentType, InferResultType } from "../types/public-types"; | ||
import { createGroqBuilder } from "../index"; | ||
import { TypeMismatchError } from "../types/utils"; | ||
|
||
const q = createGroqBuilder<SchemaConfig>(); | ||
|
||
describe("fragment", () => { | ||
// define a fragment: | ||
const variantFragment = q.fragment<SanitySchema.Variant>().project({ | ||
name: true, | ||
price: true, | ||
slug: "slug.current", | ||
}); | ||
type VariantFragment = InferFragmentType<typeof variantFragment>; | ||
|
||
it("should have the correct type", () => { | ||
expectType<VariantFragment>().toStrictEqual<{ | ||
name: string; | ||
price: number; | ||
slug: string; | ||
}>(); | ||
}); | ||
|
||
const productFrag = q.fragment<SanitySchema.Product>().project((qP) => ({ | ||
name: true, | ||
slug: "slug.current", | ||
variants: qP | ||
.field("variants[]") | ||
.deref() | ||
.project({ | ||
...variantFragment, | ||
msrp: true, | ||
}), | ||
})); | ||
type ProductFragment = InferFragmentType<typeof productFrag>; | ||
|
||
it("should have the correct types", () => { | ||
expectType<ProductFragment>().toEqual<{ | ||
name: string; | ||
slug: string; | ||
variants: null | Array<{ | ||
name: string; | ||
price: number; | ||
slug: string; | ||
msrp: number; | ||
}>; | ||
}>(); | ||
}); | ||
|
||
it("fragments can be used in a query", () => { | ||
const qVariants = q.star.filterByType("variant").project(variantFragment); | ||
expectType<InferResultType<typeof qVariants>>().toStrictEqual< | ||
Array<VariantFragment> | ||
>(); | ||
|
||
expect(qVariants.query).toMatchInlineSnapshot( | ||
'"*[_type == \\"variant\\"] { name, price, \\"slug\\": slug.current }"' | ||
); | ||
}); | ||
it("fragments can be spread in a query", () => { | ||
const qVariantsPlus = q.star.filterByType("variant").project({ | ||
...variantFragment, | ||
msrp: true, | ||
}); | ||
expectType<InferResultType<typeof qVariantsPlus>>().toStrictEqual< | ||
Array<{ name: string; price: number; slug: string; msrp: number }> | ||
>(); | ||
|
||
expect(qVariantsPlus.query).toMatchInlineSnapshot( | ||
'"*[_type == \\"variant\\"] { name, price, \\"slug\\": slug.current, msrp }"' | ||
); | ||
}); | ||
|
||
it("should have errors if the variant is used incorrectly", () => { | ||
const qInvalid = q.star.filterByType("product").project(variantFragment); | ||
expectType< | ||
InferResultType<typeof qInvalid>[number]["price"] | ||
>().toStrictEqual< | ||
TypeMismatchError<{ | ||
error: "⛔️ 'true' can only be used for known properties ⛔️"; | ||
expected: keyof SanitySchema.Product; | ||
actual: "price"; | ||
}> | ||
>(); | ||
}); | ||
|
||
it("can be composed", () => { | ||
const idFrag = q.fragment<SanitySchema.Variant>().project({ id: true }); | ||
const variantDetailsFrag = q.fragment<SanitySchema.Variant>().project({ | ||
...idFrag, | ||
...variantFragment, | ||
msrp: true, | ||
}); | ||
|
||
type VariantDetails = InferFragmentType<typeof variantDetailsFrag>; | ||
|
||
expectType<VariantDetails>().toStrictEqual<{ | ||
slug: string; | ||
name: string; | ||
msrp: number; | ||
price: number; | ||
id: string | undefined; | ||
}>(); | ||
}); | ||
|
||
it("can be used to query multiple types", () => { | ||
const commonFrag = q | ||
.fragment< | ||
SanitySchema.Product | SanitySchema.Variant | SanitySchema.Category | ||
>() | ||
.project({ | ||
_type: true, | ||
_id: true, | ||
name: true, | ||
}); | ||
type CommonFrag = InferFragmentType<typeof commonFrag>; | ||
expectType<CommonFrag>().toStrictEqual<{ | ||
_type: "product" | "variant" | "category"; | ||
_id: string; | ||
name: string; | ||
}>(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { GroqBuilder } from "../groq-builder"; | ||
import { ProjectionMap } from "./projection-types"; | ||
import { Fragment } from "../types/public-types"; | ||
|
||
declare module "../groq-builder" { | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
export interface GroqBuilder<TResult, TRootConfig> { | ||
fragment<TFragmentInput>(): { | ||
project<TProjectionMap extends ProjectionMap<TFragmentInput>>( | ||
projectionMap: | ||
| TProjectionMap | ||
| ((q: GroqBuilder<TFragmentInput, TRootConfig>) => TProjectionMap) | ||
): Fragment<TProjectionMap, TFragmentInput>; | ||
}; | ||
} | ||
} | ||
|
||
GroqBuilder.implement({ | ||
fragment(this: GroqBuilder<any>) { | ||
return { | ||
project: (projectionMap) => { | ||
if (typeof projectionMap === "function") { | ||
projectionMap = projectionMap(this); | ||
} | ||
return projectionMap; | ||
}, | ||
}; | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters