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.
Files changed (57) hide show
  1. package/README.md +243 -26
  2. package/dist/cjs/cli/config.js +151 -0
  3. package/dist/cjs/cli/index.js +1176 -0
  4. package/dist/cjs/cli/migrate.js +446 -0
  5. package/dist/cjs/cli/ui.js +233 -0
  6. package/dist/cjs/client.js +512 -0
  7. package/dist/cjs/errors.js +293 -0
  8. package/dist/cjs/generate.js +321 -0
  9. package/dist/cjs/index.js +94 -0
  10. package/dist/cjs/introspect.js +287 -0
  11. package/dist/cjs/package.json +1 -0
  12. package/dist/cjs/pipeline.js +78 -0
  13. package/dist/cjs/query.js +1891 -0
  14. package/dist/cjs/schema-builder.js +238 -0
  15. package/dist/cjs/schema-sql.js +509 -0
  16. package/dist/cjs/schema.js +140 -0
  17. package/dist/cjs/serverless.js +110 -0
  18. package/dist/cli/config.js +6 -16
  19. package/dist/cli/index.js +256 -49
  20. package/dist/cli/migrate.d.ts +35 -6
  21. package/dist/cli/migrate.js +124 -76
  22. package/dist/cli/ui.js +5 -9
  23. package/dist/client.d.ts +87 -3
  24. package/dist/client.js +122 -46
  25. package/dist/errors.d.ts +138 -0
  26. package/dist/errors.js +278 -0
  27. package/dist/generate.js +37 -11
  28. package/dist/index.d.ts +10 -8
  29. package/dist/index.js +15 -11
  30. package/dist/introspect.js +3 -5
  31. package/dist/pipeline.js +8 -1
  32. package/dist/query.d.ts +310 -45
  33. package/dist/query.js +565 -237
  34. package/dist/schema-builder.js +91 -23
  35. package/dist/schema-sql.d.ts +6 -2
  36. package/dist/schema-sql.js +180 -26
  37. package/dist/schema.js +4 -1
  38. package/dist/serverless.d.ts +91 -139
  39. package/dist/serverless.js +86 -173
  40. package/package.json +44 -21
  41. package/dist/cli/config.d.ts.map +0 -1
  42. package/dist/cli/index.d.ts.map +0 -1
  43. package/dist/cli/migrate.d.ts.map +0 -1
  44. package/dist/cli/ui.d.ts.map +0 -1
  45. package/dist/client.d.ts.map +0 -1
  46. package/dist/generate.d.ts.map +0 -1
  47. package/dist/index.d.ts.map +0 -1
  48. package/dist/introspect.d.ts.map +0 -1
  49. package/dist/pipeline.d.ts.map +0 -1
  50. package/dist/query.d.ts.map +0 -1
  51. package/dist/schema-builder.d.ts.map +0 -1
  52. package/dist/schema-sql.d.ts.map +0 -1
  53. package/dist/schema.d.ts.map +0 -1
  54. package/dist/serverless.d.ts.map +0 -1
  55. package/dist/types.d.ts +0 -93
  56. package/dist/types.d.ts.map +0 -1
  57. package/dist/types.js +0 -126
@@ -11,15 +11,19 @@
11
11
  * -- DOWN
12
12
  * DROP TABLE users;
13
13
  */
14
- import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
15
- import { join, basename } from 'node:path';
14
+ import { createHash } from 'node:crypto';
15
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
16
+ import { join } from 'node:path';
16
17
  import pg from 'pg';
18
+ import { MigrationError } from '../errors.js';
19
+ import { quoteIdent } from '../query.js';
17
20
  // ---------------------------------------------------------------------------
18
21
  // Tracking table management
19
22
  // ---------------------------------------------------------------------------
20
23
  const TRACKING_TABLE = '_turbine_migrations';
24
+ const QUOTED_TRACKING_TABLE = quoteIdent(TRACKING_TABLE);
21
25
  const CREATE_TRACKING_TABLE = `
22
- CREATE TABLE IF NOT EXISTS ${TRACKING_TABLE} (
26
+ CREATE TABLE IF NOT EXISTS ${QUOTED_TRACKING_TABLE} (
23
27
  id SERIAL PRIMARY KEY,
24
28
  name TEXT NOT NULL UNIQUE,
25
29
  checksum TEXT NOT NULL,
@@ -31,7 +35,7 @@ async function ensureTrackingTable(client) {
31
35
  }
32
36
  async function getAppliedMigrations(client) {
33
37
  await ensureTrackingTable(client);
34
- const result = await client.query(`SELECT id, name, applied_at, checksum FROM ${TRACKING_TABLE} ORDER BY id ASC`);
38
+ const result = await client.query(`SELECT id, name, applied_at, checksum FROM ${QUOTED_TRACKING_TABLE} ORDER BY id ASC`);
35
39
  return result.rows;
36
40
  }
37
41
  // ---------------------------------------------------------------------------
@@ -39,10 +43,10 @@ async function getAppliedMigrations(client) {
39
43
  // ---------------------------------------------------------------------------
40
44
  /**
41
45
  * Parse a migration filename into its components.
42
- * Expected format: YYYYMMDD_NNN_description.sql
46
+ * Expected format: YYYYMMDDHHMMSS_description.sql
43
47
  */
44
- function parseMigrationFilename(filename) {
45
- const match = filename.match(/^(\d{8})_(\d{3})_(.+)\.sql$/);
48
+ export function parseMigrationFilename(filename) {
49
+ const match = filename.match(/^(\d{14})_(.+)\.sql$/);
46
50
  if (!match)
47
51
  return null;
48
52
  return {
@@ -50,9 +54,39 @@ function parseMigrationFilename(filename) {
50
54
  path: '', // Set by caller
51
55
  name: filename.replace(/\.sql$/, ''),
52
56
  timestamp: match[1],
53
- sequence: match[2],
54
57
  };
55
58
  }
59
+ /**
60
+ * Sanitize a migration name: lowercase, replace non-alnum with _, collapse duplicates, trim.
61
+ */
62
+ export function sanitizeName(name) {
63
+ return name
64
+ .toLowerCase()
65
+ .replace(/[^a-z0-9_]/g, '_')
66
+ .replace(/_+/g, '_')
67
+ .replace(/^_|_$/g, '');
68
+ }
69
+ /**
70
+ * Generate a YYYYMMDDHHMMSS timestamp string from a Date.
71
+ */
72
+ export function formatTimestamp(date) {
73
+ return [
74
+ date.getFullYear(),
75
+ String(date.getMonth() + 1).padStart(2, '0'),
76
+ String(date.getDate()).padStart(2, '0'),
77
+ String(date.getHours()).padStart(2, '0'),
78
+ String(date.getMinutes()).padStart(2, '0'),
79
+ String(date.getSeconds()).padStart(2, '0'),
80
+ ].join('');
81
+ }
82
+ /**
83
+ * Get pending migration files — those not yet applied.
84
+ * Returns files sorted by timestamp (ascending).
85
+ */
86
+ export function getPendingMigrations(migrationsDir, applied) {
87
+ const appliedSet = new Set(applied);
88
+ return listMigrationFiles(migrationsDir).filter((f) => !appliedSet.has(f.name));
89
+ }
56
90
  /**
57
91
  * List all migration files in the migrations directory, sorted by name.
58
92
  */
@@ -73,10 +107,10 @@ export function listMigrationFiles(migrationsDir) {
73
107
  return files;
74
108
  }
75
109
  /**
76
- * Parse a migration file into UP and DOWN sections.
110
+ * Parse migration content string into UP and DOWN sections.
111
+ * Exported for unit testing.
77
112
  */
78
- export function parseMigrationSQL(filePath) {
79
- const content = readFileSync(filePath, 'utf-8');
113
+ export function parseMigrationContent(content) {
80
114
  const lines = content.split('\n');
81
115
  let section = 'none';
82
116
  const upLines = [];
@@ -102,58 +136,67 @@ export function parseMigrationSQL(filePath) {
102
136
  };
103
137
  }
104
138
  /**
105
- * Simple checksum for a migration file (for drift detection).
139
+ * Parse a migration file into UP and DOWN sections.
140
+ */
141
+ export function parseMigrationSQL(filePath) {
142
+ const content = readFileSync(filePath, 'utf-8');
143
+ return parseMigrationContent(content);
144
+ }
145
+ /**
146
+ * SHA-256 checksum for migration drift detection.
147
+ * Returns a hex-encoded hash of the file content.
106
148
  */
107
149
  function checksum(content) {
108
- let hash = 0;
109
- for (let i = 0; i < content.length; i++) {
110
- const chr = content.charCodeAt(i);
111
- hash = ((hash << 5) - hash + chr) | 0;
112
- }
113
- return Math.abs(hash).toString(36);
150
+ return createHash('sha256').update(content, 'utf-8').digest('hex');
151
+ }
152
+ /** Detect legacy djb2 checksums (short alphanumeric strings, pre-v0.6) */
153
+ function isLegacyChecksum(hash) {
154
+ return hash.length < 64;
114
155
  }
115
156
  // ---------------------------------------------------------------------------
116
157
  // Commands
117
158
  // ---------------------------------------------------------------------------
118
159
  /**
119
160
  * Create a new migration file.
161
+ * If `autoContent` is provided, the UP/DOWN sections are pre-populated with the given SQL.
120
162
  */
121
- export function createMigration(migrationsDir, name) {
163
+ export function createMigration(migrationsDir, name, autoContent) {
122
164
  mkdirSync(migrationsDir, { recursive: true });
123
- // Get today's date as YYYYMMDD
124
165
  const now = new Date();
125
- const datePart = [
126
- now.getFullYear(),
127
- String(now.getMonth() + 1).padStart(2, '0'),
128
- String(now.getDate()).padStart(2, '0'),
129
- ].join('');
130
- // Find the next sequence number for today
131
- const existing = listMigrationFiles(migrationsDir);
132
- const todayMigrations = existing.filter((f) => f.timestamp === datePart);
133
- const nextSeq = String(todayMigrations.length + 1).padStart(3, '0');
134
- // Sanitize name: lowercase, replace spaces/special chars with underscores
135
- const safeName = name
136
- .toLowerCase()
137
- .replace(/[^a-z0-9_]/g, '_')
138
- .replace(/_+/g, '_')
139
- .replace(/^_|_$/g, '');
140
- const filename = `${datePart}_${nextSeq}_${safeName}.sql`;
166
+ const ts = formatTimestamp(now);
167
+ const safeName = sanitizeName(name);
168
+ const filename = `${ts}_${safeName}.sql`;
141
169
  const filePath = join(migrationsDir, filename);
142
- const template = `-- UP
143
- -- Write your migration SQL here
170
+ let template;
171
+ if (autoContent) {
172
+ template = `-- Migration: ${name} (auto-generated from schema diff)
173
+ -- Created: ${now.toISOString()}
174
+ -- Review this file before running: npx turbine migrate up
144
175
 
176
+ -- UP
177
+ ${autoContent.up}
145
178
 
146
179
  -- DOWN
147
- -- Write the rollback SQL here
180
+ ${autoContent.down}
181
+ `;
182
+ }
183
+ else {
184
+ template = `-- Migration: ${name}
185
+ -- Created: ${now.toISOString()}
148
186
 
187
+ -- UP
188
+ -- Write your migration SQL here
189
+
190
+ -- DOWN
191
+ -- Write your rollback SQL here
149
192
  `;
193
+ }
150
194
  writeFileSync(filePath, template, 'utf-8');
151
195
  return {
152
196
  filename,
153
197
  path: filePath,
154
198
  name: filename.replace(/\.sql$/, ''),
155
- timestamp: datePart,
156
- sequence: nextSeq,
199
+ timestamp: ts,
157
200
  };
158
201
  }
159
202
  // ---------------------------------------------------------------------------
@@ -162,17 +205,16 @@ export function createMigration(migrationsDir, name) {
162
205
  /** Fixed lock ID for Turbine migrations — prevents concurrent migrate runs */
163
206
  const MIGRATION_LOCK_ID = 8_347_291; // arbitrary but stable
164
207
  async function acquireLock(client) {
165
- const result = await client.query(`SELECT pg_try_advisory_lock($1)`, [MIGRATION_LOCK_ID]);
208
+ const result = await client.query(`SELECT pg_try_advisory_lock($1)`, [
209
+ MIGRATION_LOCK_ID,
210
+ ]);
166
211
  return result.rows[0]?.pg_try_advisory_lock ?? false;
167
212
  }
168
213
  async function releaseLock(client) {
169
214
  await client.query(`SELECT pg_advisory_unlock($1)`, [MIGRATION_LOCK_ID]);
170
215
  }
171
- // ---------------------------------------------------------------------------
172
- // Checksum validation
173
- // ---------------------------------------------------------------------------
174
216
  /**
175
- * Validate that applied migration files have not been modified since they were run.
217
+ * Validate that applied migration files have not been modified or deleted since they were run.
176
218
  * Returns an array of mismatched migrations (empty if all are clean).
177
219
  */
178
220
  async function validateChecksums(client, migrationsDir) {
@@ -182,15 +224,31 @@ async function validateChecksums(client, migrationsDir) {
182
224
  const mismatches = [];
183
225
  for (const migration of applied) {
184
226
  const file = fileMap.get(migration.name);
185
- if (!file)
186
- continue; // file deleted — not a checksum issue
227
+ if (!file) {
228
+ mismatches.push({
229
+ name: migration.name,
230
+ expected: migration.checksum,
231
+ actual: '',
232
+ type: 'missing',
233
+ });
234
+ continue;
235
+ }
187
236
  const content = readFileSync(file.path, 'utf-8');
188
237
  const currentHash = checksum(content);
189
238
  if (currentHash !== migration.checksum) {
239
+ // Auto-upgrade legacy djb2 checksums to SHA-256 without flagging as modified
240
+ if (isLegacyChecksum(migration.checksum)) {
241
+ await client.query(`UPDATE ${QUOTED_TRACKING_TABLE} SET checksum = $1 WHERE name = $2`, [
242
+ currentHash,
243
+ migration.name,
244
+ ]);
245
+ continue;
246
+ }
190
247
  mismatches.push({
191
248
  name: migration.name,
192
249
  expected: migration.checksum,
193
250
  actual: currentHash,
251
+ type: 'modified',
194
252
  });
195
253
  }
196
254
  }
@@ -212,27 +270,23 @@ export async function migrateUp(connectionString, migrationsDir, options) {
212
270
  // Acquire advisory lock to prevent concurrent migrations
213
271
  const gotLock = await acquireLock(client);
214
272
  if (!gotLock) {
215
- return {
216
- applied: [],
217
- errors: [{
218
- file: { filename: '', path: '', name: '', timestamp: '', sequence: '' },
219
- error: 'Could not acquire migration lock — another migration is already running',
220
- }],
221
- };
273
+ throw new MigrationError('[turbine] Could not acquire migration lock — another migration is already running');
222
274
  }
223
275
  try {
224
276
  await ensureTrackingTable(client);
225
- // Validate checksums of already-applied migrations
226
- const mismatches = await validateChecksums(client, migrationsDir);
227
- if (mismatches.length > 0) {
228
- const names = mismatches.map((m) => m.name).join(', ');
229
- return {
230
- applied: [],
231
- errors: [{
232
- file: { filename: '', path: '', name: '', timestamp: '', sequence: '' },
233
- error: `Checksum mismatch: migration file(s) modified after application: ${names}. This is dangerous — applied migrations should be immutable. Use --force to skip this check.`,
234
- }],
235
- };
277
+ // Validate checksums of already-applied migrations (skip with --force)
278
+ if (!options?.force) {
279
+ const mismatches = await validateChecksums(client, migrationsDir);
280
+ if (mismatches.length > 0) {
281
+ const modified = mismatches.filter((m) => m.type === 'modified');
282
+ const missing = mismatches.filter((m) => m.type === 'missing');
283
+ const parts = [];
284
+ if (modified.length > 0)
285
+ parts.push(`modified: ${modified.map((m) => m.name).join(', ')}`);
286
+ if (missing.length > 0)
287
+ parts.push(`deleted: ${missing.map((m) => m.name).join(', ')}`);
288
+ throw new MigrationError(`[turbine] Migration integrity check failed — ${parts.join('; ')}. Applied migrations should be immutable. Use --force to skip this check.`);
289
+ }
236
290
  }
237
291
  const applied = await getAppliedMigrations(client);
238
292
  const appliedNames = new Set(applied.map((m) => m.name));
@@ -254,7 +308,7 @@ export async function migrateUp(connectionString, migrationsDir, options) {
254
308
  try {
255
309
  await client.query('BEGIN');
256
310
  await client.query(up);
257
- await client.query(`INSERT INTO ${TRACKING_TABLE} (name, checksum) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING`, [file.name, hash]);
311
+ await client.query(`INSERT INTO ${QUOTED_TRACKING_TABLE} (name, checksum) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING`, [file.name, hash]);
258
312
  await client.query('COMMIT');
259
313
  results.push(file);
260
314
  }
@@ -290,13 +344,7 @@ export async function migrateDown(connectionString, migrationsDir, options) {
290
344
  try {
291
345
  const gotLock = await acquireLock(client);
292
346
  if (!gotLock) {
293
- return {
294
- rolledBack: [],
295
- errors: [{
296
- file: { filename: '', path: '', name: '', timestamp: '', sequence: '' },
297
- error: 'Could not acquire migration lock — another migration is already running',
298
- }],
299
- };
347
+ throw new MigrationError('[turbine] Could not acquire migration lock — another migration is already running');
300
348
  }
301
349
  try {
302
350
  await ensureTrackingTable(client);
@@ -314,7 +362,7 @@ export async function migrateDown(connectionString, migrationsDir, options) {
314
362
  const file = fileMap.get(migration.name);
315
363
  if (!file) {
316
364
  errors.push({
317
- file: { filename: migration.name + '.sql', path: '', name: migration.name, timestamp: '', sequence: '' },
365
+ file: { filename: `${migration.name}.sql`, path: '', name: migration.name, timestamp: '' },
318
366
  error: `Migration file not found for "${migration.name}"`,
319
367
  });
320
368
  continue;
@@ -327,7 +375,7 @@ export async function migrateDown(connectionString, migrationsDir, options) {
327
375
  try {
328
376
  await client.query('BEGIN');
329
377
  await client.query(down);
330
- await client.query(`DELETE FROM ${TRACKING_TABLE} WHERE name = $1`, [migration.name]);
378
+ await client.query(`DELETE FROM ${QUOTED_TRACKING_TABLE} WHERE name = $1`, [migration.name]);
331
379
  await client.query('COMMIT');
332
380
  results.push(file);
333
381
  }
package/dist/cli/ui.js CHANGED
@@ -7,9 +7,7 @@
7
7
  // ---------------------------------------------------------------------------
8
8
  // ANSI escape codes
9
9
  // ---------------------------------------------------------------------------
10
- const isColorSupported = process.env['NO_COLOR'] == null &&
11
- process.env['TERM'] !== 'dumb' &&
12
- (process.stdout.isTTY ?? false);
10
+ const isColorSupported = process.env.NO_COLOR == null && process.env.TERM !== 'dumb' && (process.stdout.isTTY ?? false);
13
11
  function code(open, close) {
14
12
  if (!isColorSupported)
15
13
  return (s) => s;
@@ -96,9 +94,7 @@ export function table(headers, rows) {
96
94
  return ` ${bold(h)}${' '.repeat(Math.max(0, w - stripAnsi(h).length))} `;
97
95
  })
98
96
  .join(dim(symbols.vertLine));
99
- const separator = colWidths
100
- .map((w) => symbols.line.repeat(w + 2))
101
- .join(dim(symbols.line));
97
+ const separator = colWidths.map((w) => symbols.line.repeat(w + 2)).join(dim(symbols.line));
102
98
  const bodyLines = rows.map((row) => row
103
99
  .map((cell, i) => {
104
100
  const w = colWidths[i];
@@ -177,7 +173,7 @@ export function info(msg) {
177
173
  console.log(` ${blue(symbols.info)} ${msg}`);
178
174
  }
179
175
  export function label(key, value) {
180
- console.log(` ${dim(key + ':')} ${value}`);
176
+ console.log(` ${dim(`${key}:`)} ${value}`);
181
177
  }
182
178
  export function newline() {
183
179
  console.log('');
@@ -191,7 +187,7 @@ export function divider() {
191
187
  // ---------------------------------------------------------------------------
192
188
  export function banner() {
193
189
  console.log('');
194
- console.log(` ${bold(cyan('Turbine ORM'))}`);
190
+ console.log(` ${bold(cyan('turbine-orm'))}`);
195
191
  console.log(` ${dim('TypeScript ORM with json_agg nested queries')}`);
196
192
  console.log('');
197
193
  }
@@ -208,7 +204,7 @@ export function elapsed(startMs) {
208
204
  // Strip ANSI codes (for width calculation)
209
205
  // ---------------------------------------------------------------------------
210
206
  export function stripAnsi(s) {
211
- // eslint-disable-next-line no-control-regex
207
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape codes require control characters
212
208
  return s.replace(/\x1b\[[0-9;]*m/g, '');
213
209
  }
214
210
  // ---------------------------------------------------------------------------
package/dist/client.d.ts CHANGED
@@ -22,10 +22,63 @@
22
22
  * ```
23
23
  */
24
24
  import pg from 'pg';
25
- import { QueryInterface, type DeferredQuery } from './query.js';
26
25
  import { type PipelineResults } from './pipeline.js';
26
+ import { type DeferredQuery, QueryInterface, type QueryInterfaceOptions } from './query.js';
27
27
  import type { SchemaMetadata } from './schema.js';
28
+ /**
29
+ * Minimal pg-compatible query result.
30
+ * `pg.Pool`, `@neondatabase/serverless` Pool, `@vercel/postgres` Pool and
31
+ * any driver speaking the node-postgres API all satisfy this shape.
32
+ */
33
+ export interface PgCompatQueryResult<R = Record<string, unknown>> {
34
+ rows: R[];
35
+ rowCount: number | null;
36
+ fields?: Array<{
37
+ name: string;
38
+ dataTypeID: number;
39
+ }>;
40
+ }
41
+ /**
42
+ * Minimal pg-compatible client used by TurbineClient for transactions.
43
+ * `pg.PoolClient` satisfies this; so do Neon and Vercel's equivalents.
44
+ */
45
+ export interface PgCompatPoolClient {
46
+ query<R = Record<string, unknown>>(text: string, values?: unknown[]): Promise<PgCompatQueryResult<R>>;
47
+ release(err?: Error | boolean): void;
48
+ }
49
+ /**
50
+ * Minimal pg-compatible pool. Pass any driver that satisfies this interface
51
+ * via `TurbineConfig.pool` — lets Turbine run on Neon HTTP, Vercel Postgres,
52
+ * Cloudflare Hyperdrive, or any other serverless Postgres driver.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import { Pool } from '@neondatabase/serverless';
57
+ * import { TurbineClient } from 'turbine-orm';
58
+ *
59
+ * const neonPool = new Pool({ connectionString: process.env.DATABASE_URL });
60
+ * const db = new TurbineClient({ pool: neonPool }, schema);
61
+ * ```
62
+ */
63
+ export interface PgCompatPool {
64
+ query<R = Record<string, unknown>>(text: string, values?: unknown[]): Promise<PgCompatQueryResult<R>>;
65
+ connect(): Promise<PgCompatPoolClient>;
66
+ end(): Promise<void>;
67
+ /** Optional — pools that expose stats (pg.Pool does; Neon HTTP does not) */
68
+ readonly totalCount?: number;
69
+ readonly idleCount?: number;
70
+ readonly waitingCount?: number;
71
+ /** Optional — pg.Pool supports 'error' event; HTTP drivers typically do not */
72
+ on?(event: 'error', listener: (err: Error) => void): this;
73
+ }
28
74
  export interface TurbineConfig {
75
+ /**
76
+ * An external pg-compatible pool. Use this to plug in serverless drivers
77
+ * like `@neondatabase/serverless`, `@vercel/postgres`, or any other pg-API
78
+ * compatible pool. When provided, all connection-string fields are ignored
79
+ * and Turbine will NOT create its own pg.Pool.
80
+ */
81
+ pool?: PgCompatPool;
29
82
  /** Postgres connection string (e.g. postgres://user:pass@host:5432/db) */
30
83
  connectionString?: string;
31
84
  /** Host (used if connectionString is not set) */
@@ -38,6 +91,13 @@ export interface TurbineConfig {
38
91
  user?: string;
39
92
  /** Password */
40
93
  password?: string;
94
+ /** SSL/TLS options for the connection (required for most cloud providers) */
95
+ ssl?: boolean | {
96
+ rejectUnauthorized?: boolean;
97
+ ca?: string;
98
+ key?: string;
99
+ cert?: string;
100
+ };
41
101
  /** Maximum number of connections in the pool (default: 10) */
42
102
  poolSize?: number;
43
103
  /** Idle timeout in ms before a connection is closed (default: 30000) */
@@ -46,6 +106,10 @@ export interface TurbineConfig {
46
106
  connectionTimeoutMs?: number;
47
107
  /** Enable query logging to console (default: false) */
48
108
  logging?: boolean;
109
+ /** Default LIMIT applied to findMany() when no limit is specified (opt-in, default: undefined) */
110
+ defaultLimit?: number;
111
+ /** Log a warning when findMany() is called without a limit (default: false) */
112
+ warnOnUnlimited?: boolean;
49
113
  }
50
114
  /** Parameters passed to middleware functions */
51
115
  export interface MiddlewareParams {
@@ -75,9 +139,10 @@ export declare class TransactionClient {
75
139
  private readonly client;
76
140
  readonly schema: SchemaMetadata;
77
141
  private readonly middlewares;
142
+ private readonly queryOptions?;
78
143
  private readonly tableCache;
79
144
  private savepointCounter;
80
- constructor(client: pg.PoolClient, schema: SchemaMetadata, middlewares: Middleware[]);
145
+ constructor(client: pg.PoolClient, schema: SchemaMetadata, middlewares: Middleware[], queryOptions?: QueryInterfaceOptions | undefined);
81
146
  /**
82
147
  * Get a QueryInterface for a table within this transaction.
83
148
  * Uses the dedicated transaction connection instead of the pool.
@@ -96,6 +161,10 @@ export declare class TransactionClient {
96
161
  * Create a pool-like wrapper around the transaction client.
97
162
  * This allows QueryInterface to work with the transaction connection
98
163
  * without knowing it's in a transaction.
164
+ *
165
+ * pg driver errors thrown by queries are translated into typed Turbine
166
+ * errors via wrapPgError so transaction-scoped queries surface the same
167
+ * typed errors as pool-scoped queries.
99
168
  */
100
169
  private createTxPool;
101
170
  }
@@ -104,13 +173,22 @@ export declare class TurbineClient {
104
173
  readonly pool: pg.Pool;
105
174
  /** The schema metadata this client was built from */
106
175
  readonly schema: SchemaMetadata;
176
+ private static int8ParserRegistered;
107
177
  private readonly logging;
108
178
  private readonly tableCache;
109
179
  private readonly middlewares;
180
+ private readonly queryOptions;
181
+ /** True when Turbine created the pool and is responsible for tearing it down */
182
+ private readonly ownsPool;
110
183
  constructor(config: TurbineConfig | undefined, schema: SchemaMetadata);
111
184
  /**
112
185
  * Register a middleware function that runs before/after every query.
113
186
  *
187
+ * Middleware can inspect and log query parameters, modify results after execution,
188
+ * and measure timing. Note: query SQL is generated before middleware runs, so
189
+ * modifying params.args in middleware will NOT affect the executed SQL.
190
+ * To intercept queries before SQL generation, use the raw() method instead.
191
+ *
114
192
  * @example
115
193
  * ```ts
116
194
  * // Query timing middleware
@@ -198,11 +276,17 @@ export declare class TurbineClient {
198
276
  connect(): Promise<void>;
199
277
  /**
200
278
  * Gracefully shut down the connection pool.
279
+ *
280
+ * If Turbine was given an external pool via `TurbineConfig.pool`, this
281
+ * method is a no-op — the caller is responsible for the pool's lifecycle.
201
282
  */
202
283
  disconnect(): Promise<void>;
203
284
  /** Alias for disconnect() */
204
285
  end(): Promise<void>;
205
- /** Pool statistics for monitoring. */
286
+ /**
287
+ * Pool statistics for monitoring. Returns zeros for pools that don't
288
+ * expose connection counts (e.g., stateless HTTP drivers like Neon).
289
+ */
206
290
  get stats(): {
207
291
  totalCount: number;
208
292
  idleCount: number;