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.
- package/README.md +34 -16
- package/dist/adapters/cockroachdb.d.ts +40 -0
- package/dist/adapters/cockroachdb.js +172 -0
- package/dist/adapters/index.d.ts +107 -0
- package/dist/adapters/index.js +83 -0
- package/dist/adapters/yugabytedb.d.ts +52 -0
- package/dist/adapters/yugabytedb.js +156 -0
- package/dist/cjs/adapters/cockroachdb.js +174 -0
- package/dist/cjs/adapters/index.js +87 -0
- package/dist/cjs/adapters/yugabytedb.js +158 -0
- package/dist/cjs/cli/index.js +2 -1
- package/dist/cjs/cli/migrate.js +18 -12
- package/dist/cjs/cli/studio.js +5 -4
- package/dist/cjs/client.js +1 -0
- package/dist/cjs/dialect.js +57 -0
- package/dist/cjs/generate.js +8 -1
- package/dist/cjs/index.js +12 -3
- package/dist/cjs/introspect.js +46 -18
- package/dist/cjs/query/builder.js +129 -96
- package/dist/cjs/query/index.js +4 -1
- package/dist/cjs/query/utils.js +18 -0
- package/dist/cjs/schema.js +8 -0
- package/dist/cli/config.d.ts +11 -0
- package/dist/cli/index.js +2 -1
- package/dist/cli/migrate.d.ts +3 -0
- package/dist/cli/migrate.js +16 -10
- package/dist/cli/studio.d.ts +4 -0
- package/dist/cli/studio.js +5 -4
- package/dist/client.d.ts +3 -0
- package/dist/client.js +1 -0
- package/dist/dialect.d.ts +61 -0
- package/dist/dialect.js +55 -0
- package/dist/generate.js +8 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/introspect.js +46 -18
- package/dist/query/builder.d.ts +9 -1
- package/dist/query/builder.js +130 -97
- package/dist/query/index.d.ts +3 -1
- package/dist/query/index.js +2 -1
- package/dist/query/utils.d.ts +8 -0
- package/dist/query/utils.js +17 -0
- package/dist/schema.d.ts +6 -4
- package/dist/schema.js +7 -0
- package/package.json +8 -3
package/dist/cjs/cli/studio.js
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
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(
|
|
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;
|
package/dist/cjs/client.js
CHANGED
|
@@ -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
|
+
};
|
package/dist/cjs/generate.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
75
|
-
Object.defineProperty(exports, "QueryInterface", { enumerable: true, get: function () { return
|
|
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; } });
|
package/dist/cjs/introspect.js
CHANGED
|
@@ -72,13 +72,16 @@ const SQL_FOREIGN_KEYS = `
|
|
|
72
72
|
const SQL_UNIQUE_CONSTRAINTS = `
|
|
73
73
|
SELECT
|
|
74
74
|
tc.table_name,
|
|
75
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
?
|
|
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
|
|
231
|
-
referenceKey
|
|
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
|
-
?
|
|
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
|
|
246
|
-
referenceKey
|
|
273
|
+
foreignKey,
|
|
274
|
+
referenceKey,
|
|
247
275
|
};
|
|
248
276
|
}
|
|
249
277
|
// ----- Assemble TableMetadata for each table -----
|