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.
Files changed (50) hide show
  1. package/README.md +292 -26
  2. package/dist/cjs/cli/config.js +5 -15
  3. package/dist/cjs/cli/index.js +311 -43
  4. package/dist/cjs/cli/loader.js +129 -0
  5. package/dist/cjs/cli/migrate.js +96 -47
  6. package/dist/cjs/cli/ui.js +5 -9
  7. package/dist/cjs/client.js +158 -49
  8. package/dist/cjs/errors.js +424 -0
  9. package/dist/cjs/generate.js +145 -14
  10. package/dist/cjs/index.js +43 -20
  11. package/dist/cjs/introspect.js +3 -5
  12. package/dist/cjs/pipeline.js +9 -2
  13. package/dist/cjs/query.js +544 -115
  14. package/dist/cjs/schema-builder.js +150 -30
  15. package/dist/cjs/schema-sql.js +241 -37
  16. package/dist/cjs/schema.js +5 -2
  17. package/dist/cjs/serverless.js +88 -176
  18. package/dist/cli/config.js +6 -16
  19. package/dist/cli/index.js +316 -48
  20. package/dist/cli/loader.d.ts +45 -0
  21. package/dist/cli/loader.js +91 -0
  22. package/dist/cli/migrate.d.ts +13 -2
  23. package/dist/cli/migrate.js +97 -48
  24. package/dist/cli/ui.d.ts +1 -1
  25. package/dist/cli/ui.js +5 -9
  26. package/dist/client.d.ts +92 -4
  27. package/dist/client.js +158 -49
  28. package/dist/errors.d.ts +225 -0
  29. package/dist/errors.js +405 -0
  30. package/dist/generate.d.ts +7 -1
  31. package/dist/generate.js +148 -18
  32. package/dist/index.d.ts +11 -9
  33. package/dist/index.js +16 -12
  34. package/dist/introspect.d.ts +1 -1
  35. package/dist/introspect.js +4 -6
  36. package/dist/pipeline.d.ts +1 -1
  37. package/dist/pipeline.js +9 -2
  38. package/dist/query.d.ts +374 -38
  39. package/dist/query.js +545 -116
  40. package/dist/schema-builder.d.ts +38 -5
  41. package/dist/schema-builder.js +150 -31
  42. package/dist/schema-sql.d.ts +7 -3
  43. package/dist/schema-sql.js +241 -37
  44. package/dist/schema.d.ts +1 -1
  45. package/dist/schema.js +5 -2
  46. package/dist/serverless.d.ts +92 -139
  47. package/dist/serverless.js +87 -173
  48. package/package.json +33 -16
  49. package/dist/types.d.ts +0 -93
  50. package/dist/types.js +0 -126
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * @batadata/turbine — TurbineClient
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 '@batadata/turbine';
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 query_js_1 = require("./query.js");
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
- const result = await this.client.query(sql, values);
129
- return result.rows;
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) => client.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
- const poolConfig = {
167
- max: config.poolSize ?? 10,
168
- idleTimeoutMillis: config.idleTimeoutMs ?? 30_000,
169
- connectionTimeoutMillis: config.connectionTimeoutMs ?? 5_000,
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
- else {
175
- poolConfig.host = config.host ?? 'localhost';
176
- poolConfig.port = config.port ?? 5432;
177
- poolConfig.database = config.database ?? 'postgres';
178
- poolConfig.user = config.user ?? 'postgres';
179
- poolConfig.password = config.password;
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
- this.pool = new pg_1.default.Pool(poolConfig);
182
- this.pool.on('error', (err) => {
183
- console.error('[turbine] Unexpected pool error:', err.message);
184
- });
185
- if (this.logging) {
186
- console.log(`[turbine] Pool created — max ${poolConfig.max} connections, ${Object.keys(schema.tables).length} tables`);
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
- const result = await this.pool.query(sql, values);
294
- return result.rows;
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
- reject(new Error(`[turbine] Transaction timed out after ${timeout}ms`));
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
- await client.query('ROLLBACK');
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
- client.release();
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
- /** Pool statistics for monitoring. */
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
  }