Skip to content

Commit

Permalink
events: support non-zod validator
Browse files Browse the repository at this point in the history
  • Loading branch information
thdxr committed Dec 31, 2023
1 parent ceed328 commit af704d7
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 56 deletions.
34 changes: 34 additions & 0 deletions .changeset/funny-icons-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
"sst": minor
---

There is a slight breaking change in this release if you are using SST Events with `createEventBuilder()` - you should receive type errors for all the issues. We now support specifying any validation library so will need to configure that.

To continue using Zod you can specify the validator like so

```
import { createEventBuilder, ZodValidator } from "sst/node/event-bus"
const event = createEventBuilder({
bus: "MyBus",
validator: ZodValidator
})
```

Additionally we no longer assume you are passing in a zod object as the schema.
You'll have to update code from:

```
const MyEvent = event("my.event", {
foo: z.string(),
})
```

to this:

```
const MyEvent = event("my.event", z.object({
foo: z.string(),
}))
```

This also allows you to specify non-objects as the event properties. Additionally, if you were using advanced inference the `shape` field has been replaced with `typeof MyEvent.$input`, `typeof MyEvent.$output`, and `typeof MyEvent.$metadata`
3 changes: 2 additions & 1 deletion examples/quickstart-standalone/packages/core/src/event.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createEventBuilder } from "sst/node/event-bus";
import { createEventBuilder, ZodValidator } from "sst/node/event-bus";

export const event = createEventBuilder({
bus: "bus",
validator: ZodValidator,
});
9 changes: 6 additions & 3 deletions examples/quickstart-standalone/packages/core/src/todo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import crypto from "crypto";
import { event } from "./event";

export const Events = {
Created: event("todo.created", {
id: z.string(),
}),
Created: event(
"todo.created",
z.object({
id: z.string(),
})
),
};

export async function create() {
Expand Down
3 changes: 2 additions & 1 deletion examples/standard-api/packages/core/src/event.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createEventBuilder } from "sst/node/event-bus";
import { createEventBuilder, ZodValidator } from "sst/node/event-bus";

export const event = createEventBuilder({
bus: "bus",
validator: ZodValidator,
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createEventBuilder } from "sst/node/event-bus";
import { createEventBuilder, ZodValidator } from "sst/node/event-bus";

export const event = createEventBuilder({
bus: "bus",
validator: ZodValidator,
});
147 changes: 100 additions & 47 deletions packages/sst/src/node/event-bus/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
PutEventsRequestEntry,
} from "@aws-sdk/client-eventbridge";
import { EventBridgeEvent } from "aws-lambda";
import { ZodAny, ZodObject, ZodRawShape, z } from "zod";
import { ZodAny, ZodObject, ZodRawShape, ZodSchema, z } from "zod";
import { useLoader } from "../util/loader.js";
import { Config } from "../config/index.js";

Expand All @@ -25,35 +25,33 @@ import { Config } from "../config/index.js";
*/
export { PutEventsCommandOutput };

const client = new EventBridgeClient({});

export function createEventBuilder<
Bus extends keyof typeof EventBus,
MetadataShape extends ZodRawShape | undefined,
MetadataFunction extends () => any
>(props: {
MetadataFunction extends () => any,
Validator extends (schema: any) => (input: any) => any,
MetadataSchema extends Parameters<Validator>[0]
>(input: {
bus: Bus;
metadata?: MetadataShape;
metadata?: MetadataSchema;
metadataFn?: MetadataFunction;
validator: Validator;
}) {
return function createEvent<
const client = new EventBridgeClient({});
const validator = input.validator;
const metadataValidator = input.metadata ? validator(input.metadata) : null;
return function event<
Type extends string,
Shape extends ZodRawShape,
Properties = z.infer<ZodObject<Shape, "strip", ZodAny>>
>(type: Type, properties: Shape) {
type Publish = undefined extends MetadataShape
? (properties: Properties) => Promise<PutEventsCommandOutput>
Schema extends Parameters<Validator>[0]
>(type: Type, schema: Schema) {
type Parsed = inferParser<Schema>;
type Publish = undefined extends MetadataSchema
? (properties: Parsed["in"]) => Promise<PutEventsCommandOutput>
: (
properties: Properties,
metadata: z.infer<
ZodObject<Exclude<MetadataShape, undefined>, "strip", ZodAny>
>
properties: Parsed["in"],
metadata: inferParser<MetadataSchema>["in"]
) => Promise<void>;
const propertiesSchema = z.object(properties);
const metadataSchema = props.metadata
? z.object(props.metadata)
: undefined;
const publish = async (properties: any, metadata: any) => {
const validate = validator(schema);
async function publish(properties: any, metadata: any) {
const result = await useLoader(
"sst.bus.publish",
async (input: PutEventsRequestEntry[]) => {
Expand Down Expand Up @@ -88,53 +86,108 @@ export function createEventBuilder<
// @ts-expect-error
Source: Config.APP,
Detail: JSON.stringify({
properties: propertiesSchema.parse(properties),
properties: validate(properties),
metadata: (() => {
if (metadataSchema) {
return metadataSchema.parse(metadata);
if (metadataValidator) {
return metadataValidator(metadata);
}

if (props.metadataFn) {
return props.metadataFn();
if (input.metadataFn) {
return input.metadataFn();
}
})(),
}),
DetailType: type,
});
return result;
};

}
return {
publish: publish as Publish,
type,
shape: {
metadata: {} as Parameters<Publish>[1],
properties: {} as Properties,
metadataFn: {} as ReturnType<MetadataFunction>,
},
$input: {} as Parsed["in"],
$output: {} as Parsed["out"],
$metadata: {} as ReturnType<MetadataFunction>,
};
};
}

export function ZodValidator<Schema extends ZodSchema>(
schema: Schema
): (input: z.input<Schema>) => z.output<Schema> {
return (input) => {
return schema.parse(input);
};
}

// Taken from tRPC
export type ParserZodEsque<TInput, TParsedInput> = {
_input: TInput;
_output: TParsedInput;
};

export type ParserValibotEsque<TInput, TParsedInput> = {
_types?: {
input: TInput;
output: TParsedInput;
};
};

export type ParserMyZodEsque<TInput> = {
parse: (input: any) => TInput;
};

export type ParserSuperstructEsque<TInput> = {
create: (input: unknown) => TInput;
};

export type ParserCustomValidatorEsque<TInput> = (
input: unknown
) => Promise<TInput> | TInput;

export type ParserYupEsque<TInput> = {
validateSync: (input: unknown) => TInput;
};

export type ParserScaleEsque<TInput> = {
assert(value: unknown): asserts value is TInput;
};

export type ParserWithoutInput<TInput> =
| ParserCustomValidatorEsque<TInput>
| ParserMyZodEsque<TInput>
| ParserScaleEsque<TInput>
| ParserSuperstructEsque<TInput>
| ParserYupEsque<TInput>;

export type ParserWithInputOutput<TInput, TParsedInput> =
| ParserZodEsque<TInput, TParsedInput>
| ParserValibotEsque<TInput, TParsedInput>;

export type Parser = ParserWithInputOutput<any, any> | ParserWithoutInput<any>;

export type inferParser<TParser extends Parser> =
TParser extends ParserWithInputOutput<infer $TIn, infer $TOut>
? {
in: $TIn;
out: $TOut;
}
: TParser extends ParserWithoutInput<infer $InOut>
? {
in: $InOut;
out: $InOut;
}
: never;

export type inferEvent<T extends { shape: ZodObject<any> }> = z.infer<
T["shape"]
>;

type Event = {
type: string;
shape: {
properties: any;
metadata: any;
metadataFn: any;
};
};
type Event = ReturnType<ReturnType<typeof createEventBuilder>>;

export type EventPayload<E extends Event> = {
type EventPayload<E extends Event> = {
type: E["type"];
properties: E["shape"]["properties"];
metadata: undefined extends E["shape"]["metadata"]
? E["shape"]["metadataFn"]
: E["shape"]["metadata"];
properties: E["$output"];
metadata: E["$metadata"];
attempts: number;
};

Expand Down
7 changes: 4 additions & 3 deletions www/docs/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,11 @@ This configuration will retry all subscriber errors up to 10 times (with exponen
The template also creates an `event.ts` which creates an `event` function that can be used to define events.

```ts title="/packages/core/src/event.ts"
import { createEventBuilder } from "sst/node/event-bus";
import { createEventBuilder, ZodValidator } from "sst/node/event-bus";

export const event = createEventBuilder({
bus: "bus",
validator: ZodValidator
});
```

Expand All @@ -73,9 +74,9 @@ In your application you can define events. This definition provides validation u
import { event } from "./event";

export const Events = {
Created: event("todo.created", {
Created: event("todo.created", z.object({
id: z.string(),
}),
})),
};
```
---
Expand Down

0 comments on commit af704d7

Please sign in to comment.