@wataruoguchi/emmett-event-store-kysely 1.1.1 → 1.1.2
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 +218 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# @wataruoguchi/emmett-event-store-kysely
|
|
2
|
+
|
|
3
|
+
A Kysely-based event store implementation for [Emmett](https://github.com/event-driven-io/emmett), providing event sourcing capabilities with PostgreSQL.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Event Store**: Emmett event store implementation with Kysely
|
|
8
|
+
- **Projections**: Read model projections with automatic event processing
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @wataruoguchi/emmett-event-store-kysely @event-driven-io/emmett kysely
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Database Setup
|
|
17
|
+
|
|
18
|
+
First, you need to set up the event store tables in your PostgreSQL database. You can achieve this with [the Kysely migration file](https://github.com/wataruoguchi/poc-emmett/blob/main/package/1758758113676_event_sourcing_migration_example.ts)
|
|
19
|
+
|
|
20
|
+
## Basic Usage
|
|
21
|
+
|
|
22
|
+
You can find [the complete working example](https://github.com/wataruoguchi/poc-emmett/tree/main/example). The following are some snippets.
|
|
23
|
+
|
|
24
|
+
### 1. Setting up the Event Store
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { createEventStore } from "@wataruoguchi/emmett-event-store-kysely";
|
|
28
|
+
import { Kysely, PostgresDialect } from "kysely";
|
|
29
|
+
import { Pool } from "pg";
|
|
30
|
+
|
|
31
|
+
// Set up your Kysely database connection
|
|
32
|
+
const db = new Kysely<YourDatabaseSchema>({
|
|
33
|
+
dialect: new PostgresDialect({
|
|
34
|
+
pool: new Pool({
|
|
35
|
+
connectionString: process.env.DATABASE_URL,
|
|
36
|
+
}),
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Create the event store
|
|
41
|
+
const eventStore = createEventStore({ db, logger });
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 2. Using the Event Store with Emmett
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { DeciderCommandHandler } from "@event-driven-io/emmett";
|
|
48
|
+
import type { EventStore } from "@wataruoguchi/emmett-event-store-kysely";
|
|
49
|
+
|
|
50
|
+
// Define your domain events and commands
|
|
51
|
+
type CreateCartCommand = {
|
|
52
|
+
type: "CreateCart";
|
|
53
|
+
data: { tenantId: string; cartId: string; currency: string };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type CartCreatedEvent = {
|
|
57
|
+
type: "CartCreated";
|
|
58
|
+
data: { tenantId: string; cartId: string; currency: string };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Create your event handler
|
|
62
|
+
export function cartEventHandler({
|
|
63
|
+
eventStore,
|
|
64
|
+
getContext,
|
|
65
|
+
}: {
|
|
66
|
+
eventStore: EventStore;
|
|
67
|
+
getContext: () => AppContext;
|
|
68
|
+
}) {
|
|
69
|
+
const handler = DeciderCommandHandler({
|
|
70
|
+
decide: createDecide(getContext),
|
|
71
|
+
evolve: createEvolve(),
|
|
72
|
+
initialState,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
create: (cartId: string, data: CreateCartCommand["data"]) =>
|
|
77
|
+
handler(
|
|
78
|
+
eventStore,
|
|
79
|
+
cartId,
|
|
80
|
+
{ type: "CreateCart", data },
|
|
81
|
+
{ partition: data.tenantId, streamType: "cart" }
|
|
82
|
+
),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Use in your service
|
|
87
|
+
const cartService = createCartService({
|
|
88
|
+
eventStore,
|
|
89
|
+
getContext,
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Projections (Read Models)
|
|
94
|
+
|
|
95
|
+
### 1. Creating a Projection
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import type {
|
|
99
|
+
ProjectionEvent,
|
|
100
|
+
ProjectionRegistry,
|
|
101
|
+
} from "@wataruoguchi/emmett-event-store-kysely/projections";
|
|
102
|
+
|
|
103
|
+
export function cartsProjection(): ProjectionRegistry<DatabaseExecutor> {
|
|
104
|
+
return {
|
|
105
|
+
CartCreated: async (db, event) => {
|
|
106
|
+
await db
|
|
107
|
+
.insertInto("carts")
|
|
108
|
+
.values({
|
|
109
|
+
stream_id: event.metadata.streamId,
|
|
110
|
+
tenant_id: event.data.tenantId,
|
|
111
|
+
cart_id: event.data.cartId,
|
|
112
|
+
currency: event.data.currency,
|
|
113
|
+
items: JSON.stringify([]),
|
|
114
|
+
total: 0,
|
|
115
|
+
last_stream_position: event.metadata.streamPosition,
|
|
116
|
+
})
|
|
117
|
+
.execute();
|
|
118
|
+
},
|
|
119
|
+
ItemAddedToCart: async (db, event) => {
|
|
120
|
+
// Update cart with new item
|
|
121
|
+
await db
|
|
122
|
+
.updateTable("carts")
|
|
123
|
+
.set({
|
|
124
|
+
items: JSON.stringify([...existingItems, event.data.item]),
|
|
125
|
+
total: newTotal,
|
|
126
|
+
last_stream_position: event.metadata.streamPosition,
|
|
127
|
+
})
|
|
128
|
+
.where("stream_id", "=", event.metadata.streamId)
|
|
129
|
+
.execute();
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 2. Running Projections
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import {
|
|
139
|
+
createProjectionRegistry,
|
|
140
|
+
createProjectionRunner,
|
|
141
|
+
} from "@wataruoguchi/emmett-event-store-kysely/projections";
|
|
142
|
+
import { createReadStream } from "@wataruoguchi/emmett-event-store-kysely";
|
|
143
|
+
|
|
144
|
+
// Set up projection runner
|
|
145
|
+
const readStream = createReadStream({ db, logger });
|
|
146
|
+
const registry = createProjectionRegistry(cartsProjection());
|
|
147
|
+
const runner = createProjectionRunner({
|
|
148
|
+
db,
|
|
149
|
+
readStream,
|
|
150
|
+
registry,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Project events for a specific stream
|
|
154
|
+
await runner.projectEvents("carts-read-model", "cart-123", {
|
|
155
|
+
partition: "tenant-456",
|
|
156
|
+
batchSize: 100,
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 3. Projection Worker
|
|
161
|
+
|
|
162
|
+
Create a worker to continuously process projections:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
#!/usr/bin/env node
|
|
166
|
+
import { createReadStream } from "@wataruoguchi/emmett-event-store-kysely";
|
|
167
|
+
import {
|
|
168
|
+
createProjectionRegistry,
|
|
169
|
+
createProjectionRunner,
|
|
170
|
+
} from "@wataruoguchi/emmett-event-store-kysely/projections";
|
|
171
|
+
|
|
172
|
+
async function main(partition: string) {
|
|
173
|
+
const db = getDb();
|
|
174
|
+
const readStream = createReadStream({ db, logger });
|
|
175
|
+
const registry = createProjectionRegistry(cartsProjection());
|
|
176
|
+
const runner = createProjectionRunner({
|
|
177
|
+
db,
|
|
178
|
+
readStream,
|
|
179
|
+
registry,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const subscriptionId = "carts-read-model";
|
|
183
|
+
const batchSize = 200;
|
|
184
|
+
const pollIntervalMs = 1000;
|
|
185
|
+
|
|
186
|
+
while (true) {
|
|
187
|
+
// Get streams for this partition
|
|
188
|
+
const streams = await db
|
|
189
|
+
.selectFrom("streams")
|
|
190
|
+
.select(["stream_id"])
|
|
191
|
+
.where("is_archived", "=", false)
|
|
192
|
+
.where("partition", "=", partition)
|
|
193
|
+
.where("stream_type", "=", "cart")
|
|
194
|
+
.execute();
|
|
195
|
+
|
|
196
|
+
// Process each stream
|
|
197
|
+
for (const stream of streams) {
|
|
198
|
+
await runner.projectEvents(subscriptionId, stream.stream_id, {
|
|
199
|
+
partition,
|
|
200
|
+
batchSize,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Run with: node projection-worker.js tenant-123
|
|
209
|
+
main(process.argv[2]);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
MIT
|
|
215
|
+
|
|
216
|
+
## Contributing
|
|
217
|
+
|
|
218
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|