Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/easy-donkeys-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@workflow/core": patch
---

Make `@standard-schema/spec` be a regular dependency
15 changes: 8 additions & 7 deletions docs/content/docs/api-reference/workflow/define-hook.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -116,20 +116,21 @@ export async function POST(request: Request) {
```

### Validate and Transform with Schema
The optional `schema` accepts any validator that implements the [Standard Schema v1](https://standardschema.dev/) contract.

The optional `schema` accepts any validator that conforms to [Standard Schema v1](https://standardschema.dev).

Zod is shown below as one example, but libraries like Valibot, ArkType, Effect Schema, or your own custom validator work as well.

```typescript lineNumbers
import { defineHook } from "workflow";
import { z } from "zod";

const approvalSchema = z.object({
approved: z.boolean(),
comment: z.string().min(1).transform((value) => value.trim()),
});

export const approvalHook = defineHook({
// Provide a schema to validate/transform payloads.
schema: approvalSchema, // [!code highlight]
schema: z.object({ // [!code highlight]
approved: z.boolean(), // [!code highlight]
comment: z.string().min(1).transform((value) => value.trim()), // [!code highlight]
}), // [!code highlight]
});

export async function approvalWorkflow(approvalId: string) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
},
"dependencies": {
"@aws-sdk/credential-provider-web-identity": "3.609.0",
"@standard-schema/spec": "^1.0.0",
"@types/ms": "^2.1.0",
"@vercel/functions": "catalog:",
"@workflow/errors": "workspace:*",
Expand All @@ -64,7 +65,6 @@
},
"devDependencies": {
"@opentelemetry/api": "^1.9.0",
"@standard-schema/spec": "^1.0.0",
"@types/debug": "^4.1.12",
"@types/node": "catalog:",
"@types/seedrandom": "^3.0.8",
Expand Down
31 changes: 15 additions & 16 deletions packages/core/src/define-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { resumeHook } from './runtime/resume-hook.js';
/**
* Defines a typed hook for type-safe hook creation and resumption.
*
* This helper provides type safety by allowing you to define the payload type once
* and reuse it when creating hooks and resuming them.
* This helper provides type safety by allowing you to define the input and output types
* for the hook's payload, with optional validation and transformation via a schema.
*
* @param schema - Schema used to validate and transform the payload before resuming
* @returns An object with `create` and `resume` functions pre-typed with the payload type
* @param schema - Schema used to validate and transform the input payload before resuming
* @returns An object with `create` and `resume` functions pre-typed with the input and output types
*
* @example
*
Expand All @@ -23,49 +23,48 @@ import { resumeHook } from './runtime/resume-hook.js';
* "use workflow";
*
* const hook = approvalHook.create();
* const result = await hook; // Fully typed as { approved: boolean; comment: string }
* const result = await hook; // Fully typed as { approved: boolean; comment: string; }
* }
*
* // In an API route
* export async function POST(request: Request) {
* const { token, approved, comment } = await request.json();
* await approvalHook.resume(token, { approved, comment });
* await approvalHook.resume(token, { approved, comment }); // Input type
* return Response.json({ success: true });
* }
* ```
*/
export function defineHook<T>({
export function defineHook<TInput, TOutput = TInput>({
schema,
}: {
schema?: StandardSchemaV1<T, T>;
schema?: StandardSchemaV1<TInput, TOutput>;
} = {}) {
return {
/**
* Creates a new hook with the defined payload type.
* Creates a new hook with the defined output type.
*
* Note: This method is not available in runtime bundles. Use it from workflow contexts only.
*
* @param _options - Optional hook configuration
* @returns A Hook that resolves to the defined payload type
* @returns A Hook that resolves to the defined output type
*/
// @ts-expect-error `options` is here for types/docs
create(options?: HookOptions): Hook<T> {
create(_options?: HookOptions): Hook<TOutput> {
throw new Error(
'`defineHook().create()` can only be called inside a workflow function.'
);
},

/**
* Resumes a hook by sending a payload with the defined type.
* Resumes a hook by sending a payload with the defined input type.
* This is a type-safe wrapper around the `resumeHook` runtime function.
*
* @param token - The unique token identifying the hook
* @param payload - The payload to send; if a `schema` is configured it is validated/transformed before resuming
* @returns Promise resolving to the hook entity, or null if the hook doesn't exist
*/
async resume(token: string, payload: T): Promise<HookEntity | null> {
async resume(token: string, payload: TInput): Promise<HookEntity | null> {
if (!schema?.['~standard']) {
return await resumeHook<T>(token, payload);
return await resumeHook(token, payload);
}

let result = schema['~standard'].validate(payload);
Expand All @@ -78,7 +77,7 @@ export function defineHook<T>({
throw new Error(JSON.stringify(result.issues, null, 2));
}

return await resumeHook<T>(token, result.value);
return await resumeHook<TOutput>(token, result.value);
},
};
}
8 changes: 4 additions & 4 deletions packages/core/src/workflow/define-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { createHook } from './create-hook.js';
/**
* NOTE: This is the implementation of `defineHook()` that is used in workflow contexts.
*/
export function defineHook<T>() {
export function defineHook<TInput, TOutput = TInput>() {
return {
create(options?: HookOptions): Hook<T> {
return createHook<T>(options);
create(options?: HookOptions): Hook<TOutput> {
return createHook<TOutput>(options);
},

resume(_token: string, _payload: T): Promise<HookEntity | null> {
resume(_token: string, _payload: TInput): Promise<HookEntity | null> {
throw new Error(
'`defineHook().resume()` can only be called from external contexts (e.g. API routes).'
);
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.