@wataruoguchi/emmett-event-store-kysely 2.0.2 → 2.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 +9 -20
- package/dist/event-store/kysely-event-store.d.ts.map +1 -1
- package/dist/event-store/kysely-event-store.js +28 -17
- package/dist/index.cjs +36 -16
- package/dist/projections/runner.d.ts +10 -5
- package/dist/projections/runner.d.ts.map +1 -1
- package/dist/projections/runner.js +1 -1
- package/dist/projections/snapshot-projection.d.ts +1 -1
- package/dist/projections/snapshot-projection.d.ts.map +1 -1
- package/dist/projections/snapshot-projection.js +11 -1
- package/dist/types.d.ts +24 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -11
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
A Kysely-based event store implementation for [Emmett](https://github.com/event-driven-io/emmett), providing event sourcing capabilities with PostgreSQL.
|
|
4
4
|
|
|
5
|
+
## 📚 Documentation
|
|
6
|
+
|
|
7
|
+
**👉 [View Complete Documentation →](https://wataruoguchi.github.io/emmett-libs/emmett-event-store-kysely)**
|
|
8
|
+
|
|
5
9
|
## Features
|
|
6
10
|
|
|
7
11
|
- **Event Store** - Full event sourcing with Kysely and PostgreSQL
|
|
@@ -20,13 +24,12 @@ npm install @wataruoguchi/emmett-event-store-kysely @event-driven-io/emmett kyse
|
|
|
20
24
|
|
|
21
25
|
### 1. Database Setup
|
|
22
26
|
|
|
23
|
-
Set up the required PostgreSQL tables using [our migration example](https://github.com/wataruoguchi/emmett-event-store-kysely/
|
|
27
|
+
Set up the required PostgreSQL tables using [our migration example](https://github.com/wataruoguchi/emmett-libs/blob/main/packages/emmett-event-store-kysely/database/migrations/1758758113676_event_sourcing_migration_example.ts):
|
|
24
28
|
|
|
25
29
|
```typescript
|
|
26
30
|
import { Kysely } from "kysely";
|
|
27
31
|
|
|
28
32
|
// Required tables: messages, streams, subscriptions
|
|
29
|
-
// See https://github.com/wataruoguchi/emmett-event-store-kysely/blob/main/package/docs/database-setup.md for details
|
|
30
33
|
```
|
|
31
34
|
|
|
32
35
|
A read model table expects to have the following columns:
|
|
@@ -110,24 +113,10 @@ await runner.projectEvents("subscription-id", "cart-123", {
|
|
|
110
113
|
});
|
|
111
114
|
```
|
|
112
115
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
## Documentation
|
|
116
|
-
|
|
117
|
-
📚 **[Complete Documentation](https://github.com/wataruoguchi/emmett-event-store-kysely/blob/main/package/docs/README.md)**
|
|
118
|
-
|
|
119
|
-
### Core Guides
|
|
120
|
-
|
|
121
|
-
- [Database Setup](https://github.com/wataruoguchi/emmett-event-store-kysely/blob/main/package/docs/database-setup.md) - PostgreSQL schema and requirements
|
|
122
|
-
- [Event Store](https://github.com/wataruoguchi/emmett-event-store-kysely/blob/main/package/docs/event-store.md) - Core event store API
|
|
123
|
-
- [Snapshot Projections](https://github.com/wataruoguchi/emmett-event-store-kysely/blob/main/package/docs/snapshot-projections.md) - Build read models (recommended) ⭐
|
|
124
|
-
- [Event Consumer](https://github.com/wataruoguchi/emmett-event-store-kysely/blob/main/package/docs/consumer.md) - Continuous background processing
|
|
125
|
-
- [Projection Runner](https://github.com/wataruoguchi/emmett-event-store-kysely/blob/main/package/docs/projection-runner.md) - On-demand processing for tests
|
|
126
|
-
|
|
127
|
-
### Examples
|
|
116
|
+
## Examples
|
|
128
117
|
|
|
129
|
-
- [Working Example](https://github.com/wataruoguchi/emmett-
|
|
130
|
-
- [Migration Example](https://github.com/wataruoguchi/emmett-event-store-kysely/
|
|
118
|
+
- [Working Example](https://github.com/wataruoguchi/emmett-libs/tree/main/example/) - Complete application with carts and generators
|
|
119
|
+
- [Migration Example](https://github.com/wataruoguchi/emmett-libs/blob/main/packages/emmett-event-store-kysely/database/migrations/1758758113676_event_sourcing_migration_example.ts) - Database setup
|
|
131
120
|
|
|
132
121
|
## License
|
|
133
122
|
|
|
@@ -135,4 +124,4 @@ MIT
|
|
|
135
124
|
|
|
136
125
|
## Contributing
|
|
137
126
|
|
|
138
|
-
Contributions are welcome! Please see our [GitHub repository](https://github.com/wataruoguchi/emmett-
|
|
127
|
+
Contributions are welcome! Please see our [GitHub repository](https://github.com/wataruoguchi/emmett-libs) for issues and PRs.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kysely-event-store.d.ts","sourceRoot":"","sources":["../../src/event-store/kysely-event-store.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,qBAAqB,EAC1B,KAAK,sCAAsC,EAC3C,KAAK,KAAK,EACV,KAAK,UAAU,EAEf,KAAK,wBAAwB,EAE7B,KAAK,mCAAmC,EACxC,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAErB,KAAK,uBAAuB,GAAG,mCAAmC,CAAC;AACnE,KAAK,6BAA6B,GAAG,qBAAqB,GAAG,eAAe,CAAC;AAI7E,MAAM,MAAM,2BAA2B,GAAG;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,WAAW,gBACf,SAAQ,UAAU,CAAC,uBAAuB,CAAC,EACzC,wBAAwB,CAAC,gBAAgB,CAAC;IAE5C,UAAU,CAAC,SAAS,SAAS,KAAK,EAChC,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,iBAAiB,CAAC,MAAM,CAAC,GAAG,2BAA2B,GAChE,OAAO,CAAC,gBAAgB,CAAC,SAAS,EAAE,uBAAuB,CAAC,CAAC,CAAC;IACjE,cAAc,CAAC,SAAS,SAAS,KAAK,EACpC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,EAAE,EACnB,OAAO,CAAC,EAAE,6BAA6B,GACtC,OAAO,CAAC,sCAAsC,CAAC,CAAC;IACnD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,EAAE;QACN,GAAG,IAAI,MAAM,CAAC;QACd,KAAK,IAAI,IAAI,CAAC;QACd,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;KAC1B,CAAC;CACH;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,kCAAkC;IAClC,iBAAiB,CAAC,EAAE;QAClB,iDAAiD;QACjD,EAAE,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;KACtB,CAAC;IACF,gCAAgC;IAChC,MAAM,CAAC,EAAE;QACP,8BAA8B;QAC9B,aAAa,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC;KAC3C,CAAC;IACF,iCAAiC;IACjC,KAAK,CAAC,EAAE;QACN,qCAAqC;QACrC,oBAAoB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;KACnD,CAAC;CACH,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,uBAIlC,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,MAAM,YAAY,KAAG,
|
|
1
|
+
{"version":3,"file":"kysely-event-store.d.ts","sourceRoot":"","sources":["../../src/event-store/kysely-event-store.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,qBAAqB,EAC1B,KAAK,sCAAsC,EAC3C,KAAK,KAAK,EACV,KAAK,UAAU,EAEf,KAAK,wBAAwB,EAE7B,KAAK,mCAAmC,EACxC,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAErB,KAAK,uBAAuB,GAAG,mCAAmC,CAAC;AACnE,KAAK,6BAA6B,GAAG,qBAAqB,GAAG,eAAe,CAAC;AAI7E,MAAM,MAAM,2BAA2B,GAAG;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,WAAW,gBACf,SAAQ,UAAU,CAAC,uBAAuB,CAAC,EACzC,wBAAwB,CAAC,gBAAgB,CAAC;IAE5C,UAAU,CAAC,SAAS,SAAS,KAAK,EAChC,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,iBAAiB,CAAC,MAAM,CAAC,GAAG,2BAA2B,GAChE,OAAO,CAAC,gBAAgB,CAAC,SAAS,EAAE,uBAAuB,CAAC,CAAC,CAAC;IACjE,cAAc,CAAC,SAAS,SAAS,KAAK,EACpC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,EAAE,EACnB,OAAO,CAAC,EAAE,6BAA6B,GACtC,OAAO,CAAC,sCAAsC,CAAC,CAAC;IACnD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,EAAE;QACN,GAAG,IAAI,MAAM,CAAC;QACd,KAAK,IAAI,IAAI,CAAC;QACd,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;KAC1B,CAAC;CACH;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,kCAAkC;IAClC,iBAAiB,CAAC,EAAE;QAClB,iDAAiD;QACjD,EAAE,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;KACtB,CAAC;IACF,gCAAgC;IAChC,MAAM,CAAC,EAAE;QACP,8BAA8B;QAC9B,aAAa,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC;KAC3C,CAAC;IACF,iCAAiC;IACjC,KAAK,CAAC,EAAE;QACN,qCAAqC;QACrC,oBAAoB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;KACnD,CAAC;CACH,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,uBAIlC,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,MAAM,YAAY,KAAG,gBAkLxD,CAAC"}
|
|
@@ -7,7 +7,7 @@ export const defaultKyselyOptions = {
|
|
|
7
7
|
},
|
|
8
8
|
};
|
|
9
9
|
export const getKyselyEventStore = (deps) => {
|
|
10
|
-
const { db, logger } = deps;
|
|
10
|
+
const { db, logger, inTransaction = false } = deps;
|
|
11
11
|
const eventStore = {
|
|
12
12
|
/**
|
|
13
13
|
* @description We do not use schema management in this package.
|
|
@@ -18,17 +18,25 @@ export const getKyselyEventStore = (deps) => {
|
|
|
18
18
|
migrate: async () => Promise.resolve(),
|
|
19
19
|
},
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* Provide a session-bound event store using a Kysely transaction.
|
|
22
|
+
* All operations within the callback will share the same DB transaction.
|
|
22
23
|
*/
|
|
23
24
|
async withSession(callback) {
|
|
24
|
-
return await
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
return await db.transaction().execute(async (trx) => {
|
|
26
|
+
const sessionEventStore = getKyselyEventStore({
|
|
27
|
+
db: trx,
|
|
28
|
+
logger,
|
|
29
|
+
inTransaction: true,
|
|
30
|
+
});
|
|
31
|
+
return await callback({
|
|
32
|
+
eventStore: sessionEventStore,
|
|
33
|
+
close: () => Promise.resolve(),
|
|
34
|
+
});
|
|
27
35
|
});
|
|
28
36
|
},
|
|
29
37
|
async aggregateStream(streamName, options) {
|
|
30
38
|
const { evolve, initialState, read } = options;
|
|
31
|
-
logger.
|
|
39
|
+
logger.debug?.({ streamName, options }, "aggregateStream");
|
|
32
40
|
const expectedStreamVersion = read?.expectedStreamVersion;
|
|
33
41
|
const result = await eventStore.readStream(streamName, read);
|
|
34
42
|
assertExpectedVersionMatchesCurrent(result.currentStreamVersion, expectedStreamVersion, PostgreSQLEventStoreDefaultStreamVersion);
|
|
@@ -41,7 +49,7 @@ export const getKyselyEventStore = (deps) => {
|
|
|
41
49
|
},
|
|
42
50
|
async readStream(streamName, options) {
|
|
43
51
|
const partition = getPartition(options);
|
|
44
|
-
logger.
|
|
52
|
+
logger.debug?.({ streamName, options, partition }, "readStream");
|
|
45
53
|
const { currentStreamVersion, streamExists } = await fetchStreamInfo(db, streamName, partition);
|
|
46
54
|
const range = parseRangeOptions(options);
|
|
47
55
|
const rows = await buildEventsQuery({ db, logger }, streamName, partition, range).execute();
|
|
@@ -56,25 +64,29 @@ export const getKyselyEventStore = (deps) => {
|
|
|
56
64
|
const streamType = getStreamType(options);
|
|
57
65
|
const partition = getPartition(options);
|
|
58
66
|
const expected = options?.expectedStreamVersion;
|
|
59
|
-
logger.
|
|
67
|
+
logger.debug?.({ streamName, events, options, partition }, "appendToStream");
|
|
60
68
|
ensureEventsNotEmpty(events, expected);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const { currentStreamVersion, streamExists } = await fetchStreamInfo(trx, streamName, partition);
|
|
69
|
+
// It may be called within a transaction via withSession.
|
|
70
|
+
const executeOn = async (executor) => {
|
|
71
|
+
const { currentStreamVersion, streamExists } = await fetchStreamInfo(executor, streamName, partition);
|
|
65
72
|
assertExpectedVersion(expected, currentStreamVersion, streamExists);
|
|
66
73
|
const basePos = currentStreamVersion;
|
|
67
74
|
const nextStreamPosition = computeNextStreamPosition(basePos, events.length);
|
|
68
|
-
await upsertStreamRow(
|
|
75
|
+
await upsertStreamRow(executor, streamName, partition, streamType, basePos, nextStreamPosition, expected, streamExists);
|
|
69
76
|
const messagesToInsert = buildMessagesToInsert(events, basePos, streamName, partition);
|
|
70
|
-
const lastEventGlobalPosition = await insertMessagesAndGetLastGlobalPosition(
|
|
77
|
+
const lastEventGlobalPosition = await insertMessagesAndGetLastGlobalPosition(executor, messagesToInsert);
|
|
71
78
|
return {
|
|
72
79
|
nextExpectedStreamVersion: nextStreamPosition,
|
|
73
80
|
lastEventGlobalPosition,
|
|
74
81
|
createdNewStream: !streamExists,
|
|
75
82
|
};
|
|
76
|
-
}
|
|
77
|
-
|
|
83
|
+
};
|
|
84
|
+
if (inTransaction) {
|
|
85
|
+
return executeOn(db);
|
|
86
|
+
}
|
|
87
|
+
return db
|
|
88
|
+
.transaction()
|
|
89
|
+
.execute(async (trx) => executeOn(trx));
|
|
78
90
|
},
|
|
79
91
|
close: async () => {
|
|
80
92
|
// Kysely doesn't require explicit closing for most cases
|
|
@@ -155,7 +167,6 @@ function buildMessagesToInsert(events, basePos, streamId, partition) {
|
|
|
155
167
|
const rawMeta = "metadata" in e ? e.metadata : undefined;
|
|
156
168
|
const eventMeta = rawMeta && typeof rawMeta === "object" ? rawMeta : {};
|
|
157
169
|
const messageMetadata = {
|
|
158
|
-
messageId,
|
|
159
170
|
...eventMeta,
|
|
160
171
|
};
|
|
161
172
|
return {
|
package/dist/index.cjs
CHANGED
|
@@ -184,7 +184,7 @@ function createProjectionRegistry(...registries) {
|
|
|
184
184
|
|
|
185
185
|
// src/event-store/kysely-event-store.ts
|
|
186
186
|
var getKyselyEventStore = (deps) => {
|
|
187
|
-
const { db, logger } = deps;
|
|
187
|
+
const { db, logger, inTransaction = false } = deps;
|
|
188
188
|
const eventStore = {
|
|
189
189
|
/**
|
|
190
190
|
* @description We do not use schema management in this package.
|
|
@@ -195,17 +195,25 @@ var getKyselyEventStore = (deps) => {
|
|
|
195
195
|
migrate: async () => Promise.resolve()
|
|
196
196
|
},
|
|
197
197
|
/**
|
|
198
|
-
*
|
|
198
|
+
* Provide a session-bound event store using a Kysely transaction.
|
|
199
|
+
* All operations within the callback will share the same DB transaction.
|
|
199
200
|
*/
|
|
200
201
|
async withSession(callback) {
|
|
201
|
-
return await
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
return await db.transaction().execute(async (trx) => {
|
|
203
|
+
const sessionEventStore = getKyselyEventStore({
|
|
204
|
+
db: trx,
|
|
205
|
+
logger,
|
|
206
|
+
inTransaction: true
|
|
207
|
+
});
|
|
208
|
+
return await callback({
|
|
209
|
+
eventStore: sessionEventStore,
|
|
210
|
+
close: () => Promise.resolve()
|
|
211
|
+
});
|
|
204
212
|
});
|
|
205
213
|
},
|
|
206
214
|
async aggregateStream(streamName, options) {
|
|
207
215
|
const { evolve, initialState, read } = options;
|
|
208
|
-
logger.
|
|
216
|
+
logger.debug?.({ streamName, options }, "aggregateStream");
|
|
209
217
|
const expectedStreamVersion = read?.expectedStreamVersion;
|
|
210
218
|
const result = await eventStore.readStream(streamName, read);
|
|
211
219
|
(0, import_emmett.assertExpectedVersionMatchesCurrent)(
|
|
@@ -225,7 +233,7 @@ var getKyselyEventStore = (deps) => {
|
|
|
225
233
|
},
|
|
226
234
|
async readStream(streamName, options) {
|
|
227
235
|
const partition = getPartition(options);
|
|
228
|
-
logger.
|
|
236
|
+
logger.debug?.({ streamName, options, partition }, "readStream");
|
|
229
237
|
const { currentStreamVersion, streamExists } = await fetchStreamInfo(
|
|
230
238
|
db,
|
|
231
239
|
streamName,
|
|
@@ -251,11 +259,14 @@ var getKyselyEventStore = (deps) => {
|
|
|
251
259
|
const streamType = getStreamType(options);
|
|
252
260
|
const partition = getPartition(options);
|
|
253
261
|
const expected = options?.expectedStreamVersion;
|
|
254
|
-
logger.
|
|
262
|
+
logger.debug?.(
|
|
263
|
+
{ streamName, events, options, partition },
|
|
264
|
+
"appendToStream"
|
|
265
|
+
);
|
|
255
266
|
ensureEventsNotEmpty(events, expected);
|
|
256
|
-
const
|
|
267
|
+
const executeOn = async (executor) => {
|
|
257
268
|
const { currentStreamVersion, streamExists } = await fetchStreamInfo(
|
|
258
|
-
|
|
269
|
+
executor,
|
|
259
270
|
streamName,
|
|
260
271
|
partition
|
|
261
272
|
);
|
|
@@ -266,7 +277,7 @@ var getKyselyEventStore = (deps) => {
|
|
|
266
277
|
events.length
|
|
267
278
|
);
|
|
268
279
|
await upsertStreamRow(
|
|
269
|
-
|
|
280
|
+
executor,
|
|
270
281
|
streamName,
|
|
271
282
|
partition,
|
|
272
283
|
streamType,
|
|
@@ -281,14 +292,20 @@ var getKyselyEventStore = (deps) => {
|
|
|
281
292
|
streamName,
|
|
282
293
|
partition
|
|
283
294
|
);
|
|
284
|
-
const lastEventGlobalPosition = await insertMessagesAndGetLastGlobalPosition(
|
|
295
|
+
const lastEventGlobalPosition = await insertMessagesAndGetLastGlobalPosition(
|
|
296
|
+
executor,
|
|
297
|
+
messagesToInsert
|
|
298
|
+
);
|
|
285
299
|
return {
|
|
286
300
|
nextExpectedStreamVersion: nextStreamPosition,
|
|
287
301
|
lastEventGlobalPosition,
|
|
288
302
|
createdNewStream: !streamExists
|
|
289
303
|
};
|
|
290
|
-
}
|
|
291
|
-
|
|
304
|
+
};
|
|
305
|
+
if (inTransaction) {
|
|
306
|
+
return executeOn(db);
|
|
307
|
+
}
|
|
308
|
+
return db.transaction().execute(async (trx) => executeOn(trx));
|
|
292
309
|
},
|
|
293
310
|
close: async () => {
|
|
294
311
|
await Promise.resolve();
|
|
@@ -351,7 +368,6 @@ function buildMessagesToInsert(events, basePos, streamId, partition) {
|
|
|
351
368
|
const rawMeta = "metadata" in e ? e.metadata : void 0;
|
|
352
369
|
const eventMeta = rawMeta && typeof rawMeta === "object" ? rawMeta : {};
|
|
353
370
|
const messageMetadata = {
|
|
354
|
-
messageId,
|
|
355
371
|
...eventMeta
|
|
356
372
|
};
|
|
357
373
|
return {
|
|
@@ -430,7 +446,11 @@ async function fetchStreamInfo(executor, streamId, partition) {
|
|
|
430
446
|
}
|
|
431
447
|
|
|
432
448
|
// src/projections/runner.ts
|
|
433
|
-
function createProjectionRunner({
|
|
449
|
+
function createProjectionRunner({
|
|
450
|
+
db,
|
|
451
|
+
readStream,
|
|
452
|
+
registry
|
|
453
|
+
}) {
|
|
434
454
|
async function getOrCreateCheckpoint(subscriptionId, partition) {
|
|
435
455
|
const existing = await db.selectFrom("subscriptions").select([
|
|
436
456
|
"subscription_id as subscriptionId",
|
|
@@ -1,16 +1,21 @@
|
|
|
1
|
+
import type { Kysely } from "kysely";
|
|
1
2
|
import type { KyselyEventStore } from "../event-store/kysely-event-store.js";
|
|
2
|
-
import type {
|
|
3
|
+
import type { ProjectionRegistry } from "../types.js";
|
|
3
4
|
export type SubscriptionCheckpoint = {
|
|
4
5
|
subscriptionId: string;
|
|
5
6
|
partition: string;
|
|
6
7
|
lastProcessedPosition: bigint;
|
|
7
8
|
};
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Flexible database type for projection runner.
|
|
11
|
+
* Uses `Kysely<any> | any` to work around Kysely's private field variance issue.
|
|
12
|
+
*/
|
|
13
|
+
export type ProjectionRunnerDeps = {
|
|
14
|
+
db: Kysely<any> | any;
|
|
10
15
|
readStream: KyselyEventStore["readStream"];
|
|
11
|
-
registry: ProjectionRegistry
|
|
16
|
+
registry: ProjectionRegistry;
|
|
12
17
|
};
|
|
13
|
-
export declare function createProjectionRunner
|
|
18
|
+
export declare function createProjectionRunner({ db, readStream, registry, }: ProjectionRunnerDeps): {
|
|
14
19
|
projectEvents: (subscriptionId: string, streamId: string, opts?: {
|
|
15
20
|
partition?: string;
|
|
16
21
|
batchSize?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/projections/runner.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/projections/runner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAqB,MAAM,QAAQ,CAAC;AAExD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAC7E,OAAO,KAAK,EAAmB,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEvE,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,qBAAqB,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACtB,UAAU,EAAE,gBAAgB,CAAC,YAAY,CAAC,CAAC;IAC3C,QAAQ,EAAE,kBAAkB,CAAC;CAC9B,CAAC;AAEF,wBAAgB,sBAAsB,CAAC,EACrC,EAAE,EACF,UAAU,EACV,QAAQ,GACT,EAAE,oBAAoB;oCA4EH,MAAM,YACZ,MAAM,SACT;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE;;;;EAiDpD"}
|
|
@@ -116,5 +116,5 @@ export declare function createSnapshotProjectionRegistry<TState, TTable extends
|
|
|
116
116
|
} = {
|
|
117
117
|
type: string;
|
|
118
118
|
data: unknown;
|
|
119
|
-
}>(eventTypes: E["type"][], config: SnapshotProjectionConfig<TState, TTable, E>): ProjectionRegistry
|
|
119
|
+
}>(eventTypes: E["type"][], config: SnapshotProjectionConfig<TState, TTable, E>): ProjectionRegistry;
|
|
120
120
|
//# sourceMappingURL=snapshot-projection.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snapshot-projection.d.ts","sourceRoot":"","sources":["../../src/projections/snapshot-projection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,gBAAgB,EAEhB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,MAAM,MAAM,wBAAwB,CAClC,MAAM,EACN,MAAM,SAAS,MAAM,EACrB,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,IACzE;IACF;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,WAAW,EAAE,MAAM,EAAE,CAAC;IAEtB;;OAEG;IACH,WAAW,EAAE,CACX,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,EACzB,SAAS,EAAE,MAAM,KACd,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAE5B;;;OAGG;IACH,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC;IAE7D;;OAEG;IACH,YAAY,EAAE,MAAM,MAAM,CAAC;IAE3B;;;;;;;;;;;;OAYG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3D,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EACN,MAAM,SAAS,MAAM,EACrB,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,EAE3E,MAAM,EAAE,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,GAClD,iBAAiB,CAAC,gBAAgB,EAAE,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"snapshot-projection.d.ts","sourceRoot":"","sources":["../../src/projections/snapshot-projection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,gBAAgB,EAEhB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,MAAM,MAAM,wBAAwB,CAClC,MAAM,EACN,MAAM,SAAS,MAAM,EACrB,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,IACzE;IACF;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,WAAW,EAAE,MAAM,EAAE,CAAC;IAEtB;;OAEG;IACH,WAAW,EAAE,CACX,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,EACzB,SAAS,EAAE,MAAM,KACd,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAE5B;;;OAGG;IACH,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC;IAE7D;;OAEG;IACH,YAAY,EAAE,MAAM,MAAM,CAAC;IAE3B;;;;;;;;;;;;OAYG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3D,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EACN,MAAM,SAAS,MAAM,EACrB,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,EAE3E,MAAM,EAAE,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,GAClD,iBAAiB,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAkGxC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,gCAAgC,CAC9C,MAAM,EACN,MAAM,SAAS,MAAM,EACrB,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,EAE3E,UAAU,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,EACvB,MAAM,EAAE,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,GAClD,kBAAkB,CAYpB"}
|
|
@@ -34,10 +34,14 @@ export function createSnapshotProjection(config) {
|
|
|
34
34
|
return async ({ db, partition }, event) => {
|
|
35
35
|
const keys = extractKeys(event, partition);
|
|
36
36
|
// Check if event is newer than what we've already processed
|
|
37
|
-
// Note: Casting to any is necessary because Kysely cannot infer types for dynamic table names
|
|
37
|
+
// Note: Casting to `any` is necessary because Kysely cannot infer types for dynamic table names.
|
|
38
|
+
// The table name is provided at runtime, so TypeScript cannot verify the table structure at compile time.
|
|
39
|
+
// This is a known limitation when working with dynamic table names in Kysely.
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
41
|
const existing = await db
|
|
39
42
|
.selectFrom(tableName)
|
|
40
43
|
.select(["last_stream_position", "snapshot"])
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
45
|
.where((eb) => {
|
|
42
46
|
const conditions = Object.entries(keys).map(([key, value]) => eb(key, "=", value));
|
|
43
47
|
return eb.and(conditions);
|
|
@@ -86,6 +90,9 @@ export function createSnapshotProjection(config) {
|
|
|
86
90
|
}
|
|
87
91
|
}
|
|
88
92
|
await insertQuery
|
|
93
|
+
// Note: `any` is used here because the conflict builder needs to work with any table schema.
|
|
94
|
+
// The actual schema is validated at runtime through Kysely's query builder.
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
96
|
.onConflict((oc) => {
|
|
90
97
|
const conflictBuilder = oc.columns(primaryKeys);
|
|
91
98
|
return conflictBuilder.doUpdateSet(updateSet);
|
|
@@ -119,6 +126,9 @@ export function createSnapshotProjectionRegistry(eventTypes, config) {
|
|
|
119
126
|
const handler = createSnapshotProjection(config);
|
|
120
127
|
const registry = {};
|
|
121
128
|
for (const eventType of eventTypes) {
|
|
129
|
+
// Type cast is safe here because ProjectionHandler is contravariant in its event type parameter.
|
|
130
|
+
// A handler for a specific event type E can safely handle any event that matches E's structure.
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
122
132
|
registry[eventType] = [handler];
|
|
123
133
|
}
|
|
124
134
|
return registry;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import type { Kysely } from "kysely";
|
|
2
2
|
export type DatabaseExecutor<T = any> = Kysely<T>;
|
|
3
|
+
/**
|
|
4
|
+
* Flexible database type that accepts any Kysely instance.
|
|
5
|
+
* The `Kysely<any> | any` union type is intentional to work around Kysely's private field variance
|
|
6
|
+
* issue in TypeScript, allowing DatabaseExecutor from different modules to be passed.
|
|
7
|
+
*/
|
|
8
|
+
export type FlexibleDatabase = Kysely<any> | any;
|
|
3
9
|
export type Logger = {
|
|
4
10
|
info: (obj: unknown, msg?: string) => void;
|
|
5
11
|
error: (obj: unknown, msg?: string) => void;
|
|
6
12
|
warn?: (obj: unknown, msg?: string) => void;
|
|
7
13
|
debug?: (obj: unknown, msg?: string) => void;
|
|
8
14
|
};
|
|
9
|
-
export type Dependencies
|
|
10
|
-
db:
|
|
15
|
+
export type Dependencies = {
|
|
16
|
+
db: FlexibleDatabase;
|
|
11
17
|
logger: Logger;
|
|
18
|
+
/** If true, the provided db is already a transaction executor. */
|
|
19
|
+
inTransaction?: boolean;
|
|
12
20
|
};
|
|
13
21
|
export type ExtendedOptions = {
|
|
14
22
|
partition?: string;
|
|
@@ -50,17 +58,27 @@ export type ProjectionEvent<E extends {
|
|
|
50
58
|
}> = E & {
|
|
51
59
|
metadata: ProjectionEventMetadata;
|
|
52
60
|
};
|
|
53
|
-
export type ProjectionContext<T =
|
|
61
|
+
export type ProjectionContext<T = FlexibleDatabase> = {
|
|
54
62
|
db: T;
|
|
55
63
|
partition: string;
|
|
56
64
|
};
|
|
57
|
-
export type ProjectionHandler<T =
|
|
65
|
+
export type ProjectionHandler<T = FlexibleDatabase, E extends {
|
|
58
66
|
type: string;
|
|
59
67
|
data: unknown;
|
|
60
68
|
} = {
|
|
61
69
|
type: string;
|
|
62
70
|
data: unknown;
|
|
63
71
|
}> = (ctx: ProjectionContext<T>, event: ProjectionEvent<E>) => void | Promise<void>;
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
/**
|
|
73
|
+
* ProjectionRegistry maps event types to their handlers.
|
|
74
|
+
* The `any` in `ProjectionHandler<T, any>[]` is intentional - it allows handlers
|
|
75
|
+
* for different event types to be registered together, with type safety enforced
|
|
76
|
+
* at the handler level through the ProjectionHandler generic parameter.
|
|
77
|
+
* Uses FlexibleDatabase by default to work around Kysely's private field variance issue.
|
|
78
|
+
*/
|
|
79
|
+
export type ProjectionRegistry<T = FlexibleDatabase> = Record<string, ProjectionHandler<T, {
|
|
80
|
+
type: string;
|
|
81
|
+
data: unknown;
|
|
82
|
+
}>[]>;
|
|
83
|
+
export declare function createProjectionRegistry<T = FlexibleDatabase>(...registries: ProjectionRegistry<T>[]): ProjectionRegistry<T>;
|
|
66
84
|
//# 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,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,
|
|
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;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;AAEjD,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,GAAG;IACzB,EAAE,EAAE,gBAAgB,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,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;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,IAAI,CAAC,GAAG;IAC3E,QAAQ,EAAE,uBAAuB,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,iBAAiB,CAAC,CAAC,GAAG,gBAAgB,IAAI;IACpD,EAAE,EAAE,CAAC,CAAC;IACN,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,CAC3B,CAAC,GAAG,gBAAgB,EACpB,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,IACzE,CACF,GAAG,EAAE,iBAAiB,CAAC,CAAC,CAAC,EACzB,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,KACtB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,CAAC,CAAC,GAAG,gBAAgB,IAAI,MAAM,CAC3D,MAAM,EACN,iBAAiB,CAAC,CAAC,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC,EAAE,CACxD,CAAC;AAEF,wBAAgB,wBAAwB,CAAC,CAAC,GAAG,gBAAgB,EAC3D,GAAG,UAAU,EAAE,kBAAkB,CAAC,CAAC,CAAC,EAAE,GACrC,kBAAkB,CAAC,CAAC,CAAC,CAYvB"}
|
package/package.json
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.1.2",
|
|
7
7
|
"description": "Emmett Event Store with Kysely",
|
|
8
8
|
"author": "Wataru Oguchi",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
|
-
"url": "https://github.com/wataruoguchi/emmett-
|
|
12
|
+
"url": "https://github.com/wataruoguchi/emmett-libs.git"
|
|
13
13
|
},
|
|
14
|
-
"homepage": "https://github.com/wataruoguchi/emmett-
|
|
15
|
-
"bugs": "https://github.com/wataruoguchi/emmett-
|
|
14
|
+
"homepage": "https://github.com/wataruoguchi/emmett-libs",
|
|
15
|
+
"bugs": "https://github.com/wataruoguchi/emmett-libs/issues",
|
|
16
16
|
"type": "module",
|
|
17
17
|
"main": "dist/index.js",
|
|
18
18
|
"module": "dist/index.js",
|
|
@@ -30,17 +30,13 @@
|
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "rm -rf dist && tsc -p tsconfig.build.json && tsup src/index.ts src/projections/index.ts",
|
|
32
32
|
"type-check": "tsc --noEmit",
|
|
33
|
+
"tc": "npm run type-check",
|
|
33
34
|
"test": "npm run type-check && vitest run",
|
|
34
|
-
"
|
|
35
|
-
"release:dry-run": "semantic-release --dry-run"
|
|
35
|
+
"test:coverage": "vitest run --coverage"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@
|
|
39
|
-
"@semantic-release/github": "^11.0.6",
|
|
40
|
-
"@semantic-release/npm": "^12.0.2",
|
|
41
|
-
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
38
|
+
"@types/node": "^24.9.2",
|
|
42
39
|
"@vitest/coverage-v8": "^3.2.4",
|
|
43
|
-
"semantic-release": "^24.2.9",
|
|
44
40
|
"tsup": "^8.5.0",
|
|
45
41
|
"typescript": "^5.8.3",
|
|
46
42
|
"vitest": "^3.2.4"
|