@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 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