@event-driven-io/emmett-sqlite 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 +493 -0
- package/dist/index.cjs +288 -234
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +222 -168
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
# @event-driven-io/emmett-sqlite
|
|
2
|
+
|
|
3
|
+
SQLite event store adapter for the Emmett event sourcing library, providing persistent event storage with stream management, projections, and subscription-based event processing.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This package provides a SQLite-based implementation of the Emmett event store interface. It enables event sourcing applications to persist events to a SQLite database with support for:
|
|
8
|
+
|
|
9
|
+
- File-based and in-memory database storage
|
|
10
|
+
- WAL (Write-Ahead Logging) mode for improved concurrency
|
|
11
|
+
- Automatic schema management
|
|
12
|
+
- Inline and async projections for read models
|
|
13
|
+
- Polling-based consumers for background event processing
|
|
14
|
+
- Optimistic concurrency control
|
|
15
|
+
|
|
16
|
+
SQLite is an excellent choice for development, testing, embedded applications, and scenarios where a lightweight, serverless database is preferred.
|
|
17
|
+
|
|
18
|
+
## Key Concepts
|
|
19
|
+
|
|
20
|
+
### Event Store
|
|
21
|
+
|
|
22
|
+
The `SQLiteEventStore` interface extends Emmett's base `EventStore` with SQLite-specific functionality:
|
|
23
|
+
|
|
24
|
+
- **Append events** to streams with optimistic concurrency control
|
|
25
|
+
- **Read streams** to retrieve events for a specific aggregate
|
|
26
|
+
- **Aggregate streams** to rebuild state using an evolve function
|
|
27
|
+
- **Consumer** for background event processing
|
|
28
|
+
|
|
29
|
+
### Database Schema
|
|
30
|
+
|
|
31
|
+
The event store uses three tables (prefixed with `emt_`):
|
|
32
|
+
|
|
33
|
+
| Table | Purpose |
|
|
34
|
+
| ------------------- | ------------------------------------------- |
|
|
35
|
+
| `emt_streams` | Stream metadata (stream ID, position, type) |
|
|
36
|
+
| `emt_messages` | Events with global position ordering |
|
|
37
|
+
| `emt_subscriptions` | Processor checkpoint positions |
|
|
38
|
+
|
|
39
|
+
### Consumers and Processors
|
|
40
|
+
|
|
41
|
+
- **Consumer**: Coordinates multiple processors, manages polling lifecycle
|
|
42
|
+
- **Processor**: Handles event batches, maintains checkpoint position
|
|
43
|
+
- **Projection Processor**: Specialized processor for updating read models
|
|
44
|
+
|
|
45
|
+
### Projections
|
|
46
|
+
|
|
47
|
+
Two projection types are supported:
|
|
48
|
+
|
|
49
|
+
- **Inline projections**: Execute within the append transaction for immediate consistency
|
|
50
|
+
- **Async projections**: Run via consumers for eventual consistency
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install @event-driven-io/emmett-sqlite
|
|
56
|
+
# or
|
|
57
|
+
pnpm add @event-driven-io/emmett-sqlite
|
|
58
|
+
# or
|
|
59
|
+
yarn add @event-driven-io/emmett-sqlite
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Peer Dependencies
|
|
63
|
+
|
|
64
|
+
This package requires the following peer dependencies:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm install @event-driven-io/emmett sqlite3
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Quick Start
|
|
71
|
+
|
|
72
|
+
### Basic Event Store Setup
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { getSQLiteEventStore } from '@event-driven-io/emmett-sqlite';
|
|
76
|
+
|
|
77
|
+
// File-based database
|
|
78
|
+
const eventStore = getSQLiteEventStore({
|
|
79
|
+
fileName: './events.db',
|
|
80
|
+
schema: {
|
|
81
|
+
autoMigration: 'CreateOrUpdate', // Automatically create tables
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// In-memory database (useful for testing)
|
|
86
|
+
import { InMemorySQLiteDatabase } from '@event-driven-io/emmett-sqlite';
|
|
87
|
+
|
|
88
|
+
const inMemoryEventStore = getSQLiteEventStore({
|
|
89
|
+
fileName: InMemorySQLiteDatabase, // ':memory:'
|
|
90
|
+
schema: {
|
|
91
|
+
autoMigration: 'CreateOrUpdate',
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Appending Events
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import type { Event } from '@event-driven-io/emmett';
|
|
100
|
+
|
|
101
|
+
// Define your event types
|
|
102
|
+
type ProductItemAdded = Event<
|
|
103
|
+
'ProductItemAdded',
|
|
104
|
+
{ productItem: { productId: string; quantity: number; price: number } }
|
|
105
|
+
>;
|
|
106
|
+
|
|
107
|
+
type DiscountApplied = Event<
|
|
108
|
+
'DiscountApplied',
|
|
109
|
+
{ percent: number; couponId: string }
|
|
110
|
+
>;
|
|
111
|
+
|
|
112
|
+
type ShoppingCartEvent = ProductItemAdded | DiscountApplied;
|
|
113
|
+
|
|
114
|
+
// Append events to a stream
|
|
115
|
+
const streamId = 'shopping_cart-123';
|
|
116
|
+
|
|
117
|
+
const result = await eventStore.appendToStream<ShoppingCartEvent>(streamId, [
|
|
118
|
+
{
|
|
119
|
+
type: 'ProductItemAdded',
|
|
120
|
+
data: {
|
|
121
|
+
productItem: { productId: 'shoes', quantity: 2, price: 100 },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
// Append with optimistic concurrency
|
|
127
|
+
await eventStore.appendToStream<ShoppingCartEvent>(
|
|
128
|
+
streamId,
|
|
129
|
+
[
|
|
130
|
+
{
|
|
131
|
+
type: 'DiscountApplied',
|
|
132
|
+
data: { percent: 10, couponId: 'SAVE10' },
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
{ expectedStreamVersion: result.nextExpectedStreamVersion },
|
|
136
|
+
);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Reading Events
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// Read all events from a stream
|
|
143
|
+
const { events, currentStreamVersion } = await eventStore.readStream(streamId);
|
|
144
|
+
|
|
145
|
+
for (const event of events) {
|
|
146
|
+
console.log(`Event: ${event.type}`, event.data);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Aggregating State
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
type ShoppingCart = {
|
|
154
|
+
productItems: Array<{ productId: string; quantity: number; price: number }>;
|
|
155
|
+
totalAmount: number;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const evolve = (
|
|
159
|
+
state: ShoppingCart,
|
|
160
|
+
event: ShoppingCartEvent,
|
|
161
|
+
): ShoppingCart => {
|
|
162
|
+
switch (event.type) {
|
|
163
|
+
case 'ProductItemAdded': {
|
|
164
|
+
const item = event.data.productItem;
|
|
165
|
+
return {
|
|
166
|
+
productItems: [...state.productItems, item],
|
|
167
|
+
totalAmount: state.totalAmount + item.price * item.quantity,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
case 'DiscountApplied':
|
|
171
|
+
return {
|
|
172
|
+
...state,
|
|
173
|
+
totalAmount: state.totalAmount * (1 - event.data.percent / 100),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const initialState = (): ShoppingCart => ({
|
|
179
|
+
productItems: [],
|
|
180
|
+
totalAmount: 0,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const { state, currentStreamVersion } = await eventStore.aggregateStream(
|
|
184
|
+
streamId,
|
|
185
|
+
{ evolve, initialState },
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
console.log('Cart total:', state.totalAmount);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## How-to Guides
|
|
192
|
+
|
|
193
|
+
### Using Inline Projections
|
|
194
|
+
|
|
195
|
+
Inline projections execute within the same transaction as the event append, ensuring immediate consistency.
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import {
|
|
199
|
+
getSQLiteEventStore,
|
|
200
|
+
sqliteRawSQLProjection,
|
|
201
|
+
} from '@event-driven-io/emmett-sqlite';
|
|
202
|
+
|
|
203
|
+
const shoppingCartSummaryProjection = sqliteRawSQLProjection<ShoppingCartEvent>(
|
|
204
|
+
(event, context) => {
|
|
205
|
+
switch (event.type) {
|
|
206
|
+
case 'ProductItemAdded': {
|
|
207
|
+
const { quantity, price } = event.data.productItem;
|
|
208
|
+
return `
|
|
209
|
+
INSERT INTO shopping_cart_summary (id, item_count, total)
|
|
210
|
+
VALUES ('${event.metadata.streamName}', ${quantity}, ${quantity * price})
|
|
211
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
212
|
+
item_count = item_count + ${quantity},
|
|
213
|
+
total = total + ${quantity * price};
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
216
|
+
case 'DiscountApplied':
|
|
217
|
+
return `
|
|
218
|
+
UPDATE shopping_cart_summary
|
|
219
|
+
SET total = total * (100 - ${event.data.percent}) / 100
|
|
220
|
+
WHERE id = '${event.metadata.streamName}';
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
'ProductItemAdded',
|
|
225
|
+
'DiscountApplied',
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const eventStore = getSQLiteEventStore({
|
|
229
|
+
fileName: './events.db',
|
|
230
|
+
projections: [{ type: 'inline', projection: shoppingCartSummaryProjection }],
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Using Background Consumers
|
|
235
|
+
|
|
236
|
+
Consumers poll for new events and process them with registered processors.
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import {
|
|
240
|
+
getSQLiteEventStore,
|
|
241
|
+
sqliteProcessor,
|
|
242
|
+
} from '@event-driven-io/emmett-sqlite';
|
|
243
|
+
|
|
244
|
+
const eventStore = getSQLiteEventStore({
|
|
245
|
+
fileName: './events.db',
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Create a consumer
|
|
249
|
+
const consumer = eventStore.consumer<ShoppingCartEvent>();
|
|
250
|
+
|
|
251
|
+
// Register a processor
|
|
252
|
+
consumer.processor({
|
|
253
|
+
processorId: 'notification-sender',
|
|
254
|
+
eachMessage: async (event, context) => {
|
|
255
|
+
if (event.type === 'ProductItemAdded') {
|
|
256
|
+
console.log('New item added:', event.data.productItem);
|
|
257
|
+
// Send notification, update cache, etc.
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Start consuming
|
|
263
|
+
await consumer.start();
|
|
264
|
+
|
|
265
|
+
// Stop when done
|
|
266
|
+
await consumer.stop();
|
|
267
|
+
await consumer.close();
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Using Projection Processors
|
|
271
|
+
|
|
272
|
+
Projection processors provide a convenient way to run projections asynchronously.
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { sqliteProjectionProcessor } from '@event-driven-io/emmett-sqlite';
|
|
276
|
+
|
|
277
|
+
const consumer = eventStore.consumer<ShoppingCartEvent>();
|
|
278
|
+
|
|
279
|
+
consumer.processor({
|
|
280
|
+
projection: shoppingCartSummaryProjection,
|
|
281
|
+
processorId: 'cart-summary-projection',
|
|
282
|
+
startFrom: 'CURRENT', // Resume from last checkpoint
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
await consumer.start();
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Configuring Polling Behavior
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
const consumer = eventStore.consumer<ShoppingCartEvent>({
|
|
292
|
+
pulling: {
|
|
293
|
+
batchSize: 100, // Events per batch (default: 100)
|
|
294
|
+
pullingFrequencyInMs: 50, // Polling interval (default: 50ms)
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Manual Schema Management
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
import {
|
|
303
|
+
getSQLiteEventStore,
|
|
304
|
+
sqliteConnection,
|
|
305
|
+
createEventStoreSchema,
|
|
306
|
+
} from '@event-driven-io/emmett-sqlite';
|
|
307
|
+
|
|
308
|
+
// Create connection
|
|
309
|
+
const connection = sqliteConnection({ fileName: './events.db' });
|
|
310
|
+
|
|
311
|
+
// Manually create schema
|
|
312
|
+
await createEventStoreSchema(connection);
|
|
313
|
+
|
|
314
|
+
// Create event store without auto-migration
|
|
315
|
+
const eventStore = getSQLiteEventStore({
|
|
316
|
+
fileName: './events.db',
|
|
317
|
+
schema: {
|
|
318
|
+
autoMigration: 'None',
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Using Shared In-Memory Database
|
|
324
|
+
|
|
325
|
+
For testing scenarios where multiple connections need to share the same in-memory database:
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
import { InMemorySharedCacheSQLiteDatabase } from '@event-driven-io/emmett-sqlite';
|
|
329
|
+
|
|
330
|
+
const eventStore = getSQLiteEventStore({
|
|
331
|
+
fileName: InMemorySharedCacheSQLiteDatabase, // 'file::memory:?cache=shared'
|
|
332
|
+
});
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Using Before-Commit Hooks
|
|
336
|
+
|
|
337
|
+
Execute custom logic before events are committed:
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
const eventStore = getSQLiteEventStore({
|
|
341
|
+
fileName: './events.db',
|
|
342
|
+
hooks: {
|
|
343
|
+
onBeforeCommit: async (events, context) => {
|
|
344
|
+
// Custom validation, logging, or side effects
|
|
345
|
+
for (const event of events) {
|
|
346
|
+
console.log('About to commit:', event.type);
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## API Reference
|
|
354
|
+
|
|
355
|
+
### getSQLiteEventStore
|
|
356
|
+
|
|
357
|
+
Creates a new SQLite event store instance.
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
function getSQLiteEventStore(
|
|
361
|
+
options: SQLiteEventStoreOptions,
|
|
362
|
+
): SQLiteEventStore;
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**Options:**
|
|
366
|
+
|
|
367
|
+
| Property | Type | Description |
|
|
368
|
+
| ---------------------- | ------------------------------------------------------ | -------------------------------------------------- |
|
|
369
|
+
| `fileName` | `string \| ':memory:' \| 'file::memory:?cache=shared'` | Database file path or in-memory identifier |
|
|
370
|
+
| `schema.autoMigration` | `'None' \| 'CreateOrUpdate'` | Schema creation mode (default: `'CreateOrUpdate'`) |
|
|
371
|
+
| `projections` | `ProjectionRegistration[]` | Inline projections to register |
|
|
372
|
+
| `hooks.onBeforeCommit` | `BeforeEventStoreCommitHandler` | Hook called before event commit |
|
|
373
|
+
|
|
374
|
+
### SQLiteEventStore
|
|
375
|
+
|
|
376
|
+
| Method | Description |
|
|
377
|
+
| ---------------------------------------------- | ------------------------- |
|
|
378
|
+
| `appendToStream(streamName, events, options?)` | Append events to a stream |
|
|
379
|
+
| `readStream(streamName, options?)` | Read events from a stream |
|
|
380
|
+
| `aggregateStream(streamName, options)` | Rebuild state from events |
|
|
381
|
+
| `consumer(options?)` | Create an event consumer |
|
|
382
|
+
|
|
383
|
+
### sqliteConnection
|
|
384
|
+
|
|
385
|
+
Creates a SQLite database connection with transaction support.
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
function sqliteConnection(options: { fileName: string }): SQLiteConnection;
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**SQLiteConnection interface:**
|
|
392
|
+
|
|
393
|
+
| Method | Description |
|
|
394
|
+
| ------------------------------ | --------------------------------------- |
|
|
395
|
+
| `command(sql, params?)` | Execute a write command |
|
|
396
|
+
| `query<T>(sql, params?)` | Execute a query returning multiple rows |
|
|
397
|
+
| `querySingle<T>(sql, params?)` | Execute a query returning a single row |
|
|
398
|
+
| `withTransaction<T>(fn)` | Execute function within a transaction |
|
|
399
|
+
| `close()` | Close the connection |
|
|
400
|
+
|
|
401
|
+
### Projection Helpers
|
|
402
|
+
|
|
403
|
+
| Function | Description |
|
|
404
|
+
| ----------------------------------------------------- | --------------------------------------------------- |
|
|
405
|
+
| `sqliteProjection(definition)` | Create a projection definition |
|
|
406
|
+
| `sqliteRawSQLProjection(handler, ...eventTypes)` | Create projection returning raw SQL per event |
|
|
407
|
+
| `sqliteRawBatchSQLProjection(handler, ...eventTypes)` | Create projection returning raw SQL array for batch |
|
|
408
|
+
|
|
409
|
+
### Consumer Types
|
|
410
|
+
|
|
411
|
+
| Type | Description |
|
|
412
|
+
| ---------------------------- | ---------------------------------------------- |
|
|
413
|
+
| `SQLiteEventStoreConsumer` | Consumer interface with start/stop lifecycle |
|
|
414
|
+
| `SQLiteProcessor` | Processor interface for handling event batches |
|
|
415
|
+
| `SQLiteProjectionDefinition` | Projection definition type |
|
|
416
|
+
|
|
417
|
+
## Architecture
|
|
418
|
+
|
|
419
|
+
```
|
|
420
|
+
+------------------+
|
|
421
|
+
| Application |
|
|
422
|
+
+--------+---------+
|
|
423
|
+
|
|
|
424
|
+
+--------+--------+--------+
|
|
425
|
+
| |
|
|
426
|
+
Commands/Queries Consumers
|
|
427
|
+
| |
|
|
428
|
+
+------------v------------+ +--------v--------+
|
|
429
|
+
| SQLiteEventStore | | SQLiteConsumer |
|
|
430
|
+
| - appendToStream() | | - processor() |
|
|
431
|
+
| - readStream() | | - start() |
|
|
432
|
+
| - aggregateStream() | | - stop() |
|
|
433
|
+
+------------+------------+ +--------+--------+
|
|
434
|
+
| |
|
|
435
|
+
+------------+-------------+
|
|
436
|
+
|
|
|
437
|
+
+---------v---------+
|
|
438
|
+
| SQLiteConnection |
|
|
439
|
+
| - command() |
|
|
440
|
+
| - query() |
|
|
441
|
+
| - withTransaction|
|
|
442
|
+
+---------+---------+
|
|
443
|
+
|
|
|
444
|
+
+---------v---------+
|
|
445
|
+
| SQLite |
|
|
446
|
+
| (WAL mode) |
|
|
447
|
+
| |
|
|
448
|
+
| +---------------+ |
|
|
449
|
+
| | emt_streams | |
|
|
450
|
+
| +---------------+ |
|
|
451
|
+
| | emt_messages | |
|
|
452
|
+
| +---------------+ |
|
|
453
|
+
| | emt_subscript | |
|
|
454
|
+
| +---------------+ |
|
|
455
|
+
+-------------------+
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Event Flow
|
|
459
|
+
|
|
460
|
+
1. **Write Path**: Events are appended via `appendToStream()`, stored in `emt_messages` with auto-incremented global position
|
|
461
|
+
2. **Read Path**: Events are read via `readStream()` or `aggregateStream()` for state reconstruction
|
|
462
|
+
3. **Projection Path (Inline)**: Projections execute within the append transaction
|
|
463
|
+
4. **Projection Path (Async)**: Consumers poll `emt_messages`, processors handle batches, checkpoints stored in `emt_subscriptions`
|
|
464
|
+
|
|
465
|
+
### Concurrency Model
|
|
466
|
+
|
|
467
|
+
- SQLite WAL mode enables concurrent reads during writes
|
|
468
|
+
- Optimistic concurrency via expected stream version checks
|
|
469
|
+
- Consumers use polling (not push) for event delivery
|
|
470
|
+
- Processor checkpoints enable resume-from-position
|
|
471
|
+
|
|
472
|
+
## Dependencies
|
|
473
|
+
|
|
474
|
+
### Peer Dependencies
|
|
475
|
+
|
|
476
|
+
| Package | Version | Purpose |
|
|
477
|
+
| ------------------------- | -------- | --------------------------------------- |
|
|
478
|
+
| `@event-driven-io/emmett` | `0.38.3` | Core event sourcing types and utilities |
|
|
479
|
+
| `sqlite3` | `^5.1.7` | SQLite database driver |
|
|
480
|
+
|
|
481
|
+
### Internal Dependencies
|
|
482
|
+
|
|
483
|
+
| Package | Purpose |
|
|
484
|
+
| ------------------------ | --------------------- |
|
|
485
|
+
| `@event-driven-io/dumbo` | SQL utilities |
|
|
486
|
+
| `uuid` | Message ID generation |
|
|
487
|
+
|
|
488
|
+
## Related Packages
|
|
489
|
+
|
|
490
|
+
- [@event-driven-io/emmett](https://www.npmjs.com/package/@event-driven-io/emmett) - Core library
|
|
491
|
+
- [@event-driven-io/emmett-postgresql](https://www.npmjs.com/package/@event-driven-io/emmett-postgresql) - PostgreSQL adapter
|
|
492
|
+
- [@event-driven-io/emmett-mongodb](https://www.npmjs.com/package/@event-driven-io/emmett-mongodb) - MongoDB adapter
|
|
493
|
+
- [@event-driven-io/emmett-esdb](https://www.npmjs.com/package/@event-driven-io/emmett-esdb) - EventStoreDB adapter
|