Zod guide and better handling of Zod event parsing errors

This commit is contained in:
Eric Allam
2023-01-23 21:11:21 +00:00
parent ada6e43bb7
commit 39b167ed1f
8 changed files with 359 additions and 15 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Better handle event parsing errors from Zod
+8 -5
View File
@@ -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
View File
@@ -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.
+6 -5
View File
@@ -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({
+1 -1
View File
@@ -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) => {
+2 -1
View File
@@ -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"
}
}
+28 -1
View File
@@ -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);
+8
View File
@@ -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==}