Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions packages/groqd/src/examples.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, expectTypeOf, it } from "vitest";
import { q } from "./tests/schemas/nextjs-sanity-fe";
import { InferResultItem } from "./types/public-types";
import { SanityContentBlocks } from "./validation/content-blocks";

describe("example queries", () => {
describe("portable text", () => {
// This is the type for ContentBlocks generated from Sanity:

// There are various ways to query the content:

it("can be queried without client-side validation", () => {
const qText = q.star.filterByType("product").project((sub) => ({
description: sub.field("description[]"),
}));
expectTypeOf<InferResultItem<typeof qText>>().toMatchTypeOf<{
description: null | SanityContentBlocks;
}>();
});
it("can be queried via q.contentBlocks()", () => {
const qText = q.star.filterByType("product").project((sub) => ({
description: sub.field("description[]", q.contentBlocks().nullable()),
}));
expectTypeOf<
InferResultItem<typeof qText>["description"]
>().toEqualTypeOf<null | SanityContentBlocks>();
});
it("can be queried conditionally based on type", () => {
const qText = q.star.filterByType("product").project((sub) => ({
description: sub.field("description[]").project((desc) => ({
...desc.conditionalByType({
block: {
"...": true,
},
}),
})),
}));
expectTypeOf<
InferResultItem<typeof qText>["description"]
>().toMatchTypeOf<null | SanityContentBlocks>();
});
});
});
82 changes: 82 additions & 0 deletions packages/groqd/src/validation/content-blocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { z } from "zod";

/**
* Represents an array of blocks of Sanity's portable text.
*/
export type SanityContentBlocks<TMarkDefs extends MarkDefsBase = MarkDefsBase> =
Array<SanityContentBlock<TMarkDefs>>;

/**
* Represents a block of Sanity's portable text, for fields defined as `type: "block"`.
* This rich text type can be expanded via custom `markDefs`.
*/
export type SanityContentBlock<TMarkDefs extends MarkDefsBase = MarkDefsBase> =
{
_type: "block";
_key: string;
level?: number;
style?: string; // eg. "normal" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "blockquote";
listItem?: string; // eg. "bullet" | "number";
markDefs?: Array<TMarkDefs>;
children?: Array<{
_type: string; // eg. "span";
_key: string;
text?: string;
marks?: Array<string>;
}>;
};

export type MarkDefsBase = {
_type: string; // eg. "link";
_key: string;
};

const markDefBase = z
.object({
_type: z.string(),
_key: z.string(),
})
.catchall(z.unknown()) satisfies z.ZodType<MarkDefsBase>;

export type ContentBlockOverrides<TMarkDefs extends MarkDefsBase> = {
/**
* Supply your own custom markDef definitions.
*
* @example
* markDefs: z.object({
* _type: z.literal("link"),
* _key: z.string(),
* href: z.string().optional(),
* })
*/
markDefs?: z.ZodType<TMarkDefs>;
};
export function contentBlocks<TMarkDefs extends MarkDefsBase = MarkDefsBase>(
overrides?: ContentBlockOverrides<TMarkDefs>
) {
return z.array(contentBlock(overrides));
}
export function contentBlock<TMarkDefs extends MarkDefsBase = MarkDefsBase>({
markDefs = markDefBase as z.ZodType<TMarkDefs>,
}: ContentBlockOverrides<TMarkDefs> = {}): z.ZodType<
SanityContentBlock<TMarkDefs>
> {
return z.object({
_type: z.literal("block"),
_key: z.string(),
level: z.number().optional(),
style: z.string().optional(),
listItem: z.string().optional(),
markDefs: z.array(markDefs).optional(),
children: z
.array(
z.object({
_type: z.string(),
_key: z.string(),
text: z.string().optional(),
marks: z.array(z.string()),
})
)
.optional(),
});
}
6 changes: 6 additions & 0 deletions packages/groqd/src/validation/zod.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { z } from "zod";
import { ParserFunction } from "../types/public-types";
import { pick } from "../types/utils";
import { contentBlock, contentBlocks } from "./content-blocks";

const zodPrimitives = pick(z, [
"string",
Expand All @@ -12,6 +13,8 @@ const zodPrimitives = pick(z, [
"union",
"array",
"object",
"any",
"enum",
]);

const zodExtras = {
Expand Down Expand Up @@ -54,6 +57,9 @@ const zodExtras = {
slug<TFieldName extends string>(fieldName: TFieldName) {
return [`${fieldName}.current`, z.string()] as const;
},

contentBlock: contentBlock,
contentBlocks: contentBlocks,
};

export const zodMethods = {
Expand Down
Loading