turbine-orm 0.4.0 → 0.5.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 (59) hide show
  1. package/README.md +51 -2
  2. package/dist/cjs/cli/config.js +161 -0
  3. package/dist/cjs/cli/index.js +977 -0
  4. package/dist/cjs/cli/migrate.js +421 -0
  5. package/dist/cjs/cli/ui.js +237 -0
  6. package/dist/cjs/client.js +449 -0
  7. package/dist/cjs/generate.js +301 -0
  8. package/dist/cjs/index.js +75 -0
  9. package/dist/cjs/introspect.js +289 -0
  10. package/dist/cjs/package.json +1 -0
  11. package/dist/cjs/pipeline.js +71 -0
  12. package/dist/cjs/query.js +1558 -0
  13. package/dist/cjs/schema-builder.js +169 -0
  14. package/dist/cjs/schema-sql.js +371 -0
  15. package/dist/cjs/schema.js +137 -0
  16. package/dist/cjs/serverless.js +199 -0
  17. package/dist/cli/config.js +1 -1
  18. package/dist/cli/index.js +16 -8
  19. package/dist/cli/migrate.d.ts +29 -5
  20. package/dist/cli/migrate.js +58 -35
  21. package/dist/cli/ui.js +1 -1
  22. package/dist/client.d.ts +15 -4
  23. package/dist/client.js +28 -15
  24. package/dist/generate.d.ts +1 -1
  25. package/dist/generate.js +13 -7
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.js +1 -1
  28. package/dist/introspect.d.ts +1 -1
  29. package/dist/introspect.js +1 -1
  30. package/dist/pipeline.d.ts +1 -1
  31. package/dist/pipeline.js +1 -1
  32. package/dist/query.d.ts +55 -11
  33. package/dist/query.js +135 -140
  34. package/dist/schema-builder.d.ts +2 -2
  35. package/dist/schema-builder.js +2 -2
  36. package/dist/schema-sql.d.ts +1 -1
  37. package/dist/schema-sql.js +31 -15
  38. package/dist/schema.d.ts +1 -1
  39. package/dist/schema.js +1 -1
  40. package/dist/serverless.d.ts +3 -3
  41. package/dist/serverless.js +4 -4
  42. package/dist/types.d.ts +1 -1
  43. package/dist/types.js +1 -1
  44. package/package.json +17 -11
  45. package/dist/cli/config.d.ts.map +0 -1
  46. package/dist/cli/index.d.ts.map +0 -1
  47. package/dist/cli/migrate.d.ts.map +0 -1
  48. package/dist/cli/ui.d.ts.map +0 -1
  49. package/dist/client.d.ts.map +0 -1
  50. package/dist/generate.d.ts.map +0 -1
  51. package/dist/index.d.ts.map +0 -1
  52. package/dist/introspect.d.ts.map +0 -1
  53. package/dist/pipeline.d.ts.map +0 -1
  54. package/dist/query.d.ts.map +0 -1
  55. package/dist/schema-builder.d.ts.map +0 -1
  56. package/dist/schema-sql.d.ts.map +0 -1
  57. package/dist/schema.d.ts.map +0 -1
  58. package/dist/serverless.d.ts.map +0 -1
  59. package/dist/types.d.ts.map +0 -1
@@ -0,0 +1,449 @@
1
+ "use strict";
2
+ /**
3
+ * @batadata/turbine — 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 '@batadata/turbine';
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 query_js_1 = require("./query.js");
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
+ });
47
+ /** Maps isolation level names to SQL */
48
+ const ISOLATION_LEVELS = {
49
+ ReadUncommitted: 'READ UNCOMMITTED',
50
+ ReadCommitted: 'READ COMMITTED',
51
+ RepeatableRead: 'REPEATABLE READ',
52
+ Serializable: 'SERIALIZABLE',
53
+ };
54
+ // ---------------------------------------------------------------------------
55
+ // TransactionClient — provides typed table accessors within a transaction
56
+ // ---------------------------------------------------------------------------
57
+ /**
58
+ * A transaction-scoped client that provides the same table accessor API as TurbineClient.
59
+ * All queries run on a dedicated connection within a BEGIN/COMMIT block.
60
+ * Supports nested transactions via SAVEPOINTs.
61
+ */
62
+ class TransactionClient {
63
+ client;
64
+ schema;
65
+ middlewares;
66
+ queryOptions;
67
+ tableCache = new Map();
68
+ savepointCounter = 0;
69
+ constructor(client, schema, middlewares, queryOptions) {
70
+ this.client = client;
71
+ this.schema = schema;
72
+ this.middlewares = middlewares;
73
+ this.queryOptions = queryOptions;
74
+ // Auto-create typed table accessors for all tables in the schema
75
+ for (const tableName of Object.keys(schema.tables)) {
76
+ const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
77
+ if (!(camelName in this)) {
78
+ Object.defineProperty(this, camelName, {
79
+ get: () => this.table(tableName),
80
+ enumerable: true,
81
+ });
82
+ }
83
+ }
84
+ }
85
+ /**
86
+ * Get a QueryInterface for a table within this transaction.
87
+ * Uses the dedicated transaction connection instead of the pool.
88
+ */
89
+ table(name) {
90
+ let qi = this.tableCache.get(name);
91
+ if (!qi) {
92
+ // Create a QueryInterface that uses the transaction client as its "pool"
93
+ // We use a proxy pool that routes queries through the transaction client
94
+ const txPool = this.createTxPool();
95
+ qi = new query_js_1.QueryInterface(txPool, name, this.schema, this.middlewares, this.queryOptions);
96
+ this.tableCache.set(name, qi);
97
+ }
98
+ return qi;
99
+ }
100
+ /**
101
+ * Execute a nested transaction via SAVEPOINT.
102
+ * If the inner function throws, only the savepoint is rolled back.
103
+ */
104
+ async $transaction(fn) {
105
+ const savepointName = `sp_${++this.savepointCounter}`;
106
+ await this.client.query(`SAVEPOINT ${savepointName}`);
107
+ try {
108
+ const result = await fn(this);
109
+ await this.client.query(`RELEASE SAVEPOINT ${savepointName}`);
110
+ return result;
111
+ }
112
+ catch (err) {
113
+ await this.client.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
114
+ throw err;
115
+ }
116
+ }
117
+ /**
118
+ * Execute a raw SQL query within this transaction.
119
+ */
120
+ async raw(strings, ...values) {
121
+ let sql = '';
122
+ strings.forEach((str, i) => {
123
+ sql += str;
124
+ if (i < values.length) {
125
+ sql += `$${i + 1}`;
126
+ }
127
+ });
128
+ const result = await this.client.query(sql, values);
129
+ return result.rows;
130
+ }
131
+ /**
132
+ * Create a pool-like wrapper around the transaction client.
133
+ * This allows QueryInterface to work with the transaction connection
134
+ * without knowing it's in a transaction.
135
+ */
136
+ createTxPool() {
137
+ const client = this.client;
138
+ // Return a minimal pool-compatible object that routes queries
139
+ // through the transaction client
140
+ return {
141
+ query: (text, values) => client.query(text, values),
142
+ connect: () => Promise.resolve(client),
143
+ };
144
+ }
145
+ }
146
+ exports.TransactionClient = TransactionClient;
147
+ // ---------------------------------------------------------------------------
148
+ // TurbineClient
149
+ // ---------------------------------------------------------------------------
150
+ class TurbineClient {
151
+ /** The underlying pg.Pool — exposed for escape hatches */
152
+ pool;
153
+ /** The schema metadata this client was built from */
154
+ schema;
155
+ logging;
156
+ tableCache = new Map();
157
+ middlewares = [];
158
+ queryOptions;
159
+ constructor(config = {}, schema) {
160
+ this.logging = config.logging ?? false;
161
+ this.schema = schema;
162
+ this.queryOptions = {
163
+ defaultLimit: config.defaultLimit,
164
+ warnOnUnlimited: config.warnOnUnlimited,
165
+ };
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;
173
+ }
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;
180
+ }
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`);
187
+ }
188
+ // Auto-create typed table accessors for all tables in the schema
189
+ for (const tableName of Object.keys(schema.tables)) {
190
+ const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
191
+ if (!(camelName in this)) {
192
+ Object.defineProperty(this, camelName, {
193
+ get: () => this.table(tableName),
194
+ enumerable: true,
195
+ });
196
+ }
197
+ }
198
+ }
199
+ // -------------------------------------------------------------------------
200
+ // Middleware — intercept all queries
201
+ // -------------------------------------------------------------------------
202
+ /**
203
+ * Register a middleware function that runs before/after every query.
204
+ *
205
+ * Middleware can inspect and log query parameters, modify results after execution,
206
+ * and measure timing. Note: query SQL is generated before middleware runs, so
207
+ * modifying params.args in middleware will NOT affect the executed SQL.
208
+ * To intercept queries before SQL generation, use the raw() method instead.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * // Query timing middleware
213
+ * db.$use(async (params, next) => {
214
+ * const before = Date.now();
215
+ * const result = await next(params);
216
+ * console.log(`${params.model}.${params.action} took ${Date.now() - before}ms`);
217
+ * return result;
218
+ * });
219
+ *
220
+ * // Soft-delete middleware
221
+ * db.$use(async (params, next) => {
222
+ * if (params.action === 'findMany' || params.action === 'findUnique') {
223
+ * params.args.where = { ...params.args.where, deletedAt: null };
224
+ * }
225
+ * if (params.action === 'delete') {
226
+ * params.action = 'update';
227
+ * params.args = { where: params.args.where, data: { deletedAt: new Date() } };
228
+ * }
229
+ * return next(params);
230
+ * });
231
+ * ```
232
+ */
233
+ $use(middleware) {
234
+ this.middlewares.push(middleware);
235
+ // Clear table cache so new QueryInterfaces pick up the middleware
236
+ this.tableCache.clear();
237
+ }
238
+ // -------------------------------------------------------------------------
239
+ // Table accessor — creates QueryInterface for any table
240
+ // -------------------------------------------------------------------------
241
+ /**
242
+ * Get a QueryInterface for a table.
243
+ * Results are cached — calling `table('users')` twice returns the same instance.
244
+ */
245
+ table(name) {
246
+ let qi = this.tableCache.get(name);
247
+ if (!qi) {
248
+ qi = new query_js_1.QueryInterface(this.pool, name, this.schema, this.middlewares, this.queryOptions);
249
+ this.tableCache.set(name, qi);
250
+ }
251
+ return qi;
252
+ }
253
+ // -------------------------------------------------------------------------
254
+ // Pipeline — batch multiple queries into one round-trip
255
+ // -------------------------------------------------------------------------
256
+ /**
257
+ * Execute multiple queries in a single database round-trip.
258
+ *
259
+ * Pass the result of any `.build*()` method on a table accessor.
260
+ */
261
+ async pipeline(...queries) {
262
+ if (this.logging) {
263
+ console.log(`[turbine] Pipeline: ${queries.length} queries — ${queries.map((q) => q.tag).join(', ')}`);
264
+ }
265
+ return (0, pipeline_js_1.executePipeline)(this.pool, queries);
266
+ }
267
+ // -------------------------------------------------------------------------
268
+ // Raw SQL — tagged template literal escape hatch
269
+ // -------------------------------------------------------------------------
270
+ /**
271
+ * Execute a raw SQL query with parameter interpolation via tagged templates.
272
+ *
273
+ * @example
274
+ * ```ts
275
+ * const result = await db.raw<{ day: Date; count: number }>`
276
+ * SELECT DATE_TRUNC('day', created_at) as day, COUNT(*)::int as count
277
+ * FROM posts WHERE org_id = ${orgId}
278
+ * GROUP BY day ORDER BY day
279
+ * `;
280
+ * ```
281
+ */
282
+ async raw(strings, ...values) {
283
+ let sql = '';
284
+ strings.forEach((str, i) => {
285
+ sql += str;
286
+ if (i < values.length) {
287
+ sql += `$${i + 1}`;
288
+ }
289
+ });
290
+ if (this.logging) {
291
+ console.log(`[turbine] Raw SQL: ${sql.trim().substring(0, 120)}...`);
292
+ }
293
+ const result = await this.pool.query(sql, values);
294
+ return result.rows;
295
+ }
296
+ // -------------------------------------------------------------------------
297
+ // Transaction support (raw — legacy)
298
+ // -------------------------------------------------------------------------
299
+ /**
300
+ * Execute a function within a database transaction (raw pg.PoolClient).
301
+ * For the typed API, use `$transaction()` instead.
302
+ *
303
+ * @example
304
+ * ```ts
305
+ * await db.transaction(async (client) => {
306
+ * await client.query('INSERT INTO users (name) VALUES ($1)', ['Alice']);
307
+ * });
308
+ * ```
309
+ */
310
+ async transaction(fn) {
311
+ const client = await this.pool.connect();
312
+ try {
313
+ await client.query('BEGIN');
314
+ const result = await fn(client);
315
+ await client.query('COMMIT');
316
+ return result;
317
+ }
318
+ catch (err) {
319
+ await client.query('ROLLBACK');
320
+ throw err;
321
+ }
322
+ finally {
323
+ client.release();
324
+ }
325
+ }
326
+ // -------------------------------------------------------------------------
327
+ // $transaction — Prisma-style typed transaction API
328
+ // -------------------------------------------------------------------------
329
+ /**
330
+ * Execute a function within a database transaction with full typed table accessors.
331
+ *
332
+ * The `tx` object provides the same table accessor API as the main client.
333
+ * Supports nested transactions via SAVEPOINTs, timeouts, and isolation levels.
334
+ *
335
+ * @example
336
+ * ```ts
337
+ * await db.$transaction(async (tx) => {
338
+ * const user = await tx.users.create({ data: { email: 'a@b.com' } });
339
+ * await tx.posts.create({ data: { userId: user.id, title: 'Hello' } });
340
+ * });
341
+ *
342
+ * // With options:
343
+ * await db.$transaction(async (tx) => {
344
+ * // ...
345
+ * }, { timeout: 5000, isolationLevel: 'Serializable' });
346
+ * ```
347
+ */
348
+ async $transaction(fn, options) {
349
+ const client = await this.pool.connect();
350
+ const timeout = options?.timeout;
351
+ try {
352
+ // BEGIN with optional isolation level
353
+ let beginSQL = 'BEGIN';
354
+ if (options?.isolationLevel) {
355
+ const level = ISOLATION_LEVELS[options.isolationLevel];
356
+ if (level)
357
+ beginSQL += ` ISOLATION LEVEL ${level}`;
358
+ }
359
+ await client.query(beginSQL);
360
+ // Create the transaction client with typed table accessors
361
+ const tx = new TransactionClient(client, this.schema, this.middlewares, this.queryOptions);
362
+ // Dynamically attach table accessors to tx
363
+ for (const tableName of Object.keys(this.schema.tables)) {
364
+ const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
365
+ if (!(camelName in tx)) {
366
+ Object.defineProperty(tx, camelName, {
367
+ get: () => tx.table(tableName),
368
+ enumerable: true,
369
+ });
370
+ }
371
+ }
372
+ let result;
373
+ if (timeout) {
374
+ // Race between the function and a timeout
375
+ let timer;
376
+ const timeoutPromise = new Promise((_, reject) => {
377
+ timer = setTimeout(() => {
378
+ reject(new Error(`[turbine] Transaction timed out after ${timeout}ms`));
379
+ }, timeout);
380
+ });
381
+ try {
382
+ result = await Promise.race([fn(tx), timeoutPromise]);
383
+ }
384
+ finally {
385
+ clearTimeout(timer);
386
+ }
387
+ }
388
+ else {
389
+ result = await fn(tx);
390
+ }
391
+ await client.query('COMMIT');
392
+ if (this.logging) {
393
+ console.log('[turbine] Transaction committed');
394
+ }
395
+ return result;
396
+ }
397
+ catch (err) {
398
+ await client.query('ROLLBACK');
399
+ if (this.logging) {
400
+ console.log('[turbine] Transaction rolled back');
401
+ }
402
+ throw err;
403
+ }
404
+ finally {
405
+ client.release();
406
+ }
407
+ }
408
+ // -------------------------------------------------------------------------
409
+ // Connection lifecycle
410
+ // -------------------------------------------------------------------------
411
+ /**
412
+ * Test the database connection.
413
+ * Throws if the connection fails.
414
+ */
415
+ async connect() {
416
+ const client = await this.pool.connect();
417
+ try {
418
+ await client.query('SELECT 1');
419
+ if (this.logging) {
420
+ console.log('[turbine] Connection verified');
421
+ }
422
+ }
423
+ finally {
424
+ client.release();
425
+ }
426
+ }
427
+ /**
428
+ * Gracefully shut down the connection pool.
429
+ */
430
+ async disconnect() {
431
+ await this.pool.end();
432
+ if (this.logging) {
433
+ console.log('[turbine] Pool disconnected');
434
+ }
435
+ }
436
+ /** Alias for disconnect() */
437
+ async end() {
438
+ return this.disconnect();
439
+ }
440
+ /** Pool statistics for monitoring. */
441
+ get stats() {
442
+ return {
443
+ totalCount: this.pool.totalCount,
444
+ idleCount: this.pool.idleCount,
445
+ waitingCount: this.pool.waitingCount,
446
+ };
447
+ }
448
+ }
449
+ exports.TurbineClient = TurbineClient;