@saga-bus/store-sqlserver 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Dean Foran
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,264 @@
1
+ # @saga-bus/store-sqlserver
2
+
3
+ SQL Server / Azure SQL Database saga store for saga-bus.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @saga-bus/store-sqlserver mssql
9
+ # or
10
+ pnpm add @saga-bus/store-sqlserver mssql
11
+ ```
12
+
13
+ ## Features
14
+
15
+ - **SQL Server 2016+**: Full support for modern SQL Server versions
16
+ - **Azure SQL Database**: Works with Azure SQL and Azure SQL Managed Instance
17
+ - **Optimistic Concurrency**: Version-based conflict detection
18
+ - **Schema Support**: Use custom schemas (default: "dbo")
19
+ - **Connection Pooling**: Built-in connection pool management
20
+ - **Query Helpers**: Find, count, and cleanup methods
21
+
22
+ ## Quick Start
23
+
24
+ ```typescript
25
+ import { createBus } from "@saga-bus/core";
26
+ import { SqlServerSagaStore } from "@saga-bus/store-sqlserver";
27
+
28
+ const store = new SqlServerSagaStore({
29
+ pool: {
30
+ server: "localhost",
31
+ database: "sagas",
32
+ user: "sa",
33
+ password: "YourPassword123!",
34
+ options: {
35
+ trustServerCertificate: true, // For local development
36
+ },
37
+ },
38
+ });
39
+
40
+ await store.initialize();
41
+
42
+ const bus = createBus({
43
+ store,
44
+ // ... other config
45
+ });
46
+
47
+ await bus.start();
48
+ ```
49
+
50
+ ## Configuration
51
+
52
+ ```typescript
53
+ interface SqlServerSagaStoreOptions {
54
+ /** Connection pool or pool configuration */
55
+ pool: ConnectionPool | config;
56
+
57
+ /** Table name for saga instances (default: "saga_instances") */
58
+ tableName?: string;
59
+
60
+ /** Schema name (default: "dbo") */
61
+ schema?: string;
62
+ }
63
+ ```
64
+
65
+ ## Database Schema
66
+
67
+ Create the required table:
68
+
69
+ ```sql
70
+ CREATE TABLE [dbo].[saga_instances] (
71
+ id NVARCHAR(128) NOT NULL,
72
+ saga_name NVARCHAR(128) NOT NULL,
73
+ correlation_id NVARCHAR(256) NOT NULL,
74
+ version INT NOT NULL,
75
+ is_completed BIT NOT NULL DEFAULT 0,
76
+ state NVARCHAR(MAX) NOT NULL,
77
+ created_at DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
78
+ updated_at DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
79
+
80
+ CONSTRAINT PK_saga_instances PRIMARY KEY (saga_name, id)
81
+ );
82
+
83
+ CREATE UNIQUE INDEX IX_saga_correlation
84
+ ON [dbo].[saga_instances] (saga_name, correlation_id);
85
+
86
+ CREATE INDEX IX_saga_cleanup
87
+ ON [dbo].[saga_instances] (saga_name, is_completed, updated_at)
88
+ WHERE is_completed = 1;
89
+ ```
90
+
91
+ ## Examples
92
+
93
+ ### Basic Usage
94
+
95
+ ```typescript
96
+ import { SqlServerSagaStore } from "@saga-bus/store-sqlserver";
97
+
98
+ const store = new SqlServerSagaStore({
99
+ pool: {
100
+ server: "localhost",
101
+ database: "sagas",
102
+ user: "sa",
103
+ password: "password",
104
+ },
105
+ });
106
+
107
+ await store.initialize();
108
+
109
+ // Find by saga ID
110
+ const state = await store.getById("OrderSaga", "saga-123");
111
+
112
+ // Find by correlation ID
113
+ const stateByCorr = await store.getByCorrelationId("OrderSaga", "order-456");
114
+
115
+ // Insert new saga
116
+ await store.insert("OrderSaga", "order-789", {
117
+ orderId: "order-789",
118
+ status: "pending",
119
+ metadata: {
120
+ sagaId: "saga-new",
121
+ version: 1,
122
+ isCompleted: false,
123
+ createdAt: new Date(),
124
+ updatedAt: new Date(),
125
+ archivedAt: null,
126
+ timeoutMs: null,
127
+ timeoutExpiresAt: null,
128
+ },
129
+ });
130
+
131
+ // Update with concurrency check
132
+ await store.update("OrderSaga", updatedState, expectedVersion);
133
+
134
+ // Delete
135
+ await store.delete("OrderSaga", "saga-123");
136
+ ```
137
+
138
+ ### With Azure SQL Database
139
+
140
+ ```typescript
141
+ const store = new SqlServerSagaStore({
142
+ pool: {
143
+ server: "your-server.database.windows.net",
144
+ database: "sagas",
145
+ authentication: {
146
+ type: "azure-active-directory-default",
147
+ },
148
+ options: {
149
+ encrypt: true,
150
+ },
151
+ },
152
+ });
153
+ ```
154
+
155
+ ### With Existing Connection Pool
156
+
157
+ ```typescript
158
+ import { ConnectionPool } from "mssql";
159
+
160
+ const pool = new ConnectionPool({
161
+ server: "localhost",
162
+ database: "sagas",
163
+ user: "sa",
164
+ password: "password",
165
+ });
166
+
167
+ await pool.connect();
168
+
169
+ const store = new SqlServerSagaStore({
170
+ pool, // Use existing pool
171
+ schema: "app",
172
+ });
173
+
174
+ await store.initialize();
175
+ ```
176
+
177
+ ### Query Helpers
178
+
179
+ ```typescript
180
+ // Find sagas by name with pagination
181
+ const sagas = await store.findByName("OrderSaga", {
182
+ limit: 10,
183
+ offset: 0,
184
+ completed: false,
185
+ });
186
+
187
+ // Count sagas
188
+ const total = await store.countByName("OrderSaga");
189
+ const completed = await store.countByName("OrderSaga", { completed: true });
190
+
191
+ // Cleanup old completed sagas
192
+ const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
193
+ const deleted = await store.deleteCompletedBefore("OrderSaga", oneWeekAgo);
194
+ console.log(`Deleted ${deleted} completed sagas`);
195
+ ```
196
+
197
+ ## Optimistic Concurrency
198
+
199
+ The store uses version-based optimistic concurrency control:
200
+
201
+ ```typescript
202
+ // Read current state
203
+ const state = await store.getById("OrderSaga", "saga-123");
204
+ const expectedVersion = state.metadata.version;
205
+
206
+ // Make changes
207
+ state.status = "completed";
208
+ state.metadata.version += 1;
209
+ state.metadata.updatedAt = new Date();
210
+
211
+ try {
212
+ // Update with version check
213
+ await store.update("OrderSaga", state, expectedVersion);
214
+ } catch (error) {
215
+ if (error instanceof ConcurrencyError) {
216
+ // State was modified by another process
217
+ // Reload and retry
218
+ }
219
+ }
220
+ ```
221
+
222
+ ## Error Handling
223
+
224
+ ```typescript
225
+ import { ConcurrencyError } from "@saga-bus/core";
226
+
227
+ try {
228
+ await store.update("OrderSaga", state, expectedVersion);
229
+ } catch (error) {
230
+ if (error instanceof ConcurrencyError) {
231
+ console.log(`Concurrency conflict: expected v${error.expectedVersion}, actual v${error.actualVersion}`);
232
+ }
233
+ }
234
+ ```
235
+
236
+ ## Testing
237
+
238
+ For local development, you can run SQL Server in Docker:
239
+
240
+ ```bash
241
+ docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=YourPassword123!" \
242
+ -p 1433:1433 --name sqlserver \
243
+ mcr.microsoft.com/mssql/server:2022-latest
244
+ ```
245
+
246
+ Then create the database and table:
247
+
248
+ ```bash
249
+ docker exec -it sqlserver /opt/mssql-tools/bin/sqlcmd \
250
+ -S localhost -U sa -P YourPassword123! \
251
+ -Q "CREATE DATABASE sagas"
252
+ ```
253
+
254
+ For unit tests, use an in-memory store:
255
+
256
+ ```typescript
257
+ import { InMemorySagaStore } from "@saga-bus/core";
258
+
259
+ const testStore = new InMemorySagaStore();
260
+ ```
261
+
262
+ ## License
263
+
264
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ SqlServerSagaStore: () => SqlServerSagaStore
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/SqlServerSagaStore.ts
38
+ var import_mssql = __toESM(require("mssql"), 1);
39
+ var import_core = require("@saga-bus/core");
40
+ var SqlServerSagaStore = class {
41
+ pool = null;
42
+ poolConfig;
43
+ tableName;
44
+ schema;
45
+ ownsPool;
46
+ constructor(options) {
47
+ if (options.pool instanceof import_mssql.ConnectionPool) {
48
+ this.pool = options.pool;
49
+ this.poolConfig = null;
50
+ this.ownsPool = false;
51
+ } else {
52
+ this.poolConfig = options.pool;
53
+ this.ownsPool = true;
54
+ }
55
+ this.tableName = options.tableName ?? "saga_instances";
56
+ this.schema = options.schema ?? "dbo";
57
+ }
58
+ /**
59
+ * Get the full table name with schema.
60
+ */
61
+ get fullTableName() {
62
+ return `[${this.schema}].[${this.tableName}]`;
63
+ }
64
+ /**
65
+ * Initialize the connection pool if using config.
66
+ */
67
+ async initialize() {
68
+ if (this.poolConfig && !this.pool) {
69
+ this.pool = await new import_mssql.ConnectionPool(this.poolConfig).connect();
70
+ }
71
+ }
72
+ async getById(sagaName, sagaId) {
73
+ if (!this.pool) throw new Error("Store not initialized");
74
+ const result = await this.pool.request().input("id", import_mssql.default.NVarChar(128), sagaId).input("saga_name", import_mssql.default.NVarChar(128), sagaName).query(
75
+ `SELECT * FROM ${this.fullTableName} WHERE id = @id AND saga_name = @saga_name`
76
+ );
77
+ if (result.recordset.length === 0) {
78
+ return null;
79
+ }
80
+ return this.rowToState(result.recordset[0]);
81
+ }
82
+ async getByCorrelationId(sagaName, correlationId) {
83
+ if (!this.pool) throw new Error("Store not initialized");
84
+ const result = await this.pool.request().input("saga_name", import_mssql.default.NVarChar(128), sagaName).input("correlation_id", import_mssql.default.NVarChar(256), correlationId).query(
85
+ `SELECT * FROM ${this.fullTableName} WHERE saga_name = @saga_name AND correlation_id = @correlation_id`
86
+ );
87
+ if (result.recordset.length === 0) {
88
+ return null;
89
+ }
90
+ return this.rowToState(result.recordset[0]);
91
+ }
92
+ async insert(sagaName, correlationId, state) {
93
+ if (!this.pool) throw new Error("Store not initialized");
94
+ const { sagaId, version, isCompleted, createdAt, updatedAt } = state.metadata;
95
+ await this.pool.request().input("id", import_mssql.default.NVarChar(128), sagaId).input("saga_name", import_mssql.default.NVarChar(128), sagaName).input("correlation_id", import_mssql.default.NVarChar(256), correlationId).input("version", import_mssql.default.Int, version).input("is_completed", import_mssql.default.Bit, isCompleted ? 1 : 0).input("state", import_mssql.default.NVarChar(import_mssql.default.MAX), JSON.stringify(state)).input("created_at", import_mssql.default.DateTime2, createdAt).input("updated_at", import_mssql.default.DateTime2, updatedAt).query(
96
+ `INSERT INTO ${this.fullTableName}
97
+ (id, saga_name, correlation_id, version, is_completed, state, created_at, updated_at)
98
+ VALUES (@id, @saga_name, @correlation_id, @version, @is_completed, @state, @created_at, @updated_at)`
99
+ );
100
+ }
101
+ async update(sagaName, state, expectedVersion) {
102
+ if (!this.pool) throw new Error("Store not initialized");
103
+ const { sagaId, version, isCompleted, updatedAt } = state.metadata;
104
+ const result = await this.pool.request().input("version", import_mssql.default.Int, version).input("is_completed", import_mssql.default.Bit, isCompleted ? 1 : 0).input("state", import_mssql.default.NVarChar(import_mssql.default.MAX), JSON.stringify(state)).input("updated_at", import_mssql.default.DateTime2, updatedAt).input("id", import_mssql.default.NVarChar(128), sagaId).input("saga_name", import_mssql.default.NVarChar(128), sagaName).input("expected_version", import_mssql.default.Int, expectedVersion).query(
105
+ `UPDATE ${this.fullTableName}
106
+ SET version = @version, is_completed = @is_completed, state = @state, updated_at = @updated_at
107
+ WHERE id = @id AND saga_name = @saga_name AND version = @expected_version`
108
+ );
109
+ if (result.rowsAffected[0] === 0) {
110
+ const existing = await this.getById(sagaName, sagaId);
111
+ if (existing) {
112
+ throw new import_core.ConcurrencyError(
113
+ sagaId,
114
+ expectedVersion,
115
+ existing.metadata.version
116
+ );
117
+ } else {
118
+ throw new Error(`Saga ${sagaId} not found`);
119
+ }
120
+ }
121
+ }
122
+ async delete(sagaName, sagaId) {
123
+ if (!this.pool) throw new Error("Store not initialized");
124
+ await this.pool.request().input("id", import_mssql.default.NVarChar(128), sagaId).input("saga_name", import_mssql.default.NVarChar(128), sagaName).query(
125
+ `DELETE FROM ${this.fullTableName} WHERE id = @id AND saga_name = @saga_name`
126
+ );
127
+ }
128
+ /**
129
+ * Convert a database row to saga state.
130
+ */
131
+ rowToState(row) {
132
+ const state = JSON.parse(row.state);
133
+ return {
134
+ ...state,
135
+ metadata: {
136
+ ...state.metadata,
137
+ sagaId: row.id,
138
+ version: row.version,
139
+ isCompleted: row.is_completed,
140
+ createdAt: new Date(row.created_at),
141
+ updatedAt: new Date(row.updated_at)
142
+ }
143
+ };
144
+ }
145
+ /**
146
+ * Close the connection pool (if owned by this store).
147
+ */
148
+ async close() {
149
+ if (this.ownsPool && this.pool) {
150
+ await this.pool.close();
151
+ }
152
+ this.pool = null;
153
+ }
154
+ /**
155
+ * Get the underlying pool for advanced operations.
156
+ */
157
+ getPool() {
158
+ return this.pool;
159
+ }
160
+ // ============ Query Helpers ============
161
+ /**
162
+ * Find sagas by name with pagination.
163
+ */
164
+ async findByName(sagaName, options) {
165
+ if (!this.pool) throw new Error("Store not initialized");
166
+ const request = this.pool.request().input("saga_name", import_mssql.default.NVarChar(128), sagaName);
167
+ let query = `SELECT * FROM ${this.fullTableName} WHERE saga_name = @saga_name`;
168
+ if (options?.completed !== void 0) {
169
+ query += ` AND is_completed = @is_completed`;
170
+ request.input("is_completed", import_mssql.default.Bit, options.completed ? 1 : 0);
171
+ }
172
+ query += ` ORDER BY created_at DESC`;
173
+ if (options?.offset !== void 0) {
174
+ query += ` OFFSET @offset ROWS`;
175
+ request.input("offset", import_mssql.default.Int, options.offset);
176
+ if (options?.limit !== void 0) {
177
+ query += ` FETCH NEXT @limit ROWS ONLY`;
178
+ request.input("limit", import_mssql.default.Int, options.limit);
179
+ }
180
+ } else if (options?.limit !== void 0) {
181
+ query += ` OFFSET 0 ROWS FETCH NEXT @limit ROWS ONLY`;
182
+ request.input("limit", import_mssql.default.Int, options.limit);
183
+ }
184
+ const result = await request.query(query);
185
+ return result.recordset.map((row) => this.rowToState(row));
186
+ }
187
+ /**
188
+ * Count sagas by name.
189
+ */
190
+ async countByName(sagaName, options) {
191
+ if (!this.pool) throw new Error("Store not initialized");
192
+ const request = this.pool.request().input("saga_name", import_mssql.default.NVarChar(128), sagaName);
193
+ let query = `SELECT COUNT(*) as count FROM ${this.fullTableName} WHERE saga_name = @saga_name`;
194
+ if (options?.completed !== void 0) {
195
+ query += ` AND is_completed = @is_completed`;
196
+ request.input("is_completed", import_mssql.default.Bit, options.completed ? 1 : 0);
197
+ }
198
+ const result = await request.query(query);
199
+ return result.recordset[0]?.count ?? 0;
200
+ }
201
+ /**
202
+ * Delete completed sagas older than a given date.
203
+ */
204
+ async deleteCompletedBefore(sagaName, before) {
205
+ if (!this.pool) throw new Error("Store not initialized");
206
+ const result = await this.pool.request().input("saga_name", import_mssql.default.NVarChar(128), sagaName).input("before", import_mssql.default.DateTime2, before).query(
207
+ `DELETE FROM ${this.fullTableName}
208
+ WHERE saga_name = @saga_name AND is_completed = 1 AND updated_at < @before`
209
+ );
210
+ return result.rowsAffected[0] ?? 0;
211
+ }
212
+ };
213
+ // Annotate the CommonJS export names for ESM import in node:
214
+ 0 && (module.exports = {
215
+ SqlServerSagaStore
216
+ });
217
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/SqlServerSagaStore.ts"],"sourcesContent":["export { SqlServerSagaStore } from \"./SqlServerSagaStore.js\";\nexport type { SqlServerSagaStoreOptions, SagaInstanceRow } from \"./types.js\";\n","import sql, { ConnectionPool, config } from \"mssql\";\nimport type { SagaStore, SagaState } from \"@saga-bus/core\";\nimport { ConcurrencyError } from \"@saga-bus/core\";\nimport type { SqlServerSagaStoreOptions, SagaInstanceRow } from \"./types.js\";\n\n/**\n * SQL Server-backed saga store for saga-bus.\n *\n * @example\n * ```typescript\n * const store = new SqlServerSagaStore<OrderState>({\n * pool: {\n * server: \"localhost\",\n * database: \"sagas\",\n * user: \"sa\",\n * password: \"password\",\n * options: { trustServerCertificate: true },\n * },\n * });\n *\n * await store.initialize();\n * ```\n */\nexport class SqlServerSagaStore<TState extends SagaState>\n implements SagaStore<TState>\n{\n private pool: ConnectionPool | null = null;\n private readonly poolConfig: config | null;\n private readonly tableName: string;\n private readonly schema: string;\n private readonly ownsPool: boolean;\n\n constructor(options: SqlServerSagaStoreOptions) {\n if (options.pool instanceof ConnectionPool) {\n this.pool = options.pool;\n this.poolConfig = null;\n this.ownsPool = false;\n } else {\n this.poolConfig = options.pool;\n this.ownsPool = true;\n }\n\n this.tableName = options.tableName ?? \"saga_instances\";\n this.schema = options.schema ?? \"dbo\";\n }\n\n /**\n * Get the full table name with schema.\n */\n private get fullTableName(): string {\n return `[${this.schema}].[${this.tableName}]`;\n }\n\n /**\n * Initialize the connection pool if using config.\n */\n async initialize(): Promise<void> {\n if (this.poolConfig && !this.pool) {\n this.pool = await new ConnectionPool(this.poolConfig).connect();\n }\n }\n\n async getById(sagaName: string, sagaId: string): Promise<TState | null> {\n if (!this.pool) throw new Error(\"Store not initialized\");\n\n const result = await this.pool\n .request()\n .input(\"id\", sql.NVarChar(128), sagaId)\n .input(\"saga_name\", sql.NVarChar(128), sagaName)\n .query<SagaInstanceRow>(\n `SELECT * FROM ${this.fullTableName} WHERE id = @id AND saga_name = @saga_name`\n );\n\n if (result.recordset.length === 0) {\n return null;\n }\n\n return this.rowToState(result.recordset[0]!);\n }\n\n async getByCorrelationId(\n sagaName: string,\n correlationId: string\n ): Promise<TState | null> {\n if (!this.pool) throw new Error(\"Store not initialized\");\n\n const result = await this.pool\n .request()\n .input(\"saga_name\", sql.NVarChar(128), sagaName)\n .input(\"correlation_id\", sql.NVarChar(256), correlationId)\n .query<SagaInstanceRow>(\n `SELECT * FROM ${this.fullTableName} WHERE saga_name = @saga_name AND correlation_id = @correlation_id`\n );\n\n if (result.recordset.length === 0) {\n return null;\n }\n\n return this.rowToState(result.recordset[0]!);\n }\n\n async insert(\n sagaName: string,\n correlationId: string,\n state: TState\n ): Promise<void> {\n if (!this.pool) throw new Error(\"Store not initialized\");\n\n const { sagaId, version, isCompleted, createdAt, updatedAt } =\n state.metadata;\n\n await this.pool\n .request()\n .input(\"id\", sql.NVarChar(128), sagaId)\n .input(\"saga_name\", sql.NVarChar(128), sagaName)\n .input(\"correlation_id\", sql.NVarChar(256), correlationId)\n .input(\"version\", sql.Int, version)\n .input(\"is_completed\", sql.Bit, isCompleted ? 1 : 0)\n .input(\"state\", sql.NVarChar(sql.MAX), JSON.stringify(state))\n .input(\"created_at\", sql.DateTime2, createdAt)\n .input(\"updated_at\", sql.DateTime2, updatedAt)\n .query(\n `INSERT INTO ${this.fullTableName}\n (id, saga_name, correlation_id, version, is_completed, state, created_at, updated_at)\n VALUES (@id, @saga_name, @correlation_id, @version, @is_completed, @state, @created_at, @updated_at)`\n );\n }\n\n async update(\n sagaName: string,\n state: TState,\n expectedVersion: number\n ): Promise<void> {\n if (!this.pool) throw new Error(\"Store not initialized\");\n\n const { sagaId, version, isCompleted, updatedAt } = state.metadata;\n\n const result = await this.pool\n .request()\n .input(\"version\", sql.Int, version)\n .input(\"is_completed\", sql.Bit, isCompleted ? 1 : 0)\n .input(\"state\", sql.NVarChar(sql.MAX), JSON.stringify(state))\n .input(\"updated_at\", sql.DateTime2, updatedAt)\n .input(\"id\", sql.NVarChar(128), sagaId)\n .input(\"saga_name\", sql.NVarChar(128), sagaName)\n .input(\"expected_version\", sql.Int, expectedVersion)\n .query(\n `UPDATE ${this.fullTableName}\n SET version = @version, is_completed = @is_completed, state = @state, updated_at = @updated_at\n WHERE id = @id AND saga_name = @saga_name AND version = @expected_version`\n );\n\n if (result.rowsAffected[0] === 0) {\n // Either saga doesn't exist or version mismatch\n const existing = await this.getById(sagaName, sagaId);\n if (existing) {\n throw new ConcurrencyError(\n sagaId,\n expectedVersion,\n existing.metadata.version\n );\n } else {\n throw new Error(`Saga ${sagaId} not found`);\n }\n }\n }\n\n async delete(sagaName: string, sagaId: string): Promise<void> {\n if (!this.pool) throw new Error(\"Store not initialized\");\n\n await this.pool\n .request()\n .input(\"id\", sql.NVarChar(128), sagaId)\n .input(\"saga_name\", sql.NVarChar(128), sagaName)\n .query(\n `DELETE FROM ${this.fullTableName} WHERE id = @id AND saga_name = @saga_name`\n );\n }\n\n /**\n * Convert a database row to saga state.\n */\n private rowToState(row: SagaInstanceRow): TState {\n const state = JSON.parse(row.state) as TState;\n\n // Ensure metadata dates are Date objects\n return {\n ...state,\n metadata: {\n ...state.metadata,\n sagaId: row.id,\n version: row.version,\n isCompleted: row.is_completed,\n createdAt: new Date(row.created_at),\n updatedAt: new Date(row.updated_at),\n },\n };\n }\n\n /**\n * Close the connection pool (if owned by this store).\n */\n async close(): Promise<void> {\n if (this.ownsPool && this.pool) {\n await this.pool.close();\n }\n this.pool = null;\n }\n\n /**\n * Get the underlying pool for advanced operations.\n */\n getPool(): ConnectionPool | null {\n return this.pool;\n }\n\n // ============ Query Helpers ============\n\n /**\n * Find sagas by name with pagination.\n */\n async findByName(\n sagaName: string,\n options?: {\n limit?: number;\n offset?: number;\n completed?: boolean;\n }\n ): Promise<TState[]> {\n if (!this.pool) throw new Error(\"Store not initialized\");\n\n const request = this.pool\n .request()\n .input(\"saga_name\", sql.NVarChar(128), sagaName);\n\n let query = `SELECT * FROM ${this.fullTableName} WHERE saga_name = @saga_name`;\n\n if (options?.completed !== undefined) {\n query += ` AND is_completed = @is_completed`;\n request.input(\"is_completed\", sql.Bit, options.completed ? 1 : 0);\n }\n\n query += ` ORDER BY created_at DESC`;\n\n if (options?.offset !== undefined) {\n query += ` OFFSET @offset ROWS`;\n request.input(\"offset\", sql.Int, options.offset);\n\n if (options?.limit !== undefined) {\n query += ` FETCH NEXT @limit ROWS ONLY`;\n request.input(\"limit\", sql.Int, options.limit);\n }\n } else if (options?.limit !== undefined) {\n // SQL Server requires OFFSET when using FETCH\n query += ` OFFSET 0 ROWS FETCH NEXT @limit ROWS ONLY`;\n request.input(\"limit\", sql.Int, options.limit);\n }\n\n const result = await request.query<SagaInstanceRow>(query);\n return result.recordset.map((row) => this.rowToState(row));\n }\n\n /**\n * Count sagas by name.\n */\n async countByName(\n sagaName: string,\n options?: { completed?: boolean }\n ): Promise<number> {\n if (!this.pool) throw new Error(\"Store not initialized\");\n\n const request = this.pool\n .request()\n .input(\"saga_name\", sql.NVarChar(128), sagaName);\n\n let query = `SELECT COUNT(*) as count FROM ${this.fullTableName} WHERE saga_name = @saga_name`;\n\n if (options?.completed !== undefined) {\n query += ` AND is_completed = @is_completed`;\n request.input(\"is_completed\", sql.Bit, options.completed ? 1 : 0);\n }\n\n const result = await request.query<{ count: number }>(query);\n return result.recordset[0]?.count ?? 0;\n }\n\n /**\n * Delete completed sagas older than a given date.\n */\n async deleteCompletedBefore(\n sagaName: string,\n before: Date\n ): Promise<number> {\n if (!this.pool) throw new Error(\"Store not initialized\");\n\n const result = await this.pool\n .request()\n .input(\"saga_name\", sql.NVarChar(128), sagaName)\n .input(\"before\", sql.DateTime2, before)\n .query(\n `DELETE FROM ${this.fullTableName}\n WHERE saga_name = @saga_name AND is_completed = 1 AND updated_at < @before`\n );\n\n return result.rowsAffected[0] ?? 0;\n }\n}\n"],"mappings":"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","names":["sql"]}
@@ -0,0 +1,105 @@
1
+ import { ConnectionPool, config } from 'mssql';
2
+ import { SagaState, SagaStore } from '@saga-bus/core';
3
+
4
+ /**
5
+ * Options for creating a SqlServerSagaStore.
6
+ */
7
+ interface SqlServerSagaStoreOptions {
8
+ /**
9
+ * SQL Server connection pool or pool configuration.
10
+ */
11
+ pool: ConnectionPool | config;
12
+ /**
13
+ * Table name for saga instances. Default: "saga_instances"
14
+ */
15
+ tableName?: string;
16
+ /**
17
+ * Schema name. Default: "dbo"
18
+ */
19
+ schema?: string;
20
+ }
21
+ /**
22
+ * Row structure in the saga_instances table.
23
+ */
24
+ interface SagaInstanceRow {
25
+ id: string;
26
+ saga_name: string;
27
+ correlation_id: string;
28
+ version: number;
29
+ is_completed: boolean;
30
+ state: string;
31
+ created_at: Date;
32
+ updated_at: Date;
33
+ }
34
+
35
+ /**
36
+ * SQL Server-backed saga store for saga-bus.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * const store = new SqlServerSagaStore<OrderState>({
41
+ * pool: {
42
+ * server: "localhost",
43
+ * database: "sagas",
44
+ * user: "sa",
45
+ * password: "password",
46
+ * options: { trustServerCertificate: true },
47
+ * },
48
+ * });
49
+ *
50
+ * await store.initialize();
51
+ * ```
52
+ */
53
+ declare class SqlServerSagaStore<TState extends SagaState> implements SagaStore<TState> {
54
+ private pool;
55
+ private readonly poolConfig;
56
+ private readonly tableName;
57
+ private readonly schema;
58
+ private readonly ownsPool;
59
+ constructor(options: SqlServerSagaStoreOptions);
60
+ /**
61
+ * Get the full table name with schema.
62
+ */
63
+ private get fullTableName();
64
+ /**
65
+ * Initialize the connection pool if using config.
66
+ */
67
+ initialize(): Promise<void>;
68
+ getById(sagaName: string, sagaId: string): Promise<TState | null>;
69
+ getByCorrelationId(sagaName: string, correlationId: string): Promise<TState | null>;
70
+ insert(sagaName: string, correlationId: string, state: TState): Promise<void>;
71
+ update(sagaName: string, state: TState, expectedVersion: number): Promise<void>;
72
+ delete(sagaName: string, sagaId: string): Promise<void>;
73
+ /**
74
+ * Convert a database row to saga state.
75
+ */
76
+ private rowToState;
77
+ /**
78
+ * Close the connection pool (if owned by this store).
79
+ */
80
+ close(): Promise<void>;
81
+ /**
82
+ * Get the underlying pool for advanced operations.
83
+ */
84
+ getPool(): ConnectionPool | null;
85
+ /**
86
+ * Find sagas by name with pagination.
87
+ */
88
+ findByName(sagaName: string, options?: {
89
+ limit?: number;
90
+ offset?: number;
91
+ completed?: boolean;
92
+ }): Promise<TState[]>;
93
+ /**
94
+ * Count sagas by name.
95
+ */
96
+ countByName(sagaName: string, options?: {
97
+ completed?: boolean;
98
+ }): Promise<number>;
99
+ /**
100
+ * Delete completed sagas older than a given date.
101
+ */
102
+ deleteCompletedBefore(sagaName: string, before: Date): Promise<number>;
103
+ }
104
+
105
+ export { type SagaInstanceRow, SqlServerSagaStore, type SqlServerSagaStoreOptions };
@@ -0,0 +1,105 @@
1
+ import { ConnectionPool, config } from 'mssql';
2
+ import { SagaState, SagaStore } from '@saga-bus/core';
3
+
4
+ /**
5
+ * Options for creating a SqlServerSagaStore.
6
+ */
7
+ interface SqlServerSagaStoreOptions {
8
+ /**
9
+ * SQL Server connection pool or pool configuration.
10
+ */
11
+ pool: ConnectionPool | config;
12
+ /**
13
+ * Table name for saga instances. Default: "saga_instances"
14
+ */
15
+ tableName?: string;
16
+ /**
17
+ * Schema name. Default: "dbo"
18
+ */
19
+ schema?: string;
20
+ }
21
+ /**
22
+ * Row structure in the saga_instances table.
23
+ */
24
+ interface SagaInstanceRow {
25
+ id: string;
26
+ saga_name: string;
27
+ correlation_id: string;
28
+ version: number;
29
+ is_completed: boolean;
30
+ state: string;
31
+ created_at: Date;
32
+ updated_at: Date;
33
+ }
34
+
35
+ /**
36
+ * SQL Server-backed saga store for saga-bus.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * const store = new SqlServerSagaStore<OrderState>({
41
+ * pool: {
42
+ * server: "localhost",
43
+ * database: "sagas",
44
+ * user: "sa",
45
+ * password: "password",
46
+ * options: { trustServerCertificate: true },
47
+ * },
48
+ * });
49
+ *
50
+ * await store.initialize();
51
+ * ```
52
+ */
53
+ declare class SqlServerSagaStore<TState extends SagaState> implements SagaStore<TState> {
54
+ private pool;
55
+ private readonly poolConfig;
56
+ private readonly tableName;
57
+ private readonly schema;
58
+ private readonly ownsPool;
59
+ constructor(options: SqlServerSagaStoreOptions);
60
+ /**
61
+ * Get the full table name with schema.
62
+ */
63
+ private get fullTableName();
64
+ /**
65
+ * Initialize the connection pool if using config.
66
+ */
67
+ initialize(): Promise<void>;
68
+ getById(sagaName: string, sagaId: string): Promise<TState | null>;
69
+ getByCorrelationId(sagaName: string, correlationId: string): Promise<TState | null>;
70
+ insert(sagaName: string, correlationId: string, state: TState): Promise<void>;
71
+ update(sagaName: string, state: TState, expectedVersion: number): Promise<void>;
72
+ delete(sagaName: string, sagaId: string): Promise<void>;
73
+ /**
74
+ * Convert a database row to saga state.
75
+ */
76
+ private rowToState;
77
+ /**
78
+ * Close the connection pool (if owned by this store).
79
+ */
80
+ close(): Promise<void>;
81
+ /**
82
+ * Get the underlying pool for advanced operations.
83
+ */
84
+ getPool(): ConnectionPool | null;
85
+ /**
86
+ * Find sagas by name with pagination.
87
+ */
88
+ findByName(sagaName: string, options?: {
89
+ limit?: number;
90
+ offset?: number;
91
+ completed?: boolean;
92
+ }): Promise<TState[]>;
93
+ /**
94
+ * Count sagas by name.
95
+ */
96
+ countByName(sagaName: string, options?: {
97
+ completed?: boolean;
98
+ }): Promise<number>;
99
+ /**
100
+ * Delete completed sagas older than a given date.
101
+ */
102
+ deleteCompletedBefore(sagaName: string, before: Date): Promise<number>;
103
+ }
104
+
105
+ export { type SagaInstanceRow, SqlServerSagaStore, type SqlServerSagaStoreOptions };
package/dist/index.js ADDED
@@ -0,0 +1,180 @@
1
+ // src/SqlServerSagaStore.ts
2
+ import sql, { ConnectionPool } from "mssql";
3
+ import { ConcurrencyError } from "@saga-bus/core";
4
+ var SqlServerSagaStore = class {
5
+ pool = null;
6
+ poolConfig;
7
+ tableName;
8
+ schema;
9
+ ownsPool;
10
+ constructor(options) {
11
+ if (options.pool instanceof ConnectionPool) {
12
+ this.pool = options.pool;
13
+ this.poolConfig = null;
14
+ this.ownsPool = false;
15
+ } else {
16
+ this.poolConfig = options.pool;
17
+ this.ownsPool = true;
18
+ }
19
+ this.tableName = options.tableName ?? "saga_instances";
20
+ this.schema = options.schema ?? "dbo";
21
+ }
22
+ /**
23
+ * Get the full table name with schema.
24
+ */
25
+ get fullTableName() {
26
+ return `[${this.schema}].[${this.tableName}]`;
27
+ }
28
+ /**
29
+ * Initialize the connection pool if using config.
30
+ */
31
+ async initialize() {
32
+ if (this.poolConfig && !this.pool) {
33
+ this.pool = await new ConnectionPool(this.poolConfig).connect();
34
+ }
35
+ }
36
+ async getById(sagaName, sagaId) {
37
+ if (!this.pool) throw new Error("Store not initialized");
38
+ const result = await this.pool.request().input("id", sql.NVarChar(128), sagaId).input("saga_name", sql.NVarChar(128), sagaName).query(
39
+ `SELECT * FROM ${this.fullTableName} WHERE id = @id AND saga_name = @saga_name`
40
+ );
41
+ if (result.recordset.length === 0) {
42
+ return null;
43
+ }
44
+ return this.rowToState(result.recordset[0]);
45
+ }
46
+ async getByCorrelationId(sagaName, correlationId) {
47
+ if (!this.pool) throw new Error("Store not initialized");
48
+ const result = await this.pool.request().input("saga_name", sql.NVarChar(128), sagaName).input("correlation_id", sql.NVarChar(256), correlationId).query(
49
+ `SELECT * FROM ${this.fullTableName} WHERE saga_name = @saga_name AND correlation_id = @correlation_id`
50
+ );
51
+ if (result.recordset.length === 0) {
52
+ return null;
53
+ }
54
+ return this.rowToState(result.recordset[0]);
55
+ }
56
+ async insert(sagaName, correlationId, state) {
57
+ if (!this.pool) throw new Error("Store not initialized");
58
+ const { sagaId, version, isCompleted, createdAt, updatedAt } = state.metadata;
59
+ await this.pool.request().input("id", sql.NVarChar(128), sagaId).input("saga_name", sql.NVarChar(128), sagaName).input("correlation_id", sql.NVarChar(256), correlationId).input("version", sql.Int, version).input("is_completed", sql.Bit, isCompleted ? 1 : 0).input("state", sql.NVarChar(sql.MAX), JSON.stringify(state)).input("created_at", sql.DateTime2, createdAt).input("updated_at", sql.DateTime2, updatedAt).query(
60
+ `INSERT INTO ${this.fullTableName}
61
+ (id, saga_name, correlation_id, version, is_completed, state, created_at, updated_at)
62
+ VALUES (@id, @saga_name, @correlation_id, @version, @is_completed, @state, @created_at, @updated_at)`
63
+ );
64
+ }
65
+ async update(sagaName, state, expectedVersion) {
66
+ if (!this.pool) throw new Error("Store not initialized");
67
+ const { sagaId, version, isCompleted, updatedAt } = state.metadata;
68
+ const result = await this.pool.request().input("version", sql.Int, version).input("is_completed", sql.Bit, isCompleted ? 1 : 0).input("state", sql.NVarChar(sql.MAX), JSON.stringify(state)).input("updated_at", sql.DateTime2, updatedAt).input("id", sql.NVarChar(128), sagaId).input("saga_name", sql.NVarChar(128), sagaName).input("expected_version", sql.Int, expectedVersion).query(
69
+ `UPDATE ${this.fullTableName}
70
+ SET version = @version, is_completed = @is_completed, state = @state, updated_at = @updated_at
71
+ WHERE id = @id AND saga_name = @saga_name AND version = @expected_version`
72
+ );
73
+ if (result.rowsAffected[0] === 0) {
74
+ const existing = await this.getById(sagaName, sagaId);
75
+ if (existing) {
76
+ throw new ConcurrencyError(
77
+ sagaId,
78
+ expectedVersion,
79
+ existing.metadata.version
80
+ );
81
+ } else {
82
+ throw new Error(`Saga ${sagaId} not found`);
83
+ }
84
+ }
85
+ }
86
+ async delete(sagaName, sagaId) {
87
+ if (!this.pool) throw new Error("Store not initialized");
88
+ await this.pool.request().input("id", sql.NVarChar(128), sagaId).input("saga_name", sql.NVarChar(128), sagaName).query(
89
+ `DELETE FROM ${this.fullTableName} WHERE id = @id AND saga_name = @saga_name`
90
+ );
91
+ }
92
+ /**
93
+ * Convert a database row to saga state.
94
+ */
95
+ rowToState(row) {
96
+ const state = JSON.parse(row.state);
97
+ return {
98
+ ...state,
99
+ metadata: {
100
+ ...state.metadata,
101
+ sagaId: row.id,
102
+ version: row.version,
103
+ isCompleted: row.is_completed,
104
+ createdAt: new Date(row.created_at),
105
+ updatedAt: new Date(row.updated_at)
106
+ }
107
+ };
108
+ }
109
+ /**
110
+ * Close the connection pool (if owned by this store).
111
+ */
112
+ async close() {
113
+ if (this.ownsPool && this.pool) {
114
+ await this.pool.close();
115
+ }
116
+ this.pool = null;
117
+ }
118
+ /**
119
+ * Get the underlying pool for advanced operations.
120
+ */
121
+ getPool() {
122
+ return this.pool;
123
+ }
124
+ // ============ Query Helpers ============
125
+ /**
126
+ * Find sagas by name with pagination.
127
+ */
128
+ async findByName(sagaName, options) {
129
+ if (!this.pool) throw new Error("Store not initialized");
130
+ const request = this.pool.request().input("saga_name", sql.NVarChar(128), sagaName);
131
+ let query = `SELECT * FROM ${this.fullTableName} WHERE saga_name = @saga_name`;
132
+ if (options?.completed !== void 0) {
133
+ query += ` AND is_completed = @is_completed`;
134
+ request.input("is_completed", sql.Bit, options.completed ? 1 : 0);
135
+ }
136
+ query += ` ORDER BY created_at DESC`;
137
+ if (options?.offset !== void 0) {
138
+ query += ` OFFSET @offset ROWS`;
139
+ request.input("offset", sql.Int, options.offset);
140
+ if (options?.limit !== void 0) {
141
+ query += ` FETCH NEXT @limit ROWS ONLY`;
142
+ request.input("limit", sql.Int, options.limit);
143
+ }
144
+ } else if (options?.limit !== void 0) {
145
+ query += ` OFFSET 0 ROWS FETCH NEXT @limit ROWS ONLY`;
146
+ request.input("limit", sql.Int, options.limit);
147
+ }
148
+ const result = await request.query(query);
149
+ return result.recordset.map((row) => this.rowToState(row));
150
+ }
151
+ /**
152
+ * Count sagas by name.
153
+ */
154
+ async countByName(sagaName, options) {
155
+ if (!this.pool) throw new Error("Store not initialized");
156
+ const request = this.pool.request().input("saga_name", sql.NVarChar(128), sagaName);
157
+ let query = `SELECT COUNT(*) as count FROM ${this.fullTableName} WHERE saga_name = @saga_name`;
158
+ if (options?.completed !== void 0) {
159
+ query += ` AND is_completed = @is_completed`;
160
+ request.input("is_completed", sql.Bit, options.completed ? 1 : 0);
161
+ }
162
+ const result = await request.query(query);
163
+ return result.recordset[0]?.count ?? 0;
164
+ }
165
+ /**
166
+ * Delete completed sagas older than a given date.
167
+ */
168
+ async deleteCompletedBefore(sagaName, before) {
169
+ if (!this.pool) throw new Error("Store not initialized");
170
+ const result = await this.pool.request().input("saga_name", sql.NVarChar(128), sagaName).input("before", sql.DateTime2, before).query(
171
+ `DELETE FROM ${this.fullTableName}
172
+ WHERE saga_name = @saga_name AND is_completed = 1 AND updated_at < @before`
173
+ );
174
+ return result.rowsAffected[0] ?? 0;
175
+ }
176
+ };
177
+ export {
178
+ SqlServerSagaStore
179
+ };
180
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/SqlServerSagaStore.ts"],"sourcesContent":["import sql, { ConnectionPool, config } from \"mssql\";\nimport type { SagaStore, SagaState } from \"@saga-bus/core\";\nimport { ConcurrencyError } from \"@saga-bus/core\";\nimport type { SqlServerSagaStoreOptions, SagaInstanceRow } from \"./types.js\";\n\n/**\n * SQL Server-backed saga store for saga-bus.\n *\n * @example\n * ```typescript\n * const store = new SqlServerSagaStore<OrderState>({\n * pool: {\n * server: \"localhost\",\n * database: \"sagas\",\n * user: \"sa\",\n * password: \"password\",\n * options: { trustServerCertificate: true },\n * },\n * });\n *\n * await store.initialize();\n * ```\n */\nexport class SqlServerSagaStore<TState extends SagaState>\n implements SagaStore<TState>\n{\n private pool: ConnectionPool | null = null;\n private readonly poolConfig: config | null;\n private readonly tableName: string;\n private readonly schema: string;\n private readonly ownsPool: boolean;\n\n constructor(options: SqlServerSagaStoreOptions) {\n if (options.pool instanceof ConnectionPool) {\n this.pool = options.pool;\n this.poolConfig = null;\n this.ownsPool = false;\n } else {\n this.poolConfig = options.pool;\n this.ownsPool = true;\n }\n\n this.tableName = options.tableName ?? \"saga_instances\";\n this.schema = options.schema ?? \"dbo\";\n }\n\n /**\n * Get the full table name with schema.\n */\n private get fullTableName(): string {\n return `[${this.schema}].[${this.tableName}]`;\n }\n\n /**\n * Initialize the connection pool if using config.\n */\n async initialize(): Promise<void> {\n if (this.poolConfig && !this.pool) {\n this.pool = await new ConnectionPool(this.poolConfig).connect();\n }\n }\n\n async getById(sagaName: string, sagaId: string): Promise<TState | null> {\n if (!this.pool) throw new Error(\"Store not initialized\");\n\n const result = await this.pool\n .request()\n .input(\"id\", sql.NVarChar(128), sagaId)\n .input(\"saga_name\", sql.NVarChar(128), sagaName)\n .query<SagaInstanceRow>(\n `SELECT * FROM ${this.fullTableName} WHERE id = @id AND saga_name = @saga_name`\n );\n\n if (result.recordset.length === 0) {\n return null;\n }\n\n return this.rowToState(result.recordset[0]!);\n }\n\n async getByCorrelationId(\n sagaName: string,\n correlationId: string\n ): Promise<TState | null> {\n if (!this.pool) throw new Error(\"Store not initialized\");\n\n const result = await this.pool\n .request()\n .input(\"saga_name\", sql.NVarChar(128), sagaName)\n .input(\"correlation_id\", sql.NVarChar(256), correlationId)\n .query<SagaInstanceRow>(\n `SELECT * FROM ${this.fullTableName} WHERE saga_name = @saga_name AND correlation_id = @correlation_id`\n );\n\n if (result.recordset.length === 0) {\n return null;\n }\n\n return this.rowToState(result.recordset[0]!);\n }\n\n async insert(\n sagaName: string,\n correlationId: string,\n state: TState\n ): Promise<void> {\n if (!this.pool) throw new Error(\"Store not initialized\");\n\n const { sagaId, version, isCompleted, createdAt, updatedAt } =\n state.metadata;\n\n await this.pool\n .request()\n .input(\"id\", sql.NVarChar(128), sagaId)\n .input(\"saga_name\", sql.NVarChar(128), sagaName)\n .input(\"correlation_id\", sql.NVarChar(256), correlationId)\n .input(\"version\", sql.Int, version)\n .input(\"is_completed\", sql.Bit, isCompleted ? 1 : 0)\n .input(\"state\", sql.NVarChar(sql.MAX), JSON.stringify(state))\n .input(\"created_at\", sql.DateTime2, createdAt)\n .input(\"updated_at\", sql.DateTime2, updatedAt)\n .query(\n `INSERT INTO ${this.fullTableName}\n (id, saga_name, correlation_id, version, is_completed, state, created_at, updated_at)\n VALUES (@id, @saga_name, @correlation_id, @version, @is_completed, @state, @created_at, @updated_at)`\n );\n }\n\n async update(\n sagaName: string,\n state: TState,\n expectedVersion: number\n ): Promise<void> {\n if (!this.pool) throw new Error(\"Store not initialized\");\n\n const { sagaId, version, isCompleted, updatedAt } = state.metadata;\n\n const result = await this.pool\n .request()\n .input(\"version\", sql.Int, version)\n .input(\"is_completed\", sql.Bit, isCompleted ? 1 : 0)\n .input(\"state\", sql.NVarChar(sql.MAX), JSON.stringify(state))\n .input(\"updated_at\", sql.DateTime2, updatedAt)\n .input(\"id\", sql.NVarChar(128), sagaId)\n .input(\"saga_name\", sql.NVarChar(128), sagaName)\n .input(\"expected_version\", sql.Int, expectedVersion)\n .query(\n `UPDATE ${this.fullTableName}\n SET version = @version, is_completed = @is_completed, state = @state, updated_at = @updated_at\n WHERE id = @id AND saga_name = @saga_name AND version = @expected_version`\n );\n\n if (result.rowsAffected[0] === 0) {\n // Either saga doesn't exist or version mismatch\n const existing = await this.getById(sagaName, sagaId);\n if (existing) {\n throw new ConcurrencyError(\n sagaId,\n expectedVersion,\n existing.metadata.version\n );\n } else {\n throw new Error(`Saga ${sagaId} not found`);\n }\n }\n }\n\n async delete(sagaName: string, sagaId: string): Promise<void> {\n if (!this.pool) throw new Error(\"Store not initialized\");\n\n await this.pool\n .request()\n .input(\"id\", sql.NVarChar(128), sagaId)\n .input(\"saga_name\", sql.NVarChar(128), sagaName)\n .query(\n `DELETE FROM ${this.fullTableName} WHERE id = @id AND saga_name = @saga_name`\n );\n }\n\n /**\n * Convert a database row to saga state.\n */\n private rowToState(row: SagaInstanceRow): TState {\n const state = JSON.parse(row.state) as TState;\n\n // Ensure metadata dates are Date objects\n return {\n ...state,\n metadata: {\n ...state.metadata,\n sagaId: row.id,\n version: row.version,\n isCompleted: row.is_completed,\n createdAt: new Date(row.created_at),\n updatedAt: new Date(row.updated_at),\n },\n };\n }\n\n /**\n * Close the connection pool (if owned by this store).\n */\n async close(): Promise<void> {\n if (this.ownsPool && this.pool) {\n await this.pool.close();\n }\n this.pool = null;\n }\n\n /**\n * Get the underlying pool for advanced operations.\n */\n getPool(): ConnectionPool | null {\n return this.pool;\n }\n\n // ============ Query Helpers ============\n\n /**\n * Find sagas by name with pagination.\n */\n async findByName(\n sagaName: string,\n options?: {\n limit?: number;\n offset?: number;\n completed?: boolean;\n }\n ): Promise<TState[]> {\n if (!this.pool) throw new Error(\"Store not initialized\");\n\n const request = this.pool\n .request()\n .input(\"saga_name\", sql.NVarChar(128), sagaName);\n\n let query = `SELECT * FROM ${this.fullTableName} WHERE saga_name = @saga_name`;\n\n if (options?.completed !== undefined) {\n query += ` AND is_completed = @is_completed`;\n request.input(\"is_completed\", sql.Bit, options.completed ? 1 : 0);\n }\n\n query += ` ORDER BY created_at DESC`;\n\n if (options?.offset !== undefined) {\n query += ` OFFSET @offset ROWS`;\n request.input(\"offset\", sql.Int, options.offset);\n\n if (options?.limit !== undefined) {\n query += ` FETCH NEXT @limit ROWS ONLY`;\n request.input(\"limit\", sql.Int, options.limit);\n }\n } else if (options?.limit !== undefined) {\n // SQL Server requires OFFSET when using FETCH\n query += ` OFFSET 0 ROWS FETCH NEXT @limit ROWS ONLY`;\n request.input(\"limit\", sql.Int, options.limit);\n }\n\n const result = await request.query<SagaInstanceRow>(query);\n return result.recordset.map((row) => this.rowToState(row));\n }\n\n /**\n * Count sagas by name.\n */\n async countByName(\n sagaName: string,\n options?: { completed?: boolean }\n ): Promise<number> {\n if (!this.pool) throw new Error(\"Store not initialized\");\n\n const request = this.pool\n .request()\n .input(\"saga_name\", sql.NVarChar(128), sagaName);\n\n let query = `SELECT COUNT(*) as count FROM ${this.fullTableName} WHERE saga_name = @saga_name`;\n\n if (options?.completed !== undefined) {\n query += ` AND is_completed = @is_completed`;\n request.input(\"is_completed\", sql.Bit, options.completed ? 1 : 0);\n }\n\n const result = await request.query<{ count: number }>(query);\n return result.recordset[0]?.count ?? 0;\n }\n\n /**\n * Delete completed sagas older than a given date.\n */\n async deleteCompletedBefore(\n sagaName: string,\n before: Date\n ): Promise<number> {\n if (!this.pool) throw new Error(\"Store not initialized\");\n\n const result = await this.pool\n .request()\n .input(\"saga_name\", sql.NVarChar(128), sagaName)\n .input(\"before\", sql.DateTime2, before)\n .query(\n `DELETE FROM ${this.fullTableName}\n WHERE saga_name = @saga_name AND is_completed = 1 AND updated_at < @before`\n );\n\n return result.rowsAffected[0] ?? 0;\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@saga-bus/store-sqlserver",
3
+ "version": "1.0.0",
4
+ "description": "SQL Server saga store for saga-bus",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
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
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/deanforan/saga-bus.git",
26
+ "directory": "packages/store-sqlserver"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/deanforan/saga-bus/issues"
30
+ },
31
+ "homepage": "https://github.com/deanforan/saga-bus#readme",
32
+ "keywords": [
33
+ "saga",
34
+ "message-bus",
35
+ "store",
36
+ "sqlserver",
37
+ "mssql",
38
+ "azure-sql"
39
+ ],
40
+ "dependencies": {
41
+ "@saga-bus/core": "0.1.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/mssql": "^9.1.0",
45
+ "mssql": "^11.0.0",
46
+ "tsup": "^8.0.0",
47
+ "typescript": "^5.9.2",
48
+ "vitest": "^3.0.0",
49
+ "@repo/eslint-config": "0.0.0",
50
+ "@repo/typescript-config": "0.0.0"
51
+ },
52
+ "peerDependencies": {
53
+ "@saga-bus/core": ">=0.1.0",
54
+ "mssql": ">=10.0.0"
55
+ },
56
+ "scripts": {
57
+ "build": "tsup",
58
+ "dev": "tsup --watch",
59
+ "lint": "eslint src/",
60
+ "check-types": "tsc --noEmit",
61
+ "test": "vitest run",
62
+ "test:watch": "vitest"
63
+ }
64
+ }