@crossdelta/cloudevents 0.1.1

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.
Files changed (58) hide show
  1. package/README.md +125 -0
  2. package/dist/src/adapters/cloudevents/cloudevents.d.ts +14 -0
  3. package/dist/src/adapters/cloudevents/cloudevents.js +58 -0
  4. package/dist/src/adapters/cloudevents/index.d.ts +8 -0
  5. package/dist/src/adapters/cloudevents/index.js +7 -0
  6. package/dist/src/adapters/cloudevents/parsers/binary-mode.d.ts +5 -0
  7. package/dist/src/adapters/cloudevents/parsers/binary-mode.js +32 -0
  8. package/dist/src/adapters/cloudevents/parsers/pubsub.d.ts +5 -0
  9. package/dist/src/adapters/cloudevents/parsers/pubsub.js +54 -0
  10. package/dist/src/adapters/cloudevents/parsers/raw-event.d.ts +5 -0
  11. package/dist/src/adapters/cloudevents/parsers/raw-event.js +17 -0
  12. package/dist/src/adapters/cloudevents/parsers/structured-mode.d.ts +5 -0
  13. package/dist/src/adapters/cloudevents/parsers/structured-mode.js +18 -0
  14. package/dist/src/adapters/cloudevents/types.d.ts +29 -0
  15. package/dist/src/adapters/cloudevents/types.js +1 -0
  16. package/dist/src/domain/discovery.d.ts +24 -0
  17. package/dist/src/domain/discovery.js +137 -0
  18. package/dist/src/domain/handler-factory.d.ts +24 -0
  19. package/dist/src/domain/handler-factory.js +62 -0
  20. package/dist/src/domain/index.d.ts +5 -0
  21. package/dist/src/domain/index.js +3 -0
  22. package/dist/src/domain/types.d.ts +52 -0
  23. package/dist/src/domain/types.js +6 -0
  24. package/dist/src/domain/validation.d.ts +37 -0
  25. package/dist/src/domain/validation.js +53 -0
  26. package/dist/src/index.d.ts +5 -0
  27. package/dist/src/index.js +4 -0
  28. package/dist/src/infrastructure/errors.d.ts +53 -0
  29. package/dist/src/infrastructure/errors.js +54 -0
  30. package/dist/src/infrastructure/index.d.ts +4 -0
  31. package/dist/src/infrastructure/index.js +2 -0
  32. package/dist/src/infrastructure/logging.d.ts +18 -0
  33. package/dist/src/infrastructure/logging.js +27 -0
  34. package/dist/src/middlewares/cloudevents-middleware.d.ts +171 -0
  35. package/dist/src/middlewares/cloudevents-middleware.js +276 -0
  36. package/dist/src/middlewares/index.d.ts +1 -0
  37. package/dist/src/middlewares/index.js +1 -0
  38. package/dist/src/processing/dlq-safe.d.ts +34 -0
  39. package/dist/src/processing/dlq-safe.js +91 -0
  40. package/dist/src/processing/handler-cache.d.ts +36 -0
  41. package/dist/src/processing/handler-cache.js +94 -0
  42. package/dist/src/processing/index.d.ts +3 -0
  43. package/dist/src/processing/index.js +3 -0
  44. package/dist/src/processing/validation.d.ts +41 -0
  45. package/dist/src/processing/validation.js +48 -0
  46. package/dist/src/publishing/index.d.ts +2 -0
  47. package/dist/src/publishing/index.js +2 -0
  48. package/dist/src/publishing/nats.publisher.d.ts +22 -0
  49. package/dist/src/publishing/nats.publisher.js +66 -0
  50. package/dist/src/publishing/pubsub.publisher.d.ts +39 -0
  51. package/dist/src/publishing/pubsub.publisher.js +84 -0
  52. package/dist/src/transports/nats/index.d.ts +2 -0
  53. package/dist/src/transports/nats/index.js +2 -0
  54. package/dist/src/transports/nats/nats-consumer.d.ts +30 -0
  55. package/dist/src/transports/nats/nats-consumer.js +54 -0
  56. package/dist/src/transports/nats/nats-message-processor.d.ts +22 -0
  57. package/dist/src/transports/nats/nats-message-processor.js +95 -0
  58. package/package.json +46 -0
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # Orderboss CloudEvents Toolkit
2
+
3
+ [![Architecture](https://img.shields.io/badge/architecture-arc42-blue?style=flat-square&logo=gitbook)](docs/architecture.md)
4
+
5
+ CloudEvents middleware, publishers, and consumers that integrate cleanly with [Hono](https://hono.dev/). Ship handler discovery, DLQ-safe processing for Google Pub/Sub & Eventarc, and NATS utilities without wiring everything yourself.
6
+
7
+ ## Features
8
+
9
+ - Automatic handler discovery for `*.event.(ts|js)` files with caching
10
+ - CloudEvents parsing for structured, binary, Pub/Sub push, and raw payloads
11
+ - DLQ-safe mode that quarantines invalid messages and reports recoverable errors
12
+ - First-class Zod support with optional safe-parse fallback
13
+ - Shared tooling for Google Pub/Sub and NATS (publish & consume)
14
+
15
+ ## Getting started
16
+
17
+ ### 1. Install the package
18
+
19
+ ```bash
20
+ bun add @orderboss/cloudevents
21
+ ```
22
+
23
+ ### 2. Define a typed event handler
24
+
25
+ ```typescript
26
+ import { z } from 'zod'
27
+ import { eventSchema, handleEvent } from '@orderboss/cloudevents'
28
+
29
+ const CustomerCreatedSchema = eventSchema({
30
+ type: z.literal('customer.created'),
31
+ payload: z.object({
32
+ id: z.string(),
33
+ email: z.string().email(),
34
+ }),
35
+ })
36
+
37
+ export const CustomerCreatedHandler = handleEvent(CustomerCreatedSchema, async ({ payload }) => {
38
+ await provisionAccount(payload)
39
+ })
40
+ ```
41
+
42
+ > Need a softer failure mode? Set `CustomerCreatedHandler.__eventarcMetadata.safeParse = true` before registering it to skip invalid payloads in favour of quarantine logs.
43
+
44
+ ### 3. Register the middleware in Hono
45
+
46
+ ```typescript
47
+ import { Hono } from 'hono'
48
+ import { cloudEvents } from '@orderboss/cloudevents'
49
+
50
+ const app = new Hono()
51
+
52
+ app.use('/events', cloudEvents({
53
+ discover: 'src/events/**/*.event.{ts,js}',
54
+ quarantineTopic: process.env.QUARANTINE_TOPIC,
55
+ errorTopic: process.env.ERROR_TOPIC,
56
+ projectId: process.env.GCP_PROJECT_ID,
57
+ log: process.env.NODE_ENV === 'production' ? 'structured' : 'pretty',
58
+ }))
59
+ ```
60
+
61
+ DLQ-safe mode activates automatically when `quarantineTopic` or `errorTopic` is provided.
62
+
63
+ ## Publishing events
64
+
65
+ ```typescript
66
+ import {
67
+ publishEvent,
68
+ publishRawEvent,
69
+ publishNatsEvent,
70
+ publishNatsRawEvent,
71
+ } from '@orderboss/cloudevents'
72
+
73
+ // Google Pub/Sub
74
+ await publishEvent('customer-events', CustomerCreatedSchema, {
75
+ payload: { id: 'cust_1', email: 'jane@example.com' },
76
+ })
77
+
78
+ await publishRawEvent('customer-events', 'customer.deleted', { id: 'cust_1' }, {
79
+ source: 'orderboss://customers',
80
+ projectId: 'my-project',
81
+ })
82
+
83
+ // NATS Streaming (reuse the schema you defined for your handler)
84
+ await publishNatsEvent('orders.placed', OrderPlacedSchema, {
85
+ payload: { id: 'ord_42', total: 149.9 },
86
+ })
87
+
88
+ await publishNatsRawEvent('orders.canceled', 'order.canceled', { id: 'ord_42' }, {
89
+ servers: process.env.NATS_URL,
90
+ })
91
+ ```
92
+
93
+ ## Consuming from NATS
94
+
95
+ ```typescript
96
+ import { consumeNatsEvents } from '@orderboss/cloudevents/transports/nats'
97
+
98
+ await consumeNatsEvents({
99
+ servers: process.env.NATS_URL,
100
+ subject: 'orders.placed',
101
+ discover: 'dist/events/*.event.js',
102
+ consumerName: 'orders-api',
103
+ quarantineTopic: 'projects/my-app/topics/nats-quarantine',
104
+ errorTopic: 'projects/my-app/topics/nats-errors',
105
+ })
106
+ ```
107
+
108
+ The consumer reuses your handler definitions, honours `safeParse`, and mirrors the DLQ-safe behaviour of the middleware.
109
+
110
+ ## API surface
111
+
112
+ - `cloudEvents(options)` – Hono middleware with discovery, dedupe, and DLQ-safe semantics
113
+ - `clearHandlerCache()` – resets the discovery cache (tests, hot reload)
114
+ - `eventSchema(schema)` – wraps a Zod object that includes a `type` literal
115
+ - `handleEvent(schemaOrOptions, handler)` – builds discovery-ready handler classes
116
+ - `parseEventFromContext(ctx)` – normalises incoming CloudEvents for manual flows
117
+ - `publishEvent` / `publishRawEvent` – publish to Google Pub/Sub
118
+ - `publishNatsEvent` / `publishNatsRawEvent` – publish to NATS subjects
119
+ - `consumeNatsEvents` – subscribe to NATS and execute discovered handlers
120
+
121
+ ## Further reading
122
+
123
+ - [Architecture notes](docs/architecture.md)
124
+ - Examples in `packages/cloudevents/test`
125
+ - Project changelog in the repository root
@@ -0,0 +1,14 @@
1
+ import type { Context } from 'hono';
2
+ import type { CloudEventParseResult } from './types';
3
+ /**
4
+ * Parses CloudEvent from Hono context with automatic format detection.
5
+ *
6
+ * Supports:
7
+ * 1. Structured CloudEvent (body with specversion)
8
+ * 2. Pub/Sub push message format (body with message field)
9
+ * 3. Binary CloudEvent (CloudEvent headers)
10
+ * 4. Raw event data (fallback)
11
+ *
12
+ * Uses dynamic imports to load only necessary parsers for optimal performance.
13
+ */
14
+ export declare const parseEventFromContext: (context: Context) => Promise<CloudEventParseResult>;
@@ -0,0 +1,58 @@
1
+ import { logger } from '../../infrastructure';
2
+ /**
3
+ * Checks if request has CloudEvent headers (for binary mode)
4
+ */
5
+ const hasCloudEventHeaders = (headers) => Object.keys(headers).some((key) => key.toLowerCase().startsWith('ce-'));
6
+ const isRecord = (value) => typeof value === 'object' && value !== null;
7
+ /**
8
+ * Parses CloudEvent from Hono context with automatic format detection.
9
+ *
10
+ * Supports:
11
+ * 1. Structured CloudEvent (body with specversion)
12
+ * 2. Pub/Sub push message format (body with message field)
13
+ * 3. Binary CloudEvent (CloudEvent headers)
14
+ * 4. Raw event data (fallback)
15
+ *
16
+ * Uses dynamic imports to load only necessary parsers for optimal performance.
17
+ */
18
+ export const parseEventFromContext = async (context) => {
19
+ try {
20
+ const headers = context.req.header();
21
+ const rawBody = await context.req.text();
22
+ let parsedBody;
23
+ if (rawBody?.length) {
24
+ try {
25
+ parsedBody = JSON.parse(rawBody);
26
+ }
27
+ catch {
28
+ parsedBody = undefined;
29
+ }
30
+ }
31
+ const bodyObject = isRecord(parsedBody) ? parsedBody : undefined;
32
+ // 1. Structured CloudEvent (has specversion in body)
33
+ if (bodyObject && 'specversion' in bodyObject) {
34
+ const { parseStructuredMode } = await import('./parsers/structured-mode');
35
+ return parseStructuredMode(bodyObject);
36
+ }
37
+ // 2. PubSub message format (has message field)
38
+ if (bodyObject && 'message' in bodyObject) {
39
+ const { parsePubSubMessage } = await import('./parsers/pubsub');
40
+ return await parsePubSubMessage(bodyObject, headers);
41
+ }
42
+ // 3. Binary CloudEvent (CloudEvent headers, no specversion in body)
43
+ if (hasCloudEventHeaders(headers)) {
44
+ const { parseBinaryMode } = await import('./parsers/binary-mode');
45
+ return await parseBinaryMode(headers, rawBody);
46
+ }
47
+ // 4. Raw event data (fallback)
48
+ const { parseRawEvent } = await import('./parsers/raw-event');
49
+ if (bodyObject) {
50
+ return parseRawEvent(bodyObject);
51
+ }
52
+ return parseRawEvent({ raw: rawBody });
53
+ }
54
+ catch (error) {
55
+ logger.error('Failed to parse event:', error);
56
+ throw new Error(`Failed to parse CloudEvent: ${error instanceof Error ? error.message : 'Unknown error'}`);
57
+ }
58
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Adapters Module - External Protocol Handlers
3
+ *
4
+ * Entry point for all protocol adapters with automatic format detection
5
+ * and dynamic loading for optimal performance.
6
+ */
7
+ export * from './cloudevents';
8
+ export type { CloudEventParseResult, EventContext } from './types';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Adapters Module - External Protocol Handlers
3
+ *
4
+ * Entry point for all protocol adapters with automatic format detection
5
+ * and dynamic loading for optimal performance.
6
+ */
7
+ export * from './cloudevents';
@@ -0,0 +1,5 @@
1
+ import type { CloudEventParseResult } from '../types';
2
+ /**
3
+ * Parse binary mode CloudEvent using CloudEvents SDK
4
+ */
5
+ export declare const parseBinaryMode: (headers: Record<string, string | undefined>, rawBody: string) => Promise<CloudEventParseResult>;
@@ -0,0 +1,32 @@
1
+ import { HTTP } from 'cloudevents';
2
+ import { createCloudEventParseError, logger } from '../../../infrastructure';
3
+ /**
4
+ * Parse binary mode CloudEvent using CloudEvents SDK
5
+ */
6
+ export const parseBinaryMode = async (headers, rawBody) => {
7
+ const normalizedHeaders = {};
8
+ Object.entries(headers).forEach(([key, value]) => {
9
+ if (typeof value === 'string') {
10
+ normalizedHeaders[key] = value;
11
+ }
12
+ });
13
+ const body = rawBody ?? '';
14
+ try {
15
+ const event = HTTP.toEvent({ headers: normalizedHeaders, body });
16
+ const finalEvent = Array.isArray(event) ? event[0] : event;
17
+ const cloudEventData = {
18
+ id: finalEvent.id,
19
+ type: finalEvent.type,
20
+ specversion: finalEvent.specversion,
21
+ source: finalEvent.source,
22
+ subject: finalEvent.subject,
23
+ time: finalEvent.time || new Date().toISOString(),
24
+ data: finalEvent.data,
25
+ };
26
+ return { event: finalEvent, cloudEventData };
27
+ }
28
+ catch (error) {
29
+ logger.error('Binary mode parsing failed:', error);
30
+ throw createCloudEventParseError(`Binary mode parsing failed: ${error}`);
31
+ }
32
+ };
@@ -0,0 +1,5 @@
1
+ import type { CloudEventParseResult } from '../types';
2
+ /**
3
+ * Parses raw PubSub message into CloudEvent wrapper format
4
+ */
5
+ export declare const parsePubSubMessage: (rawBody: Record<string, unknown>, headers: Record<string, string | undefined>) => Promise<CloudEventParseResult>;
@@ -0,0 +1,54 @@
1
+ import { logger } from '../../../infrastructure';
2
+ /**
3
+ * Parses raw PubSub message into CloudEvent wrapper format
4
+ */
5
+ export const parsePubSubMessage = async (rawBody, headers) => {
6
+ const body = rawBody;
7
+ if (!body.message?.data) {
8
+ throw new Error('Invalid PubSub message: missing message.data');
9
+ }
10
+ // Try to extract real event type from message attributes or decoded data
11
+ let eventType = 'google.cloud.pubsub.topic.v1.messagePublished';
12
+ let decodedData = body;
13
+ try {
14
+ // Always try to decode the base64 data first
15
+ const base64Data = body.message.data;
16
+ const decodedString = Buffer.from(base64Data, 'base64').toString('utf8');
17
+ const parsedData = JSON.parse(decodedString);
18
+ // Check for event type in attributes first, then in parsed data
19
+ if (body.message.attributes?.eventType) {
20
+ eventType = body.message.attributes.eventType;
21
+ decodedData = parsedData;
22
+ }
23
+ else if (body.message.attributes?.type) {
24
+ eventType = body.message.attributes.type;
25
+ decodedData = parsedData;
26
+ }
27
+ else if (parsedData.type) {
28
+ eventType = parsedData.type;
29
+ decodedData = parsedData;
30
+ }
31
+ else {
32
+ // If no type found, use decoded data but keep default type
33
+ decodedData = parsedData;
34
+ }
35
+ }
36
+ catch (error) {
37
+ // If decoding fails, keep original type and data
38
+ logger.warn('Failed to extract event type from PubSub message:', error);
39
+ }
40
+ const messageId = body.message.messageId || 'unknown';
41
+ const eventSource = headers['ce-source'] || '//pubsub.googleapis.com';
42
+ const subject = headers['ce-subject'];
43
+ const publishTime = headers['ce-time'];
44
+ const cloudEventData = {
45
+ id: messageId,
46
+ type: eventType,
47
+ specversion: '1.0',
48
+ source: eventSource,
49
+ subject: subject,
50
+ time: publishTime || new Date().toISOString(),
51
+ data: decodedData,
52
+ };
53
+ return { event: null, cloudEventData };
54
+ };
@@ -0,0 +1,5 @@
1
+ import type { CloudEventParseResult } from '../types';
2
+ /**
3
+ * Parse raw event data and wrap it in CloudEvent structure
4
+ */
5
+ export declare const parseRawEvent: (body: Record<string, unknown>) => CloudEventParseResult;
@@ -0,0 +1,17 @@
1
+ import { logger } from '../../../infrastructure';
2
+ /**
3
+ * Parse raw event data and wrap it in CloudEvent structure
4
+ */
5
+ export const parseRawEvent = (body) => {
6
+ logger.info('Creating CloudEvent wrapper for raw data');
7
+ const cloudEventData = {
8
+ id: `raw-${Date.now()}`,
9
+ type: body.type || 'unknown.event',
10
+ specversion: '1.0',
11
+ source: '//direct.post',
12
+ subject: undefined,
13
+ time: new Date().toISOString(),
14
+ data: body,
15
+ };
16
+ return { event: null, cloudEventData };
17
+ };
@@ -0,0 +1,5 @@
1
+ import type { CloudEventParseResult } from '../types';
2
+ /**
3
+ * Parse structured mode CloudEvent (CloudEvents JSON in body)
4
+ */
5
+ export declare const parseStructuredMode: (body: Record<string, unknown>) => CloudEventParseResult;
@@ -0,0 +1,18 @@
1
+ import { logger } from '../../../infrastructure';
2
+ /**
3
+ * Parse structured mode CloudEvent (CloudEvents JSON in body)
4
+ */
5
+ export const parseStructuredMode = (body) => {
6
+ logger.info('Structured mode CloudEvent detected');
7
+ // For structured CloudEvents, use data directly since it's already in CloudEvent format
8
+ const cloudEventData = {
9
+ id: String(body.id || 'unknown'),
10
+ type: String(body.type || 'unknown.event'),
11
+ specversion: String(body.specversion || '1.0'),
12
+ source: String(body.source || '//unknown'),
13
+ subject: body.subject ? String(body.subject) : undefined,
14
+ time: String(body.time) || new Date().toISOString(),
15
+ data: body.data,
16
+ };
17
+ return { event: null, cloudEventData };
18
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Adapter types for external protocols
3
+ */
4
+ import type { CloudEventV1 } from 'cloudevents';
5
+ /**
6
+ * Simplified event context for handler consumption
7
+ *
8
+ * Provides essential CloudEvent metadata without the complexity of the full CloudEventV1 interface.
9
+ * This is what handler functions receive as context parameter.
10
+ */
11
+ export interface EventContext {
12
+ /** Event type identifier (from CloudEvent.type) */
13
+ eventType: string;
14
+ /** Event source (from CloudEvent.source) */
15
+ source: string;
16
+ /** Optional subject (from CloudEvent.subject) */
17
+ subject?: string;
18
+ /** Event timestamp (from CloudEvent.time) */
19
+ time: string;
20
+ /** Message ID for deduplication (from CloudEvent.id) */
21
+ messageId?: string;
22
+ }
23
+ /**
24
+ * Result of CloudEvent parsing - either CloudEvents SDK object or normalized data
25
+ */
26
+ export interface CloudEventParseResult {
27
+ event: CloudEventV1<unknown> | null;
28
+ cloudEventData: CloudEventV1<unknown> | null;
29
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import type { HandlerConstructor } from './types';
2
+ /**
3
+ * Discovers event handlers from files matching the given glob pattern.
4
+ *
5
+ * @param pattern - Glob pattern to match handler files (e.g., 'events/*.event.ts')
6
+ * @param options - Configuration options
7
+ * @returns Promise resolving to array of discovered handler constructors
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * // Discover all handlers in events directory
12
+ * const handlers = await discoverHandlers('events/*.event.ts')
13
+ *
14
+ * // With filtering and logging
15
+ * const handlers = await discoverHandlers('events/*.event.ts', {
16
+ * filter: (name, handler) => name.includes('Customer'),
17
+ * log: true
18
+ * })
19
+ * ```
20
+ */
21
+ export declare const discoverHandlers: (pattern: string, options?: {
22
+ filter?: (name: string, handler: unknown) => boolean;
23
+ log?: boolean;
24
+ }) => Promise<HandlerConstructor[]>;
@@ -0,0 +1,137 @@
1
+ import { dirname } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { glob } from 'glob';
4
+ import { logger } from '../infrastructure';
5
+ import { isValidHandler } from './validation';
6
+ /**
7
+ * Gets the search directories for event handler discovery.
8
+ * @returns Array of search directory paths.
9
+ */
10
+ const getSearchDirectories = () => {
11
+ const directories = new Set();
12
+ directories.add(process.cwd());
13
+ try {
14
+ const currentDir = dirname(fileURLToPath(import.meta.url));
15
+ directories.add(currentDir);
16
+ }
17
+ catch {
18
+ // Ignore resolution errors; fallback to process.cwd()
19
+ }
20
+ return [...directories];
21
+ };
22
+ /**
23
+ * Loads and validates handlers from a TypeScript/JavaScript file.
24
+ */
25
+ const loadHandlers = async (filePath, filter) => {
26
+ try {
27
+ const module = await import(filePath);
28
+ return Object.entries(module)
29
+ .filter(([name, handler]) => isValidHandler(handler) && (!filter || filter(name, handler)))
30
+ .map(([name, handler]) => {
31
+ const HandlerClass = handler;
32
+ return HandlerClass.name !== name
33
+ ? Object.defineProperty(HandlerClass, 'name', { value: name, configurable: true })
34
+ : HandlerClass;
35
+ });
36
+ }
37
+ catch (error) {
38
+ logger?.warn(`Failed to load ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
39
+ return [];
40
+ }
41
+ };
42
+ /**
43
+ * Deduplicates handlers by class name to avoid loading the same handler from .ts and .js
44
+ * @param handlers - Array of handler constructors
45
+ * @returns Array of unique handlers
46
+ */
47
+ const deduplicateHandlers = (handlers) => handlers.reduce((acc, handler) => {
48
+ const existing = acc.find((h) => h.name === handler.name);
49
+ if (!existing) {
50
+ acc.push(handler);
51
+ }
52
+ return acc;
53
+ }, []);
54
+ /**
55
+ * Discovers files matching the pattern across multiple search paths
56
+ * @param pattern - Glob pattern to match
57
+ * @param searchPaths - Array of directories to search in
58
+ * @param logger - Logger instance for debug output
59
+ * @param log - Whether logging is enabled
60
+ * @returns Promise resolving to array of unique file paths
61
+ */
62
+ /**
63
+ * Simple, elegant file discovery using glob patterns
64
+ */
65
+ const EXTENSION_FALLBACKS = ['js', 'mjs', 'cjs'];
66
+ const expandPatternVariants = (pattern, preferCompiled) => {
67
+ if (!/\.tsx?\b/.test(pattern)) {
68
+ return [pattern];
69
+ }
70
+ const basePattern = pattern;
71
+ const compiledVariants = EXTENSION_FALLBACKS.map((ext) => basePattern.replace(/\.tsx?\b/g, `.${ext}`));
72
+ const ordered = preferCompiled ? [...compiledVariants, basePattern] : [basePattern, ...compiledVariants];
73
+ return [...new Set(ordered)];
74
+ };
75
+ const discoverFiles = async (pattern, basePath, preferCompiled) => {
76
+ const prefixedPattern = `{src,dist,build,lib,out}/${pattern}`;
77
+ const patterns = preferCompiled ? [prefixedPattern, pattern] : [pattern, prefixedPattern];
78
+ const allFiles = [];
79
+ for (const globPattern of patterns) {
80
+ const variants = expandPatternVariants(globPattern, preferCompiled);
81
+ for (const variant of variants) {
82
+ try {
83
+ const files = await glob(variant, { cwd: basePath, absolute: true });
84
+ allFiles.push(...files);
85
+ }
86
+ catch {
87
+ // Ignore errors
88
+ }
89
+ }
90
+ }
91
+ return [...new Set(allFiles)]; // Remove duplicates
92
+ };
93
+ /**
94
+ * Discovers event handlers from files matching the given glob pattern.
95
+ *
96
+ * @param pattern - Glob pattern to match handler files (e.g., 'events/*.event.ts')
97
+ * @param options - Configuration options
98
+ * @returns Promise resolving to array of discovered handler constructors
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * // Discover all handlers in events directory
103
+ * const handlers = await discoverHandlers('events/*.event.ts')
104
+ *
105
+ * // With filtering and logging
106
+ * const handlers = await discoverHandlers('events/*.event.ts', {
107
+ * filter: (name, handler) => name.includes('Customer'),
108
+ * log: true
109
+ * })
110
+ * ```
111
+ */
112
+ export const discoverHandlers = async (pattern, options = {}) => {
113
+ const { filter } = options;
114
+ const searchDirectories = getSearchDirectories();
115
+ const preferCompiled = searchDirectories.some((dir) => dir.includes('/dist/') || dir.includes('\\dist\\'));
116
+ try {
117
+ const uniqueFiles = new Set();
118
+ for (const basePath of searchDirectories) {
119
+ const discoveredFiles = await discoverFiles(pattern, basePath, preferCompiled);
120
+ for (const file of discoveredFiles) {
121
+ uniqueFiles.add(file);
122
+ }
123
+ }
124
+ if (uniqueFiles.size === 0) {
125
+ logger.warn(`No files found matching pattern: ${pattern}`);
126
+ return [];
127
+ }
128
+ const handlers = await Promise.all([...uniqueFiles].map((file) => loadHandlers(file, filter)));
129
+ const flatHandlers = handlers.flat();
130
+ const uniqueHandlers = deduplicateHandlers(flatHandlers);
131
+ return uniqueHandlers;
132
+ }
133
+ catch (error) {
134
+ logger.error('Discovery failed:', error);
135
+ return [];
136
+ }
137
+ };
@@ -0,0 +1,24 @@
1
+ import { type ZodTypeAny, z } from 'zod';
2
+ import type { EventContext } from '../adapters/cloudevents';
3
+ import type { HandlerConstructor } from './types';
4
+ /**
5
+ * Creates an event handler using the handleEvent pattern
6
+ * Compatible with the new middleware system
7
+ */
8
+ export declare function handleEvent<TSchema extends ZodTypeAny>(schemaOrOptions: TSchema | {
9
+ schema: TSchema;
10
+ type?: string;
11
+ }, handler: (payload: TSchema['_output'], context?: EventContext) => Promise<void> | void, eventType?: string): HandlerConstructor;
12
+ /**
13
+ * Creates an event schema with type inference
14
+ * Automatically enforces the presence of a 'type' field
15
+ */
16
+ export declare function eventSchema<T extends Record<string, ZodTypeAny>>(schema: T & {
17
+ type: ZodTypeAny;
18
+ }): z.ZodObject<T & {
19
+ type: ZodTypeAny;
20
+ }, "strip", ZodTypeAny, z.objectUtil.addQuestionMarks<z.baseObjectOutputType<T & {
21
+ type: ZodTypeAny;
22
+ }>, any> extends infer T_1 ? { [k in keyof T_1]: T_1[k]; } : never, z.baseObjectInputType<T & {
23
+ type: ZodTypeAny;
24
+ }> extends infer T_2 ? { [k_1 in keyof T_2]: T_2[k_1]; } : never>;
@@ -0,0 +1,62 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Extracts event type from schema
4
+ */
5
+ function extractEventTypeFromSchema(schema) {
6
+ if ('shape' in schema && schema.shape && typeof schema.shape === 'object') {
7
+ const shape = schema.shape;
8
+ if (shape.type && typeof shape.type === 'object' && shape.type !== null && '_def' in shape.type) {
9
+ const typeDef = shape.type._def;
10
+ return typeDef?.value;
11
+ }
12
+ }
13
+ return undefined;
14
+ }
15
+ /**
16
+ * Creates an event handler using the handleEvent pattern
17
+ * Compatible with the new middleware system
18
+ */
19
+ export function handleEvent(schemaOrOptions, handler, eventType) {
20
+ // Handle both API formats
21
+ let schema;
22
+ let finalEventType = eventType;
23
+ if (schemaOrOptions && typeof schemaOrOptions === 'object' && 'schema' in schemaOrOptions) {
24
+ schema = schemaOrOptions.schema;
25
+ finalEventType = schemaOrOptions.type || eventType;
26
+ }
27
+ else {
28
+ schema = schemaOrOptions;
29
+ }
30
+ // Try to extract event type from schema if not provided
31
+ if (!finalEventType) {
32
+ finalEventType = extractEventTypeFromSchema(schema);
33
+ }
34
+ // Fallback to unknown event
35
+ if (!finalEventType) {
36
+ finalEventType = 'unknown.event';
37
+ }
38
+ // Create handler class with proper naming
39
+ const handlerName = `${finalEventType.replace(/[^a-zA-Z0-9]/g, '')}Handler`;
40
+ const HandlerClass = class extends Object {
41
+ static __eventarcMetadata = {
42
+ schema,
43
+ declaredType: finalEventType,
44
+ };
45
+ async handle(payload, context) {
46
+ await Promise.resolve(handler(payload, context));
47
+ }
48
+ };
49
+ // Set the class name properly
50
+ Object.defineProperty(HandlerClass, 'name', {
51
+ value: handlerName,
52
+ configurable: true,
53
+ });
54
+ return HandlerClass;
55
+ }
56
+ /**
57
+ * Creates an event schema with type inference
58
+ * Automatically enforces the presence of a 'type' field
59
+ */
60
+ export function eventSchema(schema) {
61
+ return z.object(schema);
62
+ }
@@ -0,0 +1,5 @@
1
+ export { discoverHandlers } from './discovery';
2
+ export { eventSchema, handleEvent } from './handler-factory';
3
+ export type { EnrichedEvent, EventHandler, HandleEventOptions, HandlerConstructor, HandlerMetadata, MatchFn, } from './types';
4
+ export type { HandlerValidationError, ValidationErrorDetail } from './validation';
5
+ export { extractTypeFromSchema, isValidHandler } from './validation';
@@ -0,0 +1,3 @@
1
+ export { discoverHandlers } from './discovery';
2
+ export { eventSchema, handleEvent } from './handler-factory';
3
+ export { extractTypeFromSchema, isValidHandler } from './validation';