Skip to content

Commit

Permalink
groq-builder: strongly-typed parameters (variables) and conditionals (#…
Browse files Browse the repository at this point in the history
…263)

* feature(variables): renamed `TRootConfig` to `TQueryConfig`

* feature(variables): added variables command

* feature(variables): improved `makeSafeQueryRunner` to handle variables and other custom fields

* feature(variables): added changeset

* feature(variables): added conditional statement auto-complete suggestions

* feature(variables): improved auto-complete suggestions

* feature(variables): added strongly-typed `filterBy`

* feature(variables): fixed minor issue with InferResultItem / InferVariablesType

* feature(variables): use uppercase keys for conditionals

* feature(variables): updated conditionals to accept IGroqBuilder

* feature(variables): allow paths within conditionals

* feature(variables): extracted `createGroqBuilder` into separate file

* feature(variables): renamed 'variables' to 'parameters' for better consistency  with Groq

* feature(variables): renamed 'variables' to 'parameters' for better consistency  with Groq

* feature(variables): infer raw type from parser

* feature(variables): moved createGroqBuilder back into index, since the import order was important

* feature(variables): added jsdocs

* feature(variables): updated lockfile

* feature(variables): support deeply-nested parameters

* feature(variables): improved docs for makeSafeQueryRunner

* feature(variables): improved code for expression suggestions

* feature(variables): improved changeset

---------

Co-authored-by: scottrippey <[email protected]>
  • Loading branch information
scottrippey and scottrippey authored Feb 8, 2024
1 parent d273147 commit 1a5eb69
Show file tree
Hide file tree
Showing 53 changed files with 1,191 additions and 274 deletions.
12 changes: 12 additions & 0 deletions .changeset/cyan-hats-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"groq-builder": minor
---

Added support for parameters.

Added `filterBy` for strongly-typed filtering.

Added auto-complete help for conditional statements and filters.

Changed how parameters are passed to `makeSafeQueryRunner`.

3 changes: 2 additions & 1 deletion packages/groq-builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
],
"main": "./dist/index.js",
"sideEffects": [
"./dist/commands/**"
"./dist/commands/**",
"./dist/groq-builder"
],
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
Expand Down
32 changes: 14 additions & 18 deletions packages/groq-builder/src/commands/conditional-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,23 @@ import {
ProjectionMapOrCallback,
} from "./projection-types";
import { Empty, IntersectionOfValues, Simplify, ValueOf } from "../types/utils";
import { ExtractTypeNames, RootConfig } from "../types/schema-types";
import { ExtractTypeNames, QueryConfig } from "../types/schema-types";
import { GroqBuilder } from "../groq-builder";
import { IGroqBuilder, InferResultType } from "../types/public-types";
import { Expressions } from "../types/groq-expressions";

export type ConditionalProjectionMap<
TResultItem,
TRootConfig extends RootConfig
> = {
[Condition: ConditionalExpression<TResultItem>]:
TQueryConfig extends QueryConfig
> = Partial<
Record<
Expressions.AnyConditional<TResultItem, TQueryConfig>,
| ProjectionMap<TResultItem>
| ((
q: GroqBuilder<TResultItem, TRootConfig>
) => ProjectionMap<TResultItem>);
};

/**
* For now, none of our "conditions" are strongly-typed,
* so we'll just use "string":
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export type ConditionalExpression<TResultItem> = string;
q: GroqBuilder<TResultItem, TQueryConfig>
) => ProjectionMap<TResultItem>)
>
>;

export type ExtractConditionalProjectionResults<
TResultItem,
Expand Down Expand Up @@ -52,11 +48,11 @@ export type ExtractConditionalProjectionTypes<TProjectionMap> = Simplify<

export type ConditionalByTypeProjectionMap<
TResultItem,
TRootConfig extends RootConfig
TQueryConfig extends QueryConfig
> = {
[_type in ExtractTypeNames<TResultItem>]?: ProjectionMapOrCallback<
Extract<TResultItem, { _type: _type }>,
TRootConfig
TQueryConfig
>;
};

Expand All @@ -82,9 +78,9 @@ export type ExtractConditionalByTypeProjectionResults<
}>
>;

export type ConditionalKey<TKey extends string> = `[Conditional] ${TKey}`;
export type ConditionalKey<TKey extends string> = `[CONDITIONAL] ${TKey}`;
export function isConditional(key: string): key is ConditionalKey<string> {
return key.startsWith("[Conditional] ");
return key.startsWith("[CONDITIONAL] ");
}
export type SpreadableConditionals<
TKey extends string,
Expand Down
10 changes: 5 additions & 5 deletions packages/groq-builder/src/commands/conditional.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ExtractConditionalProjectionTypes } from "./conditional-types";
import { Empty, Simplify } from "../types/utils";

const q = createGroqBuilder<SchemaConfig>({ indent: " " });
const qBase = q.star.filterByType("variant");
const qVariants = q.star.filterByType("variant");

describe("conditional", () => {
describe("by itself", () => {
Expand All @@ -36,14 +36,14 @@ describe("conditional", () => {
});
it("should return a spreadable object", () => {
expect(conditionalResult).toMatchObject({
"[Conditional] [$]": expect.any(GroqBuilder),
"[CONDITIONAL] [KEY]": expect.any(GroqBuilder),
});
});
});

const qAll = qBase.project((qA) => ({
const qAll = qVariants.project((qV) => ({
name: true,
...qA.conditional({
...qV.conditional({
"price == msrp": {
onSale: q.value(false),
},
Expand Down Expand Up @@ -142,7 +142,7 @@ describe("conditional", () => {
"another == condition1": { foo: q.value("FOO") },
"another == condition2": { bar: q.value("BAR") },
},
{ key: "unique-key" }
{ key: "[UNIQUE-KEY]" }
),
}));

Expand Down
14 changes: 8 additions & 6 deletions packages/groq-builder/src/commands/conditional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import { ProjectionMap } from "./projection-types";

declare module "../groq-builder" {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export interface GroqBuilder<TResult, TRootConfig> {
export interface GroqBuilder<TResult, TQueryConfig> {
conditional<
TConditionalProjections extends ConditionalProjectionMap<
ResultItem.Infer<TResult>,
TRootConfig
TQueryConfig
>,
TKey extends string = "[$]",
TKey extends string = typeof DEFAULT_KEY,
TIsExhaustive extends boolean = false
>(
conditionalProjections: TConditionalProjections,
Expand All @@ -31,6 +31,8 @@ declare module "../groq-builder" {
}
}

const DEFAULT_KEY = "[KEY]" as const;

GroqBuilder.implement({
conditional<
TCP extends object,
Expand Down Expand Up @@ -58,15 +60,15 @@ GroqBuilder.implement({
.join(`,${newLine}`);

const parsers = allConditionalProjections
.map((q) => q.internal.parser)
.map((q) => q.parser)
.filter(notNull);
const conditionalParser = !parsers.length
? null
: createConditionalParserUnion(parsers, config?.isExhaustive ?? false);

const conditionalQuery = root.chain(query, conditionalParser);
const key = config?.key || ("[$]" as TKey);
const conditionalKey: ConditionalKey<TKey> = `[Conditional] ${key}`;
const key = config?.key || (DEFAULT_KEY as TKey);
const conditionalKey: ConditionalKey<TKey> = `[CONDITIONAL] ${key}`;
return {
[conditionalKey]: conditionalQuery,
} as any;
Expand Down
6 changes: 3 additions & 3 deletions packages/groq-builder/src/commands/conditionalByType.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ describe("conditionalByType", () => {
it('should have a "spreadable" signature', () => {
expectTypeOf<SimplifyDeep<typeof conditionalByType>>().toEqualTypeOf<
SimplifyDeep<{
"[Conditional] [ByType]": IGroqBuilder<ExpectedConditionalUnion>;
"[CONDITIONAL] [BY_TYPE]": IGroqBuilder<ExpectedConditionalUnion>;
}>
>();

expect(conditionalByType).toMatchObject({
"[Conditional] [ByType]": expect.any(GroqBuilder),
"[CONDITIONAL] [BY_TYPE]": expect.any(GroqBuilder),
});
});

Expand Down Expand Up @@ -183,7 +183,7 @@ describe("conditionalByType", () => {
});

it("should execute correctly", async () => {
const res = await executeBuilder(qAll, data.datalake);
const res = await executeBuilder(qAll, data);

expect(res.find((item) => item._type === "category"))
.toMatchInlineSnapshot(`
Expand Down
15 changes: 8 additions & 7 deletions packages/groq-builder/src/commands/conditionalByType.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GroqBuilder } from "../groq-builder";
import { ExtractTypeNames, RootConfig } from "../types/schema-types";
import { ExtractTypeNames, QueryConfig } from "../types/schema-types";
import { ResultItem } from "../types/result-types";
import {
ExtractConditionalByTypeProjectionResults,
Expand All @@ -10,13 +10,13 @@ import {
import { ProjectionMap } from "./projection-types";

declare module "../groq-builder" {
export interface GroqBuilder<TResult, TRootConfig> {
export interface GroqBuilder<TResult, TQueryConfig> {
conditionalByType<
TConditionalProjections extends ConditionalByTypeProjectionMap<
ResultItem.Infer<TResult>,
TRootConfig
TQueryConfig
>,
TKey extends string = "[ByType]",
TKey extends string = typeof DEFAULT_KEY,
/**
* Did we supply a condition for all possible _type values?
*/
Expand All @@ -35,14 +35,15 @@ declare module "../groq-builder" {
>;
}
}
const DEFAULT_KEY = "[BY_TYPE]" as const;

GroqBuilder.implement({
conditionalByType<
TConditionalProjections extends object,
TKey extends string,
TIsExhaustive extends boolean
>(
this: GroqBuilder<any, RootConfig>,
this: GroqBuilder<any, QueryConfig>,
conditionalProjections: TConditionalProjections,
config?: Partial<ConditionalConfig<TKey, TIsExhaustive>>
) {
Expand Down Expand Up @@ -80,8 +81,8 @@ GroqBuilder.implement({
};

const conditionalQuery = this.root.chain(query, conditionalParser);
const key: TKey = config?.key || ("[ByType]" as TKey);
const conditionalKey: ConditionalKey<TKey> = `[Conditional] ${key}`;
const key: TKey = config?.key || (DEFAULT_KEY as TKey);
const conditionalKey: ConditionalKey<TKey> = `[CONDITIONAL] ${key}`;
return {
_type: true, // Ensure we request the `_type` parameter
[conditionalKey]: conditionalQuery,
Expand Down
7 changes: 4 additions & 3 deletions packages/groq-builder/src/commands/deref.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import { InferResultType } from "../types/public-types";
import { createGroqBuilder } from "../index";
import { executeBuilder } from "../tests/mocks/executeQuery";
import { mock } from "../tests/mocks/nextjs-sanity-fe-mocks";
import { SanitySchema, SchemaConfig } from "../tests/schemas/nextjs-sanity-fe";

import { createGroqBuilder } from "../index";

const q = createGroqBuilder<SchemaConfig>();
const data = mock.generateSeedData({});

Expand Down Expand Up @@ -48,11 +49,11 @@ describe("deref", () => {
});

it("should execute correctly (single)", async () => {
const results = await executeBuilder(qCategory, data.datalake);
const results = await executeBuilder(qCategory, data);
expect(results).toEqual(data.categories[0]);
});
it("should execute correctly (multiple)", async () => {
const results = await executeBuilder(qVariants, data.datalake);
const results = await executeBuilder(qVariants, data);
expect(results).toEqual(data.variants);
});
});
10 changes: 5 additions & 5 deletions packages/groq-builder/src/commands/deref.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { GroqBuilder } from "../groq-builder";
import { ExtractRefType, RootConfig } from "../types/schema-types";
import { ExtractRefType, QueryConfig } from "../types/schema-types";
import { ResultItem } from "../types/result-types";

declare module "../groq-builder" {
export interface GroqBuilder<TResult, TRootConfig> {
export interface GroqBuilder<TResult, TQueryConfig> {
deref<
TReferencedType = ExtractRefType<ResultItem.Infer<TResult>, TRootConfig>
TReferencedType = ExtractRefType<ResultItem.Infer<TResult>, TQueryConfig>
>(): GroqBuilder<
ResultItem.Override<TResult, TReferencedType>,
TRootConfig
TQueryConfig
>;
}
}

GroqBuilder.implement({
deref(this: GroqBuilder<any, RootConfig>) {
deref(this: GroqBuilder<any, QueryConfig>) {
return this.chain("->");
},
});
Loading

0 comments on commit 1a5eb69

Please sign in to comment.