@outbox-event-bus/sqlite-better-sqlite3-outbox 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.
@@ -0,0 +1,42 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks"
2
+ import type { Database } from "better-sqlite3"
3
+
4
+ export type { Database }
5
+
6
+ export const betterSqlite3TransactionStorage: AsyncLocalStorage<Database> =
7
+ new AsyncLocalStorage<Database>()
8
+
9
+ export async function withBetterSqlite3Transaction<T>(
10
+ db: Database,
11
+ fn: (tx: Database) => Promise<T>
12
+ ): Promise<T> {
13
+ return betterSqlite3TransactionStorage.run(db, async () => {
14
+ if (db.inTransaction) {
15
+ const savepointName = `sp_${Date.now()}_${Math.random().toString(36).slice(2)}`
16
+ db.prepare(`SAVEPOINT ${savepointName}`).run()
17
+ try {
18
+ const result = await fn(db)
19
+ db.prepare(`RELEASE ${savepointName}`).run()
20
+ return result
21
+ } catch (error) {
22
+ db.prepare(`ROLLBACK TO ${savepointName}`).run()
23
+ db.prepare(`RELEASE ${savepointName}`).run()
24
+ throw error
25
+ }
26
+ } else {
27
+ db.prepare("BEGIN").run()
28
+ try {
29
+ const result = await fn(db)
30
+ db.prepare("COMMIT").run()
31
+ return result
32
+ } catch (error) {
33
+ db.prepare("ROLLBACK").run()
34
+ throw error
35
+ }
36
+ }
37
+ })
38
+ }
39
+
40
+ export function getBetterSqlite3Transaction(): () => Database | undefined {
41
+ return () => betterSqlite3TransactionStorage.getStore()
42
+ }
@@ -0,0 +1,196 @@
1
+ import { existsSync, unlinkSync } from "node:fs"
2
+ import Database from "better-sqlite3"
3
+ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"
4
+ import { OutboxEventBus } from "../../../core/src/bus/outbox-event-bus"
5
+ import {
6
+ getBetterSqlite3Transaction,
7
+ SqliteBetterSqlite3Outbox,
8
+ withBetterSqlite3Transaction,
9
+ } from "./index"
10
+
11
+ const DB_PATH = "./test-transactions.db"
12
+
13
+ describe("SqliteBetterSqlite3Outbox Transactions with AsyncLocalStorage", () => {
14
+ let db: Database.Database
15
+
16
+ beforeAll(() => {
17
+ if (existsSync(DB_PATH)) {
18
+ unlinkSync(DB_PATH)
19
+ }
20
+ db = new Database(DB_PATH)
21
+ })
22
+
23
+ afterAll(() => {
24
+ if (db) {
25
+ db.close()
26
+ }
27
+ if (existsSync(DB_PATH)) {
28
+ unlinkSync(DB_PATH)
29
+ }
30
+ })
31
+
32
+ beforeEach(() => {
33
+ // Tables are created by init() in constructor
34
+ })
35
+
36
+ it("should commit both business data and outbox event in a transaction", async () => {
37
+ const outbox = new SqliteBetterSqlite3Outbox({
38
+ dbPath: DB_PATH,
39
+ getTransaction: getBetterSqlite3Transaction(),
40
+ })
41
+
42
+ const eventBus = new OutboxEventBus(
43
+ outbox,
44
+ () => {},
45
+ () => {}
46
+ )
47
+
48
+ db.exec("CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT)")
49
+
50
+ const eventId = "event-commit"
51
+ const userId = "user-commit"
52
+
53
+ // Use withBetterSqlite3Transaction helper
54
+ await withBetterSqlite3Transaction(db, async () => {
55
+ // 1. Business logic
56
+ db.prepare("INSERT INTO users (id, name) VALUES (?, ?)").run(userId, "Alice")
57
+
58
+ // 2. Emit event
59
+ await eventBus.emit({
60
+ id: eventId,
61
+ type: "USER_CREATED",
62
+ payload: { userId },
63
+ occurredAt: new Date(),
64
+ })
65
+ })
66
+
67
+ // Verify
68
+ const user = db.prepare("SELECT * FROM users WHERE id = ?").get(userId)
69
+ expect(user).toBeDefined()
70
+
71
+ const event = db.prepare("SELECT * FROM outbox_events WHERE id = ?").get(eventId)
72
+ expect(event).toBeDefined()
73
+ })
74
+
75
+ it("should rollback both business data and outbox event on failure", async () => {
76
+ const outbox = new SqliteBetterSqlite3Outbox({
77
+ dbPath: DB_PATH,
78
+ getTransaction: getBetterSqlite3Transaction(),
79
+ })
80
+
81
+ const eventBus = new OutboxEventBus(
82
+ outbox,
83
+ () => {},
84
+ () => {}
85
+ )
86
+
87
+ const eventId = "event-rollback"
88
+ const userId = "user-rollback"
89
+
90
+ try {
91
+ await withBetterSqlite3Transaction(db, async () => {
92
+ db.prepare("INSERT INTO users (id, name) VALUES (?, ?)").run(userId, "Bob")
93
+
94
+ await eventBus.emit({
95
+ id: eventId,
96
+ type: "USER_CREATED",
97
+ payload: { userId },
98
+ occurredAt: new Date(),
99
+ })
100
+
101
+ throw new Error("Forced rollback")
102
+ })
103
+ } catch (_err) {
104
+ // Expected
105
+ }
106
+
107
+ // Verify
108
+ const user = db.prepare("SELECT * FROM users WHERE id = ?").get(userId)
109
+ expect(user).toBeUndefined()
110
+
111
+ const event = db.prepare("SELECT * FROM outbox_events WHERE id = ?").get(eventId)
112
+ expect(event).toBeUndefined()
113
+ })
114
+
115
+ it("should work with explicit transaction parameter", async () => {
116
+ const outbox = new SqliteBetterSqlite3Outbox({
117
+ dbPath: DB_PATH,
118
+ })
119
+
120
+ const eventBus = new OutboxEventBus(
121
+ outbox,
122
+ () => {},
123
+ () => {}
124
+ )
125
+
126
+ const eventId = "event-explicit"
127
+ const userId = "user-explicit"
128
+
129
+ const transaction = db.transaction(() => {
130
+ db.prepare("INSERT INTO users (id, name) VALUES (?, ?)").run(userId, "Charlie")
131
+
132
+ eventBus.emit(
133
+ {
134
+ id: eventId,
135
+ type: "USER_CREATED",
136
+ payload: { userId },
137
+ occurredAt: new Date(),
138
+ },
139
+ db
140
+ ) // Pass db explicitly
141
+ })
142
+
143
+ transaction()
144
+
145
+ // Verify
146
+ const user = db.prepare("SELECT * FROM users WHERE id = ?").get(userId)
147
+ expect(user).toBeDefined()
148
+
149
+ const event = db.prepare("SELECT * FROM outbox_events WHERE id = ?").get(eventId)
150
+ expect(event).toBeDefined()
151
+ })
152
+
153
+ it("should rollback with explicit transaction parameter on failure", async () => {
154
+ const outbox = new SqliteBetterSqlite3Outbox({
155
+ dbPath: DB_PATH,
156
+ })
157
+
158
+ const eventBus = new OutboxEventBus(
159
+ outbox,
160
+ () => {},
161
+ () => {}
162
+ )
163
+
164
+ const eventId = "event-explicit-rollback"
165
+ const userId = "user-explicit-rollback"
166
+
167
+ const transaction = db.transaction(() => {
168
+ db.prepare("INSERT INTO users (id, name) VALUES (?, ?)").run(userId, "Dave")
169
+
170
+ eventBus.emit(
171
+ {
172
+ id: eventId,
173
+ type: "USER_CREATED",
174
+ payload: { userId },
175
+ occurredAt: new Date(),
176
+ },
177
+ db
178
+ )
179
+
180
+ throw new Error("Forced rollback")
181
+ })
182
+
183
+ try {
184
+ transaction()
185
+ } catch (_err) {
186
+ // Expected
187
+ }
188
+
189
+ // Verify
190
+ const user = db.prepare("SELECT * FROM users WHERE id = ?").get(userId)
191
+ expect(user).toBeUndefined()
192
+
193
+ const event = db.prepare("SELECT * FROM outbox_events WHERE id = ?").get(eventId)
194
+ expect(event).toBeUndefined()
195
+ })
196
+ })