@saga-bus/middleware-validation 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Dean Foran
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,355 @@
1
+ # @saga-bus/middleware-validation
2
+
3
+ Validation middleware for saga-bus that validates message payloads before processing.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @saga-bus/middleware-validation
9
+ # or
10
+ pnpm add @saga-bus/middleware-validation
11
+ ```
12
+
13
+ For Zod support:
14
+
15
+ ```bash
16
+ npm install zod
17
+ ```
18
+
19
+ ## Features
20
+
21
+ - **Schema Validation**: Validate messages against Zod schemas or custom validators
22
+ - **Multiple Actions**: Skip, log, throw, or DLQ invalid messages
23
+ - **Strict Mode**: Optionally reject messages without registered validators
24
+ - **Type Exclusions**: Skip validation for specific message types
25
+ - **Custom Validators**: Use function-based validators for complex logic
26
+
27
+ ## Quick Start
28
+
29
+ ```typescript
30
+ import { createBus } from "@saga-bus/core";
31
+ import { z } from "zod";
32
+ import {
33
+ createValidationMiddleware,
34
+ createZodValidator,
35
+ } from "@saga-bus/middleware-validation";
36
+
37
+ const OrderCreatedSchema = z.object({
38
+ type: z.literal("OrderCreated"),
39
+ orderId: z.string().uuid(),
40
+ customerId: z.string(),
41
+ items: z.array(z.object({
42
+ sku: z.string(),
43
+ quantity: z.number().positive(),
44
+ })).min(1),
45
+ });
46
+
47
+ const validationMiddleware = createValidationMiddleware({
48
+ validators: {
49
+ OrderCreated: createZodValidator(OrderCreatedSchema),
50
+ },
51
+ onInvalid: "throw",
52
+ });
53
+
54
+ const bus = createBus({
55
+ transport,
56
+ store,
57
+ sagas: [OrderSaga],
58
+ middleware: [validationMiddleware],
59
+ });
60
+ ```
61
+
62
+ ## API Reference
63
+
64
+ ### createValidationMiddleware(options)
65
+
66
+ Creates middleware that validates message payloads.
67
+
68
+ ```typescript
69
+ interface ValidationMiddlewareOptions {
70
+ /** Map of message type to validator */
71
+ validators: Record<string, MessageValidator>;
72
+
73
+ /** Action on invalid: "skip" | "log" | "throw" | "dlq" (default: "throw") */
74
+ onInvalid?: "skip" | "log" | "throw" | "dlq";
75
+
76
+ /** Handler for DLQ (required if onInvalid is "dlq") */
77
+ deadLetterHandler?: (envelope: MessageEnvelope, errors: ValidationError[]) => Promise<void>;
78
+
79
+ /** Logger for validation errors */
80
+ logger?: { warn(...): void; error(...): void };
81
+
82
+ /** Reject messages without validators (default: false) */
83
+ strictMode?: boolean;
84
+
85
+ /** Message types to skip validation */
86
+ excludeTypes?: string[];
87
+ }
88
+ ```
89
+
90
+ ### createZodValidator(schema)
91
+
92
+ Creates a validator from a Zod schema.
93
+
94
+ ```typescript
95
+ import { z } from "zod";
96
+ import { createZodValidator } from "@saga-bus/middleware-validation";
97
+
98
+ const validator = createZodValidator(z.object({
99
+ type: z.literal("OrderCreated"),
100
+ orderId: z.string(),
101
+ }));
102
+ ```
103
+
104
+ ### createZodValidators(schemas)
105
+
106
+ Creates multiple validators from a map of Zod schemas.
107
+
108
+ ```typescript
109
+ import { createZodValidators } from "@saga-bus/middleware-validation";
110
+
111
+ const validators = createZodValidators({
112
+ OrderCreated: OrderCreatedSchema,
113
+ OrderShipped: OrderShippedSchema,
114
+ PaymentReceived: PaymentReceivedSchema,
115
+ });
116
+ ```
117
+
118
+ ### createFunctionValidator(fn)
119
+
120
+ Creates a validator from a simple validation function.
121
+
122
+ ```typescript
123
+ import { createFunctionValidator } from "@saga-bus/middleware-validation";
124
+
125
+ const validator = createFunctionValidator((message) => {
126
+ if (!message.orderId) return "orderId is required";
127
+ if (message.items.length === 0) return "Order must have items";
128
+ return true;
129
+ });
130
+ ```
131
+
132
+ ### combineValidators(validators)
133
+
134
+ Combines multiple validators into one.
135
+
136
+ ```typescript
137
+ import { combineValidators, createFunctionValidator } from "@saga-bus/middleware-validation";
138
+
139
+ const validator = combineValidators([
140
+ createFunctionValidator((msg) => !!msg.orderId || "orderId required"),
141
+ createFunctionValidator((msg) => msg.items.length > 0 || "items required"),
142
+ ]);
143
+ ```
144
+
145
+ ### MessageValidationError
146
+
147
+ Error thrown when validation fails and `onInvalid: "throw"`.
148
+
149
+ ```typescript
150
+ import { MessageValidationError } from "@saga-bus/middleware-validation";
151
+
152
+ try {
153
+ await bus.publish(message);
154
+ } catch (error) {
155
+ if (error instanceof MessageValidationError) {
156
+ console.log(`Invalid message: ${error.messageId}`);
157
+ console.log(`Errors:`, error.validationErrors);
158
+ }
159
+ }
160
+ ```
161
+
162
+ ## Examples
163
+
164
+ ### Basic Zod Validation
165
+
166
+ ```typescript
167
+ import { z } from "zod";
168
+ import { createValidationMiddleware, createZodValidator } from "@saga-bus/middleware-validation";
169
+
170
+ const middleware = createValidationMiddleware({
171
+ validators: {
172
+ OrderCreated: createZodValidator(z.object({
173
+ type: z.literal("OrderCreated"),
174
+ orderId: z.string().uuid(),
175
+ customerId: z.string(),
176
+ items: z.array(z.object({
177
+ sku: z.string(),
178
+ quantity: z.number().int().positive(),
179
+ price: z.number().nonnegative(),
180
+ })).min(1),
181
+ total: z.number().nonnegative(),
182
+ })),
183
+ },
184
+ });
185
+ ```
186
+
187
+ ### Custom Function Validator
188
+
189
+ ```typescript
190
+ import { createValidationMiddleware, createFunctionValidator } from "@saga-bus/middleware-validation";
191
+
192
+ const middleware = createValidationMiddleware({
193
+ validators: {
194
+ PaymentReceived: createFunctionValidator(async (message) => {
195
+ // Async validation - check against external service
196
+ const isValidPaymentId = await paymentService.verify(message.paymentId);
197
+ if (!isValidPaymentId) {
198
+ return "Invalid payment ID";
199
+ }
200
+ return true;
201
+ }),
202
+ },
203
+ });
204
+ ```
205
+
206
+ ### Dead Letter Queue
207
+
208
+ ```typescript
209
+ import { createValidationMiddleware } from "@saga-bus/middleware-validation";
210
+
211
+ const middleware = createValidationMiddleware({
212
+ validators,
213
+ onInvalid: "dlq",
214
+ deadLetterHandler: async (envelope, errors) => {
215
+ await deadLetterQueue.send({
216
+ originalMessage: envelope,
217
+ validationErrors: errors,
218
+ timestamp: new Date(),
219
+ });
220
+ },
221
+ logger: console,
222
+ });
223
+ ```
224
+
225
+ ### Strict Mode
226
+
227
+ ```typescript
228
+ // Reject any message without a registered validator
229
+ const middleware = createValidationMiddleware({
230
+ validators: {
231
+ OrderCreated: orderCreatedValidator,
232
+ OrderShipped: orderShippedValidator,
233
+ },
234
+ strictMode: true,
235
+ onInvalid: "throw",
236
+ });
237
+ ```
238
+
239
+ ### Excluding Message Types
240
+
241
+ ```typescript
242
+ // Don't validate system messages
243
+ const middleware = createValidationMiddleware({
244
+ validators,
245
+ excludeTypes: ["Heartbeat", "HealthCheck", "SagaTimeoutExpired"],
246
+ });
247
+ ```
248
+
249
+ ### Logging Invalid Messages
250
+
251
+ ```typescript
252
+ import { logger } from "./logger";
253
+
254
+ const middleware = createValidationMiddleware({
255
+ validators,
256
+ onInvalid: "log",
257
+ logger: {
258
+ warn: (msg, meta) => logger.warn(msg, meta),
259
+ error: (msg, meta) => logger.error(msg, meta),
260
+ },
261
+ });
262
+ ```
263
+
264
+ ### Combining Multiple Validation Strategies
265
+
266
+ ```typescript
267
+ import { combineValidators, createZodValidator, createFunctionValidator } from "@saga-bus/middleware-validation";
268
+ import { z } from "zod";
269
+
270
+ // Schema validation + business rule validation
271
+ const orderValidator = combineValidators([
272
+ // Schema validation
273
+ createZodValidator(z.object({
274
+ type: z.literal("OrderCreated"),
275
+ orderId: z.string(),
276
+ customerId: z.string(),
277
+ items: z.array(z.object({
278
+ sku: z.string(),
279
+ quantity: z.number().positive(),
280
+ })),
281
+ total: z.number(),
282
+ })),
283
+
284
+ // Business rules
285
+ createFunctionValidator(async (msg) => {
286
+ const errors: string[] = [];
287
+
288
+ // Check customer exists
289
+ const customer = await customerService.get(msg.customerId);
290
+ if (!customer) {
291
+ errors.push(`Customer ${msg.customerId} not found`);
292
+ }
293
+
294
+ // Verify total matches items
295
+ const calculatedTotal = msg.items.reduce(
296
+ (sum, item) => sum + item.quantity * (item.price || 0),
297
+ 0
298
+ );
299
+ if (Math.abs(calculatedTotal - msg.total) > 0.01) {
300
+ errors.push("Total doesn't match item prices");
301
+ }
302
+
303
+ return errors.length === 0 ? true : errors;
304
+ }),
305
+ ]);
306
+ ```
307
+
308
+ ## Custom Validator Implementation
309
+
310
+ Create custom validators by implementing the `MessageValidator` interface:
311
+
312
+ ```typescript
313
+ import type { MessageValidator, ValidationResult } from "@saga-bus/middleware-validation";
314
+
315
+ const customValidator: MessageValidator = (message) => {
316
+ const errors = [];
317
+
318
+ if (!message.orderId) {
319
+ errors.push({ path: "orderId", message: "Required" });
320
+ }
321
+
322
+ if (message.items?.length === 0) {
323
+ errors.push({ path: "items", message: "Must have at least one item" });
324
+ }
325
+
326
+ return errors.length === 0
327
+ ? { valid: true }
328
+ : { valid: false, errors };
329
+ };
330
+ ```
331
+
332
+ ## ValidationResult Interface
333
+
334
+ ```typescript
335
+ interface ValidationResult {
336
+ valid: boolean;
337
+ errors?: Array<{
338
+ path: string; // e.g., "items[0].quantity"
339
+ message: string; // e.g., "Must be positive"
340
+ value?: unknown; // The invalid value
341
+ }>;
342
+ }
343
+ ```
344
+
345
+ ## Best Practices
346
+
347
+ 1. **Use Zod for complex schemas** - It provides excellent type inference and error messages
348
+ 2. **Use function validators for async/business logic** - External service calls, database lookups
349
+ 3. **Consider strict mode in production** - Catch unvalidated message types early
350
+ 4. **Use DLQ for debugging** - Store invalid messages for analysis
351
+ 5. **Exclude system messages** - Don't validate internal messages like timeouts
352
+
353
+ ## License
354
+
355
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ MessageValidationError: () => MessageValidationError,
24
+ combineValidators: () => combineValidators,
25
+ createFunctionValidator: () => createFunctionValidator,
26
+ createValidationMiddleware: () => createValidationMiddleware,
27
+ createZodValidator: () => createZodValidator,
28
+ createZodValidators: () => createZodValidators
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/types.ts
33
+ var MessageValidationError = class extends Error {
34
+ messageId;
35
+ messageType;
36
+ validationErrors;
37
+ constructor(messageId, messageType, errors) {
38
+ const errorSummary = errors.map((e) => `${e.path}: ${e.message}`).join(", ");
39
+ super(`Validation failed for message ${messageId}: ${errorSummary}`);
40
+ this.name = "MessageValidationError";
41
+ this.messageId = messageId;
42
+ this.messageType = messageType;
43
+ this.validationErrors = errors;
44
+ }
45
+ };
46
+
47
+ // src/ValidationMiddleware.ts
48
+ function createValidationMiddleware(options) {
49
+ const {
50
+ validators,
51
+ onInvalid = "throw",
52
+ deadLetterHandler,
53
+ logger,
54
+ strictMode = false,
55
+ excludeTypes = []
56
+ } = options;
57
+ if (onInvalid === "dlq" && !deadLetterHandler) {
58
+ throw new Error(
59
+ "deadLetterHandler is required when onInvalid is set to 'dlq'"
60
+ );
61
+ }
62
+ const excludeSet = new Set(excludeTypes);
63
+ return async (ctx, next) => {
64
+ const { envelope } = ctx;
65
+ const messageType = envelope.type;
66
+ if (excludeSet.has(messageType)) {
67
+ await next();
68
+ return;
69
+ }
70
+ const validator = validators[messageType];
71
+ if (!validator) {
72
+ if (strictMode) {
73
+ const errors = [
74
+ {
75
+ path: "type",
76
+ message: `No validator registered for message type: ${messageType}`
77
+ }
78
+ ];
79
+ await handleInvalidMessage(
80
+ ctx,
81
+ errors,
82
+ onInvalid,
83
+ deadLetterHandler,
84
+ logger
85
+ );
86
+ return;
87
+ }
88
+ await next();
89
+ return;
90
+ }
91
+ const result = await validator(envelope.payload);
92
+ if (result.valid) {
93
+ await next();
94
+ return;
95
+ }
96
+ await handleInvalidMessage(
97
+ ctx,
98
+ result.errors || [],
99
+ onInvalid,
100
+ deadLetterHandler,
101
+ logger
102
+ );
103
+ };
104
+ }
105
+ async function handleInvalidMessage(ctx, errors, onInvalid, deadLetterHandler, logger) {
106
+ const { envelope } = ctx;
107
+ switch (onInvalid) {
108
+ case "throw":
109
+ throw new MessageValidationError(envelope.id, envelope.type, errors);
110
+ case "dlq":
111
+ logger?.error("Invalid message, sending to DLQ", {
112
+ messageId: envelope.id,
113
+ messageType: envelope.type,
114
+ correlationId: ctx.correlationId,
115
+ sagaName: ctx.sagaName,
116
+ errors
117
+ });
118
+ await deadLetterHandler(envelope, errors);
119
+ break;
120
+ case "log":
121
+ logger?.warn("Invalid message detected, skipping", {
122
+ messageId: envelope.id,
123
+ messageType: envelope.type,
124
+ correlationId: ctx.correlationId,
125
+ sagaName: ctx.sagaName,
126
+ errors
127
+ });
128
+ // Fall through to skip
129
+ case "skip":
130
+ default:
131
+ break;
132
+ }
133
+ }
134
+
135
+ // src/validators/ZodValidator.ts
136
+ function createZodValidator(schema) {
137
+ return (message) => {
138
+ const result = schema.safeParse(message);
139
+ if (result.success) {
140
+ return { valid: true };
141
+ }
142
+ const errors = result.error.issues.map((issue) => ({
143
+ path: issue.path.join("."),
144
+ message: issue.message
145
+ }));
146
+ return { valid: false, errors };
147
+ };
148
+ }
149
+ function createZodValidators(schemas) {
150
+ const validators = {};
151
+ for (const [type, schema] of Object.entries(schemas)) {
152
+ validators[type] = createZodValidator(schema);
153
+ }
154
+ return validators;
155
+ }
156
+
157
+ // src/validators/FunctionValidator.ts
158
+ function createFunctionValidator(fn) {
159
+ return async (message) => {
160
+ const result = await fn(message);
161
+ if (result === true) {
162
+ return { valid: true };
163
+ }
164
+ const errors = [];
165
+ if (result === false) {
166
+ errors.push({ path: "", message: "Validation failed" });
167
+ } else if (typeof result === "string") {
168
+ errors.push({ path: "", message: result });
169
+ } else if (Array.isArray(result)) {
170
+ for (const msg of result) {
171
+ errors.push({ path: "", message: msg });
172
+ }
173
+ }
174
+ return { valid: false, errors };
175
+ };
176
+ }
177
+ function combineValidators(validators) {
178
+ return async (message) => {
179
+ const allErrors = [];
180
+ for (const validator of validators) {
181
+ const result = await validator(message);
182
+ if (!result.valid && result.errors) {
183
+ allErrors.push(...result.errors);
184
+ }
185
+ }
186
+ if (allErrors.length === 0) {
187
+ return { valid: true };
188
+ }
189
+ return { valid: false, errors: allErrors };
190
+ };
191
+ }
192
+ // Annotate the CommonJS export names for ESM import in node:
193
+ 0 && (module.exports = {
194
+ MessageValidationError,
195
+ combineValidators,
196
+ createFunctionValidator,
197
+ createValidationMiddleware,
198
+ createZodValidator,
199
+ createZodValidators
200
+ });
201
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/types.ts","../src/ValidationMiddleware.ts","../src/validators/ZodValidator.ts","../src/validators/FunctionValidator.ts"],"sourcesContent":["export { createValidationMiddleware } from \"./ValidationMiddleware.js\";\nexport { createZodValidator, createZodValidators } from \"./validators/ZodValidator.js\";\nexport {\n createFunctionValidator,\n combineValidators,\n} from \"./validators/FunctionValidator.js\";\nexport {\n MessageValidationError,\n type ValidationMiddlewareOptions,\n type ValidationResult,\n type ValidationError,\n type MessageValidator,\n type SyncMessageValidator,\n type AsyncMessageValidator,\n type AnyMessageValidator,\n type InvalidMessageAction,\n type DeadLetterHandler,\n} from \"./types.js\";\n","import type { MessageEnvelope, BaseMessage } from \"@saga-bus/core\";\n\n/**\n * Result of a validation.\n */\nexport interface ValidationResult {\n /** Whether the validation passed */\n valid: boolean;\n /** Validation errors if invalid */\n errors?: ValidationError[];\n}\n\n/**\n * A single validation error.\n */\nexport interface ValidationError {\n /** Path to the invalid field (e.g., \"payload.items[0].quantity\") */\n path: string;\n /** Error message */\n message: string;\n /** The invalid value */\n value?: unknown;\n}\n\n/**\n * A synchronous validator function that validates a message.\n */\nexport type SyncMessageValidator<T extends BaseMessage = BaseMessage> = (\n message: T\n) => ValidationResult;\n\n/**\n * An asynchronous validator function that validates a message.\n */\nexport type AsyncMessageValidator<T extends BaseMessage = BaseMessage> = (\n message: T\n) => Promise<ValidationResult>;\n\n/**\n * A validator function that validates a message (sync or async).\n */\nexport type MessageValidator<T extends BaseMessage = BaseMessage> = (\n message: T\n) => ValidationResult | Promise<ValidationResult>;\n\n/**\n * Action to take when validation fails.\n */\nexport type InvalidMessageAction = \"skip\" | \"log\" | \"throw\" | \"dlq\";\n\n/**\n * Dead-letter queue handler for invalid messages.\n */\nexport type DeadLetterHandler = (\n envelope: MessageEnvelope,\n errors: ValidationError[]\n) => Promise<void>;\n\n/**\n * A validator that can be used in the middleware (accepts any message type).\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyMessageValidator = (message: any) => ValidationResult | Promise<ValidationResult>;\n\n/**\n * Options for the validation middleware.\n */\nexport interface ValidationMiddlewareOptions {\n /**\n * Map of message type to validator.\n * Messages without a registered validator will pass through.\n */\n validators: Record<string, AnyMessageValidator>;\n\n /**\n * Action to take when validation fails.\n * - \"skip\": Silently skip processing\n * - \"log\": Log warning and skip\n * - \"throw\": Throw a ValidationError\n * - \"dlq\": Send to dead-letter queue handler\n * @default \"throw\"\n */\n onInvalid?: InvalidMessageAction;\n\n /**\n * Handler for dead-letter queue (required if onInvalid is \"dlq\").\n */\n deadLetterHandler?: DeadLetterHandler;\n\n /**\n * Custom logger for validation errors.\n */\n logger?: {\n warn(message: string, meta?: Record<string, unknown>): void;\n error(message: string, meta?: Record<string, unknown>): void;\n };\n\n /**\n * Whether to validate messages that don't have a registered validator.\n * If true, messages without validators will be rejected.\n * @default false\n */\n strictMode?: boolean;\n\n /**\n * Message types that should not be validated.\n */\n excludeTypes?: string[];\n}\n\n/**\n * Error thrown when validation fails and onInvalid is \"throw\".\n */\nexport class MessageValidationError extends Error {\n public readonly messageId: string;\n public readonly messageType: string;\n public readonly validationErrors: ValidationError[];\n\n constructor(\n messageId: string,\n messageType: string,\n errors: ValidationError[]\n ) {\n const errorSummary = errors\n .map((e) => `${e.path}: ${e.message}`)\n .join(\", \");\n super(`Validation failed for message ${messageId}: ${errorSummary}`);\n this.name = \"MessageValidationError\";\n this.messageId = messageId;\n this.messageType = messageType;\n this.validationErrors = errors;\n }\n}\n","import type { SagaMiddleware, SagaPipelineContext } from \"@saga-bus/core\";\nimport type { ValidationMiddlewareOptions, ValidationError } from \"./types.js\";\nimport { MessageValidationError } from \"./types.js\";\n\n/**\n * Creates validation middleware that validates message payloads before processing.\n *\n * @example\n * ```typescript\n * import { createValidationMiddleware, createZodValidator } from \"@saga-bus/middleware-validation\";\n * import { z } from \"zod\";\n *\n * const validationMiddleware = createValidationMiddleware({\n * validators: {\n * OrderCreated: createZodValidator(z.object({\n * type: z.literal(\"OrderCreated\"),\n * orderId: z.string(),\n * customerId: z.string(),\n * })),\n * },\n * onInvalid: \"throw\",\n * });\n *\n * const bus = createBus({\n * transport,\n * store,\n * sagas: [MySaga],\n * middleware: [validationMiddleware],\n * });\n * ```\n */\nexport function createValidationMiddleware(\n options: ValidationMiddlewareOptions\n): SagaMiddleware {\n const {\n validators,\n onInvalid = \"throw\",\n deadLetterHandler,\n logger,\n strictMode = false,\n excludeTypes = [],\n } = options;\n\n // Validate options\n if (onInvalid === \"dlq\" && !deadLetterHandler) {\n throw new Error(\n \"deadLetterHandler is required when onInvalid is set to 'dlq'\"\n );\n }\n\n const excludeSet = new Set(excludeTypes);\n\n return async (ctx: SagaPipelineContext, next: () => Promise<void>) => {\n const { envelope } = ctx;\n const messageType = envelope.type;\n\n // Skip excluded types\n if (excludeSet.has(messageType)) {\n await next();\n return;\n }\n\n // Get validator for this message type\n const validator = validators[messageType];\n\n // Handle missing validator\n if (!validator) {\n if (strictMode) {\n const errors: ValidationError[] = [\n {\n path: \"type\",\n message: `No validator registered for message type: ${messageType}`,\n },\n ];\n await handleInvalidMessage(\n ctx,\n errors,\n onInvalid,\n deadLetterHandler,\n logger\n );\n return;\n }\n // Not strict mode - pass through\n await next();\n return;\n }\n\n // Validate the message payload\n const result = await validator(envelope.payload);\n\n if (result.valid) {\n await next();\n return;\n }\n\n // Handle invalid message\n await handleInvalidMessage(\n ctx,\n result.errors || [],\n onInvalid,\n deadLetterHandler,\n logger\n );\n };\n}\n\nasync function handleInvalidMessage(\n ctx: SagaPipelineContext,\n errors: ValidationError[],\n onInvalid: string,\n deadLetterHandler: ((envelope: typeof ctx.envelope, errors: ValidationError[]) => Promise<void>) | undefined,\n logger: { warn(message: string, meta?: Record<string, unknown>): void; error(message: string, meta?: Record<string, unknown>): void } | undefined\n): Promise<void> {\n const { envelope } = ctx;\n\n switch (onInvalid) {\n case \"throw\":\n throw new MessageValidationError(envelope.id, envelope.type, errors);\n\n case \"dlq\":\n logger?.error(\"Invalid message, sending to DLQ\", {\n messageId: envelope.id,\n messageType: envelope.type,\n correlationId: ctx.correlationId,\n sagaName: ctx.sagaName,\n errors,\n });\n await deadLetterHandler!(envelope, errors);\n break;\n\n case \"log\":\n logger?.warn(\"Invalid message detected, skipping\", {\n messageId: envelope.id,\n messageType: envelope.type,\n correlationId: ctx.correlationId,\n sagaName: ctx.sagaName,\n errors,\n });\n // Fall through to skip\n\n case \"skip\":\n default:\n // Don't call next() - silently skip processing\n break;\n }\n}\n","import type { BaseMessage } from \"@saga-bus/core\";\nimport type { SyncMessageValidator, ValidationResult, ValidationError, AnyMessageValidator } from \"../types.js\";\n\n// Type for Zod schema - we don't import zod directly to keep it optional\ninterface ZodSchema {\n safeParse(data: unknown): {\n success: boolean;\n data?: unknown;\n error?: {\n issues: Array<{\n path: (string | number)[];\n message: string;\n }>;\n };\n };\n}\n\n/**\n * Creates a validator from a Zod schema.\n *\n * @example\n * ```typescript\n * import { z } from \"zod\";\n * import { createZodValidator } from \"@saga-bus/middleware-validation\";\n *\n * const OrderCreatedSchema = z.object({\n * type: z.literal(\"OrderCreated\"),\n * orderId: z.string().uuid(),\n * customerId: z.string(),\n * items: z.array(z.object({\n * sku: z.string(),\n * quantity: z.number().positive(),\n * })),\n * total: z.number().nonnegative(),\n * });\n *\n * const validator = createZodValidator(OrderCreatedSchema);\n * ```\n */\nexport function createZodValidator<T extends BaseMessage = BaseMessage>(schema: ZodSchema): SyncMessageValidator<T> {\n return (message: T): ValidationResult => {\n const result = schema.safeParse(message);\n\n if (result.success) {\n return { valid: true };\n }\n\n const errors: ValidationError[] = result.error!.issues.map((issue) => ({\n path: issue.path.join(\".\"),\n message: issue.message,\n }));\n\n return { valid: false, errors };\n };\n}\n\n/**\n * Creates multiple validators from a map of Zod schemas.\n *\n * @example\n * ```typescript\n * import { z } from \"zod\";\n * import { createZodValidators } from \"@saga-bus/middleware-validation\";\n *\n * const validators = createZodValidators({\n * OrderCreated: z.object({\n * type: z.literal(\"OrderCreated\"),\n * orderId: z.string(),\n * }),\n * OrderShipped: z.object({\n * type: z.literal(\"OrderShipped\"),\n * orderId: z.string(),\n * trackingNumber: z.string(),\n * }),\n * });\n * ```\n */\nexport function createZodValidators(\n schemas: Record<string, ZodSchema>\n): Record<string, AnyMessageValidator> {\n const validators: Record<string, AnyMessageValidator> = {};\n\n for (const [type, schema] of Object.entries(schemas)) {\n validators[type] = createZodValidator(schema);\n }\n\n return validators;\n}\n","import type { BaseMessage } from \"@saga-bus/core\";\nimport type {\n MessageValidator,\n ValidationResult,\n ValidationError,\n} from \"../types.js\";\n\n/**\n * A simple validation function that returns true/false or an error message.\n */\nexport type SimpleValidationFn<T> = (\n message: T\n) => boolean | string | string[] | Promise<boolean | string | string[]>;\n\n/**\n * Creates a validator from a simple validation function.\n *\n * @example\n * ```typescript\n * import { createFunctionValidator } from \"@saga-bus/middleware-validation\";\n *\n * const validator = createFunctionValidator((message: OrderCreated) => {\n * if (!message.orderId) {\n * return \"orderId is required\";\n * }\n * if (message.items.length === 0) {\n * return \"Order must have at least one item\";\n * }\n * return true;\n * });\n * ```\n */\nexport function createFunctionValidator<T extends BaseMessage = BaseMessage>(\n fn: SimpleValidationFn<T>\n): MessageValidator<T> {\n return async (message: T): Promise<ValidationResult> => {\n const result = await fn(message);\n\n if (result === true) {\n return { valid: true };\n }\n\n const errors: ValidationError[] = [];\n\n if (result === false) {\n errors.push({ path: \"\", message: \"Validation failed\" });\n } else if (typeof result === \"string\") {\n errors.push({ path: \"\", message: result });\n } else if (Array.isArray(result)) {\n for (const msg of result) {\n errors.push({ path: \"\", message: msg });\n }\n }\n\n return { valid: false, errors };\n };\n}\n\n/**\n * Combines multiple validators into one.\n * All validators must pass for the message to be valid.\n *\n * @example\n * ```typescript\n * import { combineValidators, createFunctionValidator } from \"@saga-bus/middleware-validation\";\n *\n * const validator = combineValidators([\n * createFunctionValidator((msg) => !!msg.orderId || \"orderId required\"),\n * createFunctionValidator((msg) => msg.items.length > 0 || \"items required\"),\n * ]);\n * ```\n */\nexport function combineValidators<T extends BaseMessage = BaseMessage>(\n validators: MessageValidator<T>[]\n): MessageValidator<T> {\n return async (message: T): Promise<ValidationResult> => {\n const allErrors: ValidationError[] = [];\n\n for (const validator of validators) {\n const result = await validator(message);\n if (!result.valid && result.errors) {\n allErrors.push(...result.errors);\n }\n }\n\n if (allErrors.length === 0) {\n return { valid: true };\n }\n\n return { valid: false, errors: allErrors };\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,223 @@
1
+ import { MessageEnvelope, BaseMessage, SagaMiddleware } from '@saga-bus/core';
2
+
3
+ /**
4
+ * Result of a validation.
5
+ */
6
+ interface ValidationResult {
7
+ /** Whether the validation passed */
8
+ valid: boolean;
9
+ /** Validation errors if invalid */
10
+ errors?: ValidationError[];
11
+ }
12
+ /**
13
+ * A single validation error.
14
+ */
15
+ interface ValidationError {
16
+ /** Path to the invalid field (e.g., "payload.items[0].quantity") */
17
+ path: string;
18
+ /** Error message */
19
+ message: string;
20
+ /** The invalid value */
21
+ value?: unknown;
22
+ }
23
+ /**
24
+ * A synchronous validator function that validates a message.
25
+ */
26
+ type SyncMessageValidator<T extends BaseMessage = BaseMessage> = (message: T) => ValidationResult;
27
+ /**
28
+ * An asynchronous validator function that validates a message.
29
+ */
30
+ type AsyncMessageValidator<T extends BaseMessage = BaseMessage> = (message: T) => Promise<ValidationResult>;
31
+ /**
32
+ * A validator function that validates a message (sync or async).
33
+ */
34
+ type MessageValidator<T extends BaseMessage = BaseMessage> = (message: T) => ValidationResult | Promise<ValidationResult>;
35
+ /**
36
+ * Action to take when validation fails.
37
+ */
38
+ type InvalidMessageAction = "skip" | "log" | "throw" | "dlq";
39
+ /**
40
+ * Dead-letter queue handler for invalid messages.
41
+ */
42
+ type DeadLetterHandler = (envelope: MessageEnvelope, errors: ValidationError[]) => Promise<void>;
43
+ /**
44
+ * A validator that can be used in the middleware (accepts any message type).
45
+ */
46
+ type AnyMessageValidator = (message: any) => ValidationResult | Promise<ValidationResult>;
47
+ /**
48
+ * Options for the validation middleware.
49
+ */
50
+ interface ValidationMiddlewareOptions {
51
+ /**
52
+ * Map of message type to validator.
53
+ * Messages without a registered validator will pass through.
54
+ */
55
+ validators: Record<string, AnyMessageValidator>;
56
+ /**
57
+ * Action to take when validation fails.
58
+ * - "skip": Silently skip processing
59
+ * - "log": Log warning and skip
60
+ * - "throw": Throw a ValidationError
61
+ * - "dlq": Send to dead-letter queue handler
62
+ * @default "throw"
63
+ */
64
+ onInvalid?: InvalidMessageAction;
65
+ /**
66
+ * Handler for dead-letter queue (required if onInvalid is "dlq").
67
+ */
68
+ deadLetterHandler?: DeadLetterHandler;
69
+ /**
70
+ * Custom logger for validation errors.
71
+ */
72
+ logger?: {
73
+ warn(message: string, meta?: Record<string, unknown>): void;
74
+ error(message: string, meta?: Record<string, unknown>): void;
75
+ };
76
+ /**
77
+ * Whether to validate messages that don't have a registered validator.
78
+ * If true, messages without validators will be rejected.
79
+ * @default false
80
+ */
81
+ strictMode?: boolean;
82
+ /**
83
+ * Message types that should not be validated.
84
+ */
85
+ excludeTypes?: string[];
86
+ }
87
+ /**
88
+ * Error thrown when validation fails and onInvalid is "throw".
89
+ */
90
+ declare class MessageValidationError extends Error {
91
+ readonly messageId: string;
92
+ readonly messageType: string;
93
+ readonly validationErrors: ValidationError[];
94
+ constructor(messageId: string, messageType: string, errors: ValidationError[]);
95
+ }
96
+
97
+ /**
98
+ * Creates validation middleware that validates message payloads before processing.
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * import { createValidationMiddleware, createZodValidator } from "@saga-bus/middleware-validation";
103
+ * import { z } from "zod";
104
+ *
105
+ * const validationMiddleware = createValidationMiddleware({
106
+ * validators: {
107
+ * OrderCreated: createZodValidator(z.object({
108
+ * type: z.literal("OrderCreated"),
109
+ * orderId: z.string(),
110
+ * customerId: z.string(),
111
+ * })),
112
+ * },
113
+ * onInvalid: "throw",
114
+ * });
115
+ *
116
+ * const bus = createBus({
117
+ * transport,
118
+ * store,
119
+ * sagas: [MySaga],
120
+ * middleware: [validationMiddleware],
121
+ * });
122
+ * ```
123
+ */
124
+ declare function createValidationMiddleware(options: ValidationMiddlewareOptions): SagaMiddleware;
125
+
126
+ interface ZodSchema {
127
+ safeParse(data: unknown): {
128
+ success: boolean;
129
+ data?: unknown;
130
+ error?: {
131
+ issues: Array<{
132
+ path: (string | number)[];
133
+ message: string;
134
+ }>;
135
+ };
136
+ };
137
+ }
138
+ /**
139
+ * Creates a validator from a Zod schema.
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * import { z } from "zod";
144
+ * import { createZodValidator } from "@saga-bus/middleware-validation";
145
+ *
146
+ * const OrderCreatedSchema = z.object({
147
+ * type: z.literal("OrderCreated"),
148
+ * orderId: z.string().uuid(),
149
+ * customerId: z.string(),
150
+ * items: z.array(z.object({
151
+ * sku: z.string(),
152
+ * quantity: z.number().positive(),
153
+ * })),
154
+ * total: z.number().nonnegative(),
155
+ * });
156
+ *
157
+ * const validator = createZodValidator(OrderCreatedSchema);
158
+ * ```
159
+ */
160
+ declare function createZodValidator<T extends BaseMessage = BaseMessage>(schema: ZodSchema): SyncMessageValidator<T>;
161
+ /**
162
+ * Creates multiple validators from a map of Zod schemas.
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * import { z } from "zod";
167
+ * import { createZodValidators } from "@saga-bus/middleware-validation";
168
+ *
169
+ * const validators = createZodValidators({
170
+ * OrderCreated: z.object({
171
+ * type: z.literal("OrderCreated"),
172
+ * orderId: z.string(),
173
+ * }),
174
+ * OrderShipped: z.object({
175
+ * type: z.literal("OrderShipped"),
176
+ * orderId: z.string(),
177
+ * trackingNumber: z.string(),
178
+ * }),
179
+ * });
180
+ * ```
181
+ */
182
+ declare function createZodValidators(schemas: Record<string, ZodSchema>): Record<string, AnyMessageValidator>;
183
+
184
+ /**
185
+ * A simple validation function that returns true/false or an error message.
186
+ */
187
+ type SimpleValidationFn<T> = (message: T) => boolean | string | string[] | Promise<boolean | string | string[]>;
188
+ /**
189
+ * Creates a validator from a simple validation function.
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * import { createFunctionValidator } from "@saga-bus/middleware-validation";
194
+ *
195
+ * const validator = createFunctionValidator((message: OrderCreated) => {
196
+ * if (!message.orderId) {
197
+ * return "orderId is required";
198
+ * }
199
+ * if (message.items.length === 0) {
200
+ * return "Order must have at least one item";
201
+ * }
202
+ * return true;
203
+ * });
204
+ * ```
205
+ */
206
+ declare function createFunctionValidator<T extends BaseMessage = BaseMessage>(fn: SimpleValidationFn<T>): MessageValidator<T>;
207
+ /**
208
+ * Combines multiple validators into one.
209
+ * All validators must pass for the message to be valid.
210
+ *
211
+ * @example
212
+ * ```typescript
213
+ * import { combineValidators, createFunctionValidator } from "@saga-bus/middleware-validation";
214
+ *
215
+ * const validator = combineValidators([
216
+ * createFunctionValidator((msg) => !!msg.orderId || "orderId required"),
217
+ * createFunctionValidator((msg) => msg.items.length > 0 || "items required"),
218
+ * ]);
219
+ * ```
220
+ */
221
+ declare function combineValidators<T extends BaseMessage = BaseMessage>(validators: MessageValidator<T>[]): MessageValidator<T>;
222
+
223
+ export { type AnyMessageValidator, type AsyncMessageValidator, type DeadLetterHandler, type InvalidMessageAction, MessageValidationError, type MessageValidator, type SyncMessageValidator, type ValidationError, type ValidationMiddlewareOptions, type ValidationResult, combineValidators, createFunctionValidator, createValidationMiddleware, createZodValidator, createZodValidators };
@@ -0,0 +1,223 @@
1
+ import { MessageEnvelope, BaseMessage, SagaMiddleware } from '@saga-bus/core';
2
+
3
+ /**
4
+ * Result of a validation.
5
+ */
6
+ interface ValidationResult {
7
+ /** Whether the validation passed */
8
+ valid: boolean;
9
+ /** Validation errors if invalid */
10
+ errors?: ValidationError[];
11
+ }
12
+ /**
13
+ * A single validation error.
14
+ */
15
+ interface ValidationError {
16
+ /** Path to the invalid field (e.g., "payload.items[0].quantity") */
17
+ path: string;
18
+ /** Error message */
19
+ message: string;
20
+ /** The invalid value */
21
+ value?: unknown;
22
+ }
23
+ /**
24
+ * A synchronous validator function that validates a message.
25
+ */
26
+ type SyncMessageValidator<T extends BaseMessage = BaseMessage> = (message: T) => ValidationResult;
27
+ /**
28
+ * An asynchronous validator function that validates a message.
29
+ */
30
+ type AsyncMessageValidator<T extends BaseMessage = BaseMessage> = (message: T) => Promise<ValidationResult>;
31
+ /**
32
+ * A validator function that validates a message (sync or async).
33
+ */
34
+ type MessageValidator<T extends BaseMessage = BaseMessage> = (message: T) => ValidationResult | Promise<ValidationResult>;
35
+ /**
36
+ * Action to take when validation fails.
37
+ */
38
+ type InvalidMessageAction = "skip" | "log" | "throw" | "dlq";
39
+ /**
40
+ * Dead-letter queue handler for invalid messages.
41
+ */
42
+ type DeadLetterHandler = (envelope: MessageEnvelope, errors: ValidationError[]) => Promise<void>;
43
+ /**
44
+ * A validator that can be used in the middleware (accepts any message type).
45
+ */
46
+ type AnyMessageValidator = (message: any) => ValidationResult | Promise<ValidationResult>;
47
+ /**
48
+ * Options for the validation middleware.
49
+ */
50
+ interface ValidationMiddlewareOptions {
51
+ /**
52
+ * Map of message type to validator.
53
+ * Messages without a registered validator will pass through.
54
+ */
55
+ validators: Record<string, AnyMessageValidator>;
56
+ /**
57
+ * Action to take when validation fails.
58
+ * - "skip": Silently skip processing
59
+ * - "log": Log warning and skip
60
+ * - "throw": Throw a ValidationError
61
+ * - "dlq": Send to dead-letter queue handler
62
+ * @default "throw"
63
+ */
64
+ onInvalid?: InvalidMessageAction;
65
+ /**
66
+ * Handler for dead-letter queue (required if onInvalid is "dlq").
67
+ */
68
+ deadLetterHandler?: DeadLetterHandler;
69
+ /**
70
+ * Custom logger for validation errors.
71
+ */
72
+ logger?: {
73
+ warn(message: string, meta?: Record<string, unknown>): void;
74
+ error(message: string, meta?: Record<string, unknown>): void;
75
+ };
76
+ /**
77
+ * Whether to validate messages that don't have a registered validator.
78
+ * If true, messages without validators will be rejected.
79
+ * @default false
80
+ */
81
+ strictMode?: boolean;
82
+ /**
83
+ * Message types that should not be validated.
84
+ */
85
+ excludeTypes?: string[];
86
+ }
87
+ /**
88
+ * Error thrown when validation fails and onInvalid is "throw".
89
+ */
90
+ declare class MessageValidationError extends Error {
91
+ readonly messageId: string;
92
+ readonly messageType: string;
93
+ readonly validationErrors: ValidationError[];
94
+ constructor(messageId: string, messageType: string, errors: ValidationError[]);
95
+ }
96
+
97
+ /**
98
+ * Creates validation middleware that validates message payloads before processing.
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * import { createValidationMiddleware, createZodValidator } from "@saga-bus/middleware-validation";
103
+ * import { z } from "zod";
104
+ *
105
+ * const validationMiddleware = createValidationMiddleware({
106
+ * validators: {
107
+ * OrderCreated: createZodValidator(z.object({
108
+ * type: z.literal("OrderCreated"),
109
+ * orderId: z.string(),
110
+ * customerId: z.string(),
111
+ * })),
112
+ * },
113
+ * onInvalid: "throw",
114
+ * });
115
+ *
116
+ * const bus = createBus({
117
+ * transport,
118
+ * store,
119
+ * sagas: [MySaga],
120
+ * middleware: [validationMiddleware],
121
+ * });
122
+ * ```
123
+ */
124
+ declare function createValidationMiddleware(options: ValidationMiddlewareOptions): SagaMiddleware;
125
+
126
+ interface ZodSchema {
127
+ safeParse(data: unknown): {
128
+ success: boolean;
129
+ data?: unknown;
130
+ error?: {
131
+ issues: Array<{
132
+ path: (string | number)[];
133
+ message: string;
134
+ }>;
135
+ };
136
+ };
137
+ }
138
+ /**
139
+ * Creates a validator from a Zod schema.
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * import { z } from "zod";
144
+ * import { createZodValidator } from "@saga-bus/middleware-validation";
145
+ *
146
+ * const OrderCreatedSchema = z.object({
147
+ * type: z.literal("OrderCreated"),
148
+ * orderId: z.string().uuid(),
149
+ * customerId: z.string(),
150
+ * items: z.array(z.object({
151
+ * sku: z.string(),
152
+ * quantity: z.number().positive(),
153
+ * })),
154
+ * total: z.number().nonnegative(),
155
+ * });
156
+ *
157
+ * const validator = createZodValidator(OrderCreatedSchema);
158
+ * ```
159
+ */
160
+ declare function createZodValidator<T extends BaseMessage = BaseMessage>(schema: ZodSchema): SyncMessageValidator<T>;
161
+ /**
162
+ * Creates multiple validators from a map of Zod schemas.
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * import { z } from "zod";
167
+ * import { createZodValidators } from "@saga-bus/middleware-validation";
168
+ *
169
+ * const validators = createZodValidators({
170
+ * OrderCreated: z.object({
171
+ * type: z.literal("OrderCreated"),
172
+ * orderId: z.string(),
173
+ * }),
174
+ * OrderShipped: z.object({
175
+ * type: z.literal("OrderShipped"),
176
+ * orderId: z.string(),
177
+ * trackingNumber: z.string(),
178
+ * }),
179
+ * });
180
+ * ```
181
+ */
182
+ declare function createZodValidators(schemas: Record<string, ZodSchema>): Record<string, AnyMessageValidator>;
183
+
184
+ /**
185
+ * A simple validation function that returns true/false or an error message.
186
+ */
187
+ type SimpleValidationFn<T> = (message: T) => boolean | string | string[] | Promise<boolean | string | string[]>;
188
+ /**
189
+ * Creates a validator from a simple validation function.
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * import { createFunctionValidator } from "@saga-bus/middleware-validation";
194
+ *
195
+ * const validator = createFunctionValidator((message: OrderCreated) => {
196
+ * if (!message.orderId) {
197
+ * return "orderId is required";
198
+ * }
199
+ * if (message.items.length === 0) {
200
+ * return "Order must have at least one item";
201
+ * }
202
+ * return true;
203
+ * });
204
+ * ```
205
+ */
206
+ declare function createFunctionValidator<T extends BaseMessage = BaseMessage>(fn: SimpleValidationFn<T>): MessageValidator<T>;
207
+ /**
208
+ * Combines multiple validators into one.
209
+ * All validators must pass for the message to be valid.
210
+ *
211
+ * @example
212
+ * ```typescript
213
+ * import { combineValidators, createFunctionValidator } from "@saga-bus/middleware-validation";
214
+ *
215
+ * const validator = combineValidators([
216
+ * createFunctionValidator((msg) => !!msg.orderId || "orderId required"),
217
+ * createFunctionValidator((msg) => msg.items.length > 0 || "items required"),
218
+ * ]);
219
+ * ```
220
+ */
221
+ declare function combineValidators<T extends BaseMessage = BaseMessage>(validators: MessageValidator<T>[]): MessageValidator<T>;
222
+
223
+ export { type AnyMessageValidator, type AsyncMessageValidator, type DeadLetterHandler, type InvalidMessageAction, MessageValidationError, type MessageValidator, type SyncMessageValidator, type ValidationError, type ValidationMiddlewareOptions, type ValidationResult, combineValidators, createFunctionValidator, createValidationMiddleware, createZodValidator, createZodValidators };
package/dist/index.js ADDED
@@ -0,0 +1,169 @@
1
+ // src/types.ts
2
+ var MessageValidationError = class extends Error {
3
+ messageId;
4
+ messageType;
5
+ validationErrors;
6
+ constructor(messageId, messageType, errors) {
7
+ const errorSummary = errors.map((e) => `${e.path}: ${e.message}`).join(", ");
8
+ super(`Validation failed for message ${messageId}: ${errorSummary}`);
9
+ this.name = "MessageValidationError";
10
+ this.messageId = messageId;
11
+ this.messageType = messageType;
12
+ this.validationErrors = errors;
13
+ }
14
+ };
15
+
16
+ // src/ValidationMiddleware.ts
17
+ function createValidationMiddleware(options) {
18
+ const {
19
+ validators,
20
+ onInvalid = "throw",
21
+ deadLetterHandler,
22
+ logger,
23
+ strictMode = false,
24
+ excludeTypes = []
25
+ } = options;
26
+ if (onInvalid === "dlq" && !deadLetterHandler) {
27
+ throw new Error(
28
+ "deadLetterHandler is required when onInvalid is set to 'dlq'"
29
+ );
30
+ }
31
+ const excludeSet = new Set(excludeTypes);
32
+ return async (ctx, next) => {
33
+ const { envelope } = ctx;
34
+ const messageType = envelope.type;
35
+ if (excludeSet.has(messageType)) {
36
+ await next();
37
+ return;
38
+ }
39
+ const validator = validators[messageType];
40
+ if (!validator) {
41
+ if (strictMode) {
42
+ const errors = [
43
+ {
44
+ path: "type",
45
+ message: `No validator registered for message type: ${messageType}`
46
+ }
47
+ ];
48
+ await handleInvalidMessage(
49
+ ctx,
50
+ errors,
51
+ onInvalid,
52
+ deadLetterHandler,
53
+ logger
54
+ );
55
+ return;
56
+ }
57
+ await next();
58
+ return;
59
+ }
60
+ const result = await validator(envelope.payload);
61
+ if (result.valid) {
62
+ await next();
63
+ return;
64
+ }
65
+ await handleInvalidMessage(
66
+ ctx,
67
+ result.errors || [],
68
+ onInvalid,
69
+ deadLetterHandler,
70
+ logger
71
+ );
72
+ };
73
+ }
74
+ async function handleInvalidMessage(ctx, errors, onInvalid, deadLetterHandler, logger) {
75
+ const { envelope } = ctx;
76
+ switch (onInvalid) {
77
+ case "throw":
78
+ throw new MessageValidationError(envelope.id, envelope.type, errors);
79
+ case "dlq":
80
+ logger?.error("Invalid message, sending to DLQ", {
81
+ messageId: envelope.id,
82
+ messageType: envelope.type,
83
+ correlationId: ctx.correlationId,
84
+ sagaName: ctx.sagaName,
85
+ errors
86
+ });
87
+ await deadLetterHandler(envelope, errors);
88
+ break;
89
+ case "log":
90
+ logger?.warn("Invalid message detected, skipping", {
91
+ messageId: envelope.id,
92
+ messageType: envelope.type,
93
+ correlationId: ctx.correlationId,
94
+ sagaName: ctx.sagaName,
95
+ errors
96
+ });
97
+ // Fall through to skip
98
+ case "skip":
99
+ default:
100
+ break;
101
+ }
102
+ }
103
+
104
+ // src/validators/ZodValidator.ts
105
+ function createZodValidator(schema) {
106
+ return (message) => {
107
+ const result = schema.safeParse(message);
108
+ if (result.success) {
109
+ return { valid: true };
110
+ }
111
+ const errors = result.error.issues.map((issue) => ({
112
+ path: issue.path.join("."),
113
+ message: issue.message
114
+ }));
115
+ return { valid: false, errors };
116
+ };
117
+ }
118
+ function createZodValidators(schemas) {
119
+ const validators = {};
120
+ for (const [type, schema] of Object.entries(schemas)) {
121
+ validators[type] = createZodValidator(schema);
122
+ }
123
+ return validators;
124
+ }
125
+
126
+ // src/validators/FunctionValidator.ts
127
+ function createFunctionValidator(fn) {
128
+ return async (message) => {
129
+ const result = await fn(message);
130
+ if (result === true) {
131
+ return { valid: true };
132
+ }
133
+ const errors = [];
134
+ if (result === false) {
135
+ errors.push({ path: "", message: "Validation failed" });
136
+ } else if (typeof result === "string") {
137
+ errors.push({ path: "", message: result });
138
+ } else if (Array.isArray(result)) {
139
+ for (const msg of result) {
140
+ errors.push({ path: "", message: msg });
141
+ }
142
+ }
143
+ return { valid: false, errors };
144
+ };
145
+ }
146
+ function combineValidators(validators) {
147
+ return async (message) => {
148
+ const allErrors = [];
149
+ for (const validator of validators) {
150
+ const result = await validator(message);
151
+ if (!result.valid && result.errors) {
152
+ allErrors.push(...result.errors);
153
+ }
154
+ }
155
+ if (allErrors.length === 0) {
156
+ return { valid: true };
157
+ }
158
+ return { valid: false, errors: allErrors };
159
+ };
160
+ }
161
+ export {
162
+ MessageValidationError,
163
+ combineValidators,
164
+ createFunctionValidator,
165
+ createValidationMiddleware,
166
+ createZodValidator,
167
+ createZodValidators
168
+ };
169
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/ValidationMiddleware.ts","../src/validators/ZodValidator.ts","../src/validators/FunctionValidator.ts"],"sourcesContent":["import type { MessageEnvelope, BaseMessage } from \"@saga-bus/core\";\n\n/**\n * Result of a validation.\n */\nexport interface ValidationResult {\n /** Whether the validation passed */\n valid: boolean;\n /** Validation errors if invalid */\n errors?: ValidationError[];\n}\n\n/**\n * A single validation error.\n */\nexport interface ValidationError {\n /** Path to the invalid field (e.g., \"payload.items[0].quantity\") */\n path: string;\n /** Error message */\n message: string;\n /** The invalid value */\n value?: unknown;\n}\n\n/**\n * A synchronous validator function that validates a message.\n */\nexport type SyncMessageValidator<T extends BaseMessage = BaseMessage> = (\n message: T\n) => ValidationResult;\n\n/**\n * An asynchronous validator function that validates a message.\n */\nexport type AsyncMessageValidator<T extends BaseMessage = BaseMessage> = (\n message: T\n) => Promise<ValidationResult>;\n\n/**\n * A validator function that validates a message (sync or async).\n */\nexport type MessageValidator<T extends BaseMessage = BaseMessage> = (\n message: T\n) => ValidationResult | Promise<ValidationResult>;\n\n/**\n * Action to take when validation fails.\n */\nexport type InvalidMessageAction = \"skip\" | \"log\" | \"throw\" | \"dlq\";\n\n/**\n * Dead-letter queue handler for invalid messages.\n */\nexport type DeadLetterHandler = (\n envelope: MessageEnvelope,\n errors: ValidationError[]\n) => Promise<void>;\n\n/**\n * A validator that can be used in the middleware (accepts any message type).\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyMessageValidator = (message: any) => ValidationResult | Promise<ValidationResult>;\n\n/**\n * Options for the validation middleware.\n */\nexport interface ValidationMiddlewareOptions {\n /**\n * Map of message type to validator.\n * Messages without a registered validator will pass through.\n */\n validators: Record<string, AnyMessageValidator>;\n\n /**\n * Action to take when validation fails.\n * - \"skip\": Silently skip processing\n * - \"log\": Log warning and skip\n * - \"throw\": Throw a ValidationError\n * - \"dlq\": Send to dead-letter queue handler\n * @default \"throw\"\n */\n onInvalid?: InvalidMessageAction;\n\n /**\n * Handler for dead-letter queue (required if onInvalid is \"dlq\").\n */\n deadLetterHandler?: DeadLetterHandler;\n\n /**\n * Custom logger for validation errors.\n */\n logger?: {\n warn(message: string, meta?: Record<string, unknown>): void;\n error(message: string, meta?: Record<string, unknown>): void;\n };\n\n /**\n * Whether to validate messages that don't have a registered validator.\n * If true, messages without validators will be rejected.\n * @default false\n */\n strictMode?: boolean;\n\n /**\n * Message types that should not be validated.\n */\n excludeTypes?: string[];\n}\n\n/**\n * Error thrown when validation fails and onInvalid is \"throw\".\n */\nexport class MessageValidationError extends Error {\n public readonly messageId: string;\n public readonly messageType: string;\n public readonly validationErrors: ValidationError[];\n\n constructor(\n messageId: string,\n messageType: string,\n errors: ValidationError[]\n ) {\n const errorSummary = errors\n .map((e) => `${e.path}: ${e.message}`)\n .join(\", \");\n super(`Validation failed for message ${messageId}: ${errorSummary}`);\n this.name = \"MessageValidationError\";\n this.messageId = messageId;\n this.messageType = messageType;\n this.validationErrors = errors;\n }\n}\n","import type { SagaMiddleware, SagaPipelineContext } from \"@saga-bus/core\";\nimport type { ValidationMiddlewareOptions, ValidationError } from \"./types.js\";\nimport { MessageValidationError } from \"./types.js\";\n\n/**\n * Creates validation middleware that validates message payloads before processing.\n *\n * @example\n * ```typescript\n * import { createValidationMiddleware, createZodValidator } from \"@saga-bus/middleware-validation\";\n * import { z } from \"zod\";\n *\n * const validationMiddleware = createValidationMiddleware({\n * validators: {\n * OrderCreated: createZodValidator(z.object({\n * type: z.literal(\"OrderCreated\"),\n * orderId: z.string(),\n * customerId: z.string(),\n * })),\n * },\n * onInvalid: \"throw\",\n * });\n *\n * const bus = createBus({\n * transport,\n * store,\n * sagas: [MySaga],\n * middleware: [validationMiddleware],\n * });\n * ```\n */\nexport function createValidationMiddleware(\n options: ValidationMiddlewareOptions\n): SagaMiddleware {\n const {\n validators,\n onInvalid = \"throw\",\n deadLetterHandler,\n logger,\n strictMode = false,\n excludeTypes = [],\n } = options;\n\n // Validate options\n if (onInvalid === \"dlq\" && !deadLetterHandler) {\n throw new Error(\n \"deadLetterHandler is required when onInvalid is set to 'dlq'\"\n );\n }\n\n const excludeSet = new Set(excludeTypes);\n\n return async (ctx: SagaPipelineContext, next: () => Promise<void>) => {\n const { envelope } = ctx;\n const messageType = envelope.type;\n\n // Skip excluded types\n if (excludeSet.has(messageType)) {\n await next();\n return;\n }\n\n // Get validator for this message type\n const validator = validators[messageType];\n\n // Handle missing validator\n if (!validator) {\n if (strictMode) {\n const errors: ValidationError[] = [\n {\n path: \"type\",\n message: `No validator registered for message type: ${messageType}`,\n },\n ];\n await handleInvalidMessage(\n ctx,\n errors,\n onInvalid,\n deadLetterHandler,\n logger\n );\n return;\n }\n // Not strict mode - pass through\n await next();\n return;\n }\n\n // Validate the message payload\n const result = await validator(envelope.payload);\n\n if (result.valid) {\n await next();\n return;\n }\n\n // Handle invalid message\n await handleInvalidMessage(\n ctx,\n result.errors || [],\n onInvalid,\n deadLetterHandler,\n logger\n );\n };\n}\n\nasync function handleInvalidMessage(\n ctx: SagaPipelineContext,\n errors: ValidationError[],\n onInvalid: string,\n deadLetterHandler: ((envelope: typeof ctx.envelope, errors: ValidationError[]) => Promise<void>) | undefined,\n logger: { warn(message: string, meta?: Record<string, unknown>): void; error(message: string, meta?: Record<string, unknown>): void } | undefined\n): Promise<void> {\n const { envelope } = ctx;\n\n switch (onInvalid) {\n case \"throw\":\n throw new MessageValidationError(envelope.id, envelope.type, errors);\n\n case \"dlq\":\n logger?.error(\"Invalid message, sending to DLQ\", {\n messageId: envelope.id,\n messageType: envelope.type,\n correlationId: ctx.correlationId,\n sagaName: ctx.sagaName,\n errors,\n });\n await deadLetterHandler!(envelope, errors);\n break;\n\n case \"log\":\n logger?.warn(\"Invalid message detected, skipping\", {\n messageId: envelope.id,\n messageType: envelope.type,\n correlationId: ctx.correlationId,\n sagaName: ctx.sagaName,\n errors,\n });\n // Fall through to skip\n\n case \"skip\":\n default:\n // Don't call next() - silently skip processing\n break;\n }\n}\n","import type { BaseMessage } from \"@saga-bus/core\";\nimport type { SyncMessageValidator, ValidationResult, ValidationError, AnyMessageValidator } from \"../types.js\";\n\n// Type for Zod schema - we don't import zod directly to keep it optional\ninterface ZodSchema {\n safeParse(data: unknown): {\n success: boolean;\n data?: unknown;\n error?: {\n issues: Array<{\n path: (string | number)[];\n message: string;\n }>;\n };\n };\n}\n\n/**\n * Creates a validator from a Zod schema.\n *\n * @example\n * ```typescript\n * import { z } from \"zod\";\n * import { createZodValidator } from \"@saga-bus/middleware-validation\";\n *\n * const OrderCreatedSchema = z.object({\n * type: z.literal(\"OrderCreated\"),\n * orderId: z.string().uuid(),\n * customerId: z.string(),\n * items: z.array(z.object({\n * sku: z.string(),\n * quantity: z.number().positive(),\n * })),\n * total: z.number().nonnegative(),\n * });\n *\n * const validator = createZodValidator(OrderCreatedSchema);\n * ```\n */\nexport function createZodValidator<T extends BaseMessage = BaseMessage>(schema: ZodSchema): SyncMessageValidator<T> {\n return (message: T): ValidationResult => {\n const result = schema.safeParse(message);\n\n if (result.success) {\n return { valid: true };\n }\n\n const errors: ValidationError[] = result.error!.issues.map((issue) => ({\n path: issue.path.join(\".\"),\n message: issue.message,\n }));\n\n return { valid: false, errors };\n };\n}\n\n/**\n * Creates multiple validators from a map of Zod schemas.\n *\n * @example\n * ```typescript\n * import { z } from \"zod\";\n * import { createZodValidators } from \"@saga-bus/middleware-validation\";\n *\n * const validators = createZodValidators({\n * OrderCreated: z.object({\n * type: z.literal(\"OrderCreated\"),\n * orderId: z.string(),\n * }),\n * OrderShipped: z.object({\n * type: z.literal(\"OrderShipped\"),\n * orderId: z.string(),\n * trackingNumber: z.string(),\n * }),\n * });\n * ```\n */\nexport function createZodValidators(\n schemas: Record<string, ZodSchema>\n): Record<string, AnyMessageValidator> {\n const validators: Record<string, AnyMessageValidator> = {};\n\n for (const [type, schema] of Object.entries(schemas)) {\n validators[type] = createZodValidator(schema);\n }\n\n return validators;\n}\n","import type { BaseMessage } from \"@saga-bus/core\";\nimport type {\n MessageValidator,\n ValidationResult,\n ValidationError,\n} from \"../types.js\";\n\n/**\n * A simple validation function that returns true/false or an error message.\n */\nexport type SimpleValidationFn<T> = (\n message: T\n) => boolean | string | string[] | Promise<boolean | string | string[]>;\n\n/**\n * Creates a validator from a simple validation function.\n *\n * @example\n * ```typescript\n * import { createFunctionValidator } from \"@saga-bus/middleware-validation\";\n *\n * const validator = createFunctionValidator((message: OrderCreated) => {\n * if (!message.orderId) {\n * return \"orderId is required\";\n * }\n * if (message.items.length === 0) {\n * return \"Order must have at least one item\";\n * }\n * return true;\n * });\n * ```\n */\nexport function createFunctionValidator<T extends BaseMessage = BaseMessage>(\n fn: SimpleValidationFn<T>\n): MessageValidator<T> {\n return async (message: T): Promise<ValidationResult> => {\n const result = await fn(message);\n\n if (result === true) {\n return { valid: true };\n }\n\n const errors: ValidationError[] = [];\n\n if (result === false) {\n errors.push({ path: \"\", message: \"Validation failed\" });\n } else if (typeof result === \"string\") {\n errors.push({ path: \"\", message: result });\n } else if (Array.isArray(result)) {\n for (const msg of result) {\n errors.push({ path: \"\", message: msg });\n }\n }\n\n return { valid: false, errors };\n };\n}\n\n/**\n * Combines multiple validators into one.\n * All validators must pass for the message to be valid.\n *\n * @example\n * ```typescript\n * import { combineValidators, createFunctionValidator } from \"@saga-bus/middleware-validation\";\n *\n * const validator = combineValidators([\n * createFunctionValidator((msg) => !!msg.orderId || \"orderId required\"),\n * createFunctionValidator((msg) => msg.items.length > 0 || \"items required\"),\n * ]);\n * ```\n */\nexport function combineValidators<T extends BaseMessage = BaseMessage>(\n validators: MessageValidator<T>[]\n): MessageValidator<T> {\n return async (message: T): Promise<ValidationResult> => {\n const allErrors: ValidationError[] = [];\n\n for (const validator of validators) {\n const result = await validator(message);\n if (!result.valid && result.errors) {\n allErrors.push(...result.errors);\n }\n }\n\n if (allErrors.length === 0) {\n return { valid: true };\n }\n\n return { valid: false, errors: allErrors };\n };\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@saga-bus/middleware-validation",
3
+ "version": "0.1.0",
4
+ "description": "Validation middleware for saga-bus message payload validation",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "dependencies": {
24
+ "@saga-bus/core": "0.1.0"
25
+ },
26
+ "peerDependencies": {
27
+ "zod": ">=3.0.0"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "zod": {
31
+ "optional": true
32
+ }
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.15.21",
36
+ "tsup": "^8.0.0",
37
+ "typescript": "^5.9.2",
38
+ "vitest": "^3.0.0",
39
+ "zod": "^3.24.0",
40
+ "@repo/eslint-config": "0.0.0",
41
+ "@repo/typescript-config": "0.0.0"
42
+ },
43
+ "keywords": [
44
+ "saga",
45
+ "saga-bus",
46
+ "middleware",
47
+ "validation",
48
+ "zod",
49
+ "schema",
50
+ "event-sourcing"
51
+ ],
52
+ "license": "MIT",
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "dev": "tsup --watch",
56
+ "lint": "eslint src/",
57
+ "check-types": "tsc --noEmit",
58
+ "test": "vitest run",
59
+ "test:watch": "vitest"
60
+ }
61
+ }