@wataruoguchi/emmett-event-store-kysely 1.1.2 → 2.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 +76 -164
- 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 +256 -0
- package/dist/index.cjs +584 -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 +3 -2
- package/dist/projections/runner.d.ts.map +1 -1
- 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 +125 -0
- package/dist/types.d.ts +39 -11
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -14
- package/dist/event-store/aggregate-stream.d.ts +0 -10
- package/dist/event-store/aggregate-stream.d.ts.map +0 -1
- package/dist/event-store/aggregate-stream.js +0 -18
- package/dist/event-store/append-to-stream.d.ts +0 -7
- package/dist/event-store/append-to-stream.d.ts.map +0 -1
- package/dist/event-store/append-to-stream.js +0 -143
- package/dist/event-store/index.cjs +0 -291
- package/dist/event-store/index.d.ts +0 -13
- package/dist/event-store/index.d.ts.map +0 -1
- package/dist/event-store/index.js +0 -10
- package/dist/event-store/read-stream.d.ts +0 -14
- package/dist/event-store/read-stream.d.ts.map +0 -1
- package/dist/event-store/read-stream.js +0 -88
- package/dist/projections/index.cjs +0 -124
- package/dist/projections/index.d.ts +0 -4
- package/dist/projections/index.d.ts.map +0 -1
- package/dist/projections/index.js +0 -2
|
@@ -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,CA2FxC;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,CAStC"}
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
const existing = await db
|
|
39
|
+
.selectFrom(tableName)
|
|
40
|
+
.select(["last_stream_position", "snapshot"])
|
|
41
|
+
.where((eb) => {
|
|
42
|
+
const conditions = Object.entries(keys).map(([key, value]) => eb(key, "=", value));
|
|
43
|
+
return eb.and(conditions);
|
|
44
|
+
})
|
|
45
|
+
.executeTakeFirst();
|
|
46
|
+
const lastPos = existing?.last_stream_position
|
|
47
|
+
? BigInt(String(existing.last_stream_position))
|
|
48
|
+
: -1n;
|
|
49
|
+
// Skip if we've already processed a newer event
|
|
50
|
+
if (event.metadata.streamPosition <= lastPos) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Load current state from snapshot or use initial state
|
|
54
|
+
// Note: snapshot is stored as JSONB and Kysely returns it as parsed JSON
|
|
55
|
+
const currentState = existing?.snapshot
|
|
56
|
+
? existing.snapshot
|
|
57
|
+
: initialState();
|
|
58
|
+
// Apply the event to get new state
|
|
59
|
+
const newState = evolve(currentState, event);
|
|
60
|
+
// Prepare the row data with snapshot
|
|
61
|
+
const rowData = {
|
|
62
|
+
...keys,
|
|
63
|
+
snapshot: JSON.stringify(newState),
|
|
64
|
+
stream_id: event.metadata.streamId,
|
|
65
|
+
last_stream_position: event.metadata.streamPosition.toString(),
|
|
66
|
+
last_global_position: event.metadata.globalPosition.toString(),
|
|
67
|
+
};
|
|
68
|
+
// If mapToColumns is provided, add the denormalized columns
|
|
69
|
+
if (mapToColumns) {
|
|
70
|
+
const columns = mapToColumns(newState);
|
|
71
|
+
Object.assign(rowData, columns);
|
|
72
|
+
}
|
|
73
|
+
// Upsert the snapshot
|
|
74
|
+
const insertQuery = db.insertInto(tableName).values(rowData);
|
|
75
|
+
const updateSet = {
|
|
76
|
+
snapshot: (eb) => eb.ref("excluded.snapshot"),
|
|
77
|
+
stream_id: (eb) => eb.ref("excluded.stream_id"),
|
|
78
|
+
last_stream_position: (eb) => eb.ref("excluded.last_stream_position"),
|
|
79
|
+
last_global_position: (eb) => eb.ref("excluded.last_global_position"),
|
|
80
|
+
};
|
|
81
|
+
// If mapToColumns is provided, also update the denormalized columns
|
|
82
|
+
if (mapToColumns) {
|
|
83
|
+
const columns = mapToColumns(newState);
|
|
84
|
+
for (const columnName of Object.keys(columns)) {
|
|
85
|
+
updateSet[columnName] = (eb) => eb.ref(`excluded.${columnName}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
await insertQuery
|
|
89
|
+
.onConflict((oc) => {
|
|
90
|
+
const conflictBuilder = oc.columns(primaryKeys);
|
|
91
|
+
return conflictBuilder.doUpdateSet(updateSet);
|
|
92
|
+
})
|
|
93
|
+
.execute();
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Creates multiple projection handlers that all use the same snapshot projection logic.
|
|
98
|
+
* This is a convenience function to avoid repeating the same handler for multiple event types.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* const registry = createSnapshotProjectionRegistry(
|
|
103
|
+
* ['CartCreated', 'ItemAddedToCart', 'ItemRemovedFromCart'],
|
|
104
|
+
* {
|
|
105
|
+
* tableName: 'carts',
|
|
106
|
+
* primaryKeys: ['tenant_id', 'cart_id', 'partition'],
|
|
107
|
+
* extractKeys: (event, partition) => ({
|
|
108
|
+
* tenant_id: event.data.eventMeta.tenantId,
|
|
109
|
+
* cart_id: event.data.eventMeta.cartId,
|
|
110
|
+
* partition
|
|
111
|
+
* }),
|
|
112
|
+
* evolve: cartEvolve,
|
|
113
|
+
* initialState: () => ({ status: 'init', items: [] })
|
|
114
|
+
* }
|
|
115
|
+
* );
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export function createSnapshotProjectionRegistry(eventTypes, config) {
|
|
119
|
+
const handler = createSnapshotProjection(config);
|
|
120
|
+
const registry = {};
|
|
121
|
+
for (const eventType of eventTypes) {
|
|
122
|
+
registry[eventType] = [handler];
|
|
123
|
+
}
|
|
124
|
+
return registry;
|
|
125
|
+
}
|
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 = {
|
|
@@ -22,17 +21,46 @@ export type ProjectionEventMetadata = {
|
|
|
22
21
|
streamPosition: bigint;
|
|
23
22
|
globalPosition: bigint;
|
|
24
23
|
};
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* ProjectionEvent that preserves discriminated union relationships.
|
|
26
|
+
*
|
|
27
|
+
* Instead of independent EventType and EventData generics, this accepts a union type
|
|
28
|
+
* where each variant has a specific type-data pairing. This allows TypeScript to
|
|
29
|
+
* properly narrow the data type when you narrow the event type.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* type MyEvent =
|
|
34
|
+
* | { type: "Created"; data: { id: string } }
|
|
35
|
+
* | { type: "Updated"; data: { name: string } };
|
|
36
|
+
*
|
|
37
|
+
* type MyProjectionEvent = ProjectionEvent<MyEvent>;
|
|
38
|
+
*
|
|
39
|
+
* function handle(event: MyProjectionEvent) {
|
|
40
|
+
* if (event.type === "Created") {
|
|
41
|
+
* // TypeScript knows event.data is { id: string }
|
|
42
|
+
* console.log(event.data.id);
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export type ProjectionEvent<E extends {
|
|
26
48
|
type: string;
|
|
27
49
|
data: unknown;
|
|
50
|
+
}> = E & {
|
|
28
51
|
metadata: ProjectionEventMetadata;
|
|
29
52
|
};
|
|
30
|
-
export type ProjectionContext<T = DatabaseExecutor
|
|
53
|
+
export type ProjectionContext<T = DatabaseExecutor<any>> = {
|
|
31
54
|
db: T;
|
|
32
55
|
partition: string;
|
|
33
56
|
};
|
|
34
|
-
export type ProjectionHandler<T = DatabaseExecutor
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
export type ProjectionHandler<T = DatabaseExecutor<any>, E extends {
|
|
58
|
+
type: string;
|
|
59
|
+
data: unknown;
|
|
60
|
+
} = {
|
|
61
|
+
type: string;
|
|
62
|
+
data: unknown;
|
|
63
|
+
}> = (ctx: ProjectionContext<T>, event: ProjectionEvent<E>) => void | Promise<void>;
|
|
64
|
+
export type ProjectionRegistry<T = DatabaseExecutor<any>> = Record<string, ProjectionHandler<T, any>[]>;
|
|
65
|
+
export declare function createProjectionRegistry<T = DatabaseExecutor<any>>(...registries: ProjectionRegistry<T>[]): ProjectionRegistry<T>;
|
|
38
66
|
//# 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;;;;;;;;;;;;;;;;;;;;;;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,MAAM,MAAM,kBAAkB,CAAC,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,IAAI,MAAM,CAChE,MAAM,EACN,iBAAiB,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAC5B,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/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "
|
|
6
|
+
"version": "2.0.0",
|
|
7
7
|
"description": "Emmett Event Store with Kysely",
|
|
8
8
|
"author": "Wataru Oguchi",
|
|
9
9
|
"license": "MIT",
|
|
@@ -14,22 +14,17 @@
|
|
|
14
14
|
"homepage": "https://github.com/wataruoguchi/poc-emmett",
|
|
15
15
|
"bugs": "https://github.com/wataruoguchi/poc-emmett/issues",
|
|
16
16
|
"type": "module",
|
|
17
|
-
"main": "dist/
|
|
18
|
-
"module": "dist/
|
|
17
|
+
"main": "dist/index.js",
|
|
18
|
+
"module": "dist/index.js",
|
|
19
19
|
"types": "dist/types.d.ts",
|
|
20
20
|
"files": [
|
|
21
21
|
"dist"
|
|
22
22
|
],
|
|
23
23
|
"exports": {
|
|
24
24
|
".": {
|
|
25
|
-
"types": "./dist/
|
|
26
|
-
"import": "./dist/
|
|
27
|
-
"require": "./dist/
|
|
28
|
-
},
|
|
29
|
-
"./event-store": {
|
|
30
|
-
"types": "./dist/event-store/index.d.ts",
|
|
31
|
-
"import": "./dist/event-store/index.js",
|
|
32
|
-
"require": "./dist/event-store/index.cjs"
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"require": "./dist/index.cjs"
|
|
33
28
|
},
|
|
34
29
|
"./projections": {
|
|
35
30
|
"types": "./dist/projections/index.d.ts",
|
|
@@ -38,9 +33,9 @@
|
|
|
38
33
|
}
|
|
39
34
|
},
|
|
40
35
|
"scripts": {
|
|
41
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json && tsup src/
|
|
36
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && tsup src/index.ts src/projections/index.ts",
|
|
42
37
|
"type-check": "tsc --noEmit",
|
|
43
|
-
"test": "vitest run",
|
|
38
|
+
"test": "npm run type-check && vitest run",
|
|
44
39
|
"release": "semantic-release",
|
|
45
40
|
"release:dry-run": "semantic-release --dry-run"
|
|
46
41
|
},
|
|
@@ -56,7 +51,7 @@
|
|
|
56
51
|
"vitest": "^3.2.4"
|
|
57
52
|
},
|
|
58
53
|
"peerDependencies": {
|
|
59
|
-
"@event-driven-io/emmett": "^0.38.
|
|
54
|
+
"@event-driven-io/emmett": "^0.38.6",
|
|
60
55
|
"kysely": "^0.28.7"
|
|
61
56
|
},
|
|
62
57
|
"optionalDependencies": {
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { type AggregateStreamOptions, type AggregateStreamResult, type Event, type ReadEventMetadataWithGlobalPosition } from "@event-driven-io/emmett";
|
|
2
|
-
import { type Dependencies } from "../types.js";
|
|
3
|
-
import type { ReadStream } from "./read-stream.js";
|
|
4
|
-
type PostgresReadEventMetadata = ReadEventMetadataWithGlobalPosition;
|
|
5
|
-
export type AggregateStream = <State, EventType extends Event>(streamId: string, options: AggregateStreamOptions<State, EventType, PostgresReadEventMetadata>) => Promise<AggregateStreamResult<State>>;
|
|
6
|
-
export declare function createAggregateStream({ readStream }: {
|
|
7
|
-
readStream: ReadStream;
|
|
8
|
-
}, { logger }: Dependencies): AggregateStream;
|
|
9
|
-
export {};
|
|
10
|
-
//# sourceMappingURL=aggregate-stream.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"aggregate-stream.d.ts","sourceRoot":"","sources":["../../src/event-store/aggregate-stream.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,KAAK,EACV,KAAK,mCAAmC,EACzC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAEL,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,KAAK,yBAAyB,GAAG,mCAAmC,CAAC;AAErE,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,SAAS,SAAS,KAAK,EAC3D,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,sBAAsB,CAAC,KAAK,EAAE,SAAS,EAAE,yBAAyB,CAAC,KACzE,OAAO,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAAC;AAE3C,wBAAgB,qBAAqB,CACnC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,UAAU,CAAA;CAAE,EAC1C,EAAE,MAAM,EAAE,EAAE,YAAY,GACvB,eAAe,CAgCjB"}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// biome-ignore assist/source/organizeImports: retain import order similar to app code
|
|
2
|
-
import { assertExpectedVersionMatchesCurrent, } from "@event-driven-io/emmett";
|
|
3
|
-
import { PostgreSQLEventStoreDefaultStreamVersion, } from "../types.js";
|
|
4
|
-
export function createAggregateStream({ readStream }, { logger }) {
|
|
5
|
-
return async function aggregateStream(streamId, options) {
|
|
6
|
-
const { evolve, initialState, read } = options;
|
|
7
|
-
logger.info({ streamId, options }, "aggregateStream");
|
|
8
|
-
const expectedStreamVersion = read?.expectedStreamVersion;
|
|
9
|
-
const result = await readStream(streamId, options.read);
|
|
10
|
-
assertExpectedVersionMatchesCurrent(result.currentStreamVersion, expectedStreamVersion, PostgreSQLEventStoreDefaultStreamVersion);
|
|
11
|
-
const state = result.events.reduce((state, event) => (event ? evolve(state, event) : state), initialState());
|
|
12
|
-
return {
|
|
13
|
-
state,
|
|
14
|
-
currentStreamVersion: result.currentStreamVersion,
|
|
15
|
-
streamExists: result.streamExists,
|
|
16
|
-
};
|
|
17
|
-
};
|
|
18
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { type AppendToStreamOptions, type AppendToStreamResultWithGlobalPosition, type Event } from "@event-driven-io/emmett";
|
|
2
|
-
import { type Dependencies, type ExtendedOptions } from "../types.js";
|
|
3
|
-
type ExtendedAppendToStreamOptions = AppendToStreamOptions & ExtendedOptions;
|
|
4
|
-
export type AppendToStream = <EventType extends Event>(streamId: string, events: EventType[], options?: ExtendedAppendToStreamOptions) => Promise<AppendToStreamResultWithGlobalPosition>;
|
|
5
|
-
export declare function createAppendToStream({ db, logger, }: Dependencies): AppendToStream;
|
|
6
|
-
export {};
|
|
7
|
-
//# sourceMappingURL=append-to-stream.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"append-to-stream.d.ts","sourceRoot":"","sources":["../../src/event-store/append-to-stream.ts"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,qBAAqB,EAC1B,KAAK,sCAAsC,EAC3C,KAAK,KAAK,EACX,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAIrB,KAAK,6BAA6B,GAAG,qBAAqB,GAAG,eAAe,CAAC;AAC7E,MAAM,MAAM,cAAc,GAAG,CAAC,SAAS,SAAS,KAAK,EACnD,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,SAAS,EAAE,EACnB,OAAO,CAAC,EAAE,6BAA6B,KACpC,OAAO,CAAC,sCAAsC,CAAC,CAAC;AAErD,wBAAgB,oBAAoB,CAAC,EACnC,EAAE,EACF,MAAM,GACP,EAAE,YAAY,GAAG,cAAc,CA6D/B"}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
// biome-ignore assist/source/organizeImports: retain import order similar to app code
|
|
2
|
-
import { ExpectedVersionConflictError, NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, STREAM_EXISTS, } from "@event-driven-io/emmett";
|
|
3
|
-
import { DEFAULT_PARTITION, PostgreSQLEventStoreDefaultStreamVersion, } from "../types.js";
|
|
4
|
-
const PostgreSQLEventStoreDefaultGlobalPosition = 0n;
|
|
5
|
-
export function createAppendToStream({ db, logger, }) {
|
|
6
|
-
return async function appendToStream(streamId, events, options) {
|
|
7
|
-
const streamType = getStreamType(options);
|
|
8
|
-
const partition = getPartition(options);
|
|
9
|
-
const expected = options?.expectedStreamVersion;
|
|
10
|
-
logger.info({ streamId, events, options, partition }, "appendToStream");
|
|
11
|
-
ensureEventsNotEmpty(events, expected);
|
|
12
|
-
const result = await db
|
|
13
|
-
.transaction()
|
|
14
|
-
.execute(async (trx) => {
|
|
15
|
-
const { currentStreamVersion, streamExists } = await fetchStreamInfo(trx, streamId, partition);
|
|
16
|
-
assertExpectedVersion(expected, currentStreamVersion, streamExists);
|
|
17
|
-
const basePos = currentStreamVersion;
|
|
18
|
-
const nextStreamPosition = computeNextStreamPosition(basePos, events.length);
|
|
19
|
-
await upsertStreamRow(trx, streamId, partition, streamType, basePos, nextStreamPosition, expected, streamExists);
|
|
20
|
-
const messagesToInsert = buildMessagesToInsert(events, basePos, streamId, partition);
|
|
21
|
-
const lastEventGlobalPosition = await insertMessagesAndGetLastGlobalPosition(trx, messagesToInsert);
|
|
22
|
-
return {
|
|
23
|
-
nextExpectedStreamVersion: nextStreamPosition,
|
|
24
|
-
lastEventGlobalPosition,
|
|
25
|
-
createdNewStream: !streamExists,
|
|
26
|
-
};
|
|
27
|
-
});
|
|
28
|
-
return result;
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
function getStreamType(options) {
|
|
32
|
-
return options?.streamType ?? "unknown";
|
|
33
|
-
}
|
|
34
|
-
function ensureEventsNotEmpty(events, expected) {
|
|
35
|
-
if (events.length === 0) {
|
|
36
|
-
throw new ExpectedVersionConflictError(-1n, expected ?? NO_CONCURRENCY_CHECK);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function assertExpectedVersion(expected, currentPos, streamExistsNow) {
|
|
40
|
-
if (expected === STREAM_EXISTS && !streamExistsNow) {
|
|
41
|
-
throw new ExpectedVersionConflictError(-1n, STREAM_EXISTS);
|
|
42
|
-
}
|
|
43
|
-
if (expected === STREAM_DOES_NOT_EXIST && streamExistsNow) {
|
|
44
|
-
throw new ExpectedVersionConflictError(-1n, STREAM_DOES_NOT_EXIST);
|
|
45
|
-
}
|
|
46
|
-
if (typeof expected === "bigint" && expected !== currentPos) {
|
|
47
|
-
throw new ExpectedVersionConflictError(currentPos, expected);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
function computeNextStreamPosition(basePos, eventCount) {
|
|
51
|
-
return basePos + BigInt(eventCount);
|
|
52
|
-
}
|
|
53
|
-
async function upsertStreamRow(executor, streamId, partition, streamType, basePos, nextStreamPosition, expected, streamExistsNow) {
|
|
54
|
-
if (!streamExistsNow) {
|
|
55
|
-
await executor
|
|
56
|
-
.insertInto("streams")
|
|
57
|
-
.values({
|
|
58
|
-
stream_id: streamId,
|
|
59
|
-
stream_position: nextStreamPosition,
|
|
60
|
-
partition,
|
|
61
|
-
stream_type: streamType,
|
|
62
|
-
stream_metadata: {},
|
|
63
|
-
is_archived: false,
|
|
64
|
-
})
|
|
65
|
-
.execute();
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (typeof expected === "bigint") {
|
|
69
|
-
const updatedRow = await executor
|
|
70
|
-
.updateTable("streams")
|
|
71
|
-
.set({ stream_position: nextStreamPosition })
|
|
72
|
-
.where("stream_id", "=", streamId)
|
|
73
|
-
.where("partition", "=", partition)
|
|
74
|
-
.where("is_archived", "=", false)
|
|
75
|
-
.where("stream_position", "=", basePos)
|
|
76
|
-
.returning("stream_position")
|
|
77
|
-
.executeTakeFirst();
|
|
78
|
-
if (!updatedRow) {
|
|
79
|
-
throw new ExpectedVersionConflictError(basePos, expected);
|
|
80
|
-
}
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
await executor
|
|
84
|
-
.updateTable("streams")
|
|
85
|
-
.set({ stream_position: nextStreamPosition })
|
|
86
|
-
.where("stream_id", "=", streamId)
|
|
87
|
-
.where("partition", "=", partition)
|
|
88
|
-
.where("is_archived", "=", false)
|
|
89
|
-
.execute();
|
|
90
|
-
}
|
|
91
|
-
function buildMessagesToInsert(events, basePos, streamId, partition) {
|
|
92
|
-
return events.map((e, index) => {
|
|
93
|
-
const messageId = crypto.randomUUID();
|
|
94
|
-
const streamPosition = basePos + BigInt(index + 1);
|
|
95
|
-
const rawMeta = "metadata" in e ? e.metadata : undefined;
|
|
96
|
-
const eventMeta = rawMeta && typeof rawMeta === "object" ? rawMeta : {};
|
|
97
|
-
const messageMetadata = {
|
|
98
|
-
messageId,
|
|
99
|
-
...eventMeta,
|
|
100
|
-
};
|
|
101
|
-
return {
|
|
102
|
-
stream_id: streamId,
|
|
103
|
-
stream_position: streamPosition,
|
|
104
|
-
partition,
|
|
105
|
-
message_data: e.data,
|
|
106
|
-
message_metadata: messageMetadata,
|
|
107
|
-
message_schema_version: index.toString(),
|
|
108
|
-
message_type: e.type,
|
|
109
|
-
message_kind: "E",
|
|
110
|
-
message_id: messageId,
|
|
111
|
-
is_archived: false,
|
|
112
|
-
created: new Date(),
|
|
113
|
-
};
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
async function insertMessagesAndGetLastGlobalPosition(executor, messagesToInsert) {
|
|
117
|
-
const inserted = await executor
|
|
118
|
-
.insertInto("messages")
|
|
119
|
-
.values(messagesToInsert)
|
|
120
|
-
.returning("global_position")
|
|
121
|
-
.execute();
|
|
122
|
-
if (!inserted || (Array.isArray(inserted) && inserted.length === 0)) {
|
|
123
|
-
return PostgreSQLEventStoreDefaultGlobalPosition;
|
|
124
|
-
}
|
|
125
|
-
const globalPositions = inserted.map((r) => BigInt(String(r.global_position)));
|
|
126
|
-
return globalPositions[globalPositions.length - 1];
|
|
127
|
-
}
|
|
128
|
-
function getPartition(options) {
|
|
129
|
-
return options?.partition ?? DEFAULT_PARTITION;
|
|
130
|
-
}
|
|
131
|
-
async function fetchStreamInfo(executor, streamId, partition) {
|
|
132
|
-
const streamRow = await executor
|
|
133
|
-
.selectFrom("streams")
|
|
134
|
-
.select(["stream_position"])
|
|
135
|
-
.where("stream_id", "=", streamId)
|
|
136
|
-
.where("partition", "=", partition)
|
|
137
|
-
.where("is_archived", "=", false)
|
|
138
|
-
.executeTakeFirst();
|
|
139
|
-
const currentStreamVersion = streamRow
|
|
140
|
-
? BigInt(String(streamRow.stream_position))
|
|
141
|
-
: PostgreSQLEventStoreDefaultStreamVersion;
|
|
142
|
-
return { currentStreamVersion, streamExists: !!streamRow };
|
|
143
|
-
}
|