turbine-orm 0.9.1 → 0.10.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 (49) hide show
  1. package/README.md +35 -12
  2. package/dist/adapters/cockroachdb.d.ts +40 -0
  3. package/dist/adapters/cockroachdb.js +172 -0
  4. package/dist/adapters/index.d.ts +107 -0
  5. package/dist/adapters/index.js +83 -0
  6. package/dist/adapters/yugabytedb.d.ts +52 -0
  7. package/dist/adapters/yugabytedb.js +156 -0
  8. package/dist/cjs/adapters/cockroachdb.js +174 -0
  9. package/dist/cjs/adapters/index.js +87 -0
  10. package/dist/cjs/adapters/yugabytedb.js +158 -0
  11. package/dist/cjs/cli/index.js +2 -1
  12. package/dist/cjs/cli/migrate.js +18 -12
  13. package/dist/cjs/cli/studio.js +12 -11
  14. package/dist/cjs/client.js +3 -3
  15. package/dist/cjs/generate.js +8 -1
  16. package/dist/cjs/index.js +10 -3
  17. package/dist/cjs/introspect.js +46 -18
  18. package/dist/cjs/query/builder.js +2658 -0
  19. package/dist/cjs/query/index.js +21 -0
  20. package/dist/cjs/query/types.js +7 -0
  21. package/dist/cjs/query/utils.js +140 -0
  22. package/dist/cjs/schema-sql.js +26 -26
  23. package/dist/cjs/schema.js +8 -0
  24. package/dist/cli/config.d.ts +11 -0
  25. package/dist/cli/index.js +2 -1
  26. package/dist/cli/migrate.d.ts +3 -0
  27. package/dist/cli/migrate.js +17 -11
  28. package/dist/cli/studio.d.ts +4 -0
  29. package/dist/cli/studio.js +6 -5
  30. package/dist/client.d.ts +1 -1
  31. package/dist/client.js +1 -1
  32. package/dist/generate.js +8 -1
  33. package/dist/index.d.ts +4 -2
  34. package/dist/index.js +3 -2
  35. package/dist/introspect.js +46 -18
  36. package/dist/pipeline-submittable.d.ts +1 -1
  37. package/dist/pipeline.d.ts +1 -1
  38. package/dist/query/builder.d.ts +498 -0
  39. package/dist/query/builder.js +2655 -0
  40. package/dist/query/index.d.ts +13 -0
  41. package/dist/query/index.js +10 -0
  42. package/dist/query/types.d.ts +365 -0
  43. package/dist/query/types.js +7 -0
  44. package/dist/query/utils.d.ts +68 -0
  45. package/dist/query/utils.js +131 -0
  46. package/dist/schema-sql.js +1 -1
  47. package/dist/schema.d.ts +6 -4
  48. package/dist/schema.js +7 -0
  49. package/package.json +14 -2
@@ -38,7 +38,7 @@ const node_os_1 = require("node:os");
38
38
  const node_path_1 = require("node:path");
39
39
  const pg_1 = __importDefault(require("pg"));
40
40
  const introspect_js_1 = require("../introspect.js");
41
- const query_js_1 = require("../query.js");
41
+ const index_js_1 = require("../query/index.js");
42
42
  const studio_ui_generated_js_1 = require("./studio-ui.generated.js");
43
43
  // ---------------------------------------------------------------------------
44
44
  // Main entry point
@@ -74,7 +74,8 @@ async function startStudio(options) {
74
74
  });
75
75
  const authToken = (0, node_crypto_1.randomBytes)(24).toString('hex');
76
76
  const stateDir = (0, node_path_1.resolve)(options.stateDir ?? '.turbine');
77
- const ctx = { pool, metadata, options, authToken, stateDir };
77
+ const statementTimeoutSQL = options.adapter?.statementTimeout?.(30) ?? `SET LOCAL statement_timeout = '30s'`;
78
+ const ctx = { pool, metadata, options, authToken, stateDir, statementTimeoutSQL };
78
79
  const server = (0, node_http_1.createServer)((req, res) => {
79
80
  handleRequest(req, res, ctx).catch((err) => {
80
81
  sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
@@ -259,10 +260,10 @@ async function apiTableRows(res, ctx, rawTableName, params) {
259
260
  if (orderByRaw) {
260
261
  const col = resolveColumnName(table, orderByRaw);
261
262
  if (col)
262
- orderByClause = `ORDER BY ${(0, query_js_1.quoteIdent)(col)} ${dir}`;
263
+ orderByClause = `ORDER BY ${(0, index_js_1.quoteIdent)(col)} ${dir}`;
263
264
  }
264
265
  if (!orderByClause && table.primaryKey.length > 0 && table.primaryKey[0]) {
265
- orderByClause = `ORDER BY ${(0, query_js_1.quoteIdent)(table.primaryKey[0])} ${dir}`;
266
+ orderByClause = `ORDER BY ${(0, index_js_1.quoteIdent)(table.primaryKey[0])} ${dir}`;
266
267
  }
267
268
  // Full-text-ish search: ILIKE across text/varchar columns. The value is
268
269
  // parameterized so injection is impossible. Each query gets its own
@@ -276,7 +277,7 @@ async function apiTableRows(res, ctx, rawTableName, params) {
276
277
  let mainWhere = '';
277
278
  if (hasSearch && pattern !== null) {
278
279
  mainValues.push(pattern);
279
- const conds = textColumns.map((c) => `${(0, query_js_1.quoteIdent)(c)} ILIKE $3`);
280
+ const conds = textColumns.map((c) => `${(0, index_js_1.quoteIdent)(c)} ILIKE $3`);
280
281
  mainWhere = `WHERE (${conds.join(' OR ')})`;
281
282
  }
282
283
  // Count query: $1 = pattern (if search)
@@ -284,16 +285,16 @@ async function apiTableRows(res, ctx, rawTableName, params) {
284
285
  let countWhere = '';
285
286
  if (hasSearch && pattern !== null) {
286
287
  countValues.push(pattern);
287
- const conds = textColumns.map((c) => `${(0, query_js_1.quoteIdent)(c)} ILIKE $1`);
288
+ const conds = textColumns.map((c) => `${(0, index_js_1.quoteIdent)(c)} ILIKE $1`);
288
289
  countWhere = `WHERE (${conds.join(' OR ')})`;
289
290
  }
290
- const qualifiedTable = `${(0, query_js_1.quoteIdent)(ctx.options.schema)}.${(0, query_js_1.quoteIdent)(table.name)}`;
291
+ const qualifiedTable = `${(0, index_js_1.quoteIdent)(ctx.options.schema)}.${(0, index_js_1.quoteIdent)(table.name)}`;
291
292
  const sql = `SELECT * FROM ${qualifiedTable} ${mainWhere} ${orderByClause} LIMIT $1 OFFSET $2`;
292
293
  const countSql = `SELECT COUNT(*)::text AS count FROM ${qualifiedTable} ${countWhere}`;
293
294
  const client = await ctx.pool.connect();
294
295
  try {
295
296
  await client.query('BEGIN READ ONLY');
296
- await client.query(`SET LOCAL statement_timeout = '30s'`);
297
+ await client.query(ctx.statementTimeoutSQL);
297
298
  const result = await client.query(sql, mainValues);
298
299
  const countResult = await client.query(countSql, countValues);
299
300
  await client.query('COMMIT');
@@ -359,7 +360,7 @@ async function apiQuery(req, res, ctx) {
359
360
  const client = await ctx.pool.connect();
360
361
  try {
361
362
  await client.query('BEGIN READ ONLY');
362
- await client.query(`SET LOCAL statement_timeout = '30s'`);
363
+ await client.query(ctx.statementTimeoutSQL);
363
364
  const started = Date.now();
364
365
  const result = await client.query(rawSql);
365
366
  const elapsedMs = Date.now() - started;
@@ -397,7 +398,7 @@ async function apiBuilder(req, res, ctx) {
397
398
  }
398
399
  let deferred;
399
400
  try {
400
- const qi = new query_js_1.QueryInterface(ctx.pool, tableName, ctx.metadata, [], {
401
+ const qi = new index_js_1.QueryInterface(ctx.pool, tableName, ctx.metadata, [], {
401
402
  warnOnUnlimited: false,
402
403
  sqlCache: false,
403
404
  preparedStatements: false,
@@ -411,7 +412,7 @@ async function apiBuilder(req, res, ctx) {
411
412
  const client = await ctx.pool.connect();
412
413
  try {
413
414
  await client.query('BEGIN READ ONLY');
414
- await client.query(`SET LOCAL statement_timeout = '30s'`);
415
+ await client.query(ctx.statementTimeoutSQL);
415
416
  const started = Date.now();
416
417
  const result = await client.query(deferred.sql, deferred.params);
417
418
  const elapsedMs = Date.now() - started;
@@ -30,7 +30,7 @@ exports.TurbineClient = exports.TransactionClient = void 0;
30
30
  const pg_1 = __importDefault(require("pg"));
31
31
  const errors_js_1 = require("./errors.js");
32
32
  const pipeline_js_1 = require("./pipeline.js");
33
- const query_js_1 = require("./query.js");
33
+ const index_js_1 = require("./query/index.js");
34
34
  /** Maps isolation level names to SQL */
35
35
  const ISOLATION_LEVELS = {
36
36
  ReadUncommitted: 'READ UNCOMMITTED',
@@ -79,7 +79,7 @@ class TransactionClient {
79
79
  // Create a QueryInterface that uses the transaction client as its "pool"
80
80
  // We use a proxy pool that routes queries through the transaction client
81
81
  const txPool = this.createTxPool();
82
- qi = new query_js_1.QueryInterface(txPool, name, this.schema, this.middlewares, this.queryOptions);
82
+ qi = new index_js_1.QueryInterface(txPool, name, this.schema, this.middlewares, this.queryOptions);
83
83
  this.tableCache.set(name, qi);
84
84
  }
85
85
  return qi;
@@ -306,7 +306,7 @@ class TurbineClient {
306
306
  table(name) {
307
307
  let qi = this.tableCache.get(name);
308
308
  if (!qi) {
309
- qi = new query_js_1.QueryInterface(this.pool, name, this.schema, this.middlewares, this.queryOptions);
309
+ qi = new index_js_1.QueryInterface(this.pool, name, this.schema, this.middlewares, this.queryOptions);
310
310
  this.tableCache.set(name, qi);
311
311
  }
312
312
  return qi;
@@ -233,7 +233,14 @@ function generateMetadata(schema) {
233
233
  // relations
234
234
  lines.push(' relations: {');
235
235
  for (const [relName, rel] of Object.entries(table.relations)) {
236
- lines.push(` ${relName}: { type: '${escSQ(rel.type)}', name: '${escSQ(rel.name)}', from: '${escSQ(rel.from)}', to: '${escSQ(rel.to)}', foreignKey: '${escSQ(rel.foreignKey)}', referenceKey: '${escSQ(rel.referenceKey)}' },`);
236
+ // Emit foreignKey/referenceKey as string for single-column, array for composite
237
+ const fkLiteral = Array.isArray(rel.foreignKey)
238
+ ? `[${rel.foreignKey.map((c) => `'${escSQ(c)}'`).join(', ')}]`
239
+ : `'${escSQ(rel.foreignKey)}'`;
240
+ const refLiteral = Array.isArray(rel.referenceKey)
241
+ ? `[${rel.referenceKey.map((c) => `'${escSQ(c)}'`).join(', ')}]`
242
+ : `'${escSQ(rel.referenceKey)}'`;
243
+ lines.push(` ${relName}: { type: '${escSQ(rel.type)}', name: '${escSQ(rel.name)}', from: '${escSQ(rel.from)}', to: '${escSQ(rel.to)}', foreignKey: ${fkLiteral}, referenceKey: ${refLiteral} },`);
237
244
  }
238
245
  lines.push(' },');
239
246
  // indexes
package/dist/cjs/index.js CHANGED
@@ -34,7 +34,13 @@
34
34
  * ```
35
35
  */
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
- exports.turbineHttp = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.table = exports.defineSchema = exports.column = exports.ColumnBuilder = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.isDateType = exports.camelToSnake = exports.QueryInterface = exports.pipelineSupported = exports.executePipeline = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.PipelineError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.TurbineClient = exports.TransactionClient = void 0;
37
+ exports.turbineHttp = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.table = exports.defineSchema = exports.column = exports.ColumnBuilder = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = exports.isDateType = exports.camelToSnake = exports.QueryInterface = exports.pipelineSupported = exports.executePipeline = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.PipelineError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.TurbineClient = exports.TransactionClient = exports.yugabytedb = exports.timescale = exports.postgresql = exports.cockroachdb = exports.alloydb = void 0;
38
+ var index_js_1 = require("./adapters/index.js");
39
+ Object.defineProperty(exports, "alloydb", { enumerable: true, get: function () { return index_js_1.alloydb; } });
40
+ Object.defineProperty(exports, "cockroachdb", { enumerable: true, get: function () { return index_js_1.cockroachdb; } });
41
+ Object.defineProperty(exports, "postgresql", { enumerable: true, get: function () { return index_js_1.postgresql; } });
42
+ Object.defineProperty(exports, "timescale", { enumerable: true, get: function () { return index_js_1.timescale; } });
43
+ Object.defineProperty(exports, "yugabytedb", { enumerable: true, get: function () { return index_js_1.yugabytedb; } });
38
44
  // Client
39
45
  var client_js_1 = require("./client.js");
40
46
  Object.defineProperty(exports, "TransactionClient", { enumerable: true, get: function () { return client_js_1.TransactionClient; } });
@@ -71,12 +77,13 @@ var pipeline_js_1 = require("./pipeline.js");
71
77
  Object.defineProperty(exports, "executePipeline", { enumerable: true, get: function () { return pipeline_js_1.executePipeline; } });
72
78
  Object.defineProperty(exports, "pipelineSupported", { enumerable: true, get: function () { return pipeline_js_1.pipelineSupported; } });
73
79
  // Query builder
74
- var query_js_1 = require("./query.js");
75
- Object.defineProperty(exports, "QueryInterface", { enumerable: true, get: function () { return query_js_1.QueryInterface; } });
80
+ var index_js_2 = require("./query/index.js");
81
+ Object.defineProperty(exports, "QueryInterface", { enumerable: true, get: function () { return index_js_2.QueryInterface; } });
76
82
  // Schema utilities
77
83
  var schema_js_1 = require("./schema.js");
78
84
  Object.defineProperty(exports, "camelToSnake", { enumerable: true, get: function () { return schema_js_1.camelToSnake; } });
79
85
  Object.defineProperty(exports, "isDateType", { enumerable: true, get: function () { return schema_js_1.isDateType; } });
86
+ Object.defineProperty(exports, "normalizeKeyColumns", { enumerable: true, get: function () { return schema_js_1.normalizeKeyColumns; } });
80
87
  Object.defineProperty(exports, "pgArrayType", { enumerable: true, get: function () { return schema_js_1.pgArrayType; } });
81
88
  Object.defineProperty(exports, "pgTypeToTs", { enumerable: true, get: function () { return schema_js_1.pgTypeToTs; } });
82
89
  Object.defineProperty(exports, "singularize", { enumerable: true, get: function () { return schema_js_1.singularize; } });
@@ -72,13 +72,16 @@ const SQL_FOREIGN_KEYS = `
72
72
  const SQL_UNIQUE_CONSTRAINTS = `
73
73
  SELECT
74
74
  tc.table_name,
75
- kcu.column_name
75
+ tc.constraint_name,
76
+ kcu.column_name,
77
+ kcu.ordinal_position
76
78
  FROM information_schema.table_constraints tc
77
79
  JOIN information_schema.key_column_usage kcu
78
80
  ON tc.constraint_name = kcu.constraint_name
79
81
  AND tc.table_schema = kcu.table_schema
80
82
  WHERE tc.constraint_type = 'UNIQUE'
81
83
  AND tc.table_schema = $1
84
+ ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position
82
85
  `;
83
86
  const SQL_INDEXES = `
84
87
  SELECT tablename, indexname, indexdef
@@ -159,14 +162,22 @@ async function introspect(options) {
159
162
  pkByTable.get(row.table_name).push(row.column_name);
160
163
  }
161
164
  // ----- Group unique constraints by table -----
165
+ // Group rows by (table_name, constraint_name) to correctly handle multi-column unique constraints
162
166
  const uniqueByTable = new Map();
167
+ const uniqueConstraintGroups = new Map();
163
168
  for (const row of uniqueResult.rows) {
164
169
  if (!tableSet.has(row.table_name))
165
170
  continue;
166
- if (!uniqueByTable.has(row.table_name))
167
- uniqueByTable.set(row.table_name, []);
168
- // Each unique constraint may be multi-column; for simplicity, treat as single-col here
169
- uniqueByTable.get(row.table_name).push([row.column_name]);
171
+ const key = `${row.table_name}::${row.constraint_name}`;
172
+ if (!uniqueConstraintGroups.has(key)) {
173
+ uniqueConstraintGroups.set(key, { table: row.table_name, columns: [] });
174
+ }
175
+ uniqueConstraintGroups.get(key).columns.push(row.column_name);
176
+ }
177
+ for (const { table, columns } of uniqueConstraintGroups.values()) {
178
+ if (!uniqueByTable.has(table))
179
+ uniqueByTable.set(table, []);
180
+ uniqueByTable.get(table).push(columns);
170
181
  }
171
182
  // ----- Group indexes by table -----
172
183
  const indexesByTable = new Map();
@@ -193,17 +204,25 @@ async function introspect(options) {
193
204
  enums[row.typname] = [];
194
205
  enums[row.typname].push(row.enumlabel);
195
206
  }
196
- const foreignKeys = [];
207
+ const fkGroups = new Map();
197
208
  for (const row of fkResult.rows) {
198
209
  if (!tableSet.has(row.source_table) || !tableSet.has(row.target_table))
199
210
  continue;
200
- foreignKeys.push({
201
- sourceTable: row.source_table,
202
- sourceColumn: row.source_column,
203
- targetTable: row.target_table,
204
- targetColumn: row.target_column,
205
- });
211
+ const key = row.constraint_name;
212
+ if (!fkGroups.has(key)) {
213
+ fkGroups.set(key, {
214
+ sourceTable: row.source_table,
215
+ sourceColumns: [],
216
+ targetTable: row.target_table,
217
+ targetColumns: [],
218
+ constraintName: key,
219
+ });
220
+ }
221
+ const entry = fkGroups.get(key);
222
+ entry.sourceColumns.push(row.source_column);
223
+ entry.targetColumns.push(row.target_column);
206
224
  }
225
+ const foreignKeys = Array.from(fkGroups.values());
207
226
  // ----- Build relations from foreign keys -----
208
227
  // Count FKs per (source, target) pair for disambiguation
209
228
  const fkCounts = new Map();
@@ -215,10 +234,17 @@ async function introspect(options) {
215
234
  for (const fk of foreignKeys) {
216
235
  const pairKey = `${fk.sourceTable}→${fk.targetTable}`;
217
236
  const needsDisambiguation = (fkCounts.get(pairKey) ?? 0) > 1;
237
+ // For single-column FKs, keep string form for backwards compatibility.
238
+ // For multi-column (composite) FKs, use array form.
239
+ const foreignKey = fk.sourceColumns.length === 1 ? fk.sourceColumns[0] : fk.sourceColumns;
240
+ const referenceKey = fk.targetColumns.length === 1 ? fk.targetColumns[0] : fk.targetColumns;
218
241
  // --- belongsTo on the source (child) table ---
219
242
  // e.g. posts.user_id → users.id creates posts.user (belongsTo)
243
+ // For composite FKs with disambiguation, use the constraint name
220
244
  const belongsToName = needsDisambiguation
221
- ? (0, schema_js_1.snakeToCamel)(fk.sourceColumn.replace(/_id$/, ''))
245
+ ? fk.sourceColumns.length === 1
246
+ ? (0, schema_js_1.snakeToCamel)(fk.sourceColumns[0].replace(/_id$/, ''))
247
+ : (0, schema_js_1.snakeToCamel)(fk.constraintName.replace(/^fk_/, '').replace(/_fkey$/, ''))
222
248
  : (0, schema_js_1.singularize)((0, schema_js_1.snakeToCamel)(fk.targetTable));
223
249
  if (!relationsByTable.has(fk.sourceTable))
224
250
  relationsByTable.set(fk.sourceTable, {});
@@ -227,13 +253,15 @@ async function introspect(options) {
227
253
  name: belongsToName,
228
254
  from: fk.sourceTable,
229
255
  to: fk.targetTable,
230
- foreignKey: fk.sourceColumn,
231
- referenceKey: fk.targetColumn,
256
+ foreignKey,
257
+ referenceKey,
232
258
  };
233
259
  // --- hasMany on the target (parent) table ---
234
260
  // e.g. posts.user_id → users.id creates users.posts (hasMany)
235
261
  const hasManyName = needsDisambiguation
236
- ? (0, schema_js_1.snakeToCamel)(`${fk.sourceTable}_by_${fk.sourceColumn.replace(/_id$/, '')}`)
262
+ ? fk.sourceColumns.length === 1
263
+ ? (0, schema_js_1.snakeToCamel)(`${fk.sourceTable}_by_${fk.sourceColumns[0].replace(/_id$/, '')}`)
264
+ : (0, schema_js_1.snakeToCamel)(`${fk.sourceTable}_by_${fk.constraintName.replace(/^fk_/, '').replace(/_fkey$/, '')}`)
237
265
  : (0, schema_js_1.snakeToCamel)(fk.sourceTable);
238
266
  if (!relationsByTable.has(fk.targetTable))
239
267
  relationsByTable.set(fk.targetTable, {});
@@ -242,8 +270,8 @@ async function introspect(options) {
242
270
  name: hasManyName,
243
271
  from: fk.targetTable,
244
272
  to: fk.sourceTable,
245
- foreignKey: fk.sourceColumn,
246
- referenceKey: fk.targetColumn,
273
+ foreignKey,
274
+ referenceKey,
247
275
  };
248
276
  }
249
277
  // ----- Assemble TableMetadata for each table -----