@wataruoguchi/emmett-event-store-kysely 1.0.0
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 +127 -0
- package/dist/db-schema.d.ts +34 -0
- package/dist/db-schema.d.ts.map +1 -0
- package/dist/db-schema.js +3 -0
- package/dist/event-store/consumers.d.ts +23 -0
- package/dist/event-store/consumers.d.ts.map +1 -0
- package/dist/event-store/consumers.js +155 -0
- package/dist/event-store/kysely-event-store.d.ts +42 -0
- package/dist/event-store/kysely-event-store.d.ts.map +1 -0
- package/dist/event-store/kysely-event-store.js +265 -0
- package/dist/index.cjs +600 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/projections/runner.d.ts +22 -0
- package/dist/projections/runner.d.ts.map +1 -0
- package/dist/projections/runner.js +82 -0
- package/dist/projections/snapshot-projection.d.ts +120 -0
- package/dist/projections/snapshot-projection.d.ts.map +1 -0
- package/dist/projections/snapshot-projection.js +135 -0
- package/dist/types.d.ts +77 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/package.json +72 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export function createProjectionRunner({ db, readStream, registry }) {
|
|
2
|
+
async function getOrCreateCheckpoint(subscriptionId, partition) {
|
|
3
|
+
const existing = await db
|
|
4
|
+
.selectFrom("subscriptions")
|
|
5
|
+
.select([
|
|
6
|
+
"subscription_id as subscriptionId",
|
|
7
|
+
"partition",
|
|
8
|
+
"last_processed_position as lastProcessedPosition",
|
|
9
|
+
])
|
|
10
|
+
.where("subscription_id", "=", subscriptionId)
|
|
11
|
+
.where("partition", "=", partition)
|
|
12
|
+
.executeTakeFirst();
|
|
13
|
+
if (existing) {
|
|
14
|
+
const last = BigInt(String(existing
|
|
15
|
+
.lastProcessedPosition));
|
|
16
|
+
return {
|
|
17
|
+
subscriptionId,
|
|
18
|
+
partition,
|
|
19
|
+
lastProcessedPosition: last,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
await db
|
|
23
|
+
.insertInto("subscriptions")
|
|
24
|
+
.values({
|
|
25
|
+
subscription_id: subscriptionId,
|
|
26
|
+
partition,
|
|
27
|
+
version: 1,
|
|
28
|
+
last_processed_position: 0n,
|
|
29
|
+
})
|
|
30
|
+
.onConflict((oc) => oc.columns(["subscription_id", "partition", "version"]).doUpdateSet({
|
|
31
|
+
last_processed_position: (eb) => eb.ref("excluded.last_processed_position"),
|
|
32
|
+
}))
|
|
33
|
+
.execute();
|
|
34
|
+
return {
|
|
35
|
+
subscriptionId,
|
|
36
|
+
partition,
|
|
37
|
+
lastProcessedPosition: 0n,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function updateCheckpoint(subscriptionId, partition, lastProcessedPosition) {
|
|
41
|
+
await db
|
|
42
|
+
.updateTable("subscriptions")
|
|
43
|
+
.set({ last_processed_position: lastProcessedPosition })
|
|
44
|
+
.where("subscription_id", "=", subscriptionId)
|
|
45
|
+
.where("partition", "=", partition)
|
|
46
|
+
.execute();
|
|
47
|
+
}
|
|
48
|
+
async function projectEvents(subscriptionId, streamId, opts) {
|
|
49
|
+
const partition = opts?.partition ?? "default_partition";
|
|
50
|
+
const batchSize = BigInt(opts?.batchSize ?? 500);
|
|
51
|
+
const checkpoint = await getOrCreateCheckpoint(subscriptionId, partition);
|
|
52
|
+
const { events, currentStreamVersion } = await readStream(streamId, {
|
|
53
|
+
from: checkpoint.lastProcessedPosition + 1n,
|
|
54
|
+
to: checkpoint.lastProcessedPosition + batchSize,
|
|
55
|
+
partition,
|
|
56
|
+
});
|
|
57
|
+
for (const ev of events) {
|
|
58
|
+
if (!ev)
|
|
59
|
+
continue;
|
|
60
|
+
const handlers = registry[ev.type] ?? [];
|
|
61
|
+
if (handlers.length === 0) {
|
|
62
|
+
await updateCheckpoint(subscriptionId, partition, ev.metadata.streamPosition);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const projectionEvent = {
|
|
66
|
+
type: ev.type,
|
|
67
|
+
data: ev.data,
|
|
68
|
+
metadata: {
|
|
69
|
+
streamId: ev.metadata.streamId,
|
|
70
|
+
streamPosition: ev.metadata.streamPosition,
|
|
71
|
+
globalPosition: ev.metadata.globalPosition,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
for (const handler of handlers) {
|
|
75
|
+
await handler({ db, partition }, projectionEvent);
|
|
76
|
+
}
|
|
77
|
+
await updateCheckpoint(subscriptionId, partition, projectionEvent.metadata.streamPosition);
|
|
78
|
+
}
|
|
79
|
+
return { processed: events.length, currentStreamVersion };
|
|
80
|
+
}
|
|
81
|
+
return { projectEvents };
|
|
82
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { DatabaseExecutor, ProjectionEvent, ProjectionHandler, ProjectionRegistry } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Configuration for snapshot-based projections.
|
|
4
|
+
*
|
|
5
|
+
* @template TState - The aggregate state type that will be stored in the snapshot
|
|
6
|
+
* @template TTable - The table name as a string literal type
|
|
7
|
+
* @template E - The event union type (must be a discriminated union with type and data properties)
|
|
8
|
+
*/
|
|
9
|
+
export type SnapshotProjectionConfig<TState, TTable extends string, E extends {
|
|
10
|
+
type: string;
|
|
11
|
+
data: unknown;
|
|
12
|
+
} = {
|
|
13
|
+
type: string;
|
|
14
|
+
data: unknown;
|
|
15
|
+
}> = {
|
|
16
|
+
/**
|
|
17
|
+
* The name of the database table for this projection
|
|
18
|
+
*/
|
|
19
|
+
tableName: TTable;
|
|
20
|
+
/**
|
|
21
|
+
* The primary key columns that uniquely identify a row
|
|
22
|
+
* e.g., ['tenant_id', 'cart_id', 'partition']
|
|
23
|
+
*/
|
|
24
|
+
primaryKeys: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Extract primary key values from the event data
|
|
27
|
+
*/
|
|
28
|
+
extractKeys: (event: ProjectionEvent<E>, partition: string) => Record<string, string>;
|
|
29
|
+
/**
|
|
30
|
+
* The evolve function that takes current state and event, returns new state.
|
|
31
|
+
* This is the same evolve function used in the aggregate.
|
|
32
|
+
*/
|
|
33
|
+
evolve: (state: TState, event: ProjectionEvent<E>) => TState;
|
|
34
|
+
/**
|
|
35
|
+
* Initial state for the aggregate when no snapshot exists
|
|
36
|
+
*/
|
|
37
|
+
initialState: () => TState;
|
|
38
|
+
/**
|
|
39
|
+
* Optional: Map the snapshot state to individual columns for easier querying.
|
|
40
|
+
* This allows you to denormalize specific fields from the snapshot into table columns.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* mapToColumns: (state) => ({
|
|
45
|
+
* currency: state.currency,
|
|
46
|
+
* items_json: JSON.stringify(state.items),
|
|
47
|
+
* is_checked_out: state.status === 'checkedOut'
|
|
48
|
+
* })
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
mapToColumns?: (state: TState) => Record<string, unknown>;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Creates a projection handler that stores the aggregate state as a snapshot.
|
|
55
|
+
*
|
|
56
|
+
* This is a generic helper that works with any aggregate that follows the evolve pattern.
|
|
57
|
+
* Instead of manually mapping event fields to table columns, it:
|
|
58
|
+
* 1. Loads the current snapshot from the database (or starts with initial state)
|
|
59
|
+
* 2. Applies the event using the evolve function
|
|
60
|
+
* 3. Stores the new state back to the snapshot column
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const cartProjection = createSnapshotProjection({
|
|
65
|
+
* tableName: 'carts',
|
|
66
|
+
* primaryKeys: ['tenant_id', 'cart_id', 'partition'],
|
|
67
|
+
* extractKeys: (event, partition) => ({
|
|
68
|
+
* tenant_id: event.data.eventMeta.tenantId,
|
|
69
|
+
* cart_id: event.data.eventMeta.cartId,
|
|
70
|
+
* partition
|
|
71
|
+
* }),
|
|
72
|
+
* evolve: cartEvolve,
|
|
73
|
+
* initialState: () => ({ status: 'init', items: [] })
|
|
74
|
+
* });
|
|
75
|
+
*
|
|
76
|
+
* // Use it in a projection registry
|
|
77
|
+
* const registry: ProjectionRegistry = {
|
|
78
|
+
* CartCreated: [cartProjection],
|
|
79
|
+
* ItemAddedToCart: [cartProjection],
|
|
80
|
+
* // ... other events
|
|
81
|
+
* };
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export declare function createSnapshotProjection<TState, TTable extends string, E extends {
|
|
85
|
+
type: string;
|
|
86
|
+
data: unknown;
|
|
87
|
+
} = {
|
|
88
|
+
type: string;
|
|
89
|
+
data: unknown;
|
|
90
|
+
}>(config: SnapshotProjectionConfig<TState, TTable, E>): ProjectionHandler<DatabaseExecutor, E>;
|
|
91
|
+
/**
|
|
92
|
+
* Creates multiple projection handlers that all use the same snapshot projection logic.
|
|
93
|
+
* This is a convenience function to avoid repeating the same handler for multiple event types.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```typescript
|
|
97
|
+
* const registry = createSnapshotProjectionRegistry(
|
|
98
|
+
* ['CartCreated', 'ItemAddedToCart', 'ItemRemovedFromCart'],
|
|
99
|
+
* {
|
|
100
|
+
* tableName: 'carts',
|
|
101
|
+
* primaryKeys: ['tenant_id', 'cart_id', 'partition'],
|
|
102
|
+
* extractKeys: (event, partition) => ({
|
|
103
|
+
* tenant_id: event.data.eventMeta.tenantId,
|
|
104
|
+
* cart_id: event.data.eventMeta.cartId,
|
|
105
|
+
* partition
|
|
106
|
+
* }),
|
|
107
|
+
* evolve: cartEvolve,
|
|
108
|
+
* initialState: () => ({ status: 'init', items: [] })
|
|
109
|
+
* }
|
|
110
|
+
* );
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export declare function createSnapshotProjectionRegistry<TState, TTable extends string, E extends {
|
|
114
|
+
type: string;
|
|
115
|
+
data: unknown;
|
|
116
|
+
} = {
|
|
117
|
+
type: string;
|
|
118
|
+
data: unknown;
|
|
119
|
+
}>(eventTypes: E["type"][], config: SnapshotProjectionConfig<TState, TTable, E>): ProjectionRegistry<DatabaseExecutor>;
|
|
120
|
+
//# sourceMappingURL=snapshot-projection.d.ts.map
|
|
@@ -0,0 +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,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,CAAC,gBAAgB,CAAC,CAYtC"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a projection handler that stores the aggregate state as a snapshot.
|
|
3
|
+
*
|
|
4
|
+
* This is a generic helper that works with any aggregate that follows the evolve pattern.
|
|
5
|
+
* Instead of manually mapping event fields to table columns, it:
|
|
6
|
+
* 1. Loads the current snapshot from the database (or starts with initial state)
|
|
7
|
+
* 2. Applies the event using the evolve function
|
|
8
|
+
* 3. Stores the new state back to the snapshot column
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const cartProjection = createSnapshotProjection({
|
|
13
|
+
* tableName: 'carts',
|
|
14
|
+
* primaryKeys: ['tenant_id', 'cart_id', 'partition'],
|
|
15
|
+
* extractKeys: (event, partition) => ({
|
|
16
|
+
* tenant_id: event.data.eventMeta.tenantId,
|
|
17
|
+
* cart_id: event.data.eventMeta.cartId,
|
|
18
|
+
* partition
|
|
19
|
+
* }),
|
|
20
|
+
* evolve: cartEvolve,
|
|
21
|
+
* initialState: () => ({ status: 'init', items: [] })
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Use it in a projection registry
|
|
25
|
+
* const registry: ProjectionRegistry = {
|
|
26
|
+
* CartCreated: [cartProjection],
|
|
27
|
+
* ItemAddedToCart: [cartProjection],
|
|
28
|
+
* // ... other events
|
|
29
|
+
* };
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function createSnapshotProjection(config) {
|
|
33
|
+
const { tableName, primaryKeys, extractKeys, evolve, initialState, mapToColumns, } = config;
|
|
34
|
+
return async ({ db, partition }, event) => {
|
|
35
|
+
const keys = extractKeys(event, partition);
|
|
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.
|
|
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
|
|
41
|
+
const existing = await db
|
|
42
|
+
.selectFrom(tableName)
|
|
43
|
+
.select(["last_stream_position", "snapshot"])
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
.where((eb) => {
|
|
46
|
+
const conditions = Object.entries(keys).map(([key, value]) => eb(key, "=", value));
|
|
47
|
+
return eb.and(conditions);
|
|
48
|
+
})
|
|
49
|
+
.executeTakeFirst();
|
|
50
|
+
const lastPos = existing?.last_stream_position
|
|
51
|
+
? BigInt(String(existing.last_stream_position))
|
|
52
|
+
: -1n;
|
|
53
|
+
// Skip if we've already processed a newer event
|
|
54
|
+
if (event.metadata.streamPosition <= lastPos) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Load current state from snapshot or use initial state
|
|
58
|
+
// Note: snapshot is stored as JSONB and Kysely returns it as parsed JSON
|
|
59
|
+
const currentState = existing?.snapshot
|
|
60
|
+
? existing.snapshot
|
|
61
|
+
: initialState();
|
|
62
|
+
// Apply the event to get new state
|
|
63
|
+
const newState = evolve(currentState, event);
|
|
64
|
+
// Prepare the row data with snapshot
|
|
65
|
+
const rowData = {
|
|
66
|
+
...keys,
|
|
67
|
+
snapshot: JSON.stringify(newState),
|
|
68
|
+
stream_id: event.metadata.streamId,
|
|
69
|
+
last_stream_position: event.metadata.streamPosition.toString(),
|
|
70
|
+
last_global_position: event.metadata.globalPosition.toString(),
|
|
71
|
+
};
|
|
72
|
+
// If mapToColumns is provided, add the denormalized columns
|
|
73
|
+
if (mapToColumns) {
|
|
74
|
+
const columns = mapToColumns(newState);
|
|
75
|
+
Object.assign(rowData, columns);
|
|
76
|
+
}
|
|
77
|
+
// Upsert the snapshot
|
|
78
|
+
const insertQuery = db.insertInto(tableName).values(rowData);
|
|
79
|
+
const updateSet = {
|
|
80
|
+
snapshot: (eb) => eb.ref("excluded.snapshot"),
|
|
81
|
+
stream_id: (eb) => eb.ref("excluded.stream_id"),
|
|
82
|
+
last_stream_position: (eb) => eb.ref("excluded.last_stream_position"),
|
|
83
|
+
last_global_position: (eb) => eb.ref("excluded.last_global_position"),
|
|
84
|
+
};
|
|
85
|
+
// If mapToColumns is provided, also update the denormalized columns
|
|
86
|
+
if (mapToColumns) {
|
|
87
|
+
const columns = mapToColumns(newState);
|
|
88
|
+
for (const columnName of Object.keys(columns)) {
|
|
89
|
+
updateSet[columnName] = (eb) => eb.ref(`excluded.${columnName}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
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
|
|
96
|
+
.onConflict((oc) => {
|
|
97
|
+
const conflictBuilder = oc.columns(primaryKeys);
|
|
98
|
+
return conflictBuilder.doUpdateSet(updateSet);
|
|
99
|
+
})
|
|
100
|
+
.execute();
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Creates multiple projection handlers that all use the same snapshot projection logic.
|
|
105
|
+
* This is a convenience function to avoid repeating the same handler for multiple event types.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* const registry = createSnapshotProjectionRegistry(
|
|
110
|
+
* ['CartCreated', 'ItemAddedToCart', 'ItemRemovedFromCart'],
|
|
111
|
+
* {
|
|
112
|
+
* tableName: 'carts',
|
|
113
|
+
* primaryKeys: ['tenant_id', 'cart_id', 'partition'],
|
|
114
|
+
* extractKeys: (event, partition) => ({
|
|
115
|
+
* tenant_id: event.data.eventMeta.tenantId,
|
|
116
|
+
* cart_id: event.data.eventMeta.cartId,
|
|
117
|
+
* partition
|
|
118
|
+
* }),
|
|
119
|
+
* evolve: cartEvolve,
|
|
120
|
+
* initialState: () => ({ status: 'init', items: [] })
|
|
121
|
+
* }
|
|
122
|
+
* );
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export function createSnapshotProjectionRegistry(eventTypes, config) {
|
|
126
|
+
const handler = createSnapshotProjection(config);
|
|
127
|
+
const registry = {};
|
|
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
|
|
132
|
+
registry[eventType] = [handler];
|
|
133
|
+
}
|
|
134
|
+
return registry;
|
|
135
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Kysely } from "kysely";
|
|
2
|
+
export type DatabaseExecutor<T = any> = Kysely<T>;
|
|
3
|
+
export type Logger = {
|
|
4
|
+
info: (obj: unknown, msg?: string) => void;
|
|
5
|
+
error: (obj: unknown, msg?: string) => void;
|
|
6
|
+
warn?: (obj: unknown, msg?: string) => void;
|
|
7
|
+
debug?: (obj: unknown, msg?: string) => void;
|
|
8
|
+
};
|
|
9
|
+
export type Dependencies<T = any> = {
|
|
10
|
+
db: DatabaseExecutor<T>;
|
|
11
|
+
logger: Logger;
|
|
12
|
+
/** If true, the provided db is already a transaction executor. */
|
|
13
|
+
inTransaction?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export type ExtendedOptions = {
|
|
16
|
+
partition?: string;
|
|
17
|
+
streamType?: string;
|
|
18
|
+
};
|
|
19
|
+
export declare const PostgreSQLEventStoreDefaultStreamVersion = 0n;
|
|
20
|
+
export declare const DEFAULT_PARTITION: "default_partition";
|
|
21
|
+
export type ProjectionEventMetadata = {
|
|
22
|
+
streamId: string;
|
|
23
|
+
streamPosition: bigint;
|
|
24
|
+
globalPosition: bigint;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* ProjectionEvent that preserves discriminated union relationships.
|
|
28
|
+
*
|
|
29
|
+
* Instead of independent EventType and EventData generics, this accepts a union type
|
|
30
|
+
* where each variant has a specific type-data pairing. This allows TypeScript to
|
|
31
|
+
* properly narrow the data type when you narrow the event type.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* type MyEvent =
|
|
36
|
+
* | { type: "Created"; data: { id: string } }
|
|
37
|
+
* | { type: "Updated"; data: { name: string } };
|
|
38
|
+
*
|
|
39
|
+
* type MyProjectionEvent = ProjectionEvent<MyEvent>;
|
|
40
|
+
*
|
|
41
|
+
* function handle(event: MyProjectionEvent) {
|
|
42
|
+
* if (event.type === "Created") {
|
|
43
|
+
* // TypeScript knows event.data is { id: string }
|
|
44
|
+
* console.log(event.data.id);
|
|
45
|
+
* }
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export type ProjectionEvent<E extends {
|
|
50
|
+
type: string;
|
|
51
|
+
data: unknown;
|
|
52
|
+
}> = E & {
|
|
53
|
+
metadata: ProjectionEventMetadata;
|
|
54
|
+
};
|
|
55
|
+
export type ProjectionContext<T = DatabaseExecutor<any>> = {
|
|
56
|
+
db: T;
|
|
57
|
+
partition: string;
|
|
58
|
+
};
|
|
59
|
+
export type ProjectionHandler<T = DatabaseExecutor<any>, E extends {
|
|
60
|
+
type: string;
|
|
61
|
+
data: unknown;
|
|
62
|
+
} = {
|
|
63
|
+
type: string;
|
|
64
|
+
data: unknown;
|
|
65
|
+
}> = (ctx: ProjectionContext<T>, event: ProjectionEvent<E>) => void | Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* ProjectionRegistry maps event types to their handlers.
|
|
68
|
+
* The `any` in `ProjectionHandler<T, any>[]` is intentional - it allows handlers
|
|
69
|
+
* for different event types to be registered together, with type safety enforced
|
|
70
|
+
* at the handler level through the ProjectionHandler generic parameter.
|
|
71
|
+
*/
|
|
72
|
+
export type ProjectionRegistry<T = DatabaseExecutor<any>> = Record<string, ProjectionHandler<T, {
|
|
73
|
+
type: string;
|
|
74
|
+
data: unknown;
|
|
75
|
+
}>[]>;
|
|
76
|
+
export declare function createProjectionRegistry<T = DatabaseExecutor<any>>(...registries: ProjectionRegistry<T>[]): ProjectionRegistry<T>;
|
|
77
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +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,CAAC,CAAC,GAAG,GAAG,IAAI;IAClC,EAAE,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IACxB,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,CAAC,GAAG,CAAC,IAAI;IACzD,EAAE,EAAE,CAAC,CAAC;IACN,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,CAC3B,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,EACzB,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;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,CAAC,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,IAAI,MAAM,CAChE,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,CAAC,GAAG,CAAC,EAChE,GAAG,UAAU,EAAE,kBAAkB,CAAC,CAAC,CAAC,EAAE,GACrC,kBAAkB,CAAC,CAAC,CAAC,CAYvB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const PostgreSQLEventStoreDefaultStreamVersion = 0n;
|
|
2
|
+
export const DEFAULT_PARTITION = "default_partition";
|
|
3
|
+
export function createProjectionRegistry(...registries) {
|
|
4
|
+
const combined = {};
|
|
5
|
+
/**
|
|
6
|
+
* This is necessary because the projection runner can be used to project events from multiple partitions.
|
|
7
|
+
* e.g., the generators-read-model projection runner can be used to project events for partition A, partition B, and partition C.
|
|
8
|
+
*/
|
|
9
|
+
for (const reg of registries) {
|
|
10
|
+
for (const [eventType, handlers] of Object.entries(reg)) {
|
|
11
|
+
combined[eventType] = [...(combined[eventType] ?? []), ...handlers];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return combined;
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wataruoguchi/emmett-event-store-kysely",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"description": "Emmett Event Store with Kysely",
|
|
8
|
+
"author": "Wataru Oguchi",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/wataruoguchi/emmett-libs.git"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/wataruoguchi/emmett-libs",
|
|
15
|
+
"bugs": "https://github.com/wataruoguchi/emmett-libs/issues",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "dist/index.js",
|
|
18
|
+
"module": "dist/index.js",
|
|
19
|
+
"types": "dist/types.d.ts",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"require": "./dist/index.cjs"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && tsup src/index.ts src/projections/index.ts",
|
|
32
|
+
"type-check": "tsc --noEmit",
|
|
33
|
+
"tc": "npm run type-check",
|
|
34
|
+
"test": "npm run type-check && vitest run",
|
|
35
|
+
"release": "semantic-release",
|
|
36
|
+
"release:dry-run": "semantic-release --dry-run"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
40
|
+
"@semantic-release/github": "^11.0.6",
|
|
41
|
+
"@semantic-release/npm": "^12.0.2",
|
|
42
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
43
|
+
"conventional-changelog-conventionalcommits": "^9.0.0",
|
|
44
|
+
"@types/node": "^24.9.2",
|
|
45
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
46
|
+
"semantic-release": "^24.2.9",
|
|
47
|
+
"semantic-release-monorepo": "^8.0.2",
|
|
48
|
+
"tsup": "^8.5.0",
|
|
49
|
+
"typescript": "^5.8.3",
|
|
50
|
+
"vitest": "^3.2.4"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@event-driven-io/emmett": "0",
|
|
54
|
+
"kysely": "0"
|
|
55
|
+
},
|
|
56
|
+
"optionalDependencies": {
|
|
57
|
+
"@rollup/rollup-linux-x64-gnu": "4.9.5"
|
|
58
|
+
},
|
|
59
|
+
"keywords": [
|
|
60
|
+
"emmett",
|
|
61
|
+
"event-store",
|
|
62
|
+
"kysely",
|
|
63
|
+
"postgres",
|
|
64
|
+
"postgresql",
|
|
65
|
+
"Event Sourcing",
|
|
66
|
+
"event-sourcing",
|
|
67
|
+
"read-model",
|
|
68
|
+
"read-models",
|
|
69
|
+
"read-models-projection",
|
|
70
|
+
"read-models-projections"
|
|
71
|
+
]
|
|
72
|
+
}
|