@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.
- package/README.md +413 -0
- 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.
|
|
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.
|
|
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",
|