turbine-orm 0.7.0 → 0.8.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 +134 -40
- package/dist/cjs/cli/index.js +72 -3
- package/dist/cjs/cli/loader.js +129 -0
- package/dist/cjs/cli/migrate.js +33 -9
- package/dist/cjs/client.js +92 -8
- package/dist/cjs/errors.js +177 -4
- package/dist/cjs/generate.js +120 -9
- package/dist/cjs/index.js +7 -1
- package/dist/cjs/pipeline-submittable.js +403 -0
- package/dist/cjs/pipeline.js +90 -37
- package/dist/cjs/query.js +943 -137
- package/dist/cjs/schema-builder.js +57 -6
- package/dist/cjs/schema-sql.js +85 -19
- package/dist/cjs/serverless.js +8 -7
- package/dist/cli/index.js +72 -3
- package/dist/cli/loader.d.ts +45 -0
- package/dist/cli/loader.js +91 -0
- package/dist/cli/migrate.d.ts +7 -1
- package/dist/cli/migrate.js +33 -9
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +47 -3
- package/dist/client.js +94 -10
- package/dist/errors.d.ts +132 -1
- package/dist/errors.js +171 -3
- package/dist/generate.d.ts +6 -0
- package/dist/generate.js +120 -10
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/pipeline-submittable.d.ts +94 -0
- package/dist/pipeline-submittable.js +397 -0
- package/dist/pipeline.d.ts +37 -9
- package/dist/pipeline.js +89 -37
- package/dist/query.d.ts +268 -17
- package/dist/query.js +941 -137
- package/dist/schema-builder.d.ts +36 -3
- package/dist/schema-builder.js +57 -6
- package/dist/schema-sql.js +85 -19
- package/dist/serverless.d.ts +8 -7
- package/dist/serverless.js +8 -7
- package/package.json +3 -3
package/dist/cli/migrate.js
CHANGED
|
@@ -260,12 +260,19 @@ async function validateChecksums(client, migrationsDir) {
|
|
|
260
260
|
* Features:
|
|
261
261
|
* - Idempotent: running twice is safe (already-applied migrations are skipped)
|
|
262
262
|
* - Advisory lock: prevents concurrent migration runs
|
|
263
|
-
* - Checksum validation: detects modified migration files
|
|
263
|
+
* - Checksum validation: detects modified migration files (BLOCKING — use
|
|
264
|
+
* `allowDrift: true` to bypass when intentionally rewriting history)
|
|
264
265
|
* - Each migration runs in its own transaction
|
|
266
|
+
*
|
|
267
|
+
* Throws `MigrationError` if any applied migration has been modified or deleted
|
|
268
|
+
* on disk, listing the offending files. Pass `{ allowDrift: true }` to bypass
|
|
269
|
+
* this check (the CLI exposes this as `--allow-drift`).
|
|
265
270
|
*/
|
|
266
271
|
export async function migrateUp(connectionString, migrationsDir, options) {
|
|
267
272
|
const client = new pg.Client({ connectionString });
|
|
268
273
|
await client.connect();
|
|
274
|
+
// Treat `force` as an alias for `allowDrift` for backwards compatibility.
|
|
275
|
+
const allowDrift = options?.allowDrift === true || options?.force === true;
|
|
269
276
|
try {
|
|
270
277
|
// Acquire advisory lock to prevent concurrent migrations
|
|
271
278
|
const gotLock = await acquireLock(client);
|
|
@@ -274,18 +281,35 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
274
281
|
}
|
|
275
282
|
try {
|
|
276
283
|
await ensureTrackingTable(client);
|
|
277
|
-
// Validate checksums of already-applied migrations
|
|
278
|
-
|
|
284
|
+
// Validate checksums of already-applied migrations.
|
|
285
|
+
// Drift = an APPLIED migration's on-disk file has changed (or been deleted)
|
|
286
|
+
// since it was run. Either situation means the database state and the
|
|
287
|
+
// migration history no longer agree, so we BLOCK the run by default.
|
|
288
|
+
// Users can pass `allowDrift: true` (CLI: `--allow-drift`) to force past
|
|
289
|
+
// the block when they are intentionally rewriting history.
|
|
290
|
+
if (!allowDrift) {
|
|
279
291
|
const mismatches = await validateChecksums(client, migrationsDir);
|
|
280
292
|
if (mismatches.length > 0) {
|
|
281
293
|
const modified = mismatches.filter((m) => m.type === 'modified');
|
|
282
294
|
const missing = mismatches.filter((m) => m.type === 'missing');
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
295
|
+
const lines = [
|
|
296
|
+
'[turbine] Migration drift detected — refusing to apply pending migrations.',
|
|
297
|
+
'',
|
|
298
|
+
'Applied migrations should be immutable. The following files no longer match their applied state:',
|
|
299
|
+
'',
|
|
300
|
+
];
|
|
301
|
+
for (const m of modified) {
|
|
302
|
+
lines.push(` - ${m.name}.sql (modified on disk)`);
|
|
303
|
+
}
|
|
304
|
+
for (const m of missing) {
|
|
305
|
+
lines.push(` - ${m.name}.sql (deleted from disk)`);
|
|
306
|
+
}
|
|
307
|
+
lines.push('');
|
|
308
|
+
lines.push('Fix one of these:');
|
|
309
|
+
lines.push(' 1. Restore the file(s) to their original content, OR');
|
|
310
|
+
lines.push(' 2. Roll back the affected migrations with `npx turbine migrate down`, OR');
|
|
311
|
+
lines.push(' 3. Pass `--allow-drift` to bypass this check (advanced — make sure you know what you are doing).');
|
|
312
|
+
throw new MigrationError(lines.join('\n'));
|
|
289
313
|
}
|
|
290
314
|
}
|
|
291
315
|
const applied = await getAppliedMigrations(client);
|
package/dist/cli/ui.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export declare const symbols: {
|
|
|
34
34
|
readonly warning: "⚠" | "!";
|
|
35
35
|
readonly dot: "." | "∙";
|
|
36
36
|
readonly line: "─" | "-";
|
|
37
|
-
readonly vertLine: "
|
|
37
|
+
readonly vertLine: "|" | "│";
|
|
38
38
|
readonly topLeft: "╭" | "+";
|
|
39
39
|
readonly topRight: "+" | "╮";
|
|
40
40
|
readonly bottomLeft: "+" | "╰";
|
package/dist/client.d.ts
CHANGED
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
import pg from 'pg';
|
|
25
|
-
import { type
|
|
25
|
+
import { type ErrorMessageMode } from './errors.js';
|
|
26
|
+
import { type PipelineOptions, type PipelineResults } from './pipeline.js';
|
|
26
27
|
import { type DeferredQuery, QueryInterface, type QueryInterfaceOptions } from './query.js';
|
|
27
28
|
import type { SchemaMetadata } from './schema.js';
|
|
28
29
|
/**
|
|
@@ -110,6 +111,37 @@ export interface TurbineConfig {
|
|
|
110
111
|
defaultLimit?: number;
|
|
111
112
|
/** Log a warning when findMany() is called without a limit (default: false) */
|
|
112
113
|
warnOnUnlimited?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Controls how `NotFoundError` (and other where-aware errors) format their
|
|
116
|
+
* messages.
|
|
117
|
+
*
|
|
118
|
+
* - `'safe'` (default): the message includes only the keys of the where
|
|
119
|
+
* clause (e.g. `where: { id, email }`). Values are redacted to avoid
|
|
120
|
+
* leaking PII into error logs (Sentry, Datadog, etc.).
|
|
121
|
+
* - `'verbose'`: the message includes the full JSON-serialized where
|
|
122
|
+
* clause (e.g. `where: {"id":1,"email":"alice@x.com"}`).
|
|
123
|
+
*
|
|
124
|
+
* The full `where` object is always available as `err.where` for
|
|
125
|
+
* programmatic access regardless of mode.
|
|
126
|
+
*/
|
|
127
|
+
errorMessages?: ErrorMessageMode;
|
|
128
|
+
/**
|
|
129
|
+
* Enable prepared statements. Queries are submitted with `{ name, text, values }`
|
|
130
|
+
* to the pg driver, which caches the parse+plan on the server per connection.
|
|
131
|
+
*
|
|
132
|
+
* Default: `true` for Turbine-owned pools, `false` for external pools (serverless
|
|
133
|
+
* drivers may not support named statements).
|
|
134
|
+
*
|
|
135
|
+
* Override with `TURBINE_DISABLE_PREPARED=1` env var.
|
|
136
|
+
*/
|
|
137
|
+
preparedStatements?: boolean;
|
|
138
|
+
/**
|
|
139
|
+
* Enable the SQL template cache. Repeated queries with the same shape reuse
|
|
140
|
+
* cached SQL text instead of rebuilding from scratch.
|
|
141
|
+
*
|
|
142
|
+
* Default: `true`. Set to `false` as a nuclear kill switch.
|
|
143
|
+
*/
|
|
144
|
+
sqlCache?: boolean;
|
|
113
145
|
}
|
|
114
146
|
/** Parameters passed to middleware functions */
|
|
115
147
|
export interface MiddlewareParams {
|
|
@@ -221,9 +253,21 @@ export declare class TurbineClient {
|
|
|
221
253
|
/**
|
|
222
254
|
* Execute multiple queries in a single database round-trip.
|
|
223
255
|
*
|
|
224
|
-
*
|
|
256
|
+
* Two call styles:
|
|
257
|
+
* - `db.pipeline(q1, q2, q3)` — rest params (backward-compatible)
|
|
258
|
+
* - `db.pipeline([q1, q2, q3], { transactional: false })` — array + options
|
|
259
|
+
*
|
|
260
|
+
* On pg.Pool-backed connections with TCP, this uses the real Postgres
|
|
261
|
+
* extended-query pipeline protocol (one TCP flush, one round-trip).
|
|
262
|
+
* On HTTP-based drivers it falls back to sequential execution.
|
|
263
|
+
*/
|
|
264
|
+
pipeline<T extends readonly DeferredQuery<unknown>[]>(...args: T | [T, PipelineOptions?]): Promise<PipelineResults<T>>;
|
|
265
|
+
/**
|
|
266
|
+
* Check whether the underlying pool supports the real pipeline protocol.
|
|
267
|
+
* Returns `true` for standard pg.Pool TCP connections, `false` for HTTP
|
|
268
|
+
* drivers (Neon HTTP, Vercel Postgres, etc.) and mock pools.
|
|
225
269
|
*/
|
|
226
|
-
|
|
270
|
+
pipelineSupported(): Promise<boolean>;
|
|
227
271
|
/**
|
|
228
272
|
* Execute a raw SQL query with parameter interpolation via tagged templates.
|
|
229
273
|
*
|
package/dist/client.js
CHANGED
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
import pg from 'pg';
|
|
25
|
-
import { TimeoutError, wrapPgError } from './errors.js';
|
|
26
|
-
import { executePipeline } from './pipeline.js';
|
|
25
|
+
import { setErrorMessageMode, TimeoutError, wrapPgError } from './errors.js';
|
|
26
|
+
import { executePipeline, pipelineSupported } from './pipeline.js';
|
|
27
27
|
import { QueryInterface } from './query.js';
|
|
28
28
|
/** Maps isolation level names to SQL */
|
|
29
29
|
const ISOLATION_LEVELS = {
|
|
@@ -128,9 +128,15 @@ export class TransactionClient {
|
|
|
128
128
|
// Return a minimal pool-compatible object that routes queries
|
|
129
129
|
// through the transaction client
|
|
130
130
|
return {
|
|
131
|
-
query: async (
|
|
131
|
+
query: async (textOrConfig, values) => {
|
|
132
132
|
try {
|
|
133
|
-
|
|
133
|
+
if (typeof textOrConfig === 'string') {
|
|
134
|
+
return await client.query(textOrConfig, values);
|
|
135
|
+
}
|
|
136
|
+
// Object form for prepared statements: { name, text, values }
|
|
137
|
+
// pg.PoolClient.query accepts QueryConfig but the overloads make TS
|
|
138
|
+
// unhappy with the union, so we cast through unknown.
|
|
139
|
+
return await client.query(textOrConfig);
|
|
134
140
|
}
|
|
135
141
|
catch (err) {
|
|
136
142
|
throw wrapPgError(err);
|
|
@@ -184,10 +190,19 @@ export class TurbineClient {
|
|
|
184
190
|
}
|
|
185
191
|
this.logging = config.logging ?? false;
|
|
186
192
|
this.schema = schema;
|
|
193
|
+
// Respect env var kill switch
|
|
194
|
+
const envDisablePrepared = typeof process !== 'undefined' && process.env?.TURBINE_DISABLE_PREPARED === '1';
|
|
187
195
|
this.queryOptions = {
|
|
188
196
|
defaultLimit: config.defaultLimit,
|
|
189
197
|
warnOnUnlimited: config.warnOnUnlimited,
|
|
198
|
+
preparedStatements: envDisablePrepared ? false : (config.preparedStatements ?? !config.pool),
|
|
199
|
+
sqlCache: config.sqlCache ?? true,
|
|
190
200
|
};
|
|
201
|
+
// Apply NotFoundError message redaction mode (default: safe — values are
|
|
202
|
+
// stripped from messages to avoid leaking PII into error logs).
|
|
203
|
+
if (config.errorMessages) {
|
|
204
|
+
setErrorMessageMode(config.errorMessages);
|
|
205
|
+
}
|
|
191
206
|
if (config.pool) {
|
|
192
207
|
// External pool — use directly. Turbine doesn't manage its lifecycle.
|
|
193
208
|
this.pool = config.pool;
|
|
@@ -295,13 +310,41 @@ export class TurbineClient {
|
|
|
295
310
|
/**
|
|
296
311
|
* Execute multiple queries in a single database round-trip.
|
|
297
312
|
*
|
|
298
|
-
*
|
|
313
|
+
* Two call styles:
|
|
314
|
+
* - `db.pipeline(q1, q2, q3)` — rest params (backward-compatible)
|
|
315
|
+
* - `db.pipeline([q1, q2, q3], { transactional: false })` — array + options
|
|
316
|
+
*
|
|
317
|
+
* On pg.Pool-backed connections with TCP, this uses the real Postgres
|
|
318
|
+
* extended-query pipeline protocol (one TCP flush, one round-trip).
|
|
319
|
+
* On HTTP-based drivers it falls back to sequential execution.
|
|
299
320
|
*/
|
|
300
|
-
async pipeline(...
|
|
321
|
+
async pipeline(...args) {
|
|
322
|
+
let queries;
|
|
323
|
+
let options;
|
|
324
|
+
// Detect which overload was used
|
|
325
|
+
if (args.length > 0 &&
|
|
326
|
+
Array.isArray(args[0]) &&
|
|
327
|
+
args[0].every((item) => item && typeof item === 'object' && 'sql' in item)) {
|
|
328
|
+
// Array form: pipeline([q1, q2], opts?)
|
|
329
|
+
queries = args[0];
|
|
330
|
+
options = args[1];
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
// Rest-param form: pipeline(q1, q2, q3)
|
|
334
|
+
queries = args;
|
|
335
|
+
}
|
|
301
336
|
if (this.logging) {
|
|
302
337
|
console.log(`[turbine] Pipeline: ${queries.length} queries — ${queries.map((q) => q.tag).join(', ')}`);
|
|
303
338
|
}
|
|
304
|
-
return executePipeline(this.pool, queries);
|
|
339
|
+
return executePipeline(this.pool, queries, options);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Check whether the underlying pool supports the real pipeline protocol.
|
|
343
|
+
* Returns `true` for standard pg.Pool TCP connections, `false` for HTTP
|
|
344
|
+
* drivers (Neon HTTP, Vercel Postgres, etc.) and mock pools.
|
|
345
|
+
*/
|
|
346
|
+
async pipelineSupported() {
|
|
347
|
+
return pipelineSupported(this.pool);
|
|
305
348
|
}
|
|
306
349
|
// -------------------------------------------------------------------------
|
|
307
350
|
// Raw SQL — tagged template literal escape hatch
|
|
@@ -392,6 +435,24 @@ export class TurbineClient {
|
|
|
392
435
|
async $transaction(fn, options) {
|
|
393
436
|
const client = await this.pool.connect();
|
|
394
437
|
const timeout = options?.timeout;
|
|
438
|
+
/**
|
|
439
|
+
* Track whether the connection has already been released so the finally
|
|
440
|
+
* block doesn't double-release. When a timeout fires we destroy the
|
|
441
|
+
* connection eagerly to abort the in-flight backend query.
|
|
442
|
+
*/
|
|
443
|
+
let released = false;
|
|
444
|
+
const releaseOnce = (err) => {
|
|
445
|
+
if (released)
|
|
446
|
+
return;
|
|
447
|
+
released = true;
|
|
448
|
+
try {
|
|
449
|
+
client.release(err);
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
// pg may throw if the client is already released — swallow.
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
let timedOut = false;
|
|
395
456
|
try {
|
|
396
457
|
// BEGIN with optional isolation level
|
|
397
458
|
let beginSQL = 'BEGIN';
|
|
@@ -415,10 +476,22 @@ export class TurbineClient {
|
|
|
415
476
|
}
|
|
416
477
|
let result;
|
|
417
478
|
if (timeout) {
|
|
418
|
-
// Race between the function and a timeout
|
|
479
|
+
// Race between the function and a timeout. If the timeout fires we
|
|
480
|
+
// need to actually abort the in-flight query — otherwise the backend
|
|
481
|
+
// keeps running until pg's own timeout, holding a pool slot the whole
|
|
482
|
+
// time. The simplest reliable cancellation is to destroy the
|
|
483
|
+
// connection: passing a truthy argument to client.release() tells the
|
|
484
|
+
// pg pool to discard the client (its socket is closed, which causes
|
|
485
|
+
// Postgres to abort the active query and roll back the transaction).
|
|
486
|
+
// The pool will spin up a fresh connection on the next checkout.
|
|
419
487
|
let timer;
|
|
420
488
|
const timeoutPromise = new Promise((_, reject) => {
|
|
421
489
|
timer = setTimeout(() => {
|
|
490
|
+
timedOut = true;
|
|
491
|
+
// Destroy the connection to abort the in-flight backend query.
|
|
492
|
+
// We do this BEFORE rejecting so the socket is gone by the time
|
|
493
|
+
// the caller's catch block runs.
|
|
494
|
+
releaseOnce(new Error('[turbine] Transaction timeout — connection destroyed'));
|
|
422
495
|
reject(new TimeoutError(timeout, 'Transaction'));
|
|
423
496
|
}, timeout);
|
|
424
497
|
});
|
|
@@ -439,14 +512,25 @@ export class TurbineClient {
|
|
|
439
512
|
return result;
|
|
440
513
|
}
|
|
441
514
|
catch (err) {
|
|
442
|
-
|
|
515
|
+
// If the timeout fired we already destroyed the connection — issuing a
|
|
516
|
+
// ROLLBACK on a released client would throw "Client has already been
|
|
517
|
+
// released". Skip the rollback in that case (the backend rolled back
|
|
518
|
+
// when its socket was closed).
|
|
519
|
+
if (!timedOut && !released) {
|
|
520
|
+
try {
|
|
521
|
+
await client.query('ROLLBACK');
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
// Best-effort rollback — the connection may have died mid-query.
|
|
525
|
+
}
|
|
526
|
+
}
|
|
443
527
|
if (this.logging) {
|
|
444
528
|
console.log('[turbine] Transaction rolled back');
|
|
445
529
|
}
|
|
446
530
|
throw err;
|
|
447
531
|
}
|
|
448
532
|
finally {
|
|
449
|
-
|
|
533
|
+
releaseOnce();
|
|
450
534
|
}
|
|
451
535
|
}
|
|
452
536
|
// -------------------------------------------------------------------------
|
package/dist/errors.d.ts
CHANGED
|
@@ -17,6 +17,9 @@ export declare const TurbineErrorCode: {
|
|
|
17
17
|
readonly FOREIGN_KEY_VIOLATION: "TURBINE_E009";
|
|
18
18
|
readonly NOT_NULL_VIOLATION: "TURBINE_E010";
|
|
19
19
|
readonly CHECK_VIOLATION: "TURBINE_E011";
|
|
20
|
+
readonly DEADLOCK_DETECTED: "TURBINE_E012";
|
|
21
|
+
readonly SERIALIZATION_FAILURE: "TURBINE_E013";
|
|
22
|
+
readonly PIPELINE: "TURBINE_E014";
|
|
20
23
|
};
|
|
21
24
|
export type TurbineErrorCode = (typeof TurbineErrorCode)[keyof typeof TurbineErrorCode];
|
|
22
25
|
/** Base error class for all Turbine errors */
|
|
@@ -26,6 +29,30 @@ export declare class TurbineError extends Error {
|
|
|
26
29
|
cause?: unknown;
|
|
27
30
|
});
|
|
28
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Controls whether NotFoundError messages include the actual `where` values
|
|
34
|
+
* (`'verbose'`) or only the where-clause keys (`'safe'`, the default).
|
|
35
|
+
*
|
|
36
|
+
* Defaults to `'safe'` to avoid leaking PII into error logs (Sentry, Datadog,
|
|
37
|
+
* etc.). The full `where` object is always available as `err.where` for
|
|
38
|
+
* programmatic access — only the human-readable message is redacted.
|
|
39
|
+
*
|
|
40
|
+
* Set via `setErrorMessageMode('verbose')` or by constructing TurbineClient
|
|
41
|
+
* with `{ errorMessages: 'verbose' }`.
|
|
42
|
+
*/
|
|
43
|
+
export type ErrorMessageMode = 'safe' | 'verbose';
|
|
44
|
+
/**
|
|
45
|
+
* Set the global NotFoundError message mode. Called from the TurbineClient
|
|
46
|
+
* constructor when `TurbineConfig.errorMessages` is provided.
|
|
47
|
+
*
|
|
48
|
+
* - `'safe'` (default): the message includes only the keys of the where
|
|
49
|
+
* clause (e.g. `where: { id, email }`). Values are redacted.
|
|
50
|
+
* - `'verbose'`: the message includes the full JSON-serialized where
|
|
51
|
+
* clause (e.g. `where: {"id":1,"email":"alice@x.com"}`).
|
|
52
|
+
*/
|
|
53
|
+
export declare function setErrorMessageMode(mode: ErrorMessageMode): void;
|
|
54
|
+
/** Returns the current NotFoundError message mode. Exported for tests. */
|
|
55
|
+
export declare function getErrorMessageMode(): ErrorMessageMode;
|
|
29
56
|
/**
|
|
30
57
|
* Thrown when a record is not found (findUniqueOrThrow, findFirstOrThrow,
|
|
31
58
|
* update/delete against a non-matching row, etc.)
|
|
@@ -35,8 +62,16 @@ export declare class TurbineError extends Error {
|
|
|
35
62
|
* - `new NotFoundError({ table, where, operation, cause, message })`
|
|
36
63
|
*
|
|
37
64
|
* When called with an options object and no explicit `message`, a Prisma-style
|
|
38
|
-
* message is built automatically,
|
|
65
|
+
* message is built automatically. By default, only the where-clause keys are
|
|
66
|
+
* shown to avoid leaking PII into logs:
|
|
67
|
+
* `[turbine] findUniqueOrThrow on "users" found no record matching where: { id }`
|
|
68
|
+
*
|
|
69
|
+
* Set `setErrorMessageMode('verbose')` (or pass `errorMessages: 'verbose'` to
|
|
70
|
+
* the TurbineClient constructor) to include the full where values:
|
|
39
71
|
* `[turbine] findUniqueOrThrow on "users" found no record matching where: {"id":1}`
|
|
72
|
+
*
|
|
73
|
+
* The full `where` object, `table`, and `operation` are always available as
|
|
74
|
+
* structured properties on the error instance regardless of mode.
|
|
40
75
|
*/
|
|
41
76
|
export declare class NotFoundError extends TurbineError {
|
|
42
77
|
readonly table?: string;
|
|
@@ -111,6 +146,57 @@ export declare class NotNullViolationError extends TurbineError {
|
|
|
111
146
|
cause?: unknown;
|
|
112
147
|
});
|
|
113
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Thrown when Postgres detects a deadlock (pg code 40P01).
|
|
151
|
+
*
|
|
152
|
+
* This error is **retryable** — when caught, callers can safely retry the
|
|
153
|
+
* transaction (typically with backoff). Catch it explicitly:
|
|
154
|
+
*
|
|
155
|
+
* ```ts
|
|
156
|
+
* try {
|
|
157
|
+
* await db.$transaction(async (tx) => { ... });
|
|
158
|
+
* } catch (err) {
|
|
159
|
+
* if (err instanceof DeadlockError) {
|
|
160
|
+
* // safe to retry
|
|
161
|
+
* }
|
|
162
|
+
* }
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
export declare class DeadlockError extends TurbineError {
|
|
166
|
+
/** Marks this error as safe to retry */
|
|
167
|
+
readonly isRetryable: true;
|
|
168
|
+
readonly constraint?: string;
|
|
169
|
+
constructor(opts?: {
|
|
170
|
+
message?: string;
|
|
171
|
+
constraint?: string;
|
|
172
|
+
cause?: unknown;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Thrown when a Serializable transaction fails due to a serialization
|
|
177
|
+
* conflict (pg code 40001 — `could not serialize access due to ...`).
|
|
178
|
+
*
|
|
179
|
+
* This error is **retryable** — by Postgres documentation, the recommended
|
|
180
|
+
* response is to re-run the entire transaction. Catch it explicitly:
|
|
181
|
+
*
|
|
182
|
+
* ```ts
|
|
183
|
+
* try {
|
|
184
|
+
* await db.$transaction(async (tx) => { ... }, { isolationLevel: 'Serializable' });
|
|
185
|
+
* } catch (err) {
|
|
186
|
+
* if (err instanceof SerializationFailureError) {
|
|
187
|
+
* // safe to retry the whole transaction
|
|
188
|
+
* }
|
|
189
|
+
* }
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
192
|
+
export declare class SerializationFailureError extends TurbineError {
|
|
193
|
+
/** Marks this error as safe to retry */
|
|
194
|
+
readonly isRetryable: true;
|
|
195
|
+
constructor(opts?: {
|
|
196
|
+
message?: string;
|
|
197
|
+
cause?: unknown;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
114
200
|
/** Thrown when a CHECK constraint is violated (pg code 23514) */
|
|
115
201
|
export declare class CheckConstraintError extends TurbineError {
|
|
116
202
|
readonly constraint?: string;
|
|
@@ -122,6 +208,49 @@ export declare class CheckConstraintError extends TurbineError {
|
|
|
122
208
|
cause?: unknown;
|
|
123
209
|
});
|
|
124
210
|
}
|
|
211
|
+
/** Result slot for a single query in a non-transactional pipeline */
|
|
212
|
+
export type PipelineResultSlot = {
|
|
213
|
+
status: 'ok';
|
|
214
|
+
value: unknown;
|
|
215
|
+
} | {
|
|
216
|
+
status: 'error';
|
|
217
|
+
error: Error;
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* Thrown when a non-transactional pipeline has partial failures.
|
|
221
|
+
*
|
|
222
|
+
* In non-transactional mode (`{ transactional: false }`), each query executes
|
|
223
|
+
* independently. If one or more queries fail, the pipeline rejects with a
|
|
224
|
+
* `PipelineError` that carries per-query results so callers can inspect which
|
|
225
|
+
* succeeded and which failed.
|
|
226
|
+
*
|
|
227
|
+
* ```ts
|
|
228
|
+
* try {
|
|
229
|
+
* await db.pipeline([q1, q2, q3], { transactional: false });
|
|
230
|
+
* } catch (err) {
|
|
231
|
+
* if (err instanceof PipelineError) {
|
|
232
|
+
* for (const slot of err.results) {
|
|
233
|
+
* if (slot.status === 'error') console.error(slot.error);
|
|
234
|
+
* }
|
|
235
|
+
* }
|
|
236
|
+
* }
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
export declare class PipelineError extends TurbineError {
|
|
240
|
+
/** Per-query results: each slot is either `{status:'ok', value}` or `{status:'error', error}` */
|
|
241
|
+
readonly results: PipelineResultSlot[];
|
|
242
|
+
/** Zero-based index of the first query that failed */
|
|
243
|
+
readonly failedIndex?: number;
|
|
244
|
+
/** Tag of the first query that failed (from DeferredQuery.tag) */
|
|
245
|
+
readonly failedTag?: string;
|
|
246
|
+
constructor(opts: {
|
|
247
|
+
message?: string;
|
|
248
|
+
results: PipelineResultSlot[];
|
|
249
|
+
failedIndex?: number;
|
|
250
|
+
failedTag?: string;
|
|
251
|
+
cause?: unknown;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
125
254
|
/**
|
|
126
255
|
* Translate a pg driver error into a typed Turbine error.
|
|
127
256
|
* If the error doesn't match a known constraint code, returns it unchanged.
|
|
@@ -131,6 +260,8 @@ export declare class CheckConstraintError extends TurbineError {
|
|
|
131
260
|
* 23503 (foreign_key_violation) -> ForeignKeyError
|
|
132
261
|
* 23502 (not_null_violation) -> NotNullViolationError
|
|
133
262
|
* 23514 (check_violation) -> CheckConstraintError
|
|
263
|
+
* 40P01 (deadlock_detected) -> DeadlockError (retryable)
|
|
264
|
+
* 40001 (serialization_failure) -> SerializationFailureError (retryable)
|
|
134
265
|
*
|
|
135
266
|
* The original pg error is preserved as `.cause` on the wrapped error.
|
|
136
267
|
*/
|