@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.
- package/README.md +125 -0
- package/dist/src/adapters/cloudevents/cloudevents.d.ts +14 -0
- package/dist/src/adapters/cloudevents/cloudevents.js +58 -0
- package/dist/src/adapters/cloudevents/index.d.ts +8 -0
- package/dist/src/adapters/cloudevents/index.js +7 -0
- package/dist/src/adapters/cloudevents/parsers/binary-mode.d.ts +5 -0
- package/dist/src/adapters/cloudevents/parsers/binary-mode.js +32 -0
- package/dist/src/adapters/cloudevents/parsers/pubsub.d.ts +5 -0
- package/dist/src/adapters/cloudevents/parsers/pubsub.js +54 -0
- package/dist/src/adapters/cloudevents/parsers/raw-event.d.ts +5 -0
- package/dist/src/adapters/cloudevents/parsers/raw-event.js +17 -0
- package/dist/src/adapters/cloudevents/parsers/structured-mode.d.ts +5 -0
- package/dist/src/adapters/cloudevents/parsers/structured-mode.js +18 -0
- package/dist/src/adapters/cloudevents/types.d.ts +29 -0
- package/dist/src/adapters/cloudevents/types.js +1 -0
- package/dist/src/domain/discovery.d.ts +24 -0
- package/dist/src/domain/discovery.js +137 -0
- package/dist/src/domain/handler-factory.d.ts +24 -0
- package/dist/src/domain/handler-factory.js +62 -0
- package/dist/src/domain/index.d.ts +5 -0
- package/dist/src/domain/index.js +3 -0
- package/dist/src/domain/types.d.ts +52 -0
- package/dist/src/domain/types.js +6 -0
- package/dist/src/domain/validation.d.ts +37 -0
- package/dist/src/domain/validation.js +53 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +4 -0
- package/dist/src/infrastructure/errors.d.ts +53 -0
- package/dist/src/infrastructure/errors.js +54 -0
- package/dist/src/infrastructure/index.d.ts +4 -0
- package/dist/src/infrastructure/index.js +2 -0
- package/dist/src/infrastructure/logging.d.ts +18 -0
- package/dist/src/infrastructure/logging.js +27 -0
- package/dist/src/middlewares/cloudevents-middleware.d.ts +171 -0
- package/dist/src/middlewares/cloudevents-middleware.js +276 -0
- package/dist/src/middlewares/index.d.ts +1 -0
- package/dist/src/middlewares/index.js +1 -0
- package/dist/src/processing/dlq-safe.d.ts +34 -0
- package/dist/src/processing/dlq-safe.js +91 -0
- package/dist/src/processing/handler-cache.d.ts +36 -0
- package/dist/src/processing/handler-cache.js +94 -0
- package/dist/src/processing/index.d.ts +3 -0
- package/dist/src/processing/index.js +3 -0
- package/dist/src/processing/validation.d.ts +41 -0
- package/dist/src/processing/validation.js +48 -0
- package/dist/src/publishing/index.d.ts +2 -0
- package/dist/src/publishing/index.js +2 -0
- package/dist/src/publishing/nats.publisher.d.ts +22 -0
- package/dist/src/publishing/nats.publisher.js +66 -0
- package/dist/src/publishing/pubsub.publisher.d.ts +39 -0
- package/dist/src/publishing/pubsub.publisher.js +84 -0
- package/dist/src/transports/nats/index.d.ts +2 -0
- package/dist/src/transports/nats/index.js +2 -0
- package/dist/src/transports/nats/nats-consumer.d.ts +30 -0
- package/dist/src/transports/nats/nats-consumer.js +54 -0
- package/dist/src/transports/nats/nats-message-processor.d.ts +22 -0
- package/dist/src/transports/nats/nats-message-processor.js +95 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Orderboss CloudEvents Toolkit
|
|
2
|
+
|
|
3
|
+
[](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,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,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,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';
|