@nimbus-cqrs/core 2.0.0-beta.0 → 2.0.2

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 CHANGED
@@ -5,40 +5,185 @@
5
5
 
6
6
  # Nimbus Core
7
7
 
8
- The core package of the Nimbus framework.
8
+ The core package of the Nimbus framework — a small, [CloudEvents](https://cloudevents.io/)-based CQRS toolkit for TypeScript. It provides typed **Commands**, **Queries** and **Events**, a validating **Router** and an in-process **EventBus** with retries and OpenTelemetry instrumentation.
9
9
 
10
10
  Refer to the [Nimbus main repository](https://github.com/overlap-dev/Nimbus) or the [Nimbus documentation](https://nimbus.overlap.at) for more information about the Nimbus framework.
11
11
 
12
+ ## Install
13
+
14
+ ```bash
15
+ # Deno
16
+ deno add jsr:@nimbus-cqrs/core
17
+
18
+ # NPM
19
+ npm install @nimbus-cqrs/core
20
+
21
+ # Bun
22
+ bun add @nimbus-cqrs/core
23
+ ```
24
+
12
25
  # Examples
13
26
 
14
- These are some quick examples on how the basics of the Nimbus framework work.
27
+ The snippets below walk through a tiny "todo" domain so you can see how the core building blocks fit together. Each example is runnable on its own.
15
28
 
16
29
  For detailed documentation, please refer to the [Nimbus documentation](https://nimbus.overlap.at).
17
30
 
18
31
  ## Command
19
32
 
20
- ```typescript
33
+ A **Command** asks the system to *do* something (a write). You declare a [Zod](https://zod.dev/) schema that extends Nimbus' built-in `commandSchema`, write a handler, and register both on the router. Incoming messages are validated against the schema before they reach your handler.
21
34
 
35
+ ```typescript
36
+ import {
37
+ commandSchema,
38
+ createCommand,
39
+ getRouter,
40
+ } from '@nimbus-cqrs/core';
41
+ import { z } from 'zod';
42
+
43
+ const ADD_TODO = 'com.example.todo.add';
44
+
45
+ const addTodoSchema = commandSchema.extend({
46
+ type: z.literal(ADD_TODO),
47
+ data: z.object({
48
+ title: z.string().min(1),
49
+ }),
50
+ });
51
+ type AddTodoCommand = z.infer<typeof addTodoSchema>;
52
+
53
+ const handleAddTodo = async (cmd: AddTodoCommand) => {
54
+ // ...persist the todo here...
55
+ return { id: 'todo-1', title: cmd.data.title, status: 'open' };
56
+ };
57
+
58
+ const router = getRouter();
59
+ router.register(ADD_TODO, handleAddTodo, addTodoSchema);
60
+
61
+ const result = await router.route(
62
+ createCommand<AddTodoCommand>({
63
+ type: ADD_TODO,
64
+ source: 'https://app.example.com',
65
+ data: { title: 'Write the README' },
66
+ }),
67
+ );
68
+
69
+ console.log(result);
70
+ // { id: 'todo-1', title: 'Write the README', status: 'open' }
22
71
  ```
23
72
 
73
+ `createCommand` fills in the boilerplate CloudEvents fields (`id`, `correlationid`, `time`, …) for you, so you only specify the parts that matter to your domain.
74
+
24
75
  ## Query
25
76
 
26
- ```typescript
77
+ A **Query** asks the system to *read* something. Mechanically it is identical to a Command — same router, same validation, same shape — only the intent differs.
27
78
 
79
+ ```typescript
80
+ import { createQuery, getRouter, querySchema } from '@nimbus-cqrs/core';
81
+ import { z } from 'zod';
82
+
83
+ const GET_TODO = 'com.example.todo.get';
84
+
85
+ const getTodoSchema = querySchema.extend({
86
+ type: z.literal(GET_TODO),
87
+ data: z.object({ id: z.string() }),
88
+ });
89
+ type GetTodoQuery = z.infer<typeof getTodoSchema>;
90
+
91
+ const handleGetTodo = async (q: GetTodoQuery) => {
92
+ // ...load the todo from your read model...
93
+ return { id: q.data.id, title: 'Write the README', status: 'open' };
94
+ };
95
+
96
+ const router = getRouter();
97
+ router.register(GET_TODO, handleGetTodo, getTodoSchema);
98
+
99
+ const todo = await router.route(
100
+ createQuery<GetTodoQuery>({
101
+ type: GET_TODO,
102
+ source: 'https://app.example.com',
103
+ data: { id: 'todo-1' },
104
+ }),
105
+ );
28
106
  ```
29
107
 
30
108
  ## Event
31
109
 
32
- ```typescript
110
+ An **Event** announces that something *has happened*. Events are published to the in-process EventBus, which delivers them to every matching subscriber asynchronously, with exponential-backoff retries on handler errors and built-in OpenTelemetry traces and metrics.
33
111
 
112
+ ```typescript
113
+ import {
114
+ createEvent,
115
+ eventSchema,
116
+ getEventBus,
117
+ } from '@nimbus-cqrs/core';
118
+ import { z } from 'zod';
119
+
120
+ const TODO_ADDED = 'com.example.todo.added';
121
+
122
+ const todoAddedSchema = eventSchema.extend({
123
+ type: z.literal(TODO_ADDED),
124
+ data: z.object({
125
+ id: z.string(),
126
+ title: z.string(),
127
+ }),
128
+ });
129
+ type TodoAddedEvent = z.infer<typeof todoAddedSchema>;
130
+
131
+ const eventBus = getEventBus();
132
+
133
+ eventBus.subscribeEvent<TodoAddedEvent>({
134
+ type: TODO_ADDED,
135
+ handler: async (event) => {
136
+ console.log(`New todo added: ${event.data.title}`);
137
+ },
138
+ onError: (error, event) => {
139
+ console.error(`Failed to handle ${event.id} after retries`, error);
140
+ },
141
+ });
142
+
143
+ eventBus.putEvent(
144
+ createEvent<TodoAddedEvent>({
145
+ type: TODO_ADDED,
146
+ source: 'https://app.example.com',
147
+ subject: '/todos/todo-1',
148
+ data: { id: 'todo-1', title: 'Write the README' },
149
+ }),
150
+ );
34
151
  ```
35
152
 
153
+ A typical flow is to publish an event from inside a command handler once the write has succeeded — that's how reads, side effects and integrations stay decoupled from the command path.
154
+
36
155
  ## Router
37
156
 
38
- ```typescript
157
+ A typical app configures a single named router at startup with cross-cutting concerns (logging, correlation IDs, …) and then resolves it from anywhere via `getRouter()`.
39
158
 
159
+ ```typescript
160
+ import { getLogger, getRouter, setupRouter } from '@nimbus-cqrs/core';
161
+
162
+ setupRouter('default', {
163
+ logInput: (input) => {
164
+ getLogger().debug({
165
+ category: 'Router',
166
+ message: `Routing ${input.type}`,
167
+ correlationId: input.correlationid,
168
+ });
169
+ },
170
+ logOutput: (output) => {
171
+ getLogger().debug({
172
+ category: 'Router',
173
+ message: 'Handler completed',
174
+ data: { output },
175
+ });
176
+ },
177
+ });
178
+
179
+ // Anywhere else in your app:
180
+ const router = getRouter('default');
181
+ router.register(/* type, handler, schema */);
182
+ await router.route(/* command or query */);
40
183
  ```
41
184
 
185
+ You can have multiple named routers (for example one per transport) by calling `setupRouter` with different names and resolving them with `getRouter('<name>')`.
186
+
42
187
  # License
43
188
 
44
189
  Copyright 2024-present Overlap GmbH & Co KG (https://overlap.at)
@@ -67,7 +67,7 @@ export type SubscribeEventInput<TEvent extends Event> = {
67
67
  *
68
68
  * @example
69
69
  * ```ts
70
- * import { createEvent, NimbusEventBus } from '@nimbus/core';
70
+ * import { createEvent, NimbusEventBus } from '@nimbus-cqrs/core';
71
71
  *
72
72
  * const eventBus = new NimbusEventBus({
73
73
  * name: 'orders',
@@ -131,7 +131,7 @@ export declare class NimbusEventBus {
131
131
  *
132
132
  * @example
133
133
  * ```ts
134
- * import { getLogger, NimbusEventBus } from '@nimbus/core';
134
+ * import { getLogger, NimbusEventBus } from '@nimbus-cqrs/core';
135
135
  *
136
136
  * const eventBus = new NimbusEventBus({
137
137
  * name: 'orders',
@@ -162,7 +162,7 @@ export declare class NimbusEventBus {
162
162
  *
163
163
  * @example
164
164
  * ```ts
165
- * import { createEvent, getEventBus } from '@nimbus/core';
165
+ * import { createEvent, getEventBus } from '@nimbus-cqrs/core';
166
166
  *
167
167
  * const eventBus = getEventBus('default');
168
168
  *
@@ -206,7 +206,7 @@ export declare class NimbusEventBus {
206
206
  *
207
207
  * @example
208
208
  * ```ts
209
- * import { getEventBus, getLogger } from '@nimbus/core';
209
+ * import { getEventBus, getLogger } from '@nimbus-cqrs/core';
210
210
  *
211
211
  * const eventBus = getEventBus('default');
212
212
  *
@@ -268,7 +268,7 @@ export declare class NimbusEventBus {
268
268
  *
269
269
  * @example
270
270
  * ```ts
271
- * import { getLogger, setupEventBus } from '@nimbus/core';
271
+ * import { getLogger, setupEventBus } from '@nimbus-cqrs/core';
272
272
  *
273
273
  * // At application startup, configure the event bus with all options
274
274
  * setupEventBus('default', {
@@ -300,7 +300,7 @@ export declare const setupEventBus: (name: string, options?: Omit<NimbusEventBus
300
300
  *
301
301
  * @example
302
302
  * ```ts
303
- * import { createEvent, getEventBus } from '@nimbus/core';
303
+ * import { createEvent, getEventBus } from '@nimbus-cqrs/core';
304
304
  *
305
305
  * // Get the event bus configured earlier with setupEventBus
306
306
  * const eventBus = getEventBus('default');
@@ -32,7 +32,7 @@ const eventSizeBytes = meter.createHistogram('eventbus_event_size_bytes', {
32
32
  *
33
33
  * @example
34
34
  * ```ts
35
- * import { createEvent, NimbusEventBus } from '@nimbus/core';
35
+ * import { createEvent, NimbusEventBus } from '@nimbus-cqrs/core';
36
36
  *
37
37
  * const eventBus = new NimbusEventBus({
38
38
  * name: 'orders',
@@ -96,7 +96,7 @@ export class NimbusEventBus {
96
96
  *
97
97
  * @example
98
98
  * ```ts
99
- * import { getLogger, NimbusEventBus } from '@nimbus/core';
99
+ * import { getLogger, NimbusEventBus } from '@nimbus-cqrs/core';
100
100
  *
101
101
  * const eventBus = new NimbusEventBus({
102
102
  * name: 'orders',
@@ -135,7 +135,7 @@ export class NimbusEventBus {
135
135
  *
136
136
  * @example
137
137
  * ```ts
138
- * import { createEvent, getEventBus } from '@nimbus/core';
138
+ * import { createEvent, getEventBus } from '@nimbus-cqrs/core';
139
139
  *
140
140
  * const eventBus = getEventBus('default');
141
141
  *
@@ -223,7 +223,7 @@ export class NimbusEventBus {
223
223
  *
224
224
  * @example
225
225
  * ```ts
226
- * import { getEventBus, getLogger } from '@nimbus/core';
226
+ * import { getEventBus, getLogger } from '@nimbus-cqrs/core';
227
227
  *
228
228
  * const eventBus = getEventBus('default');
229
229
  *
@@ -404,7 +404,7 @@ const eventBusRegistry = new Map();
404
404
  *
405
405
  * @example
406
406
  * ```ts
407
- * import { getLogger, setupEventBus } from '@nimbus/core';
407
+ * import { getLogger, setupEventBus } from '@nimbus-cqrs/core';
408
408
  *
409
409
  * // At application startup, configure the event bus with all options
410
410
  * setupEventBus('default', {
@@ -438,7 +438,7 @@ export const setupEventBus = (name, options) => {
438
438
  *
439
439
  * @example
440
440
  * ```ts
441
- * import { createEvent, getEventBus } from '@nimbus/core';
441
+ * import { createEvent, getEventBus } from '@nimbus-cqrs/core';
442
442
  *
443
443
  * // Get the event bus configured earlier with setupEventBus
444
444
  * const eventBus = getEventBus('default');
@@ -85,7 +85,7 @@ export type LogRecord = {
85
85
  * parseLogLevel,
86
86
  * prettyLogFormatter,
87
87
  * setupLogger,
88
- * } from '@nimbus/core';
88
+ * } from '@nimbus-cqrs/core';
89
89
  *
90
90
  * // Configure the logger at application startup
91
91
  * setupLogger({
@@ -145,7 +145,7 @@ export declare class Logger {
145
145
  *
146
146
  * @example
147
147
  * ```ts
148
- * import { getLogger } from "@nimbus/core";
148
+ * import { getLogger } from "@nimbus-cqrs/core";
149
149
  *
150
150
  * getLogger().debug({
151
151
  * message: 'Hello World!',
@@ -171,7 +171,7 @@ export declare class Logger {
171
171
  *
172
172
  * @example
173
173
  * ```ts
174
- * import { getLogger } from "@nimbus/core";
174
+ * import { getLogger } from "@nimbus-cqrs/core";
175
175
  *
176
176
  * getLogger().info({
177
177
  * message: 'Hello World!',
@@ -197,7 +197,7 @@ export declare class Logger {
197
197
  *
198
198
  * @example
199
199
  * ```ts
200
- * import { getLogger } from "@nimbus/core";
200
+ * import { getLogger } from "@nimbus-cqrs/core";
201
201
  *
202
202
  * getLogger().warn({
203
203
  * message: 'Hello World!',
@@ -223,7 +223,7 @@ export declare class Logger {
223
223
  *
224
224
  * @example
225
225
  * ```ts
226
- * import { getLogger } from "@nimbus/core";
226
+ * import { getLogger } from "@nimbus-cqrs/core";
227
227
  *
228
228
  * getLogger().error({
229
229
  * message: 'Hello World!',
@@ -249,7 +249,7 @@ export declare class Logger {
249
249
  *
250
250
  * @example
251
251
  * ```ts
252
- * import { getLogger } from "@nimbus/core";
252
+ * import { getLogger } from "@nimbus-cqrs/core";
253
253
  *
254
254
  * getLogger().critical({
255
255
  * message: 'Hello World!',
@@ -310,7 +310,7 @@ export declare class Logger {
310
310
  * parseLogLevel,
311
311
  * prettyLogFormatter,
312
312
  * setupLogger,
313
- * } from "@nimbus/core";
313
+ * } from "@nimbus-cqrs/core";
314
314
  *
315
315
  * setupLogger({
316
316
  * logLevel: parseLogLevel(process.env.LOG_LEVEL),
@@ -333,7 +333,7 @@ export declare const setupLogger: (options: LogOptions) => void;
333
333
  *
334
334
  * @example
335
335
  * ```ts
336
- * import { getLogger } from '@nimbus/core';
336
+ * import { getLogger } from '@nimbus-cqrs/core';
337
337
  *
338
338
  * const logger = getLogger();
339
339
  *
@@ -18,7 +18,7 @@ import { defaultLogOptions } from './options.js';
18
18
  * parseLogLevel,
19
19
  * prettyLogFormatter,
20
20
  * setupLogger,
21
- * } from '@nimbus/core';
21
+ * } from '@nimbus-cqrs/core';
22
22
  *
23
23
  * // Configure the logger at application startup
24
24
  * setupLogger({
@@ -90,7 +90,7 @@ export class Logger {
90
90
  *
91
91
  * @example
92
92
  * ```ts
93
- * import { getLogger } from "@nimbus/core";
93
+ * import { getLogger } from "@nimbus-cqrs/core";
94
94
  *
95
95
  * getLogger().debug({
96
96
  * message: 'Hello World!',
@@ -120,7 +120,7 @@ export class Logger {
120
120
  *
121
121
  * @example
122
122
  * ```ts
123
- * import { getLogger } from "@nimbus/core";
123
+ * import { getLogger } from "@nimbus-cqrs/core";
124
124
  *
125
125
  * getLogger().info({
126
126
  * message: 'Hello World!',
@@ -150,7 +150,7 @@ export class Logger {
150
150
  *
151
151
  * @example
152
152
  * ```ts
153
- * import { getLogger } from "@nimbus/core";
153
+ * import { getLogger } from "@nimbus-cqrs/core";
154
154
  *
155
155
  * getLogger().warn({
156
156
  * message: 'Hello World!',
@@ -180,7 +180,7 @@ export class Logger {
180
180
  *
181
181
  * @example
182
182
  * ```ts
183
- * import { getLogger } from "@nimbus/core";
183
+ * import { getLogger } from "@nimbus-cqrs/core";
184
184
  *
185
185
  * getLogger().error({
186
186
  * message: 'Hello World!',
@@ -210,7 +210,7 @@ export class Logger {
210
210
  *
211
211
  * @example
212
212
  * ```ts
213
- * import { getLogger } from "@nimbus/core";
213
+ * import { getLogger } from "@nimbus-cqrs/core";
214
214
  *
215
215
  * getLogger().critical({
216
216
  * message: 'Hello World!',
@@ -324,7 +324,7 @@ export class Logger {
324
324
  * parseLogLevel,
325
325
  * prettyLogFormatter,
326
326
  * setupLogger,
327
- * } from "@nimbus/core";
327
+ * } from "@nimbus-cqrs/core";
328
328
  *
329
329
  * setupLogger({
330
330
  * logLevel: parseLogLevel(process.env.LOG_LEVEL),
@@ -349,7 +349,7 @@ export const setupLogger = (options) => {
349
349
  *
350
350
  * @example
351
351
  * ```ts
352
- * import { getLogger } from '@nimbus/core';
352
+ * import { getLogger } from '@nimbus-cqrs/core';
353
353
  *
354
354
  * const logger = getLogger();
355
355
  *
@@ -40,7 +40,7 @@ type ZodSchema<T = any> = z.ZodType<T>;
40
40
  *
41
41
  * @example
42
42
  * ```ts
43
- * import { createCommand, MessageRouter } from '@nimbus/core';
43
+ * import { createCommand, MessageRouter } from '@nimbus-cqrs/core';
44
44
  *
45
45
  * const messageRouter = new MessageRouter({
46
46
  * name: 'api',
@@ -92,7 +92,7 @@ export declare class MessageRouter {
92
92
  *
93
93
  * @example
94
94
  * ```ts
95
- * import { commandSchema, type Command, getRouter } from '@nimbus/core';
95
+ * import { commandSchema, type Command, getRouter } from '@nimbus-cqrs/core';
96
96
  * import { z } from 'zod';
97
97
  *
98
98
  * // Define the command type and schema
@@ -133,7 +133,7 @@ export declare class MessageRouter {
133
133
  *
134
134
  * @example
135
135
  * ```ts
136
- * import { createCommand, getRouter } from '@nimbus/core';
136
+ * import { createCommand, getRouter } from '@nimbus-cqrs/core';
137
137
  *
138
138
  * const router = getRouter('default');
139
139
  *
@@ -169,7 +169,7 @@ export declare class MessageRouter {
169
169
  *
170
170
  * @example
171
171
  * ```ts
172
- * import { getLogger, setupRouter } from '@nimbus/core';
172
+ * import { getLogger, setupRouter } from '@nimbus-cqrs/core';
173
173
  *
174
174
  * // At application startup, configure the router with all options
175
175
  * setupRouter('default', {
@@ -204,7 +204,7 @@ export declare const setupRouter: (name: string, options?: Omit<MessageRouterOpt
204
204
  *
205
205
  * @example
206
206
  * ```ts
207
- * import { createCommand, getRouter } from '@nimbus/core';
207
+ * import { createCommand, getRouter } from '@nimbus-cqrs/core';
208
208
  *
209
209
  * // Get the router configured earlier with setupRouter
210
210
  * const router = getRouter('default');
@@ -19,7 +19,7 @@ const routingDuration = meter.createHistogram('router_routing_duration_seconds',
19
19
  *
20
20
  * @example
21
21
  * ```ts
22
- * import { createCommand, MessageRouter } from '@nimbus/core';
22
+ * import { createCommand, MessageRouter } from '@nimbus-cqrs/core';
23
23
  *
24
24
  * const messageRouter = new MessageRouter({
25
25
  * name: 'api',
@@ -76,7 +76,7 @@ export class MessageRouter {
76
76
  *
77
77
  * @example
78
78
  * ```ts
79
- * import { commandSchema, type Command, getRouter } from '@nimbus/core';
79
+ * import { commandSchema, type Command, getRouter } from '@nimbus-cqrs/core';
80
80
  * import { z } from 'zod';
81
81
  *
82
82
  * // Define the command type and schema
@@ -126,7 +126,7 @@ export class MessageRouter {
126
126
  *
127
127
  * @example
128
128
  * ```ts
129
- * import { createCommand, getRouter } from '@nimbus/core';
129
+ * import { createCommand, getRouter } from '@nimbus-cqrs/core';
130
130
  *
131
131
  * const router = getRouter('default');
132
132
  *
@@ -233,7 +233,7 @@ const routerRegistry = new Map();
233
233
  *
234
234
  * @example
235
235
  * ```ts
236
- * import { getLogger, setupRouter } from '@nimbus/core';
236
+ * import { getLogger, setupRouter } from '@nimbus-cqrs/core';
237
237
  *
238
238
  * // At application startup, configure the router with all options
239
239
  * setupRouter('default', {
@@ -270,7 +270,7 @@ export const setupRouter = (name, options) => {
270
270
  *
271
271
  * @example
272
272
  * ```ts
273
- * import { createCommand, getRouter } from '@nimbus/core';
273
+ * import { createCommand, getRouter } from '@nimbus-cqrs/core';
274
274
  *
275
275
  * // Get the router configured earlier with setupRouter
276
276
  * const router = getRouter('default');
@@ -31,7 +31,7 @@ export type WithSpanOptions = {
31
31
  *
32
32
  * @example
33
33
  * ```ts
34
- * import { withSpan } from '@nimbus/core';
34
+ * import { withSpan } from '@nimbus-cqrs/core';
35
35
  *
36
36
  * const fetchUser = withSpan(
37
37
  * {
@@ -10,7 +10,7 @@ import { context, SpanKind, SpanStatusCode, trace, } from '@opentelemetry/api';
10
10
  *
11
11
  * @example
12
12
  * ```ts
13
- * import { withSpan } from '@nimbus/core';
13
+ * import { withSpan } from '@nimbus-cqrs/core';
14
14
  *
15
15
  * const fetchUser = withSpan(
16
16
  * {
package/package.json CHANGED
@@ -1,7 +1,17 @@
1
1
  {
2
2
  "name": "@nimbus-cqrs/core",
3
- "version": "2.0.0-beta.0",
4
- "description": "Core building blocks of the Nimbus CQRS framework.",
3
+ "version": "2.0.2",
4
+ "description": "Simplify Event-Driven Applications - Core building blocks of the Nimbus framework.",
5
+ "keywords": [
6
+ "nimbus",
7
+ "cqrs",
8
+ "event-sourcing",
9
+ "event-driven",
10
+ "typescript",
11
+ "command",
12
+ "query",
13
+ "event"
14
+ ],
5
15
  "author": "Daniel Gördes <d.goerdes@overlap.at> (https://overlap.at)",
6
16
  "homepage": "https://nimbus.overlap.at",
7
17
  "repository": {