Skip to content

Commit 8ede72e

Browse files
authored
filter: support multiple expressions (#387)
2 parents fd1288d + 4dc641d commit 8ede72e

11 files changed

Lines changed: 158 additions & 296 deletions

File tree

.changeset/selfish-bees-smoke.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"groqd": patch
3+
"website": patch
4+
---
5+
6+
`filterBy`: support multiple expressions. Expressions will be combined using the `||` "OR" operator.

packages/groqd/src/commands/as.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ declare module "../groq-builder" {
2828
* Use this carefully, since it's essentially "lying" to TypeScript, and there's no runtime validation.
2929
*
3030
* @example
31-
* q.star.filter("slug.current == $productSlug").as<Product>()...
31+
* q.star.filterBy("slug.current == $productSlug").as<Product>()...
3232
*
3333
*/
3434
as<TResultNew>(): GroqBuilderOfType<TResultNew, TQueryConfig, ReturnType>;
@@ -39,7 +39,7 @@ declare module "../groq-builder" {
3939
* Use this carefully, since it's essentially "lying" to TypeScript, and there's no runtime validation.
4040
*
4141
* @example
42-
* q.star.filter("slug.current == $productSlug").asType<"product">()...
42+
* q.star.filterBy("slug.current == $productSlug").asType<"product">()...
4343
*/
4444
asType<
4545
_type extends ExtractDocumentTypes<TQueryConfig["schemaTypes"]>

packages/groqd/src/commands/filter.ts

Lines changed: 0 additions & 68 deletions
This file was deleted.

packages/groqd/src/commands/filter.test.ts renamed to packages/groqd/src/commands/filterBy.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,4 +280,57 @@ describe("filterBy", () => {
280280
}>();
281281
});
282282
});
283+
284+
describe("should support multiple filters", () => {
285+
const qMultiple = q.star
286+
.filterByType("variant")
287+
.filterBy("msrp < 50", "price <= 50")
288+
.project({ name: true, msrp: true, price: true });
289+
it("should have the correct type", () => {
290+
expectTypeOf<InferResultItem<typeof qMultiple>>().toEqualTypeOf<{
291+
name: string;
292+
msrp: number;
293+
price: number;
294+
}>();
295+
});
296+
it("should generate the correct query", () => {
297+
expect(qMultiple.query).toMatchInlineSnapshot(`
298+
"*[_type == "variant"][msrp < 50 || price <= 50] {
299+
name,
300+
msrp,
301+
price
302+
}"
303+
`);
304+
});
305+
it("should execute correctly", async () => {
306+
const data = mock.generateSeedData({
307+
variants: [
308+
mock.variant({ name: "Yes 1", price: 99, msrp: 5 }),
309+
mock.variant({ name: "Yes 2", price: 5, msrp: 99 }),
310+
mock.variant({ name: "No", price: 99, msrp: 99 }),
311+
mock.variant({ name: "Yes 3", price: 5, msrp: 5 }),
312+
],
313+
});
314+
const result = await executeBuilder(qMultiple, data);
315+
expect(result).toMatchInlineSnapshot(`
316+
[
317+
{
318+
"msrp": 5,
319+
"name": "Yes 1",
320+
"price": 99,
321+
},
322+
{
323+
"msrp": 99,
324+
"name": "Yes 2",
325+
"price": 5,
326+
},
327+
{
328+
"msrp": 5,
329+
"name": "Yes 3",
330+
"price": 5,
331+
},
332+
]
333+
`);
334+
});
335+
});
283336
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { GroqBuilder } from "../groq-builder";
2+
import { Expressions } from "../types/groq-expressions";
3+
import { ConfigCreateNestedScope } from "../types/query-config";
4+
import { ResultItem } from "../types/result-types";
5+
6+
declare module "../groq-builder" {
7+
export interface GroqBuilder<TResult, TQueryConfig> {
8+
/**
9+
* This is an alias for the `filterRaw` method; please use that instead.
10+
*
11+
* Filters based on any raw filter expression.
12+
* This method is NOT type-checked, but does provide suggestions.
13+
*
14+
* @deprecated Please use `filterRaw` instead!
15+
*
16+
* @example
17+
* q.star.filterRaw("count(items[]) > 5")
18+
*
19+
* @param filterExpression - Any valid GROQ expression that can be used for filtering
20+
*/
21+
filter(
22+
...filterExpression: NonEmptyArray<
23+
Expressions.AnyConditional<ResultItem.Infer<TResult>, TQueryConfig>
24+
>
25+
): GroqBuilder<TResult, TQueryConfig>;
26+
27+
/**
28+
* Filters based on any raw filter expression(s).
29+
* This method is NOT type-checked, but does provide suggestions.
30+
*
31+
* Multiple expressions will be combined with "OR" logic.
32+
*
33+
* @example
34+
* q.star.filterRaw("count(items[]) > 5")
35+
*
36+
* @param filterExpression - Any valid GROQ expression that can be used for filtering
37+
*/
38+
filterRaw(
39+
...filterExpression: NonEmptyArray<
40+
Expressions.AnyConditional<
41+
ResultItem.Infer<TResult>,
42+
ConfigCreateNestedScope<TQueryConfig, ResultItem.Infer<TResult>>
43+
>
44+
>
45+
): GroqBuilder<TResult, TQueryConfig>;
46+
47+
/**
48+
* Filters the results based on a simple,
49+
* strongly-typed equality expression.
50+
*
51+
* Multiple calls will result in "AND" logic.
52+
* Multiple arguments will be combined with "OR" logic.
53+
*
54+
* This method is strongly-typed, but only supports common expressions.
55+
* If you'd like to filter based off more complex logic, use `filterRaw` instead.
56+
*
57+
* @example
58+
* q.star.filterByType("product")
59+
* .filterBy("image.url != null")
60+
* .filterBy('category == "food"')
61+
* .filterBy("price < 50", "msrp < 50")
62+
* .filterBy("references(^._id)")
63+
*/
64+
filterBy(
65+
...filterExpression: NonEmptyArray<
66+
Expressions.Conditional<
67+
ResultItem.Infer<TResult>,
68+
ConfigCreateNestedScope<TQueryConfig, ResultItem.Infer<TResult>>
69+
>
70+
>
71+
): GroqBuilder<TResult, TQueryConfig>;
72+
}
73+
}
74+
75+
type NonEmptyArray<T> = [T, ...T[]];
76+
77+
GroqBuilder.implement({
78+
filter(this: GroqBuilder, ...filterExpressions) {
79+
return this.filterRaw(...filterExpressions);
80+
},
81+
filterRaw(this: GroqBuilder, ...filterExpressions) {
82+
const needsWrap = this.query.endsWith("->");
83+
const self = needsWrap ? this.extend({ query: `(${this.query})` }) : this;
84+
return self.chain(`[${filterExpressions.join(" || ")}]`, "passthrough");
85+
},
86+
filterBy(this: GroqBuilder, ...filterExpressions) {
87+
return this.filterRaw(...filterExpressions);
88+
},
89+
});

packages/groqd/src/commands/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import "./subquery/selectByType";
1313
import "./as";
1414
import "./asCombined";
1515
import "./deref";
16-
import "./filter";
16+
import "./filterBy";
1717
import "./filterByType";
1818
import "./grab-deprecated";
1919
import "./notNull";

packages/groqd/src/commands/notNull.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ declare module "../groq-builder" {
1515
*
1616
* @example
1717
* q.star
18-
* .filter("slug.current == $slug")
18+
* .filterBy("slug.current == $slug")
1919
* .slice(0) // <- this return type is nullable, even though we expect there will be a match
2020
* .project({ name: z.string() })
2121
* .notNull() // <- this ensures that the results are not null

packages/groqd/src/commands/root/parameters.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe("parameters", () => {
2020
const qWithParameters = q
2121
.parameters<{ slug: string }>()
2222
.star.filterByType("variant")
23-
.filter("slug.current == $slug")
23+
.filterBy("slug.current == $slug")
2424
.project({ slug: "slug.current" });
2525

2626
it("chains should retain the parameters type", () => {

packages/groqd/src/commands/root/star.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ interface StarDefinition<TResult, TQueryConfig extends QueryConfig> {
2121
* This is how most queries start.
2222
*
2323
* @example
24-
* q.star.filter(...).project(...)
24+
* q.star.filterByType(...).project(...)
2525
*
2626
*/
2727
star: GroqBuilder<

website/docs/API/filters.md

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Selects all documents, via GROQ's `*` selector.
1111
This is how most queries start.
1212

1313
```ts
14-
q.star.filter(...).project(...)
14+
q.star.filterByType(...).project(...)
1515
```
1616

1717
## `.filterByType(type)`
@@ -36,10 +36,10 @@ q.star
3636
.filterBy('category == "shoe"')
3737
```
3838

39-
> For more complex expressions, use `.filter(expression)`:
39+
> For more complex expressions, use `.filterRaw(expression)`:
4040
4141

42-
## `.filter(expression)`
42+
## `.filterRaw(expression)`
4343

4444
Filters the query based on **any** GROQ expression.
4545

@@ -48,7 +48,7 @@ Filters the query based on **any** GROQ expression.
4848
```ts
4949
q.star
5050
.filterByType("product")
51-
.filter("price >= 50");
51+
.filterRaw("price >= 50");
5252
// Result GROQ: *[_type == "product"][price >= 50]
5353
// Result Type: Product[]
5454
```
@@ -65,25 +65,6 @@ q.star
6565
// Result Type: Product[]
6666
```
6767

68-
<!--
69-
## `.score(expression)`
70-
## `.score(expression)`
71-
72-
Used to pipe a list of results through the `score` GROQ function.
73-
74-
```ts
75-
// Fetch first 9 Pokemon's names, bubble Char* (Charmander, etc) to the top.
76-
q.star
77-
.filter("_type == 'pokemon'")
78-
.slice(0, 8)
79-
.score(`name match "char*"`)
80-
.order("_score desc")
81-
.grabOne("name", z.string());
82-
```
83-
-->
84-
85-
86-
8768
## `.slice(index)`
8869

8970
Returns a single item from the results, based on the index.

0 commit comments

Comments
 (0)