turbine-orm 0.5.0 → 0.7.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/README.md +292 -26
- package/dist/cjs/cli/config.js +5 -15
- package/dist/cjs/cli/index.js +311 -43
- package/dist/cjs/cli/loader.js +129 -0
- package/dist/cjs/cli/migrate.js +96 -47
- package/dist/cjs/cli/ui.js +5 -9
- package/dist/cjs/client.js +158 -49
- package/dist/cjs/errors.js +424 -0
- package/dist/cjs/generate.js +145 -14
- package/dist/cjs/index.js +43 -20
- package/dist/cjs/introspect.js +3 -5
- package/dist/cjs/pipeline.js +9 -2
- package/dist/cjs/query.js +544 -115
- package/dist/cjs/schema-builder.js +150 -30
- package/dist/cjs/schema-sql.js +241 -37
- package/dist/cjs/schema.js +5 -2
- package/dist/cjs/serverless.js +88 -176
- package/dist/cli/config.js +6 -16
- package/dist/cli/index.js +316 -48
- package/dist/cli/loader.d.ts +45 -0
- package/dist/cli/loader.js +91 -0
- package/dist/cli/migrate.d.ts +13 -2
- package/dist/cli/migrate.js +97 -48
- package/dist/cli/ui.d.ts +1 -1
- package/dist/cli/ui.js +5 -9
- package/dist/client.d.ts +92 -4
- package/dist/client.js +158 -49
- package/dist/errors.d.ts +225 -0
- package/dist/errors.js +405 -0
- package/dist/generate.d.ts +7 -1
- package/dist/generate.js +148 -18
- package/dist/index.d.ts +11 -9
- package/dist/index.js +16 -12
- package/dist/introspect.d.ts +1 -1
- package/dist/introspect.js +4 -6
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +9 -2
- package/dist/query.d.ts +374 -38
- package/dist/query.js +545 -116
- package/dist/schema-builder.d.ts +38 -5
- package/dist/schema-builder.js +150 -31
- package/dist/schema-sql.d.ts +7 -3
- package/dist/schema-sql.js +241 -37
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +5 -2
- package/dist/serverless.d.ts +92 -139
- package/dist/serverless.js +87 -173
- package/package.json +33 -16
- package/dist/types.d.ts +0 -93
- package/dist/types.js +0 -126
package/dist/cjs/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* turbine-orm — TurbineClient
|
|
4
4
|
*
|
|
5
5
|
* The main entry point for the Turbine TypeScript SDK.
|
|
6
6
|
* Manages connection pooling and provides typed table accessors.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* const user = await db.users.findUnique({ where: { id: 1 } });
|
|
18
18
|
*
|
|
19
19
|
* // With base client (dynamic):
|
|
20
|
-
* import { TurbineClient } from '
|
|
20
|
+
* import { TurbineClient } from 'turbine-orm';
|
|
21
21
|
* const db = new TurbineClient({ connectionString: '...' }, schema);
|
|
22
22
|
* const users = db.table<User>('users');
|
|
23
23
|
* ```
|
|
@@ -28,22 +28,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
29
|
exports.TurbineClient = exports.TransactionClient = void 0;
|
|
30
30
|
const pg_1 = __importDefault(require("pg"));
|
|
31
|
-
const
|
|
31
|
+
const errors_js_1 = require("./errors.js");
|
|
32
32
|
const pipeline_js_1 = require("./pipeline.js");
|
|
33
|
-
|
|
34
|
-
* Parse int8 (bigint, OID 20) as JavaScript number instead of string.
|
|
35
|
-
* Safe for values up to Number.MAX_SAFE_INTEGER (9,007,199,254,740,991).
|
|
36
|
-
*
|
|
37
|
-
* NOTE: For values exceeding Number.MAX_SAFE_INTEGER, the parser falls back
|
|
38
|
-
* to returning the raw string to avoid precision loss. The generated TypeScript
|
|
39
|
-
* type maps int8/bigint to `number`, which is correct for the vast majority of
|
|
40
|
-
* use cases (IDs, counts, timestamps). If you store values > 2^53 - 1 in a
|
|
41
|
-
* bigint column, the runtime return type will be `string` for those rows.
|
|
42
|
-
*/
|
|
43
|
-
pg_1.default.types.setTypeParser(20, (val) => {
|
|
44
|
-
const n = Number(val);
|
|
45
|
-
return Number.isSafeInteger(n) ? n : val; // fall back to string for huge values
|
|
46
|
-
});
|
|
33
|
+
const query_js_1 = require("./query.js");
|
|
47
34
|
/** Maps isolation level names to SQL */
|
|
48
35
|
const ISOLATION_LEVELS = {
|
|
49
36
|
ReadUncommitted: 'READ UNCOMMITTED',
|
|
@@ -125,20 +112,36 @@ class TransactionClient {
|
|
|
125
112
|
sql += `$${i + 1}`;
|
|
126
113
|
}
|
|
127
114
|
});
|
|
128
|
-
|
|
129
|
-
|
|
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
|
+
}
|
|
130
122
|
}
|
|
131
123
|
/**
|
|
132
124
|
* Create a pool-like wrapper around the transaction client.
|
|
133
125
|
* This allows QueryInterface to work with the transaction connection
|
|
134
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.
|
|
135
131
|
*/
|
|
136
132
|
createTxPool() {
|
|
137
133
|
const client = this.client;
|
|
138
134
|
// Return a minimal pool-compatible object that routes queries
|
|
139
135
|
// through the transaction client
|
|
140
136
|
return {
|
|
141
|
-
query: (text, values) =>
|
|
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
|
+
},
|
|
142
145
|
connect: () => Promise.resolve(client),
|
|
143
146
|
};
|
|
144
147
|
}
|
|
@@ -152,38 +155,86 @@ class TurbineClient {
|
|
|
152
155
|
pool;
|
|
153
156
|
/** The schema metadata this client was built from */
|
|
154
157
|
schema;
|
|
158
|
+
static int8ParserRegistered = false;
|
|
155
159
|
logging;
|
|
156
160
|
tableCache = new Map();
|
|
157
161
|
middlewares = [];
|
|
158
162
|
queryOptions;
|
|
163
|
+
/** True when Turbine created the pool and is responsible for tearing it down */
|
|
164
|
+
ownsPool = true;
|
|
159
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
|
+
}
|
|
160
192
|
this.logging = config.logging ?? false;
|
|
161
193
|
this.schema = schema;
|
|
162
194
|
this.queryOptions = {
|
|
163
195
|
defaultLimit: config.defaultLimit,
|
|
164
196
|
warnOnUnlimited: config.warnOnUnlimited,
|
|
165
197
|
};
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
};
|
|
171
|
-
if (config.connectionString) {
|
|
172
|
-
poolConfig.connectionString = config.connectionString;
|
|
198
|
+
// Apply NotFoundError message redaction mode (default: safe — values are
|
|
199
|
+
// stripped from messages to avoid leaking PII into error logs).
|
|
200
|
+
if (config.errorMessages) {
|
|
201
|
+
(0, errors_js_1.setErrorMessageMode)(config.errorMessages);
|
|
173
202
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
203
|
+
if (config.pool) {
|
|
204
|
+
// External pool — use directly. Turbine doesn't manage its lifecycle.
|
|
205
|
+
this.pool = config.pool;
|
|
206
|
+
this.ownsPool = false;
|
|
207
|
+
if (this.logging) {
|
|
208
|
+
console.log(`[turbine] Using external pool — ${Object.keys(schema.tables).length} tables`);
|
|
209
|
+
}
|
|
180
210
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
211
|
+
else {
|
|
212
|
+
const poolConfig = {
|
|
213
|
+
max: config.poolSize ?? 10,
|
|
214
|
+
idleTimeoutMillis: config.idleTimeoutMs ?? 30_000,
|
|
215
|
+
connectionTimeoutMillis: config.connectionTimeoutMs ?? 5_000,
|
|
216
|
+
};
|
|
217
|
+
if (config.connectionString) {
|
|
218
|
+
poolConfig.connectionString = config.connectionString;
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
poolConfig.host = config.host ?? 'localhost';
|
|
222
|
+
poolConfig.port = config.port ?? 5432;
|
|
223
|
+
poolConfig.database = config.database ?? 'postgres';
|
|
224
|
+
poolConfig.user = config.user ?? 'postgres';
|
|
225
|
+
poolConfig.password = config.password;
|
|
226
|
+
}
|
|
227
|
+
if (config.ssl !== undefined) {
|
|
228
|
+
poolConfig.ssl = config.ssl;
|
|
229
|
+
}
|
|
230
|
+
this.pool = new pg_1.default.Pool(poolConfig);
|
|
231
|
+
this.ownsPool = true;
|
|
232
|
+
this.pool.on('error', (err) => {
|
|
233
|
+
console.error('[turbine] Unexpected pool error:', err.message);
|
|
234
|
+
});
|
|
235
|
+
if (this.logging) {
|
|
236
|
+
console.log(`[turbine] Pool created — max ${poolConfig.max} connections, ${Object.keys(schema.tables).length} tables`);
|
|
237
|
+
}
|
|
187
238
|
}
|
|
188
239
|
// Auto-create typed table accessors for all tables in the schema
|
|
189
240
|
for (const tableName of Object.keys(schema.tables)) {
|
|
@@ -290,8 +341,13 @@ class TurbineClient {
|
|
|
290
341
|
if (this.logging) {
|
|
291
342
|
console.log(`[turbine] Raw SQL: ${sql.trim().substring(0, 120)}...`);
|
|
292
343
|
}
|
|
293
|
-
|
|
294
|
-
|
|
344
|
+
try {
|
|
345
|
+
const result = await this.pool.query(sql, values);
|
|
346
|
+
return result.rows;
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
throw (0, errors_js_1.wrapPgError)(err);
|
|
350
|
+
}
|
|
295
351
|
}
|
|
296
352
|
// -------------------------------------------------------------------------
|
|
297
353
|
// Transaction support (raw — legacy)
|
|
@@ -348,6 +404,24 @@ class TurbineClient {
|
|
|
348
404
|
async $transaction(fn, options) {
|
|
349
405
|
const client = await this.pool.connect();
|
|
350
406
|
const timeout = options?.timeout;
|
|
407
|
+
/**
|
|
408
|
+
* Track whether the connection has already been released so the finally
|
|
409
|
+
* block doesn't double-release. When a timeout fires we destroy the
|
|
410
|
+
* connection eagerly to abort the in-flight backend query.
|
|
411
|
+
*/
|
|
412
|
+
let released = false;
|
|
413
|
+
const releaseOnce = (err) => {
|
|
414
|
+
if (released)
|
|
415
|
+
return;
|
|
416
|
+
released = true;
|
|
417
|
+
try {
|
|
418
|
+
client.release(err);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// pg may throw if the client is already released — swallow.
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
let timedOut = false;
|
|
351
425
|
try {
|
|
352
426
|
// BEGIN with optional isolation level
|
|
353
427
|
let beginSQL = 'BEGIN';
|
|
@@ -371,11 +445,23 @@ class TurbineClient {
|
|
|
371
445
|
}
|
|
372
446
|
let result;
|
|
373
447
|
if (timeout) {
|
|
374
|
-
// Race between the function and a timeout
|
|
448
|
+
// Race between the function and a timeout. If the timeout fires we
|
|
449
|
+
// need to actually abort the in-flight query — otherwise the backend
|
|
450
|
+
// keeps running until pg's own timeout, holding a pool slot the whole
|
|
451
|
+
// time. The simplest reliable cancellation is to destroy the
|
|
452
|
+
// connection: passing a truthy argument to client.release() tells the
|
|
453
|
+
// pg pool to discard the client (its socket is closed, which causes
|
|
454
|
+
// Postgres to abort the active query and roll back the transaction).
|
|
455
|
+
// The pool will spin up a fresh connection on the next checkout.
|
|
375
456
|
let timer;
|
|
376
457
|
const timeoutPromise = new Promise((_, reject) => {
|
|
377
458
|
timer = setTimeout(() => {
|
|
378
|
-
|
|
459
|
+
timedOut = true;
|
|
460
|
+
// Destroy the connection to abort the in-flight backend query.
|
|
461
|
+
// We do this BEFORE rejecting so the socket is gone by the time
|
|
462
|
+
// the caller's catch block runs.
|
|
463
|
+
releaseOnce(new Error('[turbine] Transaction timeout — connection destroyed'));
|
|
464
|
+
reject(new errors_js_1.TimeoutError(timeout, 'Transaction'));
|
|
379
465
|
}, timeout);
|
|
380
466
|
});
|
|
381
467
|
try {
|
|
@@ -395,14 +481,25 @@ class TurbineClient {
|
|
|
395
481
|
return result;
|
|
396
482
|
}
|
|
397
483
|
catch (err) {
|
|
398
|
-
|
|
484
|
+
// If the timeout fired we already destroyed the connection — issuing a
|
|
485
|
+
// ROLLBACK on a released client would throw "Client has already been
|
|
486
|
+
// released". Skip the rollback in that case (the backend rolled back
|
|
487
|
+
// when its socket was closed).
|
|
488
|
+
if (!timedOut && !released) {
|
|
489
|
+
try {
|
|
490
|
+
await client.query('ROLLBACK');
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
// Best-effort rollback — the connection may have died mid-query.
|
|
494
|
+
}
|
|
495
|
+
}
|
|
399
496
|
if (this.logging) {
|
|
400
497
|
console.log('[turbine] Transaction rolled back');
|
|
401
498
|
}
|
|
402
499
|
throw err;
|
|
403
500
|
}
|
|
404
501
|
finally {
|
|
405
|
-
|
|
502
|
+
releaseOnce();
|
|
406
503
|
}
|
|
407
504
|
}
|
|
408
505
|
// -------------------------------------------------------------------------
|
|
@@ -426,8 +523,17 @@ class TurbineClient {
|
|
|
426
523
|
}
|
|
427
524
|
/**
|
|
428
525
|
* Gracefully shut down the connection pool.
|
|
526
|
+
*
|
|
527
|
+
* If Turbine was given an external pool via `TurbineConfig.pool`, this
|
|
528
|
+
* method is a no-op — the caller is responsible for the pool's lifecycle.
|
|
429
529
|
*/
|
|
430
530
|
async disconnect() {
|
|
531
|
+
if (!this.ownsPool) {
|
|
532
|
+
if (this.logging) {
|
|
533
|
+
console.log('[turbine] disconnect() skipped — external pool is not owned by Turbine');
|
|
534
|
+
}
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
431
537
|
await this.pool.end();
|
|
432
538
|
if (this.logging) {
|
|
433
539
|
console.log('[turbine] Pool disconnected');
|
|
@@ -437,12 +543,15 @@ class TurbineClient {
|
|
|
437
543
|
async end() {
|
|
438
544
|
return this.disconnect();
|
|
439
545
|
}
|
|
440
|
-
/**
|
|
546
|
+
/**
|
|
547
|
+
* Pool statistics for monitoring. Returns zeros for pools that don't
|
|
548
|
+
* expose connection counts (e.g., stateless HTTP drivers like Neon).
|
|
549
|
+
*/
|
|
441
550
|
get stats() {
|
|
442
551
|
return {
|
|
443
|
-
totalCount: this.pool.totalCount,
|
|
444
|
-
idleCount: this.pool.idleCount,
|
|
445
|
-
waitingCount: this.pool.waitingCount,
|
|
552
|
+
totalCount: this.pool.totalCount ?? 0,
|
|
553
|
+
idleCount: this.pool.idleCount ?? 0,
|
|
554
|
+
waitingCount: this.pool.waitingCount ?? 0,
|
|
446
555
|
};
|
|
447
556
|
}
|
|
448
557
|
}
|