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
package/dist/client.js 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,7 +16,7 @@
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
  * ```
@@ -24,19 +24,20 @@
24
24
  import pg from 'pg';
25
25
  import { QueryInterface } from './query.js';
26
26
  import { executePipeline } from './pipeline.js';
27
- // Parse int8 (bigint, OID 20) as JavaScript number instead of string.
28
- // Safe for values up to Number.MAX_SAFE_INTEGER (9,007,199,254,740,991).
27
+ /**
28
+ * Parse int8 (bigint, OID 20) as JavaScript number instead of string.
29
+ * Safe for values up to Number.MAX_SAFE_INTEGER (9,007,199,254,740,991).
30
+ *
31
+ * NOTE: For values exceeding Number.MAX_SAFE_INTEGER, the parser falls back
32
+ * to returning the raw string to avoid precision loss. The generated TypeScript
33
+ * type maps int8/bigint to `number`, which is correct for the vast majority of
34
+ * use cases (IDs, counts, timestamps). If you store values > 2^53 - 1 in a
35
+ * bigint column, the runtime return type will be `string` for those rows.
36
+ */
29
37
  pg.types.setTypeParser(20, (val) => {
30
38
  const n = Number(val);
31
39
  return Number.isSafeInteger(n) ? n : val; // fall back to string for huge values
32
40
  });
33
- // Parse numeric (OID 1700) as number when safe, string otherwise
34
- pg.types.setTypeParser(1700, (val) => {
35
- const n = Number(val);
36
- if (val.includes('.') && Number.isFinite(n))
37
- return n;
38
- return Number.isSafeInteger(n) ? n : val;
39
- });
40
41
  /** Maps isolation level names to SQL */
41
42
  const ISOLATION_LEVELS = {
42
43
  ReadUncommitted: 'READ UNCOMMITTED',
@@ -56,12 +57,14 @@ export class TransactionClient {
56
57
  client;
57
58
  schema;
58
59
  middlewares;
60
+ queryOptions;
59
61
  tableCache = new Map();
60
62
  savepointCounter = 0;
61
- constructor(client, schema, middlewares) {
63
+ constructor(client, schema, middlewares, queryOptions) {
62
64
  this.client = client;
63
65
  this.schema = schema;
64
66
  this.middlewares = middlewares;
67
+ this.queryOptions = queryOptions;
65
68
  // Auto-create typed table accessors for all tables in the schema
66
69
  for (const tableName of Object.keys(schema.tables)) {
67
70
  const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
@@ -83,7 +86,7 @@ export class TransactionClient {
83
86
  // Create a QueryInterface that uses the transaction client as its "pool"
84
87
  // We use a proxy pool that routes queries through the transaction client
85
88
  const txPool = this.createTxPool();
86
- qi = new QueryInterface(txPool, name, this.schema, this.middlewares);
89
+ qi = new QueryInterface(txPool, name, this.schema, this.middlewares, this.queryOptions);
87
90
  this.tableCache.set(name, qi);
88
91
  }
89
92
  return qi;
@@ -145,9 +148,14 @@ export class TurbineClient {
145
148
  logging;
146
149
  tableCache = new Map();
147
150
  middlewares = [];
151
+ queryOptions;
148
152
  constructor(config = {}, schema) {
149
153
  this.logging = config.logging ?? false;
150
154
  this.schema = schema;
155
+ this.queryOptions = {
156
+ defaultLimit: config.defaultLimit,
157
+ warnOnUnlimited: config.warnOnUnlimited,
158
+ };
151
159
  const poolConfig = {
152
160
  max: config.poolSize ?? 10,
153
161
  idleTimeoutMillis: config.idleTimeoutMs ?? 30_000,
@@ -187,6 +195,11 @@ export class TurbineClient {
187
195
  /**
188
196
  * Register a middleware function that runs before/after every query.
189
197
  *
198
+ * Middleware can inspect and log query parameters, modify results after execution,
199
+ * and measure timing. Note: query SQL is generated before middleware runs, so
200
+ * modifying params.args in middleware will NOT affect the executed SQL.
201
+ * To intercept queries before SQL generation, use the raw() method instead.
202
+ *
190
203
  * @example
191
204
  * ```ts
192
205
  * // Query timing middleware
@@ -225,7 +238,7 @@ export class TurbineClient {
225
238
  table(name) {
226
239
  let qi = this.tableCache.get(name);
227
240
  if (!qi) {
228
- qi = new QueryInterface(this.pool, name, this.schema, this.middlewares);
241
+ qi = new QueryInterface(this.pool, name, this.schema, this.middlewares, this.queryOptions);
229
242
  this.tableCache.set(name, qi);
230
243
  }
231
244
  return qi;
@@ -338,7 +351,7 @@ export class TurbineClient {
338
351
  }
339
352
  await client.query(beginSQL);
340
353
  // Create the transaction client with typed table accessors
341
- const tx = new TransactionClient(client, this.schema, this.middlewares);
354
+ const tx = new TransactionClient(client, this.schema, this.middlewares, this.queryOptions);
342
355
  // Dynamically attach table accessors to tx
343
356
  for (const tableName of Object.keys(this.schema.tables)) {
344
357
  const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
@@ -1,5 +1,5 @@
1
1
  /**
2
- * turbine-orm — Code generator
2
+ * @batadata/turbine — Code generator
3
3
  *
4
4
  * Takes an IntrospectedSchema and emits TypeScript files:
5
5
  * - types.ts — Entity interfaces, Create/Update input types
package/dist/generate.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * turbine-orm — Code generator
2
+ * @batadata/turbine — Code generator
3
3
  *
4
4
  * Takes an IntrospectedSchema and emits TypeScript files:
5
5
  * - types.ts — Entity interfaces, Create/Update input types
@@ -9,7 +9,7 @@
9
9
  * Output goes to the specified directory (default: ./generated/turbine/).
10
10
  */
11
11
  import { writeFileSync, mkdirSync } from 'node:fs';
12
- import { join } from 'node:path';
12
+ import { join, resolve, relative } from 'node:path';
13
13
  import { snakeToPascal, singularize, } from './schema.js';
14
14
  /** Get the TypeScript type name for a table (singularized PascalCase) */
15
15
  function entityName(tableName) {
@@ -24,6 +24,12 @@ function escSQ(value) {
24
24
  // ---------------------------------------------------------------------------
25
25
  export function generate(options) {
26
26
  const outDir = options.outDir ?? './generated/turbine';
27
+ // Path traversal protection — ensure output stays within project root
28
+ const resolved = resolve(outDir);
29
+ const rel = relative(process.cwd(), resolved);
30
+ if (rel.startsWith('..') || resolve(rel) !== resolved) {
31
+ throw new Error(`Output directory must be within the project root. Got: ${outDir}`);
32
+ }
27
33
  mkdirSync(outDir, { recursive: true });
28
34
  const files = [];
29
35
  // Generate types.ts
@@ -46,10 +52,10 @@ export function generate(options) {
46
52
  function generatedFileHeader() {
47
53
  return [
48
54
  '/**',
49
- ' * Auto-generated by turbine-orm — DO NOT EDIT',
55
+ ' * Auto-generated by @batadata/turbine — DO NOT EDIT',
50
56
  ' *',
51
57
  ` * Generated at: ${new Date().toISOString()}`,
52
- ' * @see https://github.com/zvndev/turbine-orm',
58
+ ' * @see https://batadata.com/docs/turbine',
53
59
  ' */',
54
60
  '',
55
61
  ];
@@ -136,7 +142,7 @@ function generateTypes(schema) {
136
142
  function generateMetadata(schema) {
137
143
  const lines = [
138
144
  ...generatedFileHeader(),
139
- "import type { SchemaMetadata } from 'turbine-orm';",
145
+ "import type { SchemaMetadata } from '@batadata/turbine';",
140
146
  '',
141
147
  'export const SCHEMA: SchemaMetadata = {',
142
148
  ' tables: {',
@@ -209,8 +215,8 @@ function generateIndex(schema) {
209
215
  const tableEntries = Object.values(schema.tables);
210
216
  const lines = [
211
217
  ...generatedFileHeader(),
212
- "import { TurbineClient as BaseTurbineClient, QueryInterface } from 'turbine-orm';",
213
- "import type { TurbineConfig } from 'turbine-orm';",
218
+ "import { TurbineClient as BaseTurbineClient, QueryInterface } from '@batadata/turbine';",
219
+ "import type { TurbineConfig } from '@batadata/turbine';",
214
220
  "import { SCHEMA } from './metadata.js';",
215
221
  ];
216
222
  // Import all entity types
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * turbine-orm
2
+ * @batadata/turbine
3
3
  *
4
4
  * Turbine TypeScript SDK — type-safe Postgres queries with nested relations
5
5
  * and pipeline batching. Feels like Prisma, runs at raw-SQL speed.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * turbine-orm
2
+ * @batadata/turbine
3
3
  *
4
4
  * Turbine TypeScript SDK — type-safe Postgres queries with nested relations
5
5
  * and pipeline batching. Feels like Prisma, runs at raw-SQL speed.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * turbine-orm — Schema introspection
2
+ * @batadata/turbine — Schema introspection
3
3
  *
4
4
  * Connects to a live Postgres database, reads information_schema + pg_catalog,
5
5
  * and produces a SchemaMetadata object describing every table, column, relation,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * turbine-orm — Schema introspection
2
+ * @batadata/turbine — Schema introspection
3
3
  *
4
4
  * Connects to a live Postgres database, reads information_schema + pg_catalog,
5
5
  * and produces a SchemaMetadata object describing every table, column, relation,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * turbine-orm — Pipeline execution
2
+ * @batadata/turbine — Pipeline execution
3
3
  *
4
4
  * Pipelines batch multiple independent queries into a single database round-trip.
5
5
  * Instead of N sequential awaits (N round-trips), you get 1 round-trip for all N queries.
package/dist/pipeline.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * turbine-orm — Pipeline execution
2
+ * @batadata/turbine — Pipeline execution
3
3
  *
4
4
  * Pipelines batch multiple independent queries into a single database round-trip.
5
5
  * Instead of N sequential awaits (N round-trips), you get 1 round-trip for all N queries.
package/dist/query.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * turbine-orm — Query builder
2
+ * @batadata/turbine — Query builder
3
3
  *
4
4
  * Each table accessor (db.users, db.posts, etc.) returns a QueryInterface<T>
5
5
  * that builds parameterized SQL and executes it through the connection pool.
@@ -35,6 +35,8 @@ export interface WhereOperator<V = unknown> {
35
35
  contains?: string;
36
36
  startsWith?: string;
37
37
  endsWith?: string;
38
+ /** Set to 'insensitive' to use ILIKE instead of LIKE for string comparisons */
39
+ mode?: 'default' | 'insensitive';
38
40
  }
39
41
  /**
40
42
  * A where value can be:
@@ -77,6 +79,8 @@ export interface FindUniqueArgs<T> {
77
79
  select?: Record<string, boolean>;
78
80
  omit?: Record<string, boolean>;
79
81
  with?: WithClause;
82
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
83
+ timeout?: number;
80
84
  }
81
85
  export interface FindManyArgs<T> {
82
86
  where?: WhereClause<T>;
@@ -92,36 +96,54 @@ export interface FindManyArgs<T> {
92
96
  take?: number;
93
97
  /** De-duplicate results by specified fields */
94
98
  distinct?: (keyof T & string)[];
99
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
100
+ timeout?: number;
95
101
  }
96
102
  export interface CreateArgs<T> {
97
103
  data: Partial<T>;
104
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
105
+ timeout?: number;
98
106
  }
99
107
  export interface CreateManyArgs<T> {
100
108
  data: Partial<T>[];
101
109
  /** When true, adds ON CONFLICT DO NOTHING to skip duplicate rows */
102
110
  skipDuplicates?: boolean;
111
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
112
+ timeout?: number;
103
113
  }
104
114
  export interface UpdateArgs<T> {
105
115
  where: WhereClause<T>;
106
116
  data: Partial<T>;
117
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
118
+ timeout?: number;
107
119
  }
108
120
  export interface UpdateManyArgs<T> {
109
121
  where: WhereClause<T>;
110
122
  data: Partial<T>;
123
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
124
+ timeout?: number;
111
125
  }
112
126
  export interface DeleteArgs<T> {
113
127
  where: WhereClause<T>;
128
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
129
+ timeout?: number;
114
130
  }
115
131
  export interface DeleteManyArgs<T> {
116
132
  where: WhereClause<T>;
133
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
134
+ timeout?: number;
117
135
  }
118
136
  export interface UpsertArgs<T> {
119
137
  where: WhereClause<T>;
120
138
  create: Partial<T>;
121
139
  update: Partial<T>;
140
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
141
+ timeout?: number;
122
142
  }
123
143
  export interface CountArgs<T> {
124
144
  where?: WhereClause<T>;
145
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
146
+ timeout?: number;
125
147
  }
126
148
  export interface GroupByArgs<T> {
127
149
  by: (keyof T & string)[];
@@ -140,6 +162,8 @@ export interface GroupByArgs<T> {
140
162
  having?: Record<string, unknown>;
141
163
  /** Order groups */
142
164
  orderBy?: Record<string, OrderDirection>;
165
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
166
+ timeout?: number;
143
167
  }
144
168
  /** Arguments for the standalone aggregate method */
145
169
  export interface AggregateArgs<T> {
@@ -154,6 +178,8 @@ export interface AggregateArgs<T> {
154
178
  _min?: Partial<Record<keyof T & string, boolean>>;
155
179
  /** Maximum value of fields */
156
180
  _max?: Partial<Record<keyof T & string, boolean>>;
181
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
182
+ timeout?: number;
157
183
  }
158
184
  /** Result type for aggregate queries */
159
185
  export interface AggregateResult<T> {
@@ -211,6 +237,13 @@ type MiddlewareFn = (params: {
211
237
  action: string;
212
238
  args: Record<string, unknown>;
213
239
  }) => Promise<unknown>) => Promise<unknown>;
240
+ /** Options passed from TurbineClient to QueryInterface */
241
+ export interface QueryInterfaceOptions {
242
+ /** Default LIMIT applied to findMany() when no limit is specified */
243
+ defaultLimit?: number;
244
+ /** Log a warning when findMany() is called without a limit */
245
+ warnOnUnlimited?: boolean;
246
+ }
214
247
  export declare class QueryInterface<T extends object> {
215
248
  private readonly pool;
216
249
  private readonly table;
@@ -219,11 +252,25 @@ export declare class QueryInterface<T extends object> {
219
252
  /** SQL template cache: cacheKey → sql string (params are always positional $1,$2,...) */
220
253
  private readonly sqlCache;
221
254
  private readonly middlewares;
222
- private readonly hasNoDateColumns;
223
- constructor(pool: pg.Pool, table: string, schema: SchemaMetadata, middlewares?: MiddlewareFn[]);
255
+ private readonly defaultLimit?;
256
+ private readonly warnOnUnlimited;
257
+ /** Pre-computed column type lookups (avoids linear scans per query) */
258
+ private readonly columnPgTypeMap;
259
+ private readonly columnArrayTypeMap;
260
+ constructor(pool: pg.Pool, table: string, schema: SchemaMetadata, middlewares?: MiddlewareFn[], options?: QueryInterfaceOptions);
261
+ /**
262
+ * Execute a pool.query with an optional timeout.
263
+ * If timeout is set, races the query against a timer and rejects on expiry.
264
+ */
265
+ private queryWithTimeout;
224
266
  /**
225
267
  * Execute a query through the middleware chain.
226
268
  * If no middlewares are registered, executes directly.
269
+ *
270
+ * Middleware can inspect and log query parameters, modify results after execution,
271
+ * and measure timing. Note: query SQL is generated before middleware runs, so
272
+ * modifying params.args in middleware will NOT affect the executed SQL.
273
+ * To intercept queries before SQL generation, use the raw() method instead.
227
274
  */
228
275
  private executeWithMiddleware;
229
276
  /**
@@ -305,13 +352,6 @@ export declare class QueryInterface<T extends object> {
305
352
  private buildOrderBy;
306
353
  /** Parse a flat row: convert snake_case to camelCase + Date coercion */
307
354
  private parseRow;
308
- /**
309
- * Fast path: parse a flat row when the table has NO date columns.
310
- * Only renames snake_case -> camelCase via the pre-computed reverseMap.
311
- * Skips all date coercion checks — avoids Set.has() per field per row.
312
- * Used by findUnique/findMany fast paths when hasNoDateColumns is true.
313
- */
314
- private parseRowFast;
315
355
  /** Parse a row that may contain JSON nested relation columns */
316
356
  private parseNestedRow;
317
357
  /**
@@ -338,6 +378,7 @@ export declare class QueryInterface<T extends object> {
338
378
  /**
339
379
  * Get the Postgres type for a column (e.g. 'jsonb', 'text', '_int4').
340
380
  * Used to detect JSONB/array columns for specialized operators.
381
+ * Uses pre-computed Map for O(1) lookup instead of linear scan.
341
382
  */
342
383
  private getColumnPgType;
343
384
  /**
@@ -355,7 +396,10 @@ export declare class QueryInterface<T extends object> {
355
396
  * Supports: has, hasEvery, hasSome, isEmpty.
356
397
  */
357
398
  private buildArrayFilterClauses;
358
- /** Get the Postgres array type for a column (used by UNNEST in createMany) */
399
+ /**
400
+ * Get the Postgres array type for a column (used by UNNEST in createMany).
401
+ * Uses pre-computed Map for O(1) lookup instead of linear scan.
402
+ */
359
403
  private getColumnArrayType;
360
404
  }
361
405
  export {};