@crossdelta/cloudevents 0.5.3 → 0.5.4
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.
|
@@ -1,59 +1,19 @@
|
|
|
1
1
|
import type { ZodTypeAny } from 'zod';
|
|
2
2
|
import { type RoutingConfig } from '../domain';
|
|
3
3
|
export interface PublishNatsEventOptions {
|
|
4
|
-
/**
|
|
5
|
-
* NATS URL(s), e.g., "nats://localhost:4222".
|
|
6
|
-
* Defaults to `process.env.NATS_URL` or `nats://localhost:4222`.
|
|
7
|
-
*/
|
|
8
4
|
servers?: string;
|
|
9
|
-
/**
|
|
10
|
-
* CloudEvent source identifier (e.g., "orderboss://orders-service").
|
|
11
|
-
*/
|
|
12
5
|
source?: string;
|
|
13
|
-
/**
|
|
14
|
-
* Optional CloudEvent subject (e.g., an order ID). Not to be confused with the NATS subject.
|
|
15
|
-
*/
|
|
16
6
|
subject?: string;
|
|
17
|
-
/**
|
|
18
|
-
* Tenant identifier for multi-tenant event routing.
|
|
19
|
-
* Will be added as a CloudEvent extension attribute.
|
|
20
|
-
*/
|
|
21
7
|
tenantId?: string;
|
|
22
8
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* // Returns: 'orders.created'
|
|
35
|
-
*
|
|
36
|
-
* deriveSubjectFromType('unknown.event.type', config)
|
|
37
|
-
* // Returns: 'events.unknown.event.type'
|
|
38
|
-
* ```
|
|
39
|
-
*/
|
|
40
|
-
export declare function deriveSubjectFromType(eventType: string, config?: RoutingConfig): string;
|
|
41
|
-
/**
|
|
42
|
-
* Derives a JetStream stream name from a CloudEvent type using routing configuration.
|
|
43
|
-
*/
|
|
44
|
-
export declare function deriveStreamFromType(eventType: string, config?: RoutingConfig): string | undefined;
|
|
45
|
-
export declare function publishNatsEvent<T extends ZodTypeAny>(subjectName: string, schema: T, eventData: unknown, options?: PublishNatsEventOptions): Promise<string>;
|
|
46
|
-
export declare function publishNatsRawEvent(subjectName: string, eventType: string, eventData: unknown, options?: PublishNatsEventOptions): Promise<string>;
|
|
47
|
-
/**
|
|
48
|
-
* Simplified publish function where subject equals event type.
|
|
49
|
-
*
|
|
50
|
-
* @example
|
|
51
|
-
* ```typescript
|
|
52
|
-
* await publish('orders.created', { orderId: '123', total: 99.99 })
|
|
53
|
-
* ```
|
|
54
|
-
*/
|
|
55
|
-
export declare function publish(eventType: string, eventData: unknown, options?: PublishNatsEventOptions): Promise<string>;
|
|
56
|
-
/**
|
|
57
|
-
* @internal Resets the cached NATS connection. Intended for testing only.
|
|
58
|
-
*/
|
|
59
|
-
export declare function __resetNatsPublisher(): void;
|
|
9
|
+
export declare const deriveSubjectFromType: (eventType: string, config?: RoutingConfig) => string;
|
|
10
|
+
export declare const deriveStreamFromType: (eventType: string, config?: RoutingConfig) => string | undefined;
|
|
11
|
+
export declare const publishNatsRawEvent: (subjectName: string, eventType: string, eventData: unknown, options?: PublishNatsEventOptions) => Promise<string>;
|
|
12
|
+
export declare const publishNatsEvent: <T extends ZodTypeAny>(subjectName: string, schema: T, eventData: unknown, options?: PublishNatsEventOptions) => Promise<string>;
|
|
13
|
+
export declare const publish: (eventTypeOrContract: string | {
|
|
14
|
+
type: string;
|
|
15
|
+
channel?: {
|
|
16
|
+
subject?: string;
|
|
17
|
+
};
|
|
18
|
+
}, eventData: unknown, options?: PublishNatsEventOptions) => Promise<string>;
|
|
19
|
+
export declare const __resetNatsPublisher: () => void;
|
|
@@ -2,49 +2,59 @@ import { connect, StringCodec } from 'nats';
|
|
|
2
2
|
import { extractTypeFromSchema } from '../domain';
|
|
3
3
|
import { createValidationError, logger } from '../infrastructure';
|
|
4
4
|
const sc = StringCodec();
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
5
|
+
let natsConnectionPromise = null;
|
|
6
|
+
const pluralize = (word) => {
|
|
7
|
+
if (word.endsWith('y') && !['a', 'e', 'i', 'o', 'u'].includes(word[word.length - 2])) {
|
|
8
|
+
return `${word.slice(0, -1)}ies`;
|
|
9
|
+
}
|
|
10
|
+
if (word.endsWith('s') || word.endsWith('sh') || word.endsWith('ch') || word.endsWith('x')) {
|
|
11
|
+
return `${word}es`;
|
|
12
|
+
}
|
|
13
|
+
return `${word}s`;
|
|
14
|
+
};
|
|
15
|
+
const deriveSubjectFromEventType = (eventType) => {
|
|
16
|
+
const parts = eventType.split('.');
|
|
17
|
+
if (parts.length < 2)
|
|
18
|
+
return eventType;
|
|
19
|
+
const domain = parts[0];
|
|
20
|
+
const action = parts.slice(1).join('.');
|
|
21
|
+
const pluralDomain = pluralize(domain);
|
|
22
|
+
return `${pluralDomain}.${action}`;
|
|
23
|
+
};
|
|
24
|
+
const getNatsConnection = async (servers) => {
|
|
25
|
+
if (!natsConnectionPromise) {
|
|
26
|
+
const url = servers ?? process.env.NATS_URL ?? 'nats://localhost:4222';
|
|
27
|
+
natsConnectionPromise = connect({ servers: url })
|
|
28
|
+
.then((connection) => {
|
|
29
|
+
logger.debug(`[NATS] connected to ${url}`);
|
|
30
|
+
return connection;
|
|
31
|
+
})
|
|
32
|
+
.catch((error) => {
|
|
33
|
+
logger.error('[NATS] connection error', error);
|
|
34
|
+
natsConnectionPromise = null;
|
|
35
|
+
throw error;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return natsConnectionPromise;
|
|
39
|
+
};
|
|
40
|
+
export const deriveSubjectFromType = (eventType, config) => {
|
|
23
41
|
if (!config?.typeToSubjectMap) {
|
|
24
|
-
// No mapping configured, use type as-is (replace dots with dots is a no-op)
|
|
25
42
|
return config?.defaultSubjectPrefix ? `${config.defaultSubjectPrefix}.${eventType}` : eventType;
|
|
26
43
|
}
|
|
27
|
-
// Find the longest matching prefix
|
|
28
44
|
const sortedPrefixes = Object.keys(config.typeToSubjectMap).sort((a, b) => b.length - a.length);
|
|
29
45
|
for (const prefix of sortedPrefixes) {
|
|
30
46
|
if (eventType.startsWith(prefix)) {
|
|
31
47
|
const suffix = eventType.slice(prefix.length);
|
|
32
48
|
const mappedPrefix = config.typeToSubjectMap[prefix];
|
|
33
|
-
// Remove leading dot from suffix if present
|
|
34
49
|
const cleanSuffix = suffix.startsWith('.') ? suffix.slice(1) : suffix;
|
|
35
50
|
return cleanSuffix ? `${mappedPrefix}.${cleanSuffix}` : mappedPrefix;
|
|
36
51
|
}
|
|
37
52
|
}
|
|
38
|
-
// No match found, use default prefix or type as-is
|
|
39
53
|
return config.defaultSubjectPrefix ? `${config.defaultSubjectPrefix}.${eventType}` : eventType;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
* Derives a JetStream stream name from a CloudEvent type using routing configuration.
|
|
43
|
-
*/
|
|
44
|
-
export function deriveStreamFromType(eventType, config) {
|
|
54
|
+
};
|
|
55
|
+
export const deriveStreamFromType = (eventType, config) => {
|
|
45
56
|
if (!config?.typeToStreamMap)
|
|
46
57
|
return undefined;
|
|
47
|
-
// Find the longest matching prefix
|
|
48
58
|
const sortedPrefixes = Object.keys(config.typeToStreamMap).sort((a, b) => b.length - a.length);
|
|
49
59
|
for (const prefix of sortedPrefixes) {
|
|
50
60
|
if (eventType.startsWith(prefix)) {
|
|
@@ -52,25 +62,26 @@ export function deriveStreamFromType(eventType, config) {
|
|
|
52
62
|
}
|
|
53
63
|
}
|
|
54
64
|
return undefined;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
};
|
|
66
|
+
export const publishNatsRawEvent = async (subjectName, eventType, eventData, options) => {
|
|
67
|
+
const cloudEvent = {
|
|
68
|
+
specversion: '1.0',
|
|
69
|
+
type: eventType,
|
|
70
|
+
source: options?.source || 'hono-service',
|
|
71
|
+
id: crypto.randomUUID(),
|
|
72
|
+
time: new Date().toISOString(),
|
|
73
|
+
datacontenttype: 'application/json',
|
|
74
|
+
data: eventData,
|
|
75
|
+
...(options?.subject && { subject: options.subject }),
|
|
76
|
+
...(options?.tenantId && { tenantid: options.tenantId }),
|
|
77
|
+
};
|
|
78
|
+
const data = JSON.stringify(cloudEvent);
|
|
79
|
+
const nc = await getNatsConnection(options?.servers);
|
|
80
|
+
nc.publish(subjectName, sc.encode(data));
|
|
81
|
+
logger.debug(`Published CloudEvent ${eventType} to NATS subject ${subjectName} (id=${cloudEvent.id})`);
|
|
82
|
+
return cloudEvent.id;
|
|
83
|
+
};
|
|
84
|
+
export const publishNatsEvent = async (subjectName, schema, eventData, options) => {
|
|
74
85
|
const eventType = extractTypeFromSchema(schema);
|
|
75
86
|
if (!eventType) {
|
|
76
87
|
throw new Error('Could not extract event type from schema. Make sure your schema has proper metadata.');
|
|
@@ -91,39 +102,14 @@ export async function publishNatsEvent(subjectName, schema, eventData, options)
|
|
|
91
102
|
throw createValidationError(eventType, [handlerValidationError]);
|
|
92
103
|
}
|
|
93
104
|
return publishNatsRawEvent(subjectName, eventType, validationResult.data, options);
|
|
94
|
-
}
|
|
95
|
-
export async
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
data: eventData,
|
|
104
|
-
...(options?.subject && { subject: options.subject }),
|
|
105
|
-
...(options?.tenantId && { tenantid: options.tenantId }), // CloudEvents extension (lowercase)
|
|
106
|
-
};
|
|
107
|
-
const data = JSON.stringify(cloudEvent);
|
|
108
|
-
const nc = await getNatsConnection(options?.servers);
|
|
109
|
-
nc.publish(subjectName, sc.encode(data));
|
|
110
|
-
logger.debug(`Published CloudEvent ${eventType} to NATS subject ${subjectName} (id=${cloudEvent.id})`);
|
|
111
|
-
return cloudEvent.id;
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Simplified publish function where subject equals event type.
|
|
115
|
-
*
|
|
116
|
-
* @example
|
|
117
|
-
* ```typescript
|
|
118
|
-
* await publish('orders.created', { orderId: '123', total: 99.99 })
|
|
119
|
-
* ```
|
|
120
|
-
*/
|
|
121
|
-
export async function publish(eventType, eventData, options) {
|
|
122
|
-
return publishNatsRawEvent(eventType, eventType, eventData, options);
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* @internal Resets the cached NATS connection. Intended for testing only.
|
|
126
|
-
*/
|
|
127
|
-
export function __resetNatsPublisher() {
|
|
105
|
+
};
|
|
106
|
+
export const publish = async (eventTypeOrContract, eventData, options) => {
|
|
107
|
+
const eventType = typeof eventTypeOrContract === 'string' ? eventTypeOrContract : eventTypeOrContract.type;
|
|
108
|
+
const natsSubject = typeof eventTypeOrContract === 'string'
|
|
109
|
+
? deriveSubjectFromEventType(eventTypeOrContract)
|
|
110
|
+
: (eventTypeOrContract.channel?.subject ?? eventTypeOrContract.type);
|
|
111
|
+
return publishNatsRawEvent(natsSubject, eventType, eventData, options);
|
|
112
|
+
};
|
|
113
|
+
export const __resetNatsPublisher = () => {
|
|
128
114
|
natsConnectionPromise = null;
|
|
129
|
-
}
|
|
115
|
+
};
|