Skip to content
Open
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/hungry-kangaroos-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"inngest": patch
---

Allow branded types in eventType schemas
50 changes: 50 additions & 0 deletions packages/inngest/src/components/triggers/trigger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,56 @@ describe("eventType with schema", () => {
},
);
});

test("branded object type", () => {
const inngest = new Inngest({ id: "app" });
inngest.createFunction(
{
id: "fn",
triggers: [
eventType("event-1", {
schema: z.object({
animal: z.object({ name: z.string() }).brand<"Cat">(),
}),
}),
],
},
({ event }) => {
expectTypeOf(event.name).not.toBeAny();
expectTypeOf(event.name).toEqualTypeOf<
"event-1" | "inngest/function.invoked"
>();

expectTypeOf(event.data).not.toBeAny();
expectTypeOf(event.data).toEqualTypeOf<{ animal: { name: string } }>();
},
);
});

test("branded primitive type", () => {
const inngest = new Inngest({ id: "app" });
inngest.createFunction(
{
id: "fn",
triggers: [
eventType("event-1", {
schema: z.object({
someId: z.string().uuid().brand<"something">(),
}),
}),
],
},
({ event }) => {
expectTypeOf(event.name).not.toBeAny();
expectTypeOf(event.name).toEqualTypeOf<
"event-1" | "inngest/function.invoked"
>();

expectTypeOf(event.data).not.toBeAny();
expectTypeOf(event.data).toEqualTypeOf<{ someId: string }>();
},
);
});
});

test("eventType with version", () => {
Expand Down
39 changes: 28 additions & 11 deletions packages/inngest/src/components/triggers/triggers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,23 +201,40 @@ export class EventType<
*/
type StaticTypeError<TMessage extends string> = TMessage;

/**
* Strip symbol-keyed properties from a type. This is used to ignore type-only
* branding (e.g. Zod's `BRAND`) when comparing schema input/output types.
*/
type StripSymbolKeys<T> = {
[K in keyof T as K extends symbol ? never : K]: StripSymbolKeys<T[K]>;
};
Comment on lines +208 to +210
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug (P0): Missing base case for primitives causes StripSymbolKeys<string> and StripSymbolKeys<number> to both resolve to {}, so IsEqualIgnoringBrands<string, number> returns true and transforms on primitive fields are silently allowed.

Suggested change
Suggested change
type StripSymbolKeys<T> = {
[K in keyof T as K extends symbol ? never : K]: StripSymbolKeys<T[K]>;
};
type StripSymbolKeys<T> = T extends object
? { [K in keyof T as K extends symbol ? never : K]: StripSymbolKeys<T[K]> }
: T;
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/inngest/src/components/triggers/triggers.ts, line 208:

<issue>
Missing base case for primitives causes `StripSymbolKeys<string>` and `StripSymbolKeys<number>` to both resolve to `{}`, so `IsEqualIgnoringBrands<string, number>` returns `true` and transforms on primitive fields are silently allowed.
</issue>


/**
* Check if two types are structurally equal, ignoring symbol-keyed properties
* (e.g. Zod's `BRAND`).
*/
type IsEqualIgnoringBrands<A, B> = [StripSymbolKeys<A>] extends [
StripSymbolKeys<B>,
]
? // Wrapped in tuples to prevent distributive conditional over union types.
[StripSymbolKeys<B>] extends [StripSymbolKeys<A>]
? true
: false
: false;

/**
* Ensure that users don't use transforms in their schemas, since we don't
* support transforms.
* support transforms. Branded types (which only add symbol-keyed properties)
* are allowed because they don't affect runtime values.
*/
type AssertNoTransform<TSchema extends StandardSchemaV1 | undefined> =
TSchema extends undefined
? // Undefined schema is OK
undefined
? undefined
: TSchema extends StandardSchemaV1<infer TInput, infer TOutput>
? // Wrap in tuples to prevent distributive conditional over union types. This ensures that the schema can be a union.
[TInput] extends [TOutput]
? // Input and output schemas match, so we're good
TSchema
: // Return an error message since the input and output schemas don't match
StaticTypeError<"Transforms not supported: schema input/output types must match">
: // Return an error message since the schema is not a StandardSchemaV1
StaticTypeError<"Transforms not supported: schema input/output types must match">;
? IsEqualIgnoringBrands<TInput, TOutput> extends true
? TSchema
: StaticTypeError<"Transforms not supported: schema input/output types must match">
: StaticTypeError<"Transforms not supported: schema input/output types must match">;

/**
* Create an event type definition that can be used as a trigger and for
Expand Down
Loading