@wataruoguchi/emmett-event-store-kysely 1.1.3 → 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.
Files changed (36) hide show
  1. package/README.md +76 -164
  2. package/dist/event-store/consumers.d.ts +23 -0
  3. package/dist/event-store/consumers.d.ts.map +1 -0
  4. package/dist/event-store/consumers.js +155 -0
  5. package/dist/event-store/kysely-event-store.d.ts +42 -0
  6. package/dist/event-store/kysely-event-store.d.ts.map +1 -0
  7. package/dist/event-store/kysely-event-store.js +256 -0
  8. package/dist/index.cjs +584 -0
  9. package/dist/index.d.ts +10 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +5 -0
  12. package/dist/projections/runner.d.ts +3 -2
  13. package/dist/projections/runner.d.ts.map +1 -1
  14. package/dist/projections/snapshot-projection.d.ts +120 -0
  15. package/dist/projections/snapshot-projection.d.ts.map +1 -0
  16. package/dist/projections/snapshot-projection.js +125 -0
  17. package/dist/types.d.ts +33 -4
  18. package/dist/types.d.ts.map +1 -1
  19. package/package.json +9 -14
  20. package/dist/event-store/aggregate-stream.d.ts +0 -10
  21. package/dist/event-store/aggregate-stream.d.ts.map +0 -1
  22. package/dist/event-store/aggregate-stream.js +0 -18
  23. package/dist/event-store/append-to-stream.d.ts +0 -7
  24. package/dist/event-store/append-to-stream.d.ts.map +0 -1
  25. package/dist/event-store/append-to-stream.js +0 -143
  26. package/dist/event-store/index.cjs +0 -294
  27. package/dist/event-store/index.d.ts +0 -13
  28. package/dist/event-store/index.d.ts.map +0 -1
  29. package/dist/event-store/index.js +0 -10
  30. package/dist/event-store/read-stream.d.ts +0 -14
  31. package/dist/event-store/read-stream.d.ts.map +0 -1
  32. package/dist/event-store/read-stream.js +0 -88
  33. package/dist/projections/index.cjs +0 -124
  34. package/dist/projections/index.d.ts +0 -4
  35. package/dist/projections/index.d.ts.map +0 -1
  36. 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
@@ -21,17 +21,46 @@ export type ProjectionEventMetadata = {
21
21
  streamPosition: bigint;
22
22
  globalPosition: bigint;
23
23
  };
24
- export type ProjectionEvent = {
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 {
25
48
  type: string;
26
49
  data: unknown;
50
+ }> = E & {
27
51
  metadata: ProjectionEventMetadata;
28
52
  };
29
53
  export type ProjectionContext<T = DatabaseExecutor<any>> = {
30
54
  db: T;
31
55
  partition: string;
32
56
  };
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>[]>;
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>[]>;
35
65
  export declare function createProjectionRegistry<T = DatabaseExecutor<any>>(...registries: ProjectionRegistry<T>[]): ProjectionRegistry<T>;
36
- export type { ReadStream } from "./event-store/read-stream.js";
37
66
  //# sourceMappingURL=types.d.ts.map
@@ -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,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"}
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": "1.1.3",
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/event-store/index.js",
18
- "module": "dist/event-store/index.js",
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/event-store/index.d.ts",
26
- "import": "./dist/event-store/index.js",
27
- "require": "./dist/event-store/index.cjs"
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/event-store/index.ts src/projections/index.ts",
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.5",
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
- }