Skip to content

Commit 08e1d66

Browse files
author
Pierre Milliotte
committed
feat: add zod parser
1 parent 9f6f382 commit 08e1d66

5 files changed

Lines changed: 205 additions & 38 deletions

File tree

packages/core/src/event/eventType.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
import type { EventDetail } from './eventDetail';
22
import { reservedEventTypes } from './reservedEventTypes';
33

4-
export type ParsedEventDetail<TYPE extends string, PAYLOAD, METADATA> =
5-
| {
6-
isValid: true;
7-
parsedEventDetail: EventDetail<TYPE, PAYLOAD, METADATA>;
8-
parsingErrors?: never;
9-
}
4+
export type ParsedCandidate<T> =
105
| {
116
isValid: false;
12-
parsedEventDetail?: never;
7+
parsedCandidate?: never;
138
parsingErrors: [Error, ...Error[]];
9+
}
10+
| {
11+
isValid: true;
12+
parsedCandidate: T;
13+
parsingErrors?: never;
1414
};
1515

16+
export type CandidateParser<T> = (candidate: EventDetail) => ParsedCandidate<T>;
17+
1618
export type EventDetailParser<
1719
TYPE extends string = string,
1820
PAYLOAD = unknown,
1921
METADATA = unknown,
20-
> = (candidate: EventDetail) => ParsedEventDetail<TYPE, PAYLOAD, METADATA>;
22+
> = CandidateParser<EventDetail<TYPE, PAYLOAD, METADATA>>;
2123

2224
export class EventType<
2325
TYPE extends string = string,
@@ -28,14 +30,14 @@ export class EventType<
2830
detail: EventDetail<TYPE, PAYLOAD, METADATA>;
2931
};
3032
type: TYPE;
31-
parseEventDetail?: EventDetailParser<TYPE, PAYLOAD>;
33+
parseEventDetail?: EventDetailParser<TYPE, PAYLOAD, METADATA> | undefined;
3234

3335
constructor({
3436
type,
3537
parseEventDetail,
3638
}: {
3739
type: TYPE;
38-
parseEventDetail?: EventDetailParser<TYPE, PAYLOAD>;
40+
parseEventDetail?: EventDetailParser<TYPE, PAYLOAD, METADATA> | undefined;
3941
}) {
4042
if (reservedEventTypes.has(type)) {
4143
throw new Error(

packages/core/src/eventStore/eventStore.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
/* eslint-disable max-lines */
22
import type { Aggregate } from '~/aggregate';
33
import type { EventDetail } from '~/event/eventDetail';
4-
import type {
5-
EventDetailParser,
6-
EventType,
7-
EventTypeDetails,
8-
} from '~/event/eventType';
4+
import type { EventType, EventTypeDetails } from '~/event/eventType';
95
import { GroupedEvent } from '~/event/groupedEvent';
106
import type { EventStorageAdapter } from '~/eventStorageAdapter';
117
import type { $Contravariant } from '~/utils';
128

9+
import { EventDetailParser } from '../event/eventType';
1310
import {
1411
EventDetailParserNotDefinedError,
1512
EventDetailTypeDoesNotExistError,

packages/core/src/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
export type { Aggregate } from './aggregate';
2-
export {
3-
EventType,
4-
EventDetailParser,
5-
ParsedEventDetail,
6-
} from './event/eventType';
2+
export { EventType, EventDetailParser } from './event/eventType';
73
export type { EventTypeDetail, EventTypeDetails } from './event/eventType';
84
export { GroupedEvent } from './event/groupedEvent';
95
export { __REPLAYED__, __AGGREGATE_EXISTS__ } from './event/reservedEventTypes';
Lines changed: 134 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,140 @@
1-
import type { z, ZodType } from 'zod';
1+
import { ZodType, ZodTypeAny } from 'zod';
22

3-
import { EventType } from '@castore/core';
3+
import { EventDetail, EventType, EventDetailParser } from '@castore/core';
4+
5+
const zodPayloadParser =
6+
<PAYLOAD>(payloadSchema: ZodType<PAYLOAD>) =>
7+
(eventDetail: EventDetail) => {
8+
const parsedPayload = payloadSchema.safeParse(eventDetail.payload);
9+
10+
if (parsedPayload.success) {
11+
return {
12+
isValid: true,
13+
parsedCandidate: {
14+
...eventDetail,
15+
payload: parsedPayload.data,
16+
},
17+
};
18+
}
19+
20+
return {
21+
isValid: false,
22+
parsingErrors: parsedPayload.error.errors.map(
23+
({ message }) => new Error(message),
24+
),
25+
};
26+
};
27+
28+
const zodMetadataParser =
29+
<METADATA>(metadataSchema: ZodType<METADATA>) =>
30+
(eventDetail: EventDetail) => {
31+
const parsedMetadata = metadataSchema.safeParse(eventDetail.metadata);
32+
33+
if (parsedMetadata.success) {
34+
return {
35+
isValid: true,
36+
parsedCandidate: {
37+
...eventDetail,
38+
metadata: parsedMetadata.data,
39+
},
40+
};
41+
}
42+
43+
return {
44+
isValid: false,
45+
parsingErrors: parsedMetadata.error.errors.map(
46+
({ message }) => new Error(message),
47+
),
48+
};
49+
};
50+
51+
/* eslint-disable complexity */
52+
const zodEventDetailParser = <TYPE extends string, PAYLOAD, METADATA>(
53+
payloadSchema: ZodType<PAYLOAD> | undefined,
54+
metadataSchema: ZodType<METADATA> | undefined,
55+
): EventDetailParser<TYPE, PAYLOAD, METADATA> | undefined => {
56+
if (payloadSchema === undefined && metadataSchema === undefined) {
57+
return undefined;
58+
}
59+
60+
if (metadataSchema === undefined && payloadSchema !== undefined) {
61+
return zodPayloadParser(payloadSchema) as EventDetailParser<
62+
TYPE,
63+
PAYLOAD,
64+
METADATA
65+
>;
66+
}
67+
68+
if (metadataSchema !== undefined && payloadSchema === undefined) {
69+
return zodMetadataParser(metadataSchema) as EventDetailParser<
70+
TYPE,
71+
PAYLOAD,
72+
METADATA
73+
>;
74+
}
75+
76+
if (payloadSchema !== undefined && metadataSchema !== undefined) {
77+
const parser = (eventDetail: EventDetail) => {
78+
const parsedPayload = payloadSchema.safeParse(eventDetail.payload);
79+
const parsedMetadata = metadataSchema.safeParse(eventDetail.metadata);
80+
81+
if (parsedPayload.success && parsedMetadata.success) {
82+
return {
83+
isValid: true,
84+
parsedCandidate: {
85+
...eventDetail,
86+
payload: parsedPayload.data,
87+
metadata: parsedMetadata.data,
88+
},
89+
};
90+
}
91+
92+
const payloadErrors = parsedPayload.success
93+
? []
94+
: parsedPayload.error.errors.map(({ message }) => new Error(message));
95+
const metadataErrors = parsedMetadata.success
96+
? []
97+
: parsedMetadata.error.errors.map(({ message }) => new Error(message));
98+
99+
return {
100+
isValid: false,
101+
parsingErrors: [...payloadErrors, ...metadataErrors],
102+
};
103+
};
104+
105+
return parser as EventDetailParser<TYPE, PAYLOAD, METADATA>;
106+
}
107+
108+
return undefined;
109+
};
4110

5111
export class ZodEventType<
6112
TYPE extends string = string,
7-
PAYLOAD_SCHEMA extends ZodType | undefined = ZodType | undefined,
8-
PAYLOAD = ZodType extends PAYLOAD_SCHEMA
9-
? string extends TYPE
10-
? unknown
11-
: never
12-
: PAYLOAD_SCHEMA extends ZodType
13-
? z.infer<PAYLOAD_SCHEMA>
113+
PAYLOAD_SCHEMA extends ZodType | undefined = string extends TYPE
114+
? ZodTypeAny | undefined
14115
: never,
15-
METADATA_SCHEMA extends ZodType | undefined = ZodType | undefined,
16-
METADATA = ZodType extends METADATA_SCHEMA
17-
? string extends TYPE
18-
? unknown
19-
: never
20-
: METADATA_SCHEMA extends ZodType
21-
? z.infer<METADATA_SCHEMA>
116+
METADATA_SCHEMA extends ZodType | undefined = string extends TYPE
117+
? ZodTypeAny | undefined
22118
: never,
23-
> extends EventType<TYPE, PAYLOAD, METADATA> {
119+
PAYLOAD = string extends TYPE
120+
? never
121+
: PAYLOAD_SCHEMA extends ZodType
122+
? Zod.infer<PAYLOAD_SCHEMA>
123+
: unknown,
124+
METADATA = string extends TYPE
125+
? never
126+
: METADATA_SCHEMA extends ZodType
127+
? Zod.infer<METADATA_SCHEMA>
128+
: unknown,
129+
> implements EventType<TYPE, PAYLOAD, METADATA>
130+
{
131+
_types?: {
132+
detail: EventDetail<TYPE, PAYLOAD, METADATA>;
133+
};
134+
type: TYPE;
24135
payloadSchema?: PAYLOAD_SCHEMA;
25136
metadataSchema?: METADATA_SCHEMA;
137+
parseEventDetail?: EventDetailParser<TYPE, PAYLOAD, METADATA> | undefined;
26138

27139
constructor({
28140
type,
@@ -33,8 +145,12 @@ export class ZodEventType<
33145
payloadSchema?: PAYLOAD_SCHEMA;
34146
metadataSchema?: METADATA_SCHEMA;
35147
}) {
36-
super({ type });
148+
this.type = type;
37149
this.payloadSchema = payloadSchema;
38150
this.metadataSchema = metadataSchema;
151+
this.parseEventDetail = zodEventDetailParser<TYPE, PAYLOAD, METADATA>(
152+
payloadSchema,
153+
metadataSchema,
154+
);
39155
}
40156
}

packages/event-type-zod/src/eventType.unit.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ describe('zodEvent implementation', () => {
4949
expect(simpleEventType.type).toStrictEqual(type);
5050
expect(simpleEventType.payloadSchema).toStrictEqual(undefined);
5151
expect(simpleEventType.metadataSchema).toStrictEqual(undefined);
52+
expect(simpleEventType.parseEventDetail).toStrictEqual(undefined);
5253
});
5354

5455
it('has correct properties (with payload, no metadata)', () => {
@@ -76,6 +77,7 @@ describe('zodEvent implementation', () => {
7677
expect(payloadEventType.type).toStrictEqual(type);
7778
expect(payloadEventType.payloadSchema).toStrictEqual(payloadSchema);
7879
expect(payloadEventType.metadataSchema).toStrictEqual(undefined);
80+
expect(payloadEventType.parseEventDetail).toBeDefined();
7981
});
8082

8183
it('has correct properties (no payload, with metadata)', () => {
@@ -103,6 +105,7 @@ describe('zodEvent implementation', () => {
103105
expect(metadataEventType.type).toStrictEqual(type);
104106
expect(metadataEventType.payloadSchema).toStrictEqual(undefined);
105107
expect(metadataEventType.metadataSchema).toStrictEqual(metadataSchema);
108+
expect(metadataEventType.parseEventDetail).toBeDefined();
106109
});
107110

108111
it('has correct properties (with payload, with metadata)', () => {
@@ -135,5 +138,58 @@ describe('zodEvent implementation', () => {
135138
expect(fullEventType.type).toStrictEqual(type);
136139
expect(fullEventType.payloadSchema).toStrictEqual(payloadSchema);
137140
expect(fullEventType.metadataSchema).toStrictEqual(metadataSchema);
141+
expect(fullEventType.parseEventDetail).toBeDefined();
142+
});
143+
144+
it('validates schemas', () => {
145+
const fullEventType = new ZodEventType({
146+
type,
147+
payloadSchema,
148+
metadataSchema,
149+
});
150+
151+
expect(fullEventType.parseEventDetail).toBeDefined();
152+
console.log(
153+
fullEventType.parseEventDetail?.({
154+
payload: { message: 'ok' },
155+
metadata: { userEmail: 'email' },
156+
aggregateId: 'id',
157+
version: 10,
158+
timestamp: 'anyString',
159+
type: 'anyType',
160+
}),
161+
);
162+
// Email not valid
163+
expect(
164+
fullEventType.parseEventDetail?.({
165+
payload: { message: 'ok' },
166+
metadata: { userEmail: 'email' },
167+
aggregateId: 'id',
168+
version: 10,
169+
timestamp: 'anyString',
170+
type: 'anyType',
171+
}),
172+
).toMatchObject({ isValid: false });
173+
// Payload key unknown
174+
expect(
175+
fullEventType.parseEventDetail?.({
176+
payload: { someKey: 'ok' },
177+
metadata: { userEmail: 'email@format.com' },
178+
aggregateId: 'id',
179+
version: 10,
180+
timestamp: 'anyString',
181+
type: 'anyType',
182+
}),
183+
).toMatchObject({ isValid: false });
184+
expect(
185+
fullEventType.parseEventDetail?.({
186+
payload: { message: 'ok' },
187+
metadata: { userEmail: 'email@format.com' },
188+
aggregateId: 'id',
189+
version: 10,
190+
timestamp: 'anyString',
191+
type: 'anyType',
192+
}),
193+
).toMatchObject({ isValid: true });
138194
});
139195
});

0 commit comments

Comments
 (0)