turbine-orm 0.4.0 → 0.7.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 +243 -26
- package/dist/cjs/cli/config.js +151 -0
- package/dist/cjs/cli/index.js +1176 -0
- package/dist/cjs/cli/migrate.js +446 -0
- package/dist/cjs/cli/ui.js +233 -0
- package/dist/cjs/client.js +512 -0
- package/dist/cjs/errors.js +293 -0
- package/dist/cjs/generate.js +321 -0
- package/dist/cjs/index.js +94 -0
- package/dist/cjs/introspect.js +287 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pipeline.js +78 -0
- package/dist/cjs/query.js +1891 -0
- package/dist/cjs/schema-builder.js +238 -0
- package/dist/cjs/schema-sql.js +509 -0
- package/dist/cjs/schema.js +140 -0
- package/dist/cjs/serverless.js +110 -0
- package/dist/cli/config.js +6 -16
- package/dist/cli/index.js +256 -49
- package/dist/cli/migrate.d.ts +35 -6
- package/dist/cli/migrate.js +124 -76
- package/dist/cli/ui.js +5 -9
- package/dist/client.d.ts +87 -3
- package/dist/client.js +122 -46
- package/dist/errors.d.ts +138 -0
- package/dist/errors.js +278 -0
- package/dist/generate.js +37 -11
- package/dist/index.d.ts +10 -8
- package/dist/index.js +15 -11
- package/dist/introspect.js +3 -5
- package/dist/pipeline.js +8 -1
- package/dist/query.d.ts +310 -45
- package/dist/query.js +565 -237
- package/dist/schema-builder.js +91 -23
- package/dist/schema-sql.d.ts +6 -2
- package/dist/schema-sql.js +180 -26
- package/dist/schema.js +4 -1
- package/dist/serverless.d.ts +91 -139
- package/dist/serverless.js +86 -173
- package/package.json +44 -21
- package/dist/cli/config.d.ts.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/migrate.d.ts.map +0 -1
- package/dist/cli/ui.d.ts.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/generate.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/introspect.d.ts.map +0 -1
- package/dist/pipeline.d.ts.map +0 -1
- package/dist/query.d.ts.map +0 -1
- package/dist/schema-builder.d.ts.map +0 -1
- package/dist/schema-sql.d.ts.map +0 -1
- package/dist/schema.d.ts.map +0 -1
- package/dist/serverless.d.ts.map +0 -1
- package/dist/types.d.ts +0 -93
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -126
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* turbine-orm — TurbineClient
|
|
4
|
+
*
|
|
5
|
+
* The main entry point for the Turbine TypeScript SDK.
|
|
6
|
+
* Manages connection pooling and provides typed table accessors.
|
|
7
|
+
*
|
|
8
|
+
* Schema-driven: call `table<T>(name)` to get a QueryInterface for any
|
|
9
|
+
* table in the introspected schema. Generated clients extend this with
|
|
10
|
+
* typed properties (e.g. `db.users`, `db.posts`).
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // With generated client (recommended):
|
|
15
|
+
* import { turbine } from './generated/turbine';
|
|
16
|
+
* const db = turbine({ connectionString: process.env.DATABASE_URL });
|
|
17
|
+
* const user = await db.users.findUnique({ where: { id: 1 } });
|
|
18
|
+
*
|
|
19
|
+
* // With base client (dynamic):
|
|
20
|
+
* import { TurbineClient } from 'turbine-orm';
|
|
21
|
+
* const db = new TurbineClient({ connectionString: '...' }, schema);
|
|
22
|
+
* const users = db.table<User>('users');
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.TurbineClient = exports.TransactionClient = void 0;
|
|
30
|
+
const pg_1 = __importDefault(require("pg"));
|
|
31
|
+
const errors_js_1 = require("./errors.js");
|
|
32
|
+
const pipeline_js_1 = require("./pipeline.js");
|
|
33
|
+
const query_js_1 = require("./query.js");
|
|
34
|
+
/** Maps isolation level names to SQL */
|
|
35
|
+
const ISOLATION_LEVELS = {
|
|
36
|
+
ReadUncommitted: 'READ UNCOMMITTED',
|
|
37
|
+
ReadCommitted: 'READ COMMITTED',
|
|
38
|
+
RepeatableRead: 'REPEATABLE READ',
|
|
39
|
+
Serializable: 'SERIALIZABLE',
|
|
40
|
+
};
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// TransactionClient — provides typed table accessors within a transaction
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
/**
|
|
45
|
+
* A transaction-scoped client that provides the same table accessor API as TurbineClient.
|
|
46
|
+
* All queries run on a dedicated connection within a BEGIN/COMMIT block.
|
|
47
|
+
* Supports nested transactions via SAVEPOINTs.
|
|
48
|
+
*/
|
|
49
|
+
class TransactionClient {
|
|
50
|
+
client;
|
|
51
|
+
schema;
|
|
52
|
+
middlewares;
|
|
53
|
+
queryOptions;
|
|
54
|
+
tableCache = new Map();
|
|
55
|
+
savepointCounter = 0;
|
|
56
|
+
constructor(client, schema, middlewares, queryOptions) {
|
|
57
|
+
this.client = client;
|
|
58
|
+
this.schema = schema;
|
|
59
|
+
this.middlewares = middlewares;
|
|
60
|
+
this.queryOptions = queryOptions;
|
|
61
|
+
// Auto-create typed table accessors for all tables in the schema
|
|
62
|
+
for (const tableName of Object.keys(schema.tables)) {
|
|
63
|
+
const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
64
|
+
if (!(camelName in this)) {
|
|
65
|
+
Object.defineProperty(this, camelName, {
|
|
66
|
+
get: () => this.table(tableName),
|
|
67
|
+
enumerable: true,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get a QueryInterface for a table within this transaction.
|
|
74
|
+
* Uses the dedicated transaction connection instead of the pool.
|
|
75
|
+
*/
|
|
76
|
+
table(name) {
|
|
77
|
+
let qi = this.tableCache.get(name);
|
|
78
|
+
if (!qi) {
|
|
79
|
+
// Create a QueryInterface that uses the transaction client as its "pool"
|
|
80
|
+
// We use a proxy pool that routes queries through the transaction client
|
|
81
|
+
const txPool = this.createTxPool();
|
|
82
|
+
qi = new query_js_1.QueryInterface(txPool, name, this.schema, this.middlewares, this.queryOptions);
|
|
83
|
+
this.tableCache.set(name, qi);
|
|
84
|
+
}
|
|
85
|
+
return qi;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Execute a nested transaction via SAVEPOINT.
|
|
89
|
+
* If the inner function throws, only the savepoint is rolled back.
|
|
90
|
+
*/
|
|
91
|
+
async $transaction(fn) {
|
|
92
|
+
const savepointName = `sp_${++this.savepointCounter}`;
|
|
93
|
+
await this.client.query(`SAVEPOINT ${savepointName}`);
|
|
94
|
+
try {
|
|
95
|
+
const result = await fn(this);
|
|
96
|
+
await this.client.query(`RELEASE SAVEPOINT ${savepointName}`);
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
await this.client.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Execute a raw SQL query within this transaction.
|
|
106
|
+
*/
|
|
107
|
+
async raw(strings, ...values) {
|
|
108
|
+
let sql = '';
|
|
109
|
+
strings.forEach((str, i) => {
|
|
110
|
+
sql += str;
|
|
111
|
+
if (i < values.length) {
|
|
112
|
+
sql += `$${i + 1}`;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
try {
|
|
116
|
+
const result = await this.client.query(sql, values);
|
|
117
|
+
return result.rows;
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
throw (0, errors_js_1.wrapPgError)(err);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Create a pool-like wrapper around the transaction client.
|
|
125
|
+
* This allows QueryInterface to work with the transaction connection
|
|
126
|
+
* without knowing it's in a transaction.
|
|
127
|
+
*
|
|
128
|
+
* pg driver errors thrown by queries are translated into typed Turbine
|
|
129
|
+
* errors via wrapPgError so transaction-scoped queries surface the same
|
|
130
|
+
* typed errors as pool-scoped queries.
|
|
131
|
+
*/
|
|
132
|
+
createTxPool() {
|
|
133
|
+
const client = this.client;
|
|
134
|
+
// Return a minimal pool-compatible object that routes queries
|
|
135
|
+
// through the transaction client
|
|
136
|
+
return {
|
|
137
|
+
query: async (text, values) => {
|
|
138
|
+
try {
|
|
139
|
+
return await client.query(text, values);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
throw (0, errors_js_1.wrapPgError)(err);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
connect: () => Promise.resolve(client),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
exports.TransactionClient = TransactionClient;
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// TurbineClient
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
class TurbineClient {
|
|
154
|
+
/** The underlying pg.Pool — exposed for escape hatches */
|
|
155
|
+
pool;
|
|
156
|
+
/** The schema metadata this client was built from */
|
|
157
|
+
schema;
|
|
158
|
+
static int8ParserRegistered = false;
|
|
159
|
+
logging;
|
|
160
|
+
tableCache = new Map();
|
|
161
|
+
middlewares = [];
|
|
162
|
+
queryOptions;
|
|
163
|
+
/** True when Turbine created the pool and is responsible for tearing it down */
|
|
164
|
+
ownsPool = true;
|
|
165
|
+
constructor(config = {}, schema) {
|
|
166
|
+
/**
|
|
167
|
+
* Parse int8 (bigint, OID 20) as JavaScript number instead of string.
|
|
168
|
+
* Safe for values up to Number.MAX_SAFE_INTEGER (9,007,199,254,740,991).
|
|
169
|
+
*
|
|
170
|
+
* NOTE: For values exceeding Number.MAX_SAFE_INTEGER, the parser falls back
|
|
171
|
+
* to returning the raw string to avoid precision loss. The generated TypeScript
|
|
172
|
+
* type maps int8/bigint to `number`, which is correct for the vast majority of
|
|
173
|
+
* use cases (IDs, counts, timestamps). If you store values > 2^53 - 1 in a
|
|
174
|
+
* bigint column, the runtime return type will be `string` for those rows.
|
|
175
|
+
*
|
|
176
|
+
* NOTE: We intentionally do NOT register a parser for numeric (OID 1700).
|
|
177
|
+
* Postgres numeric is arbitrary-precision, so the default pg driver behavior
|
|
178
|
+
* of returning a string is correct and matches the generated TypeScript type
|
|
179
|
+
* (numeric → string). Users who want number can cast explicitly in SQL.
|
|
180
|
+
*/
|
|
181
|
+
// Only register the int8 parser when we own the pg driver. External
|
|
182
|
+
// pools (Neon HTTP, Vercel Postgres) may ship their own pg-types fork
|
|
183
|
+
// and rely on their own parser configuration — don't mutate global state
|
|
184
|
+
// we don't own.
|
|
185
|
+
if (!config.pool && !TurbineClient.int8ParserRegistered) {
|
|
186
|
+
pg_1.default.types.setTypeParser(20, (val) => {
|
|
187
|
+
const n = Number(val);
|
|
188
|
+
return Number.isSafeInteger(n) ? n : val;
|
|
189
|
+
});
|
|
190
|
+
TurbineClient.int8ParserRegistered = true;
|
|
191
|
+
}
|
|
192
|
+
this.logging = config.logging ?? false;
|
|
193
|
+
this.schema = schema;
|
|
194
|
+
this.queryOptions = {
|
|
195
|
+
defaultLimit: config.defaultLimit,
|
|
196
|
+
warnOnUnlimited: config.warnOnUnlimited,
|
|
197
|
+
};
|
|
198
|
+
if (config.pool) {
|
|
199
|
+
// External pool — use directly. Turbine doesn't manage its lifecycle.
|
|
200
|
+
this.pool = config.pool;
|
|
201
|
+
this.ownsPool = false;
|
|
202
|
+
if (this.logging) {
|
|
203
|
+
console.log(`[turbine] Using external pool — ${Object.keys(schema.tables).length} tables`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const poolConfig = {
|
|
208
|
+
max: config.poolSize ?? 10,
|
|
209
|
+
idleTimeoutMillis: config.idleTimeoutMs ?? 30_000,
|
|
210
|
+
connectionTimeoutMillis: config.connectionTimeoutMs ?? 5_000,
|
|
211
|
+
};
|
|
212
|
+
if (config.connectionString) {
|
|
213
|
+
poolConfig.connectionString = config.connectionString;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
poolConfig.host = config.host ?? 'localhost';
|
|
217
|
+
poolConfig.port = config.port ?? 5432;
|
|
218
|
+
poolConfig.database = config.database ?? 'postgres';
|
|
219
|
+
poolConfig.user = config.user ?? 'postgres';
|
|
220
|
+
poolConfig.password = config.password;
|
|
221
|
+
}
|
|
222
|
+
if (config.ssl !== undefined) {
|
|
223
|
+
poolConfig.ssl = config.ssl;
|
|
224
|
+
}
|
|
225
|
+
this.pool = new pg_1.default.Pool(poolConfig);
|
|
226
|
+
this.ownsPool = true;
|
|
227
|
+
this.pool.on('error', (err) => {
|
|
228
|
+
console.error('[turbine] Unexpected pool error:', err.message);
|
|
229
|
+
});
|
|
230
|
+
if (this.logging) {
|
|
231
|
+
console.log(`[turbine] Pool created — max ${poolConfig.max} connections, ${Object.keys(schema.tables).length} tables`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Auto-create typed table accessors for all tables in the schema
|
|
235
|
+
for (const tableName of Object.keys(schema.tables)) {
|
|
236
|
+
const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
237
|
+
if (!(camelName in this)) {
|
|
238
|
+
Object.defineProperty(this, camelName, {
|
|
239
|
+
get: () => this.table(tableName),
|
|
240
|
+
enumerable: true,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// -------------------------------------------------------------------------
|
|
246
|
+
// Middleware — intercept all queries
|
|
247
|
+
// -------------------------------------------------------------------------
|
|
248
|
+
/**
|
|
249
|
+
* Register a middleware function that runs before/after every query.
|
|
250
|
+
*
|
|
251
|
+
* Middleware can inspect and log query parameters, modify results after execution,
|
|
252
|
+
* and measure timing. Note: query SQL is generated before middleware runs, so
|
|
253
|
+
* modifying params.args in middleware will NOT affect the executed SQL.
|
|
254
|
+
* To intercept queries before SQL generation, use the raw() method instead.
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```ts
|
|
258
|
+
* // Query timing middleware
|
|
259
|
+
* db.$use(async (params, next) => {
|
|
260
|
+
* const before = Date.now();
|
|
261
|
+
* const result = await next(params);
|
|
262
|
+
* console.log(`${params.model}.${params.action} took ${Date.now() - before}ms`);
|
|
263
|
+
* return result;
|
|
264
|
+
* });
|
|
265
|
+
*
|
|
266
|
+
* // Soft-delete middleware
|
|
267
|
+
* db.$use(async (params, next) => {
|
|
268
|
+
* if (params.action === 'findMany' || params.action === 'findUnique') {
|
|
269
|
+
* params.args.where = { ...params.args.where, deletedAt: null };
|
|
270
|
+
* }
|
|
271
|
+
* if (params.action === 'delete') {
|
|
272
|
+
* params.action = 'update';
|
|
273
|
+
* params.args = { where: params.args.where, data: { deletedAt: new Date() } };
|
|
274
|
+
* }
|
|
275
|
+
* return next(params);
|
|
276
|
+
* });
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
$use(middleware) {
|
|
280
|
+
this.middlewares.push(middleware);
|
|
281
|
+
// Clear table cache so new QueryInterfaces pick up the middleware
|
|
282
|
+
this.tableCache.clear();
|
|
283
|
+
}
|
|
284
|
+
// -------------------------------------------------------------------------
|
|
285
|
+
// Table accessor — creates QueryInterface for any table
|
|
286
|
+
// -------------------------------------------------------------------------
|
|
287
|
+
/**
|
|
288
|
+
* Get a QueryInterface for a table.
|
|
289
|
+
* Results are cached — calling `table('users')` twice returns the same instance.
|
|
290
|
+
*/
|
|
291
|
+
table(name) {
|
|
292
|
+
let qi = this.tableCache.get(name);
|
|
293
|
+
if (!qi) {
|
|
294
|
+
qi = new query_js_1.QueryInterface(this.pool, name, this.schema, this.middlewares, this.queryOptions);
|
|
295
|
+
this.tableCache.set(name, qi);
|
|
296
|
+
}
|
|
297
|
+
return qi;
|
|
298
|
+
}
|
|
299
|
+
// -------------------------------------------------------------------------
|
|
300
|
+
// Pipeline — batch multiple queries into one round-trip
|
|
301
|
+
// -------------------------------------------------------------------------
|
|
302
|
+
/**
|
|
303
|
+
* Execute multiple queries in a single database round-trip.
|
|
304
|
+
*
|
|
305
|
+
* Pass the result of any `.build*()` method on a table accessor.
|
|
306
|
+
*/
|
|
307
|
+
async pipeline(...queries) {
|
|
308
|
+
if (this.logging) {
|
|
309
|
+
console.log(`[turbine] Pipeline: ${queries.length} queries — ${queries.map((q) => q.tag).join(', ')}`);
|
|
310
|
+
}
|
|
311
|
+
return (0, pipeline_js_1.executePipeline)(this.pool, queries);
|
|
312
|
+
}
|
|
313
|
+
// -------------------------------------------------------------------------
|
|
314
|
+
// Raw SQL — tagged template literal escape hatch
|
|
315
|
+
// -------------------------------------------------------------------------
|
|
316
|
+
/**
|
|
317
|
+
* Execute a raw SQL query with parameter interpolation via tagged templates.
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* ```ts
|
|
321
|
+
* const result = await db.raw<{ day: Date; count: number }>`
|
|
322
|
+
* SELECT DATE_TRUNC('day', created_at) as day, COUNT(*)::int as count
|
|
323
|
+
* FROM posts WHERE org_id = ${orgId}
|
|
324
|
+
* GROUP BY day ORDER BY day
|
|
325
|
+
* `;
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
328
|
+
async raw(strings, ...values) {
|
|
329
|
+
let sql = '';
|
|
330
|
+
strings.forEach((str, i) => {
|
|
331
|
+
sql += str;
|
|
332
|
+
if (i < values.length) {
|
|
333
|
+
sql += `$${i + 1}`;
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
if (this.logging) {
|
|
337
|
+
console.log(`[turbine] Raw SQL: ${sql.trim().substring(0, 120)}...`);
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
const result = await this.pool.query(sql, values);
|
|
341
|
+
return result.rows;
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
throw (0, errors_js_1.wrapPgError)(err);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// -------------------------------------------------------------------------
|
|
348
|
+
// Transaction support (raw — legacy)
|
|
349
|
+
// -------------------------------------------------------------------------
|
|
350
|
+
/**
|
|
351
|
+
* Execute a function within a database transaction (raw pg.PoolClient).
|
|
352
|
+
* For the typed API, use `$transaction()` instead.
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* ```ts
|
|
356
|
+
* await db.transaction(async (client) => {
|
|
357
|
+
* await client.query('INSERT INTO users (name) VALUES ($1)', ['Alice']);
|
|
358
|
+
* });
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
async transaction(fn) {
|
|
362
|
+
const client = await this.pool.connect();
|
|
363
|
+
try {
|
|
364
|
+
await client.query('BEGIN');
|
|
365
|
+
const result = await fn(client);
|
|
366
|
+
await client.query('COMMIT');
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
await client.query('ROLLBACK');
|
|
371
|
+
throw err;
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
374
|
+
client.release();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// -------------------------------------------------------------------------
|
|
378
|
+
// $transaction — Prisma-style typed transaction API
|
|
379
|
+
// -------------------------------------------------------------------------
|
|
380
|
+
/**
|
|
381
|
+
* Execute a function within a database transaction with full typed table accessors.
|
|
382
|
+
*
|
|
383
|
+
* The `tx` object provides the same table accessor API as the main client.
|
|
384
|
+
* Supports nested transactions via SAVEPOINTs, timeouts, and isolation levels.
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* ```ts
|
|
388
|
+
* await db.$transaction(async (tx) => {
|
|
389
|
+
* const user = await tx.users.create({ data: { email: 'a@b.com' } });
|
|
390
|
+
* await tx.posts.create({ data: { userId: user.id, title: 'Hello' } });
|
|
391
|
+
* });
|
|
392
|
+
*
|
|
393
|
+
* // With options:
|
|
394
|
+
* await db.$transaction(async (tx) => {
|
|
395
|
+
* // ...
|
|
396
|
+
* }, { timeout: 5000, isolationLevel: 'Serializable' });
|
|
397
|
+
* ```
|
|
398
|
+
*/
|
|
399
|
+
async $transaction(fn, options) {
|
|
400
|
+
const client = await this.pool.connect();
|
|
401
|
+
const timeout = options?.timeout;
|
|
402
|
+
try {
|
|
403
|
+
// BEGIN with optional isolation level
|
|
404
|
+
let beginSQL = 'BEGIN';
|
|
405
|
+
if (options?.isolationLevel) {
|
|
406
|
+
const level = ISOLATION_LEVELS[options.isolationLevel];
|
|
407
|
+
if (level)
|
|
408
|
+
beginSQL += ` ISOLATION LEVEL ${level}`;
|
|
409
|
+
}
|
|
410
|
+
await client.query(beginSQL);
|
|
411
|
+
// Create the transaction client with typed table accessors
|
|
412
|
+
const tx = new TransactionClient(client, this.schema, this.middlewares, this.queryOptions);
|
|
413
|
+
// Dynamically attach table accessors to tx
|
|
414
|
+
for (const tableName of Object.keys(this.schema.tables)) {
|
|
415
|
+
const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
416
|
+
if (!(camelName in tx)) {
|
|
417
|
+
Object.defineProperty(tx, camelName, {
|
|
418
|
+
get: () => tx.table(tableName),
|
|
419
|
+
enumerable: true,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
let result;
|
|
424
|
+
if (timeout) {
|
|
425
|
+
// Race between the function and a timeout
|
|
426
|
+
let timer;
|
|
427
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
428
|
+
timer = setTimeout(() => {
|
|
429
|
+
reject(new errors_js_1.TimeoutError(timeout, 'Transaction'));
|
|
430
|
+
}, timeout);
|
|
431
|
+
});
|
|
432
|
+
try {
|
|
433
|
+
result = await Promise.race([fn(tx), timeoutPromise]);
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
clearTimeout(timer);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
result = await fn(tx);
|
|
441
|
+
}
|
|
442
|
+
await client.query('COMMIT');
|
|
443
|
+
if (this.logging) {
|
|
444
|
+
console.log('[turbine] Transaction committed');
|
|
445
|
+
}
|
|
446
|
+
return result;
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
await client.query('ROLLBACK');
|
|
450
|
+
if (this.logging) {
|
|
451
|
+
console.log('[turbine] Transaction rolled back');
|
|
452
|
+
}
|
|
453
|
+
throw err;
|
|
454
|
+
}
|
|
455
|
+
finally {
|
|
456
|
+
client.release();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// -------------------------------------------------------------------------
|
|
460
|
+
// Connection lifecycle
|
|
461
|
+
// -------------------------------------------------------------------------
|
|
462
|
+
/**
|
|
463
|
+
* Test the database connection.
|
|
464
|
+
* Throws if the connection fails.
|
|
465
|
+
*/
|
|
466
|
+
async connect() {
|
|
467
|
+
const client = await this.pool.connect();
|
|
468
|
+
try {
|
|
469
|
+
await client.query('SELECT 1');
|
|
470
|
+
if (this.logging) {
|
|
471
|
+
console.log('[turbine] Connection verified');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
finally {
|
|
475
|
+
client.release();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Gracefully shut down the connection pool.
|
|
480
|
+
*
|
|
481
|
+
* If Turbine was given an external pool via `TurbineConfig.pool`, this
|
|
482
|
+
* method is a no-op — the caller is responsible for the pool's lifecycle.
|
|
483
|
+
*/
|
|
484
|
+
async disconnect() {
|
|
485
|
+
if (!this.ownsPool) {
|
|
486
|
+
if (this.logging) {
|
|
487
|
+
console.log('[turbine] disconnect() skipped — external pool is not owned by Turbine');
|
|
488
|
+
}
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
await this.pool.end();
|
|
492
|
+
if (this.logging) {
|
|
493
|
+
console.log('[turbine] Pool disconnected');
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/** Alias for disconnect() */
|
|
497
|
+
async end() {
|
|
498
|
+
return this.disconnect();
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Pool statistics for monitoring. Returns zeros for pools that don't
|
|
502
|
+
* expose connection counts (e.g., stateless HTTP drivers like Neon).
|
|
503
|
+
*/
|
|
504
|
+
get stats() {
|
|
505
|
+
return {
|
|
506
|
+
totalCount: this.pool.totalCount ?? 0,
|
|
507
|
+
idleCount: this.pool.idleCount ?? 0,
|
|
508
|
+
waitingCount: this.pool.waitingCount ?? 0,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
exports.TurbineClient = TurbineClient;
|