turbine-orm 0.19.1 → 0.21.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 +156 -24
- package/dist/adapters/cockroachdb.js +4 -9
- package/dist/cjs/adapters/cockroachdb.js +4 -9
- package/dist/cjs/cli/index.js +23 -12
- package/dist/cjs/cli/observe-ui.js +12 -2
- package/dist/cjs/cli/observe.js +6 -7
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.js +6 -37
- package/dist/cjs/client.js +45 -33
- package/dist/cjs/dialect.js +116 -0
- package/dist/cjs/errors.js +19 -1
- package/dist/cjs/generate.js +4 -0
- package/dist/cjs/index.js +3 -2
- package/dist/cjs/introspect.js +20 -0
- package/dist/cjs/mssql.js +1338 -0
- package/dist/cjs/mysql.js +1052 -0
- package/dist/cjs/query/builder.js +408 -122
- package/dist/cjs/query/utils.js +1 -0
- package/dist/cjs/sqlite.js +849 -0
- package/dist/cjs/typed-sql.js +9 -7
- package/dist/cli/index.js +23 -12
- package/dist/cli/migrate.d.ts +2 -2
- package/dist/cli/observe-ui.d.ts +1 -1
- package/dist/cli/observe-ui.js +12 -2
- package/dist/cli/observe.js +7 -8
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +0 -10
- package/dist/cli/studio.js +7 -37
- package/dist/client.d.ts +35 -14
- package/dist/client.js +46 -34
- package/dist/dialect.d.ts +258 -1
- package/dist/dialect.js +83 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +17 -0
- package/dist/generate.js +4 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/introspect.d.ts +18 -0
- package/dist/introspect.js +19 -0
- package/dist/mssql.d.ts +233 -0
- package/dist/mssql.js +1298 -0
- package/dist/mysql.d.ts +174 -0
- package/dist/mysql.js +1012 -0
- package/dist/query/builder.d.ts +105 -6
- package/dist/query/builder.js +409 -123
- package/dist/query/index.d.ts +2 -2
- package/dist/query/types.d.ts +62 -12
- package/dist/query/utils.js +1 -0
- package/dist/sqlite.d.ts +144 -0
- package/dist/sqlite.js +842 -0
- package/dist/typed-sql.d.ts +7 -5
- package/dist/typed-sql.js +9 -7
- package/package.json +32 -3
package/dist/cjs/cli/studio.js
CHANGED
|
@@ -34,7 +34,6 @@ exports.apiBuilder = apiBuilder;
|
|
|
34
34
|
exports.apiListSavedQueries = apiListSavedQueries;
|
|
35
35
|
exports.apiCreateSavedQuery = apiCreateSavedQuery;
|
|
36
36
|
exports.apiDeleteSavedQuery = apiDeleteSavedQuery;
|
|
37
|
-
exports.isReadOnlyStatement = isReadOnlyStatement;
|
|
38
37
|
const node_child_process_1 = require("node:child_process");
|
|
39
38
|
const node_crypto_1 = require("node:crypto");
|
|
40
39
|
const node_fs_1 = require("node:fs");
|
|
@@ -238,13 +237,12 @@ function checkRateLimit(limiter, token) {
|
|
|
238
237
|
return { allowed: true, resetAt: entry.resetAt };
|
|
239
238
|
}
|
|
240
239
|
function constantTimeEqual(a, b) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return result === 0;
|
|
240
|
+
// Hash both inputs to fixed-length 32-byte SHA-256 digests before comparing.
|
|
241
|
+
// This makes the comparison constant-length (timingSafeEqual never throws on a
|
|
242
|
+
// length mismatch) and leaks neither length nor content via timing.
|
|
243
|
+
const ah = (0, node_crypto_1.createHash)('sha256').update(a).digest();
|
|
244
|
+
const bh = (0, node_crypto_1.createHash)('sha256').update(b).digest();
|
|
245
|
+
return (0, node_crypto_1.timingSafeEqual)(ah, bh);
|
|
248
246
|
}
|
|
249
247
|
/**
|
|
250
248
|
* Build a helpful "unknown table" error that lists the available tables so the
|
|
@@ -550,35 +548,6 @@ function clampInt(value, fallback, min, max) {
|
|
|
550
548
|
return fallback;
|
|
551
549
|
return Math.min(Math.max(n, min), max);
|
|
552
550
|
}
|
|
553
|
-
/**
|
|
554
|
-
* Accept only SELECT or WITH (CTE) statements. Reject any statement that
|
|
555
|
-
* contains a semicolon followed by non-whitespace (prevents statement
|
|
556
|
-
* stacking), and require the first non-comment keyword to be SELECT or WITH.
|
|
557
|
-
*
|
|
558
|
-
* This is a first-line filter — the transaction's READ ONLY mode is the
|
|
559
|
-
* second line of defense. Both must fail before a destructive statement
|
|
560
|
-
* could run.
|
|
561
|
-
*/
|
|
562
|
-
function isReadOnlyStatement(sql) {
|
|
563
|
-
const stripped = stripSqlComments(sql).trim();
|
|
564
|
-
if (!stripped)
|
|
565
|
-
return false;
|
|
566
|
-
// Disallow statement stacking. A single trailing `;` is fine.
|
|
567
|
-
const withoutTrailingSemi = stripped.replace(/;+\s*$/, '');
|
|
568
|
-
if (withoutTrailingSemi.includes(';'))
|
|
569
|
-
return false;
|
|
570
|
-
const firstWord = withoutTrailingSemi.slice(0, 6).toUpperCase();
|
|
571
|
-
if (firstWord.startsWith('SELECT'))
|
|
572
|
-
return true;
|
|
573
|
-
if (firstWord.startsWith('WITH'))
|
|
574
|
-
return true;
|
|
575
|
-
return false;
|
|
576
|
-
}
|
|
577
|
-
function stripSqlComments(sql) {
|
|
578
|
-
// Strip -- line comments and /* block comments */. Not a full SQL parser,
|
|
579
|
-
// but sufficient to catch the common bypass attempts.
|
|
580
|
-
return sql.replace(/--[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
581
|
-
}
|
|
582
551
|
function serializeRow(row) {
|
|
583
552
|
const out = {};
|
|
584
553
|
for (const [k, v] of Object.entries(row)) {
|
package/dist/cjs/client.js
CHANGED
|
@@ -29,6 +29,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
29
29
|
exports.TurbineClient = exports.TransactionClient = void 0;
|
|
30
30
|
exports.withRetry = withRetry;
|
|
31
31
|
const pg_1 = __importDefault(require("pg"));
|
|
32
|
+
const dialect_js_1 = require("./dialect.js");
|
|
32
33
|
const errors_js_1 = require("./errors.js");
|
|
33
34
|
const observe_js_1 = require("./observe.js");
|
|
34
35
|
const pipeline_js_1 = require("./pipeline.js");
|
|
@@ -89,11 +90,14 @@ class TransactionClient {
|
|
|
89
90
|
queryOptions;
|
|
90
91
|
tableCache = new Map();
|
|
91
92
|
savepointCounter = 0;
|
|
93
|
+
/** Active SQL dialect — owns savepoint keywords and raw-SQL placeholders. */
|
|
94
|
+
dialect;
|
|
92
95
|
constructor(client, schema, middlewares, queryOptions) {
|
|
93
96
|
this.client = client;
|
|
94
97
|
this.schema = schema;
|
|
95
98
|
this.middlewares = middlewares;
|
|
96
99
|
this.queryOptions = queryOptions;
|
|
100
|
+
this.dialect = queryOptions?.dialect ?? dialect_js_1.postgresDialect;
|
|
97
101
|
// Auto-create typed table accessors for all tables in the schema
|
|
98
102
|
for (const tableName of Object.keys(schema.tables)) {
|
|
99
103
|
const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
@@ -127,14 +131,14 @@ class TransactionClient {
|
|
|
127
131
|
*/
|
|
128
132
|
async $transaction(fn) {
|
|
129
133
|
const savepointName = `sp_${++this.savepointCounter}`;
|
|
130
|
-
await this.client.query(
|
|
134
|
+
await this.client.query(this.dialect.savepointStatement(savepointName));
|
|
131
135
|
try {
|
|
132
136
|
const result = await fn(this);
|
|
133
|
-
await this.client.query(
|
|
137
|
+
await this.client.query(this.dialect.releaseSavepointStatement(savepointName));
|
|
134
138
|
return result;
|
|
135
139
|
}
|
|
136
140
|
catch (err) {
|
|
137
|
-
await this.client.query(
|
|
141
|
+
await this.client.query(this.dialect.rollbackToSavepointStatement(savepointName));
|
|
138
142
|
throw err;
|
|
139
143
|
}
|
|
140
144
|
}
|
|
@@ -146,7 +150,7 @@ class TransactionClient {
|
|
|
146
150
|
strings.forEach((str, i) => {
|
|
147
151
|
sql += str;
|
|
148
152
|
if (i < values.length) {
|
|
149
|
-
sql +=
|
|
153
|
+
sql += this.dialect.paramPlaceholder(i + 1);
|
|
150
154
|
}
|
|
151
155
|
});
|
|
152
156
|
try {
|
|
@@ -200,6 +204,8 @@ class TurbineClient {
|
|
|
200
204
|
schema;
|
|
201
205
|
static int8ParserRegistered = false;
|
|
202
206
|
logging;
|
|
207
|
+
/** Active SQL dialect — owns transaction keywords, set_config, raw-SQL placeholders, capability flags. */
|
|
208
|
+
dialect;
|
|
203
209
|
tableCache = new Map();
|
|
204
210
|
middlewares = [];
|
|
205
211
|
queryListeners = new Set();
|
|
@@ -245,6 +251,7 @@ class TurbineClient {
|
|
|
245
251
|
TurbineClient.int8ParserRegistered = true;
|
|
246
252
|
}
|
|
247
253
|
this.logging = config.logging ?? false;
|
|
254
|
+
this.dialect = config.dialect ?? dialect_js_1.postgresDialect;
|
|
248
255
|
this.schema = schema;
|
|
249
256
|
// Respect env var kill switch
|
|
250
257
|
const envDisablePrepared = typeof process !== 'undefined' && process.env?.TURBINE_DISABLE_PREPARED === '1';
|
|
@@ -331,12 +338,14 @@ class TurbineClient {
|
|
|
331
338
|
// Middleware — intercept all queries
|
|
332
339
|
// -------------------------------------------------------------------------
|
|
333
340
|
/**
|
|
334
|
-
* Register a middleware function that runs
|
|
341
|
+
* Register a middleware function that runs around every query.
|
|
335
342
|
*
|
|
336
|
-
* Middleware can inspect and log query parameters,
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
343
|
+
* Middleware can inspect and log query parameters, measure timing, and
|
|
344
|
+
* transform the result returned by `next()`. Note: query SQL is generated
|
|
345
|
+
* BEFORE middleware runs — `params.args` is a read-only snapshot, and
|
|
346
|
+
* mutating it does NOT change the executed SQL. Cross-cutting filters
|
|
347
|
+
* (e.g. soft deletes) belong in the query itself: pass an explicit
|
|
348
|
+
* `where: { deletedAt: null }` or wrap the table accessor in a small helper.
|
|
340
349
|
*
|
|
341
350
|
* @example
|
|
342
351
|
* ```ts
|
|
@@ -348,16 +357,13 @@ class TurbineClient {
|
|
|
348
357
|
* return result;
|
|
349
358
|
* });
|
|
350
359
|
*
|
|
351
|
-
* //
|
|
360
|
+
* // Result transformation middleware — redact a field on the way out
|
|
352
361
|
* db.$use(async (params, next) => {
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
* if (params.action === 'delete') {
|
|
357
|
-
* params.action = 'update';
|
|
358
|
-
* params.args = { where: params.args.where, data: { deletedAt: new Date() } };
|
|
362
|
+
* const result = await next(params);
|
|
363
|
+
* if (params.model === 'users' && Array.isArray(result)) {
|
|
364
|
+
* for (const row of result as { email?: string }[]) row.email = '[redacted]';
|
|
359
365
|
* }
|
|
360
|
-
* return
|
|
366
|
+
* return result;
|
|
361
367
|
* });
|
|
362
368
|
* ```
|
|
363
369
|
*/
|
|
@@ -474,7 +480,7 @@ class TurbineClient {
|
|
|
474
480
|
strings.forEach((str, i) => {
|
|
475
481
|
sql += str;
|
|
476
482
|
if (i < values.length) {
|
|
477
|
-
sql +=
|
|
483
|
+
sql += this.dialect.paramPlaceholder(i + 1);
|
|
478
484
|
}
|
|
479
485
|
});
|
|
480
486
|
if (this.logging) {
|
|
@@ -519,7 +525,7 @@ class TurbineClient {
|
|
|
519
525
|
* ```
|
|
520
526
|
*/
|
|
521
527
|
sql(strings, ...values) {
|
|
522
|
-
const { sql, params } = (0, typed_sql_js_1.buildTypedSql)(strings, values);
|
|
528
|
+
const { sql, params } = (0, typed_sql_js_1.buildTypedSql)(strings, values, this.dialect);
|
|
523
529
|
return new typed_sql_js_1.TypedSqlQuery(this.pool, sql, params, this.logging);
|
|
524
530
|
}
|
|
525
531
|
// -------------------------------------------------------------------------
|
|
@@ -539,13 +545,13 @@ class TurbineClient {
|
|
|
539
545
|
async transaction(fn) {
|
|
540
546
|
const client = await this.pool.connect();
|
|
541
547
|
try {
|
|
542
|
-
await client.query(
|
|
548
|
+
await client.query(this.dialect.beginStatement());
|
|
543
549
|
const result = await fn(client);
|
|
544
|
-
await client.query(
|
|
550
|
+
await client.query(this.dialect.commitStatement());
|
|
545
551
|
return result;
|
|
546
552
|
}
|
|
547
553
|
catch (err) {
|
|
548
|
-
await client.query(
|
|
554
|
+
await client.query(this.dialect.rollbackStatement());
|
|
549
555
|
throw err;
|
|
550
556
|
}
|
|
551
557
|
finally {
|
|
@@ -596,14 +602,10 @@ class TurbineClient {
|
|
|
596
602
|
};
|
|
597
603
|
let timedOut = false;
|
|
598
604
|
try {
|
|
599
|
-
// BEGIN with optional isolation level
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if (level)
|
|
604
|
-
beginSQL += ` ISOLATION LEVEL ${level}`;
|
|
605
|
-
}
|
|
606
|
-
await client.query(beginSQL);
|
|
605
|
+
// BEGIN with optional isolation level — the dialect owns the keyword and
|
|
606
|
+
// BEGIN+isolation composition (Postgres appends ` ISOLATION LEVEL …`).
|
|
607
|
+
const isolationSql = options?.isolationLevel ? ISOLATION_LEVELS[options.isolationLevel] : undefined;
|
|
608
|
+
await client.query(this.dialect.beginStatement(isolationSql));
|
|
607
609
|
// Apply transaction-local session context (RLS / multi-tenant GUCs).
|
|
608
610
|
// Order matters: BEGIN -> isolation level (above) -> set_config loop ->
|
|
609
611
|
// user fn. Any error here propagates to the catch below and rolls back
|
|
@@ -611,12 +613,16 @@ class TurbineClient {
|
|
|
611
613
|
// is_local=true) — the parameterizable, transaction-scoped equivalent of
|
|
612
614
|
// SET LOCAL — so both name and value are BOUND params, never interpolated.
|
|
613
615
|
if (options?.sessionContext) {
|
|
616
|
+
if (!this.dialect.supportsRLS) {
|
|
617
|
+
throw new errors_js_1.UnsupportedFeatureError('sessionContext (RLS session GUCs)', this.dialect.name, 'set_config-based row-level-security context requires PostgreSQL.');
|
|
618
|
+
}
|
|
614
619
|
for (const [name, value] of Object.entries(options.sessionContext)) {
|
|
615
620
|
if (!GUC_NAME_REGEX.test(name)) {
|
|
616
621
|
throw new errors_js_1.ValidationError(`[turbine] Invalid session-context GUC name "${name}" — must match ` +
|
|
617
622
|
'/^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$/ (optionally namespaced, e.g. "app.current_tenant")');
|
|
618
623
|
}
|
|
619
|
-
|
|
624
|
+
const cfg = this.dialect.buildSetSessionConfig(name, String(value));
|
|
625
|
+
await client.query(cfg.sql, cfg.params);
|
|
620
626
|
}
|
|
621
627
|
}
|
|
622
628
|
// Create the transaction client with typed table accessors
|
|
@@ -662,7 +668,7 @@ class TurbineClient {
|
|
|
662
668
|
else {
|
|
663
669
|
result = await fn(tx);
|
|
664
670
|
}
|
|
665
|
-
await client.query(
|
|
671
|
+
await client.query(this.dialect.commitStatement());
|
|
666
672
|
if (this.logging) {
|
|
667
673
|
console.log('[turbine] Transaction committed');
|
|
668
674
|
}
|
|
@@ -675,7 +681,7 @@ class TurbineClient {
|
|
|
675
681
|
// when its socket was closed).
|
|
676
682
|
if (!timedOut && !released) {
|
|
677
683
|
try {
|
|
678
|
-
await client.query(
|
|
684
|
+
await client.query(this.dialect.rollbackStatement());
|
|
679
685
|
}
|
|
680
686
|
catch {
|
|
681
687
|
// Best-effort rollback — the connection may have died mid-query.
|
|
@@ -742,6 +748,9 @@ class TurbineClient {
|
|
|
742
748
|
* ```
|
|
743
749
|
*/
|
|
744
750
|
async $listen(channel, handler) {
|
|
751
|
+
if (!this.dialect.supportsListenNotify) {
|
|
752
|
+
throw new errors_js_1.UnsupportedFeatureError('$listen (LISTEN/NOTIFY realtime)', this.dialect.name, 'Realtime pub/sub requires PostgreSQL.');
|
|
753
|
+
}
|
|
745
754
|
(0, realtime_js_1.validateChannel)(channel);
|
|
746
755
|
const quoted = (0, utils_js_1.quoteIdent)(channel);
|
|
747
756
|
if (this.logging) {
|
|
@@ -767,6 +776,9 @@ class TurbineClient {
|
|
|
767
776
|
* ```
|
|
768
777
|
*/
|
|
769
778
|
async $notify(channel, payload) {
|
|
779
|
+
if (!this.dialect.supportsListenNotify) {
|
|
780
|
+
throw new errors_js_1.UnsupportedFeatureError('$notify (LISTEN/NOTIFY realtime)', this.dialect.name, 'Realtime pub/sub requires PostgreSQL.');
|
|
781
|
+
}
|
|
770
782
|
(0, realtime_js_1.validateChannel)(channel);
|
|
771
783
|
if (this.logging) {
|
|
772
784
|
console.log(`[turbine] NOTIFY ${channel}`);
|
package/dist/cjs/dialect.js
CHANGED
|
@@ -6,6 +6,39 @@
|
|
|
6
6
|
* PostgreSQL-native by default, but query generation now depends on this
|
|
7
7
|
* contract for the SQL primitives that vary across MySQL and SQLite.
|
|
8
8
|
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
9
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
43
|
exports.postgresDialect = void 0;
|
|
11
44
|
const errors_js_1 = require("./errors.js");
|
|
@@ -13,11 +46,17 @@ const schema_js_1 = require("./schema.js");
|
|
|
13
46
|
/** PostgreSQL implementation of the dialect contract. */
|
|
14
47
|
exports.postgresDialect = {
|
|
15
48
|
name: 'postgresql',
|
|
49
|
+
resultStrategy: 'returning',
|
|
16
50
|
supportsReturning: true,
|
|
17
51
|
supportsILike: true,
|
|
18
52
|
jsonPathSupport: 'native',
|
|
19
53
|
emptyJsonArrayLiteral: "'[]'::json",
|
|
20
54
|
nullJsonLiteral: 'NULL',
|
|
55
|
+
aggSupportsInlineOrderBy: true,
|
|
56
|
+
supportsVector: true,
|
|
57
|
+
supportsListenNotify: true,
|
|
58
|
+
supportsRLS: true,
|
|
59
|
+
supportsAdvisoryLock: true,
|
|
21
60
|
paramPlaceholder(index) {
|
|
22
61
|
return `$${index}`;
|
|
23
62
|
},
|
|
@@ -35,6 +74,9 @@ exports.postgresDialect = {
|
|
|
35
74
|
const suffix = orderBy ? ` ${orderBy}` : '';
|
|
36
75
|
return `COALESCE(json_agg(${jsonObjectExpr}${suffix}), ${this.emptyJsonArrayLiteral})`;
|
|
37
76
|
},
|
|
77
|
+
wrapJsonSubresult(subquery, fallback) {
|
|
78
|
+
return `COALESCE((${subquery}), ${fallback})`;
|
|
79
|
+
},
|
|
38
80
|
buildReturningClause(selection = '*') {
|
|
39
81
|
return ` RETURNING ${selection}`;
|
|
40
82
|
},
|
|
@@ -76,6 +118,15 @@ exports.postgresDialect = {
|
|
|
76
118
|
typeToTypeScript(dialectType, nullable) {
|
|
77
119
|
return (0, schema_js_1.pgTypeToTs)(dialectType, nullable);
|
|
78
120
|
},
|
|
121
|
+
castAggregate(expr, target) {
|
|
122
|
+
return `${expr}::${target}`;
|
|
123
|
+
},
|
|
124
|
+
buildInClause(expr, paramRef, negated) {
|
|
125
|
+
return negated ? `${expr} != ALL(${paramRef})` : `${expr} = ANY(${paramRef})`;
|
|
126
|
+
},
|
|
127
|
+
inClauseParam(values) {
|
|
128
|
+
return values;
|
|
129
|
+
},
|
|
79
130
|
arrayType(baseType) {
|
|
80
131
|
return (0, schema_js_1.pgArrayType)(baseType);
|
|
81
132
|
},
|
|
@@ -131,4 +182,69 @@ exports.postgresDialect = {
|
|
|
131
182
|
buildMigrationDeleteApplied(table) {
|
|
132
183
|
return `DELETE FROM ${table} WHERE name = ${this.paramPlaceholder(1)}`;
|
|
133
184
|
},
|
|
185
|
+
beginStatement(isolationLevel) {
|
|
186
|
+
return isolationLevel ? `BEGIN ISOLATION LEVEL ${isolationLevel}` : 'BEGIN';
|
|
187
|
+
},
|
|
188
|
+
commitStatement() {
|
|
189
|
+
return 'COMMIT';
|
|
190
|
+
},
|
|
191
|
+
rollbackStatement() {
|
|
192
|
+
return 'ROLLBACK';
|
|
193
|
+
},
|
|
194
|
+
savepointStatement(name) {
|
|
195
|
+
return `SAVEPOINT ${name}`;
|
|
196
|
+
},
|
|
197
|
+
releaseSavepointStatement(name) {
|
|
198
|
+
return `RELEASE SAVEPOINT ${name}`;
|
|
199
|
+
},
|
|
200
|
+
rollbackToSavepointStatement(name) {
|
|
201
|
+
return `ROLLBACK TO SAVEPOINT ${name}`;
|
|
202
|
+
},
|
|
203
|
+
buildSetSessionConfig(name, value) {
|
|
204
|
+
// set_config(name, value, is_local=true) — the parameterizable,
|
|
205
|
+
// transaction-local equivalent of `SET LOCAL` (which rejects bind params).
|
|
206
|
+
return {
|
|
207
|
+
sql: `SELECT set_config(${this.paramPlaceholder(1)}, ${this.paramPlaceholder(2)}, true)`,
|
|
208
|
+
params: [name, value],
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
async *openStream(connection, sql, params, batchSize) {
|
|
212
|
+
// Cursors require a single connection inside a transaction. Identical SQL
|
|
213
|
+
// sequence to the historical inline implementation: BEGIN → DECLARE … NO
|
|
214
|
+
// SCROLL CURSOR FOR → FETCH n (loop) → CLOSE → COMMIT; ROLLBACK on error.
|
|
215
|
+
const cursorName = `turbine_cursor_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
216
|
+
const quotedCursor = this.quoteIdentifier(cursorName);
|
|
217
|
+
await connection.query(this.beginStatement());
|
|
218
|
+
try {
|
|
219
|
+
await connection.query(`DECLARE ${quotedCursor} NO SCROLL CURSOR FOR ${sql}`, params);
|
|
220
|
+
while (true) {
|
|
221
|
+
const batch = await connection.query(`FETCH ${batchSize} FROM ${quotedCursor}`);
|
|
222
|
+
if (batch.rows.length === 0)
|
|
223
|
+
break;
|
|
224
|
+
yield batch.rows;
|
|
225
|
+
if (batch.rows.length < batchSize)
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
await connection.query(`CLOSE ${quotedCursor}`);
|
|
229
|
+
await connection.query(this.commitStatement());
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
try {
|
|
233
|
+
await connection.query(this.rollbackStatement());
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// Connection may already be broken — ignore rollback error.
|
|
237
|
+
}
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
// Postgres introspection wraps the information_schema / pg_catalog reader in
|
|
242
|
+
// introspect.ts. A dynamic import keeps the static graph acyclic (introspect.ts
|
|
243
|
+
// imports postgresDialect) and calls the raw catalog reader (never the router).
|
|
244
|
+
introspector: {
|
|
245
|
+
async introspect(options) {
|
|
246
|
+
const { introspectPostgresCatalog } = await Promise.resolve().then(() => __importStar(require('./introspect.js')));
|
|
247
|
+
return introspectPostgresCatalog(options);
|
|
248
|
+
},
|
|
249
|
+
},
|
|
134
250
|
};
|
package/dist/cjs/errors.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* All Turbine errors extend TurbineError which includes a `code` property.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.OptimisticLockError = exports.PipelineError = exports.ExclusionConstraintError = exports.CheckConstraintError = exports.SerializationFailureError = exports.DeadlockError = exports.NotNullViolationError = exports.ForeignKeyError = exports.UniqueConstraintError = exports.CircularRelationError = exports.MigrationError = exports.RelationError = exports.ConnectionError = exports.ValidationError = exports.TimeoutError = exports.NotFoundError = exports.TurbineError = exports.TurbineErrorCode = void 0;
|
|
9
|
+
exports.UnsupportedFeatureError = exports.OptimisticLockError = exports.PipelineError = exports.ExclusionConstraintError = exports.CheckConstraintError = exports.SerializationFailureError = exports.DeadlockError = exports.NotNullViolationError = exports.ForeignKeyError = exports.UniqueConstraintError = exports.CircularRelationError = exports.MigrationError = exports.RelationError = exports.ConnectionError = exports.ValidationError = exports.TimeoutError = exports.NotFoundError = exports.TurbineError = exports.TurbineErrorCode = void 0;
|
|
10
10
|
exports.setErrorMessageMode = setErrorMessageMode;
|
|
11
11
|
exports.getErrorMessageMode = getErrorMessageMode;
|
|
12
12
|
exports.wrapPgError = wrapPgError;
|
|
@@ -28,6 +28,7 @@ exports.TurbineErrorCode = {
|
|
|
28
28
|
PIPELINE: 'TURBINE_E014',
|
|
29
29
|
OPTIMISTIC_LOCK: 'TURBINE_E015',
|
|
30
30
|
EXCLUSION_VIOLATION: 'TURBINE_E016',
|
|
31
|
+
UNSUPPORTED_FEATURE: 'TURBINE_E017',
|
|
31
32
|
};
|
|
32
33
|
/** Base error class for all Turbine errors */
|
|
33
34
|
class TurbineError extends Error {
|
|
@@ -458,6 +459,23 @@ class OptimisticLockError extends TurbineError {
|
|
|
458
459
|
}
|
|
459
460
|
}
|
|
460
461
|
exports.OptimisticLockError = OptimisticLockError;
|
|
462
|
+
/**
|
|
463
|
+
* Thrown when a Postgres-only feature (pgvector distance ops, LISTEN/NOTIFY
|
|
464
|
+
* realtime, RLS session GUCs, advisory-lock migration locking, ...) is invoked
|
|
465
|
+
* on a dialect/engine whose capability flag reports it unsupported. Surfaces a
|
|
466
|
+
* clear `unsupported on <engine>` message instead of generating broken SQL.
|
|
467
|
+
*/
|
|
468
|
+
class UnsupportedFeatureError extends TurbineError {
|
|
469
|
+
feature;
|
|
470
|
+
dialect;
|
|
471
|
+
constructor(feature, dialect, hint) {
|
|
472
|
+
super(exports.TurbineErrorCode.UNSUPPORTED_FEATURE, `[turbine] ${feature} is unsupported on "${dialect}".${hint ? ` ${hint}` : ''}`);
|
|
473
|
+
this.name = 'UnsupportedFeatureError';
|
|
474
|
+
this.feature = feature;
|
|
475
|
+
this.dialect = dialect;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
exports.UnsupportedFeatureError = UnsupportedFeatureError;
|
|
461
479
|
/**
|
|
462
480
|
* Parse column names out of a pg `detail` string like:
|
|
463
481
|
* "Key (email)=(foo@bar) already exists."
|
package/dist/cjs/generate.js
CHANGED
|
@@ -363,6 +363,10 @@ function generateMetadata(schema) {
|
|
|
363
363
|
lines.push(' },');
|
|
364
364
|
lines.push('};');
|
|
365
365
|
lines.push('');
|
|
366
|
+
// Back-compat lowercase alias. `SCHEMA` is the canonical export, but docs and
|
|
367
|
+
// users frequently import `schema`; emit both so either name resolves.
|
|
368
|
+
lines.push('export const schema = SCHEMA;');
|
|
369
|
+
lines.push('');
|
|
366
370
|
return lines.join('\n');
|
|
367
371
|
}
|
|
368
372
|
// ---------------------------------------------------------------------------
|
package/dist/cjs/index.js
CHANGED
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
-
exports.
|
|
38
|
-
exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.table = exports.defineSchema = void 0;
|
|
37
|
+
exports.ColumnBuilder = exports.applyManyToManyRelations = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = exports.isDateType = exports.camelToSnake = exports.validateChannel = exports.QueryInterface = exports.pipelineSupported = exports.executePipeline = exports.hasRelationFields = exports.executeNestedUpdate = exports.executeNestedCreate = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UnsupportedFeatureError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.PipelineError = exports.OptimisticLockError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.ExclusionConstraintError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.postgresDialect = exports.withRetry = exports.TurbineClient = exports.TransactionClient = exports.yugabytedb = exports.timescale = exports.postgresql = exports.cockroachdb = exports.alloydb = void 0;
|
|
38
|
+
exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.table = exports.defineSchema = exports.column = void 0;
|
|
39
39
|
var index_js_1 = require("./adapters/index.js");
|
|
40
40
|
Object.defineProperty(exports, "alloydb", { enumerable: true, get: function () { return index_js_1.alloydb; } });
|
|
41
41
|
Object.defineProperty(exports, "cockroachdb", { enumerable: true, get: function () { return index_js_1.cockroachdb; } });
|
|
@@ -70,6 +70,7 @@ Object.defineProperty(exports, "TimeoutError", { enumerable: true, get: function
|
|
|
70
70
|
Object.defineProperty(exports, "TurbineError", { enumerable: true, get: function () { return errors_js_1.TurbineError; } });
|
|
71
71
|
Object.defineProperty(exports, "TurbineErrorCode", { enumerable: true, get: function () { return errors_js_1.TurbineErrorCode; } });
|
|
72
72
|
Object.defineProperty(exports, "UniqueConstraintError", { enumerable: true, get: function () { return errors_js_1.UniqueConstraintError; } });
|
|
73
|
+
Object.defineProperty(exports, "UnsupportedFeatureError", { enumerable: true, get: function () { return errors_js_1.UnsupportedFeatureError; } });
|
|
73
74
|
Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return errors_js_1.ValidationError; } });
|
|
74
75
|
Object.defineProperty(exports, "wrapPgError", { enumerable: true, get: function () { return errors_js_1.wrapPgError; } });
|
|
75
76
|
// Code generation
|
package/dist/cjs/introspect.js
CHANGED
|
@@ -13,6 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.introspect = introspect;
|
|
16
|
+
exports.introspectPostgresCatalog = introspectPostgresCatalog;
|
|
16
17
|
const pg_1 = __importDefault(require("pg"));
|
|
17
18
|
const dialect_js_1 = require("./dialect.js");
|
|
18
19
|
const schema_js_1 = require("./schema.js");
|
|
@@ -100,7 +101,26 @@ const SQL_ENUMS = `
|
|
|
100
101
|
// ---------------------------------------------------------------------------
|
|
101
102
|
// Main introspection function
|
|
102
103
|
// ---------------------------------------------------------------------------
|
|
104
|
+
/**
|
|
105
|
+
* Introspect a database into {@link SchemaMetadata}, routing through the active
|
|
106
|
+
* dialect's {@link Dialect.introspector} so each engine can override the catalog
|
|
107
|
+
* SQL. PostgreSQL is driven by {@link introspectPostgresCatalog}.
|
|
108
|
+
*/
|
|
103
109
|
async function introspect(options) {
|
|
110
|
+
const dialect = options.dialect ?? dialect_js_1.postgresDialect;
|
|
111
|
+
const introspector = dialect.introspector;
|
|
112
|
+
if (introspector) {
|
|
113
|
+
return introspector.introspect(options);
|
|
114
|
+
}
|
|
115
|
+
// Dialects without an introspector fall back to the Postgres catalog reader.
|
|
116
|
+
return introspectPostgresCatalog(options);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* PostgreSQL catalog introspector: reads information_schema + pg_catalog and
|
|
120
|
+
* produces {@link SchemaMetadata}. This is the implementation wrapped by
|
|
121
|
+
* `postgresDialect.introspector`; call {@link introspect} for dialect routing.
|
|
122
|
+
*/
|
|
123
|
+
async function introspectPostgresCatalog(options) {
|
|
104
124
|
const schema = options.schema ?? 'public';
|
|
105
125
|
const dialect = dialect_js_1.postgresDialect;
|
|
106
126
|
const pool = new pg_1.default.Pool({
|