@event-driven-io/emmett-postgresql 0.43.0-beta.12 → 0.43.0-beta.14
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 +437 -0
- package/dist/index.cjs +1722 -3243
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +520 -462
- package/dist/index.d.ts +520 -462
- package/dist/index.js +1630 -3242
- package/dist/index.js.map +1 -1
- package/package.json +12 -11
package/README.md
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# @event-driven-io/emmett-postgresql
|
|
2
|
+
|
|
3
|
+
PostgreSQL adapter for the Emmett event sourcing library, providing persistent event storage with partitioned tables, inline projections, and async message consumers.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This package implements the Emmett `EventStore` interface for PostgreSQL databases. It provides:
|
|
8
|
+
|
|
9
|
+
- Persistent event stream storage with optimistic concurrency control
|
|
10
|
+
- Partitioned tables for multi-tenant and module isolation
|
|
11
|
+
- Inline projections that execute within the append transaction
|
|
12
|
+
- Async message consumers with batch processing and checkpointing
|
|
13
|
+
- Pongo integration for document-style projections using PostgreSQL JSONB
|
|
14
|
+
- CLI plugin for database migrations
|
|
15
|
+
|
|
16
|
+
## Key Concepts
|
|
17
|
+
|
|
18
|
+
### Event Store
|
|
19
|
+
|
|
20
|
+
The PostgreSQL event store persists events in partitioned tables (`emt_streams`, `emt_messages`, `emt_subscriptions`). Events are stored with:
|
|
21
|
+
|
|
22
|
+
- **Stream ID**: Unique identifier for the event stream
|
|
23
|
+
- **Stream Position**: Sequential position within the stream (BigInt)
|
|
24
|
+
- **Global Position**: Sequential position across all streams for ordering
|
|
25
|
+
- **Message Data**: Event payload stored as JSONB
|
|
26
|
+
- **Message Metadata**: Additional metadata stored as JSONB
|
|
27
|
+
|
|
28
|
+
### Projections
|
|
29
|
+
|
|
30
|
+
Two types of projections are supported:
|
|
31
|
+
|
|
32
|
+
- **Inline Projections**: Execute within the same transaction as the event append, ensuring consistency
|
|
33
|
+
- **Async Projections**: Process events asynchronously via consumers with checkpointing
|
|
34
|
+
|
|
35
|
+
### Consumers and Processors
|
|
36
|
+
|
|
37
|
+
Message consumers poll the event store and delegate to processors:
|
|
38
|
+
|
|
39
|
+
- **Projector**: Updates read models based on events
|
|
40
|
+
- **Reactor**: Triggers side effects or workflows in response to events
|
|
41
|
+
|
|
42
|
+
### Multi-Tenancy
|
|
43
|
+
|
|
44
|
+
Partitioned tables support tenant and module isolation through PostgreSQL table partitioning.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install @event-driven-io/emmett-postgresql
|
|
50
|
+
# or
|
|
51
|
+
pnpm add @event-driven-io/emmett-postgresql
|
|
52
|
+
# or
|
|
53
|
+
yarn add @event-driven-io/emmett-postgresql
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Peer Dependencies
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install @event-driven-io/emmett @event-driven-io/pongo
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick Start
|
|
63
|
+
|
|
64
|
+
### Basic Event Store Setup
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { getPostgreSQLEventStore } from '@event-driven-io/emmett-postgresql';
|
|
68
|
+
|
|
69
|
+
// Create the event store
|
|
70
|
+
const eventStore = getPostgreSQLEventStore(
|
|
71
|
+
'postgresql://user:password@localhost:5432/mydb',
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Schema is auto-migrated by default
|
|
75
|
+
// To manually control migration:
|
|
76
|
+
await eventStore.schema.migrate();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Appending Events
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { type Event } from '@event-driven-io/emmett';
|
|
83
|
+
|
|
84
|
+
// Define your events
|
|
85
|
+
type ProductItemAdded = Event<
|
|
86
|
+
'ProductItemAdded',
|
|
87
|
+
{ productId: string; quantity: number }
|
|
88
|
+
>;
|
|
89
|
+
|
|
90
|
+
type ShoppingCartConfirmed = Event<
|
|
91
|
+
'ShoppingCartConfirmed',
|
|
92
|
+
{ confirmedAt: Date }
|
|
93
|
+
>;
|
|
94
|
+
|
|
95
|
+
type ShoppingCartEvent = ProductItemAdded | ShoppingCartConfirmed;
|
|
96
|
+
|
|
97
|
+
// Append events to a stream
|
|
98
|
+
const result = await eventStore.appendToStream<ShoppingCartEvent>(
|
|
99
|
+
'ShoppingCart-123',
|
|
100
|
+
[
|
|
101
|
+
{
|
|
102
|
+
type: 'ProductItemAdded',
|
|
103
|
+
data: { productId: 'product-1', quantity: 2 },
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
console.log(result.nextExpectedStreamVersion); // 1n
|
|
109
|
+
console.log(result.lastEventGlobalPosition); // Global position for ordering
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Reading Events
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// Read all events from a stream
|
|
116
|
+
const { events, currentStreamVersion } =
|
|
117
|
+
await eventStore.readStream<ShoppingCartEvent>('ShoppingCart-123');
|
|
118
|
+
|
|
119
|
+
for (const event of events) {
|
|
120
|
+
console.log(event.type, event.data);
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Aggregating State
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { type Decider } from '@event-driven-io/emmett';
|
|
128
|
+
|
|
129
|
+
interface ShoppingCart {
|
|
130
|
+
items: Map<string, number>;
|
|
131
|
+
status: 'Open' | 'Confirmed';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const evolve = (
|
|
135
|
+
state: ShoppingCart,
|
|
136
|
+
event: ShoppingCartEvent,
|
|
137
|
+
): ShoppingCart => {
|
|
138
|
+
switch (event.type) {
|
|
139
|
+
case 'ProductItemAdded':
|
|
140
|
+
state.items.set(
|
|
141
|
+
event.data.productId,
|
|
142
|
+
(state.items.get(event.data.productId) ?? 0) + event.data.quantity,
|
|
143
|
+
);
|
|
144
|
+
return state;
|
|
145
|
+
case 'ShoppingCartConfirmed':
|
|
146
|
+
return { ...state, status: 'Confirmed' };
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const { state, currentStreamVersion } = await eventStore.aggregateStream(
|
|
151
|
+
'ShoppingCart-123',
|
|
152
|
+
{
|
|
153
|
+
evolve,
|
|
154
|
+
initialState: () => ({ items: new Map(), status: 'Open' }),
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Optimistic Concurrency
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// Append with expected version check
|
|
163
|
+
await eventStore.appendToStream(
|
|
164
|
+
'ShoppingCart-123',
|
|
165
|
+
[{ type: 'ShoppingCartConfirmed', data: { confirmedAt: new Date() } }],
|
|
166
|
+
{ expectedStreamVersion: 1n },
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Throws ExpectedVersionConflictError if version doesn't match
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## How-to Guides
|
|
173
|
+
|
|
174
|
+
### Setting Up Inline Projections
|
|
175
|
+
|
|
176
|
+
Inline projections execute within the append transaction for strong consistency.
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import {
|
|
180
|
+
getPostgreSQLEventStore,
|
|
181
|
+
pongoSingleStreamProjection,
|
|
182
|
+
} from '@event-driven-io/emmett-postgresql';
|
|
183
|
+
|
|
184
|
+
// Define projection document
|
|
185
|
+
interface ShoppingCartDetails {
|
|
186
|
+
_id: string;
|
|
187
|
+
items: Array<{ productId: string; quantity: number }>;
|
|
188
|
+
status: string;
|
|
189
|
+
totalItems: number;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Create projection
|
|
193
|
+
const shoppingCartDetailsProjection = pongoSingleStreamProjection<
|
|
194
|
+
ShoppingCartDetails,
|
|
195
|
+
ShoppingCartEvent
|
|
196
|
+
>({
|
|
197
|
+
collectionName: 'shoppingCartDetails',
|
|
198
|
+
evolve: (document, event) => {
|
|
199
|
+
switch (event.type) {
|
|
200
|
+
case 'ProductItemAdded': {
|
|
201
|
+
const items = document?.items ?? [];
|
|
202
|
+
items.push({
|
|
203
|
+
productId: event.data.productId,
|
|
204
|
+
quantity: event.data.quantity,
|
|
205
|
+
});
|
|
206
|
+
return {
|
|
207
|
+
_id: event.metadata.streamName,
|
|
208
|
+
items,
|
|
209
|
+
status: 'Open',
|
|
210
|
+
totalItems: items.reduce((sum, i) => sum + i.quantity, 0),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
case 'ShoppingCartConfirmed':
|
|
214
|
+
return document ? { ...document, status: 'Confirmed' } : null;
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
canHandle: ['ProductItemAdded', 'ShoppingCartConfirmed'],
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Register with event store
|
|
221
|
+
const eventStore = getPostgreSQLEventStore(connectionString, {
|
|
222
|
+
projections: [{ type: 'inline', projection: shoppingCartDetailsProjection }],
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Setting Up Async Consumers
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { getPostgreSQLEventStore } from '@event-driven-io/emmett-postgresql';
|
|
230
|
+
|
|
231
|
+
const eventStore = getPostgreSQLEventStore(connectionString);
|
|
232
|
+
|
|
233
|
+
// Create a consumer
|
|
234
|
+
const consumer = eventStore.consumer();
|
|
235
|
+
|
|
236
|
+
// Add a projector
|
|
237
|
+
consumer.projector({
|
|
238
|
+
processorId: 'shopping-cart-summary',
|
|
239
|
+
projection: {
|
|
240
|
+
name: 'ShoppingCartSummary',
|
|
241
|
+
canHandle: ['ProductItemAdded', 'ShoppingCartConfirmed'],
|
|
242
|
+
handle: async (events, { execute }) => {
|
|
243
|
+
for (const event of events) {
|
|
244
|
+
// Update your read model
|
|
245
|
+
await execute.command(/* SQL to update read model */);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Add a reactor for side effects
|
|
252
|
+
consumer.reactor({
|
|
253
|
+
processorId: 'order-notification',
|
|
254
|
+
eachMessage: async (message, context) => {
|
|
255
|
+
if (message.type === 'ShoppingCartConfirmed') {
|
|
256
|
+
// Send notification, trigger workflow, etc.
|
|
257
|
+
console.log('Cart confirmed:', message.metadata.streamName);
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
canHandle: ['ShoppingCartConfirmed'],
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Start consuming
|
|
264
|
+
await consumer.start();
|
|
265
|
+
|
|
266
|
+
// Stop when done
|
|
267
|
+
await consumer.stop();
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Multi-Stream Projections with Pongo
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
import { pongoMultiStreamProjection } from '@event-driven-io/emmett-postgresql';
|
|
274
|
+
|
|
275
|
+
// Project events from multiple streams into documents keyed by custom ID
|
|
276
|
+
const productSalesProjection = pongoMultiStreamProjection<
|
|
277
|
+
{ _id: string; productId: string; totalSold: number },
|
|
278
|
+
ProductItemAdded
|
|
279
|
+
>({
|
|
280
|
+
collectionName: 'productSales',
|
|
281
|
+
canHandle: ['ProductItemAdded'],
|
|
282
|
+
getDocumentId: (event) => event.data.productId, // Custom document ID
|
|
283
|
+
evolve: (document, event) => ({
|
|
284
|
+
_id: event.data.productId,
|
|
285
|
+
productId: event.data.productId,
|
|
286
|
+
totalSold: (document?.totalSold ?? 0) + event.data.quantity,
|
|
287
|
+
}),
|
|
288
|
+
initialState: () => ({ _id: '', productId: '', totalSold: 0 }),
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Using Sessions for Transactions
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
// Execute multiple operations in a single transaction
|
|
296
|
+
await eventStore.withSession(async ({ eventStore: sessionStore }) => {
|
|
297
|
+
await sessionStore.appendToStream('Cart-1', [event1]);
|
|
298
|
+
await sessionStore.appendToStream('Cart-2', [event2]);
|
|
299
|
+
// Both appends succeed or fail together
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### CLI Migration Commands
|
|
304
|
+
|
|
305
|
+
The package provides CLI commands via the Emmett CLI plugin:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
# Run migrations
|
|
309
|
+
npx emmett migrate run --connectionString "postgresql://..."
|
|
310
|
+
|
|
311
|
+
# Generate migration SQL
|
|
312
|
+
npx emmett migrate sql --print
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## API Reference
|
|
316
|
+
|
|
317
|
+
### `getPostgreSQLEventStore(connectionString, options?)`
|
|
318
|
+
|
|
319
|
+
Creates a PostgreSQL event store instance.
|
|
320
|
+
|
|
321
|
+
**Parameters:**
|
|
322
|
+
|
|
323
|
+
- `connectionString: string` - PostgreSQL connection string
|
|
324
|
+
- `options?: PostgresEventStoreOptions`
|
|
325
|
+
- `projections?: ProjectionRegistration[]` - Inline projections to register
|
|
326
|
+
- `schema?: { autoMigration?: MigrationStyle }` - Schema migration settings (`'CreateOrUpdate'` | `'None'`)
|
|
327
|
+
- `connectionOptions?: PostgresEventStoreConnectionOptions` - Connection pool settings
|
|
328
|
+
|
|
329
|
+
**Returns:** `PostgresEventStore`
|
|
330
|
+
|
|
331
|
+
### `PostgresEventStore`
|
|
332
|
+
|
|
333
|
+
#### Methods
|
|
334
|
+
|
|
335
|
+
| Method | Description |
|
|
336
|
+
| ---------------------------------------------- | ----------------------------------- |
|
|
337
|
+
| `appendToStream(streamName, events, options?)` | Append events to a stream |
|
|
338
|
+
| `readStream(streamName, options?)` | Read events from a stream |
|
|
339
|
+
| `aggregateStream(streamName, options)` | Aggregate events into state |
|
|
340
|
+
| `consumer(options?)` | Create an async message consumer |
|
|
341
|
+
| `withSession(callback)` | Execute operations in a transaction |
|
|
342
|
+
| `schema.migrate()` | Run schema migrations |
|
|
343
|
+
| `schema.sql()` | Get schema SQL |
|
|
344
|
+
| `close()` | Close connections |
|
|
345
|
+
|
|
346
|
+
### Projection Functions
|
|
347
|
+
|
|
348
|
+
| Function | Description |
|
|
349
|
+
| -------------------------------------- | ------------------------------------------------------- |
|
|
350
|
+
| `pongoSingleStreamProjection(options)` | Project single stream to document (ID from stream name) |
|
|
351
|
+
| `pongoMultiStreamProjection(options)` | Project multiple streams to documents (custom ID) |
|
|
352
|
+
| `pongoProjection(options)` | Low-level Pongo projection |
|
|
353
|
+
| `postgreSQLProjection(options)` | Raw PostgreSQL projection |
|
|
354
|
+
| `postgreSQLRawSQLProjection(options)` | Execute raw SQL in projection |
|
|
355
|
+
|
|
356
|
+
### Consumer Functions
|
|
357
|
+
|
|
358
|
+
| Function | Description |
|
|
359
|
+
| --------------------------------------- | -------------------------- |
|
|
360
|
+
| `postgreSQLEventStoreConsumer(options)` | Create standalone consumer |
|
|
361
|
+
| `postgreSQLProjector(options)` | Create projector processor |
|
|
362
|
+
| `postgreSQLReactor(options)` | Create reactor processor |
|
|
363
|
+
|
|
364
|
+
## Architecture
|
|
365
|
+
|
|
366
|
+
### Database Schema
|
|
367
|
+
|
|
368
|
+
The event store uses three partitioned tables:
|
|
369
|
+
|
|
370
|
+
```
|
|
371
|
+
emt_streams - Stream metadata and positions
|
|
372
|
+
emt_messages - Event/message storage
|
|
373
|
+
emt_subscriptions - Consumer checkpoint tracking
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Each table is partitioned by a `partition` column for multi-tenant isolation.
|
|
377
|
+
|
|
378
|
+
### Message Flow
|
|
379
|
+
|
|
380
|
+
```
|
|
381
|
+
+-----------------+
|
|
382
|
+
| Application |
|
|
383
|
+
+--------+--------+
|
|
384
|
+
|
|
|
385
|
+
+--------------+--------------+
|
|
386
|
+
| |
|
|
387
|
+
+---------v---------+ +-----------v-----------+
|
|
388
|
+
| appendToStream | | Consumer |
|
|
389
|
+
| (with inline | | (polling loop) |
|
|
390
|
+
| projections) | +----------+------------+
|
|
391
|
+
+---------+---------+ |
|
|
392
|
+
| +---------+---------+
|
|
393
|
+
| | |
|
|
394
|
+
+---------v---------+ +----v----+ +------v------+
|
|
395
|
+
| emt_messages | |Projector| | Reactor |
|
|
396
|
+
| (PostgreSQL) | +---------+ +-------------+
|
|
397
|
+
+-------------------+ |
|
|
398
|
+
+-----v-----+
|
|
399
|
+
| Read Model|
|
|
400
|
+
+-----------+
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Connection Management
|
|
404
|
+
|
|
405
|
+
The event store supports both pooled and non-pooled connections:
|
|
406
|
+
|
|
407
|
+
- **Pooled (default)**: Uses `pg.Pool` for connection pooling
|
|
408
|
+
- **Non-pooled**: Uses a single `pg.Client` for dedicated connections
|
|
409
|
+
|
|
410
|
+
## Dependencies
|
|
411
|
+
|
|
412
|
+
### Peer Dependencies
|
|
413
|
+
|
|
414
|
+
| Package | Version | Purpose |
|
|
415
|
+
| ------------------------- | -------- | ------------------------------- |
|
|
416
|
+
| `@event-driven-io/emmett` | `0.38.3` | Core event sourcing library |
|
|
417
|
+
| `@event-driven-io/pongo` | `0.16.4` | MongoDB-like API for PostgreSQL |
|
|
418
|
+
|
|
419
|
+
### Internal Dependencies
|
|
420
|
+
|
|
421
|
+
| Package | Purpose |
|
|
422
|
+
| ------------------------ | ----------------------------------------- |
|
|
423
|
+
| `@event-driven-io/dumbo` | PostgreSQL database utilities (via pongo) |
|
|
424
|
+
|
|
425
|
+
### External Dependencies
|
|
426
|
+
|
|
427
|
+
| Package | Purpose |
|
|
428
|
+
| ----------- | ----------------------------- |
|
|
429
|
+
| `pg` | PostgreSQL client for Node.js |
|
|
430
|
+
| `uuid` | UUID generation (v4 and v7) |
|
|
431
|
+
| `commander` | CLI command parsing |
|
|
432
|
+
|
|
433
|
+
## Related Packages
|
|
434
|
+
|
|
435
|
+
- [`@event-driven-io/emmett`](https://www.npmjs.com/package/@event-driven-io/emmett) - Core library
|
|
436
|
+
- [`@event-driven-io/pongo`](https://www.npmjs.com/package/@event-driven-io/pongo) - Document projections
|
|
437
|
+
- [`@event-driven-io/emmett-testcontainers`](https://www.npmjs.com/package/@event-driven-io/emmett-testcontainers) - Testing utilities
|