@wataruoguchi/emmett-event-store-kysely 1.1.1 → 1.1.3
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/dist/event-store/index.cjs +4 -1
- package/dist/event-store/index.d.ts +1 -1
- package/dist/event-store/index.d.ts.map +1 -1
- package/dist/types.d.ts +8 -9
- package/dist/types.d.ts.map +1 -1
- 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.
|
|
@@ -281,7 +281,10 @@ async function fetchStreamInfo2(executor, streamId, partition) {
|
|
|
281
281
|
function createEventStore(deps) {
|
|
282
282
|
const readStream = createReadStream(deps);
|
|
283
283
|
const appendToStream = createAppendToStream(deps);
|
|
284
|
-
const aggregateStream = createAggregateStream(
|
|
284
|
+
const aggregateStream = createAggregateStream(
|
|
285
|
+
{ readStream },
|
|
286
|
+
deps
|
|
287
|
+
);
|
|
285
288
|
return { readStream, appendToStream, aggregateStream };
|
|
286
289
|
}
|
|
287
290
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -5,7 +5,7 @@ export type { AppendToStream } from "./append-to-stream.js";
|
|
|
5
5
|
export type { ReadStream } from "./read-stream.js";
|
|
6
6
|
export type EventStore = ReturnType<typeof createEventStore>;
|
|
7
7
|
export { createReadStream } from "./read-stream.js";
|
|
8
|
-
export declare function createEventStore(deps: Dependencies): {
|
|
8
|
+
export declare function createEventStore<T = any>(deps: Dependencies<T>): {
|
|
9
9
|
readStream: import("./read-stream.js").ReadStream;
|
|
10
10
|
appendToStream: import("./append-to-stream.js").AppendToStream;
|
|
11
11
|
aggregateStream: import("./aggregate-stream.js").AggregateStream;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/event-store/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,YAAY,EACV,gBAAgB,EAChB,YAAY,EACZ,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,YAAY,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/event-store/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,YAAY,EACV,gBAAgB,EAChB,YAAY,EACZ,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,YAAY,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,wBAAgB,gBAAgB,CAAC,CAAC,GAAG,GAAG,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC;;;;EAQ9D"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import type { Kysely
|
|
2
|
-
|
|
3
|
-
export type DatabaseExecutor = Kysely<EventStoreDBSchema> | Transaction<EventStoreDBSchema>;
|
|
1
|
+
import type { Kysely } from "kysely";
|
|
2
|
+
export type DatabaseExecutor<T = any> = Kysely<T>;
|
|
4
3
|
export type Logger = {
|
|
5
4
|
info: (obj: unknown, msg?: string) => void;
|
|
6
5
|
error: (obj: unknown, msg?: string) => void;
|
|
7
6
|
warn?: (obj: unknown, msg?: string) => void;
|
|
8
7
|
debug?: (obj: unknown, msg?: string) => void;
|
|
9
8
|
};
|
|
10
|
-
export type Dependencies = {
|
|
11
|
-
db: DatabaseExecutor
|
|
9
|
+
export type Dependencies<T = any> = {
|
|
10
|
+
db: DatabaseExecutor<T>;
|
|
12
11
|
logger: Logger;
|
|
13
12
|
};
|
|
14
13
|
export type ExtendedOptions = {
|
|
@@ -27,12 +26,12 @@ export type ProjectionEvent = {
|
|
|
27
26
|
data: unknown;
|
|
28
27
|
metadata: ProjectionEventMetadata;
|
|
29
28
|
};
|
|
30
|
-
export type ProjectionContext<T = DatabaseExecutor
|
|
29
|
+
export type ProjectionContext<T = DatabaseExecutor<any>> = {
|
|
31
30
|
db: T;
|
|
32
31
|
partition: string;
|
|
33
32
|
};
|
|
34
|
-
export type ProjectionHandler<T = DatabaseExecutor
|
|
35
|
-
export type ProjectionRegistry<T = DatabaseExecutor
|
|
36
|
-
export declare function createProjectionRegistry<T = DatabaseExecutor
|
|
33
|
+
export type ProjectionHandler<T = DatabaseExecutor<any>> = (ctx: ProjectionContext<T>, event: ProjectionEvent) => void | Promise<void>;
|
|
34
|
+
export type ProjectionRegistry<T = DatabaseExecutor<any>> = Record<string, ProjectionHandler<T>[]>;
|
|
35
|
+
export declare function createProjectionRegistry<T = DatabaseExecutor<any>>(...registries: ProjectionRegistry<T>[]): ProjectionRegistry<T>;
|
|
37
36
|
export type { ReadStream } from "./event-store/read-stream.js";
|
|
38
37
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGrC,MAAM,MAAM,gBAAgB,CAAC,CAAC,GAAG,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC;AAElD,MAAM,MAAM,MAAM,GAAG;IACnB,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,KAAK,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C,CAAC;AAEF,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,GAAG,IAAI;IAClC,EAAE,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,wCAAwC,KAAK,CAAC;AAC3D,eAAO,MAAM,iBAAiB,EAAG,mBAA4B,CAAC;AAG9D,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,uBAAuB,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,iBAAiB,CAAC,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,IAAI;IACzD,EAAE,EAAE,CAAC,CAAC;IACN,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,CAAC,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,IAAI,CACzD,GAAG,EAAE,iBAAiB,CAAC,CAAC,CAAC,EACzB,KAAK,EAAE,eAAe,KACnB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B,MAAM,MAAM,kBAAkB,CAAC,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,IAAI,MAAM,CAChE,MAAM,EACN,iBAAiB,CAAC,CAAC,CAAC,EAAE,CACvB,CAAC;AAEF,wBAAgB,wBAAwB,CAAC,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,EAChE,GAAG,UAAU,EAAE,kBAAkB,CAAC,CAAC,CAAC,EAAE,GACrC,kBAAkB,CAAC,CAAC,CAAC,CAYvB;AAGD,YAAY,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC"}
|