@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 +67 -0
- package/dist/index.cjs +153 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +50 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +127 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
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":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|