sqlstack 1.0.20 → 1.0.22
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/cjs/core/errors.d.ts +4 -0
- package/dist/cjs/core/errors.d.ts.map +1 -1
- package/dist/cjs/core/errors.js +11 -1
- package/dist/cjs/core/errors.js.map +1 -1
- package/dist/cjs/core/fileResolution.d.ts.map +1 -1
- package/dist/cjs/core/fileResolution.js +37 -14
- package/dist/cjs/core/fileResolution.js.map +1 -1
- package/dist/cjs/decorators/query.d.ts.map +1 -1
- package/dist/cjs/decorators/query.js +4 -2
- package/dist/cjs/decorators/query.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/registry.d.ts +4 -0
- package/dist/cjs/registry.d.ts.map +1 -1
- package/dist/cjs/registry.js +5 -2
- package/dist/cjs/registry.js.map +1 -1
- package/dist/cjs/transactions.d.ts +52 -0
- package/dist/cjs/transactions.d.ts.map +1 -0
- package/dist/cjs/transactions.js +275 -0
- package/dist/cjs/transactions.js.map +1 -0
- package/dist/esm/core/errors.js +9 -0
- package/dist/esm/core/errors.js.map +1 -1
- package/dist/esm/core/fileResolution.js +37 -14
- package/dist/esm/core/fileResolution.js.map +1 -1
- package/dist/esm/decorators/query.js +4 -2
- package/dist/esm/decorators/query.js.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/registry.js +5 -2
- package/dist/esm/registry.js.map +1 -1
- package/dist/esm/transactions.js +268 -0
- package/dist/esm/transactions.js.map +1 -0
- package/package.json +1 -1
- package/readme.md +284 -0
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,aAAa;AACb,cAAc,0BAA0B,CAAC;AACzC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAE3C,WAAW;AACX,cAAc,YAAY,CAAC;AAE3B,wBAAwB;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AAEjC,aAAa;AACb,cAAc,aAAa,CAAC;AAI5B,OAAO,EAAE,UAAU,EAAY,MAAM,YAAY,CAAC;AAElD,mDAAmD;AACnD,cAAc,qBAAqB,CAAC","sourcesContent":["// Decorators\nexport * from './decorators/queryBinder';\nexport * from './decorators/query';\nexport * from './decorators/defaults';\nexport * from './decorators/resultShape';\nexport * from './decorators/validateResult';\nexport * from './decorators/transform';\nexport * from './decorators/page';\nexport * from './decorators/missingAsNull';\n\n// Adapters\nexport * from './adapters';\n\n// Errors and Validation\nexport * from './core/errors';\nexport * from './core/validator';\n\n// DTO Helper\nexport * from './utils/dto';\n\n// Database Registry and Types\nexport type { Dialect, WriteResult, PostgresWriteResult, QueryResult, Database } from './registry';\nexport { SqlStackDB, IStackDB } from './registry';\n\n// Legacy exports (kept for backward compatibility)\nexport * from './binders/sqlBinder';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,aAAa;AACb,cAAc,0BAA0B,CAAC;AACzC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAE3C,eAAe;AACf,cAAc,gBAAgB,CAAC;AAE/B,WAAW;AACX,cAAc,YAAY,CAAC;AAE3B,wBAAwB;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AAEjC,aAAa;AACb,cAAc,aAAa,CAAC;AAI5B,OAAO,EAAE,UAAU,EAAY,MAAM,YAAY,CAAC;AAElD,mDAAmD;AACnD,cAAc,qBAAqB,CAAC","sourcesContent":["// Decorators\nexport * from './decorators/queryBinder';\nexport * from './decorators/query';\nexport * from './decorators/defaults';\nexport * from './decorators/resultShape';\nexport * from './decorators/validateResult';\nexport * from './decorators/transform';\nexport * from './decorators/page';\nexport * from './decorators/missingAsNull';\n\n// Transactions\nexport * from './transactions';\n\n// Adapters\nexport * from './adapters';\n\n// Errors and Validation\nexport * from './core/errors';\nexport * from './core/validator';\n\n// DTO Helper\nexport * from './utils/dto';\n\n// Database Registry and Types\nexport type { Dialect, WriteResult, PostgresWriteResult, QueryResult, Database } from './registry';\nexport { SqlStackDB, IStackDB } from './registry';\n\n// Legacy exports (kept for backward compatibility)\nexport * from './binders/sqlBinder';\n"]}
|
package/dist/esm/registry.js
CHANGED
|
@@ -12,14 +12,17 @@ class Registry {
|
|
|
12
12
|
this.defaultName = name;
|
|
13
13
|
return this;
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
getEntry(name) {
|
|
16
16
|
const key = name ?? this.defaultName;
|
|
17
17
|
if (!key)
|
|
18
18
|
throw new Error('No default database set');
|
|
19
19
|
const db = this.map.get(key);
|
|
20
20
|
if (!db)
|
|
21
21
|
throw new Error(`No database registered under ${key}`);
|
|
22
|
-
return db;
|
|
22
|
+
return { name: key, db };
|
|
23
|
+
}
|
|
24
|
+
get(name) {
|
|
25
|
+
return this.getEntry(name).db;
|
|
23
26
|
}
|
|
24
27
|
}
|
|
25
28
|
// Ensure a single shared instance across module copies/usages
|
package/dist/esm/registry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/registry.ts"],"names":[],"mappings":"AAwBA,MAAM,QAAQ;IAAd;QACU,QAAG,GAAG,IAAI,GAAG,EAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/registry.ts"],"names":[],"mappings":"AAwBA,MAAM,QAAQ;IAAd;QACU,QAAG,GAAG,IAAI,GAAG,EAAoB,CAAC;IAyB5C,CAAC;IAtBC,QAAQ,CAAC,IAAY,EAAE,EAAY;QACjC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,UAAU,CAAC,IAAY;QACrB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,IAAI,EAAE,CAAC,CAAC;QACjF,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,QAAQ,CAAC,IAAa;QACpB,MAAM,GAAG,GAAG,IAAI,IAAI,IAAI,CAAC,WAAW,CAAC;QACrC,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QACrD,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,GAAG,EAAE,CAAC,CAAC;QAChE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC;IAC3B,CAAC;IAED,GAAG,CAAC,IAAa;QACf,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;IAChC,CAAC;CACF;AAED,8DAA8D;AAC9D,MAAM,CAAC,GAAG,UAAiB,CAAC;AAC5B,IAAI,CAAC,CAAC,CAAC,sBAAsB,EAAE,CAAC;IAC9B,CAAC,CAAC,sBAAsB,GAAG,IAAI,QAAQ,EAAE,CAAC;AAC5C,CAAC;AAED,MAAM,CAAC,MAAM,UAAU,GAAa,CAAC,CAAC,sBAAsB,CAAC","sourcesContent":["export type Dialect = 'sqlite' | 'mysql' | 'postgres';\n\n// Result for non-SELECT statements across adapters\nexport interface WriteResult {\n rowsAffected: number;\n lastInsertId?: unknown;\n}\n\n// Postgres-specific extension when using RETURNING on write statements\nexport interface PostgresWriteResult extends WriteResult {\n // Only present when the SQL includes RETURNING and rows are returned\n returning?: any[];\n}\n\nexport type QueryResult = any[] | WriteResult | PostgresWriteResult;\n\nexport interface Database {\n dialect: Dialect;\n query(sql: string, params: unknown[]): Promise<QueryResult>;\n // Underlying driver connection or pool (driver-specific)\n conn: unknown;\n close: () => Promise<void> | void;\n}\n\nclass Registry {\n private map = new Map<string, Database>();\n private defaultName?: string;\n\n register(name: string, db: Database) {\n this.map.set(name, db);\n return this;\n }\n\n setDefault(name: string) {\n if (!this.map.has(name)) throw new Error(`No database registered under ${name}`);\n this.defaultName = name;\n return this;\n }\n\n getEntry(name?: string): { name: string; db: Database } {\n const key = name ?? this.defaultName;\n if (!key) throw new Error('No default database set');\n const db = this.map.get(key);\n if (!db) throw new Error(`No database registered under ${key}`);\n return { name: key, db };\n }\n\n get(name?: string): Database {\n return this.getEntry(name).db;\n }\n}\n\n// Ensure a single shared instance across module copies/usages\nconst g = globalThis as any;\nif (!g.__SQLSTACK_SINGLETON__) {\n g.__SQLSTACK_SINGLETON__ = new Registry();\n}\n\nexport const SqlStackDB: Registry = g.__SQLSTACK_SINGLETON__;\n\nexport type IStackDB = typeof SqlStackDB;\n"]}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
+
};
|
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
+
};
|
|
12
|
+
var _TransactionHandle_state, _TransactionHandle_owned, _TransactionHandle_finished;
|
|
13
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
14
|
+
import { SqlStackDB } from './registry.js';
|
|
15
|
+
import { createPgDb, createMysqlDb } from './adapters/index.js';
|
|
16
|
+
import { RollbackTransactionError, SqlStackError } from './core/errors.js';
|
|
17
|
+
const als = new AsyncLocalStorage();
|
|
18
|
+
function getStore() {
|
|
19
|
+
return als.getStore();
|
|
20
|
+
}
|
|
21
|
+
function getOrCreateStore() {
|
|
22
|
+
const existing = getStore();
|
|
23
|
+
if (existing)
|
|
24
|
+
return { store: existing, isNew: false };
|
|
25
|
+
return { store: { txByDb: new Map() }, isNew: true };
|
|
26
|
+
}
|
|
27
|
+
function isRollbackTransactionError(err) {
|
|
28
|
+
return err instanceof RollbackTransactionError;
|
|
29
|
+
}
|
|
30
|
+
function unwrapRollbackCause(err) {
|
|
31
|
+
return typeof err.cause === 'undefined' ? err : err.cause;
|
|
32
|
+
}
|
|
33
|
+
export class TransactionHandle {
|
|
34
|
+
constructor(dbName, state, owned) {
|
|
35
|
+
_TransactionHandle_state.set(this, void 0);
|
|
36
|
+
_TransactionHandle_owned.set(this, void 0);
|
|
37
|
+
_TransactionHandle_finished.set(this, false);
|
|
38
|
+
this.dbName = dbName;
|
|
39
|
+
__classPrivateFieldSet(this, _TransactionHandle_state, state, "f");
|
|
40
|
+
__classPrivateFieldSet(this, _TransactionHandle_owned, owned, "f");
|
|
41
|
+
}
|
|
42
|
+
async ensureStarted() {
|
|
43
|
+
await __classPrivateFieldGet(this, _TransactionHandle_state, "f").ensureStarted();
|
|
44
|
+
}
|
|
45
|
+
rollbackOnly(errorToThrow) {
|
|
46
|
+
__classPrivateFieldGet(this, _TransactionHandle_state, "f").rollbackOnly = true;
|
|
47
|
+
if (typeof errorToThrow !== 'undefined') {
|
|
48
|
+
__classPrivateFieldGet(this, _TransactionHandle_state, "f").rollbackError = errorToThrow;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async commit() {
|
|
52
|
+
if (!__classPrivateFieldGet(this, _TransactionHandle_owned, "f"))
|
|
53
|
+
return;
|
|
54
|
+
await __classPrivateFieldGet(this, _TransactionHandle_state, "f").commitIfStarted();
|
|
55
|
+
}
|
|
56
|
+
async rollback() {
|
|
57
|
+
if (!__classPrivateFieldGet(this, _TransactionHandle_owned, "f"))
|
|
58
|
+
return;
|
|
59
|
+
await __classPrivateFieldGet(this, _TransactionHandle_state, "f").rollbackIfStarted();
|
|
60
|
+
}
|
|
61
|
+
async release() {
|
|
62
|
+
if (!__classPrivateFieldGet(this, _TransactionHandle_owned, "f"))
|
|
63
|
+
return;
|
|
64
|
+
await __classPrivateFieldGet(this, _TransactionHandle_state, "f").release();
|
|
65
|
+
}
|
|
66
|
+
async finishOnSuccess() {
|
|
67
|
+
if (!__classPrivateFieldGet(this, _TransactionHandle_owned, "f") || __classPrivateFieldGet(this, _TransactionHandle_finished, "f"))
|
|
68
|
+
return;
|
|
69
|
+
__classPrivateFieldSet(this, _TransactionHandle_finished, true, "f");
|
|
70
|
+
await __classPrivateFieldGet(this, _TransactionHandle_state, "f").finishOnSuccess();
|
|
71
|
+
}
|
|
72
|
+
async finishOnError(err) {
|
|
73
|
+
if (!__classPrivateFieldGet(this, _TransactionHandle_owned, "f") || __classPrivateFieldGet(this, _TransactionHandle_finished, "f"))
|
|
74
|
+
return;
|
|
75
|
+
__classPrivateFieldSet(this, _TransactionHandle_finished, true, "f");
|
|
76
|
+
await __classPrivateFieldGet(this, _TransactionHandle_state, "f").finishOnError(err);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
_TransactionHandle_state = new WeakMap(), _TransactionHandle_owned = new WeakMap(), _TransactionHandle_finished = new WeakMap();
|
|
80
|
+
class TxState {
|
|
81
|
+
constructor(dbName, db, options) {
|
|
82
|
+
this.depth = 1;
|
|
83
|
+
this.started = false;
|
|
84
|
+
this.completed = false;
|
|
85
|
+
this.rollbackOnly = false;
|
|
86
|
+
this.dbName = dbName;
|
|
87
|
+
this.db = db;
|
|
88
|
+
this.options = {
|
|
89
|
+
lazy: options?.lazy ?? true,
|
|
90
|
+
nested: options?.nested ?? 'join',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
get activeDb() {
|
|
94
|
+
return this.txDb ?? this.db;
|
|
95
|
+
}
|
|
96
|
+
async ensureStarted() {
|
|
97
|
+
if (this.started)
|
|
98
|
+
return;
|
|
99
|
+
const dialect = this.db.dialect ?? 'sqlite';
|
|
100
|
+
if (dialect === 'postgres') {
|
|
101
|
+
const pool = this.db.conn;
|
|
102
|
+
if (!pool || typeof pool.connect !== 'function') {
|
|
103
|
+
throw new SqlStackError('Postgres transactions require a pg Pool with .connect() as Database.conn');
|
|
104
|
+
}
|
|
105
|
+
const client = await pool.connect();
|
|
106
|
+
this.txDb = createPgDb(client);
|
|
107
|
+
this.releaseFn = async () => {
|
|
108
|
+
if (typeof client.release === 'function')
|
|
109
|
+
client.release();
|
|
110
|
+
};
|
|
111
|
+
await this.txDb.query('BEGIN', []);
|
|
112
|
+
this.started = true;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (dialect === 'mysql') {
|
|
116
|
+
const pool = this.db.conn;
|
|
117
|
+
if (!pool || typeof pool.getConnection !== 'function') {
|
|
118
|
+
throw new SqlStackError('MySQL transactions require a mysql2 Pool with .getConnection() as Database.conn');
|
|
119
|
+
}
|
|
120
|
+
const conn = await pool.getConnection();
|
|
121
|
+
this.txDb = createMysqlDb(conn);
|
|
122
|
+
this.releaseFn = async () => {
|
|
123
|
+
if (typeof conn.release === 'function')
|
|
124
|
+
conn.release();
|
|
125
|
+
};
|
|
126
|
+
await this.txDb.query('START TRANSACTION', []);
|
|
127
|
+
this.started = true;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// sqlite: existing connection is already bound
|
|
131
|
+
await this.db.query('BEGIN', []);
|
|
132
|
+
this.started = true;
|
|
133
|
+
}
|
|
134
|
+
async query(sql, params) {
|
|
135
|
+
await this.ensureStarted();
|
|
136
|
+
return await this.activeDb.query(sql, params);
|
|
137
|
+
}
|
|
138
|
+
async commitIfStarted() {
|
|
139
|
+
if (this.completed)
|
|
140
|
+
return;
|
|
141
|
+
if (!this.started) {
|
|
142
|
+
this.completed = true;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
await this.activeDb.query('COMMIT', []);
|
|
146
|
+
this.completed = true;
|
|
147
|
+
}
|
|
148
|
+
async rollbackIfStarted() {
|
|
149
|
+
if (this.completed)
|
|
150
|
+
return;
|
|
151
|
+
if (!this.started) {
|
|
152
|
+
this.completed = true;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
await this.activeDb.query('ROLLBACK', []);
|
|
156
|
+
this.completed = true;
|
|
157
|
+
}
|
|
158
|
+
async release() {
|
|
159
|
+
try {
|
|
160
|
+
if (this.releaseFn)
|
|
161
|
+
await this.releaseFn();
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
this.txDb = undefined;
|
|
165
|
+
this.releaseFn = undefined;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async finishOnSuccess() {
|
|
169
|
+
try {
|
|
170
|
+
if (this.rollbackOnly) {
|
|
171
|
+
await this.rollbackIfStarted();
|
|
172
|
+
if (typeof this.rollbackError !== 'undefined')
|
|
173
|
+
throw this.rollbackError;
|
|
174
|
+
throw new RollbackTransactionError();
|
|
175
|
+
}
|
|
176
|
+
await this.commitIfStarted();
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
await this.release();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async finishOnError(err) {
|
|
183
|
+
try {
|
|
184
|
+
await this.rollbackIfStarted();
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
await this.release();
|
|
188
|
+
}
|
|
189
|
+
if (isRollbackTransactionError(err)) {
|
|
190
|
+
throw unwrapRollbackCause(err);
|
|
191
|
+
}
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function enterTx(dbName, options) {
|
|
196
|
+
const { store, isNew } = getOrCreateStore();
|
|
197
|
+
const existing = store.txByDb.get(dbName);
|
|
198
|
+
if (existing) {
|
|
199
|
+
existing.depth++;
|
|
200
|
+
return { handle: new TransactionHandle(dbName, existing, false), store, isNewStore: isNew };
|
|
201
|
+
}
|
|
202
|
+
const { db } = SqlStackDB.getEntry(dbName);
|
|
203
|
+
const state = new TxState(dbName, db, options);
|
|
204
|
+
store.txByDb.set(dbName, state);
|
|
205
|
+
return { handle: new TransactionHandle(dbName, state, true), store, isNewStore: isNew };
|
|
206
|
+
}
|
|
207
|
+
async function exitTx(dbName, store) {
|
|
208
|
+
const state = store.txByDb.get(dbName);
|
|
209
|
+
if (!state)
|
|
210
|
+
return;
|
|
211
|
+
state.depth = Math.max(0, state.depth - 1);
|
|
212
|
+
if (state.depth === 0) {
|
|
213
|
+
store.txByDb.delete(dbName);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export function currentTransaction(db) {
|
|
217
|
+
const store = getStore();
|
|
218
|
+
if (!store)
|
|
219
|
+
return undefined;
|
|
220
|
+
const resolved = SqlStackDB.getEntry(db).name;
|
|
221
|
+
const state = store.txByDb.get(resolved);
|
|
222
|
+
if (!state)
|
|
223
|
+
return undefined;
|
|
224
|
+
return new TransactionHandle(resolved, state, false);
|
|
225
|
+
}
|
|
226
|
+
export async function withTransaction(fn, options) {
|
|
227
|
+
const resolved = SqlStackDB.getEntry(options?.db).name;
|
|
228
|
+
const { handle, store, isNewStore } = enterTx(resolved, options);
|
|
229
|
+
const run = async () => {
|
|
230
|
+
try {
|
|
231
|
+
const out = await fn();
|
|
232
|
+
await handle.finishOnSuccess();
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
await handle.finishOnError(err);
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
await exitTx(resolved, store);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
return isNewStore ? await als.run(store, run) : await run();
|
|
244
|
+
}
|
|
245
|
+
export const transaction = function (...args) {
|
|
246
|
+
// @transaction
|
|
247
|
+
if (args.length === 3 && typeof args[2]?.value === 'function') {
|
|
248
|
+
const [target, prop, desc] = args;
|
|
249
|
+
wrapMethod(target, prop, desc, undefined);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
// @transaction() or @transaction({ ... })
|
|
253
|
+
const options = args.length > 0 ? args[0] : undefined;
|
|
254
|
+
return function (target, prop, desc) {
|
|
255
|
+
wrapMethod(target, prop, desc, options);
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
function wrapMethod(_target, _prop, desc, options) {
|
|
259
|
+
const original = desc.value;
|
|
260
|
+
desc.value = async function (...callArgs) {
|
|
261
|
+
return await withTransaction(() => original.apply(this, callArgs), options);
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
export function getActiveTransaction(dbName) {
|
|
265
|
+
const store = getStore();
|
|
266
|
+
return store?.txByDb.get(dbName);
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=transactions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transactions.js","sourceRoot":"","sources":["../../src/transactions.ts"],"names":[],"mappings":"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","sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { Database, QueryResult } from './registry';\nimport { SqlStackDB } from './registry';\nimport { createPgDb, createMysqlDb } from './adapters';\nimport { RollbackTransactionError, SqlStackError } from './core/errors';\n\nexport type TransactionOptions = {\n db?: string;\n lazy?: boolean;\n nested?: 'join';\n};\n\ntype TxStore = {\n txByDb: Map<string, TxState>;\n};\n\nconst als = new AsyncLocalStorage<TxStore>();\n\nfunction getStore(): TxStore | undefined {\n return als.getStore();\n}\n\nfunction getOrCreateStore(): { store: TxStore; isNew: boolean } {\n const existing = getStore();\n if (existing) return { store: existing, isNew: false };\n return { store: { txByDb: new Map() }, isNew: true };\n}\n\nfunction isRollbackTransactionError(err: unknown): err is RollbackTransactionError {\n return err instanceof RollbackTransactionError;\n}\n\nfunction unwrapRollbackCause(err: RollbackTransactionError): unknown {\n return typeof err.cause === 'undefined' ? err : err.cause;\n}\n\nexport class TransactionHandle {\n readonly dbName: string;\n #state: TxState;\n #owned: boolean;\n #finished = false;\n\n constructor(dbName: string, state: TxState, owned: boolean) {\n this.dbName = dbName;\n this.#state = state;\n this.#owned = owned;\n }\n\n async ensureStarted() {\n await this.#state.ensureStarted();\n }\n\n rollbackOnly(errorToThrow?: unknown) {\n this.#state.rollbackOnly = true;\n if (typeof errorToThrow !== 'undefined') {\n this.#state.rollbackError = errorToThrow;\n }\n }\n\n async commit() {\n if (!this.#owned) return;\n await this.#state.commitIfStarted();\n }\n\n async rollback() {\n if (!this.#owned) return;\n await this.#state.rollbackIfStarted();\n }\n\n async release() {\n if (!this.#owned) return;\n await this.#state.release();\n }\n\n async finishOnSuccess() {\n if (!this.#owned || this.#finished) return;\n this.#finished = true;\n await this.#state.finishOnSuccess();\n }\n\n async finishOnError(err: unknown) {\n if (!this.#owned || this.#finished) return;\n this.#finished = true;\n await this.#state.finishOnError(err);\n }\n}\n\nclass TxState {\n readonly dbName: string;\n readonly db: Database;\n readonly options: Required<Pick<TransactionOptions, 'lazy'>> & Pick<TransactionOptions, 'nested'>;\n depth = 1;\n started = false;\n completed = false;\n rollbackOnly = false;\n rollbackError: unknown | undefined;\n\n private txDb: Database | undefined;\n private releaseFn: (() => Promise<void>) | undefined;\n\n constructor(dbName: string, db: Database, options: TransactionOptions | undefined) {\n this.dbName = dbName;\n this.db = db;\n this.options = {\n lazy: options?.lazy ?? true,\n nested: options?.nested ?? 'join',\n };\n }\n\n get activeDb(): Database {\n return this.txDb ?? this.db;\n }\n\n async ensureStarted() {\n if (this.started) return;\n\n const dialect = this.db.dialect ?? 'sqlite';\n if (dialect === 'postgres') {\n const pool: any = this.db.conn;\n if (!pool || typeof pool.connect !== 'function') {\n throw new SqlStackError('Postgres transactions require a pg Pool with .connect() as Database.conn');\n }\n const client = await pool.connect();\n this.txDb = createPgDb(client);\n this.releaseFn = async () => {\n if (typeof client.release === 'function') client.release();\n };\n await this.txDb.query('BEGIN', []);\n this.started = true;\n return;\n }\n\n if (dialect === 'mysql') {\n const pool: any = this.db.conn;\n if (!pool || typeof pool.getConnection !== 'function') {\n throw new SqlStackError('MySQL transactions require a mysql2 Pool with .getConnection() as Database.conn');\n }\n const conn = await pool.getConnection();\n this.txDb = createMysqlDb(conn);\n this.releaseFn = async () => {\n if (typeof conn.release === 'function') conn.release();\n };\n await this.txDb.query('START TRANSACTION', []);\n this.started = true;\n return;\n }\n\n // sqlite: existing connection is already bound\n await this.db.query('BEGIN', []);\n this.started = true;\n }\n\n async query(sql: string, params: unknown[]): Promise<QueryResult> {\n await this.ensureStarted();\n return await this.activeDb.query(sql, params);\n }\n\n async commitIfStarted() {\n if (this.completed) return;\n if (!this.started) {\n this.completed = true;\n return;\n }\n await this.activeDb.query('COMMIT', []);\n this.completed = true;\n }\n\n async rollbackIfStarted() {\n if (this.completed) return;\n if (!this.started) {\n this.completed = true;\n return;\n }\n await this.activeDb.query('ROLLBACK', []);\n this.completed = true;\n }\n\n async release() {\n try {\n if (this.releaseFn) await this.releaseFn();\n } finally {\n this.txDb = undefined;\n this.releaseFn = undefined;\n }\n }\n\n async finishOnSuccess() {\n try {\n if (this.rollbackOnly) {\n await this.rollbackIfStarted();\n if (typeof this.rollbackError !== 'undefined') throw this.rollbackError;\n throw new RollbackTransactionError();\n }\n await this.commitIfStarted();\n } finally {\n await this.release();\n }\n }\n\n async finishOnError(err: unknown) {\n try {\n await this.rollbackIfStarted();\n } finally {\n await this.release();\n }\n if (isRollbackTransactionError(err)) {\n throw unwrapRollbackCause(err);\n }\n throw err;\n }\n}\n\nfunction enterTx(dbName: string, options?: TransactionOptions): { handle: TransactionHandle; store: TxStore; isNewStore: boolean } {\n const { store, isNew } = getOrCreateStore();\n const existing = store.txByDb.get(dbName);\n if (existing) {\n existing.depth++;\n return { handle: new TransactionHandle(dbName, existing, false), store, isNewStore: isNew };\n }\n const { db } = SqlStackDB.getEntry(dbName);\n const state = new TxState(dbName, db, options);\n store.txByDb.set(dbName, state);\n return { handle: new TransactionHandle(dbName, state, true), store, isNewStore: isNew };\n}\n\nasync function exitTx(dbName: string, store: TxStore) {\n const state = store.txByDb.get(dbName);\n if (!state) return;\n state.depth = Math.max(0, state.depth - 1);\n if (state.depth === 0) {\n store.txByDb.delete(dbName);\n }\n}\n\nexport function currentTransaction(db?: string): TransactionHandle | undefined {\n const store = getStore();\n if (!store) return undefined;\n const resolved = SqlStackDB.getEntry(db).name;\n const state = store.txByDb.get(resolved);\n if (!state) return undefined;\n return new TransactionHandle(resolved, state, false);\n}\n\nexport async function withTransaction<T>(fn: () => Promise<T>, options?: TransactionOptions): Promise<T> {\n const resolved = SqlStackDB.getEntry(options?.db).name;\n const { handle, store, isNewStore } = enterTx(resolved, options);\n\n const run = async () => {\n try {\n const out = await fn();\n await handle.finishOnSuccess();\n return out;\n } catch (err) {\n await handle.finishOnError(err);\n throw err;\n } finally {\n await exitTx(resolved, store);\n }\n };\n\n return isNewStore ? await als.run(store, run) : await run();\n}\n\nexport type TransactionDecorator = {\n (target: any, prop: string | symbol, desc: PropertyDescriptor): void;\n (options?: TransactionOptions): (target: any, prop: string | symbol, desc: PropertyDescriptor) => void;\n};\n\nexport const transaction: TransactionDecorator = function (...args: any[]) {\n // @transaction\n if (args.length === 3 && typeof args[2]?.value === 'function') {\n const [target, prop, desc] = args as [any, string | symbol, PropertyDescriptor];\n wrapMethod(target, prop, desc, undefined);\n return;\n }\n // @transaction() or @transaction({ ... })\n const options: TransactionOptions | undefined = args.length > 0 ? args[0] : undefined;\n return function (target: any, prop: string | symbol, desc: PropertyDescriptor) {\n wrapMethod(target, prop, desc, options);\n };\n} as any;\n\nfunction wrapMethod(_target: any, _prop: string | symbol, desc: PropertyDescriptor, options?: TransactionOptions) {\n const original = desc.value;\n desc.value = async function (...callArgs: any[]) {\n return await withTransaction(() => original.apply(this, callArgs), options);\n };\n}\n\nexport type ActiveTransaction = {\n query(sql: string, params: unknown[]): Promise<QueryResult>;\n};\n\nexport function getActiveTransaction(dbName: string): ActiveTransaction | undefined {\n const store = getStore();\n return store?.txByDb.get(dbName);\n}\n"]}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -14,6 +14,7 @@ Write real SQL next to your code, and use small, composable decorators to bind,
|
|
|
14
14
|
- [How It Works](#how-it-works)
|
|
15
15
|
- [Result Types](#result-types)
|
|
16
16
|
- [Error Handling](#error-handling)
|
|
17
|
+
- [Transactions](#transactions)
|
|
17
18
|
- [Inline SQL](#inline-sql)
|
|
18
19
|
- [Advanced: Direct Database Access](#advanced-direct-database-access)
|
|
19
20
|
- [Database Support](#database-support)
|
|
@@ -897,6 +898,289 @@ Available error classes:
|
|
|
897
898
|
|
|
898
899
|
---
|
|
899
900
|
|
|
901
|
+
## Transactions
|
|
902
|
+
|
|
903
|
+
Run multiple queries as a single atomic unit. If any query fails, all changes are rolled back automatically.
|
|
904
|
+
|
|
905
|
+
### Basic Usage with `@transaction` Decorator
|
|
906
|
+
|
|
907
|
+
Mark a method with `@transaction` to wrap it in a transaction:
|
|
908
|
+
|
|
909
|
+
```ts
|
|
910
|
+
import { transaction, currentTransaction } from "sqlstack";
|
|
911
|
+
|
|
912
|
+
class UserService {
|
|
913
|
+
@transaction
|
|
914
|
+
async createUserWithProfile(data: { email: string; name: string }) {
|
|
915
|
+
await this.usersRepo.insert(data);
|
|
916
|
+
await this.profilesRepo.insert({ userId: data.id, bio: "" });
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// If either insert fails, both are rolled back
|
|
921
|
+
await new UserService().createUserWithProfile({ email: "alice@example.com", name: "Alice" });
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
Queries in `@Query` decorated methods automatically participate in the active transaction:
|
|
925
|
+
|
|
926
|
+
```ts
|
|
927
|
+
@QueryBinder()
|
|
928
|
+
class UsersRepo {
|
|
929
|
+
@Query({ sql: 'INSERT INTO users (email, name) VALUES (:email, :name)' })
|
|
930
|
+
async insert(data: { email: string; name: string }): Promise<WriteResult> {
|
|
931
|
+
throw new Error("replaced by @Query");
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
@QueryBinder()
|
|
936
|
+
class ProfilesRepo {
|
|
937
|
+
@Query({ sql: 'INSERT INTO profiles (user_id, bio) VALUES (:userId, :bio)' })
|
|
938
|
+
async insert(data: { userId: string; bio: string }): Promise<WriteResult> {
|
|
939
|
+
throw new Error("replaced by @Query");
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
### Commit and Rollback
|
|
945
|
+
|
|
946
|
+
**Commit on success:** If the transaction method returns normally, all queries are committed.
|
|
947
|
+
|
|
948
|
+
**Rollback on error:** If the method throws, all queries are rolled back and the error is rethrown:
|
|
949
|
+
|
|
950
|
+
```ts
|
|
951
|
+
class UserService {
|
|
952
|
+
@transaction
|
|
953
|
+
async createUser() {
|
|
954
|
+
await this.usersRepo.insert({ email: "bob@example.com" });
|
|
955
|
+
throw new Error("something went wrong");
|
|
956
|
+
// Rollback happens here; insert is undone
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
await new UserService().createUser(); // throws "something went wrong"
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
### Lazy Transactions
|
|
964
|
+
|
|
965
|
+
By default, transactions use **lazy mode**: they don't start (no `BEGIN`) until the first query runs. This avoids overhead if the method never queries:
|
|
966
|
+
|
|
967
|
+
```ts
|
|
968
|
+
@transaction({ lazy: true }) // default behavior
|
|
969
|
+
async readOnlyOperation() {
|
|
970
|
+
// No database transaction started yet
|
|
971
|
+
const data = await this.repo.fetch();
|
|
972
|
+
|
|
973
|
+
// Process data without holding a transaction
|
|
974
|
+
return processData(data);
|
|
975
|
+
}
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
Disable lazy mode with `lazy: false` to start the transaction immediately:
|
|
979
|
+
|
|
980
|
+
```ts
|
|
981
|
+
@transaction({ lazy: false })
|
|
982
|
+
async mustStartTransaction() {
|
|
983
|
+
// BEGIN is executed immediately, even before first query
|
|
984
|
+
}
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
### Accessing the Current Transaction
|
|
988
|
+
|
|
989
|
+
Use `currentTransaction()` to access the active transaction context within a method:
|
|
990
|
+
|
|
991
|
+
```ts
|
|
992
|
+
class UserService {
|
|
993
|
+
@transaction
|
|
994
|
+
async createUserWithValidation(data: { email: string }) {
|
|
995
|
+
const user = await this.usersRepo.insert(data);
|
|
996
|
+
|
|
997
|
+
// Validate user was inserted
|
|
998
|
+
const check = await this.usersRepo.findById(user.id);
|
|
999
|
+
if (!check) {
|
|
1000
|
+
// Mark transaction for rollback and throw a custom error
|
|
1001
|
+
currentTransaction()?.rollbackOnly(new Error("User not found after insert"));
|
|
1002
|
+
return; // exit gracefully
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
return user;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
### Rollback with Custom Error
|
|
1011
|
+
|
|
1012
|
+
Use `rollbackOnly(error)` to mark a transaction for rollback while specifying which error should be thrown:
|
|
1013
|
+
|
|
1014
|
+
```ts
|
|
1015
|
+
class UserService {
|
|
1016
|
+
@transaction
|
|
1017
|
+
async createUser(email: string) {
|
|
1018
|
+
try {
|
|
1019
|
+
await this.usersRepo.insert({ email });
|
|
1020
|
+
} catch (dbError) {
|
|
1021
|
+
// Roll back and throw a sanitized error
|
|
1022
|
+
currentTransaction()?.rollbackOnly(new Error("Failed to create user"));
|
|
1023
|
+
return; // rethrow happens automatically
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
You can also throw `RollbackTransactionError` to explicitly signal rollback while optionally wrapping another error:
|
|
1030
|
+
|
|
1031
|
+
```ts
|
|
1032
|
+
import { RollbackTransactionError } from "sqlstack";
|
|
1033
|
+
|
|
1034
|
+
class UserService {
|
|
1035
|
+
@transaction
|
|
1036
|
+
async createUser(email: string) {
|
|
1037
|
+
await this.usersRepo.insert({ email });
|
|
1038
|
+
|
|
1039
|
+
// Roll back and throw the wrapped error
|
|
1040
|
+
throw new RollbackTransactionError("Transaction cancelled", new Error("User rejected"));
|
|
1041
|
+
// Caller sees: Error("User rejected")
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
### Using `withTransaction()` Function
|
|
1047
|
+
|
|
1048
|
+
For programmatic control, use the `withTransaction()` function:
|
|
1049
|
+
|
|
1050
|
+
```ts
|
|
1051
|
+
import { withTransaction } from "sqlstack";
|
|
1052
|
+
|
|
1053
|
+
const result = await withTransaction(async () => {
|
|
1054
|
+
const user = await this.usersRepo.insert({ email: "charlie@example.com" });
|
|
1055
|
+
await this.profilesRepo.insert({ userId: user.id });
|
|
1056
|
+
return user;
|
|
1057
|
+
}, { db: "primary" });
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
### Multiple Databases
|
|
1061
|
+
|
|
1062
|
+
Transactions are per-database. Run transactions on different databases simultaneously:
|
|
1063
|
+
|
|
1064
|
+
```ts
|
|
1065
|
+
class MultiDbService {
|
|
1066
|
+
@transaction({ db: "primary" })
|
|
1067
|
+
async insertUser() {
|
|
1068
|
+
await this.usersRepo.insert({ email: "dave@example.com" });
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
@transaction({ db: "analytics" })
|
|
1072
|
+
async logAnalytics() {
|
|
1073
|
+
await this.analyticsRepo.insert({ event: "user_created" });
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Both transactions run independently
|
|
1078
|
+
await Promise.all([
|
|
1079
|
+
new MultiDbService().insertUser(),
|
|
1080
|
+
new MultiDbService().logAnalytics(),
|
|
1081
|
+
]);
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
### Nested Transactions (Savepoints)
|
|
1085
|
+
|
|
1086
|
+
Multiple `@transaction` decorators stack safely. Only the outermost transaction commits/rolls back:
|
|
1087
|
+
|
|
1088
|
+
```ts
|
|
1089
|
+
class UserService {
|
|
1090
|
+
@transaction
|
|
1091
|
+
async createUser(email: string) {
|
|
1092
|
+
await this.usersRepo.insert({ email });
|
|
1093
|
+
await this.createDefaultProfile();
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
@transaction // nested decorator
|
|
1097
|
+
async createDefaultProfile() {
|
|
1098
|
+
await this.profilesRepo.insert({ name: "Default Profile" });
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Outer transaction commits both inserts
|
|
1103
|
+
await new UserService().createUser("eve@example.com");
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
All queries use the same transaction context. If the inner method throws, the outer transaction rolls back everything.
|
|
1107
|
+
|
|
1108
|
+
### Connection Pooling and Resource Cleanup
|
|
1109
|
+
|
|
1110
|
+
For **Postgres** and **MySQL**, transactions acquire connections from the pool and release them when complete:
|
|
1111
|
+
|
|
1112
|
+
**Postgres:**
|
|
1113
|
+
```ts
|
|
1114
|
+
import { createPgDb } from "sqlstack/adapters";
|
|
1115
|
+
import { Pool } from "pg";
|
|
1116
|
+
|
|
1117
|
+
const pool = new Pool({ connectionString: process.env.PG_URL });
|
|
1118
|
+
SqlStackDB.register("primary", createPgDb(pool));
|
|
1119
|
+
|
|
1120
|
+
class UserService {
|
|
1121
|
+
@transaction({ db: "primary" })
|
|
1122
|
+
async createUser() {
|
|
1123
|
+
// A client is acquired from the pool for this transaction
|
|
1124
|
+
await this.usersRepo.insert({ email: "frank@example.com" });
|
|
1125
|
+
// Client is released back to the pool after commit/rollback
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
**MySQL:**
|
|
1131
|
+
```ts
|
|
1132
|
+
import { createMysqlDb } from "sqlstack/adapters";
|
|
1133
|
+
import mysql from "mysql2/promise";
|
|
1134
|
+
|
|
1135
|
+
const pool = mysql.createPool({ uri: process.env.MYSQL_URL });
|
|
1136
|
+
SqlStackDB.register("primary", createMysqlDb(pool));
|
|
1137
|
+
|
|
1138
|
+
class UserService {
|
|
1139
|
+
@transaction({ db: "primary" })
|
|
1140
|
+
async createUser() {
|
|
1141
|
+
// A connection is acquired from the pool
|
|
1142
|
+
await this.usersRepo.insert({ email: "grace@example.com" });
|
|
1143
|
+
// Connection is released back to the pool
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
```
|
|
1147
|
+
|
|
1148
|
+
**SQLite** uses the existing connection (better-sqlite3 doesn't support pooling).
|
|
1149
|
+
|
|
1150
|
+
### Error Handling in Transactions
|
|
1151
|
+
|
|
1152
|
+
Errors are always rethrown after rollback:
|
|
1153
|
+
|
|
1154
|
+
```ts
|
|
1155
|
+
class UserService {
|
|
1156
|
+
@transaction
|
|
1157
|
+
async createUser() {
|
|
1158
|
+
await this.usersRepo.insert({ email: "henry@example.com" });
|
|
1159
|
+
throw new Error("oops");
|
|
1160
|
+
// Rollback happens, then "oops" is thrown to caller
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
try {
|
|
1165
|
+
await new UserService().createUser();
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
console.error(err.message); // "oops"
|
|
1168
|
+
}
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
If rollback itself fails, that error is thrown (and the original error is lost):
|
|
1172
|
+
|
|
1173
|
+
```ts
|
|
1174
|
+
@transaction
|
|
1175
|
+
async createUser() {
|
|
1176
|
+
await this.usersRepo.insert({ email: "ivan@example.com" });
|
|
1177
|
+
throw new Error("original error");
|
|
1178
|
+
// If ROLLBACK SQL fails, the rollback error is thrown instead
|
|
1179
|
+
}
|
|
1180
|
+
```
|
|
1181
|
+
|
|
1182
|
+
---
|
|
1183
|
+
|
|
900
1184
|
## Inline SQL
|
|
901
1185
|
|
|
902
1186
|
For simple or dynamic queries, use `@Query({ sql: "..." })` with inline SQL:
|