diff --git a/.changeset/young-humans-create.md b/.changeset/young-humans-create.md new file mode 100644 index 00000000..a845151c --- /dev/null +++ b/.changeset/young-humans-create.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/docs/content/docs/api-reference/workflow/define-hook.mdx b/docs/content/docs/api-reference/workflow/define-hook.mdx index 56e8a07e..f8e8d873 100644 --- a/docs/content/docs/api-reference/workflow/define-hook.mdx +++ b/docs/content/docs/api-reference/workflow/define-hook.mdx @@ -8,10 +8,10 @@ import { generateDefinition } from "@/lib/tsdoc" Creates a type-safe hook helper that ensures the payload type is consistent between hook creation and resumption. -This is a lightweight wrapper around [`createHook()`](/docs/api-reference/workflow/create-hook) and [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) to avoid type mismatches. +This is a lightweight wrapper around [`createHook()`](/docs/api-reference/workflow/create-hook) and [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) to avoid type mismatches. It also supports optional runtime validation and transformation of payloads using any [Standard Schema v1](https://standardschema.dev) compliant validator like Zod or Valibot. -We recommend using `defineHook()` over `createHook()` in production codebases for better type safety. +We recommend using `defineHook()` over `createHook()` in production codebases for better type safety and optional runtime validation. ```ts lineNumbers @@ -67,7 +67,7 @@ export default DefineHook;` ## Examples -### Type-Safe Hook Definition +### Basic Type-Safe Hook Definition By defining the hook once with a specific payload type, you can reuse it in multiple workflows and API routes with automatic type safety. @@ -117,16 +117,21 @@ export async function POST(request: Request) { ### Validate and Transform with Schema -The optional `schema` accepts any validator that conforms to [Standard Schema v1](https://standardschema.dev). +You can provide runtime validation and transformation of hook payloads using the `schema` option. This option accepts any validator that conforms to the [Standard Schema v1](https://standardschema.dev) specification. -Zod is shown below as one example, but libraries like Valibot, ArkType, Effect Schema, or your own custom validator work as well. + +Standard Schema is a standardized specification for schema validation libraries. Most popular validation libraries support it, including Zod, Valibot, ArkType, and Effect Schema. You can also write custom validators. + + +#### Using Zod with defineHook + +Here's an example using [Zod](https://zod.dev) to validate and transform hook payloads: ```typescript lineNumbers import { defineHook } from "workflow"; import { z } from "zod"; export const approvalHook = defineHook({ - // Provide a schema to validate/transform payloads. schema: z.object({ // [!code highlight] approved: z.boolean(), // [!code highlight] comment: z.string().min(1).transform((value) => value.trim()), // [!code highlight] @@ -140,29 +145,50 @@ export async function approvalWorkflow(approvalId: string) { token: `approval:${approvalId}`, }); + // Payload is automatically typed based on the schema const { approved, comment } = await hook; console.log('Approved:', approved); - console.log('Comment:', comment); + console.log('Comment (trimmed):', comment); } ``` -In your route handler, resume the hook with the same definition; the schema validates and transforms the payload before the workflow continues. +When resuming the hook from an API route, the schema validates and transforms the incoming payload before the workflow resumes: ```typescript lineNumbers export async function POST(request: Request) { - // comment is " Ready! " here + // Incoming payload: { token: "...", approved: true, comment: " Ready! " } const { token, approved, comment } = await request.json(); - // If validation fails, Zod throws and the hook is not resumed. + // The schema validates and transforms the payload: + // - Checks that `approved` is a boolean + // - Checks that `comment` is a non-empty string + // - Trims whitespace from the comment + // If validation fails, an error is thrown and the hook is not resumed await approvalHook.resume(token, { // [!code highlight] approved, // [!code highlight] - comment, // transformed to "Ready!" [!code highlight] + comment, // Automatically trimmed to "Ready!" // [!code highlight] }); // [!code highlight] return Response.json({ success: true }); } ``` +#### Using Other Standard Schema Libraries + +The same pattern works with any Standard Schema v1 compliant library. Here's an example with [Valibot](https://valibot.dev): + +```typescript lineNumbers +import { defineHook } from "workflow"; +import * as v from "valibot"; + +export const approvalHook = defineHook({ + schema: v.object({ // [!code highlight] + approved: v.boolean(), // [!code highlight] + comment: v.pipe(v.string(), v.minLength(1), v.trim()), // [!code highlight] + }), // [!code highlight] +}); +``` + ### Customizing Tokens Tokens are used to identify a specific hook and for resuming a hook. You can customize the token to be more specific to a use case. diff --git a/docs/content/docs/foundations/hooks.mdx b/docs/content/docs/foundations/hooks.mdx index 9479ab7a..a84224bf 100644 --- a/docs/content/docs/foundations/hooks.mdx +++ b/docs/content/docs/foundations/hooks.mdx @@ -362,20 +362,21 @@ async function postToSlack(channelId: string, message: string) { ### Type-Safe Hooks with `defineHook()` -The [`defineHook()`](/docs/api-reference/workflow/define-hook) helper provides type safety between creating and resuming hooks: +The [`defineHook()`](/docs/api-reference/workflow/define-hook) helper provides type safety and runtime validation between creating and resuming hooks using [Standard Schema v1](https://standardschema.dev). Use any compliant validator like Zod or Valibot: ```typescript lineNumbers import { defineHook } from "workflow"; - -// Define the hook type once -type ApprovalRequest = { - requestId: string; - approved: boolean; - approvedBy: string; - comment: string; -}; - -const approvalHook = defineHook(); +import { z } from "zod"; + +// Define the hook with schema for type safety and runtime validation +const approvalHook = defineHook({ // [!code highlight] + schema: z.object({ // [!code highlight] + requestId: z.string(), // [!code highlight] + approved: z.boolean(), // [!code highlight] + approvedBy: z.string(), // [!code highlight] + comment: z.string().transform((value) => value.trim()), // [!code highlight] + }), // [!code highlight] +}); // [!code highlight] // In your workflow export async function documentApprovalWorkflow(documentId: string) { @@ -385,24 +386,25 @@ export async function documentApprovalWorkflow(documentId: string) { token: `approval:${documentId}` }); + // Payload is type-safe and validated const approval = await hook; console.log(`Document ${approval.requestId} ${approval.approved ? "approved" : "rejected"}`); console.log(`By: ${approval.approvedBy}, Comment: ${approval.comment}`); } -// In your API route - TypeScript ensures the payload matches! +// In your API route - both type-safe and runtime-validated! export async function POST(request: Request) { const { documentId, ...approvalData } = await request.json(); - // This is type-safe - TypeScript knows the exact shape required + // The schema validates the payload before resuming the workflow await approvalHook.resume(`approval:${documentId}`, approvalData); return new Response("OK"); } ``` -This pattern is especially valuable in larger applications where the workflow and API code are in separate files. +This pattern is especially valuable in larger applications where the workflow and API code are in separate files, providing both compile-time type safety and runtime validation. ## Best Practices