-
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.
* chore: removed unused files * feat(notNull): added notNull helper * feat(notNull): changeset * feat(notNull): changed `slice(#)` to be nullable; use notNull to fix * feat(notNull): added MISSING_PARSER detection * feat(notNull): added validationRequired to `.field` * feat(notNull): revert last commit * feat(notNull): added docs around CHAINED_PARSER_ERROR * feat(notNull): ensure `notNull` and `nullable` come last in a chain * feat(notNull): ensure `notNull` and `nullable` come last in a chain * feat(notNull): updated changeset * feat(notNull): extracted "as" helper into its own file, and updated docs * feat(notNull): improved docs for the non-chainability of `notNull / nullable` and introduced `IGroqBuilderNotChainable` * feat(notNull): added examples to docs * feat(notNull): renamed to `InvalidQueryError` * feat(notNull): changeset --------- Co-authored-by: scottrippey <[email protected]>
- Loading branch information
1 parent
d2dab36
commit 74d0628
Showing
26 changed files
with
427 additions
and
563 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,11 @@ | ||
--- | ||
"groqd": patch | ||
--- | ||
|
||
Fixed: `.slice(number)` returns a nullable result | ||
Added: `.notNull()` utility | ||
Updated: `.nullable()` utility to not be chainable | ||
Updated: require a `redundant: true` parameter for `nullable` and `notNull` | ||
Added: `InvalidQueryError` is thrown for all invalid queries detected | ||
|
||
Fixes #268 |
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,42 @@ | ||
import { GroqBuilder } from "../groq-builder"; | ||
import { ExtractDocumentTypes } from "../types/schema-types"; | ||
|
||
declare module "../groq-builder" { | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
export interface GroqBuilder<TResult, TQueryConfig> { | ||
/** | ||
* Overrides the result type to anything you specify. | ||
* | ||
* Use this carefully, since it's essentially "lying" to TypeScript, and there's no runtime validation. | ||
* | ||
* @example | ||
* q.star.filter("slug.current == $productSlug").as<Product>()... | ||
* | ||
*/ | ||
as<TResultNew>(): GroqBuilder<TResultNew, TQueryConfig>; | ||
|
||
/** | ||
* Overrides the result type to a specific document type. | ||
* | ||
* Use this carefully, since it's essentially "lying" to TypeScript, and there's no runtime validation. | ||
* | ||
* @example | ||
* q.star.filter("slug.current == $productSlug").asType<"product">()... | ||
*/ | ||
asType< | ||
_type extends ExtractDocumentTypes<TQueryConfig["schemaTypes"]> | ||
>(): GroqBuilder< | ||
Extract<TQueryConfig["schemaTypes"], { _type: _type }>, | ||
TQueryConfig | ||
>; | ||
} | ||
} | ||
|
||
GroqBuilder.implement({ | ||
as(this: GroqBuilder) { | ||
return this; | ||
}, | ||
asType(this: GroqBuilder<never>) { | ||
return this; | ||
}, | ||
}); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { describe, it, expectTypeOf, expect } from "vitest"; | ||
import { q } from "../tests/schemas/nextjs-sanity-fe"; | ||
import { mock } from "../tests/mocks/nextjs-sanity-fe-mocks"; | ||
import { InferResultItem } from "../types/public-types"; | ||
import { executeBuilder } from "../tests/mocks/executeQuery"; | ||
|
||
describe("notNull", () => { | ||
const category = mock.category({}); | ||
const data = mock.generateSeedData({ | ||
products: [ | ||
mock.product({ categories: [mock.reference(category)] }), | ||
mock.product({ categories: [mock.reference(category)] }), | ||
], | ||
categories: [category], | ||
}); | ||
const dataWithNulls = mock.generateSeedData({ | ||
products: [ | ||
mock.product({ categories: [mock.reference(category)] }), | ||
mock.product({ categories: undefined }), | ||
], | ||
categories: [category], | ||
}); | ||
|
||
describe("when used on nullable fields", () => { | ||
const qVariants = q.star.filterByType("product").project((sub) => ({ | ||
categories: sub.field("categories[]").deref().field("name"), | ||
categoriesNotNull: sub | ||
.field("categories[]") | ||
.deref() | ||
.field("name") | ||
.notNull(), | ||
})); | ||
type Result = InferResultItem<typeof qVariants>; | ||
|
||
it("should have the correct type", () => { | ||
expectTypeOf<Result["categories"]>().toEqualTypeOf<string[] | null>(); | ||
expectTypeOf<Result["categoriesNotNull"]>().toEqualTypeOf<string[]>(); | ||
}); | ||
|
||
it("should not affect the query", () => { | ||
expect(qVariants.query).toMatchInlineSnapshot(` | ||
"*[_type == "product"] { | ||
"categories": categories[]->.name, | ||
"categoriesNotNull": categories[]->.name | ||
}" | ||
`); | ||
}); | ||
|
||
it("should execute correctly when there are no nulls", async () => { | ||
const results = await executeBuilder(qVariants, data); | ||
expect(results).toMatchInlineSnapshot(` | ||
[ | ||
{ | ||
"categories": [ | ||
"Category Name", | ||
], | ||
"categoriesNotNull": [ | ||
"Category Name", | ||
], | ||
}, | ||
{ | ||
"categories": [ | ||
"Category Name", | ||
], | ||
"categoriesNotNull": [ | ||
"Category Name", | ||
], | ||
}, | ||
] | ||
`); | ||
}); | ||
|
||
it("should throw errors when finding a null value", async () => { | ||
await expect(() => executeBuilder(qVariants, dataWithNulls)).rejects | ||
.toThrowErrorMatchingInlineSnapshot(` | ||
[ValidationErrors: 1 Parsing Error: | ||
result[1].categoriesNotNull: Expected a non-null value] | ||
`); | ||
}); | ||
}); | ||
|
||
describe("when it's redundant", () => { | ||
it("should give a typescript error", () => { | ||
q.star.filterByType("flavour").project((sub) => ({ | ||
test: sub | ||
.field("_id") | ||
// You must pass `true` to acknowledge the redundancy: | ||
.notNull(true), | ||
test2: sub | ||
.field("_id") | ||
// @ts-expect-error --- otherwise it will error: | ||
.notNull(), | ||
})); | ||
}); | ||
}); | ||
}); |
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,58 @@ | ||
import { GroqBuilder } from "../groq-builder"; | ||
import { QueryConfig } from "../types/schema-types"; | ||
import { ResultUtils } from "../types/result-types"; | ||
import { Override } from "../types/utils"; | ||
import { IGroqBuilderNotChainable } from "../types/public-types"; | ||
|
||
declare module "../groq-builder" { | ||
export interface GroqBuilder<TResult, TQueryConfig> { | ||
/** | ||
* Asserts that the results are NOT nullable. | ||
* Useful when you know there must be a value, | ||
* even though the query thinks it might be null. | ||
* | ||
* ⚠️ NOTE: This method can only be used at the end of a query chain, | ||
* because you cannot chain more commands after making an assertion. | ||
* See CHAINED_ASSERTION_ERROR for more details. | ||
* | ||
* @example | ||
* q.star | ||
* .filter("slug.current == $slug") | ||
* .slice(0) // <- this return type is nullable, even though we expect there will be a match | ||
* .project({ name: q.string() }) | ||
* .notNull() // <- this ensures that the results are not null | ||
* | ||
* @example | ||
* q.star.filterByType("product").project(sub => ({ | ||
* categories: sub.field("categories[]") // <- nullable array | ||
* .deref() | ||
* .field("name", q.string()) | ||
* .notNull() | ||
* })); | ||
* | ||
* @param redundant - If the type is already not-nullable, then you must explicitly pass `.notNull(true)` to allow this redundancy. (This has no impact at runtime) | ||
*/ | ||
notNull( | ||
...redundant: ResultUtils.IsNullable<TResult> extends true ? [] : [true] | ||
): IGroqBuilderNotChainable< | ||
ResultUtils.Wrap< | ||
Override<ResultUtils.Unwrap<TResult>, { IsNullable: false }> | ||
>, | ||
TQueryConfig | ||
>; | ||
} | ||
} | ||
|
||
GroqBuilder.implement({ | ||
notNull(this: GroqBuilder<any, QueryConfig>, ..._redundant) { | ||
const parser = this.parser; | ||
return this.extend({ | ||
parser: (input) => { | ||
if (input === null) { | ||
throw new TypeError("Expected a non-null value"); | ||
} | ||
return parser ? parser(input) : input; | ||
}, | ||
}); | ||
}, | ||
}); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,47 @@ | ||
import { GroqBuilder } from "../groq-builder"; | ||
import { QueryConfig } from "../types/schema-types"; | ||
import { ResultUtils } from "../types/result-types"; | ||
import { IGroqBuilderNotChainable } from "../types/public-types"; | ||
|
||
declare module "../groq-builder" { | ||
export interface GroqBuilder<TResult, TQueryConfig> { | ||
/** | ||
* Marks a query as nullable – in case you are expecting a potential `null` value. | ||
* Useful when you expect missing values in your data, | ||
* even though the query thinks it's required. | ||
* | ||
* ⚠️ NOTE: This method can only be used at the end of a query chain, | ||
* because you cannot chain more commands after making an assertion. | ||
* See CHAINED_ASSERTION_ERROR for more details. | ||
* | ||
* @param redundant - If the type is already nullable, then you must explicitly pass `.nullable(true)` to allow this redundancy. (this has no impact at runtime) | ||
* | ||
* @example | ||
* q.star.filterByType("product").project(sub => ({ | ||
* // In our schema, "category" is required, but we know | ||
* // that we have old entries that are missing this field: | ||
* category: sub.field("category").nullable(), | ||
* }); | ||
*/ | ||
nullable(): GroqBuilder<TResult | null, TQueryConfig>; | ||
nullable( | ||
...redundant: ResultUtils.IsNullable<TResult> extends true ? [true] : [] | ||
): IGroqBuilderNotChainable<TResult | null, TQueryConfig>; | ||
} | ||
} | ||
|
||
GroqBuilder.implement({ | ||
nullable(this: GroqBuilder<any, QueryConfig>) { | ||
nullable(this: GroqBuilder<any, QueryConfig>, _redundant) { | ||
const parser = this.parser; | ||
|
||
if (!parser) { | ||
// If there's no previous parser, then this method is just used | ||
// for type-safety, and we don't need to perform runtime validation: | ||
return this; | ||
} | ||
return this.chain("", (input) => { | ||
return input === null ? null : parser(input); | ||
return this.extend({ | ||
parser: (input) => { | ||
return input === null || input === undefined ? null : parser(input); | ||
}, | ||
}); | ||
}, | ||
}); |
Oops, something went wrong.