Skip to content

Commit

Permalink
GroqD 1.0 Prep - Updated Arcade Samples (#304)
Browse files Browse the repository at this point in the history
* feat(errors): extracted projection validation helpers

* feat(errors): export `ValidationErrors` class

* feat(errors): ensure we handle arrays with `field` projections

* feat(errors): added jsdocs for ValidationErrors

* docs: updated playground examples to new syntax

* docs: updated playground to show error messages

* feat(errors): use explicit output types for better compatibility

* feat(errors): switch error `path` to array

* docs(playground): format paths correctly

* test(conditionals): added test for `isExhaustive`

* docs(arcade): pretty-print queries

* docs(arcade): updated conditional example

* node engine (#303)

* docs(arcade): improved todo-list data

* docs(arcade): added "TODO" samples to Arcade

* feat(validation): simplified public api

* docs(arcade): consolidated pathId logic

* test(conditional): fixed isExhaustive test

* docs: eslint ignore compiled files

* chore: added "checks" script

* feat: renamed default method to `createGroqBuilderLite`

* docs: updated broken links

---------

Co-authored-by: scottrippey <[email protected]>
Co-authored-by: Nathan Kluth <[email protected]>
  • Loading branch information
3 people authored Dec 27, 2024
1 parent 112d8d4 commit 4d415ce
Show file tree
Hide file tree
Showing 47 changed files with 500 additions and 263 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@
"**/node_modules/**/*",
"**/public/**/*",
"**/.docusaurus/**/*",
"website/src/arcade/**/*.d.ts"
"**/*.d.ts"
]
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,8 @@
"prettier": "^2.8.4",
"typescript": "^4.9.5",
"monaco-editor": "^0.50.0"
},
"engines": {
"node": ">=18.0.0"
}
}
2 changes: 2 additions & 0 deletions packages/groq-builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
"dist"
],
"scripts": {
"checks": "pnpm run lint && pnpm run typecheck && pnpm run test",
"lint": "eslint src --quiet",
"test:watch": "vitest",
"test": "vitest run",
"typecheck": "tsc --noEmit",
Expand Down
54 changes: 45 additions & 9 deletions packages/groq-builder/src/commands/conditional.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import {
createGroqBuilder,
GroqBuilder,
InferResultItem,
InferResultType,
} from "../index";
import { SchemaConfig } from "../tests/schemas/nextjs-sanity-fe";
import { GroqBuilder, InferResultItem, InferResultType } from "../index";
import { q } from "../tests/schemas/nextjs-sanity-fe";
import { ExtractConditionalProjectionTypes } from "./conditional-types";
import { Empty, Simplify } from "../types/utils";

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

describe("conditional", () => {
describe("by itself", () => {
const conditionalResult = q.star.filterByType("variant").conditional({
const conditionalResult = q.asType<"variant">().conditional({
"price == msrp": {
onSale: q.value(false),
},
Expand Down Expand Up @@ -199,4 +193,46 @@ describe("conditional", () => {
});
});
});

describe("isExhaustive", () => {
const exhaustiveQuery = qVariants.project((sub) => ({
name: true,
...sub.conditional(
{
"price >= msrp": {
onSale: q.value(false),
},
"price < msrp": {
onSale: q.value(true),
price: true,
msrp: true,
},
},
// Use this parameter when you know that
// at least one condition must be true:
{ isExhaustive: true }
),
}));

it("should have the correct result type", () => {
type Result = InferResultType<typeof exhaustiveQuery>;
type ExpectedResultItem =
| {
name: string;
onSale: false;
}
| {
name: string;
onSale: true;
price: number;
msrp: number;
};
expectTypeOf<Result>().toEqualTypeOf<Array<ExpectedResultItem>>();

// The "isExhaustive" parameter ensures we don't
// include the "empty" types:
type NonExhaustiveResult = { name: string } & ExpectedResultItem;
expectTypeOf<Result>().not.toEqualTypeOf<Array<NonExhaustiveResult>>();
});
});
});
4 changes: 1 addition & 3 deletions packages/groq-builder/src/commands/conditionalByType.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import {
createGroqBuilder,
ExtractDocumentTypes,
GroqBuilder,
IGroqBuilder,
InferResultItem,
InferResultType,
} from "../index";
import { SchemaConfig } from "../tests/schemas/nextjs-sanity-fe";
import { q, SchemaConfig } from "../tests/schemas/nextjs-sanity-fe";
import { ExtractConditionalProjectionTypes } from "./conditional-types";
import { executeBuilder } from "../tests/mocks/executeQuery";
import { mock } from "../tests/mocks/nextjs-sanity-fe-mocks";
import { Simplify, SimplifyDeep } from "../types/utils";

const q = createGroqBuilder<SchemaConfig>({ indent: " " });
const data = mock.generateSeedData({
products: mock.array(5, (i) =>
mock.product({ slug: mock.slug({ current: `product-slug-${i}` }) })
Expand Down
5 changes: 1 addition & 4 deletions packages/groq-builder/src/commands/deref.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ import { describe, expect, expectTypeOf, it } from "vitest";
import { InferResultType } from "../types/public-types";
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 { SanitySchema, q } from "../tests/schemas/nextjs-sanity-fe";

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

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

describe("deref", () => {
Expand Down
4 changes: 1 addition & 3 deletions packages/groq-builder/src/commands/filter.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { describe, it, expect, expectTypeOf } from "vitest";
import { SanitySchema, SchemaConfig } from "../tests/schemas/nextjs-sanity-fe";
import { SanitySchema, q } from "../tests/schemas/nextjs-sanity-fe";
import { InferResultType } from "../types/public-types";
import { executeBuilder } from "../tests/mocks/executeQuery";
import { mock } from "../tests/mocks/nextjs-sanity-fe-mocks";
import { createGroqBuilder } from "../index";

const q = createGroqBuilder<SchemaConfig>();
const qVariants = q.star.filterByType("variant");

describe("filter", () => {
Expand Down
5 changes: 1 addition & 4 deletions packages/groq-builder/src/commands/filterByType.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { describe, it, expect, expectTypeOf } from "vitest";
import { SanitySchema, SchemaConfig } from "../tests/schemas/nextjs-sanity-fe";
import { SanitySchema, q } from "../tests/schemas/nextjs-sanity-fe";
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";

const q = createGroqBuilder<SchemaConfig>();

const data = mock.generateSeedData({});

describe("filterByType", () => {
Expand Down
5 changes: 1 addition & 4 deletions packages/groq-builder/src/commands/fragment.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { describe, it, expect, expectTypeOf } from "vitest";
import { SanitySchema, SchemaConfig } from "../tests/schemas/nextjs-sanity-fe";
import { SanitySchema, q } from "../tests/schemas/nextjs-sanity-fe";
import { InferFragmentType, InferResultType } from "../types/public-types";
import { createGroqBuilder } from "../index";
import { TypeMismatchError } from "../types/utils";

const q = createGroqBuilder<SchemaConfig>({ indent: " " });

describe("fragment", () => {
// define a fragment:
const variantFragment = q.fragment<SanitySchema.Variant>().project({
Expand Down
4 changes: 1 addition & 3 deletions packages/groq-builder/src/commands/grab-deprecated.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import { InferResultType } from "../types/public-types";
import { createGroqBuilderWithZod } from "../index";
import { SchemaConfig } from "../tests/schemas/nextjs-sanity-fe";
import { q } from "../tests/schemas/nextjs-sanity-fe";

const q = createGroqBuilderWithZod<SchemaConfig>();
const qVariants = q.star.filterByType("variant");

describe("grab (backwards compatibility)", () => {
Expand Down
4 changes: 1 addition & 3 deletions packages/groq-builder/src/commands/nullable.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { describe, it, expect, expectTypeOf } from "vitest";
import { SanitySchema, SchemaConfig } from "../tests/schemas/nextjs-sanity-fe";
import { SanitySchema, q } from "../tests/schemas/nextjs-sanity-fe";
import { InferResultType } from "../types/public-types";
import { createGroqBuilderWithZod } from "../index";
import { executeBuilder } from "../tests/mocks/executeQuery";
import { mock } from "../tests/mocks/nextjs-sanity-fe-mocks";

const q = createGroqBuilderWithZod<SchemaConfig>();
const qVariants = q.star.filterByType("variant");

describe("nullable", () => {
Expand Down
4 changes: 1 addition & 3 deletions packages/groq-builder/src/commands/order.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { describe, it, expect, expectTypeOf } from "vitest";
import { SanitySchema, SchemaConfig } from "../tests/schemas/nextjs-sanity-fe";
import { SanitySchema, q } from "../tests/schemas/nextjs-sanity-fe";
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";

const q = createGroqBuilder<SchemaConfig>();
const qVariants = q.star.filterByType("variant");

describe("order", () => {
Expand Down
6 changes: 2 additions & 4 deletions packages/groq-builder/src/commands/parameters.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import { SchemaConfig } from "../tests/schemas/nextjs-sanity-fe";
import { createGroqBuilder, InferParametersType } from "../index";
import { q } from "../tests/schemas/nextjs-sanity-fe";
import { InferParametersType } from "../index";
import { executeBuilder } from "../tests/mocks/executeQuery";
import { mock } from "../tests/mocks/nextjs-sanity-fe-mocks";

const q = createGroqBuilder<SchemaConfig>();

describe("parameters", () => {
const data = mock.generateSeedData({
variants: [
Expand Down
1 change: 1 addition & 0 deletions packages/groq-builder/src/commands/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { executeBuilder } from "../tests/mocks/executeQuery";
import { currencyFormat } from "../tests/utils";

const q = createGroqBuilderWithZod<SchemaConfig>();

const qVariants = q.star.filterByType("variant");

describe("project (object projections)", () => {
Expand Down
30 changes: 7 additions & 23 deletions packages/groq-builder/src/commands/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
} from "./projection-types";
import { isConditional } from "./conditional-types";
import {
simpleArrayParser,
combineObjectParsers,
maybeArrayParser,
simpleObjectParser,
} from "../validation/simple-validation";

Expand Down Expand Up @@ -137,8 +138,6 @@ function normalizeProjectionField(
}
}

type UnknownObject = Record<string, unknown>;

type NormalizedProjectionField = {
key: string;
query: string;
Expand Down Expand Up @@ -167,26 +166,11 @@ function createProjectionParser(
.filter(notNull);

// Combine normal and conditional parsers:
const combinedParsers = [objectParser, ...conditionalParsers];
const combinedParser = (input: Record<string, unknown>) => {
const result = {};
for (const p of combinedParsers) {
const parsed = p(input);
Object.assign(result, parsed);
}
return result;
};
const combinedParser = combineObjectParsers(
objectParser,
...conditionalParsers
);

// Finally, transparently handle arrays or objects:
const arrayParser = simpleArrayParser(combinedParser);
return function projectionParser(
input: UnknownObject | Array<UnknownObject>
) {
// Operates against either an array or a single item:
if (!Array.isArray(input)) {
return combinedParser(input);
}

return arrayParser(input);
};
return maybeArrayParser(combinedParser);
}
32 changes: 29 additions & 3 deletions packages/groq-builder/src/commands/projectField.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import { mock } from "../tests/mocks/nextjs-sanity-fe-mocks";
import { InferResultType } from "../types/public-types";
import { SanitySchema, SchemaConfig } from "../tests/schemas/nextjs-sanity-fe";
import { SanitySchema, q } from "../tests/schemas/nextjs-sanity-fe";
import { executeBuilder } from "../tests/mocks/executeQuery";
import { createGroqBuilder, zod } from "../index";
import { zod } from "../index";

const q = createGroqBuilder<SchemaConfig>();
const qVariants = q.star.filterByType("variant");

describe("field (naked projections)", () => {
Expand Down Expand Up @@ -141,5 +140,32 @@ describe("field (naked projections)", () => {
result: Expected number, received string]
`);
});

describe("with arrays of data", () => {
const qPrices = qVariants.field("price", zod.number());
it("should execute correctly", async () => {
const results = await executeBuilder(qPrices, data);
expect(results).toMatchInlineSnapshot(`
[
55,
56,
57,
58,
59,
]
`);
});
it("should throw for invalid values", () => {
expect(qPrices.parse([55, 56])).toEqual([55, 56]);
expect(qPrices.parse(56)).toEqual(56);

expect(() => {
qPrices.parse([55, "56"]);
}).toThrowErrorMatchingInlineSnapshot(`
[ValidationErrors: 1 Parsing Error:
result[1]: Expected number, received string]
`);
});
});
});
});
7 changes: 6 additions & 1 deletion packages/groq-builder/src/commands/projectField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
ValidateParserInput,
} from "./projection-types";
import { Parser, ParserWithWidenedInput } from "../types/public-types";
import { maybeArrayParser } from "../validation/simple-validation";
import { normalizeValidationFunction } from "./validate-utils";

declare module "../groq-builder" {
export interface GroqBuilder<TResult, TQueryConfig> {
Expand Down Expand Up @@ -64,6 +66,9 @@ GroqBuilder.implement({
fieldName = "." + fieldName;
}

return this.chain(fieldName, parser);
// Finally, transparently handle arrays or objects:
const arrayParser = maybeArrayParser(normalizeValidationFunction(parser));

return this.chain(fieldName, arrayParser);
},
});
6 changes: 2 additions & 4 deletions packages/groq-builder/src/commands/raw.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { describe, it, expect, expectTypeOf } from "vitest";
import { SchemaConfig } from "../tests/schemas/nextjs-sanity-fe";
import { q } from "../tests/schemas/nextjs-sanity-fe";
import { InferResultType } from "../types/public-types";
import { createGroqBuilder, zod } from "../index";
import { zod } from "../index";
import { executeBuilder } from "../tests/mocks/executeQuery";
import { mock } from "../tests/mocks/nextjs-sanity-fe-mocks";

const q = createGroqBuilder<SchemaConfig>();

describe("raw", () => {
const qVariants = q.star.slice(0, 2);
const qRaw =
Expand Down
6 changes: 2 additions & 4 deletions packages/groq-builder/src/commands/select.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import { createGroqBuilder, InferResultType, zod } from "../index";
import { SchemaConfig } from "../tests/schemas/nextjs-sanity-fe";
import { InferResultType, zod } from "../index";
import { q } from "../tests/schemas/nextjs-sanity-fe";
import { mock } from "../tests/mocks/nextjs-sanity-fe-mocks";
import { executeBuilder } from "../tests/mocks/executeQuery";

const q = createGroqBuilder<SchemaConfig>({ indent: " " });

describe("select", () => {
const qBase = q.star.filterByType("variant", "product", "category");

Expand Down
5 changes: 2 additions & 3 deletions packages/groq-builder/src/commands/selectByType.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import { createGroqBuilder, InferResultType, zod } from "../index";
import { SchemaConfig } from "../tests/schemas/nextjs-sanity-fe";
import { InferResultType, zod } from "../index";
import { q } from "../tests/schemas/nextjs-sanity-fe";
import { executeBuilder } from "../tests/mocks/executeQuery";
import { mock } from "../tests/mocks/nextjs-sanity-fe-mocks";

describe("selectByType", () => {
const q = createGroqBuilder<SchemaConfig>({ indent: " " });
const qBase = q.star.filterByType("product", "variant", "category");

const data = mock.generateSeedData({
Expand Down
Loading

0 comments on commit 4d415ce

Please sign in to comment.