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/cli/studio.d.ts
CHANGED
|
@@ -79,14 +79,4 @@ export declare function apiBuilder(req: IncomingMessage, res: ServerResponse, ct
|
|
|
79
79
|
export declare function apiListSavedQueries(res: ServerResponse, ctx: StudioContext, params: URLSearchParams): void;
|
|
80
80
|
export declare function apiCreateSavedQuery(req: IncomingMessage, res: ServerResponse, ctx: StudioContext): Promise<void>;
|
|
81
81
|
export declare function apiDeleteSavedQuery(res: ServerResponse, ctx: StudioContext, id: string): void;
|
|
82
|
-
/**
|
|
83
|
-
* Accept only SELECT or WITH (CTE) statements. Reject any statement that
|
|
84
|
-
* contains a semicolon followed by non-whitespace (prevents statement
|
|
85
|
-
* stacking), and require the first non-comment keyword to be SELECT or WITH.
|
|
86
|
-
*
|
|
87
|
-
* This is a first-line filter — the transaction's READ ONLY mode is the
|
|
88
|
-
* second line of defense. Both must fail before a destructive statement
|
|
89
|
-
* could run.
|
|
90
|
-
*/
|
|
91
|
-
export declare function isReadOnlyStatement(sql: string): boolean;
|
|
92
82
|
//# sourceMappingURL=studio.d.ts.map
|
package/dist/cli/studio.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* Studio is for inspection. Use the CLI, migrate, or raw SQL for writes.
|
|
22
22
|
*/
|
|
23
23
|
import { spawn } from 'node:child_process';
|
|
24
|
-
import { randomBytes, randomUUID } from 'node:crypto';
|
|
24
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
25
25
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
26
26
|
import { createServer } from 'node:http';
|
|
27
27
|
import { platform } from 'node:os';
|
|
@@ -223,13 +223,12 @@ function checkRateLimit(limiter, token) {
|
|
|
223
223
|
return { allowed: true, resetAt: entry.resetAt };
|
|
224
224
|
}
|
|
225
225
|
function constantTimeEqual(a, b) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return result === 0;
|
|
226
|
+
// Hash both inputs to fixed-length 32-byte SHA-256 digests before comparing.
|
|
227
|
+
// This makes the comparison constant-length (timingSafeEqual never throws on a
|
|
228
|
+
// length mismatch) and leaks neither length nor content via timing.
|
|
229
|
+
const ah = createHash('sha256').update(a).digest();
|
|
230
|
+
const bh = createHash('sha256').update(b).digest();
|
|
231
|
+
return timingSafeEqual(ah, bh);
|
|
233
232
|
}
|
|
234
233
|
/**
|
|
235
234
|
* Build a helpful "unknown table" error that lists the available tables so the
|
|
@@ -535,35 +534,6 @@ function clampInt(value, fallback, min, max) {
|
|
|
535
534
|
return fallback;
|
|
536
535
|
return Math.min(Math.max(n, min), max);
|
|
537
536
|
}
|
|
538
|
-
/**
|
|
539
|
-
* Accept only SELECT or WITH (CTE) statements. Reject any statement that
|
|
540
|
-
* contains a semicolon followed by non-whitespace (prevents statement
|
|
541
|
-
* stacking), and require the first non-comment keyword to be SELECT or WITH.
|
|
542
|
-
*
|
|
543
|
-
* This is a first-line filter — the transaction's READ ONLY mode is the
|
|
544
|
-
* second line of defense. Both must fail before a destructive statement
|
|
545
|
-
* could run.
|
|
546
|
-
*/
|
|
547
|
-
export function isReadOnlyStatement(sql) {
|
|
548
|
-
const stripped = stripSqlComments(sql).trim();
|
|
549
|
-
if (!stripped)
|
|
550
|
-
return false;
|
|
551
|
-
// Disallow statement stacking. A single trailing `;` is fine.
|
|
552
|
-
const withoutTrailingSemi = stripped.replace(/;+\s*$/, '');
|
|
553
|
-
if (withoutTrailingSemi.includes(';'))
|
|
554
|
-
return false;
|
|
555
|
-
const firstWord = withoutTrailingSemi.slice(0, 6).toUpperCase();
|
|
556
|
-
if (firstWord.startsWith('SELECT'))
|
|
557
|
-
return true;
|
|
558
|
-
if (firstWord.startsWith('WITH'))
|
|
559
|
-
return true;
|
|
560
|
-
return false;
|
|
561
|
-
}
|
|
562
|
-
function stripSqlComments(sql) {
|
|
563
|
-
// Strip -- line comments and /* block comments */. Not a full SQL parser,
|
|
564
|
-
// but sufficient to catch the common bypass attempts.
|
|
565
|
-
return sql.replace(/--[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
566
|
-
}
|
|
567
537
|
function serializeRow(row) {
|
|
568
538
|
const out = {};
|
|
569
539
|
for (const [k, v] of Object.entries(row)) {
|
package/dist/client.d.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
import pg from 'pg';
|
|
25
|
-
import type
|
|
25
|
+
import { type Dialect } from './dialect.js';
|
|
26
26
|
import { type ErrorMessageMode } from './errors.js';
|
|
27
27
|
import { type ObserveConfig, type ObserveHandle } from './observe.js';
|
|
28
28
|
import { type PipelineOptions, type PipelineResults } from './pipeline.js';
|
|
@@ -83,6 +83,24 @@ export interface PgCompatPool {
|
|
|
83
83
|
/** Optional — pg.Pool supports 'error' event; HTTP drivers typically do not */
|
|
84
84
|
on?(event: 'error', listener: (err: Error) => void): this;
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Driver-neutral seam. Bundles a pg-compatible connection pool with the SQL
|
|
88
|
+
* {@link Dialect} that owns every piece of SQL text varying across engines —
|
|
89
|
+
* parameter placeholders, transaction-control keywords (BEGIN/COMMIT/ROLLBACK/
|
|
90
|
+
* SAVEPOINT/isolation/set_config), streaming, and capability flags.
|
|
91
|
+
*
|
|
92
|
+
* This is the structural boundary that keeps hard-coded Postgres SQL out of
|
|
93
|
+
* `client.ts`: the pool provides connect/query/transaction/close, the dialect
|
|
94
|
+
* provides every literal keyword and the placeholder syntax. A future MySQL /
|
|
95
|
+
* SQLite engine ships a `{ pool, dialect }` pair instead of a raw `pg.Pool`,
|
|
96
|
+
* exactly as `turbineHttp` ships a serverless pool today.
|
|
97
|
+
*/
|
|
98
|
+
export interface TurbineDriver {
|
|
99
|
+
/** The underlying pg-compatible connection pool (connect/query/transaction/close). */
|
|
100
|
+
readonly pool: PgCompatPool;
|
|
101
|
+
/** SQL dialect: placeholders, transaction keywords, session-config, capability flags. */
|
|
102
|
+
readonly dialect: Dialect;
|
|
103
|
+
}
|
|
86
104
|
export interface TurbineConfig {
|
|
87
105
|
/**
|
|
88
106
|
* An external pg-compatible pool. Use this to plug in serverless drivers
|
|
@@ -211,6 +229,8 @@ export declare class TransactionClient {
|
|
|
211
229
|
private readonly queryOptions?;
|
|
212
230
|
private readonly tableCache;
|
|
213
231
|
private savepointCounter;
|
|
232
|
+
/** Active SQL dialect — owns savepoint keywords and raw-SQL placeholders. */
|
|
233
|
+
private readonly dialect;
|
|
214
234
|
constructor(client: pg.PoolClient, schema: SchemaMetadata, middlewares: Middleware[], queryOptions?: QueryInterfaceOptions | undefined);
|
|
215
235
|
/**
|
|
216
236
|
* Get a QueryInterface for a table within this transaction.
|
|
@@ -244,6 +264,8 @@ export declare class TurbineClient {
|
|
|
244
264
|
readonly schema: SchemaMetadata;
|
|
245
265
|
private static int8ParserRegistered;
|
|
246
266
|
private readonly logging;
|
|
267
|
+
/** Active SQL dialect — owns transaction keywords, set_config, raw-SQL placeholders, capability flags. */
|
|
268
|
+
private readonly dialect;
|
|
247
269
|
private readonly tableCache;
|
|
248
270
|
private readonly middlewares;
|
|
249
271
|
private readonly queryListeners;
|
|
@@ -255,12 +277,14 @@ export declare class TurbineClient {
|
|
|
255
277
|
private readonly activeSubscriptions;
|
|
256
278
|
constructor(config: TurbineConfig | undefined, schema: SchemaMetadata);
|
|
257
279
|
/**
|
|
258
|
-
* Register a middleware function that runs
|
|
280
|
+
* Register a middleware function that runs around every query.
|
|
259
281
|
*
|
|
260
|
-
* Middleware can inspect and log query parameters,
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
282
|
+
* Middleware can inspect and log query parameters, measure timing, and
|
|
283
|
+
* transform the result returned by `next()`. Note: query SQL is generated
|
|
284
|
+
* BEFORE middleware runs — `params.args` is a read-only snapshot, and
|
|
285
|
+
* mutating it does NOT change the executed SQL. Cross-cutting filters
|
|
286
|
+
* (e.g. soft deletes) belong in the query itself: pass an explicit
|
|
287
|
+
* `where: { deletedAt: null }` or wrap the table accessor in a small helper.
|
|
264
288
|
*
|
|
265
289
|
* @example
|
|
266
290
|
* ```ts
|
|
@@ -272,16 +296,13 @@ export declare class TurbineClient {
|
|
|
272
296
|
* return result;
|
|
273
297
|
* });
|
|
274
298
|
*
|
|
275
|
-
* //
|
|
299
|
+
* // Result transformation middleware — redact a field on the way out
|
|
276
300
|
* db.$use(async (params, next) => {
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
* if (params.action === 'delete') {
|
|
281
|
-
* params.action = 'update';
|
|
282
|
-
* params.args = { where: params.args.where, data: { deletedAt: new Date() } };
|
|
301
|
+
* const result = await next(params);
|
|
302
|
+
* if (params.model === 'users' && Array.isArray(result)) {
|
|
303
|
+
* for (const row of result as { email?: string }[]) row.email = '[redacted]';
|
|
283
304
|
* }
|
|
284
|
-
* return
|
|
305
|
+
* return result;
|
|
285
306
|
* });
|
|
286
307
|
* ```
|
|
287
308
|
*/
|
package/dist/client.js
CHANGED
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
import pg from 'pg';
|
|
25
|
-
import {
|
|
25
|
+
import { postgresDialect } from './dialect.js';
|
|
26
|
+
import { setErrorMessageMode, TimeoutError, UnsupportedFeatureError, ValidationError, wrapPgError, } from './errors.js';
|
|
26
27
|
import { ObserveEngine } from './observe.js';
|
|
27
28
|
import { executePipeline, pipelineSupported } from './pipeline.js';
|
|
28
29
|
import { QueryInterface, } from './query/index.js';
|
|
@@ -82,11 +83,14 @@ export class TransactionClient {
|
|
|
82
83
|
queryOptions;
|
|
83
84
|
tableCache = new Map();
|
|
84
85
|
savepointCounter = 0;
|
|
86
|
+
/** Active SQL dialect — owns savepoint keywords and raw-SQL placeholders. */
|
|
87
|
+
dialect;
|
|
85
88
|
constructor(client, schema, middlewares, queryOptions) {
|
|
86
89
|
this.client = client;
|
|
87
90
|
this.schema = schema;
|
|
88
91
|
this.middlewares = middlewares;
|
|
89
92
|
this.queryOptions = queryOptions;
|
|
93
|
+
this.dialect = queryOptions?.dialect ?? postgresDialect;
|
|
90
94
|
// Auto-create typed table accessors for all tables in the schema
|
|
91
95
|
for (const tableName of Object.keys(schema.tables)) {
|
|
92
96
|
const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
@@ -120,14 +124,14 @@ export class TransactionClient {
|
|
|
120
124
|
*/
|
|
121
125
|
async $transaction(fn) {
|
|
122
126
|
const savepointName = `sp_${++this.savepointCounter}`;
|
|
123
|
-
await this.client.query(
|
|
127
|
+
await this.client.query(this.dialect.savepointStatement(savepointName));
|
|
124
128
|
try {
|
|
125
129
|
const result = await fn(this);
|
|
126
|
-
await this.client.query(
|
|
130
|
+
await this.client.query(this.dialect.releaseSavepointStatement(savepointName));
|
|
127
131
|
return result;
|
|
128
132
|
}
|
|
129
133
|
catch (err) {
|
|
130
|
-
await this.client.query(
|
|
134
|
+
await this.client.query(this.dialect.rollbackToSavepointStatement(savepointName));
|
|
131
135
|
throw err;
|
|
132
136
|
}
|
|
133
137
|
}
|
|
@@ -139,7 +143,7 @@ export class TransactionClient {
|
|
|
139
143
|
strings.forEach((str, i) => {
|
|
140
144
|
sql += str;
|
|
141
145
|
if (i < values.length) {
|
|
142
|
-
sql +=
|
|
146
|
+
sql += this.dialect.paramPlaceholder(i + 1);
|
|
143
147
|
}
|
|
144
148
|
});
|
|
145
149
|
try {
|
|
@@ -192,6 +196,8 @@ export class TurbineClient {
|
|
|
192
196
|
schema;
|
|
193
197
|
static int8ParserRegistered = false;
|
|
194
198
|
logging;
|
|
199
|
+
/** Active SQL dialect — owns transaction keywords, set_config, raw-SQL placeholders, capability flags. */
|
|
200
|
+
dialect;
|
|
195
201
|
tableCache = new Map();
|
|
196
202
|
middlewares = [];
|
|
197
203
|
queryListeners = new Set();
|
|
@@ -237,6 +243,7 @@ export class TurbineClient {
|
|
|
237
243
|
TurbineClient.int8ParserRegistered = true;
|
|
238
244
|
}
|
|
239
245
|
this.logging = config.logging ?? false;
|
|
246
|
+
this.dialect = config.dialect ?? postgresDialect;
|
|
240
247
|
this.schema = schema;
|
|
241
248
|
// Respect env var kill switch
|
|
242
249
|
const envDisablePrepared = typeof process !== 'undefined' && process.env?.TURBINE_DISABLE_PREPARED === '1';
|
|
@@ -323,12 +330,14 @@ export class TurbineClient {
|
|
|
323
330
|
// Middleware — intercept all queries
|
|
324
331
|
// -------------------------------------------------------------------------
|
|
325
332
|
/**
|
|
326
|
-
* Register a middleware function that runs
|
|
333
|
+
* Register a middleware function that runs around every query.
|
|
327
334
|
*
|
|
328
|
-
* Middleware can inspect and log query parameters,
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
335
|
+
* Middleware can inspect and log query parameters, measure timing, and
|
|
336
|
+
* transform the result returned by `next()`. Note: query SQL is generated
|
|
337
|
+
* BEFORE middleware runs — `params.args` is a read-only snapshot, and
|
|
338
|
+
* mutating it does NOT change the executed SQL. Cross-cutting filters
|
|
339
|
+
* (e.g. soft deletes) belong in the query itself: pass an explicit
|
|
340
|
+
* `where: { deletedAt: null }` or wrap the table accessor in a small helper.
|
|
332
341
|
*
|
|
333
342
|
* @example
|
|
334
343
|
* ```ts
|
|
@@ -340,16 +349,13 @@ export class TurbineClient {
|
|
|
340
349
|
* return result;
|
|
341
350
|
* });
|
|
342
351
|
*
|
|
343
|
-
* //
|
|
352
|
+
* // Result transformation middleware — redact a field on the way out
|
|
344
353
|
* db.$use(async (params, next) => {
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
* if (params.action === 'delete') {
|
|
349
|
-
* params.action = 'update';
|
|
350
|
-
* params.args = { where: params.args.where, data: { deletedAt: new Date() } };
|
|
354
|
+
* const result = await next(params);
|
|
355
|
+
* if (params.model === 'users' && Array.isArray(result)) {
|
|
356
|
+
* for (const row of result as { email?: string }[]) row.email = '[redacted]';
|
|
351
357
|
* }
|
|
352
|
-
* return
|
|
358
|
+
* return result;
|
|
353
359
|
* });
|
|
354
360
|
* ```
|
|
355
361
|
*/
|
|
@@ -466,7 +472,7 @@ export class TurbineClient {
|
|
|
466
472
|
strings.forEach((str, i) => {
|
|
467
473
|
sql += str;
|
|
468
474
|
if (i < values.length) {
|
|
469
|
-
sql +=
|
|
475
|
+
sql += this.dialect.paramPlaceholder(i + 1);
|
|
470
476
|
}
|
|
471
477
|
});
|
|
472
478
|
if (this.logging) {
|
|
@@ -511,7 +517,7 @@ export class TurbineClient {
|
|
|
511
517
|
* ```
|
|
512
518
|
*/
|
|
513
519
|
sql(strings, ...values) {
|
|
514
|
-
const { sql, params } = buildTypedSql(strings, values);
|
|
520
|
+
const { sql, params } = buildTypedSql(strings, values, this.dialect);
|
|
515
521
|
return new TypedSqlQuery(this.pool, sql, params, this.logging);
|
|
516
522
|
}
|
|
517
523
|
// -------------------------------------------------------------------------
|
|
@@ -531,13 +537,13 @@ export class TurbineClient {
|
|
|
531
537
|
async transaction(fn) {
|
|
532
538
|
const client = await this.pool.connect();
|
|
533
539
|
try {
|
|
534
|
-
await client.query(
|
|
540
|
+
await client.query(this.dialect.beginStatement());
|
|
535
541
|
const result = await fn(client);
|
|
536
|
-
await client.query(
|
|
542
|
+
await client.query(this.dialect.commitStatement());
|
|
537
543
|
return result;
|
|
538
544
|
}
|
|
539
545
|
catch (err) {
|
|
540
|
-
await client.query(
|
|
546
|
+
await client.query(this.dialect.rollbackStatement());
|
|
541
547
|
throw err;
|
|
542
548
|
}
|
|
543
549
|
finally {
|
|
@@ -588,14 +594,10 @@ export class TurbineClient {
|
|
|
588
594
|
};
|
|
589
595
|
let timedOut = false;
|
|
590
596
|
try {
|
|
591
|
-
// BEGIN with optional isolation level
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
if (level)
|
|
596
|
-
beginSQL += ` ISOLATION LEVEL ${level}`;
|
|
597
|
-
}
|
|
598
|
-
await client.query(beginSQL);
|
|
597
|
+
// BEGIN with optional isolation level — the dialect owns the keyword and
|
|
598
|
+
// BEGIN+isolation composition (Postgres appends ` ISOLATION LEVEL …`).
|
|
599
|
+
const isolationSql = options?.isolationLevel ? ISOLATION_LEVELS[options.isolationLevel] : undefined;
|
|
600
|
+
await client.query(this.dialect.beginStatement(isolationSql));
|
|
599
601
|
// Apply transaction-local session context (RLS / multi-tenant GUCs).
|
|
600
602
|
// Order matters: BEGIN -> isolation level (above) -> set_config loop ->
|
|
601
603
|
// user fn. Any error here propagates to the catch below and rolls back
|
|
@@ -603,12 +605,16 @@ export class TurbineClient {
|
|
|
603
605
|
// is_local=true) — the parameterizable, transaction-scoped equivalent of
|
|
604
606
|
// SET LOCAL — so both name and value are BOUND params, never interpolated.
|
|
605
607
|
if (options?.sessionContext) {
|
|
608
|
+
if (!this.dialect.supportsRLS) {
|
|
609
|
+
throw new UnsupportedFeatureError('sessionContext (RLS session GUCs)', this.dialect.name, 'set_config-based row-level-security context requires PostgreSQL.');
|
|
610
|
+
}
|
|
606
611
|
for (const [name, value] of Object.entries(options.sessionContext)) {
|
|
607
612
|
if (!GUC_NAME_REGEX.test(name)) {
|
|
608
613
|
throw new ValidationError(`[turbine] Invalid session-context GUC name "${name}" — must match ` +
|
|
609
614
|
'/^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$/ (optionally namespaced, e.g. "app.current_tenant")');
|
|
610
615
|
}
|
|
611
|
-
|
|
616
|
+
const cfg = this.dialect.buildSetSessionConfig(name, String(value));
|
|
617
|
+
await client.query(cfg.sql, cfg.params);
|
|
612
618
|
}
|
|
613
619
|
}
|
|
614
620
|
// Create the transaction client with typed table accessors
|
|
@@ -654,7 +660,7 @@ export class TurbineClient {
|
|
|
654
660
|
else {
|
|
655
661
|
result = await fn(tx);
|
|
656
662
|
}
|
|
657
|
-
await client.query(
|
|
663
|
+
await client.query(this.dialect.commitStatement());
|
|
658
664
|
if (this.logging) {
|
|
659
665
|
console.log('[turbine] Transaction committed');
|
|
660
666
|
}
|
|
@@ -667,7 +673,7 @@ export class TurbineClient {
|
|
|
667
673
|
// when its socket was closed).
|
|
668
674
|
if (!timedOut && !released) {
|
|
669
675
|
try {
|
|
670
|
-
await client.query(
|
|
676
|
+
await client.query(this.dialect.rollbackStatement());
|
|
671
677
|
}
|
|
672
678
|
catch {
|
|
673
679
|
// Best-effort rollback — the connection may have died mid-query.
|
|
@@ -734,6 +740,9 @@ export class TurbineClient {
|
|
|
734
740
|
* ```
|
|
735
741
|
*/
|
|
736
742
|
async $listen(channel, handler) {
|
|
743
|
+
if (!this.dialect.supportsListenNotify) {
|
|
744
|
+
throw new UnsupportedFeatureError('$listen (LISTEN/NOTIFY realtime)', this.dialect.name, 'Realtime pub/sub requires PostgreSQL.');
|
|
745
|
+
}
|
|
737
746
|
validateChannel(channel);
|
|
738
747
|
const quoted = quoteIdent(channel);
|
|
739
748
|
if (this.logging) {
|
|
@@ -759,6 +768,9 @@ export class TurbineClient {
|
|
|
759
768
|
* ```
|
|
760
769
|
*/
|
|
761
770
|
async $notify(channel, payload) {
|
|
771
|
+
if (!this.dialect.supportsListenNotify) {
|
|
772
|
+
throw new UnsupportedFeatureError('$notify (LISTEN/NOTIFY realtime)', this.dialect.name, 'Realtime pub/sub requires PostgreSQL.');
|
|
773
|
+
}
|
|
762
774
|
validateChannel(channel);
|
|
763
775
|
if (this.logging) {
|
|
764
776
|
console.log(`[turbine] NOTIFY ${channel}`);
|