From f03820f61324f153d9ac10b4d23428c97f1dba05 Mon Sep 17 00:00:00 2001 From: Scott Rippey Date: Wed, 29 Jan 2025 17:02:05 -0700 Subject: [PATCH] Fix: improve types for ellipsis operator (`...`) when using overrides (#324) * tests(deref): added extra test cases * tests(fragment): added extra test cases for `fragment` * tests(project): added extra test cases for projection errors * fix(projections): Allow "..." properties to be overridden * Changeset * chore: extracted `TypeMismatchError` to separate file * fix(project): ensure the ellipsis operator will pass-through all properties, and supports a parser * fix(project): unused import * changeset --------- Co-authored-by: scottrippey --- .changeset/long-geese-try.md | 6 + packages/groqd/src/commands/deref.test.ts | 16 +- packages/groqd/src/commands/fragment.test.ts | 16 +- packages/groqd/src/commands/fragment.ts | 2 +- packages/groqd/src/commands/project.test.ts | 173 +++++++++++++++--- packages/groqd/src/commands/project.ts | 28 ++- .../groqd/src/commands/projection-types.ts | 14 +- packages/groqd/src/types/schema-types.ts | 2 +- .../src/types/type-mismatch-error.test.ts | 71 +++++++ .../groqd/src/types/type-mismatch-error.ts | 43 +++++ packages/groqd/src/types/utils.test.ts | 72 +------- packages/groqd/src/types/utils.ts | 46 +---- .../groqd/src/validation/simple-validation.ts | 15 +- packages/groqd/src/validation/zod.test.ts | 2 +- 14 files changed, 339 insertions(+), 167 deletions(-) create mode 100644 .changeset/long-geese-try.md create mode 100644 packages/groqd/src/types/type-mismatch-error.test.ts create mode 100644 packages/groqd/src/types/type-mismatch-error.ts diff --git a/.changeset/long-geese-try.md b/.changeset/long-geese-try.md new file mode 100644 index 00000000..898023ff --- /dev/null +++ b/.changeset/long-geese-try.md @@ -0,0 +1,6 @@ +--- +"groqd": patch +--- + +Fixed: improve support for ellipsis operator (`...`) when using overrides +Fixes #317 diff --git a/packages/groqd/src/commands/deref.test.ts b/packages/groqd/src/commands/deref.test.ts index 44a1ab35..4a9b08f0 100644 --- a/packages/groqd/src/commands/deref.test.ts +++ b/packages/groqd/src/commands/deref.test.ts @@ -2,7 +2,7 @@ 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, q } from "../tests/schemas/nextjs-sanity-fe"; +import { q, SanitySchema } from "../tests/schemas/nextjs-sanity-fe"; const data = mock.generateSeedData({}); @@ -52,4 +52,18 @@ describe("deref", () => { const results = await executeBuilder(qVariants, data); expect(results).toEqual(data.variants); }); + + describe("as part of a projection", () => { + it("should deref an array of fields", () => { + const query = q.star.filterByType("product").project((sub) => ({ + categories: sub.field("categories[]").deref(), + })); + + expectTypeOf>().toEqualTypeOf< + Array<{ + categories: SanitySchema.Category[] | null; + }> + >(); + }); + }); }); diff --git a/packages/groqd/src/commands/fragment.test.ts b/packages/groqd/src/commands/fragment.test.ts index 07898177..83c2b44b 100644 --- a/packages/groqd/src/commands/fragment.test.ts +++ b/packages/groqd/src/commands/fragment.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, expectTypeOf } from "vitest"; import { SanitySchema, q } from "../tests/schemas/nextjs-sanity-fe"; import { InferFragmentType, InferResultType } from "../types/public-types"; -import { TypeMismatchError } from "../types/utils"; +import { TypeMismatchError } from "../types/type-mismatch-error"; describe("fragment", () => { // define a fragment: @@ -219,4 +219,18 @@ describe("fragment", () => { }>(); }); }); + + describe("fragment", () => { + const anyFrag = q.fragment().project({ + foo: q.string(), + bar: q.number(), + }); + type AnyFragType = InferFragmentType; + it("simple fragment should have the correct type", () => { + expectTypeOf().toEqualTypeOf<{ + foo: string; + bar: number; + }>(); + }); + }); }); diff --git a/packages/groqd/src/commands/fragment.ts b/packages/groqd/src/commands/fragment.ts index 1c4af1d1..616b361a 100644 --- a/packages/groqd/src/commands/fragment.ts +++ b/packages/groqd/src/commands/fragment.ts @@ -2,7 +2,7 @@ import { GroqBuilder } from "../groq-builder"; import { ExtractProjectionResult, ProjectionMap } from "./projection-types"; import { Fragment } from "../types/public-types"; import { ExtractDocumentTypes, QueryConfig } from "../types/schema-types"; -import { RequireAFakeParameterIfThereAreTypeMismatchErrors } from "../types/utils"; +import { RequireAFakeParameterIfThereAreTypeMismatchErrors } from "../types/type-mismatch-error"; declare module "../groq-builder" { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/packages/groqd/src/commands/project.test.ts b/packages/groqd/src/commands/project.test.ts index 1b02f3be..000b4a1e 100644 --- a/packages/groqd/src/commands/project.test.ts +++ b/packages/groqd/src/commands/project.test.ts @@ -1,7 +1,8 @@ import { describe, expect, expectTypeOf, it } from "vitest"; import { SanitySchema, SchemaConfig } from "../tests/schemas/nextjs-sanity-fe"; -import { InferResultType } from "../types/public-types"; -import { Simplify, TypeMismatchError } from "../types/utils"; +import { InferResultItem, InferResultType } from "../types/public-types"; +import { Simplify } from "../types/utils"; +import { TypeMismatchError } from "../types/type-mismatch-error"; import { createGroqBuilderWithZod } from "../index"; import { mock } from "../tests/mocks/nextjs-sanity-fe-mocks"; import { executeBuilder } from "../tests/mocks/executeQuery"; @@ -65,19 +66,27 @@ describe("project (object projections)", () => { }); describe("a single plain property", () => { - it("cannot use 'true' to project unknown properties", () => { + it("cannot use 'true' or a parser to project unknown properties", () => { // @ts-expect-error --- const qInvalid = qVariants.project({ + name: true, INVALID: true, + INVALID_PARSER: q.string(), }); expectTypeOf>().toEqualTypeOf< Array<{ + name: string; INVALID: TypeMismatchError<{ error: `⛔️ 'true' can only be used for known properties ⛔️`; expected: keyof SanitySchema.Variant; actual: "INVALID"; }>; + INVALID_PARSER: TypeMismatchError<{ + error: `⛔️ Parser can only be used with known properties ⛔️`; + expected: keyof SanitySchema.Variant; + actual: "INVALID_PARSER"; + }>; }> >(); }); @@ -722,31 +731,147 @@ describe("project (object projections)", () => { }); describe("ellipsis ... operator", () => { - const qEllipsis = qVariants.project((q) => ({ - "...": true, - OTHER: q.field("name"), - })); - it("query should be correct", () => { - expect(qEllipsis.query).toMatchInlineSnapshot( - `"*[_type == "variant"] { ..., "OTHER": name }"` - ); + describe("when set to 'true'", () => { + const qEllipsis = qVariants.project((sub) => ({ + "...": true, + OTHER: sub.field("name"), + })); + + it("query should be correct", () => { + expect(qEllipsis.query).toMatchInlineSnapshot( + `"*[_type == "variant"] { ..., "OTHER": name }"` + ); + }); + + it("types should be correct", () => { + expectTypeOf>().toEqualTypeOf< + Array< + Simplify< + SanitySchema.Variant & { + OTHER: string; + } + > + > + >(); + }); + + it("should not need to introduce runtime parsing", () => { + expect(qEllipsis.parser).toBeNull(); + }); + + it("should execute correctly", async () => { + const results = await executeBuilder(qEllipsis, data); + expect(results).toEqual( + data.variants.map((variant) => ({ + ...variant, + OTHER: variant.name, + })) + ); + }); }); - it("types should be correct", () => { - expectTypeOf>().toEqualTypeOf< - Array> - >(); + describe("when set to a parser", () => { + const qEllipsisWithParser = qVariants.project({ + "...": q.object({ + name: q.string(), + msrp: q.number(), + }), + price: q.number(), + }); + + it("a parser is created", () => { + expect(qEllipsisWithParser.parser).not.toBeNull(); + }); + + it("executes correctly", async () => { + const results = await executeBuilder(qEllipsisWithParser, data); + expect(results).toEqual( + data.variants.map((variant) => ({ + name: variant.name, + msrp: variant.msrp, + price: variant.price, + })) + ); + }); + + it("should throw when the data doesn't match", async () => { + const invalidData = [ + ...data.datalake, + mock.variant({ + // @ts-expect-error --- + msrp: "INVALID", + }), + ]; + + await expect(() => + executeBuilder(qEllipsisWithParser, { datalake: invalidData }) + ).rejects.toThrowErrorMatchingInlineSnapshot(` + [ValidationErrors: 1 Parsing Error: + result[5].msrp: Expected number, received string] + `); + }); }); - it("should execute correctly", async () => { - const results = await executeBuilder(qEllipsis, data); - expect(results).toEqual( - data.variants.map((v) => { - // @ts-expect-error --- - v.OTHER = v.name; - return v; - }) - ); + describe("when other fields have parsers", () => { + const qEllipsis = qVariants.project((sub) => ({ + "...": true, + VALIDATED: sub.field("name", q.string().toUpperCase()), + })); + + it("query should be correct", () => { + expect(qEllipsis.query).toMatchInlineSnapshot( + `"*[_type == "variant"] { ..., "VALIDATED": name }"` + ); + }); + + it("types should be correct", () => { + expectTypeOf>().toEqualTypeOf< + Array< + Simplify< + SanitySchema.Variant & { + VALIDATED: string; + } + > + > + >(); + }); + + it("should include runtime parsing", () => { + expect(qEllipsis.parser).not.toBeNull(); + }); + + it("should execute correctly", async () => { + const results = await executeBuilder(qEllipsis, data); + expect(results).toEqual( + data.variants.map((variant) => ({ + ...variant, + VALIDATED: variant.name.toUpperCase(), + })) + ); + }); + }); + + describe("when fields are overridden", () => { + const overridden = qVariants.project((sub) => ({ + "...": true, + style: sub.field("style[]").deref(), + })); + + type Item = InferResultItem; + + it("normal properties get passed through", () => { + // Properties like flavour should be included by "...": + expectTypeOf().toEqualTypeOf< + SanitySchema.Variant["flavour"] + >(); + }); + it("the overridden properties are correctly typed", () => { + type StyleReferences = SanitySchema.Variant["style"]; + expectTypeOf().not.toEqualTypeOf(); + + type OverriddenStyle = SanitySchema.Style[] | null; + expectTypeOf().toEqualTypeOf(); + }); }); }); }); diff --git a/packages/groqd/src/commands/project.ts b/packages/groqd/src/commands/project.ts index fe8e8eb2..41308f2b 100644 --- a/packages/groqd/src/commands/project.ts +++ b/packages/groqd/src/commands/project.ts @@ -1,8 +1,5 @@ -import { - notNull, - RequireAFakeParameterIfThereAreTypeMismatchErrors, - Simplify, -} from "../types/utils"; +import { notNull, Simplify } from "../types/utils"; +import { RequireAFakeParameterIfThereAreTypeMismatchErrors } from "../types/type-mismatch-error"; import { GroqBuilder } from "../groq-builder"; import { Parser, ParserFunction } from "../types/public-types"; import { isParser, normalizeValidationFunction } from "./validate-utils"; @@ -17,6 +14,7 @@ import { combineObjectParsers, maybeArrayParser, simpleObjectParser, + UnknownObjectParser, } from "../validation/simple-validation"; declare module "../groq-builder" { @@ -148,12 +146,23 @@ function createProjectionParser( fields: NormalizedProjectionField[] ): ParserFunction | null { if (!fields.some((f) => f.parser)) { - // No nested parsers! + // No parsers found for any keys return null; } + // Parse the ellipsis operator ("..."): + const ellipsisField = fields.find((f) => isEllipsis(f.key)); + const ellipsisParser: UnknownObjectParser | null = ellipsisField + ? // Allow a custom parser: + ellipsisField.parser || + // Or just pass-through the whole object: + ((obj) => obj) + : null; + // Parse all normal fields: - const normalFields = fields.filter((f) => !isConditional(f.key)); + const normalFields = fields.filter( + (f) => !isEllipsis(f.key) && !isConditional(f.key) + ); const objectShape = Object.fromEntries( normalFields.map((f) => [f.key, f.parser]) ); @@ -167,6 +176,7 @@ function createProjectionParser( // Combine normal and conditional parsers: const combinedParser = combineObjectParsers( + ...[ellipsisParser].filter(notNull), objectParser, ...conditionalParsers ); @@ -174,3 +184,7 @@ function createProjectionParser( // Finally, transparently handle arrays or objects: return maybeArrayParser(combinedParser); } + +function isEllipsis(key: string) { + return key === "..."; +} diff --git a/packages/groqd/src/commands/projection-types.ts b/packages/groqd/src/commands/projection-types.ts index abfaa36c..5b191136 100644 --- a/packages/groqd/src/commands/projection-types.ts +++ b/packages/groqd/src/commands/projection-types.ts @@ -3,13 +3,14 @@ import { Empty, IsAny, LiteralUnion, + Override, Simplify, SimplifyDeep, StringKeys, - TypeMismatchError, UndefinedToNull, ValueOf, } from "../types/utils"; +import { TypeMismatchError } from "../types/type-mismatch-error"; import { FragmentInputTypeTag, IGroqBuilder, @@ -69,14 +70,14 @@ export type ProjectionFieldConfig = // Use a GroqBuilder instance to create a nested projection | IGroqBuilder; -export type ExtractProjectionResult = +export type ExtractProjectionResult = Override< // Extract the "..." operator: (TProjectionMap extends { "...": true } ? TResultItem : Empty) & (TProjectionMap extends { "...": Parser } ? TOutput - : Empty) & - // Extract any conditional expressions: - ExtractConditionalProjectionTypes & + : Empty), + // Extract any conditional expressions: + ExtractConditionalProjectionTypes & // Extract all the fields: ExtractProjectionResultFields< TResultItem, @@ -85,7 +86,8 @@ export type ExtractProjectionResult = TProjectionMap, "..." | typeof FragmentInputTypeTag | ConditionalKey > - >; + > +>; type ExtractProjectionResultFields = { [P in keyof TProjectionMap]: TProjectionMap[P] extends IGroqBuilder< diff --git a/packages/groqd/src/types/schema-types.ts b/packages/groqd/src/types/schema-types.ts index 25615f31..600c915b 100644 --- a/packages/groqd/src/types/schema-types.ts +++ b/packages/groqd/src/types/schema-types.ts @@ -1,4 +1,4 @@ -import { TypeMismatchError } from "./utils"; +import { TypeMismatchError } from "./type-mismatch-error"; export type QueryConfig = { /** diff --git a/packages/groqd/src/types/type-mismatch-error.test.ts b/packages/groqd/src/types/type-mismatch-error.test.ts new file mode 100644 index 00000000..d76ec6c7 --- /dev/null +++ b/packages/groqd/src/types/type-mismatch-error.test.ts @@ -0,0 +1,71 @@ +import { describe, expectTypeOf, it } from "vitest"; +import { + ExtractTypeMismatchErrors, + RequireAFakeParameterIfThereAreTypeMismatchErrors, + TypeMismatchError, +} from "./type-mismatch-error"; + +describe("ExtractTypeMismatchErrors", () => { + type TME = TypeMismatchError<{ + error: ErrorMessage; + expected: unknown; + actual: unknown; + }>; + + type Valid = { FOO: "FOO" }; + + it("should find nested errors", () => { + type TestObject = { + FOO: TME<"foo-error">; + BAR: TME<"bar-error">; + BAZ: Valid | TME<"baz-error">; + BAT: Valid; + }; + + type Result = ExtractTypeMismatchErrors; + + expectTypeOf().toEqualTypeOf< + | 'Error in "FOO": foo-error' + | 'Error in "BAR": bar-error' + | 'Error in "BAZ": baz-error' + >(); + }); + it("should return 'never' when there's no errors", () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); +}); + +describe("RequireAFakeParameterIfThereAreTypeMismatchErrors", () => { + type NoErrors = { + foo: "foo"; + bar: "bar"; + }; + type WithErrors = { + foo: "foo"; + invalid: TypeMismatchError<{ + error: "ERROR"; + expected: "EXPECTED"; + actual: "ACTUAL"; + }>; + alsoInvalid: + | string + | TypeMismatchError<{ + error: "ERROR"; + expected: "EXPECTED"; + actual: "ACTUAL"; + }>; + }; + it("should return an empty parameter list when there are no errors", () => { + type Params = RequireAFakeParameterIfThereAreTypeMismatchErrors; + expectTypeOf().toEqualTypeOf<[]>(); + }); + it("should return errors in the parameter list when there are errors", () => { + type Params = RequireAFakeParameterIfThereAreTypeMismatchErrors; + expectTypeOf().toEqualTypeOf< + | ["⛔️ Error: this projection has type mismatches: ⛔️"] + | ['Error in "invalid": ERROR' | 'Error in "alsoInvalid": ERROR'] + >(); + }); +}); diff --git a/packages/groqd/src/types/type-mismatch-error.ts b/packages/groqd/src/types/type-mismatch-error.ts new file mode 100644 index 00000000..e44169b4 --- /dev/null +++ b/packages/groqd/src/types/type-mismatch-error.ts @@ -0,0 +1,43 @@ +import type { IsNever, Simplify } from "type-fest"; +import { StringKeys, ValueOf } from "./utils"; + +export type TypeMismatchError< + TError extends { error: string; actual: any; expected: any } = any +> = { + error: TError["error"]; + actual: Simplify; + expected: Simplify; +}; +/** + * Extracts all TypeMismatchError's from the projection result, + * making it easy to report these errors. + * Returns a string of error messages, + * or `never` if there are no errors. + */ +export type ExtractTypeMismatchErrors = ValueOf<{ + [TKey in StringKeys< + keyof TProjectionResult + >]: TypeMismatchError extends TProjectionResult[TKey] + ? `Error in "${TKey}": ${Extract< + TProjectionResult[TKey], + TypeMismatchError + >["error"]}` + : never; +}>; +/** + * When we map projection results, we return TypeMismatchError's + * for any fields that have an invalid mapping configuration. + * However, this does not cause TypeScript to throw any errors. + * + * In order to get TypeScript to complain about these invalid mappings, + * we will "require" an extra parameter, which will reveal the error messages. + */ +export type RequireAFakeParameterIfThereAreTypeMismatchErrors< + TProjectionResult, + _Errors extends string = ExtractTypeMismatchErrors +> = IsNever<_Errors> extends true + ? [] // No errors, yay! Do not require any extra parameters. + : // We've got errors; let's require an extra parameter, with the error message: + | [_Errors] + // And this extra error message causes TypeScript to always log the entire list of errors: + | ["⛔️ Error: this projection has type mismatches: ⛔️"]; diff --git a/packages/groqd/src/types/utils.test.ts b/packages/groqd/src/types/utils.test.ts index 7198fb71..e7853a9f 100644 --- a/packages/groqd/src/types/utils.test.ts +++ b/packages/groqd/src/types/utils.test.ts @@ -1,75 +1,5 @@ import { describe, expectTypeOf, it } from "vitest"; -import { - ExtractTypeMismatchErrors, - RequireAFakeParameterIfThereAreTypeMismatchErrors, - TypeMismatchError, - UndefinedToNull, -} from "./utils"; - -describe("ExtractTypeMismatchErrors", () => { - type TME = TypeMismatchError<{ - error: ErrorMessage; - expected: unknown; - actual: unknown; - }>; - - type Valid = { FOO: "FOO" }; - - it("should find nested errors", () => { - type TestObject = { - FOO: TME<"foo-error">; - BAR: TME<"bar-error">; - BAZ: Valid | TME<"baz-error">; - BAT: Valid; - }; - - type Result = ExtractTypeMismatchErrors; - - expectTypeOf().toEqualTypeOf< - | 'Error in "FOO": foo-error' - | 'Error in "BAR": bar-error' - | 'Error in "BAZ": baz-error' - >(); - }); - it("should return 'never' when there's no errors", () => { - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); - }); -}); - -describe("RequireAFakeParameterIfThereAreTypeMismatchErrors", () => { - type NoErrors = { - foo: "foo"; - bar: "bar"; - }; - type WithErrors = { - foo: "foo"; - invalid: TypeMismatchError<{ - error: "ERROR"; - expected: "EXPECTED"; - actual: "ACTUAL"; - }>; - alsoInvalid: - | string - | TypeMismatchError<{ - error: "ERROR"; - expected: "EXPECTED"; - actual: "ACTUAL"; - }>; - }; - it("should return an empty parameter list when there are no errors", () => { - type Params = RequireAFakeParameterIfThereAreTypeMismatchErrors; - expectTypeOf().toEqualTypeOf<[]>(); - }); - it("should return errors in the parameter list when there are errors", () => { - type Params = RequireAFakeParameterIfThereAreTypeMismatchErrors; - expectTypeOf().toEqualTypeOf< - | ["⛔️ Error: this projection has type mismatches: ⛔️"] - | ['Error in "invalid": ERROR' | 'Error in "alsoInvalid": ERROR'] - >(); - }); -}); +import { UndefinedToNull } from "./utils"; describe("UndefinedToNull", () => { it("should cast undefined or optional properties to null", () => { diff --git a/packages/groqd/src/types/utils.ts b/packages/groqd/src/types/utils.ts index d0ec7e3d..dc6c94a0 100644 --- a/packages/groqd/src/types/utils.ts +++ b/packages/groqd/src/types/utils.ts @@ -1,4 +1,4 @@ -import type { IsNever, Simplify } from "type-fest"; +import { TypeMismatchError } from "./type-mismatch-error"; export type { Simplify, Primitive, LiteralUnion, IsAny } from "type-fest"; @@ -10,8 +10,8 @@ export type Get = TKey extends keyof TObj ? TObj[TKey] : TypeMismatchError<{ error: "Invalid property"; - expected: keyof TObj; actual: TKey; + expected: keyof TObj; }>; /** @@ -32,31 +32,6 @@ export type SimplifyDeep = T extends object export type Override = Omit & TOverrides; -export type TypeMismatchError< - TError extends { error: string; expected: any; actual: any } = any -> = { - error: TError["error"]; - expected: Simplify; - actual: Simplify; -}; - -/** - * Extracts all TypeMismatchError's from the projection result, - * making it easy to report these errors. - * Returns a string of error messages, - * or `never` if there are no errors. - */ -export type ExtractTypeMismatchErrors = ValueOf<{ - [TKey in StringKeys< - keyof TProjectionResult - >]: TypeMismatchError extends TProjectionResult[TKey] - ? `Error in "${TKey}": ${Extract< - TProjectionResult[TKey], - TypeMismatchError - >["error"]}` - : never; -}>; - /** * Returns a union of all value types in the object */ @@ -133,20 +108,3 @@ export function pick( export type UndefinedToNull = T extends undefined ? NonNullable | null : T; -/** - * When we map projection results, we return TypeMismatchError's - * for any fields that have an invalid mapping configuration. - * However, this does not cause TypeScript to throw any errors. - * - * In order to get TypeScript to complain about these invalid mappings, - * we will "require" an extra parameter, which will reveal the error messages. - */ -export type RequireAFakeParameterIfThereAreTypeMismatchErrors< - TProjectionResult, - _Errors extends string = ExtractTypeMismatchErrors -> = IsNever<_Errors> extends true - ? [] // No errors, yay! Do not require any extra parameters. - : // We've got errors; let's require an extra parameter, with the error message: - | [_Errors] - // And this extra error message causes TypeScript to always log the entire list of errors: - | ["⛔️ Error: this projection has type mismatches: ⛔️"]; diff --git a/packages/groqd/src/validation/simple-validation.ts b/packages/groqd/src/validation/simple-validation.ts index 72a9efc8..0b77a087 100644 --- a/packages/groqd/src/validation/simple-validation.ts +++ b/packages/groqd/src/validation/simple-validation.ts @@ -81,15 +81,8 @@ export function simpleObjectParser( }; } - const keys = Object.keys(objectMapper) as Array; - const entries = keys.map( - (key) => - [ - key, - normalizeValidationFunction( - objectMapper[key as keyof typeof objectMapper] - ), - ] as const + const entries = Object.entries(objectMapper).map( + ([key, parser]) => [key, normalizeValidationFunction(parser)] as const ); return (input) => { @@ -117,7 +110,7 @@ export function simpleObjectParser( export type ObjectValidationMap = Record; type UnknownObject = Record; -type UnknownObjectParser = (input: UnknownObject) => UnknownObject; +export type UnknownObjectParser = (input: UnknownObject) => UnknownObject; /** * Combines multiple object parsers into a single parser @@ -125,7 +118,9 @@ type UnknownObjectParser = (input: UnknownObject) => UnknownObject; export function combineObjectParsers( ...objectParsers: UnknownObjectParser[] ): UnknownObjectParser { + // This is the most common use-case: if (objectParsers.length === 1) return objectParsers[0]; + return function combinedObjectParser(input) { const validationErrors = new ValidationErrors(); const result = {}; diff --git a/packages/groqd/src/validation/zod.test.ts b/packages/groqd/src/validation/zod.test.ts index 136a76b6..f5b36cc6 100644 --- a/packages/groqd/src/validation/zod.test.ts +++ b/packages/groqd/src/validation/zod.test.ts @@ -3,7 +3,7 @@ import { createGroqBuilderWithZod, InferResultType } from "../index"; import { SchemaConfig } from "../tests/schemas/nextjs-sanity-fe"; import { mock } from "../tests/mocks/nextjs-sanity-fe-mocks"; import { executeBuilder } from "../tests/mocks/executeQuery"; -import { TypeMismatchError } from "../types/utils"; +import { TypeMismatchError } from "../types/type-mismatch-error"; const q = createGroqBuilderWithZod();