@starbemtech/star-db-query-builder 1.1.0 → 1.2.1
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/.github/workflows/publish.yml +118 -0
- package/ARCHITECTURE.md +313 -0
- package/CHANGELOG.md +65 -0
- package/README.md +1275 -210
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +131 -0
- package/coverage/lcov-report/mysqlClient.ts.html +685 -0
- package/coverage/lcov-report/pgClient.ts.html +823 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +533 -0
- package/coverage/mysqlClient.ts.html +685 -0
- package/coverage/pgClient.ts.html +823 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/src/core/repository.d.ts +374 -0
- package/dist/src/core/repository.js +677 -0
- package/dist/src/core/repository.js.map +1 -0
- package/dist/src/{default → core}/types.d.ts +9 -1
- package/dist/src/{default → core}/types.js.map +1 -1
- package/dist/src/core/utils.d.ts +133 -0
- package/dist/src/{default → core}/utils.js +167 -0
- package/dist/src/core/utils.js.map +1 -0
- package/dist/src/db/IDatabaseClient.d.ts +10 -1
- package/dist/src/db/initDb.d.ts +120 -1
- package/dist/src/db/initDb.js +119 -0
- package/dist/src/db/initDb.js.map +1 -1
- package/dist/src/db/mysqlClient.d.ts +23 -1
- package/dist/src/db/mysqlClient.js +115 -0
- package/dist/src/db/mysqlClient.js.map +1 -1
- package/dist/src/db/pgClient.d.ts +22 -1
- package/dist/src/db/pgClient.js +127 -0
- package/dist/src/db/pgClient.js.map +1 -1
- package/dist/src/monitor/monitor.d.ts +6 -1
- package/dist/src/monitor/monitor.js +5 -0
- package/dist/src/monitor/monitor.js.map +1 -1
- package/dist/src/setupTests.d.ts +26 -0
- package/dist/src/setupTests.js +43 -0
- package/dist/src/setupTests.js.map +1 -0
- package/docs/INDEX.md +145 -0
- package/docs/methods/findFirst.md +394 -0
- package/docs/methods/findMany.md +587 -0
- package/docs/methods/insert.md +536 -0
- package/docs/methods/insertMany.md +627 -0
- package/docs/methods/joins.md +781 -0
- package/docs/methods/rawQuery.md +284 -0
- package/docs/methods/transactions.md +737 -0
- package/eslint.config.mjs +77 -0
- package/index.ts +5 -4
- package/jest.config.ts +23 -28
- package/package.json +52 -29
- package/scripts/release.sh +123 -0
- package/src/core/repository.ts +865 -0
- package/src/{default → core}/types.ts +11 -2
- package/src/{default → core}/utils.ts +168 -0
- package/src/db/IDatabaseClient.ts +11 -1
- package/src/db/__tests__/mysqlClient.test.ts +262 -0
- package/src/db/__tests__/pgClient.test.ts +260 -0
- package/src/db/initDb.ts +120 -1
- package/src/db/mysqlClient.ts +119 -3
- package/src/db/pgClient.ts +131 -3
- package/src/monitor/monitor.ts +5 -0
- package/src/setupTests.ts +45 -0
- package/tsconfig.test.json +21 -0
- package/.eslintignore +0 -4
- package/.eslintrc.json +0 -32
- package/dist/.eslintrc.json +0 -32
- package/dist/src/default/genericRepository.d.ts +0 -28
- package/dist/src/default/genericRepository.js +0 -284
- package/dist/src/default/genericRepository.js.map +0 -1
- package/dist/src/default/utils.d.ts +0 -9
- package/dist/src/default/utils.js.map +0 -1
- package/index.d.ts +0 -60
- package/src/default/genericRepository.ts +0 -461
- /package/dist/src/{default → core}/types.js +0 -0
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
# Transactions
|
|
2
|
+
|
|
3
|
+
Execute multiple database operations within a single transaction to ensure data consistency and atomicity.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Transactions ensure that a series of database operations either all succeed or all fail together. This is crucial for maintaining data integrity when performing complex operations that involve multiple tables or records.
|
|
8
|
+
|
|
9
|
+
## Available Methods
|
|
10
|
+
|
|
11
|
+
### withTransaction
|
|
12
|
+
|
|
13
|
+
Executes a function within a database transaction with automatic commit/rollback handling.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
withTransaction<T>(
|
|
17
|
+
dbClient: IDatabaseClient,
|
|
18
|
+
transactionFn: (tx: ITransactionClient) => Promise<T>
|
|
19
|
+
): Promise<T>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### beginTransaction
|
|
23
|
+
|
|
24
|
+
Creates a transaction client for manual transaction management.
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
beginTransaction(dbClient: IDatabaseClient): Promise<ITransactionClient>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## ITransactionClient Interface
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
interface ITransactionClient {
|
|
34
|
+
query: <T>(sql: string, params?: any[]) => Promise<T>
|
|
35
|
+
commit: () => Promise<void>
|
|
36
|
+
rollback: () => Promise<void>
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Examples
|
|
41
|
+
|
|
42
|
+
### Basic Transaction with withTransaction
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import {
|
|
46
|
+
withTransaction,
|
|
47
|
+
insert,
|
|
48
|
+
update,
|
|
49
|
+
} from '@starbemtech/star-db-query-builder'
|
|
50
|
+
|
|
51
|
+
// Create user with profile in a single transaction
|
|
52
|
+
const createUserWithProfile = async (userData: any, profileData: any) => {
|
|
53
|
+
return withTransaction(dbClient, async (tx) => {
|
|
54
|
+
// Create user
|
|
55
|
+
const user = await insert({
|
|
56
|
+
tableName: 'users',
|
|
57
|
+
dbClient: tx,
|
|
58
|
+
data: userData,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Create user profile
|
|
62
|
+
const profile = await insert({
|
|
63
|
+
tableName: 'user_profiles',
|
|
64
|
+
dbClient: tx,
|
|
65
|
+
data: {
|
|
66
|
+
...profileData,
|
|
67
|
+
user_id: user.id,
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return { user, profile }
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Usage
|
|
76
|
+
try {
|
|
77
|
+
const result = await createUserWithProfile(
|
|
78
|
+
{ name: 'John Doe', email: 'john@example.com' },
|
|
79
|
+
{ bio: 'Software developer', location: 'New York' }
|
|
80
|
+
)
|
|
81
|
+
console.log('User and profile created successfully:', result)
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Transaction failed:', error.message)
|
|
84
|
+
// Both user and profile creation were rolled back
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### E-commerce Order Processing
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
const processOrder = async (orderData: any, orderItems: any[]) => {
|
|
92
|
+
return withTransaction(dbClient, async (tx) => {
|
|
93
|
+
// Create order
|
|
94
|
+
const order = await insert({
|
|
95
|
+
tableName: 'orders',
|
|
96
|
+
dbClient: tx,
|
|
97
|
+
data: {
|
|
98
|
+
...orderData,
|
|
99
|
+
status: 'pending',
|
|
100
|
+
total: 0, // Will be calculated
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
let totalAmount = 0
|
|
105
|
+
|
|
106
|
+
// Create order items and calculate total
|
|
107
|
+
for (const item of orderItems) {
|
|
108
|
+
const orderItem = await insert({
|
|
109
|
+
tableName: 'order_items',
|
|
110
|
+
dbClient: tx,
|
|
111
|
+
data: {
|
|
112
|
+
...item,
|
|
113
|
+
order_id: order.id,
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
totalAmount += item.price * item.quantity
|
|
118
|
+
|
|
119
|
+
// Update product stock
|
|
120
|
+
await update({
|
|
121
|
+
tableName: 'products',
|
|
122
|
+
dbClient: tx,
|
|
123
|
+
id: item.product_id,
|
|
124
|
+
data: {
|
|
125
|
+
stock: { operator: '-', value: item.quantity },
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Update order total
|
|
131
|
+
await update({
|
|
132
|
+
tableName: 'orders',
|
|
133
|
+
dbClient: tx,
|
|
134
|
+
id: order.id,
|
|
135
|
+
data: {
|
|
136
|
+
total: totalAmount,
|
|
137
|
+
status: 'confirmed',
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
return { order, totalAmount }
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Account Transfer
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
const transferMoney = async (
|
|
150
|
+
fromAccountId: string,
|
|
151
|
+
toAccountId: string,
|
|
152
|
+
amount: number
|
|
153
|
+
) => {
|
|
154
|
+
return withTransaction(dbClient, async (tx) => {
|
|
155
|
+
// Check sender balance
|
|
156
|
+
const fromAccount = await findFirst({
|
|
157
|
+
tableName: 'accounts',
|
|
158
|
+
dbClient: tx,
|
|
159
|
+
where: { id: { operator: '=', value: fromAccountId } },
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
if (!fromAccount || fromAccount.balance < amount) {
|
|
163
|
+
throw new Error('Insufficient funds')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Debit from sender
|
|
167
|
+
await update({
|
|
168
|
+
tableName: 'accounts',
|
|
169
|
+
dbClient: tx,
|
|
170
|
+
id: fromAccountId,
|
|
171
|
+
data: {
|
|
172
|
+
balance: { operator: '-', value: amount },
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
// Credit to receiver
|
|
177
|
+
await update({
|
|
178
|
+
tableName: 'accounts',
|
|
179
|
+
dbClient: tx,
|
|
180
|
+
id: toAccountId,
|
|
181
|
+
data: {
|
|
182
|
+
balance: { operator: '+', value: amount },
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// Create transaction record
|
|
187
|
+
const transaction = await insert({
|
|
188
|
+
tableName: 'transactions',
|
|
189
|
+
dbClient: tx,
|
|
190
|
+
data: {
|
|
191
|
+
from_account_id: fromAccountId,
|
|
192
|
+
to_account_id: toAccountId,
|
|
193
|
+
amount,
|
|
194
|
+
type: 'transfer',
|
|
195
|
+
status: 'completed',
|
|
196
|
+
},
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
return transaction
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Manual Transaction Management
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import {
|
|
208
|
+
beginTransaction,
|
|
209
|
+
insert,
|
|
210
|
+
update,
|
|
211
|
+
} from '@starbemtech/star-db-query-builder'
|
|
212
|
+
|
|
213
|
+
const complexOperation = async () => {
|
|
214
|
+
const transaction = await beginTransaction(dbClient)
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
// First operation
|
|
218
|
+
const user = await insert({
|
|
219
|
+
tableName: 'users',
|
|
220
|
+
dbClient: transaction,
|
|
221
|
+
data: { name: 'John Doe', email: 'john@example.com' },
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// Second operation
|
|
225
|
+
const profile = await insert({
|
|
226
|
+
tableName: 'user_profiles',
|
|
227
|
+
dbClient: transaction,
|
|
228
|
+
data: { user_id: user.id, bio: 'Hello world' },
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// Third operation
|
|
232
|
+
await update({
|
|
233
|
+
tableName: 'users',
|
|
234
|
+
dbClient: transaction,
|
|
235
|
+
id: user.id,
|
|
236
|
+
data: { profile_created: true },
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
// Commit all changes
|
|
240
|
+
await transaction.commit()
|
|
241
|
+
return { user, profile }
|
|
242
|
+
} catch (error) {
|
|
243
|
+
// Rollback on any error
|
|
244
|
+
await transaction.rollback()
|
|
245
|
+
throw error
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Nested Operations with Error Handling
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
const createUserWithMultipleRelations = async (userData: any) => {
|
|
254
|
+
return withTransaction(dbClient, async (tx) => {
|
|
255
|
+
try {
|
|
256
|
+
// Create user
|
|
257
|
+
const user = await insert({
|
|
258
|
+
tableName: 'users',
|
|
259
|
+
dbClient: tx,
|
|
260
|
+
data: userData,
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// Create user preferences
|
|
264
|
+
const preferences = await insert({
|
|
265
|
+
tableName: 'user_preferences',
|
|
266
|
+
dbClient: tx,
|
|
267
|
+
data: {
|
|
268
|
+
user_id: user.id,
|
|
269
|
+
theme: 'dark',
|
|
270
|
+
notifications: true,
|
|
271
|
+
},
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
// Create user settings
|
|
275
|
+
const settings = await insert({
|
|
276
|
+
tableName: 'user_settings',
|
|
277
|
+
dbClient: tx,
|
|
278
|
+
data: {
|
|
279
|
+
user_id: user.id,
|
|
280
|
+
language: 'en',
|
|
281
|
+
timezone: 'UTC',
|
|
282
|
+
},
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// Create audit log
|
|
286
|
+
await insert({
|
|
287
|
+
tableName: 'audit_logs',
|
|
288
|
+
dbClient: tx,
|
|
289
|
+
data: {
|
|
290
|
+
user_id: user.id,
|
|
291
|
+
action: 'user_created',
|
|
292
|
+
details: JSON.stringify({ email: user.email }),
|
|
293
|
+
},
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
return { user, preferences, settings }
|
|
297
|
+
} catch (error) {
|
|
298
|
+
// Transaction will be automatically rolled back
|
|
299
|
+
console.error('Failed to create user with relations:', error)
|
|
300
|
+
throw error
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Batch Operations with Transactions
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
const bulkUserCreation = async (usersData: any[]) => {
|
|
310
|
+
return withTransaction(dbClient, async (tx) => {
|
|
311
|
+
const results = []
|
|
312
|
+
|
|
313
|
+
for (const userData of usersData) {
|
|
314
|
+
// Create user
|
|
315
|
+
const user = await insert({
|
|
316
|
+
tableName: 'users',
|
|
317
|
+
dbClient: tx,
|
|
318
|
+
data: userData,
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
// Create default profile
|
|
322
|
+
const profile = await insert({
|
|
323
|
+
tableName: 'user_profiles',
|
|
324
|
+
dbClient: tx,
|
|
325
|
+
data: {
|
|
326
|
+
user_id: user.id,
|
|
327
|
+
bio: 'New user',
|
|
328
|
+
created_at: new Date(),
|
|
329
|
+
},
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
results.push({ user, profile })
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Create batch audit log
|
|
336
|
+
await insert({
|
|
337
|
+
tableName: 'audit_logs',
|
|
338
|
+
dbClient: tx,
|
|
339
|
+
data: {
|
|
340
|
+
action: 'bulk_user_creation',
|
|
341
|
+
details: JSON.stringify({
|
|
342
|
+
count: usersData.length,
|
|
343
|
+
user_ids: results.map((r) => r.user.id),
|
|
344
|
+
}),
|
|
345
|
+
},
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
return results
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Conditional Transaction Logic
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
const processPayment = async (paymentData: any) => {
|
|
357
|
+
return withTransaction(dbClient, async (tx) => {
|
|
358
|
+
// Create payment record
|
|
359
|
+
const payment = await insert({
|
|
360
|
+
tableName: 'payments',
|
|
361
|
+
dbClient: tx,
|
|
362
|
+
data: {
|
|
363
|
+
...paymentData,
|
|
364
|
+
status: 'processing',
|
|
365
|
+
},
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
// Check if payment amount is above threshold
|
|
369
|
+
if (paymentData.amount > 1000) {
|
|
370
|
+
// Require manual approval for large payments
|
|
371
|
+
await insert({
|
|
372
|
+
tableName: 'payment_approvals',
|
|
373
|
+
dbClient: tx,
|
|
374
|
+
data: {
|
|
375
|
+
payment_id: payment.id,
|
|
376
|
+
status: 'pending',
|
|
377
|
+
requires_approval: true,
|
|
378
|
+
},
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
// Update payment status
|
|
382
|
+
await update({
|
|
383
|
+
tableName: 'payments',
|
|
384
|
+
dbClient: tx,
|
|
385
|
+
id: payment.id,
|
|
386
|
+
data: { status: 'pending_approval' },
|
|
387
|
+
})
|
|
388
|
+
} else {
|
|
389
|
+
// Auto-approve small payments
|
|
390
|
+
await update({
|
|
391
|
+
tableName: 'payments',
|
|
392
|
+
dbClient: tx,
|
|
393
|
+
id: payment.id,
|
|
394
|
+
data: { status: 'approved' },
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
// Process the payment
|
|
398
|
+
await processApprovedPayment(payment.id, tx)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return payment
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const processApprovedPayment = async (
|
|
406
|
+
paymentId: string,
|
|
407
|
+
tx: ITransactionClient
|
|
408
|
+
) => {
|
|
409
|
+
// Additional payment processing logic
|
|
410
|
+
await update({
|
|
411
|
+
tableName: 'payments',
|
|
412
|
+
dbClient: tx,
|
|
413
|
+
id: paymentId,
|
|
414
|
+
data: { status: 'completed', processed_at: new Date() },
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Best Practices
|
|
420
|
+
|
|
421
|
+
### 1. Keep Transactions Short
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
// Good: Short, focused transaction
|
|
425
|
+
const updateUserStatus = async (userId: string, status: string) => {
|
|
426
|
+
return withTransaction(dbClient, async (tx) => {
|
|
427
|
+
await update({
|
|
428
|
+
tableName: 'users',
|
|
429
|
+
dbClient: tx,
|
|
430
|
+
id: userId,
|
|
431
|
+
data: { status },
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
await insert({
|
|
435
|
+
tableName: 'user_status_history',
|
|
436
|
+
dbClient: tx,
|
|
437
|
+
data: { user_id: userId, status, changed_at: new Date() },
|
|
438
|
+
})
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Avoid: Long-running transactions
|
|
443
|
+
const badTransaction = async () => {
|
|
444
|
+
return withTransaction(dbClient, async (tx) => {
|
|
445
|
+
// ... many operations
|
|
446
|
+
await someSlowOperation() // This could timeout
|
|
447
|
+
// ... more operations
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### 2. Handle Errors Properly
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
const safeTransaction = async () => {
|
|
456
|
+
try {
|
|
457
|
+
return await withTransaction(dbClient, async (tx) => {
|
|
458
|
+
// Transaction operations
|
|
459
|
+
const result = await someOperation(tx)
|
|
460
|
+
return result
|
|
461
|
+
})
|
|
462
|
+
} catch (error) {
|
|
463
|
+
// Transaction was automatically rolled back
|
|
464
|
+
console.error('Transaction failed:', error.message)
|
|
465
|
+
|
|
466
|
+
// Handle specific error types
|
|
467
|
+
if (error.message.includes('duplicate key')) {
|
|
468
|
+
throw new Error('Record already exists')
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
throw error
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### 3. Use Appropriate Isolation Levels
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
// For read-heavy operations, consider using read-only transactions
|
|
480
|
+
const getReportData = async () => {
|
|
481
|
+
return withTransaction(dbClient, async (tx) => {
|
|
482
|
+
// Set transaction to read-only (database-specific)
|
|
483
|
+
await tx.query('SET TRANSACTION READ ONLY')
|
|
484
|
+
|
|
485
|
+
const users = await findMany({
|
|
486
|
+
tableName: 'users',
|
|
487
|
+
dbClient: tx,
|
|
488
|
+
where: { status: { operator: '=', value: 'active' } },
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
const orders = await findMany({
|
|
492
|
+
tableName: 'orders',
|
|
493
|
+
dbClient: tx,
|
|
494
|
+
where: { status: { operator: '=', value: 'completed' } },
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
return { users, orders }
|
|
498
|
+
})
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### 4. Avoid Nested Transactions
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
// Good: Single transaction for related operations
|
|
506
|
+
const createOrderWithItems = async (orderData: any, items: any[]) => {
|
|
507
|
+
return withTransaction(dbClient, async (tx) => {
|
|
508
|
+
const order = await insert({
|
|
509
|
+
tableName: 'orders',
|
|
510
|
+
dbClient: tx,
|
|
511
|
+
data: orderData,
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
for (const item of items) {
|
|
515
|
+
await insert({
|
|
516
|
+
tableName: 'order_items',
|
|
517
|
+
dbClient: tx,
|
|
518
|
+
data: { ...item, order_id: order.id },
|
|
519
|
+
})
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return order
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Avoid: Nested transactions (not supported by most databases)
|
|
527
|
+
const badNestedTransaction = async () => {
|
|
528
|
+
return withTransaction(dbClient, async (tx1) => {
|
|
529
|
+
// ... operations
|
|
530
|
+
|
|
531
|
+
return withTransaction(dbClient, async (tx2) => {
|
|
532
|
+
// This won't work as expected
|
|
533
|
+
})
|
|
534
|
+
})
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
## Error Handling
|
|
539
|
+
|
|
540
|
+
### Common Transaction Errors
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
const handleTransactionErrors = async () => {
|
|
544
|
+
try {
|
|
545
|
+
return await withTransaction(dbClient, async (tx) => {
|
|
546
|
+
// Your transaction logic
|
|
547
|
+
})
|
|
548
|
+
} catch (error) {
|
|
549
|
+
if (error.message.includes('deadlock detected')) {
|
|
550
|
+
// Handle deadlock - you might want to retry
|
|
551
|
+
console.warn('Deadlock detected, retrying...')
|
|
552
|
+
// Implement retry logic
|
|
553
|
+
} else if (error.message.includes('serialization failure')) {
|
|
554
|
+
// Handle serialization failure
|
|
555
|
+
console.warn('Serialization failure, retrying...')
|
|
556
|
+
// Implement retry logic
|
|
557
|
+
} else if (error.message.includes('connection lost')) {
|
|
558
|
+
// Handle connection issues
|
|
559
|
+
console.error('Database connection lost')
|
|
560
|
+
// Implement reconnection logic
|
|
561
|
+
} else {
|
|
562
|
+
// Handle other errors
|
|
563
|
+
console.error('Transaction error:', error.message)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
throw error
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### Retry Logic for Transient Errors
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
const retryTransaction = async <T>(
|
|
575
|
+
transactionFn: (tx: ITransactionClient) => Promise<T>,
|
|
576
|
+
maxRetries: number = 3
|
|
577
|
+
): Promise<T> => {
|
|
578
|
+
let lastError: Error
|
|
579
|
+
|
|
580
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
581
|
+
try {
|
|
582
|
+
return await withTransaction(dbClient, transactionFn)
|
|
583
|
+
} catch (error) {
|
|
584
|
+
lastError = error as Error
|
|
585
|
+
|
|
586
|
+
// Check if error is retryable
|
|
587
|
+
if (isRetryableError(error) && attempt < maxRetries) {
|
|
588
|
+
const delay = Math.pow(2, attempt) * 1000 // Exponential backoff
|
|
589
|
+
console.warn(
|
|
590
|
+
`Transaction attempt ${attempt} failed, retrying in ${delay}ms...`
|
|
591
|
+
)
|
|
592
|
+
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
593
|
+
continue
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
throw error
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
throw lastError!
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const isRetryableError = (error: any): boolean => {
|
|
604
|
+
const retryableErrors = [
|
|
605
|
+
'deadlock detected',
|
|
606
|
+
'serialization failure',
|
|
607
|
+
'connection lost',
|
|
608
|
+
'timeout',
|
|
609
|
+
]
|
|
610
|
+
|
|
611
|
+
return retryableErrors.some((msg) =>
|
|
612
|
+
error.message?.toLowerCase().includes(msg)
|
|
613
|
+
)
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
## Performance Considerations
|
|
618
|
+
|
|
619
|
+
### 1. Connection Pooling
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
// Ensure your database client is configured with proper connection pooling
|
|
623
|
+
await initDb({
|
|
624
|
+
type: 'pg',
|
|
625
|
+
options: {
|
|
626
|
+
host: 'localhost',
|
|
627
|
+
port: 5432,
|
|
628
|
+
database: 'myapp',
|
|
629
|
+
user: 'username',
|
|
630
|
+
password: 'password',
|
|
631
|
+
max: 20, // Maximum connections in pool
|
|
632
|
+
idleTimeoutMillis: 30000,
|
|
633
|
+
connectionTimeoutMillis: 2000,
|
|
634
|
+
},
|
|
635
|
+
})
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### 2. Transaction Timeout
|
|
639
|
+
|
|
640
|
+
```typescript
|
|
641
|
+
// Set appropriate timeouts for transactions
|
|
642
|
+
const quickTransaction = async () => {
|
|
643
|
+
return withTransaction(dbClient, async (tx) => {
|
|
644
|
+
// Set a timeout for this transaction
|
|
645
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
646
|
+
setTimeout(() => reject(new Error('Transaction timeout')), 5000)
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
const transactionPromise = (async () => {
|
|
650
|
+
// Your transaction logic
|
|
651
|
+
return await someOperation(tx)
|
|
652
|
+
})()
|
|
653
|
+
|
|
654
|
+
return Promise.race([transactionPromise, timeoutPromise])
|
|
655
|
+
})
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### 3. Batch Operations
|
|
660
|
+
|
|
661
|
+
```typescript
|
|
662
|
+
// For large batch operations, consider processing in chunks
|
|
663
|
+
const bulkUpdateWithTransactions = async (
|
|
664
|
+
records: any[],
|
|
665
|
+
chunkSize: number = 100
|
|
666
|
+
) => {
|
|
667
|
+
const results = []
|
|
668
|
+
|
|
669
|
+
for (let i = 0; i < records.length; i += chunkSize) {
|
|
670
|
+
const chunk = records.slice(i, i + chunkSize)
|
|
671
|
+
|
|
672
|
+
const chunkResult = await withTransaction(dbClient, async (tx) => {
|
|
673
|
+
const chunkResults = []
|
|
674
|
+
|
|
675
|
+
for (const record of chunk) {
|
|
676
|
+
const result = await update({
|
|
677
|
+
tableName: 'records',
|
|
678
|
+
dbClient: tx,
|
|
679
|
+
id: record.id,
|
|
680
|
+
data: record.data,
|
|
681
|
+
})
|
|
682
|
+
chunkResults.push(result)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return chunkResults
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
results.push(...chunkResult)
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return results
|
|
692
|
+
}
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
## Database-Specific Considerations
|
|
696
|
+
|
|
697
|
+
### PostgreSQL
|
|
698
|
+
|
|
699
|
+
- Supports nested transactions (savepoints)
|
|
700
|
+
- Has excellent transaction isolation
|
|
701
|
+
- Supports advisory locks for complex scenarios
|
|
702
|
+
|
|
703
|
+
### MySQL
|
|
704
|
+
|
|
705
|
+
- Uses autocommit mode by default
|
|
706
|
+
- Supports different isolation levels
|
|
707
|
+
- Has limitations with nested transactions
|
|
708
|
+
|
|
709
|
+
## Monitoring and Logging
|
|
710
|
+
|
|
711
|
+
```typescript
|
|
712
|
+
// Monitor transaction events
|
|
713
|
+
import { monitor, MonitorEvents } from '@starbemtech/star-db-query-builder'
|
|
714
|
+
|
|
715
|
+
monitor.on(MonitorEvents.TRANSACTION_COMMIT, (data) => {
|
|
716
|
+
console.log('Transaction committed:', data)
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
monitor.on(MonitorEvents.TRANSACTION_ROLLBACK, (data) => {
|
|
720
|
+
console.log('Transaction rolled back:', data)
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
monitor.on(MonitorEvents.QUERY_START, (data) => {
|
|
724
|
+
if (data.inTransaction) {
|
|
725
|
+
console.log('Query in transaction:', data.sql)
|
|
726
|
+
}
|
|
727
|
+
})
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
## Summary
|
|
731
|
+
|
|
732
|
+
Transactions are essential for maintaining data consistency in complex operations. The library provides two main approaches:
|
|
733
|
+
|
|
734
|
+
1. **`withTransaction`**: Automatic transaction management with commit/rollback
|
|
735
|
+
2. **`beginTransaction`**: Manual transaction control for advanced scenarios
|
|
736
|
+
|
|
737
|
+
Always handle errors properly and keep transactions as short as possible to avoid performance issues and deadlocks.
|