@event-driven-io/emmett-fastify 0.43.0-beta.11 → 0.43.0-beta.13

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 (2) hide show
  1. package/README.md +413 -0
  2. package/package.json +2 -2
package/README.md ADDED
@@ -0,0 +1,413 @@
1
+ # @event-driven-io/emmett-fastify
2
+
3
+ Fastify web framework integration for building event-sourced HTTP APIs with Emmett.
4
+
5
+ ## Purpose
6
+
7
+ This package provides seamless integration between the Emmett event sourcing library and Fastify, enabling you to build high-performance, event-sourced HTTP APIs. It includes sensible defaults for common plugins (ETag, compression, form body parsing) and automatic graceful shutdown handling.
8
+
9
+ ## Key Concepts
10
+
11
+ - **Application Factory**: Create pre-configured Fastify instances with essential plugins
12
+ - **Graceful Shutdown**: Automatic cleanup via `close-with-grace` when the server stops
13
+ - **Plugin System**: Extensible architecture using Fastify's plugin ecosystem
14
+ - **Decider Pattern**: Works seamlessly with Emmett's decider-based command handling
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @event-driven-io/emmett-fastify
20
+ ```
21
+
22
+ Since this package uses peer dependencies, you also need to install:
23
+
24
+ ```bash
25
+ npm install @event-driven-io/emmett fastify @fastify/compress @fastify/etag @fastify/formbody close-with-grace
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ### Basic Application Setup
31
+
32
+ ```typescript
33
+ import { getApplication, startAPI } from '@event-driven-io/emmett-fastify';
34
+ import { getInMemoryEventStore } from '@event-driven-io/emmett';
35
+ import type { FastifyInstance } from 'fastify';
36
+
37
+ // Create your event store
38
+ const eventStore = getInMemoryEventStore();
39
+
40
+ // Define your routes
41
+ const registerRoutes = (app: FastifyInstance) => {
42
+ app.get('/health', async () => ({ status: 'ok' }));
43
+
44
+ app.post('/carts/:cartId/items', async (request, reply) => {
45
+ // Handle command with event store
46
+ return reply.code(201).send();
47
+ });
48
+ };
49
+
50
+ // Create and start the application
51
+ const app = await getApplication({ registerRoutes });
52
+ await startAPI(app, { port: 3000 });
53
+ ```
54
+
55
+ ### Shopping Cart API Example
56
+
57
+ Here is a complete example demonstrating the decider pattern with Fastify routes:
58
+
59
+ ```typescript
60
+ import {
61
+ DeciderCommandHandler,
62
+ getInMemoryEventStore,
63
+ type EventStore,
64
+ type Decider,
65
+ } from '@event-driven-io/emmett';
66
+ import { getApplication, startAPI } from '@event-driven-io/emmett-fastify';
67
+ import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
68
+
69
+ // Define events
70
+ type ShoppingCartEvent =
71
+ | {
72
+ type: 'ShoppingCartOpened';
73
+ data: { cartId: string; clientId: string; openedAt: Date };
74
+ }
75
+ | {
76
+ type: 'ProductItemAdded';
77
+ data: { cartId: string; productId: string; quantity: number };
78
+ }
79
+ | {
80
+ type: 'ShoppingCartConfirmed';
81
+ data: { cartId: string; confirmedAt: Date };
82
+ };
83
+
84
+ // Define commands
85
+ type ShoppingCartCommand =
86
+ | {
87
+ type: 'OpenShoppingCart';
88
+ data: { cartId: string; clientId: string; now: Date };
89
+ }
90
+ | {
91
+ type: 'AddProductItem';
92
+ data: { cartId: string; productId: string; quantity: number };
93
+ }
94
+ | { type: 'ConfirmShoppingCart'; data: { cartId: string; now: Date } };
95
+
96
+ // Define state
97
+ type ShoppingCart =
98
+ | { status: 'Empty' }
99
+ | {
100
+ status: 'Pending';
101
+ id: string;
102
+ clientId: string;
103
+ items: Array<{ productId: string; quantity: number }>;
104
+ }
105
+ | { status: 'Confirmed'; id: string; confirmedAt: Date };
106
+
107
+ // Implement the decider
108
+ const decider: Decider<ShoppingCart, ShoppingCartCommand, ShoppingCartEvent> = {
109
+ decide: (command, state) => {
110
+ switch (command.type) {
111
+ case 'OpenShoppingCart':
112
+ return {
113
+ type: 'ShoppingCartOpened',
114
+ data: {
115
+ cartId: command.data.cartId,
116
+ clientId: command.data.clientId,
117
+ openedAt: command.data.now,
118
+ },
119
+ };
120
+ case 'AddProductItem':
121
+ return {
122
+ type: 'ProductItemAdded',
123
+ data: {
124
+ cartId: command.data.cartId,
125
+ productId: command.data.productId,
126
+ quantity: command.data.quantity,
127
+ },
128
+ };
129
+ case 'ConfirmShoppingCart':
130
+ return {
131
+ type: 'ShoppingCartConfirmed',
132
+ data: { cartId: command.data.cartId, confirmedAt: command.data.now },
133
+ };
134
+ }
135
+ },
136
+ evolve: (state, event) => {
137
+ switch (event.type) {
138
+ case 'ShoppingCartOpened':
139
+ return {
140
+ status: 'Pending',
141
+ id: event.data.cartId,
142
+ clientId: event.data.clientId,
143
+ items: [],
144
+ };
145
+ case 'ProductItemAdded':
146
+ if (state.status !== 'Pending') return state;
147
+ return {
148
+ ...state,
149
+ items: [
150
+ ...state.items,
151
+ { productId: event.data.productId, quantity: event.data.quantity },
152
+ ],
153
+ };
154
+ case 'ShoppingCartConfirmed':
155
+ if (state.status !== 'Pending') return state;
156
+ return {
157
+ status: 'Confirmed',
158
+ id: state.id,
159
+ confirmedAt: event.data.confirmedAt,
160
+ };
161
+ }
162
+ },
163
+ initialState: () => ({ status: 'Empty' }),
164
+ };
165
+
166
+ // Create command handler
167
+ const handle = DeciderCommandHandler(decider);
168
+
169
+ // Register routes
170
+ const registerRoutes = (eventStore: EventStore) => (app: FastifyInstance) => {
171
+ app.post(
172
+ '/clients/:clientId/shopping-carts',
173
+ async (request: FastifyRequest, reply: FastifyReply) => {
174
+ const { clientId } = request.params as { clientId: string };
175
+ const cartId = clientId;
176
+
177
+ await handle(eventStore, cartId, {
178
+ type: 'OpenShoppingCart',
179
+ data: { cartId, clientId, now: new Date() },
180
+ });
181
+
182
+ return reply.code(201).send({ id: cartId });
183
+ },
184
+ );
185
+
186
+ app.post(
187
+ '/clients/:clientId/shopping-carts/:cartId/items',
188
+ async (request: FastifyRequest, reply: FastifyReply) => {
189
+ const { cartId } = request.params as { cartId: string };
190
+ const { productId, quantity } = request.body as {
191
+ productId: string;
192
+ quantity: number;
193
+ };
194
+
195
+ await handle(eventStore, cartId, {
196
+ type: 'AddProductItem',
197
+ data: { cartId, productId, quantity },
198
+ });
199
+
200
+ return reply.code(204).send();
201
+ },
202
+ );
203
+
204
+ app.post(
205
+ '/clients/:clientId/shopping-carts/:cartId/confirm',
206
+ async (request: FastifyRequest, reply: FastifyReply) => {
207
+ const { cartId } = request.params as { cartId: string };
208
+
209
+ await handle(eventStore, cartId, {
210
+ type: 'ConfirmShoppingCart',
211
+ data: { cartId, now: new Date() },
212
+ });
213
+
214
+ return reply.code(204).send();
215
+ },
216
+ );
217
+ };
218
+
219
+ // Start the server
220
+ const eventStore = getInMemoryEventStore();
221
+ const app = await getApplication({
222
+ registerRoutes: registerRoutes(eventStore),
223
+ });
224
+ await startAPI(app, { port: 3000 });
225
+ ```
226
+
227
+ ## How-to Guides
228
+
229
+ ### Custom Server Options
230
+
231
+ Configure Fastify server options, including logging:
232
+
233
+ ```typescript
234
+ const app = await getApplication({
235
+ registerRoutes,
236
+ serverOptions: {
237
+ logger: true, // Enable Fastify logging
238
+ },
239
+ });
240
+ ```
241
+
242
+ ### Custom Plugins
243
+
244
+ Override or extend the default plugins:
245
+
246
+ ```typescript
247
+ import Cors from '@fastify/cors';
248
+
249
+ const app = await getApplication({
250
+ registerRoutes,
251
+ activeDefaultPlugins: [
252
+ { plugin: Cors, options: { origin: '*' } },
253
+ // Add your custom plugins here
254
+ ],
255
+ });
256
+ ```
257
+
258
+ ### Disable Default Plugins
259
+
260
+ Start with no default plugins:
261
+
262
+ ```typescript
263
+ const app = await getApplication({
264
+ registerRoutes,
265
+ activeDefaultPlugins: [],
266
+ });
267
+ ```
268
+
269
+ ### Testing with Fastify Inject
270
+
271
+ Use Fastify's built-in `inject` method for testing without starting a server:
272
+
273
+ ```typescript
274
+ import { getApplication } from '@event-driven-io/emmett-fastify';
275
+ import { getInMemoryEventStore } from '@event-driven-io/emmett';
276
+
277
+ const eventStore = getInMemoryEventStore();
278
+ const app = await getApplication({
279
+ registerRoutes: registerRoutes(eventStore),
280
+ });
281
+
282
+ // Test a route
283
+ const response = await app.inject({
284
+ method: 'POST',
285
+ url: '/clients/client-123/shopping-carts',
286
+ });
287
+
288
+ console.log(response.statusCode); // 201
289
+ console.log(response.json()); // { id: 'client-123' }
290
+ ```
291
+
292
+ ### Using with Different Event Stores
293
+
294
+ The package works with any Emmett event store implementation:
295
+
296
+ ```typescript
297
+ // With PostgreSQL
298
+ import { getPostgreSQLEventStore } from '@event-driven-io/emmett-postgresql';
299
+
300
+ const eventStore = getPostgreSQLEventStore(connectionPool);
301
+ const app = await getApplication({
302
+ registerRoutes: registerRoutes(eventStore),
303
+ });
304
+
305
+ // With EventStoreDB
306
+ import { getEventStoreDBEventStore } from '@event-driven-io/emmett-esdb';
307
+
308
+ const eventStore = getEventStoreDBEventStore(client);
309
+ const app = await getApplication({
310
+ registerRoutes: registerRoutes(eventStore),
311
+ });
312
+ ```
313
+
314
+ ## API Reference
315
+
316
+ ### `getApplication(options: ApplicationOptions): Promise<FastifyInstance>`
317
+
318
+ Creates a configured Fastify application instance.
319
+
320
+ **Parameters:**
321
+
322
+ | Option | Type | Default | Description |
323
+ | ---------------------- | -------------------------------- | ---------------------------- | -------------------------------- |
324
+ | `registerRoutes` | `(app: FastifyInstance) => void` | `undefined` | Function to register your routes |
325
+ | `serverOptions` | `{ logger: boolean }` | `{ logger: true }` | Fastify server configuration |
326
+ | `activeDefaultPlugins` | `Plugin[]` | `[ETag, Compress, FormBody]` | Plugins to register |
327
+
328
+ **Returns:** A Promise that resolves to a configured `FastifyInstance`.
329
+
330
+ ### `startAPI(app: FastifyInstance, options?: StartApiOptions): Promise<void>`
331
+
332
+ Starts the Fastify server.
333
+
334
+ **Parameters:**
335
+
336
+ | Option | Type | Default | Description |
337
+ | -------------- | ----------------- | -------- | -------------------------------- |
338
+ | `app` | `FastifyInstance` | required | The Fastify application instance |
339
+ | `options.port` | `number` | `5000` | Port number to listen on |
340
+
341
+ ### `ApplicationOptions`
342
+
343
+ ```typescript
344
+ interface ApplicationOptions {
345
+ serverOptions?: { logger: boolean };
346
+ registerRoutes?: (app: FastifyInstance) => void;
347
+ activeDefaultPlugins?: Plugin[];
348
+ }
349
+ ```
350
+
351
+ ### `StartApiOptions`
352
+
353
+ ```typescript
354
+ type StartApiOptions = {
355
+ port?: number;
356
+ };
357
+ ```
358
+
359
+ ### `Plugin`
360
+
361
+ ```typescript
362
+ type Plugin = {
363
+ plugin: FastifyPluginAsync | FastifyPluginCallback;
364
+ options: FastifyPluginOptions;
365
+ };
366
+ ```
367
+
368
+ ## Architecture
369
+
370
+ ### Default Plugins
371
+
372
+ The package registers these plugins by default:
373
+
374
+ | Plugin | Purpose |
375
+ | ------------------- | --------------------------------------------------- |
376
+ | `@fastify/etag` | Automatic ETag header generation for caching |
377
+ | `@fastify/compress` | Response compression (disabled globally by default) |
378
+ | `@fastify/formbody` | Form body parsing support |
379
+
380
+ ### Graceful Shutdown
381
+
382
+ The application automatically handles graceful shutdown using `close-with-grace`:
383
+
384
+ - Waits 500ms before forcing shutdown
385
+ - Logs any errors during shutdown
386
+ - Cleans up listeners when the application closes
387
+
388
+ ### Integration with Emmett Core
389
+
390
+ This package is designed to work with Emmett's core patterns:
391
+
392
+ ```
393
+ Fastify Route -> Command -> DeciderCommandHandler -> EventStore -> Events
394
+ ```
395
+
396
+ 1. HTTP requests arrive at Fastify routes
397
+ 2. Routes construct commands from request data
398
+ 3. `DeciderCommandHandler` processes commands using the decider pattern
399
+ 4. Events are appended to the event store
400
+ 5. Responses are sent back to the client
401
+
402
+ ## Dependencies
403
+
404
+ ### Peer Dependencies
405
+
406
+ | Package | Version | Purpose |
407
+ | ------------------------- | --------- | --------------------------- |
408
+ | `@event-driven-io/emmett` | `0.38.3` | Core event sourcing library |
409
+ | `fastify` | `^4.28.1` | Web framework |
410
+ | `@fastify/compress` | `^7.0.3` | Response compression |
411
+ | `@fastify/etag` | `^5.2.0` | ETag support |
412
+ | `@fastify/formbody` | `^7.4.0` | Form body parsing |
413
+ | `close-with-grace` | `^2.1.0` | Graceful shutdown |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event-driven-io/emmett-fastify",
3
- "version": "0.43.0-beta.11",
3
+ "version": "0.43.0-beta.13",
4
4
  "type": "module",
5
5
  "description": "Emmett - Event Sourcing development made simple",
6
6
  "scripts": {
@@ -53,7 +53,7 @@
53
53
  "dependencies": {},
54
54
  "devDependencies": {},
55
55
  "peerDependencies": {
56
- "@event-driven-io/emmett": "0.43.0-beta.11",
56
+ "@event-driven-io/emmett": "0.43.0-beta.13",
57
57
  "fastify": "^5.7.2",
58
58
  "@fastify/compress": "^8.3.1",
59
59
  "@fastify/etag": "^6.1.0",