Zod guide and better handling of Zod event parsing errors
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Better handle event parsing errors from Zod
|
||||
@@ -201,13 +201,13 @@ Triggers are what cause your workflows to run.
|
||||
<Card title="Webhooks" icon="rectangle-terminal" href="/triggers/webhooks">
|
||||
Easily subscribe to the APIs you're using
|
||||
</Card>
|
||||
<Card title="Scheduled" icon="sliders" href="/triggers/scheduled">
|
||||
<Card title="Scheduled" icon="clock" href="/triggers/scheduled">
|
||||
Trigger your workflows on a repeating schedule
|
||||
</Card>
|
||||
<Card title="Custom events" icon="heading" href="/triggers/custom-events">
|
||||
<Card title="Custom events" icon="code" href="/triggers/custom-events">
|
||||
More details on custom events
|
||||
</Card>
|
||||
<Card title="More coming soon" icon="heading">
|
||||
<Card title="More coming soon" icon="bookmark">
|
||||
On received email, HTTP endpoint and AWS Event Bridge
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -222,11 +222,14 @@ Triggers are what cause your workflows to run.
|
||||
>
|
||||
We are making it easy to use lots of APIs by adding integrations.
|
||||
</Card>
|
||||
<Card title="Delays" icon="sliders" href="/functions/delays">
|
||||
<Card title="Fetch" icon="server" href="/functions/fetch">
|
||||
Call any API from your workflow
|
||||
</Card>
|
||||
<Card title="Delays" icon="alarm-clock" href="/functions/delays">
|
||||
Add delays to your workflows. They're resilient so it doesn't matter if your
|
||||
server goes down.
|
||||
</Card>
|
||||
<Card title="Send event" icon="heading" href="/functions/send-event">
|
||||
<Card title="Send event" icon="code" href="/functions/send-event">
|
||||
Send an event, to trigger a custom event workflow
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
+301
-2
@@ -1,7 +1,306 @@
|
||||
---
|
||||
title: "Zod"
|
||||
title: "Zod Guide"
|
||||
sidebarTitle: "Zod"
|
||||
description: "TypeScript-first schema validation with static type inference"
|
||||
---
|
||||
|
||||
## Coming soon
|
||||
## Intro
|
||||
|
||||
Zod is a fantastic utility package by [@colinhacks](https://twitter.com/colinhacks) that allows for defining runtime schema validation and type-safety.
|
||||
|
||||
We use it [extensively](https://github.com/search?q=repo%3Atriggerdotdev%2Ftrigger.dev+%22zod%22%3B&type=code) internally at Trigger.dev to ensure we never trigger your workflows with invalid data (and more).
|
||||
|
||||
But there are a few places where we ask you to provide us with a Zod schema, for example when defining a [Custom Event Trigger](/triggers/custom-events):
|
||||
|
||||
```ts
|
||||
import { Trigger, customEvent } from "@trigger.dev/sdk";
|
||||
import { slack } from "@trigger.dev/integrations";
|
||||
import { z } from "zod";
|
||||
|
||||
//this workflow will run when a "user.created" event is sent
|
||||
new Trigger({
|
||||
id: "zod",
|
||||
name: "Zod custom event",
|
||||
//this is the custom event subscription
|
||||
on: customEvent({
|
||||
name: "user.created",
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
paidPlan: z.boolean(),
|
||||
}),
|
||||
}),
|
||||
//this function is run when the custom event is received
|
||||
run: async (event, ctx) => {
|
||||
console.log(event); // Has name, email, paidPlan
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
|
||||
So it will help to know a little about Zod and how to use it. We definitely recommend the well written [Zod README](https://github.com/colinhacks/zod#readme) but we've included a short primer below.
|
||||
|
||||
<Tip>
|
||||
Wherever we require you to pass in a Zod schema, you can always start with
|
||||
`z.any()` which accepts `any` type and then add more strict validations later.
|
||||
</Tip>
|
||||
|
||||
## Basic Usage
|
||||
|
||||
There are three main steps to using Zod:
|
||||
|
||||
1. Define a schema
|
||||
|
||||
```ts
|
||||
import { z } from "zod";
|
||||
|
||||
const mySchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
paidPlan: z.boolean(),
|
||||
});
|
||||
```
|
||||
|
||||
2. Infer TypeScript types from the schema
|
||||
|
||||
```ts
|
||||
type MySchema = z.infer<typeof mySchema>;
|
||||
```
|
||||
|
||||
3. Validate data against the schema
|
||||
|
||||
```ts
|
||||
const data: unknown = {
|
||||
name: "Eric",
|
||||
email: "eric@trigger.dev",
|
||||
paidPlan: true,
|
||||
};
|
||||
|
||||
const result = mySchema.parse(data);
|
||||
```
|
||||
|
||||
When using Zod with Trigger.dev, you'll only really need to do the first step, and by passing it to us (through the `schema` property of the `customEvent` function), we'll do the second and third steps for you.
|
||||
|
||||
## Defining Schemas
|
||||
|
||||
### Primitives
|
||||
|
||||
Zod schemas are a way to define the shape of an object. They can be as simple as a single type, or as complex as a nested object.
|
||||
|
||||
```ts
|
||||
// Primitives
|
||||
z.string();
|
||||
z.number();
|
||||
z.boolean();
|
||||
z.date();
|
||||
z.undefined();
|
||||
z.null();
|
||||
```
|
||||
|
||||
Any schema can be marked as optional, which means the schema can be `undefined` or `null`:
|
||||
|
||||
```ts
|
||||
z.string().optional();
|
||||
```
|
||||
|
||||
Schemas can also be marked optional by providing a default value:
|
||||
|
||||
```ts
|
||||
const optionalString = z.string().default("default value");
|
||||
|
||||
const value = optionalString.parse(undefined); // value === "default value"
|
||||
```
|
||||
|
||||
If you need to allow a value to be `null`, you can use `nullable()`:
|
||||
|
||||
```ts
|
||||
const nullableString = z.string().nullable();
|
||||
|
||||
const value = nullableString.parse(null);
|
||||
```
|
||||
|
||||
You can also use Zod to coerce primites into other types. For example, you can coerce a string into a number:
|
||||
|
||||
```ts
|
||||
const numberString = z.coerce.number();
|
||||
|
||||
const value = numberString.parse("123"); // value === 123
|
||||
```
|
||||
|
||||
The following primitives are supported:
|
||||
|
||||
```ts
|
||||
z.coerce.string();
|
||||
z.coerce.number();
|
||||
z.coerce.boolean();
|
||||
z.coerce.bigint();
|
||||
z.coerce.date();
|
||||
```
|
||||
|
||||
Coercing dates are especially useful when you are receiving a `string` from an API and want to convert it to a JavaScript `Date` object:
|
||||
|
||||
```ts
|
||||
const date = z.coerce.date();
|
||||
|
||||
const value = date.parse("2021-01-01T00:00:00.000Z"); // value === Date object
|
||||
```
|
||||
|
||||
### Objects
|
||||
|
||||
Object schemas are the most common type of schema. They allow you to define the shape of an object, and the types of each property.
|
||||
|
||||
```ts
|
||||
const mySchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
paidPlan: z.boolean(),
|
||||
});
|
||||
```
|
||||
|
||||
All properties are required by default, although you can make them all optional using `partial()`:
|
||||
|
||||
```ts
|
||||
const mySchema = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
paidPlan: z.boolean(),
|
||||
})
|
||||
.partial();
|
||||
```
|
||||
|
||||
You can also make individual properties optional:
|
||||
|
||||
```ts
|
||||
const mySchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
paidPlan: z.boolean().optional(),
|
||||
});
|
||||
```
|
||||
|
||||
By default an object schema will strip out any extra properties that are not defined in the schema. You can disable this behavior using `passthrough()`:
|
||||
|
||||
```ts
|
||||
const mySchema = z.object({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
mySchema.parse({ name: "Eric", email: "eric@trigger.dev" }); // { name: "Eric" }
|
||||
mySchema.passthrough().parse({ name: "Eric", email: "eric@trigger.dev" }); // { name: "Eric", email: "eric@trigger.dev" }
|
||||
```
|
||||
|
||||
Or you can use `strict()` to make the schema throw an error if there are any extra properties:
|
||||
|
||||
```ts
|
||||
const mySchema = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
mySchema.parse({ name: "Eric", email: "eric@trigger.dev" }); // throws error
|
||||
```
|
||||
|
||||
Zod includes a few useful object schema utilities to help with reusing schemas, `extends()` and `merge()`:
|
||||
|
||||
```ts
|
||||
const baseSchema = z.object({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
const extendedSchema = baseSchema.extend({
|
||||
email: z.string(),
|
||||
});
|
||||
|
||||
const mergedSchema = baseSchema.merge(
|
||||
z.object({
|
||||
email: z.string(),
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
You can also use `pick()` and `omit()` to create a new schema that only includes or excludes certain properties:
|
||||
|
||||
```ts
|
||||
const mySchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
paidPlan: z.boolean(),
|
||||
});
|
||||
|
||||
const pickedSchema = mySchema.pick({ name: true, email: true });
|
||||
const omittedSchema = mySchema.omit({ paidPlan: true });
|
||||
```
|
||||
|
||||
### Arrays
|
||||
|
||||
You can specify the schema of an array using `z.array()`:
|
||||
|
||||
```ts
|
||||
z.array(z.string()); // string[]
|
||||
z.array(z.number()); // number[]
|
||||
z.array(z.object({ name: z.string() })); // Array<{ name: string }>
|
||||
```
|
||||
|
||||
You can also specify the type of array that has a fixed number of elements using `z.tuple()`:
|
||||
|
||||
```ts
|
||||
z.tuple([z.string(), z.number(), z.boolean()]); // [string, number, boolean]
|
||||
```
|
||||
|
||||
### Unions
|
||||
|
||||
You can specify a union of schemas using `z.union()`:
|
||||
|
||||
```ts
|
||||
z.union([z.string(), z.number(), z.boolean()]); // string | number | boolean
|
||||
```
|
||||
|
||||
Discriminating unions are also supported, and especially useful when paired with type narrowing:
|
||||
|
||||
```ts
|
||||
const mySchema = z.discriminatingUnion("type", [
|
||||
z.object({
|
||||
type: z.literal("a"),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("b"),
|
||||
data: z.number(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const value = mySchema.parse({ type: "a", data: "hello" });
|
||||
|
||||
if (type.a) {
|
||||
// value is { type: "a", data: string }
|
||||
} else if (type.b) {
|
||||
// value is { type: "b", data: number }
|
||||
}
|
||||
```
|
||||
|
||||
### Records
|
||||
|
||||
You can specify a record of schemas using `z.record()`, useful for when you have a map of values but don't care about the keys:
|
||||
|
||||
```ts
|
||||
z.record(z.string()); // Record<string, string>
|
||||
z.record(z.number()); // Record<string, number>
|
||||
```
|
||||
|
||||
### JSON type
|
||||
|
||||
If you want to accept any valid JSON value, you can use the following schema:
|
||||
|
||||
```ts
|
||||
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
||||
type Literal = z.infer<typeof literalSchema>;
|
||||
type Json = Literal | { [key: string]: Json } | Json[];
|
||||
const jsonSchema: z.ZodType<Json> = z.lazy(() =>
|
||||
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
|
||||
);
|
||||
|
||||
jsonSchema.parse(data);
|
||||
```
|
||||
|
||||
Hat tip to [ggoodman](https://github.com/ggoodman) for this one.
|
||||
|
||||
@@ -4,27 +4,28 @@ sidebarTitle: "Custom events"
|
||||
description: "Custom event triggers allow you to run workflows from your own code (or your other workflows)"
|
||||
---
|
||||
|
||||
[Send an event](/functions/send-event) and any workflows that subscribe to that Custom event will get triggered.
|
||||
[Send an event](/functions/send-event) and any workflows that subscribe to that custom event will get triggered.
|
||||
|
||||
## Name and Schemas
|
||||
|
||||
### Name
|
||||
|
||||
Custom event triggers have a `name`. They will only get triggered when a custom even with that name are sent.
|
||||
Custom event triggers have a `name`. They will only get triggered when a custom event with that name are sent.
|
||||
|
||||
### Schema
|
||||
|
||||
Custom event triggers have a schema. This is used to validate the data that is sent with the event. If the data does not match the schema, the workflow will not run.
|
||||
Custom event triggers take a [Zod](https://github.com/colinhacks/zod) schema. This is used to validate the data that is sent with the event. If the data does not match the schema, the workflow will not run.
|
||||
|
||||
It also means that inside your run function you will have type safety for the data you receive. We use [Zod](https://github.com/colinhacks/zod#installation) for our schemas – it's a fantastic library that allows you to define schemas in a very simple way.
|
||||
It also means that inside your run function the event param will be typed correctly. We use [Zod](https://github.com/colinhacks/zod#installation) for our schemas – it's a fantastic library that allows you to define schemas in a very simple way.
|
||||
|
||||
You can always start out by using `z.any()` as your schema, and then later on you can add more strict validation.
|
||||
You can always start out by using `z.any()` as your schema, and then later on you can add more strict validation. See our [Zod guide](/guides/zod) for more information.
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
import { Trigger, customEvent } from "@trigger.dev/sdk";
|
||||
import { slack } from "@trigger.dev/integrations";
|
||||
import { z } from "zod";
|
||||
|
||||
//this workflow will run when a "user.created" event is sent
|
||||
new Trigger({
|
||||
|
||||
@@ -11,7 +11,7 @@ const trigger = new Trigger({
|
||||
name: "My workflow",
|
||||
apiKey: "trigger_dev_zC25mKNn6c0q",
|
||||
endpoint: "ws://localhost:8889/ws",
|
||||
logLevel: "log",
|
||||
logLevel: "debug",
|
||||
triggerTTL: 60 * 60 * 24,
|
||||
on: customEvent({ name: "user.created", schema: userCreatedEvent }),
|
||||
run: async (event, ctx) => {
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"ulid": "^2.3.0",
|
||||
"uuid": "^9.0.0",
|
||||
"ws": "^8.11.0",
|
||||
"zod": "^3.20.2"
|
||||
"zod": "^3.20.2",
|
||||
"zod-error": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,13 @@ import { triggerRunLocalStorage } from "./localStorage";
|
||||
import { ContextLogger } from "./logger";
|
||||
import { Trigger, TriggerOptions } from "./trigger";
|
||||
import { TriggerContext, TriggerFetch } from "./types";
|
||||
import { generateErrorMessage, ErrorMessageOptions } from "zod-error";
|
||||
|
||||
const zodErrorMessageOptions: ErrorMessageOptions = {
|
||||
delimiter: {
|
||||
error: " 🔥 ",
|
||||
},
|
||||
};
|
||||
|
||||
export class TriggerClient<TSchema extends z.ZodTypeAny> {
|
||||
#trigger: Trigger<TSchema>;
|
||||
@@ -292,6 +299,26 @@ export class TriggerClient<TSchema extends z.ZodTypeAny> {
|
||||
TRIGGER_WORKFLOW: async (data) => {
|
||||
this.#logger.debug("Handling TRIGGER_WORKFLOW", data);
|
||||
|
||||
const parsedEventData = this.#options.on.schema.safeParse(
|
||||
data.trigger.input
|
||||
);
|
||||
|
||||
if (!parsedEventData.success) {
|
||||
await serverRPC.send("SEND_WORKFLOW_ERROR", {
|
||||
runId: data.id,
|
||||
timestamp: String(highPrecisionTimestamp()),
|
||||
error: {
|
||||
name: "Event validation error",
|
||||
message: generateErrorMessage(
|
||||
parsedEventData.error.issues,
|
||||
zodErrorMessageOptions
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const fetchFunction: TriggerFetch = async (key, url, options) => {
|
||||
const result = new Promise<FetchOutput>((resolve, reject) => {
|
||||
this.#fetchCallbacks.set(messageKey(data.id, key), {
|
||||
@@ -399,7 +426,7 @@ export class TriggerClient<TSchema extends z.ZodTypeAny> {
|
||||
fetch: fetchFunction,
|
||||
};
|
||||
|
||||
const eventData = this.#options.on.schema.parse(data.trigger.input);
|
||||
const eventData = parsedEventData.data;
|
||||
|
||||
this.#logger.debug("Parsed event data", eventData);
|
||||
|
||||
|
||||
Generated
+8
@@ -770,6 +770,7 @@ importers:
|
||||
uuid: ^9.0.0
|
||||
ws: ^8.11.0
|
||||
zod: ^3.20.2
|
||||
zod-error: ^1.1.0
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
evt: 2.4.13
|
||||
@@ -778,6 +779,7 @@ importers:
|
||||
uuid: 9.0.0
|
||||
ws: 8.12.0
|
||||
zod: 3.20.2
|
||||
zod-error: 1.1.0
|
||||
devDependencies:
|
||||
'@trigger.dev/common-schemas': link:../common-schemas
|
||||
'@trigger.dev/tsconfig': link:../../config-packages/tsconfig
|
||||
@@ -16581,6 +16583,12 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
/zod-error/1.1.0:
|
||||
resolution: {integrity: sha512-zZ0I9/eFQsaDqglBPOM5Y/kJzbTkoD6hNZGON/kcMe2WIfLse2DIwsZpLMfKwBmcciYaEg+DubtLMnAbZ4/rfA==}
|
||||
dependencies:
|
||||
zod: 3.20.2
|
||||
dev: false
|
||||
|
||||
/zod/3.20.2:
|
||||
resolution: {integrity: sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user