@pineliner/odb-client 1.0.7 → 1.0.9
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/dist/database/adapters/bun-sqlite.d.ts.map +1 -1
- package/dist/database/adapters/libsql.d.ts.map +1 -1
- package/dist/database/adapters/odblite.d.ts.map +1 -1
- package/dist/database/types.d.ts +26 -2
- package/dist/database/types.d.ts.map +1 -1
- package/dist/index.cjs +195 -29
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +194 -30
- package/dist/orm/index.d.ts +79 -1
- package/dist/orm/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/database/adapters/bun-sqlite.ts +59 -5
- package/src/database/adapters/libsql.ts +66 -6
- package/src/database/adapters/odblite.ts +79 -16
- package/src/database/types.ts +28 -2
- package/src/index.ts +1 -0
- package/src/orm/index.ts +216 -1
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
QueryResult,
|
|
6
6
|
PreparedStatement,
|
|
7
7
|
LibSQLConfig,
|
|
8
|
+
QueryOptions,
|
|
8
9
|
} from '../types'
|
|
9
10
|
import {
|
|
10
11
|
convertTemplateToQuery,
|
|
@@ -17,6 +18,48 @@ import {
|
|
|
17
18
|
where
|
|
18
19
|
} from '../sql-template'
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Parse JSON columns in query results
|
|
23
|
+
* Only parses if the value is a string (to avoid double-parsing)
|
|
24
|
+
*/
|
|
25
|
+
function parseJsonColumns<T = any>(rows: any[], jsonColumns?: string[]): T[] {
|
|
26
|
+
if (!jsonColumns || jsonColumns.length === 0) {
|
|
27
|
+
return rows
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return rows.map(row => {
|
|
31
|
+
const parsed = { ...row }
|
|
32
|
+
for (const col of jsonColumns) {
|
|
33
|
+
if (col in parsed && typeof parsed[col] === 'string') {
|
|
34
|
+
try {
|
|
35
|
+
parsed[col] = JSON.parse(parsed[col])
|
|
36
|
+
} catch {
|
|
37
|
+
// Keep original value if parsing fails
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return parsed
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Stringify JSON parameters for INSERT/UPDATE queries
|
|
47
|
+
* Only stringifies if the value is an object/array (not already a string)
|
|
48
|
+
*/
|
|
49
|
+
function stringifyJsonParams(params: any[], stringifyParams?: Record<string, number>): any[] {
|
|
50
|
+
if (!stringifyParams || Object.keys(stringifyParams).length === 0) {
|
|
51
|
+
return params
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const result = [...params]
|
|
55
|
+
for (const [_columnName, index] of Object.entries(stringifyParams)) {
|
|
56
|
+
if (index < result.length && result[index] != null && typeof result[index] === 'object') {
|
|
57
|
+
result[index] = JSON.stringify(result[index])
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return result
|
|
61
|
+
}
|
|
62
|
+
|
|
20
63
|
/**
|
|
21
64
|
* LibSQL adapter for DatabaseManager
|
|
22
65
|
* Wraps @libsql/client with Connection interface
|
|
@@ -117,15 +160,15 @@ class LibSQLConnection implements Connection {
|
|
|
117
160
|
/**
|
|
118
161
|
* Query with SQL string and parameters (alias for execute)
|
|
119
162
|
*/
|
|
120
|
-
async query<T = any>(sql: string, params: any[] = []): Promise<QueryResult<T>> {
|
|
121
|
-
return this.execute(sql, params) as Promise<QueryResult<T>>
|
|
163
|
+
async query<T = any>(sql: string, params: any[] = [], options?: QueryOptions): Promise<QueryResult<T>> {
|
|
164
|
+
return this.execute(sql, params, options) as Promise<QueryResult<T>>
|
|
122
165
|
}
|
|
123
166
|
|
|
124
167
|
/**
|
|
125
168
|
* Execute SQL with parameters
|
|
126
169
|
* Supports both formats: execute(sql, params) and execute({sql, args})
|
|
127
170
|
*/
|
|
128
|
-
async execute(sql: string | { sql: string; args?: any[] }, params: any[] = []): Promise<QueryResult> {
|
|
171
|
+
async execute(sql: string | { sql: string; args?: any[] }, params: any[] = [], options?: QueryOptions): Promise<QueryResult> {
|
|
129
172
|
try {
|
|
130
173
|
const target = this.txClient || this.client
|
|
131
174
|
|
|
@@ -137,19 +180,36 @@ class LibSQLConnection implements Connection {
|
|
|
137
180
|
console.log('[LibSQL] Executing SQL:', sql)
|
|
138
181
|
console.log('[LibSQL] With params:', params)
|
|
139
182
|
}
|
|
140
|
-
|
|
183
|
+
let args = params
|
|
184
|
+
// Stringify JSON parameters if specified
|
|
185
|
+
if (options?.stringifyParams) {
|
|
186
|
+
args = stringifyJsonParams(args, options.stringifyParams)
|
|
187
|
+
}
|
|
188
|
+
query = { sql, args }
|
|
141
189
|
} else {
|
|
142
190
|
if (process.env.DEBUG_SQL) {
|
|
143
191
|
console.log('[LibSQL] Executing SQL:', sql.sql)
|
|
144
192
|
console.log('[LibSQL] With args:', sql.args)
|
|
145
193
|
}
|
|
146
|
-
|
|
194
|
+
let args = sql.args || []
|
|
195
|
+
// Stringify JSON parameters if specified
|
|
196
|
+
if (options?.stringifyParams) {
|
|
197
|
+
args = stringifyJsonParams(args, options.stringifyParams)
|
|
198
|
+
}
|
|
199
|
+
query = { sql: sql.sql, args }
|
|
147
200
|
}
|
|
148
201
|
|
|
149
202
|
const result = await target.execute(query)
|
|
150
203
|
|
|
204
|
+
let rows = result.rows as any[]
|
|
205
|
+
|
|
206
|
+
// Parse JSON columns if specified
|
|
207
|
+
if (options?.jsonColumns) {
|
|
208
|
+
rows = parseJsonColumns(rows, options.jsonColumns)
|
|
209
|
+
}
|
|
210
|
+
|
|
151
211
|
return {
|
|
152
|
-
rows
|
|
212
|
+
rows,
|
|
153
213
|
rowsAffected: Number(result.rowsAffected),
|
|
154
214
|
lastInsertRowid: result.lastInsertRowid
|
|
155
215
|
? BigInt(result.lastInsertRowid.toString())
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
QueryResult,
|
|
7
7
|
PreparedStatement,
|
|
8
8
|
ODBLiteConfig,
|
|
9
|
+
QueryOptions,
|
|
9
10
|
} from '../types'
|
|
10
11
|
import {
|
|
11
12
|
convertTemplateToQuery,
|
|
@@ -18,6 +19,48 @@ import {
|
|
|
18
19
|
where
|
|
19
20
|
} from '../sql-template'
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Parse JSON columns in query results
|
|
24
|
+
* Only parses if the value is a string (to avoid double-parsing)
|
|
25
|
+
*/
|
|
26
|
+
function parseJsonColumns<T = any>(rows: any[], jsonColumns?: string[]): T[] {
|
|
27
|
+
if (!jsonColumns || jsonColumns.length === 0) {
|
|
28
|
+
return rows
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return rows.map(row => {
|
|
32
|
+
const parsed = { ...row }
|
|
33
|
+
for (const col of jsonColumns) {
|
|
34
|
+
if (col in parsed && typeof parsed[col] === 'string') {
|
|
35
|
+
try {
|
|
36
|
+
parsed[col] = JSON.parse(parsed[col])
|
|
37
|
+
} catch {
|
|
38
|
+
// Keep original value if parsing fails
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return parsed
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Stringify JSON parameters for INSERT/UPDATE queries
|
|
48
|
+
* Only stringifies if the value is an object/array (not already a string)
|
|
49
|
+
*/
|
|
50
|
+
function stringifyJsonParams(params: any[], stringifyParams?: Record<string, number>): any[] {
|
|
51
|
+
if (!stringifyParams || Object.keys(stringifyParams).length === 0) {
|
|
52
|
+
return params
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = [...params]
|
|
56
|
+
for (const [_columnName, index] of Object.entries(stringifyParams)) {
|
|
57
|
+
if (index < result.length && result[index] != null && typeof result[index] === 'object') {
|
|
58
|
+
result[index] = JSON.stringify(result[index])
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result
|
|
62
|
+
}
|
|
63
|
+
|
|
21
64
|
/**
|
|
22
65
|
* ODB-Lite adapter for DatabaseManager
|
|
23
66
|
* Wraps ServiceClient and ODBLiteClient with Connection interface
|
|
@@ -162,15 +205,19 @@ class ODBLiteConnection implements Connection {
|
|
|
162
205
|
/**
|
|
163
206
|
* Query with SQL string and parameters (alias for execute)
|
|
164
207
|
*/
|
|
165
|
-
async query<T = any>(sql: string, params: any[] = []): Promise<QueryResult<T>> {
|
|
166
|
-
return this.execute(sql, params) as Promise<QueryResult<T>>
|
|
208
|
+
async query<T = any>(sql: string, params: any[] = [], options?: QueryOptions): Promise<QueryResult<T>> {
|
|
209
|
+
return this.execute(sql, params, options) as Promise<QueryResult<T>>
|
|
167
210
|
}
|
|
168
211
|
|
|
169
212
|
/**
|
|
170
213
|
* Execute SQL with parameters
|
|
171
214
|
*/
|
|
172
|
-
async execute(sql: string | { sql: string; args?: any[] }, params: any[] = []): Promise<QueryResult> {
|
|
215
|
+
async execute(sql: string | { sql: string; args?: any[] }, params: any[] = [], options?: QueryOptions): Promise<QueryResult> {
|
|
173
216
|
try {
|
|
217
|
+
let rows: any[]
|
|
218
|
+
let rowsAffected: number
|
|
219
|
+
let lastInsertRowid: any
|
|
220
|
+
|
|
174
221
|
// Handle object format { sql, args }
|
|
175
222
|
if (typeof sql === 'object') {
|
|
176
223
|
// Debug logging for SQL errors
|
|
@@ -178,25 +225,41 @@ class ODBLiteConnection implements Connection {
|
|
|
178
225
|
console.log('[ODBLite] Executing SQL:', sql.sql)
|
|
179
226
|
console.log('[ODBLite] With args:', sql.args)
|
|
180
227
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
228
|
+
let args = sql.args || []
|
|
229
|
+
// Stringify JSON parameters if specified
|
|
230
|
+
if (options?.stringifyParams) {
|
|
231
|
+
args = stringifyJsonParams(args, options.stringifyParams)
|
|
232
|
+
}
|
|
233
|
+
const result = await this.client.sql.execute(sql.sql, args)
|
|
234
|
+
rows = result.rows
|
|
235
|
+
rowsAffected = result.rowsAffected || 0
|
|
236
|
+
lastInsertRowid = (result as any).lastInsertRowid
|
|
237
|
+
} else {
|
|
238
|
+
// Handle string format
|
|
239
|
+
if (process.env.DEBUG_SQL) {
|
|
240
|
+
console.log('[ODBLite] Executing SQL:', sql)
|
|
241
|
+
console.log('[ODBLite] With params:', params)
|
|
242
|
+
}
|
|
243
|
+
let args = params
|
|
244
|
+
// Stringify JSON parameters if specified
|
|
245
|
+
if (options?.stringifyParams) {
|
|
246
|
+
args = stringifyJsonParams(args, options.stringifyParams)
|
|
186
247
|
}
|
|
248
|
+
const result = await this.client.sql.execute(sql, args)
|
|
249
|
+
rows = result.rows
|
|
250
|
+
rowsAffected = result.rowsAffected || 0
|
|
251
|
+
lastInsertRowid = (result as any).lastInsertRowid
|
|
187
252
|
}
|
|
188
253
|
|
|
189
|
-
//
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
console.log('[ODBLite] With params:', params)
|
|
254
|
+
// Parse JSON columns if specified
|
|
255
|
+
if (options?.jsonColumns) {
|
|
256
|
+
rows = parseJsonColumns(rows, options.jsonColumns)
|
|
193
257
|
}
|
|
194
|
-
const result = await this.client.sql.execute(sql, params)
|
|
195
258
|
|
|
196
259
|
return {
|
|
197
|
-
rows
|
|
198
|
-
rowsAffected
|
|
199
|
-
lastInsertRowid
|
|
260
|
+
rows,
|
|
261
|
+
rowsAffected,
|
|
262
|
+
lastInsertRowid,
|
|
200
263
|
}
|
|
201
264
|
} catch (error: any) {
|
|
202
265
|
// Re-throw the original error to let the application handle it
|
package/src/database/types.ts
CHANGED
|
@@ -25,6 +25,32 @@ export interface QueryResult<T = any> {
|
|
|
25
25
|
lastInsertRowid?: number | bigint
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Query options for fine-grained control
|
|
30
|
+
*/
|
|
31
|
+
export interface QueryOptions {
|
|
32
|
+
/**
|
|
33
|
+
* List of column names that should be auto-parsed as JSON when reading
|
|
34
|
+
* Only applies to columns that contain stringified JSON in SELECT queries
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* await conn.query('SELECT * FROM suppliers', [], { jsonColumns: ['connections', 'config'] })
|
|
38
|
+
*/
|
|
39
|
+
jsonColumns?: string[]
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Automatically stringify parameters for JSON columns in UPDATE/INSERT queries
|
|
43
|
+
* Maps column names to parameter indices (0-based)
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // UPDATE suppliers SET connections = ?, updated_at = ? WHERE id = ?
|
|
47
|
+
* await conn.execute(query, [connections, now, id], {
|
|
48
|
+
* stringifyParams: { connections: 0 } // Auto-stringify first parameter
|
|
49
|
+
* })
|
|
50
|
+
*/
|
|
51
|
+
stringifyParams?: Record<string, number>
|
|
52
|
+
}
|
|
53
|
+
|
|
28
54
|
/**
|
|
29
55
|
* Unified database connection interface
|
|
30
56
|
* All backends implement this through adapters
|
|
@@ -43,8 +69,8 @@ export interface Connection {
|
|
|
43
69
|
sql(value: any, ...keys: string[]): any
|
|
44
70
|
|
|
45
71
|
// Standard query methods (return QueryResult)
|
|
46
|
-
query<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>>
|
|
47
|
-
execute(sql: string | { sql: string; args?: any[] }, params?: any[]): Promise<QueryResult>
|
|
72
|
+
query<T = any>(sql: string, params?: any[], options?: QueryOptions): Promise<QueryResult<T>>
|
|
73
|
+
execute(sql: string | { sql: string; args?: any[] }, params?: any[], options?: QueryOptions): Promise<QueryResult>
|
|
48
74
|
prepare(sql: string): PreparedStatement
|
|
49
75
|
|
|
50
76
|
// Transaction support
|
package/src/index.ts
CHANGED
package/src/orm/index.ts
CHANGED
|
@@ -461,6 +461,129 @@ class TableQueryBuilder {
|
|
|
461
461
|
}
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
+
// ============================================================
|
|
465
|
+
// MANUAL TRANSACTION CLASS
|
|
466
|
+
// ============================================================
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Manual Transaction Control
|
|
470
|
+
* Allows explicit commit/rollback control
|
|
471
|
+
*/
|
|
472
|
+
export class Transaction {
|
|
473
|
+
private orm: ORM
|
|
474
|
+
private isCommitted: boolean = false
|
|
475
|
+
private isRolledBack: boolean = false
|
|
476
|
+
private db: Connection
|
|
477
|
+
private savepointName?: string
|
|
478
|
+
|
|
479
|
+
constructor(db: Connection, transactionDepth: number = 0, savepointName?: string) {
|
|
480
|
+
this.db = db
|
|
481
|
+
this.orm = new ORM(db, transactionDepth)
|
|
482
|
+
this.savepointName = savepointName
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Access ORM methods (select, insert, update, delete)
|
|
487
|
+
*/
|
|
488
|
+
select(fields?: Record<string, any>) {
|
|
489
|
+
this.checkNotFinalized()
|
|
490
|
+
return this.orm.select(fields)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
insert(tableName: string) {
|
|
494
|
+
this.checkNotFinalized()
|
|
495
|
+
return this.orm.insert(tableName)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
update(tableName: string) {
|
|
499
|
+
this.checkNotFinalized()
|
|
500
|
+
return this.orm.update(tableName)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
delete(tableName: string) {
|
|
504
|
+
this.checkNotFinalized()
|
|
505
|
+
return this.orm.delete(tableName)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
execute(sql: string, params?: any[]) {
|
|
509
|
+
this.checkNotFinalized()
|
|
510
|
+
return this.orm.execute(sql, params)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Create nested transaction (savepoint)
|
|
515
|
+
*/
|
|
516
|
+
async transaction(): Promise<Transaction>
|
|
517
|
+
async transaction<T>(fn: (tx: ORM) => Promise<T>): Promise<T>
|
|
518
|
+
async transaction<T>(fn?: (tx: ORM) => Promise<T>): Promise<T | Transaction> {
|
|
519
|
+
this.checkNotFinalized()
|
|
520
|
+
|
|
521
|
+
if (fn) {
|
|
522
|
+
// Callback-style nested transaction
|
|
523
|
+
return await this.orm.transaction(fn)
|
|
524
|
+
} else {
|
|
525
|
+
// Manual-style nested transaction
|
|
526
|
+
return await this.orm.transaction()
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Commit the transaction
|
|
532
|
+
*/
|
|
533
|
+
async commit(): Promise<void> {
|
|
534
|
+
if (this.isCommitted) {
|
|
535
|
+
throw new Error('Transaction already committed')
|
|
536
|
+
}
|
|
537
|
+
if (this.isRolledBack) {
|
|
538
|
+
throw new Error('Transaction already rolled back')
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (this.savepointName) {
|
|
542
|
+
// Release savepoint for nested transaction
|
|
543
|
+
await this.db.execute(`RELEASE SAVEPOINT ${this.savepointName}`, [])
|
|
544
|
+
} else {
|
|
545
|
+
// Commit top-level transaction
|
|
546
|
+
await this.db.execute('COMMIT', [])
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
this.isCommitted = true
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Rollback the transaction
|
|
554
|
+
*/
|
|
555
|
+
async rollback(): Promise<void> {
|
|
556
|
+
if (this.isCommitted) {
|
|
557
|
+
throw new Error('Transaction already committed')
|
|
558
|
+
}
|
|
559
|
+
if (this.isRolledBack) {
|
|
560
|
+
throw new Error('Transaction already rolled back')
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (this.savepointName) {
|
|
564
|
+
// Rollback to savepoint for nested transaction
|
|
565
|
+
await this.db.execute(`ROLLBACK TO SAVEPOINT ${this.savepointName}`, [])
|
|
566
|
+
} else {
|
|
567
|
+
// Rollback top-level transaction
|
|
568
|
+
await this.db.execute('ROLLBACK', [])
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
this.isRolledBack = true
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Check if transaction is finalized
|
|
576
|
+
*/
|
|
577
|
+
private checkNotFinalized() {
|
|
578
|
+
if (this.isCommitted) {
|
|
579
|
+
throw new Error('Cannot perform operations on committed transaction')
|
|
580
|
+
}
|
|
581
|
+
if (this.isRolledBack) {
|
|
582
|
+
throw new Error('Cannot perform operations on rolled back transaction')
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
464
587
|
// ============================================================
|
|
465
588
|
// ORM CONNECTION WRAPPER
|
|
466
589
|
// ============================================================
|
|
@@ -471,9 +594,11 @@ class TableQueryBuilder {
|
|
|
471
594
|
*/
|
|
472
595
|
export class ORM {
|
|
473
596
|
private db: Connection
|
|
597
|
+
private transactionDepth: number = 0
|
|
474
598
|
|
|
475
|
-
constructor(db: Connection) {
|
|
599
|
+
constructor(db: Connection, transactionDepth: number = 0) {
|
|
476
600
|
this.db = db
|
|
601
|
+
this.transactionDepth = transactionDepth
|
|
477
602
|
}
|
|
478
603
|
|
|
479
604
|
/**
|
|
@@ -525,6 +650,96 @@ export class ORM {
|
|
|
525
650
|
execute(sql: string, params?: any[]) {
|
|
526
651
|
return this.db.execute(sql, params)
|
|
527
652
|
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Execute a transaction with support for nested transactions (savepoints)
|
|
656
|
+
* @example
|
|
657
|
+
* // Callback-style transaction
|
|
658
|
+
* const result = await orm.transaction(async (tx) => {
|
|
659
|
+
* const user = await tx.insert('users').values({ name: 'Alice' }).returning()
|
|
660
|
+
* const profile = await tx.insert('profiles').values({
|
|
661
|
+
* userId: user[0].id,
|
|
662
|
+
* bio: 'Hello world',
|
|
663
|
+
* }).returning()
|
|
664
|
+
* return { user, profile }
|
|
665
|
+
* })
|
|
666
|
+
*
|
|
667
|
+
* @example
|
|
668
|
+
* // Manual transaction control
|
|
669
|
+
* const tx = await orm.transaction()
|
|
670
|
+
* try {
|
|
671
|
+
* await tx.insert('users').values({ name: 'Bob' }).execute()
|
|
672
|
+
* await tx.insert('profiles').values({ userId: 1, bio: 'Something' }).execute()
|
|
673
|
+
* await tx.commit()
|
|
674
|
+
* } catch (err) {
|
|
675
|
+
* await tx.rollback()
|
|
676
|
+
* throw err
|
|
677
|
+
* }
|
|
678
|
+
*
|
|
679
|
+
* @example
|
|
680
|
+
* // Nested transaction (savepoint)
|
|
681
|
+
* await orm.transaction(async (outer) => {
|
|
682
|
+
* await outer.insert('users').values({ name: 'Outer' })
|
|
683
|
+
*
|
|
684
|
+
* await outer.transaction(async (inner) => {
|
|
685
|
+
* await inner.insert('users').values({ name: 'Inner' })
|
|
686
|
+
* // If this inner block throws, only the inner part rolls back
|
|
687
|
+
* })
|
|
688
|
+
* })
|
|
689
|
+
*/
|
|
690
|
+
async transaction(): Promise<Transaction>
|
|
691
|
+
async transaction<T>(fn: (tx: ORM) => Promise<T>): Promise<T>
|
|
692
|
+
async transaction<T>(fn?: (tx: ORM) => Promise<T>): Promise<T | Transaction> {
|
|
693
|
+
// Manual transaction control (no callback provided)
|
|
694
|
+
if (!fn) {
|
|
695
|
+
if (this.transactionDepth > 0) {
|
|
696
|
+
// Nested manual transaction - create savepoint
|
|
697
|
+
const savepointName = `sp_${this.transactionDepth}_${Date.now()}`
|
|
698
|
+
await this.db.execute(`SAVEPOINT ${savepointName}`, [])
|
|
699
|
+
return new Transaction(this.db, this.transactionDepth + 1, savepointName)
|
|
700
|
+
} else {
|
|
701
|
+
// Top-level manual transaction
|
|
702
|
+
await this.db.execute('BEGIN TRANSACTION', [])
|
|
703
|
+
return new Transaction(this.db, 1)
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Callback-style transaction
|
|
708
|
+
// Check if we're already in a transaction (nested transaction)
|
|
709
|
+
if (this.transactionDepth > 0) {
|
|
710
|
+
// Nested transaction - use savepoint
|
|
711
|
+
const savepointName = `sp_${this.transactionDepth}_${Date.now()}`
|
|
712
|
+
|
|
713
|
+
try {
|
|
714
|
+
// Create savepoint
|
|
715
|
+
await this.db.execute(`SAVEPOINT ${savepointName}`, [])
|
|
716
|
+
|
|
717
|
+
// Create a new ORM instance with incremented depth
|
|
718
|
+
const nestedOrm = new ORM(this.db, this.transactionDepth + 1)
|
|
719
|
+
|
|
720
|
+
// Execute the user's transaction function
|
|
721
|
+
const result = await fn(nestedOrm)
|
|
722
|
+
|
|
723
|
+
// Release savepoint on success
|
|
724
|
+
await this.db.execute(`RELEASE SAVEPOINT ${savepointName}`, [])
|
|
725
|
+
|
|
726
|
+
return result
|
|
727
|
+
} catch (error) {
|
|
728
|
+
// Rollback to savepoint on error
|
|
729
|
+
await this.db.execute(`ROLLBACK TO SAVEPOINT ${savepointName}`, [])
|
|
730
|
+
throw error
|
|
731
|
+
}
|
|
732
|
+
} else {
|
|
733
|
+
// Top-level transaction - use the connection's transaction method
|
|
734
|
+
return await this.db.transaction(async (txConnection) => {
|
|
735
|
+
// Create a new ORM instance wrapping the transaction connection
|
|
736
|
+
// Start at depth 1 since we're now inside a transaction
|
|
737
|
+
const txOrm = new ORM(txConnection, 1)
|
|
738
|
+
// Execute the user's transaction function with the transaction ORM
|
|
739
|
+
return await fn(txOrm)
|
|
740
|
+
})
|
|
741
|
+
}
|
|
742
|
+
}
|
|
528
743
|
}
|
|
529
744
|
|
|
530
745
|
/**
|