@saga-bus/store-sqlite 0.1.5

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 ADDED
@@ -0,0 +1,67 @@
1
+ # @saga-bus/store-sqlite
2
+
3
+ SQLite saga store for saga-bus - perfect for local development and testing.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @saga-bus/store-sqlite better-sqlite3
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import Database from "better-sqlite3";
15
+ import { SqliteSagaStore, createSchema } from "@saga-bus/store-sqlite";
16
+ import { createBus } from "@saga-bus/core";
17
+
18
+ // Create database (use ':memory:' for in-memory or a file path)
19
+ const db = new Database(":memory:");
20
+
21
+ // Initialize schema (run once)
22
+ createSchema(db);
23
+
24
+ // Create store
25
+ const store = new SqliteSagaStore({ db });
26
+
27
+ // Use with saga-bus
28
+ const bus = createBus({
29
+ sagas: [{ definition: mySaga, store }],
30
+ transport,
31
+ });
32
+
33
+ await bus.start();
34
+ ```
35
+
36
+ ## Features
37
+
38
+ - **Zero Docker required** - perfect for local development and testing
39
+ - **In-memory mode** - use `':memory:'` for fast unit tests
40
+ - **File-based** - persist to disk for development
41
+ - **Synchronous operations** - uses better-sqlite3 for maximum performance
42
+ - **Optimistic concurrency** - version-based conflict detection
43
+
44
+ ## API
45
+
46
+ ### `SqliteSagaStore`
47
+
48
+ ```typescript
49
+ const store = new SqliteSagaStore({
50
+ db: Database, // better-sqlite3 database instance
51
+ tableName?: string, // Table name (default: 'saga_states')
52
+ });
53
+ ```
54
+
55
+ ### `createSchema`
56
+
57
+ Creates the required table in the database:
58
+
59
+ ```typescript
60
+ createSchema(db);
61
+ // Or with custom table name:
62
+ createSchema(db, "my_saga_states");
63
+ ```
64
+
65
+ ## License
66
+
67
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ SqliteSagaStore: () => SqliteSagaStore,
24
+ createSchema: () => createSchema
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+ var import_core = require("@saga-bus/core");
28
+ var SqliteSagaStore = class {
29
+ db;
30
+ tableName;
31
+ statements;
32
+ constructor(options) {
33
+ this.db = options.db;
34
+ this.tableName = options.tableName ?? "saga_states";
35
+ this.statements = {
36
+ getById: this.db.prepare(`
37
+ SELECT saga_name, saga_id, correlation_id, state, version, created_at, updated_at
38
+ FROM ${this.tableName}
39
+ WHERE saga_name = ? AND saga_id = ?
40
+ `),
41
+ getByCorrelationId: this.db.prepare(`
42
+ SELECT saga_name, saga_id, correlation_id, state, version, created_at, updated_at
43
+ FROM ${this.tableName}
44
+ WHERE saga_name = ? AND correlation_id = ?
45
+ `),
46
+ insert: this.db.prepare(`
47
+ INSERT INTO ${this.tableName} (saga_name, saga_id, correlation_id, state, version, created_at, updated_at)
48
+ VALUES (?, ?, ?, ?, ?, ?, ?)
49
+ `),
50
+ update: this.db.prepare(`
51
+ UPDATE ${this.tableName}
52
+ SET state = ?, version = ?, updated_at = ?
53
+ WHERE saga_name = ? AND saga_id = ? AND version = ?
54
+ `),
55
+ delete: this.db.prepare(`
56
+ DELETE FROM ${this.tableName}
57
+ WHERE saga_name = ? AND saga_id = ?
58
+ `)
59
+ };
60
+ }
61
+ parseRow(row) {
62
+ return JSON.parse(row.state);
63
+ }
64
+ async getById(sagaName, sagaId) {
65
+ const row = this.statements.getById.get(sagaName, sagaId);
66
+ if (!row) {
67
+ return null;
68
+ }
69
+ return this.parseRow(row);
70
+ }
71
+ async getByCorrelationId(sagaName, correlationId) {
72
+ const row = this.statements.getByCorrelationId.get(
73
+ sagaName,
74
+ correlationId
75
+ );
76
+ if (!row) {
77
+ return null;
78
+ }
79
+ return this.parseRow(row);
80
+ }
81
+ async insert(sagaName, correlationId, state) {
82
+ const { sagaId, version, createdAt, updatedAt } = state.metadata;
83
+ const stateJson = JSON.stringify(state);
84
+ try {
85
+ this.statements.insert.run(
86
+ sagaName,
87
+ sagaId,
88
+ correlationId,
89
+ stateJson,
90
+ version,
91
+ createdAt.toISOString(),
92
+ updatedAt.toISOString()
93
+ );
94
+ } catch (error) {
95
+ if (error instanceof Error && error.message.includes("UNIQUE constraint failed")) {
96
+ throw new Error(`Saga ${sagaId} already exists`);
97
+ }
98
+ throw error;
99
+ }
100
+ }
101
+ async update(sagaName, state, expectedVersion) {
102
+ const { sagaId, version, updatedAt } = state.metadata;
103
+ const stateJson = JSON.stringify(state);
104
+ const result = this.statements.update.run(
105
+ stateJson,
106
+ version,
107
+ updatedAt.toISOString(),
108
+ sagaName,
109
+ sagaId,
110
+ expectedVersion
111
+ );
112
+ if (result.changes === 0) {
113
+ const existing = await this.getById(sagaName, sagaId);
114
+ if (!existing) {
115
+ throw new Error(`Saga ${sagaId} not found`);
116
+ }
117
+ throw new import_core.ConcurrencyError(
118
+ sagaId,
119
+ expectedVersion,
120
+ existing.metadata.version
121
+ );
122
+ }
123
+ }
124
+ async delete(sagaName, sagaId) {
125
+ this.statements.delete.run(sagaName, sagaId);
126
+ }
127
+ };
128
+ function createSchema(db, tableName = "saga_states") {
129
+ db.exec(`
130
+ CREATE TABLE IF NOT EXISTS ${tableName} (
131
+ saga_name TEXT NOT NULL,
132
+ saga_id TEXT NOT NULL,
133
+ correlation_id TEXT NOT NULL,
134
+ state TEXT NOT NULL,
135
+ version INTEGER NOT NULL,
136
+ created_at TEXT NOT NULL,
137
+ updated_at TEXT NOT NULL,
138
+ PRIMARY KEY (saga_name, saga_id)
139
+ );
140
+
141
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_correlation
142
+ ON ${tableName} (saga_name, correlation_id);
143
+
144
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_updated_at
145
+ ON ${tableName} (updated_at);
146
+ `);
147
+ }
148
+ // Annotate the CommonJS export names for ESM import in node:
149
+ 0 && (module.exports = {
150
+ SqliteSagaStore,
151
+ createSchema
152
+ });
153
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { SagaStore, SagaState } from \"@saga-bus/core\";\nimport { ConcurrencyError } from \"@saga-bus/core\";\nimport type Database from \"better-sqlite3\";\n\nexport interface SqliteSagaStoreOptions {\n /** better-sqlite3 database instance */\n db: Database.Database;\n /** Table name for saga states (default: 'saga_states') */\n tableName?: string;\n}\n\ninterface StoredSaga {\n saga_name: string;\n saga_id: string;\n correlation_id: string;\n state: string;\n version: number;\n created_at: string;\n updated_at: string;\n}\n\n/**\n * SQLite saga store - perfect for local development and testing.\n *\n * @example\n * ```typescript\n * import Database from 'better-sqlite3';\n * import { SqliteSagaStore, createSchema } from '@saga-bus/store-sqlite';\n *\n * const db = new Database(':memory:'); // or 'path/to/db.sqlite'\n * createSchema(db);\n *\n * const store = new SqliteSagaStore({ db });\n * ```\n */\nexport class SqliteSagaStore<TState extends SagaState>\n implements SagaStore<TState>\n{\n private readonly db: Database.Database;\n private readonly tableName: string;\n private readonly statements: {\n getById: Database.Statement;\n getByCorrelationId: Database.Statement;\n insert: Database.Statement;\n update: Database.Statement;\n delete: Database.Statement;\n };\n\n constructor(options: SqliteSagaStoreOptions) {\n this.db = options.db;\n this.tableName = options.tableName ?? \"saga_states\";\n\n // Prepare statements for better performance\n this.statements = {\n getById: this.db.prepare(`\n SELECT saga_name, saga_id, correlation_id, state, version, created_at, updated_at\n FROM ${this.tableName}\n WHERE saga_name = ? AND saga_id = ?\n `),\n getByCorrelationId: this.db.prepare(`\n SELECT saga_name, saga_id, correlation_id, state, version, created_at, updated_at\n FROM ${this.tableName}\n WHERE saga_name = ? AND correlation_id = ?\n `),\n insert: this.db.prepare(`\n INSERT INTO ${this.tableName} (saga_name, saga_id, correlation_id, state, version, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `),\n update: this.db.prepare(`\n UPDATE ${this.tableName}\n SET state = ?, version = ?, updated_at = ?\n WHERE saga_name = ? AND saga_id = ? AND version = ?\n `),\n delete: this.db.prepare(`\n DELETE FROM ${this.tableName}\n WHERE saga_name = ? AND saga_id = ?\n `),\n };\n }\n\n private parseRow(row: StoredSaga): TState {\n return JSON.parse(row.state) as TState;\n }\n\n async getById(sagaName: string, sagaId: string): Promise<TState | null> {\n const row = this.statements.getById.get(sagaName, sagaId) as\n | StoredSaga\n | undefined;\n\n if (!row) {\n return null;\n }\n\n return this.parseRow(row);\n }\n\n async getByCorrelationId(\n sagaName: string,\n correlationId: string\n ): Promise<TState | null> {\n const row = this.statements.getByCorrelationId.get(\n sagaName,\n correlationId\n ) as StoredSaga | undefined;\n\n if (!row) {\n return null;\n }\n\n return this.parseRow(row);\n }\n\n async insert(\n sagaName: string,\n correlationId: string,\n state: TState\n ): Promise<void> {\n const { sagaId, version, createdAt, updatedAt } = state.metadata;\n const stateJson = JSON.stringify(state);\n\n try {\n this.statements.insert.run(\n sagaName,\n sagaId,\n correlationId,\n stateJson,\n version,\n createdAt.toISOString(),\n updatedAt.toISOString()\n );\n } catch (error: unknown) {\n if (\n error instanceof Error &&\n error.message.includes(\"UNIQUE constraint failed\")\n ) {\n throw new Error(`Saga ${sagaId} already exists`);\n }\n throw error;\n }\n }\n\n async update(\n sagaName: string,\n state: TState,\n expectedVersion: number\n ): Promise<void> {\n const { sagaId, version, updatedAt } = state.metadata;\n const stateJson = JSON.stringify(state);\n\n const result = this.statements.update.run(\n stateJson,\n version,\n updatedAt.toISOString(),\n sagaName,\n sagaId,\n expectedVersion\n );\n\n if (result.changes === 0) {\n // Either saga doesn't exist or version mismatch\n const existing = await this.getById(sagaName, sagaId);\n\n if (!existing) {\n throw new Error(`Saga ${sagaId} not found`);\n }\n\n throw new ConcurrencyError(\n sagaId,\n expectedVersion,\n existing.metadata.version\n );\n }\n }\n\n async delete(sagaName: string, sagaId: string): Promise<void> {\n this.statements.delete.run(sagaName, sagaId);\n }\n}\n\n/**\n * Create the saga_states table in the SQLite database.\n *\n * @example\n * ```typescript\n * import Database from 'better-sqlite3';\n * import { createSchema } from '@saga-bus/store-sqlite';\n *\n * const db = new Database('sagas.db');\n * createSchema(db);\n * ```\n */\nexport function createSchema(\n db: Database.Database,\n tableName: string = \"saga_states\"\n): void {\n db.exec(`\n CREATE TABLE IF NOT EXISTS ${tableName} (\n saga_name TEXT NOT NULL,\n saga_id TEXT NOT NULL,\n correlation_id TEXT NOT NULL,\n state TEXT NOT NULL,\n version INTEGER NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (saga_name, saga_id)\n );\n\n CREATE INDEX IF NOT EXISTS idx_${tableName}_correlation\n ON ${tableName} (saga_name, correlation_id);\n\n CREATE INDEX IF NOT EXISTS idx_${tableName}_updated_at\n ON ${tableName} (updated_at);\n `);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,50 @@
1
+ import { SagaState, SagaStore } from '@saga-bus/core';
2
+ import Database from 'better-sqlite3';
3
+
4
+ interface SqliteSagaStoreOptions {
5
+ /** better-sqlite3 database instance */
6
+ db: Database.Database;
7
+ /** Table name for saga states (default: 'saga_states') */
8
+ tableName?: string;
9
+ }
10
+ /**
11
+ * SQLite saga store - perfect for local development and testing.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import Database from 'better-sqlite3';
16
+ * import { SqliteSagaStore, createSchema } from '@saga-bus/store-sqlite';
17
+ *
18
+ * const db = new Database(':memory:'); // or 'path/to/db.sqlite'
19
+ * createSchema(db);
20
+ *
21
+ * const store = new SqliteSagaStore({ db });
22
+ * ```
23
+ */
24
+ declare class SqliteSagaStore<TState extends SagaState> implements SagaStore<TState> {
25
+ private readonly db;
26
+ private readonly tableName;
27
+ private readonly statements;
28
+ constructor(options: SqliteSagaStoreOptions);
29
+ private parseRow;
30
+ getById(sagaName: string, sagaId: string): Promise<TState | null>;
31
+ getByCorrelationId(sagaName: string, correlationId: string): Promise<TState | null>;
32
+ insert(sagaName: string, correlationId: string, state: TState): Promise<void>;
33
+ update(sagaName: string, state: TState, expectedVersion: number): Promise<void>;
34
+ delete(sagaName: string, sagaId: string): Promise<void>;
35
+ }
36
+ /**
37
+ * Create the saga_states table in the SQLite database.
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * import Database from 'better-sqlite3';
42
+ * import { createSchema } from '@saga-bus/store-sqlite';
43
+ *
44
+ * const db = new Database('sagas.db');
45
+ * createSchema(db);
46
+ * ```
47
+ */
48
+ declare function createSchema(db: Database.Database, tableName?: string): void;
49
+
50
+ export { SqliteSagaStore, type SqliteSagaStoreOptions, createSchema };
@@ -0,0 +1,50 @@
1
+ import { SagaState, SagaStore } from '@saga-bus/core';
2
+ import Database from 'better-sqlite3';
3
+
4
+ interface SqliteSagaStoreOptions {
5
+ /** better-sqlite3 database instance */
6
+ db: Database.Database;
7
+ /** Table name for saga states (default: 'saga_states') */
8
+ tableName?: string;
9
+ }
10
+ /**
11
+ * SQLite saga store - perfect for local development and testing.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import Database from 'better-sqlite3';
16
+ * import { SqliteSagaStore, createSchema } from '@saga-bus/store-sqlite';
17
+ *
18
+ * const db = new Database(':memory:'); // or 'path/to/db.sqlite'
19
+ * createSchema(db);
20
+ *
21
+ * const store = new SqliteSagaStore({ db });
22
+ * ```
23
+ */
24
+ declare class SqliteSagaStore<TState extends SagaState> implements SagaStore<TState> {
25
+ private readonly db;
26
+ private readonly tableName;
27
+ private readonly statements;
28
+ constructor(options: SqliteSagaStoreOptions);
29
+ private parseRow;
30
+ getById(sagaName: string, sagaId: string): Promise<TState | null>;
31
+ getByCorrelationId(sagaName: string, correlationId: string): Promise<TState | null>;
32
+ insert(sagaName: string, correlationId: string, state: TState): Promise<void>;
33
+ update(sagaName: string, state: TState, expectedVersion: number): Promise<void>;
34
+ delete(sagaName: string, sagaId: string): Promise<void>;
35
+ }
36
+ /**
37
+ * Create the saga_states table in the SQLite database.
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * import Database from 'better-sqlite3';
42
+ * import { createSchema } from '@saga-bus/store-sqlite';
43
+ *
44
+ * const db = new Database('sagas.db');
45
+ * createSchema(db);
46
+ * ```
47
+ */
48
+ declare function createSchema(db: Database.Database, tableName?: string): void;
49
+
50
+ export { SqliteSagaStore, type SqliteSagaStoreOptions, createSchema };
package/dist/index.js ADDED
@@ -0,0 +1,127 @@
1
+ // src/index.ts
2
+ import { ConcurrencyError } from "@saga-bus/core";
3
+ var SqliteSagaStore = class {
4
+ db;
5
+ tableName;
6
+ statements;
7
+ constructor(options) {
8
+ this.db = options.db;
9
+ this.tableName = options.tableName ?? "saga_states";
10
+ this.statements = {
11
+ getById: this.db.prepare(`
12
+ SELECT saga_name, saga_id, correlation_id, state, version, created_at, updated_at
13
+ FROM ${this.tableName}
14
+ WHERE saga_name = ? AND saga_id = ?
15
+ `),
16
+ getByCorrelationId: this.db.prepare(`
17
+ SELECT saga_name, saga_id, correlation_id, state, version, created_at, updated_at
18
+ FROM ${this.tableName}
19
+ WHERE saga_name = ? AND correlation_id = ?
20
+ `),
21
+ insert: this.db.prepare(`
22
+ INSERT INTO ${this.tableName} (saga_name, saga_id, correlation_id, state, version, created_at, updated_at)
23
+ VALUES (?, ?, ?, ?, ?, ?, ?)
24
+ `),
25
+ update: this.db.prepare(`
26
+ UPDATE ${this.tableName}
27
+ SET state = ?, version = ?, updated_at = ?
28
+ WHERE saga_name = ? AND saga_id = ? AND version = ?
29
+ `),
30
+ delete: this.db.prepare(`
31
+ DELETE FROM ${this.tableName}
32
+ WHERE saga_name = ? AND saga_id = ?
33
+ `)
34
+ };
35
+ }
36
+ parseRow(row) {
37
+ return JSON.parse(row.state);
38
+ }
39
+ async getById(sagaName, sagaId) {
40
+ const row = this.statements.getById.get(sagaName, sagaId);
41
+ if (!row) {
42
+ return null;
43
+ }
44
+ return this.parseRow(row);
45
+ }
46
+ async getByCorrelationId(sagaName, correlationId) {
47
+ const row = this.statements.getByCorrelationId.get(
48
+ sagaName,
49
+ correlationId
50
+ );
51
+ if (!row) {
52
+ return null;
53
+ }
54
+ return this.parseRow(row);
55
+ }
56
+ async insert(sagaName, correlationId, state) {
57
+ const { sagaId, version, createdAt, updatedAt } = state.metadata;
58
+ const stateJson = JSON.stringify(state);
59
+ try {
60
+ this.statements.insert.run(
61
+ sagaName,
62
+ sagaId,
63
+ correlationId,
64
+ stateJson,
65
+ version,
66
+ createdAt.toISOString(),
67
+ updatedAt.toISOString()
68
+ );
69
+ } catch (error) {
70
+ if (error instanceof Error && error.message.includes("UNIQUE constraint failed")) {
71
+ throw new Error(`Saga ${sagaId} already exists`);
72
+ }
73
+ throw error;
74
+ }
75
+ }
76
+ async update(sagaName, state, expectedVersion) {
77
+ const { sagaId, version, updatedAt } = state.metadata;
78
+ const stateJson = JSON.stringify(state);
79
+ const result = this.statements.update.run(
80
+ stateJson,
81
+ version,
82
+ updatedAt.toISOString(),
83
+ sagaName,
84
+ sagaId,
85
+ expectedVersion
86
+ );
87
+ if (result.changes === 0) {
88
+ const existing = await this.getById(sagaName, sagaId);
89
+ if (!existing) {
90
+ throw new Error(`Saga ${sagaId} not found`);
91
+ }
92
+ throw new ConcurrencyError(
93
+ sagaId,
94
+ expectedVersion,
95
+ existing.metadata.version
96
+ );
97
+ }
98
+ }
99
+ async delete(sagaName, sagaId) {
100
+ this.statements.delete.run(sagaName, sagaId);
101
+ }
102
+ };
103
+ function createSchema(db, tableName = "saga_states") {
104
+ db.exec(`
105
+ CREATE TABLE IF NOT EXISTS ${tableName} (
106
+ saga_name TEXT NOT NULL,
107
+ saga_id TEXT NOT NULL,
108
+ correlation_id TEXT NOT NULL,
109
+ state TEXT NOT NULL,
110
+ version INTEGER NOT NULL,
111
+ created_at TEXT NOT NULL,
112
+ updated_at TEXT NOT NULL,
113
+ PRIMARY KEY (saga_name, saga_id)
114
+ );
115
+
116
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_correlation
117
+ ON ${tableName} (saga_name, correlation_id);
118
+
119
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_updated_at
120
+ ON ${tableName} (updated_at);
121
+ `);
122
+ }
123
+ export {
124
+ SqliteSagaStore,
125
+ createSchema
126
+ };
127
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { SagaStore, SagaState } from \"@saga-bus/core\";\nimport { ConcurrencyError } from \"@saga-bus/core\";\nimport type Database from \"better-sqlite3\";\n\nexport interface SqliteSagaStoreOptions {\n /** better-sqlite3 database instance */\n db: Database.Database;\n /** Table name for saga states (default: 'saga_states') */\n tableName?: string;\n}\n\ninterface StoredSaga {\n saga_name: string;\n saga_id: string;\n correlation_id: string;\n state: string;\n version: number;\n created_at: string;\n updated_at: string;\n}\n\n/**\n * SQLite saga store - perfect for local development and testing.\n *\n * @example\n * ```typescript\n * import Database from 'better-sqlite3';\n * import { SqliteSagaStore, createSchema } from '@saga-bus/store-sqlite';\n *\n * const db = new Database(':memory:'); // or 'path/to/db.sqlite'\n * createSchema(db);\n *\n * const store = new SqliteSagaStore({ db });\n * ```\n */\nexport class SqliteSagaStore<TState extends SagaState>\n implements SagaStore<TState>\n{\n private readonly db: Database.Database;\n private readonly tableName: string;\n private readonly statements: {\n getById: Database.Statement;\n getByCorrelationId: Database.Statement;\n insert: Database.Statement;\n update: Database.Statement;\n delete: Database.Statement;\n };\n\n constructor(options: SqliteSagaStoreOptions) {\n this.db = options.db;\n this.tableName = options.tableName ?? \"saga_states\";\n\n // Prepare statements for better performance\n this.statements = {\n getById: this.db.prepare(`\n SELECT saga_name, saga_id, correlation_id, state, version, created_at, updated_at\n FROM ${this.tableName}\n WHERE saga_name = ? AND saga_id = ?\n `),\n getByCorrelationId: this.db.prepare(`\n SELECT saga_name, saga_id, correlation_id, state, version, created_at, updated_at\n FROM ${this.tableName}\n WHERE saga_name = ? AND correlation_id = ?\n `),\n insert: this.db.prepare(`\n INSERT INTO ${this.tableName} (saga_name, saga_id, correlation_id, state, version, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `),\n update: this.db.prepare(`\n UPDATE ${this.tableName}\n SET state = ?, version = ?, updated_at = ?\n WHERE saga_name = ? AND saga_id = ? AND version = ?\n `),\n delete: this.db.prepare(`\n DELETE FROM ${this.tableName}\n WHERE saga_name = ? AND saga_id = ?\n `),\n };\n }\n\n private parseRow(row: StoredSaga): TState {\n return JSON.parse(row.state) as TState;\n }\n\n async getById(sagaName: string, sagaId: string): Promise<TState | null> {\n const row = this.statements.getById.get(sagaName, sagaId) as\n | StoredSaga\n | undefined;\n\n if (!row) {\n return null;\n }\n\n return this.parseRow(row);\n }\n\n async getByCorrelationId(\n sagaName: string,\n correlationId: string\n ): Promise<TState | null> {\n const row = this.statements.getByCorrelationId.get(\n sagaName,\n correlationId\n ) as StoredSaga | undefined;\n\n if (!row) {\n return null;\n }\n\n return this.parseRow(row);\n }\n\n async insert(\n sagaName: string,\n correlationId: string,\n state: TState\n ): Promise<void> {\n const { sagaId, version, createdAt, updatedAt } = state.metadata;\n const stateJson = JSON.stringify(state);\n\n try {\n this.statements.insert.run(\n sagaName,\n sagaId,\n correlationId,\n stateJson,\n version,\n createdAt.toISOString(),\n updatedAt.toISOString()\n );\n } catch (error: unknown) {\n if (\n error instanceof Error &&\n error.message.includes(\"UNIQUE constraint failed\")\n ) {\n throw new Error(`Saga ${sagaId} already exists`);\n }\n throw error;\n }\n }\n\n async update(\n sagaName: string,\n state: TState,\n expectedVersion: number\n ): Promise<void> {\n const { sagaId, version, updatedAt } = state.metadata;\n const stateJson = JSON.stringify(state);\n\n const result = this.statements.update.run(\n stateJson,\n version,\n updatedAt.toISOString(),\n sagaName,\n sagaId,\n expectedVersion\n );\n\n if (result.changes === 0) {\n // Either saga doesn't exist or version mismatch\n const existing = await this.getById(sagaName, sagaId);\n\n if (!existing) {\n throw new Error(`Saga ${sagaId} not found`);\n }\n\n throw new ConcurrencyError(\n sagaId,\n expectedVersion,\n existing.metadata.version\n );\n }\n }\n\n async delete(sagaName: string, sagaId: string): Promise<void> {\n this.statements.delete.run(sagaName, sagaId);\n }\n}\n\n/**\n * Create the saga_states table in the SQLite database.\n *\n * @example\n * ```typescript\n * import Database from 'better-sqlite3';\n * import { createSchema } from '@saga-bus/store-sqlite';\n *\n * const db = new Database('sagas.db');\n * createSchema(db);\n * ```\n */\nexport function createSchema(\n db: Database.Database,\n tableName: string = \"saga_states\"\n): void {\n db.exec(`\n CREATE TABLE IF NOT EXISTS ${tableName} (\n saga_name TEXT NOT NULL,\n saga_id TEXT NOT NULL,\n correlation_id TEXT NOT NULL,\n state TEXT NOT NULL,\n version INTEGER NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (saga_name, saga_id)\n );\n\n CREATE INDEX IF NOT EXISTS idx_${tableName}_correlation\n ON ${tableName} (saga_name, correlation_id);\n\n CREATE INDEX IF NOT EXISTS idx_${tableName}_updated_at\n ON ${tableName} (updated_at);\n `);\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@saga-bus/store-sqlite",
3
+ "version": "0.1.5",
4
+ "description": "SQLite saga store for saga-bus - perfect for local development and testing",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsup",
22
+ "dev": "tsup --watch",
23
+ "lint": "eslint src/",
24
+ "check-types": "tsc --noEmit",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest"
27
+ },
28
+ "dependencies": {
29
+ "@saga-bus/core": "workspace:*"
30
+ },
31
+ "peerDependencies": {
32
+ "better-sqlite3": ">=9.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@repo/eslint-config": "workspace:*",
36
+ "@repo/typescript-config": "workspace:*",
37
+ "@types/better-sqlite3": "^7.6.8",
38
+ "@types/node": "^22.10.1",
39
+ "better-sqlite3": "^11.0.0",
40
+ "eslint": "^9.16.0",
41
+ "tsup": "^8.3.5",
42
+ "typescript": "^5.7.2",
43
+ "vitest": "^3.0.0"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "license": "MIT",
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "https://github.com/d-e-a-n-f/saga-bus.git",
52
+ "directory": "packages/store-sqlite"
53
+ },
54
+ "bugs": {
55
+ "url": "https://github.com/d-e-a-n-f/saga-bus/issues"
56
+ },
57
+ "homepage": "https://github.com/d-e-a-n-f/saga-bus#readme",
58
+ "keywords": [
59
+ "saga-bus",
60
+ "saga",
61
+ "sqlite",
62
+ "store",
63
+ "state-machine",
64
+ "local-development"
65
+ ]
66
+ }