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/query/index.js
CHANGED
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* former monolithic `import { … } from './query.js'`.
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.QueryInterface = exports.sqlToPreparedName = exports.quoteIdent = exports.OPERATOR_KEYS = exports.LRUCache = exports.fnv1a64Hex = exports.escSingleQuote = exports.escapeLike = void 0;
|
|
10
|
+
exports.QueryInterface = exports.sqlToPreparedName = exports.quoteIdent = exports.OPERATOR_KEYS = exports.LRUCache = exports.fnv1a64Hex = exports.escSingleQuote = exports.escapeLike = exports.buildCorrelation = exports.postgresDialect = void 0;
|
|
11
|
+
var dialect_js_1 = require("../dialect.js");
|
|
12
|
+
Object.defineProperty(exports, "postgresDialect", { enumerable: true, get: function () { return dialect_js_1.postgresDialect; } });
|
|
11
13
|
var utils_js_1 = require("./utils.js");
|
|
14
|
+
Object.defineProperty(exports, "buildCorrelation", { enumerable: true, get: function () { return utils_js_1.buildCorrelation; } });
|
|
12
15
|
Object.defineProperty(exports, "escapeLike", { enumerable: true, get: function () { return utils_js_1.escapeLike; } });
|
|
13
16
|
Object.defineProperty(exports, "escSingleQuote", { enumerable: true, get: function () { return utils_js_1.escSingleQuote; } });
|
|
14
17
|
Object.defineProperty(exports, "fnv1a64Hex", { enumerable: true, get: function () { return utils_js_1.fnv1a64Hex; } });
|
package/dist/cjs/query/utils.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.escSingleQuote = escSingleQuote;
|
|
|
11
11
|
exports.escapeLike = escapeLike;
|
|
12
12
|
exports.fnv1a64Hex = fnv1a64Hex;
|
|
13
13
|
exports.sqlToPreparedName = sqlToPreparedName;
|
|
14
|
+
exports.buildCorrelation = buildCorrelation;
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
// Identifier quoting — prevents SQL injection via table/column names
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
@@ -120,3 +121,20 @@ exports.OPERATOR_KEYS = new Set([
|
|
|
120
121
|
'endsWith',
|
|
121
122
|
'mode',
|
|
122
123
|
]);
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Composite key correlation helper
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
/**
|
|
128
|
+
* Build a correlation clause joining columns between two table references.
|
|
129
|
+
* Handles both single-column (string) and multi-column (string[]) foreign keys.
|
|
130
|
+
*
|
|
131
|
+
* For single-column: `"alias"."col" = "parent"."col"`
|
|
132
|
+
* For multi-column: `"alias"."col_a" = "parent"."ref_a" AND "alias"."col_b" = "parent"."ref_b"`
|
|
133
|
+
*/
|
|
134
|
+
function buildCorrelation(leftRef, leftColumns, rightRef, rightColumns) {
|
|
135
|
+
const leftCols = Array.isArray(leftColumns) ? leftColumns : [leftColumns];
|
|
136
|
+
const rightCols = Array.isArray(rightColumns) ? rightColumns : [rightColumns];
|
|
137
|
+
return leftCols
|
|
138
|
+
.map((col, i) => `${leftRef}.${quoteIdent(col)} = ${rightRef}.${quoteIdent(rightCols[i])}`)
|
|
139
|
+
.join(' AND ');
|
|
140
|
+
}
|
package/dist/cjs/schema.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* They're used by the query builder, code generator, and CLI.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.normalizeKeyColumns = normalizeKeyColumns;
|
|
9
10
|
exports.pgTypeToTs = pgTypeToTs;
|
|
10
11
|
exports.isDateType = isDateType;
|
|
11
12
|
exports.pgArrayType = pgArrayType;
|
|
@@ -14,6 +15,13 @@ exports.camelToSnake = camelToSnake;
|
|
|
14
15
|
exports.snakeToPascal = snakeToPascal;
|
|
15
16
|
exports.singularize = singularize;
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
18
|
+
// Helpers for composite key handling
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
/** Normalize foreignKey/referenceKey to always be an array for uniform processing */
|
|
21
|
+
function normalizeKeyColumns(key) {
|
|
22
|
+
return Array.isArray(key) ? key : [key];
|
|
23
|
+
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
17
25
|
// Type mapping: Postgres → TypeScript
|
|
18
26
|
// ---------------------------------------------------------------------------
|
|
19
27
|
const PG_TO_TS = {
|
package/dist/cli/config.d.ts
CHANGED
|
@@ -21,6 +21,17 @@ export interface TurbineCliConfig {
|
|
|
21
21
|
seedFile?: string;
|
|
22
22
|
/** Schema builder file path (for push command) */
|
|
23
23
|
schemaFile?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Database adapter for PostgreSQL-compatible databases that need
|
|
26
|
+
* dialect-specific behavior (e.g. CockroachDB, YugabyteDB).
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* import { cockroachdb } from 'turbine-orm/adapters';
|
|
31
|
+
* export default { url: process.env.DATABASE_URL, adapter: cockroachdb };
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
adapter?: import('../adapters/index.js').DatabaseAdapter;
|
|
24
35
|
}
|
|
25
36
|
/**
|
|
26
37
|
* Attempt to load a turbine config file from the current directory.
|
package/dist/cli/index.js
CHANGED
|
@@ -888,7 +888,8 @@ async function cmdStatus(_args, config) {
|
|
|
888
888
|
const isLast = i === rels.length - 1;
|
|
889
889
|
const prefix = isLast ? symbols.teeEnd : symbols.tee;
|
|
890
890
|
const relColor = rel.type === 'hasMany' ? blue : yellow;
|
|
891
|
-
|
|
891
|
+
const fkDisplay = Array.isArray(rel.foreignKey) ? rel.foreignKey.join(', ') : rel.foreignKey;
|
|
892
|
+
console.log(` ${dim(prefix)} ${relColor(relName)} ${dim(symbols.arrow)} ${rel.to} ${dim(`(${rel.type}, FK: ${fkDisplay})`)}`);
|
|
892
893
|
}
|
|
893
894
|
}
|
|
894
895
|
newline();
|
package/dist/cli/migrate.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* -- DOWN
|
|
12
12
|
* DROP TABLE users;
|
|
13
13
|
*/
|
|
14
|
+
import type { DatabaseAdapter } from '../adapters/index.js';
|
|
14
15
|
export interface MigrationFile {
|
|
15
16
|
/** Full filename (e.g. "20260325120000_create_users.sql") */
|
|
16
17
|
filename: string;
|
|
@@ -113,6 +114,7 @@ export declare function migrateUp(connectionString: string, migrationsDir: strin
|
|
|
113
114
|
step?: number;
|
|
114
115
|
allowDrift?: boolean /** @deprecated use allowDrift */;
|
|
115
116
|
force?: boolean;
|
|
117
|
+
adapter?: DatabaseAdapter;
|
|
116
118
|
}): Promise<{
|
|
117
119
|
applied: MigrationFile[];
|
|
118
120
|
errors: Array<{
|
|
@@ -130,6 +132,7 @@ export declare function migrateUp(connectionString: string, migrationsDir: strin
|
|
|
130
132
|
*/
|
|
131
133
|
export declare function migrateDown(connectionString: string, migrationsDir: string, options?: {
|
|
132
134
|
step?: number;
|
|
135
|
+
adapter?: DatabaseAdapter;
|
|
133
136
|
}): Promise<{
|
|
134
137
|
rolledBack: MigrationFile[];
|
|
135
138
|
errors: Array<{
|
package/dist/cli/migrate.js
CHANGED
|
@@ -15,6 +15,7 @@ import { createHash } from 'node:crypto';
|
|
|
15
15
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import pg from 'pg';
|
|
18
|
+
import { postgresql } from '../adapters/index.js';
|
|
18
19
|
import { MigrationError } from '../errors.js';
|
|
19
20
|
import { quoteIdent } from '../query/index.js';
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
@@ -234,12 +235,14 @@ async function getCurrentDatabaseName(client) {
|
|
|
234
235
|
const result = await client.query(`SELECT current_database()`);
|
|
235
236
|
return result.rows[0]?.current_database ?? '';
|
|
236
237
|
}
|
|
237
|
-
async function acquireLock(client, lockId) {
|
|
238
|
-
const
|
|
239
|
-
|
|
238
|
+
async function acquireLock(client, lockId, adapter) {
|
|
239
|
+
const a = adapter ?? postgresql;
|
|
240
|
+
// pg.Client satisfies PgCompatPoolClient (query + release)
|
|
241
|
+
return a.acquireLock(client, lockId);
|
|
240
242
|
}
|
|
241
|
-
async function releaseLock(client, lockId) {
|
|
242
|
-
|
|
243
|
+
async function releaseLock(client, lockId, adapter) {
|
|
244
|
+
const a = adapter ?? postgresql;
|
|
245
|
+
await a.releaseLock(client, lockId);
|
|
243
246
|
}
|
|
244
247
|
/**
|
|
245
248
|
* Validate that applied migration files have not been modified or deleted since they were run.
|
|
@@ -306,8 +309,10 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
306
309
|
// sibling databases on the same Postgres cluster do not contend.
|
|
307
310
|
const dbName = await getCurrentDatabaseName(client);
|
|
308
311
|
const lockId = deriveLockId(dbName);
|
|
309
|
-
// Acquire
|
|
310
|
-
|
|
312
|
+
// Acquire lock to prevent concurrent migrations.
|
|
313
|
+
// The adapter determines the strategy (advisory lock vs table lock).
|
|
314
|
+
const adapter = options?.adapter;
|
|
315
|
+
const gotLock = await acquireLock(client, lockId, adapter);
|
|
311
316
|
if (!gotLock) {
|
|
312
317
|
throw new MigrationError('[turbine] Could not acquire migration lock — another migration is already running');
|
|
313
318
|
}
|
|
@@ -379,7 +384,7 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
379
384
|
return { applied: results, errors };
|
|
380
385
|
}
|
|
381
386
|
finally {
|
|
382
|
-
await releaseLock(client, lockId);
|
|
387
|
+
await releaseLock(client, lockId, adapter);
|
|
383
388
|
}
|
|
384
389
|
}
|
|
385
390
|
finally {
|
|
@@ -402,7 +407,8 @@ export async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
402
407
|
// sibling databases on the same cluster do not contend.
|
|
403
408
|
const dbName = await getCurrentDatabaseName(client);
|
|
404
409
|
const lockId = deriveLockId(dbName);
|
|
405
|
-
const
|
|
410
|
+
const adapter = options?.adapter;
|
|
411
|
+
const gotLock = await acquireLock(client, lockId, adapter);
|
|
406
412
|
if (!gotLock) {
|
|
407
413
|
throw new MigrationError('[turbine] Could not acquire migration lock — another migration is already running');
|
|
408
414
|
}
|
|
@@ -449,7 +455,7 @@ export async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
449
455
|
return { rolledBack: results, errors };
|
|
450
456
|
}
|
|
451
457
|
finally {
|
|
452
|
-
await releaseLock(client, lockId);
|
|
458
|
+
await releaseLock(client, lockId, adapter);
|
|
453
459
|
}
|
|
454
460
|
}
|
|
455
461
|
finally {
|
package/dist/cli/studio.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export interface StudioOptions {
|
|
|
28
28
|
exclude?: string[];
|
|
29
29
|
/** Directory where studio-queries.json is persisted. Defaults to `.turbine/` in cwd. */
|
|
30
30
|
stateDir?: string;
|
|
31
|
+
/** Database adapter for dialect-specific behavior (e.g. statement timeout syntax). */
|
|
32
|
+
adapter?: import('../adapters/index.js').DatabaseAdapter;
|
|
31
33
|
}
|
|
32
34
|
export interface StudioHandle {
|
|
33
35
|
/** Shut down the server + pool cleanly. */
|
|
@@ -43,6 +45,8 @@ export interface StudioContext {
|
|
|
43
45
|
options: StudioOptions;
|
|
44
46
|
authToken: string;
|
|
45
47
|
stateDir: string;
|
|
48
|
+
/** Resolved statement timeout SQL string (adapter-aware). */
|
|
49
|
+
statementTimeoutSQL: string;
|
|
46
50
|
}
|
|
47
51
|
/**
|
|
48
52
|
* Start the Studio server. Returns a handle with the session token, a pre-built
|
package/dist/cli/studio.js
CHANGED
|
@@ -59,7 +59,8 @@ export async function startStudio(options) {
|
|
|
59
59
|
});
|
|
60
60
|
const authToken = randomBytes(24).toString('hex');
|
|
61
61
|
const stateDir = pathResolve(options.stateDir ?? '.turbine');
|
|
62
|
-
const
|
|
62
|
+
const statementTimeoutSQL = options.adapter?.statementTimeout?.(30) ?? `SET LOCAL statement_timeout = '30s'`;
|
|
63
|
+
const ctx = { pool, metadata, options, authToken, stateDir, statementTimeoutSQL };
|
|
63
64
|
const server = createServer((req, res) => {
|
|
64
65
|
handleRequest(req, res, ctx).catch((err) => {
|
|
65
66
|
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
@@ -278,7 +279,7 @@ export async function apiTableRows(res, ctx, rawTableName, params) {
|
|
|
278
279
|
const client = await ctx.pool.connect();
|
|
279
280
|
try {
|
|
280
281
|
await client.query('BEGIN READ ONLY');
|
|
281
|
-
await client.query(
|
|
282
|
+
await client.query(ctx.statementTimeoutSQL);
|
|
282
283
|
const result = await client.query(sql, mainValues);
|
|
283
284
|
const countResult = await client.query(countSql, countValues);
|
|
284
285
|
await client.query('COMMIT');
|
|
@@ -344,7 +345,7 @@ async function apiQuery(req, res, ctx) {
|
|
|
344
345
|
const client = await ctx.pool.connect();
|
|
345
346
|
try {
|
|
346
347
|
await client.query('BEGIN READ ONLY');
|
|
347
|
-
await client.query(
|
|
348
|
+
await client.query(ctx.statementTimeoutSQL);
|
|
348
349
|
const started = Date.now();
|
|
349
350
|
const result = await client.query(rawSql);
|
|
350
351
|
const elapsedMs = Date.now() - started;
|
|
@@ -396,7 +397,7 @@ export async function apiBuilder(req, res, ctx) {
|
|
|
396
397
|
const client = await ctx.pool.connect();
|
|
397
398
|
try {
|
|
398
399
|
await client.query('BEGIN READ ONLY');
|
|
399
|
-
await client.query(
|
|
400
|
+
await client.query(ctx.statementTimeoutSQL);
|
|
400
401
|
const started = Date.now();
|
|
401
402
|
const result = await client.query(deferred.sql, deferred.params);
|
|
402
403
|
const elapsedMs = Date.now() - started;
|
package/dist/client.d.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
import pg from 'pg';
|
|
25
|
+
import type { Dialect } from './dialect.js';
|
|
25
26
|
import { type ErrorMessageMode } from './errors.js';
|
|
26
27
|
import { type PipelineOptions, type PipelineResults } from './pipeline.js';
|
|
27
28
|
import { type DeferredQuery, QueryInterface, type QueryInterfaceOptions } from './query/index.js';
|
|
@@ -142,6 +143,8 @@ export interface TurbineConfig {
|
|
|
142
143
|
* Default: `true`. Set to `false` as a nuclear kill switch.
|
|
143
144
|
*/
|
|
144
145
|
sqlCache?: boolean;
|
|
146
|
+
/** SQL dialect implementation. Defaults to PostgreSQL. Internal Phase-1 seam for dialect packages. */
|
|
147
|
+
dialect?: Dialect;
|
|
145
148
|
}
|
|
146
149
|
/** Parameters passed to middleware functions */
|
|
147
150
|
export interface MiddlewareParams {
|
package/dist/client.js
CHANGED
|
@@ -197,6 +197,7 @@ export class TurbineClient {
|
|
|
197
197
|
warnOnUnlimited: config.warnOnUnlimited,
|
|
198
198
|
preparedStatements: envDisablePrepared ? false : (config.preparedStatements ?? !config.pool),
|
|
199
199
|
sqlCache: config.sqlCache ?? true,
|
|
200
|
+
dialect: config.dialect,
|
|
200
201
|
};
|
|
201
202
|
// Apply NotFoundError message redaction mode (default: safe — values are
|
|
202
203
|
// stripped from messages to avoid leaking PII into error logs).
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm — SQL dialect contract
|
|
3
|
+
*
|
|
4
|
+
* Phase-1 seam for future database packages. The current package remains
|
|
5
|
+
* PostgreSQL-native by default, but query generation now depends on this
|
|
6
|
+
* contract for the SQL primitives that vary across MySQL and SQLite.
|
|
7
|
+
*/
|
|
8
|
+
import type { SchemaMetadata } from './schema.js';
|
|
9
|
+
export type DialectName = 'postgresql' | 'mysql' | 'sqlite' | (string & {});
|
|
10
|
+
export interface Dialect {
|
|
11
|
+
/** Dialect identifier. */
|
|
12
|
+
readonly name: DialectName;
|
|
13
|
+
/** Parameter placeholder for the Nth value, using a 1-indexed public count. */
|
|
14
|
+
paramPlaceholder(index: number): string;
|
|
15
|
+
/** Quote a SQL identifier (table, column, cursor, alias). */
|
|
16
|
+
quoteIdentifier(name: string): string;
|
|
17
|
+
/** Escape a string literal body for SQL single-quoted strings. */
|
|
18
|
+
escapeStringLiteral(value: string): string;
|
|
19
|
+
/** Empty JSON array literal used as a fallback for to-many relations. */
|
|
20
|
+
readonly emptyJsonArrayLiteral: string;
|
|
21
|
+
/** JSON null literal/fallback for to-one relations. */
|
|
22
|
+
readonly nullJsonLiteral: string;
|
|
23
|
+
/** Build a JSON object expression from output keys and SQL expressions. */
|
|
24
|
+
buildJsonObject(pairs: [key: string, expr: string][]): string;
|
|
25
|
+
/** Build a JSON array aggregation expression with a dialect-specific empty-array fallback. */
|
|
26
|
+
buildJsonArrayAgg(jsonObjectExpr: string, orderBy?: string): string;
|
|
27
|
+
/** Whether INSERT/UPDATE/DELETE support RETURNING rows. */
|
|
28
|
+
readonly supportsReturning: boolean;
|
|
29
|
+
/** Whether native ILIKE is supported. */
|
|
30
|
+
readonly supportsILike: boolean;
|
|
31
|
+
/** Build a case-insensitive LIKE equivalent. */
|
|
32
|
+
buildInsensitiveLike(column: string, paramRef: string): string;
|
|
33
|
+
/** JSON operator support level for this dialect. */
|
|
34
|
+
readonly jsonPathSupport: 'native' | 'function' | 'limited';
|
|
35
|
+
/** Build a JSON containment check. */
|
|
36
|
+
buildJsonContains(column: string, paramRef: string): string;
|
|
37
|
+
/** Build a JSON path text extraction expression. */
|
|
38
|
+
buildJsonPathExtract(column: string, pathParamRef: string): string;
|
|
39
|
+
/** Build a correlation clause across single or composite keys. */
|
|
40
|
+
buildCorrelation(leftRef: string, leftColumns: string | string[], rightRef: string, rightColumns: string | string[]): string;
|
|
41
|
+
/** Type mapping hook for code generation. */
|
|
42
|
+
typeToTypeScript(dialectType: string, nullable: boolean): string;
|
|
43
|
+
/** Optional array-cast hook for bulk insert implementations. */
|
|
44
|
+
arrayType?(baseType: string): string;
|
|
45
|
+
}
|
|
46
|
+
export interface DialectIntrospector {
|
|
47
|
+
introspect(options: IntrospectOptions): Promise<SchemaMetadata>;
|
|
48
|
+
}
|
|
49
|
+
export interface IntrospectOptions {
|
|
50
|
+
connectionString: string;
|
|
51
|
+
schema?: string;
|
|
52
|
+
include?: string[];
|
|
53
|
+
exclude?: string[];
|
|
54
|
+
}
|
|
55
|
+
export interface DialectMigrator {
|
|
56
|
+
acquireLock(lockId: number): Promise<boolean>;
|
|
57
|
+
releaseLock(lockId: number): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
/** PostgreSQL implementation of the dialect contract. */
|
|
60
|
+
export declare const postgresDialect: Dialect;
|
|
61
|
+
//# sourceMappingURL=dialect.d.ts.map
|
package/dist/dialect.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm — SQL dialect contract
|
|
3
|
+
*
|
|
4
|
+
* Phase-1 seam for future database packages. The current package remains
|
|
5
|
+
* PostgreSQL-native by default, but query generation now depends on this
|
|
6
|
+
* contract for the SQL primitives that vary across MySQL and SQLite.
|
|
7
|
+
*/
|
|
8
|
+
/** PostgreSQL implementation of the dialect contract. */
|
|
9
|
+
export const postgresDialect = {
|
|
10
|
+
name: 'postgresql',
|
|
11
|
+
supportsReturning: true,
|
|
12
|
+
supportsILike: true,
|
|
13
|
+
jsonPathSupport: 'native',
|
|
14
|
+
emptyJsonArrayLiteral: "'[]'::json",
|
|
15
|
+
nullJsonLiteral: 'NULL',
|
|
16
|
+
paramPlaceholder(index) {
|
|
17
|
+
return `$${index}`;
|
|
18
|
+
},
|
|
19
|
+
quoteIdentifier(name) {
|
|
20
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
21
|
+
},
|
|
22
|
+
escapeStringLiteral(value) {
|
|
23
|
+
return value.replace(/'/g, "''");
|
|
24
|
+
},
|
|
25
|
+
buildJsonObject(pairs) {
|
|
26
|
+
const args = pairs.map(([key, expr]) => `'${this.escapeStringLiteral(key)}', ${expr}`);
|
|
27
|
+
return `json_build_object(${args.join(', ')})`;
|
|
28
|
+
},
|
|
29
|
+
buildJsonArrayAgg(jsonObjectExpr, orderBy) {
|
|
30
|
+
const suffix = orderBy ? ` ${orderBy}` : '';
|
|
31
|
+
return `COALESCE(json_agg(${jsonObjectExpr}${suffix}), ${this.emptyJsonArrayLiteral})`;
|
|
32
|
+
},
|
|
33
|
+
buildInsensitiveLike(column, paramRef) {
|
|
34
|
+
return `${column} ILIKE ${paramRef}`;
|
|
35
|
+
},
|
|
36
|
+
buildJsonContains(column, paramRef) {
|
|
37
|
+
return `${column} @> ${paramRef}::jsonb`;
|
|
38
|
+
},
|
|
39
|
+
buildJsonPathExtract(column, pathParamRef) {
|
|
40
|
+
return `${column} #>> ${pathParamRef}::text[]`;
|
|
41
|
+
},
|
|
42
|
+
buildCorrelation(leftRef, leftColumns, rightRef, rightColumns) {
|
|
43
|
+
const leftCols = Array.isArray(leftColumns) ? leftColumns : [leftColumns];
|
|
44
|
+
const rightCols = Array.isArray(rightColumns) ? rightColumns : [rightColumns];
|
|
45
|
+
return leftCols
|
|
46
|
+
.map((col, i) => `${leftRef}.${this.quoteIdentifier(col)} = ${rightRef}.${this.quoteIdentifier(rightCols[i])}`)
|
|
47
|
+
.join(' AND ');
|
|
48
|
+
},
|
|
49
|
+
typeToTypeScript(_dialectType, _nullable) {
|
|
50
|
+
// Existing PostgreSQL type mapping remains in schema.ts/generate.ts for now.
|
|
51
|
+
// This hook is the package boundary MySQL/SQLite implementations will fill.
|
|
52
|
+
return 'unknown';
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=dialect.js.map
|
package/dist/generate.js
CHANGED
|
@@ -229,7 +229,14 @@ function generateMetadata(schema) {
|
|
|
229
229
|
// relations
|
|
230
230
|
lines.push(' relations: {');
|
|
231
231
|
for (const [relName, rel] of Object.entries(table.relations)) {
|
|
232
|
-
|
|
232
|
+
// Emit foreignKey/referenceKey as string for single-column, array for composite
|
|
233
|
+
const fkLiteral = Array.isArray(rel.foreignKey)
|
|
234
|
+
? `[${rel.foreignKey.map((c) => `'${escSQ(c)}'`).join(', ')}]`
|
|
235
|
+
: `'${escSQ(rel.foreignKey)}'`;
|
|
236
|
+
const refLiteral = Array.isArray(rel.referenceKey)
|
|
237
|
+
? `[${rel.referenceKey.map((c) => `'${escSQ(c)}'`).join(', ')}]`
|
|
238
|
+
: `'${escSQ(rel.referenceKey)}'`;
|
|
239
|
+
lines.push(` ${relName}: { type: '${escSQ(rel.type)}', name: '${escSQ(rel.name)}', from: '${escSQ(rel.from)}', to: '${escSQ(rel.to)}', foreignKey: ${fkLiteral}, referenceKey: ${refLiteral} },`);
|
|
233
240
|
}
|
|
234
241
|
lines.push(' },');
|
|
235
242
|
// indexes
|
package/dist/index.d.ts
CHANGED
|
@@ -32,14 +32,18 @@
|
|
|
32
32
|
* await db.disconnect();
|
|
33
33
|
* ```
|
|
34
34
|
*/
|
|
35
|
+
export type { DatabaseAdapter, IntrospectionOverrides } from './adapters/index.js';
|
|
36
|
+
export { alloydb, cockroachdb, postgresql, timescale, yugabytedb } from './adapters/index.js';
|
|
35
37
|
export { type Middleware, type MiddlewareNext, type MiddlewareParams, type PgCompatPool, type PgCompatPoolClient, type PgCompatQueryResult, TransactionClient, type TransactionOptions, TurbineClient, type TurbineConfig, } from './client.js';
|
|
38
|
+
export type { Dialect, DialectIntrospector, DialectMigrator, DialectName, IntrospectOptions as DialectIntrospectOptions, } from './dialect.js';
|
|
39
|
+
export { postgresDialect } from './dialect.js';
|
|
36
40
|
export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, type ErrorMessageMode, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, PipelineError, type PipelineResultSlot, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, ValidationError, wrapPgError, } from './errors.js';
|
|
37
41
|
export { type GenerateOptions, generate } from './generate.js';
|
|
38
42
|
export { type IntrospectOptions, introspect } from './introspect.js';
|
|
39
43
|
export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
|
|
40
44
|
export { type AggregateArgs, type AggregateResult, type ArrayFilter, type CountArgs, type CreateArgs, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GroupByArgs, type JsonFilter, type OrderDirection, QueryInterface, type RelationDescriptor, type RelationFilter, type TypedWithClause, type UpdateArgs, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
|
|
41
45
|
export type { ColumnMetadata, IndexMetadata, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
42
|
-
export { camelToSnake, isDateType, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
|
46
|
+
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
|
43
47
|
export { ColumnBuilder, type ColumnConfig, type ColumnDef, type ColumnType, type ColumnTypeName, column, defineSchema, type SchemaDef, type TableDef, table, } from './schema-builder.js';
|
|
44
48
|
export { type AlterColumnDef, type AlterDef, type DiffResult, type PushResult, schemaDiff, schemaPush, schemaToSQL, schemaToSQLString, } from './schema-sql.js';
|
|
45
49
|
export { type TurbineHttpOptions, turbineHttp } from './serverless.js';
|
package/dist/index.js
CHANGED
|
@@ -32,8 +32,10 @@
|
|
|
32
32
|
* await db.disconnect();
|
|
33
33
|
* ```
|
|
34
34
|
*/
|
|
35
|
+
export { alloydb, cockroachdb, postgresql, timescale, yugabytedb } from './adapters/index.js';
|
|
35
36
|
// Client
|
|
36
37
|
export { TransactionClient, TurbineClient, } from './client.js';
|
|
38
|
+
export { postgresDialect } from './dialect.js';
|
|
37
39
|
// Error types
|
|
38
40
|
export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, PipelineError, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, ValidationError, wrapPgError, } from './errors.js';
|
|
39
41
|
// Code generation
|
|
@@ -45,7 +47,7 @@ export { executePipeline, pipelineSupported } from './pipeline.js';
|
|
|
45
47
|
// Query builder
|
|
46
48
|
export { QueryInterface, } from './query/index.js';
|
|
47
49
|
// Schema utilities
|
|
48
|
-
export { camelToSnake, isDateType, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
|
50
|
+
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
|
49
51
|
// Schema builder — define schemas in TypeScript
|
|
50
52
|
export { ColumnBuilder, column, defineSchema,
|
|
51
53
|
// Legacy compat (deprecated — use object format with defineSchema)
|
package/dist/introspect.js
CHANGED
|
@@ -66,13 +66,16 @@ const SQL_FOREIGN_KEYS = `
|
|
|
66
66
|
const SQL_UNIQUE_CONSTRAINTS = `
|
|
67
67
|
SELECT
|
|
68
68
|
tc.table_name,
|
|
69
|
-
|
|
69
|
+
tc.constraint_name,
|
|
70
|
+
kcu.column_name,
|
|
71
|
+
kcu.ordinal_position
|
|
70
72
|
FROM information_schema.table_constraints tc
|
|
71
73
|
JOIN information_schema.key_column_usage kcu
|
|
72
74
|
ON tc.constraint_name = kcu.constraint_name
|
|
73
75
|
AND tc.table_schema = kcu.table_schema
|
|
74
76
|
WHERE tc.constraint_type = 'UNIQUE'
|
|
75
77
|
AND tc.table_schema = $1
|
|
78
|
+
ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position
|
|
76
79
|
`;
|
|
77
80
|
const SQL_INDEXES = `
|
|
78
81
|
SELECT tablename, indexname, indexdef
|
|
@@ -153,14 +156,22 @@ export async function introspect(options) {
|
|
|
153
156
|
pkByTable.get(row.table_name).push(row.column_name);
|
|
154
157
|
}
|
|
155
158
|
// ----- Group unique constraints by table -----
|
|
159
|
+
// Group rows by (table_name, constraint_name) to correctly handle multi-column unique constraints
|
|
156
160
|
const uniqueByTable = new Map();
|
|
161
|
+
const uniqueConstraintGroups = new Map();
|
|
157
162
|
for (const row of uniqueResult.rows) {
|
|
158
163
|
if (!tableSet.has(row.table_name))
|
|
159
164
|
continue;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
const key = `${row.table_name}::${row.constraint_name}`;
|
|
166
|
+
if (!uniqueConstraintGroups.has(key)) {
|
|
167
|
+
uniqueConstraintGroups.set(key, { table: row.table_name, columns: [] });
|
|
168
|
+
}
|
|
169
|
+
uniqueConstraintGroups.get(key).columns.push(row.column_name);
|
|
170
|
+
}
|
|
171
|
+
for (const { table, columns } of uniqueConstraintGroups.values()) {
|
|
172
|
+
if (!uniqueByTable.has(table))
|
|
173
|
+
uniqueByTable.set(table, []);
|
|
174
|
+
uniqueByTable.get(table).push(columns);
|
|
164
175
|
}
|
|
165
176
|
// ----- Group indexes by table -----
|
|
166
177
|
const indexesByTable = new Map();
|
|
@@ -187,17 +198,25 @@ export async function introspect(options) {
|
|
|
187
198
|
enums[row.typname] = [];
|
|
188
199
|
enums[row.typname].push(row.enumlabel);
|
|
189
200
|
}
|
|
190
|
-
const
|
|
201
|
+
const fkGroups = new Map();
|
|
191
202
|
for (const row of fkResult.rows) {
|
|
192
203
|
if (!tableSet.has(row.source_table) || !tableSet.has(row.target_table))
|
|
193
204
|
continue;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
205
|
+
const key = row.constraint_name;
|
|
206
|
+
if (!fkGroups.has(key)) {
|
|
207
|
+
fkGroups.set(key, {
|
|
208
|
+
sourceTable: row.source_table,
|
|
209
|
+
sourceColumns: [],
|
|
210
|
+
targetTable: row.target_table,
|
|
211
|
+
targetColumns: [],
|
|
212
|
+
constraintName: key,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const entry = fkGroups.get(key);
|
|
216
|
+
entry.sourceColumns.push(row.source_column);
|
|
217
|
+
entry.targetColumns.push(row.target_column);
|
|
200
218
|
}
|
|
219
|
+
const foreignKeys = Array.from(fkGroups.values());
|
|
201
220
|
// ----- Build relations from foreign keys -----
|
|
202
221
|
// Count FKs per (source, target) pair for disambiguation
|
|
203
222
|
const fkCounts = new Map();
|
|
@@ -209,10 +228,17 @@ export async function introspect(options) {
|
|
|
209
228
|
for (const fk of foreignKeys) {
|
|
210
229
|
const pairKey = `${fk.sourceTable}→${fk.targetTable}`;
|
|
211
230
|
const needsDisambiguation = (fkCounts.get(pairKey) ?? 0) > 1;
|
|
231
|
+
// For single-column FKs, keep string form for backwards compatibility.
|
|
232
|
+
// For multi-column (composite) FKs, use array form.
|
|
233
|
+
const foreignKey = fk.sourceColumns.length === 1 ? fk.sourceColumns[0] : fk.sourceColumns;
|
|
234
|
+
const referenceKey = fk.targetColumns.length === 1 ? fk.targetColumns[0] : fk.targetColumns;
|
|
212
235
|
// --- belongsTo on the source (child) table ---
|
|
213
236
|
// e.g. posts.user_id → users.id creates posts.user (belongsTo)
|
|
237
|
+
// For composite FKs with disambiguation, use the constraint name
|
|
214
238
|
const belongsToName = needsDisambiguation
|
|
215
|
-
?
|
|
239
|
+
? fk.sourceColumns.length === 1
|
|
240
|
+
? snakeToCamel(fk.sourceColumns[0].replace(/_id$/, ''))
|
|
241
|
+
: snakeToCamel(fk.constraintName.replace(/^fk_/, '').replace(/_fkey$/, ''))
|
|
216
242
|
: singularize(snakeToCamel(fk.targetTable));
|
|
217
243
|
if (!relationsByTable.has(fk.sourceTable))
|
|
218
244
|
relationsByTable.set(fk.sourceTable, {});
|
|
@@ -221,13 +247,15 @@ export async function introspect(options) {
|
|
|
221
247
|
name: belongsToName,
|
|
222
248
|
from: fk.sourceTable,
|
|
223
249
|
to: fk.targetTable,
|
|
224
|
-
foreignKey
|
|
225
|
-
referenceKey
|
|
250
|
+
foreignKey,
|
|
251
|
+
referenceKey,
|
|
226
252
|
};
|
|
227
253
|
// --- hasMany on the target (parent) table ---
|
|
228
254
|
// e.g. posts.user_id → users.id creates users.posts (hasMany)
|
|
229
255
|
const hasManyName = needsDisambiguation
|
|
230
|
-
?
|
|
256
|
+
? fk.sourceColumns.length === 1
|
|
257
|
+
? snakeToCamel(`${fk.sourceTable}_by_${fk.sourceColumns[0].replace(/_id$/, '')}`)
|
|
258
|
+
: snakeToCamel(`${fk.sourceTable}_by_${fk.constraintName.replace(/^fk_/, '').replace(/_fkey$/, '')}`)
|
|
231
259
|
: snakeToCamel(fk.sourceTable);
|
|
232
260
|
if (!relationsByTable.has(fk.targetTable))
|
|
233
261
|
relationsByTable.set(fk.targetTable, {});
|
|
@@ -236,8 +264,8 @@ export async function introspect(options) {
|
|
|
236
264
|
name: hasManyName,
|
|
237
265
|
from: fk.targetTable,
|
|
238
266
|
to: fk.sourceTable,
|
|
239
|
-
foreignKey
|
|
240
|
-
referenceKey
|
|
267
|
+
foreignKey,
|
|
268
|
+
referenceKey,
|
|
241
269
|
};
|
|
242
270
|
}
|
|
243
271
|
// ----- Assemble TableMetadata for each table -----
|
package/dist/query/builder.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* metadata — nothing is hardcoded.
|
|
12
12
|
*/
|
|
13
13
|
import type pg from 'pg';
|
|
14
|
+
import type { Dialect } from '../dialect.js';
|
|
14
15
|
import type { SchemaMetadata } from '../schema.js';
|
|
15
16
|
import type { AggregateArgs, AggregateResult, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, TypedWithClause, UpdateArgs, UpdateManyArgs, UpsertArgs, WithClause, WithResult } from './types.js';
|
|
16
17
|
export interface DeferredQuery<T> {
|
|
@@ -64,6 +65,8 @@ export interface QueryInterfaceOptions {
|
|
|
64
65
|
* Default: `true`. Set to `false` as a nuclear kill switch.
|
|
65
66
|
*/
|
|
66
67
|
sqlCache?: boolean;
|
|
68
|
+
/** SQL dialect implementation. Defaults to PostgreSQL. */
|
|
69
|
+
dialect?: Dialect;
|
|
67
70
|
}
|
|
68
71
|
export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
69
72
|
private readonly pool;
|
|
@@ -77,6 +80,7 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
77
80
|
private readonly warnOnUnlimited;
|
|
78
81
|
private readonly preparedStatementsEnabled;
|
|
79
82
|
private readonly sqlCacheEnabled;
|
|
83
|
+
private readonly dialect;
|
|
80
84
|
/**
|
|
81
85
|
* Tracks tables that have already triggered an unlimited-query warning so
|
|
82
86
|
* the user is not spammed once per row. Per-instance state — each
|
|
@@ -95,6 +99,10 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
95
99
|
/** Tracks tables that have already triggered a deep-with warning (one-time) */
|
|
96
100
|
private readonly deepWithWarned;
|
|
97
101
|
constructor(pool: pg.Pool, table: string, schema: SchemaMetadata, middlewares?: MiddlewareFn[], options?: QueryInterfaceOptions);
|
|
102
|
+
/** Quote an identifier through the active SQL dialect. */
|
|
103
|
+
private q;
|
|
104
|
+
/** Return the active dialect's placeholder for a 1-indexed parameter position. */
|
|
105
|
+
private p;
|
|
98
106
|
/**
|
|
99
107
|
* Return cache hit/miss statistics for this QueryInterface instance.
|
|
100
108
|
* Useful for monitoring and benchmarking.
|
|
@@ -433,7 +441,7 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
433
441
|
* 8. **Parameter threading:** All user-supplied values (where filters, limit) are
|
|
434
442
|
* pushed to the shared `params` array with `$N` placeholders. No string
|
|
435
443
|
* interpolation of user data ever occurs -- all identifiers go through
|
|
436
|
-
* `
|
|
444
|
+
* `this.q()` and all values are parameterized.
|
|
437
445
|
*
|
|
438
446
|
* ### Example output (hasMany with nested relation)
|
|
439
447
|
* ```sql
|