Skip to content

Commit 0033c1a

Browse files
feat: include detailed validation errors in parseEvent (#73)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 75c25ee commit 0033c1a

3 files changed

Lines changed: 151 additions & 6 deletions

File tree

.changeset/light-trains-work.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ventyd": patch
3+
---
4+
5+
detailed validation faield error

src/standard.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,19 @@ export function standard<
7373
return (context) => {
7474
return {
7575
parseEvent(input) {
76-
for (const eventSchema of Object.values(args.event)) {
77-
try {
78-
return standardValidate(eventSchema, input);
79-
} catch {}
76+
const record = input !== null && typeof input === "object" ? (input as Record<string, unknown>) : null;
77+
const eventName = typeof record?.eventName === "string" ? record.eventName : undefined;
78+
79+
if (!eventName) {
80+
throw new Error("Validation failed: eventName is missing");
81+
}
82+
83+
if (!args.event[eventName]) {
84+
const availableEvents = Object.keys(args.event).join(", ");
85+
throw new Error(`Validation failed: event "${eventName}" is not declared. Available events: ${availableEvents}`);
8086
}
8187

82-
throw new Error("Validation failed");
88+
return standardValidate(args.event[eventName], input, eventName) as $$EventType;
8389
},
8490
parseEventByName<K extends $$EventType["eventName"]>(
8591
eventName: K,

test/standard.spec.ts

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ describe("Standard Schema Provider", () => {
299299
entityId: "usr-123",
300300
body: {},
301301
});
302-
}).toThrow("Validation failed");
302+
}).toThrow('event "user:deleted" is not declared');
303303
});
304304
});
305305

@@ -339,6 +339,140 @@ describe("Standard Schema Provider", () => {
339339
});
340340
}).toThrow('Validation failed: "user:created"');
341341
});
342+
343+
test("parseEvent should include event name and field details when eventName matches known schema", () => {
344+
const schema = defineSchema("user", {
345+
schema: standard({
346+
event: {
347+
"user:created": v.object({
348+
eventId: v.string(),
349+
eventName: v.literal("user:created"),
350+
eventCreatedAt: v.string(),
351+
entityName: v.string(),
352+
entityId: v.string(),
353+
body: v.object({
354+
email: v.pipe(v.string(), v.email()),
355+
age: v.pipe(v.number(), v.minValue(13)),
356+
}),
357+
}),
358+
},
359+
state: v.object({ email: v.string() }),
360+
}),
361+
initialEventName: "user:created",
362+
});
363+
364+
expect(() => {
365+
schema.parseEvent({
366+
eventId: "evt-123",
367+
eventName: "user:created",
368+
eventCreatedAt: new Date().toISOString(),
369+
entityName: "user",
370+
entityId: "usr-123",
371+
body: {
372+
email: "not-an-email",
373+
age: 10,
374+
},
375+
});
376+
}).toThrow('Validation failed: "user:created"');
377+
});
378+
379+
test("parseEvent should include field-level error paths in message", () => {
380+
const schema = defineSchema("user", {
381+
schema: standard({
382+
event: {
383+
"user:created": v.object({
384+
eventId: v.string(),
385+
eventName: v.literal("user:created"),
386+
eventCreatedAt: v.string(),
387+
entityName: v.string(),
388+
entityId: v.string(),
389+
body: v.object({
390+
email: v.pipe(v.string(), v.email()),
391+
}),
392+
}),
393+
},
394+
state: v.object({ email: v.string() }),
395+
}),
396+
initialEventName: "user:created",
397+
});
398+
399+
let errorMessage = "";
400+
try {
401+
schema.parseEvent({
402+
eventId: "evt-123",
403+
eventName: "user:created",
404+
eventCreatedAt: new Date().toISOString(),
405+
entityName: "user",
406+
entityId: "usr-123",
407+
body: { email: "not-an-email" },
408+
});
409+
} catch (e) {
410+
errorMessage = (e as Error).message;
411+
}
412+
413+
expect(errorMessage).toContain("body.email");
414+
});
415+
416+
test("parseEvent should throw when eventName is undeclared", () => {
417+
const schema = defineSchema("user", {
418+
schema: standard({
419+
event: {
420+
"user:created": v.object({
421+
eventId: v.string(),
422+
eventName: v.literal("user:created"),
423+
eventCreatedAt: v.string(),
424+
entityName: v.string(),
425+
entityId: v.string(),
426+
body: v.object({ email: v.string() }),
427+
}),
428+
"user:updated": v.object({
429+
eventId: v.string(),
430+
eventName: v.literal("user:updated"),
431+
eventCreatedAt: v.string(),
432+
entityName: v.string(),
433+
entityId: v.string(),
434+
body: v.object({ nickname: v.string() }),
435+
}),
436+
},
437+
state: v.object({ email: v.string() }),
438+
}),
439+
initialEventName: "user:created",
440+
});
441+
442+
expect(() => {
443+
schema.parseEvent({
444+
eventId: "evt-123",
445+
eventName: "user:deleted",
446+
eventCreatedAt: new Date().toISOString(),
447+
entityName: "user",
448+
entityId: "usr-123",
449+
body: {},
450+
});
451+
}).toThrow('event "user:deleted" is not declared');
452+
});
453+
454+
test("parseEvent should throw when eventName is missing", () => {
455+
const schema = defineSchema("user", {
456+
schema: standard({
457+
event: {
458+
"user:created": v.object({
459+
eventId: v.string(),
460+
eventName: v.literal("user:created"),
461+
eventCreatedAt: v.string(),
462+
entityName: v.string(),
463+
entityId: v.string(),
464+
body: v.object({ email: v.string() }),
465+
}),
466+
},
467+
state: v.object({ email: v.string() }),
468+
}),
469+
initialEventName: "user:created",
470+
});
471+
472+
expect(() => {
473+
schema.parseEvent({ someRandomField: "value" });
474+
}).toThrow("eventName is missing");
475+
});
342476
});
343477

344478
describe("Type inference", () => {

0 commit comments

Comments
 (0)