turbine-orm 0.9.2 → 0.11.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 (45) hide show
  1. package/README.md +34 -16
  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 +5 -4
  14. package/dist/cjs/client.js +1 -0
  15. package/dist/cjs/dialect.js +57 -0
  16. package/dist/cjs/generate.js +8 -1
  17. package/dist/cjs/index.js +12 -3
  18. package/dist/cjs/introspect.js +46 -18
  19. package/dist/cjs/query/builder.js +129 -96
  20. package/dist/cjs/query/index.js +4 -1
  21. package/dist/cjs/query/utils.js +18 -0
  22. package/dist/cjs/schema.js +8 -0
  23. package/dist/cli/config.d.ts +11 -0
  24. package/dist/cli/index.js +2 -1
  25. package/dist/cli/migrate.d.ts +3 -0
  26. package/dist/cli/migrate.js +16 -10
  27. package/dist/cli/studio.d.ts +4 -0
  28. package/dist/cli/studio.js +5 -4
  29. package/dist/client.d.ts +3 -0
  30. package/dist/client.js +1 -0
  31. package/dist/dialect.d.ts +61 -0
  32. package/dist/dialect.js +55 -0
  33. package/dist/generate.js +8 -1
  34. package/dist/index.d.ts +5 -1
  35. package/dist/index.js +3 -1
  36. package/dist/introspect.js +46 -18
  37. package/dist/query/builder.d.ts +9 -1
  38. package/dist/query/builder.js +130 -97
  39. package/dist/query/index.d.ts +3 -1
  40. package/dist/query/index.js +2 -1
  41. package/dist/query/utils.d.ts +8 -0
  42. package/dist/query/utils.js +17 -0
  43. package/dist/schema.d.ts +6 -4
  44. package/dist/schema.js +7 -0
  45. package/package.json +8 -3
@@ -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) });
@@ -293,7 +294,7 @@ async function apiTableRows(res, ctx, rawTableName, params) {
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;
@@ -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;
@@ -204,6 +204,7 @@ class TurbineClient {
204
204
  warnOnUnlimited: config.warnOnUnlimited,
205
205
  preparedStatements: envDisablePrepared ? false : (config.preparedStatements ?? !config.pool),
206
206
  sqlCache: config.sqlCache ?? true,
207
+ dialect: config.dialect,
207
208
  };
208
209
  // Apply NotFoundError message redaction mode (default: safe — values are
209
210
  // stripped from messages to avoid leaking PII into error logs).
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /**
3
+ * turbine-orm — SQL dialect contract
4
+ *
5
+ * Phase-1 seam for future database packages. The current package remains
6
+ * PostgreSQL-native by default, but query generation now depends on this
7
+ * contract for the SQL primitives that vary across MySQL and SQLite.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.postgresDialect = void 0;
11
+ /** PostgreSQL implementation of the dialect contract. */
12
+ exports.postgresDialect = {
13
+ name: 'postgresql',
14
+ supportsReturning: true,
15
+ supportsILike: true,
16
+ jsonPathSupport: 'native',
17
+ emptyJsonArrayLiteral: "'[]'::json",
18
+ nullJsonLiteral: 'NULL',
19
+ paramPlaceholder(index) {
20
+ return `$${index}`;
21
+ },
22
+ quoteIdentifier(name) {
23
+ return `"${name.replace(/"/g, '""')}"`;
24
+ },
25
+ escapeStringLiteral(value) {
26
+ return value.replace(/'/g, "''");
27
+ },
28
+ buildJsonObject(pairs) {
29
+ const args = pairs.map(([key, expr]) => `'${this.escapeStringLiteral(key)}', ${expr}`);
30
+ return `json_build_object(${args.join(', ')})`;
31
+ },
32
+ buildJsonArrayAgg(jsonObjectExpr, orderBy) {
33
+ const suffix = orderBy ? ` ${orderBy}` : '';
34
+ return `COALESCE(json_agg(${jsonObjectExpr}${suffix}), ${this.emptyJsonArrayLiteral})`;
35
+ },
36
+ buildInsensitiveLike(column, paramRef) {
37
+ return `${column} ILIKE ${paramRef}`;
38
+ },
39
+ buildJsonContains(column, paramRef) {
40
+ return `${column} @> ${paramRef}::jsonb`;
41
+ },
42
+ buildJsonPathExtract(column, pathParamRef) {
43
+ return `${column} #>> ${pathParamRef}::text[]`;
44
+ },
45
+ buildCorrelation(leftRef, leftColumns, rightRef, rightColumns) {
46
+ const leftCols = Array.isArray(leftColumns) ? leftColumns : [leftColumns];
47
+ const rightCols = Array.isArray(rightColumns) ? rightColumns : [rightColumns];
48
+ return leftCols
49
+ .map((col, i) => `${leftRef}.${this.quoteIdentifier(col)} = ${rightRef}.${this.quoteIdentifier(rightCols[i])}`)
50
+ .join(' AND ');
51
+ },
52
+ typeToTypeScript(_dialectType, _nullable) {
53
+ // Existing PostgreSQL type mapping remains in schema.ts/generate.ts for now.
54
+ // This hook is the package boundary MySQL/SQLite implementations will fill.
55
+ return 'unknown';
56
+ },
57
+ };
@@ -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,11 +34,19 @@
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.postgresDialect = 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; } });
41
47
  Object.defineProperty(exports, "TurbineClient", { enumerable: true, get: function () { return client_js_1.TurbineClient; } });
48
+ var dialect_js_1 = require("./dialect.js");
49
+ Object.defineProperty(exports, "postgresDialect", { enumerable: true, get: function () { return dialect_js_1.postgresDialect; } });
42
50
  // Error types
43
51
  var errors_js_1 = require("./errors.js");
44
52
  Object.defineProperty(exports, "CheckConstraintError", { enumerable: true, get: function () { return errors_js_1.CheckConstraintError; } });
@@ -71,12 +79,13 @@ var pipeline_js_1 = require("./pipeline.js");
71
79
  Object.defineProperty(exports, "executePipeline", { enumerable: true, get: function () { return pipeline_js_1.executePipeline; } });
72
80
  Object.defineProperty(exports, "pipelineSupported", { enumerable: true, get: function () { return pipeline_js_1.pipelineSupported; } });
73
81
  // Query builder
74
- var index_js_1 = require("./query/index.js");
75
- Object.defineProperty(exports, "QueryInterface", { enumerable: true, get: function () { return index_js_1.QueryInterface; } });
82
+ var index_js_2 = require("./query/index.js");
83
+ Object.defineProperty(exports, "QueryInterface", { enumerable: true, get: function () { return index_js_2.QueryInterface; } });
76
84
  // Schema utilities
77
85
  var schema_js_1 = require("./schema.js");
78
86
  Object.defineProperty(exports, "camelToSnake", { enumerable: true, get: function () { return schema_js_1.camelToSnake; } });
79
87
  Object.defineProperty(exports, "isDateType", { enumerable: true, get: function () { return schema_js_1.isDateType; } });
88
+ Object.defineProperty(exports, "normalizeKeyColumns", { enumerable: true, get: function () { return schema_js_1.normalizeKeyColumns; } });
80
89
  Object.defineProperty(exports, "pgArrayType", { enumerable: true, get: function () { return schema_js_1.pgArrayType; } });
81
90
  Object.defineProperty(exports, "pgTypeToTs", { enumerable: true, get: function () { return schema_js_1.pgTypeToTs; } });
82
91
  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 -----