@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.
- package/README.md +759 -0
- package/dist/index.cjs +259 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +75 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +75 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +228 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +54 -0
- package/src/error-handling.test.ts +145 -0
- package/src/index.ts +2 -0
- package/src/integration.e2e.ts +258 -0
- package/src/schema.sql +30 -0
- package/src/sqlite-better-sqlite3-outbox.ts +291 -0
- package/src/sync-transaction.test.ts +117 -0
- package/src/transaction-storage.ts +42 -0
- package/src/transactions.test.ts +196 -0
|
@@ -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
|
+
})
|