@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
- * Derives a NATS subject from a CloudEvent type using routing configuration.
25
- *
26
- * @example
27
- * ```typescript
28
- * const config: RoutingConfig = {
29
- * typeToSubjectMap: { 'orderboss.orders': 'orders' },
30
- * defaultSubjectPrefix: 'events',
31
- * }
32
- *
33
- * deriveSubjectFromType('orderboss.orders.created', config)
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
- * Derives a NATS subject from a CloudEvent type using routing configuration.
7
- *
8
- * @example
9
- * ```typescript
10
- * const config: RoutingConfig = {
11
- * typeToSubjectMap: { 'orderboss.orders': 'orders' },
12
- * defaultSubjectPrefix: 'events',
13
- * }
14
- *
15
- * deriveSubjectFromType('orderboss.orders.created', config)
16
- * // Returns: 'orders.created'
17
- *
18
- * deriveSubjectFromType('unknown.event.type', config)
19
- * // Returns: 'events.unknown.event.type'
20
- * ```
21
- */
22
- export function deriveSubjectFromType(eventType, config) {
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
- let natsConnectionPromise = null;
57
- async function getNatsConnection(servers) {
58
- if (!natsConnectionPromise) {
59
- const url = servers ?? process.env.NATS_URL ?? 'nats://localhost:4222';
60
- natsConnectionPromise = connect({ servers: url })
61
- .then((connection) => {
62
- logger.debug(`[NATS] connected to ${url}`);
63
- return connection;
64
- })
65
- .catch((error) => {
66
- logger.error('[NATS] connection error', error);
67
- natsConnectionPromise = null;
68
- throw error;
69
- });
70
- }
71
- return natsConnectionPromise;
72
- }
73
- export async function publishNatsEvent(subjectName, schema, eventData, options) {
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 function publishNatsRawEvent(subjectName, eventType, eventData, options) {
96
- const cloudEvent = {
97
- specversion: '1.0',
98
- type: eventType,
99
- source: options?.source || 'hono-service',
100
- id: crypto.randomUUID(),
101
- time: new Date().toISOString(),
102
- datacontenttype: 'application/json',
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crossdelta/cloudevents",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "CloudEvents toolkit for TypeScript - Zod validation, handler discovery, NATS JetStream",
5
5
  "author": "crossdelta",
6
6
  "license": "MIT",