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,199 @@
1
+ "use strict";
2
+ /**
3
+ * @batadata/turbine/serverless — HTTP-based query driver for edge functions
4
+ *
5
+ * Use this driver when you cannot establish a direct TCP connection to Postgres
6
+ * (e.g., Vercel Edge Functions, Cloudflare Workers, Deno Deploy).
7
+ *
8
+ * It sends queries as JSON over HTTP to a Turbine query endpoint, which executes
9
+ * them against the actual database and returns typed results.
10
+ *
11
+ * NOTE: This is a scaffold. The server-side query endpoint does not exist yet.
12
+ * The HTTP protocol and response format are defined here and will be implemented
13
+ * on the server side in a future release.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { createServerlessClient } from '@batadata/turbine/serverless';
18
+ *
19
+ * const db = createServerlessClient({
20
+ * endpoint: 'https://your-turbine-proxy.fly.dev/query',
21
+ * authToken: process.env.TURBINE_AUTH_TOKEN!,
22
+ * });
23
+ *
24
+ * const result = await db.query('SELECT * FROM users WHERE id = $1', [1]);
25
+ * ```
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.ServerlessClient = void 0;
29
+ exports.createServerlessClient = createServerlessClient;
30
+ // ---------------------------------------------------------------------------
31
+ // Serverless client
32
+ // ---------------------------------------------------------------------------
33
+ /**
34
+ * HTTP-based Postgres query client for serverless/edge environments.
35
+ *
36
+ * Sends SQL queries as JSON POST requests to a Turbine query endpoint.
37
+ * Does not require a direct TCP connection to Postgres.
38
+ */
39
+ class ServerlessClient {
40
+ config;
41
+ fetchFn;
42
+ constructor(config) {
43
+ if (!config.endpoint) {
44
+ throw new Error('[turbine/serverless] endpoint is required');
45
+ }
46
+ if (!config.authToken) {
47
+ throw new Error('[turbine/serverless] authToken is required');
48
+ }
49
+ this.config = {
50
+ ...config,
51
+ timeout: config.timeout ?? 10_000,
52
+ };
53
+ this.fetchFn = config.fetch ?? globalThis.fetch;
54
+ }
55
+ /**
56
+ * Execute a single SQL query.
57
+ *
58
+ * @param sql - SQL string with $1, $2, ... placeholders
59
+ * @param params - Parameter values
60
+ * @returns Query result with typed rows
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * const result = await client.query<{ id: number; name: string }>(
65
+ * 'SELECT id, name FROM users WHERE org_id = $1',
66
+ * [42]
67
+ * );
68
+ * console.log(result.rows);
69
+ * ```
70
+ */
71
+ async query(sql, params) {
72
+ const request = { sql, params, mode: 'rows' };
73
+ const response = await this.post('/query', request);
74
+ return response;
75
+ }
76
+ /**
77
+ * Execute a single SQL query and return the first row, or null.
78
+ */
79
+ async queryOne(sql, params) {
80
+ const request = { sql, params, mode: 'one' };
81
+ const response = await this.post('/query', request);
82
+ return response.rows[0] ?? null;
83
+ }
84
+ /**
85
+ * Execute a batch of queries in a single HTTP request.
86
+ * Optionally wraps them in a transaction.
87
+ *
88
+ * @param queries - Array of queries to execute
89
+ * @param options - Batch options
90
+ * @returns Array of results, one per query
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * const results = await client.batch([
95
+ * { sql: 'SELECT * FROM users WHERE id = $1', params: [1] },
96
+ * { sql: 'SELECT COUNT(*) FROM posts WHERE user_id = $1', params: [1] },
97
+ * ], { transaction: true });
98
+ * ```
99
+ */
100
+ async batch(queries, options) {
101
+ const request = {
102
+ queries,
103
+ transaction: options?.transaction ?? false,
104
+ };
105
+ return this.post('/batch', request);
106
+ }
107
+ /**
108
+ * Tagged template helper for SQL queries.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * const users = await client.sql<{ id: number; name: string }>`
113
+ * SELECT id, name FROM users WHERE org_id = ${orgId}
114
+ * `;
115
+ * ```
116
+ */
117
+ async sql(strings, ...values) {
118
+ let sqlStr = '';
119
+ strings.forEach((str, i) => {
120
+ sqlStr += str;
121
+ if (i < values.length) {
122
+ sqlStr += `$${i + 1}`;
123
+ }
124
+ });
125
+ const result = await this.query(sqlStr, values);
126
+ return result.rows;
127
+ }
128
+ // -------------------------------------------------------------------------
129
+ // Internal HTTP transport
130
+ // -------------------------------------------------------------------------
131
+ async post(path, body) {
132
+ const url = this.config.endpoint.replace(/\/$/, '') + path;
133
+ const controller = new AbortController();
134
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
135
+ try {
136
+ const response = await this.fetchFn(url, {
137
+ method: 'POST',
138
+ headers: {
139
+ 'Content-Type': 'application/json',
140
+ 'Authorization': `Bearer ${this.config.authToken}`,
141
+ 'User-Agent': '@batadata/turbine-serverless',
142
+ ...this.config.headers,
143
+ },
144
+ body: JSON.stringify(body),
145
+ signal: controller.signal,
146
+ });
147
+ if (!response.ok) {
148
+ const errorBody = await response.text();
149
+ let parsed;
150
+ try {
151
+ parsed = JSON.parse(errorBody);
152
+ }
153
+ catch {
154
+ // Not JSON
155
+ }
156
+ const message = parsed?.error ?? `HTTP ${response.status}: ${errorBody.slice(0, 200)}`;
157
+ const err = new Error(`[turbine/serverless] ${message}`);
158
+ err['status'] = response.status;
159
+ err['code'] = parsed?.code;
160
+ throw err;
161
+ }
162
+ return (await response.json());
163
+ }
164
+ catch (err) {
165
+ if (err instanceof DOMException && err.name === 'AbortError') {
166
+ throw new Error(`[turbine/serverless] Request timed out after ${this.config.timeout}ms`);
167
+ }
168
+ throw err;
169
+ }
170
+ finally {
171
+ clearTimeout(timeoutId);
172
+ }
173
+ }
174
+ }
175
+ exports.ServerlessClient = ServerlessClient;
176
+ // ---------------------------------------------------------------------------
177
+ // Factory function
178
+ // ---------------------------------------------------------------------------
179
+ /**
180
+ * Create a serverless Turbine client for edge/serverless environments.
181
+ *
182
+ * @param config - Endpoint URL and auth token
183
+ * @returns A ServerlessClient instance
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * import { createServerlessClient } from '@batadata/turbine/serverless';
188
+ *
189
+ * const db = createServerlessClient({
190
+ * endpoint: process.env.TURBINE_ENDPOINT!,
191
+ * authToken: process.env.TURBINE_AUTH_TOKEN!,
192
+ * });
193
+ *
194
+ * const users = await db.sql`SELECT * FROM users LIMIT 10`;
195
+ * ```
196
+ */
197
+ function createServerlessClient(config) {
198
+ return new ServerlessClient(config);
199
+ }
@@ -92,7 +92,7 @@ export function configTemplate(connectionString) {
92
92
 
93
93
  /**
94
94
  * Turbine configuration
95
- * @see https://github.com/zvndev/turbine-orm
95
+ * @see https://batadata.com/docs/turbine/config
96
96
  */
97
97
  const config: TurbineCliConfig = {
98
98
  /** Postgres connection string */
package/dist/cli/index.js CHANGED
@@ -231,7 +231,7 @@ async function cmdInit(args, config) {
231
231
  * Define your database schema in TypeScript.
232
232
  * Use \`npx turbine push\` to sync it to your database.
233
233
  *
234
- * @see https://github.com/zvndev/turbine-orm
234
+ * @see https://batadata.com/docs/turbine/schema
235
235
  */
236
236
 
237
237
  import { defineSchema } from 'turbine-orm';
@@ -248,13 +248,20 @@ export default defineSchema({
248
248
  `, 'utf-8');
249
249
  success(`Created ${cyan(config.schemaFile)}`);
250
250
  }
251
- // Add .gitignore entry for generated output
251
+ // Add .gitignore entries for generated output and config (may contain connection strings)
252
252
  const gitignorePath = '.gitignore';
253
253
  if (existsSync(gitignorePath)) {
254
254
  const gitignoreContent = readFileSync(gitignorePath, 'utf-8');
255
+ const additions = [];
255
256
  if (!gitignoreContent.includes('generated/turbine')) {
256
- appendFileSync(gitignorePath, '\n# Turbine generated client\ngenerated/turbine/\n');
257
- success(`Added ${cyan('generated/turbine/')} to ${cyan('.gitignore')}`);
257
+ additions.push('generated/turbine/');
258
+ }
259
+ if (!gitignoreContent.includes('turbine.config.ts')) {
260
+ additions.push('turbine.config.ts');
261
+ }
262
+ if (additions.length > 0) {
263
+ appendFileSync(gitignorePath, `\n# Turbine generated client & config\n${additions.join('\n')}\n`);
264
+ success(`Added ${cyan(additions.join(', '))} to ${cyan('.gitignore')}`);
258
265
  }
259
266
  }
260
267
  // If we have a URL, run initial generate
@@ -669,10 +676,12 @@ async function cmdSeed(args, config) {
669
676
  { cmd: 'npx tsx', name: 'tsx' },
670
677
  { cmd: 'node --experimental-strip-types', name: 'node' },
671
678
  ];
679
+ // Shell-escape the seed file path to prevent injection
680
+ const escapedSeedFile = seedFile.replace(/'/g, "'\\''");
672
681
  let ran = false;
673
682
  for (const runner of runners) {
674
683
  try {
675
- execSync(`${runner.cmd} ${seedFile}`, {
684
+ execSync(`${runner.cmd} '${escapedSeedFile}'`, {
676
685
  stdio: 'inherit',
677
686
  env: {
678
687
  ...process.env,
@@ -768,7 +777,7 @@ async function cmdStudio(_args, _config) {
768
777
  'A local web UI for browsing your database,',
769
778
  'exploring relations, and managing data.',
770
779
  '',
771
- `Follow ${cyan('@zvndev')} for updates.`,
780
+ `Follow ${cyan('@batadata')} for updates.`,
772
781
  ].join('\n'), { title: bold(cyan('Studio')), padding: 2 }));
773
782
  newline();
774
783
  }
@@ -819,8 +828,7 @@ function showHelp() {
819
828
  // Version
820
829
  // ---------------------------------------------------------------------------
821
830
  function showVersion() {
822
- // Read version from package.json at build time
823
- console.log(`turbine-orm v0.3.0`);
831
+ console.log(`turbine-orm v0.5.0`);
824
832
  }
825
833
  // ---------------------------------------------------------------------------
826
834
  // Main
@@ -12,16 +12,14 @@
12
12
  * DROP TABLE users;
13
13
  */
14
14
  export interface MigrationFile {
15
- /** Full filename (e.g. "20260325_001_create_users.sql") */
15
+ /** Full filename (e.g. "20260325120000_create_users.sql") */
16
16
  filename: string;
17
17
  /** Absolute path to the file */
18
18
  path: string;
19
- /** Extracted name portion (e.g. "create_users") */
19
+ /** Extracted name portion (e.g. "20260325120000_create_users") */
20
20
  name: string;
21
- /** Timestamp prefix (e.g. "20260325") */
21
+ /** Timestamp prefix (e.g. "20260325120000") — YYYYMMDDHHMMSS */
22
22
  timestamp: string;
23
- /** Sequence number (e.g. "001") */
24
- sequence: string;
25
23
  }
26
24
  export interface AppliedMigration {
27
25
  id: number;
@@ -36,10 +34,36 @@ export interface MigrationStatus {
36
34
  /** True if the file checksum matches the stored checksum (only set for applied migrations) */
37
35
  checksumValid?: boolean;
38
36
  }
37
+ /**
38
+ * Parse a migration filename into its components.
39
+ * Expected format: YYYYMMDDHHMMSS_description.sql
40
+ */
41
+ export declare function parseMigrationFilename(filename: string): MigrationFile | null;
42
+ /**
43
+ * Sanitize a migration name: lowercase, replace non-alnum with _, collapse duplicates, trim.
44
+ */
45
+ export declare function sanitizeName(name: string): string;
46
+ /**
47
+ * Generate a YYYYMMDDHHMMSS timestamp string from a Date.
48
+ */
49
+ export declare function formatTimestamp(date: Date): string;
50
+ /**
51
+ * Get pending migration files — those not yet applied.
52
+ * Returns files sorted by timestamp (ascending).
53
+ */
54
+ export declare function getPendingMigrations(migrationsDir: string, applied: string[]): MigrationFile[];
39
55
  /**
40
56
  * List all migration files in the migrations directory, sorted by name.
41
57
  */
42
58
  export declare function listMigrationFiles(migrationsDir: string): MigrationFile[];
59
+ /**
60
+ * Parse migration content string into UP and DOWN sections.
61
+ * Exported for unit testing.
62
+ */
63
+ export declare function parseMigrationContent(content: string): {
64
+ up: string;
65
+ down: string;
66
+ };
43
67
  /**
44
68
  * Parse a migration file into UP and DOWN sections.
45
69
  */
@@ -12,7 +12,7 @@
12
12
  * DROP TABLE users;
13
13
  */
14
14
  import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
15
- import { join, basename } from 'node:path';
15
+ import { join } from 'node:path';
16
16
  import pg from 'pg';
17
17
  // ---------------------------------------------------------------------------
18
18
  // Tracking table management
@@ -39,10 +39,10 @@ async function getAppliedMigrations(client) {
39
39
  // ---------------------------------------------------------------------------
40
40
  /**
41
41
  * Parse a migration filename into its components.
42
- * Expected format: YYYYMMDD_NNN_description.sql
42
+ * Expected format: YYYYMMDDHHMMSS_description.sql
43
43
  */
44
- function parseMigrationFilename(filename) {
45
- const match = filename.match(/^(\d{8})_(\d{3})_(.+)\.sql$/);
44
+ export function parseMigrationFilename(filename) {
45
+ const match = filename.match(/^(\d{14})_(.+)\.sql$/);
46
46
  if (!match)
47
47
  return null;
48
48
  return {
@@ -50,9 +50,39 @@ function parseMigrationFilename(filename) {
50
50
  path: '', // Set by caller
51
51
  name: filename.replace(/\.sql$/, ''),
52
52
  timestamp: match[1],
53
- sequence: match[2],
54
53
  };
55
54
  }
55
+ /**
56
+ * Sanitize a migration name: lowercase, replace non-alnum with _, collapse duplicates, trim.
57
+ */
58
+ export function sanitizeName(name) {
59
+ return name
60
+ .toLowerCase()
61
+ .replace(/[^a-z0-9_]/g, '_')
62
+ .replace(/_+/g, '_')
63
+ .replace(/^_|_$/g, '');
64
+ }
65
+ /**
66
+ * Generate a YYYYMMDDHHMMSS timestamp string from a Date.
67
+ */
68
+ export function formatTimestamp(date) {
69
+ return [
70
+ date.getFullYear(),
71
+ String(date.getMonth() + 1).padStart(2, '0'),
72
+ String(date.getDate()).padStart(2, '0'),
73
+ String(date.getHours()).padStart(2, '0'),
74
+ String(date.getMinutes()).padStart(2, '0'),
75
+ String(date.getSeconds()).padStart(2, '0'),
76
+ ].join('');
77
+ }
78
+ /**
79
+ * Get pending migration files — those not yet applied.
80
+ * Returns files sorted by timestamp (ascending).
81
+ */
82
+ export function getPendingMigrations(migrationsDir, applied) {
83
+ const appliedSet = new Set(applied);
84
+ return listMigrationFiles(migrationsDir).filter((f) => !appliedSet.has(f.name));
85
+ }
56
86
  /**
57
87
  * List all migration files in the migrations directory, sorted by name.
58
88
  */
@@ -73,10 +103,10 @@ export function listMigrationFiles(migrationsDir) {
73
103
  return files;
74
104
  }
75
105
  /**
76
- * Parse a migration file into UP and DOWN sections.
106
+ * Parse migration content string into UP and DOWN sections.
107
+ * Exported for unit testing.
77
108
  */
78
- export function parseMigrationSQL(filePath) {
79
- const content = readFileSync(filePath, 'utf-8');
109
+ export function parseMigrationContent(content) {
80
110
  const lines = content.split('\n');
81
111
  let section = 'none';
82
112
  const upLines = [];
@@ -101,6 +131,13 @@ export function parseMigrationSQL(filePath) {
101
131
  down: downLines.join('\n').trim(),
102
132
  };
103
133
  }
134
+ /**
135
+ * Parse a migration file into UP and DOWN sections.
136
+ */
137
+ export function parseMigrationSQL(filePath) {
138
+ const content = readFileSync(filePath, 'utf-8');
139
+ return parseMigrationContent(content);
140
+ }
104
141
  /**
105
142
  * Simple checksum for a migration file (for drift detection).
106
143
  */
@@ -120,40 +157,26 @@ function checksum(content) {
120
157
  */
121
158
  export function createMigration(migrationsDir, name) {
122
159
  mkdirSync(migrationsDir, { recursive: true });
123
- // Get today's date as YYYYMMDD
124
160
  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`;
161
+ const ts = formatTimestamp(now);
162
+ const safeName = sanitizeName(name);
163
+ const filename = `${ts}_${safeName}.sql`;
141
164
  const filePath = join(migrationsDir, filename);
142
- const template = `-- UP
143
- -- Write your migration SQL here
165
+ const template = `-- Migration: ${name}
166
+ -- Created: ${now.toISOString()}
144
167
 
168
+ -- UP
169
+ -- Write your migration SQL here
145
170
 
146
171
  -- DOWN
147
- -- Write the rollback SQL here
148
-
172
+ -- Write your rollback SQL here
149
173
  `;
150
174
  writeFileSync(filePath, template, 'utf-8');
151
175
  return {
152
176
  filename,
153
177
  path: filePath,
154
178
  name: filename.replace(/\.sql$/, ''),
155
- timestamp: datePart,
156
- sequence: nextSeq,
179
+ timestamp: ts,
157
180
  };
158
181
  }
159
182
  // ---------------------------------------------------------------------------
@@ -215,7 +238,7 @@ export async function migrateUp(connectionString, migrationsDir, options) {
215
238
  return {
216
239
  applied: [],
217
240
  errors: [{
218
- file: { filename: '', path: '', name: '', timestamp: '', sequence: '' },
241
+ file: { filename: '', path: '', name: '', timestamp: '' },
219
242
  error: 'Could not acquire migration lock — another migration is already running',
220
243
  }],
221
244
  };
@@ -229,7 +252,7 @@ export async function migrateUp(connectionString, migrationsDir, options) {
229
252
  return {
230
253
  applied: [],
231
254
  errors: [{
232
- file: { filename: '', path: '', name: '', timestamp: '', sequence: '' },
255
+ file: { filename: '', path: '', name: '', timestamp: '' },
233
256
  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
257
  }],
235
258
  };
@@ -293,7 +316,7 @@ export async function migrateDown(connectionString, migrationsDir, options) {
293
316
  return {
294
317
  rolledBack: [],
295
318
  errors: [{
296
- file: { filename: '', path: '', name: '', timestamp: '', sequence: '' },
319
+ file: { filename: '', path: '', name: '', timestamp: '' },
297
320
  error: 'Could not acquire migration lock — another migration is already running',
298
321
  }],
299
322
  };
@@ -314,7 +337,7 @@ export async function migrateDown(connectionString, migrationsDir, options) {
314
337
  const file = fileMap.get(migration.name);
315
338
  if (!file) {
316
339
  errors.push({
317
- file: { filename: migration.name + '.sql', path: '', name: migration.name, timestamp: '', sequence: '' },
340
+ file: { filename: migration.name + '.sql', path: '', name: migration.name, timestamp: '' },
318
341
  error: `Migration file not found for "${migration.name}"`,
319
342
  });
320
343
  continue;
package/dist/cli/ui.js CHANGED
@@ -191,7 +191,7 @@ export function divider() {
191
191
  // ---------------------------------------------------------------------------
192
192
  export function banner() {
193
193
  console.log('');
194
- console.log(` ${bold(cyan('Turbine ORM'))}`);
194
+ console.log(` ${bold(cyan('turbine'))} ${dim('by')} ${bold('BataData')}`);
195
195
  console.log(` ${dim('TypeScript ORM with json_agg nested queries')}`);
196
196
  console.log('');
197
197
  }
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * turbine-orm — TurbineClient
2
+ * @batadata/turbine — TurbineClient
3
3
  *
4
4
  * The main entry point for the Turbine TypeScript SDK.
5
5
  * Manages connection pooling and provides typed table accessors.
@@ -16,13 +16,13 @@
16
16
  * const user = await db.users.findUnique({ where: { id: 1 } });
17
17
  *
18
18
  * // With base client (dynamic):
19
- * import { TurbineClient } from 'turbine-orm';
19
+ * import { TurbineClient } from '@batadata/turbine';
20
20
  * const db = new TurbineClient({ connectionString: '...' }, schema);
21
21
  * const users = db.table<User>('users');
22
22
  * ```
23
23
  */
24
24
  import pg from 'pg';
25
- import { QueryInterface, type DeferredQuery } from './query.js';
25
+ import { QueryInterface, type DeferredQuery, type QueryInterfaceOptions } from './query.js';
26
26
  import { type PipelineResults } from './pipeline.js';
27
27
  import type { SchemaMetadata } from './schema.js';
28
28
  export interface TurbineConfig {
@@ -46,6 +46,10 @@ export interface TurbineConfig {
46
46
  connectionTimeoutMs?: number;
47
47
  /** Enable query logging to console (default: false) */
48
48
  logging?: boolean;
49
+ /** Default LIMIT applied to findMany() when no limit is specified (opt-in, default: undefined) */
50
+ defaultLimit?: number;
51
+ /** Log a warning when findMany() is called without a limit (default: false) */
52
+ warnOnUnlimited?: boolean;
49
53
  }
50
54
  /** Parameters passed to middleware functions */
51
55
  export interface MiddlewareParams {
@@ -75,9 +79,10 @@ export declare class TransactionClient {
75
79
  private readonly client;
76
80
  readonly schema: SchemaMetadata;
77
81
  private readonly middlewares;
82
+ private readonly queryOptions?;
78
83
  private readonly tableCache;
79
84
  private savepointCounter;
80
- constructor(client: pg.PoolClient, schema: SchemaMetadata, middlewares: Middleware[]);
85
+ constructor(client: pg.PoolClient, schema: SchemaMetadata, middlewares: Middleware[], queryOptions?: QueryInterfaceOptions | undefined);
81
86
  /**
82
87
  * Get a QueryInterface for a table within this transaction.
83
88
  * Uses the dedicated transaction connection instead of the pool.
@@ -107,10 +112,16 @@ export declare class TurbineClient {
107
112
  private readonly logging;
108
113
  private readonly tableCache;
109
114
  private readonly middlewares;
115
+ private readonly queryOptions;
110
116
  constructor(config: TurbineConfig | undefined, schema: SchemaMetadata);
111
117
  /**
112
118
  * Register a middleware function that runs before/after every query.
113
119
  *
120
+ * Middleware can inspect and log query parameters, modify results after execution,
121
+ * and measure timing. Note: query SQL is generated before middleware runs, so
122
+ * modifying params.args in middleware will NOT affect the executed SQL.
123
+ * To intercept queries before SQL generation, use the raw() method instead.
124
+ *
114
125
  * @example
115
126
  * ```ts
116
127
  * // Query timing middleware