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
@@ -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;