turbine-orm 0.77.1 → 0.78.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 +13 -9
- package/dist/cjs/cli/config.d.ts +7 -1
- package/dist/cjs/cli/config.js +11 -2
- package/dist/cjs/cli/destructive.d.ts +1 -1
- package/dist/cjs/cli/destructive.js +307 -9
- package/dist/cjs/cli/index.js +252 -42
- package/dist/cjs/cli/mcp.d.ts +23 -0
- package/dist/cjs/cli/mcp.js +190 -152
- package/dist/cjs/cli/migrate.d.ts +243 -3
- package/dist/cjs/cli/migrate.js +432 -43
- package/dist/cjs/cli/sql-statements.js +27 -0
- package/dist/cjs/cli/studio.d.ts +0 -1
- package/dist/cjs/cli/studio.js +9 -7
- package/dist/cjs/client.d.ts +8 -1
- package/dist/cjs/client.js +7 -0
- package/dist/cjs/connection-url.d.ts +82 -0
- package/dist/cjs/connection-url.js +187 -1
- package/dist/cjs/errors.d.ts +112 -12
- package/dist/cjs/errors.js +558 -114
- package/dist/cjs/generate.js +47 -15
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/introspect.d.ts +33 -0
- package/dist/cjs/introspect.js +54 -1
- package/dist/cjs/mssql.js +21 -1
- package/dist/cjs/nested-write.js +85 -14
- package/dist/cjs/pipeline-submittable.d.ts +2 -0
- package/dist/cjs/pipeline-submittable.js +88 -3
- package/dist/cjs/pipeline.js +13 -1
- package/dist/cjs/powdb-introspect.d.ts +5 -1
- package/dist/cjs/powdb-introspect.js +5 -1
- package/dist/cjs/powql.d.ts +14 -0
- package/dist/cjs/powql.js +44 -4
- package/dist/cjs/prisma-compat.js +95 -8
- package/dist/cjs/query/aggregates.js +56 -6
- package/dist/cjs/query/builder.d.ts +76 -13
- package/dist/cjs/query/builder.js +188 -58
- package/dist/cjs/query/compound-unique.d.ts +76 -0
- package/dist/cjs/query/compound-unique.js +129 -0
- package/dist/cjs/query/index.d.ts +1 -1
- package/dist/cjs/query/types.d.ts +65 -11
- package/dist/cjs/query/where.d.ts +85 -19
- package/dist/cjs/query/where.js +262 -47
- package/dist/cjs/query/writes.d.ts +11 -2
- package/dist/cjs/query/writes.js +116 -21
- package/dist/cjs/seed.d.ts +16 -0
- package/dist/cjs/seed.js +16 -0
- package/dist/cli/config.d.ts +7 -1
- package/dist/cli/config.js +11 -2
- package/dist/cli/destructive.d.ts +1 -1
- package/dist/cli/destructive.js +307 -9
- package/dist/cli/index.js +254 -44
- package/dist/cli/mcp.d.ts +23 -0
- package/dist/cli/mcp.js +187 -150
- package/dist/cli/migrate.d.ts +243 -3
- package/dist/cli/migrate.js +423 -45
- package/dist/cli/sql-statements.js +27 -0
- package/dist/cli/studio.d.ts +0 -1
- package/dist/cli/studio.js +10 -7
- package/dist/client.d.ts +8 -1
- package/dist/client.js +7 -0
- package/dist/connection-url.d.ts +82 -0
- package/dist/connection-url.js +183 -0
- package/dist/errors.d.ts +112 -12
- package/dist/errors.js +558 -114
- package/dist/generate.js +47 -15
- package/dist/index.d.ts +1 -1
- package/dist/introspect.d.ts +33 -0
- package/dist/introspect.js +53 -1
- package/dist/mssql.js +21 -1
- package/dist/nested-write.js +85 -14
- package/dist/pipeline-submittable.d.ts +2 -0
- package/dist/pipeline-submittable.js +87 -3
- package/dist/pipeline.js +14 -2
- package/dist/powdb-introspect.d.ts +5 -1
- package/dist/powdb-introspect.js +5 -1
- package/dist/powql.d.ts +14 -0
- package/dist/powql.js +45 -5
- package/dist/prisma-compat.js +96 -9
- package/dist/query/aggregates.js +56 -6
- package/dist/query/builder.d.ts +76 -13
- package/dist/query/builder.js +188 -58
- package/dist/query/compound-unique.d.ts +76 -0
- package/dist/query/compound-unique.js +126 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +65 -11
- package/dist/query/where.d.ts +85 -19
- package/dist/query/where.js +260 -47
- package/dist/query/writes.d.ts +11 -2
- package/dist/query/writes.js +117 -22
- package/dist/seed.d.ts +16 -0
- package/dist/seed.js +16 -0
- package/package.json +3 -3
- package/skills/turbine-orm/SKILL.md +37 -10
package/dist/cjs/errors.js
CHANGED
|
@@ -71,8 +71,38 @@ function formatErrorMessage(code, message) {
|
|
|
71
71
|
const body = message.startsWith(tag) ? message : message ? `${tag} ${message}` : tag;
|
|
72
72
|
return `${body}${link}`;
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* The cross-copy identity brand every Turbine error carries as an own,
|
|
76
|
+
* non-enumerable symbol property.
|
|
77
|
+
*
|
|
78
|
+
* The package ships an ESM build and a CJS build, and one process can hold
|
|
79
|
+
* both: a generated client compiled as CommonJS `require`s `dist/cjs` while an
|
|
80
|
+
* `.mts` entry file `import`s `dist`. The error is then constructed by one
|
|
81
|
+
* copy's class and tested against the other copy's, and prototype-chain
|
|
82
|
+
* `instanceof` says false, so every documented
|
|
83
|
+
* `if (err instanceof UniqueConstraintError)` branch is skipped and the error
|
|
84
|
+
* propagates as unhandled, with nothing logged. `Symbol.for` gives both copies
|
|
85
|
+
* the same key, so {@link TurbineError[Symbol.hasInstance]} can recognise the
|
|
86
|
+
* other copy's instances by brand plus code instead of by class identity. Same
|
|
87
|
+
* pattern as {@link VALUE_BEARING_MESSAGE} below.
|
|
88
|
+
*/
|
|
89
|
+
const TURBINE_ERROR_BRAND = Symbol.for('turbine-orm.error');
|
|
74
90
|
/** Base error class for all Turbine errors */
|
|
75
91
|
class TurbineError extends Error {
|
|
92
|
+
/**
|
|
93
|
+
* The code every instance of this class passes to `super()`, declared once
|
|
94
|
+
* per subclass on the line above the constructor that names it, so the two
|
|
95
|
+
* are read together. It exists for {@link TurbineError[Symbol.hasInstance]}:
|
|
96
|
+
* a subclass recognises a branded error from another copy of this module
|
|
97
|
+
* when the codes agree. `undefined` here on the base class, which therefore
|
|
98
|
+
* recognises any branded error.
|
|
99
|
+
*
|
|
100
|
+
* A static per class rather than a class-to-code map because the code is
|
|
101
|
+
* already written once per class; this puts the second mention next to the
|
|
102
|
+
* first instead of in a table at the bottom of the file, and a unit test
|
|
103
|
+
* asserts the two agree for every exported class.
|
|
104
|
+
*/
|
|
105
|
+
static CODE = undefined;
|
|
76
106
|
code;
|
|
77
107
|
/** Docs page for this code, e.g. `https://turbineorm.dev/errors#e003`. */
|
|
78
108
|
docsUrl;
|
|
@@ -87,6 +117,45 @@ class TurbineError extends Error {
|
|
|
87
117
|
this.name = 'TurbineError';
|
|
88
118
|
this.code = code;
|
|
89
119
|
this.docsUrl = docsUrlForCode(code);
|
|
120
|
+
// Non-enumerable, like the message flag, so the brand never appears in a
|
|
121
|
+
// serialized error; non-writable so nothing downstream can unbrand it.
|
|
122
|
+
Object.defineProperty(this, TURBINE_ERROR_BRAND, {
|
|
123
|
+
value: true,
|
|
124
|
+
enumerable: false,
|
|
125
|
+
writable: false,
|
|
126
|
+
configurable: false,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* `instanceof` that survives two copies of this module in one process.
|
|
131
|
+
*
|
|
132
|
+
* The ordinary prototype-chain answer is taken first, so a single-copy
|
|
133
|
+
* process behaves exactly as before, user subclasses included. Only when that
|
|
134
|
+
* says no is the brand consulted: a branded object is an instance of the base
|
|
135
|
+
* class outright, and of a coded subclass when its `code` equals that class's
|
|
136
|
+
* {@link CODE}. The `hasOwn` guard restricts the code match to classes that
|
|
137
|
+
* declare their own CODE, i.e. the ones in this module: a user subclass such
|
|
138
|
+
* as `class MyError extends ValidationError {}` inherits E003 without
|
|
139
|
+
* declaring it, and every ValidationError must not become an instance of the
|
|
140
|
+
* user's narrower class.
|
|
141
|
+
*/
|
|
142
|
+
static [Symbol.hasInstance](value) {
|
|
143
|
+
// `this` is the class `instanceof` was invoked on, which for an inherited
|
|
144
|
+
// static is the SUBCLASS (NotFoundError, not TurbineError); naming the base
|
|
145
|
+
// class here, as the lint rule wants, would make every subclass answer for
|
|
146
|
+
// the base. Named once so the reason is stated once.
|
|
147
|
+
// biome-ignore lint/complexity/noThisInStatic: the receiver is the subclass being tested against, see above
|
|
148
|
+
// biome-ignore lint/complexity/noUselessThisAlias: one alias carries one suppression instead of three
|
|
149
|
+
const cls = this;
|
|
150
|
+
if (Function.prototype[Symbol.hasInstance].call(cls, value))
|
|
151
|
+
return true;
|
|
152
|
+
if (value === null || typeof value !== 'object')
|
|
153
|
+
return false;
|
|
154
|
+
if (value[TURBINE_ERROR_BRAND] !== true)
|
|
155
|
+
return false;
|
|
156
|
+
if (!Object.hasOwn(cls, 'CODE'))
|
|
157
|
+
return false;
|
|
158
|
+
return cls.CODE === undefined || value.code === cls.CODE;
|
|
90
159
|
}
|
|
91
160
|
}
|
|
92
161
|
exports.TurbineError = TurbineError;
|
|
@@ -171,17 +240,28 @@ function runWithErrorMessageMode(mode, fn) {
|
|
|
171
240
|
* clause (e.g. `where: {"id":1,"email":"alice@x.com"}`).
|
|
172
241
|
*
|
|
173
242
|
* SCOPE, stated precisely because the useful version of this contract is the
|
|
174
|
-
* one that is true. 'safe' mode redacts row values from
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
243
|
+
* one that is true. 'safe' mode redacts row and parameter values from every
|
|
244
|
+
* error Turbine raises or wraps:
|
|
245
|
+
* - its own messages;
|
|
246
|
+
* - the `detail`, `hint`, `where` (the CONTEXT field, which carries the bound
|
|
247
|
+
* PARAMETERS when `log_parameter_max_length_on_error` is non-zero) and
|
|
248
|
+
* `internalQuery` fields of a driver error it attaches as `.cause`, and of
|
|
249
|
+
* the error it hands back. See {@link VALUE_BEARING_FIELDS} for what puts a
|
|
250
|
+
* value in each of them;
|
|
251
|
+
* - the driver MESSAGE where that is what carries the value, so `22P02
|
|
252
|
+
* invalid input syntax for type integer: "alice@example.com"` becomes a
|
|
253
|
+
* ValidationError whose safe-mode text names the column and the SQLSTATE
|
|
254
|
+
* and nothing else (see {@link wrapPgError});
|
|
255
|
+
* - the message of a PostgreSQL server error whose SQLSTATE wrapPgError has
|
|
256
|
+
* no class for AND whose SQLSTATE class can hold a value in its message
|
|
257
|
+
* text (`P0001`, say, whose text the function author wrote), replaced by a
|
|
258
|
+
* sentence naming the SQLSTATE and saying the text was withheld. A class
|
|
259
|
+
* whose grammar is written over schema object names keeps its text, because
|
|
260
|
+
* withholding what cannot leak protects nothing: see
|
|
261
|
+
* {@link messageTextIsValueFree}.
|
|
262
|
+
* It is not a log-scrubbing boundary for errors that never pass through
|
|
263
|
+
* Turbine, and an unclassified NATIVE error code from one of the non-Postgres
|
|
264
|
+
* engines is left to that engine's own classifier.
|
|
185
265
|
*/
|
|
186
266
|
function setErrorMessageMode(mode) {
|
|
187
267
|
errorMessageMode = mode;
|
|
@@ -218,11 +298,14 @@ exports.REDACTED_DETAIL = '[redacted by turbine errorMessages:"safe"]';
|
|
|
218
298
|
* every log line, Sentry event and uncaught-rejection dump that renders the
|
|
219
299
|
* cause chain, in the mode whose entire job is to prevent exactly that.
|
|
220
300
|
*
|
|
221
|
-
* A flag rather than a code list, and set by the
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
301
|
+
* A flag rather than a code list, and set by the code that knows the message
|
|
302
|
+
* grammar in question, for two reasons. It keeps every constraint-class
|
|
303
|
+
* Postgres cause byte-identical (nothing sets it for those, so they are
|
|
304
|
+
* returned exactly as before; `wrapPgError` sets it only for the class-22 and
|
|
305
|
+
* `to_tsquery` shapes whose value sits in `message`, and for a server error it
|
|
306
|
+
* cannot classify at all), and it puts "this engine's message embeds values"
|
|
307
|
+
* next to the code that reads that engine's messages instead of in a table
|
|
308
|
+
* here that would silently rot.
|
|
226
309
|
*
|
|
227
310
|
* `Symbol.for` so the ESM and CJS copies of this module agree on the key: a
|
|
228
311
|
* dual-package consumer can hand an error marked by one build to the other.
|
|
@@ -264,84 +347,125 @@ function markValueBearingMessage(err) {
|
|
|
264
347
|
*/
|
|
265
348
|
function redactedDriverMessage(code) {
|
|
266
349
|
const codePart = typeof code === 'string' && code.length > 0 ? ` (driver code ${code})` : '';
|
|
267
|
-
return `${exports.REDACTED_DETAIL}${codePart}:
|
|
350
|
+
return `${exports.REDACTED_DETAIL}${codePart}: the driver puts a row or parameter value in this message text, so errorMessages:"safe" withholds it. Use errorMessages:"verbose" to see it.`;
|
|
268
351
|
}
|
|
269
352
|
/**
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
353
|
+
* The PostgreSQL error fields that can carry a ROW or PARAMETER value, and are
|
|
354
|
+
* therefore replaced by {@link REDACTED_DETAIL} in 'safe' mode wherever this
|
|
355
|
+
* module clones a driver error.
|
|
356
|
+
*
|
|
357
|
+
* `detail` was the whole list until it turned out not to be. All four ride the
|
|
358
|
+
* same ErrorResponse wire message, node-postgres copies all four verbatim onto
|
|
359
|
+
* the error object as own ENUMERABLE properties, and `util.inspect` (which is
|
|
360
|
+
* what `console.error`, an uncaught rejection and every structured log
|
|
361
|
+
* serializer effectively run) prints all four:
|
|
274
362
|
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
363
|
+
* - `detail` (D) is where a constraint error puts the conflicting values:
|
|
364
|
+
* `Key (email)=(alice@example.com) already exists.` for 23505, `Failing row
|
|
365
|
+
* contains (7, alice@example.com, ...)` for 23502. The `message` of those
|
|
366
|
+
* carries only relation / constraint / column NAMES, which is why redacting
|
|
367
|
+
* `detail` alone looked like the whole job.
|
|
368
|
+
* - `hint` (H): server-generated hints name catalog objects, but
|
|
369
|
+
* `RAISE ... USING HINT = 'the email was ' || v` inside a plpgsql function
|
|
370
|
+
* puts a row value there. It needs no non-default server setting and no
|
|
371
|
+
* cooperation from the application.
|
|
372
|
+
* - `where` (W), the CONTEXT field: with `log_parameter_max_length_on_error`
|
|
373
|
+
* set to anything but 0 the server appends `unnamed portal parameter $1 =
|
|
374
|
+
* '...'`, i.e. THE BOUND PARAMETERS, to the context of any error raised
|
|
375
|
+
* while executing a bound statement.
|
|
376
|
+
* - `internalQuery` (q): the SQL text of a statement executed INSIDE a
|
|
377
|
+
* function, so an `EXECUTE format('SELECT %L::int', v)` hands the
|
|
378
|
+
* interpolated value straight back as SQL text.
|
|
379
|
+
*
|
|
380
|
+
* Redacted on EVERY SQLSTATE, deliberately, including the classes whose
|
|
381
|
+
* `message` is kept (see {@link messageTextIsValueFree}). None of these three
|
|
382
|
+
* is tied to the SQLSTATE the way a message grammar is: parameter context can
|
|
383
|
+
* attach to an error of any class, `format()` can build any internal query, and
|
|
384
|
+
* `RAISE ... USING ERRCODE` lets author-written hint text ride whatever code the
|
|
385
|
+
* author picked. That allowlist is a claim about one field, `message`, and
|
|
386
|
+
* stretching it over these three would be a claim nothing supports.
|
|
387
|
+
*
|
|
388
|
+
* NOT substituted out of the rendered `stack`, unlike a withheld message. V8
|
|
389
|
+
* renders a stack as `<name>: <message>` plus frames, so none of these fields is
|
|
390
|
+
* ever in it, and a global substitution of arbitrary short server text across
|
|
391
|
+
* the frames would risk mangling them to remove something that is not there.
|
|
392
|
+
*/
|
|
393
|
+
const VALUE_BEARING_FIELDS = ['detail', 'hint', 'where', 'internalQuery'];
|
|
394
|
+
/**
|
|
395
|
+
* The property overrides that redact every {@link VALUE_BEARING_FIELDS} entry
|
|
396
|
+
* `err` actually carries; empty when it carries none, which is the signal both
|
|
397
|
+
* callers use to skip cloning altogether.
|
|
398
|
+
*
|
|
399
|
+
* Replaces the DESCRIPTOR rather than assigning after the clone exists: a
|
|
400
|
+
* non-writable field would make the assignment throw in strict mode (every
|
|
401
|
+
* module here is ESM, so it always would), and losing the error is worse than
|
|
402
|
+
* paying for one descriptor literal.
|
|
403
|
+
*/
|
|
404
|
+
function redactValueBearingFields(err) {
|
|
405
|
+
const overrides = {};
|
|
406
|
+
for (const key of VALUE_BEARING_FIELDS) {
|
|
407
|
+
const current = err[key];
|
|
408
|
+
if (typeof current !== 'string' || current.length === 0)
|
|
409
|
+
continue;
|
|
410
|
+
overrides[key] = {
|
|
411
|
+
value: exports.REDACTED_DETAIL,
|
|
412
|
+
writable: true,
|
|
413
|
+
// Keep whichever visibility the driver chose so the clone serializes with
|
|
414
|
+
// the same key set as the original.
|
|
415
|
+
enumerable: Object.getOwnPropertyDescriptor(err, key)?.enumerable ?? true,
|
|
416
|
+
configurable: true,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
return overrides;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* 'safe' mode keeps a driver error's row values out of the Turbine error's own
|
|
423
|
+
* message, but the raw driver error used to be attached verbatim as `.cause`,
|
|
424
|
+
* so the values still reached every place an error object gets rendered whole:
|
|
278
425
|
* - `console.error(err)` / an uncaught rejection: Node's error printer walks
|
|
279
|
-
* the cause chain and prints `[cause]:
|
|
280
|
-
* that `cause` is ALREADY non-enumerable (the Error constructor defines
|
|
281
|
-
* that way) and Node prints it anyway, so hiding the property is not a
|
|
426
|
+
* the cause chain and prints `[cause]: ... detail: 'Key (email)=(...)'`.
|
|
427
|
+
* Note that `cause` is ALREADY non-enumerable (the Error constructor defines
|
|
428
|
+
* it that way) and Node prints it anyway, so hiding the property is not a
|
|
429
|
+
* fix;
|
|
282
430
|
* - Sentry and similar sinks link `cause` chains by default and serialize
|
|
283
431
|
* each link's own properties.
|
|
284
432
|
*
|
|
285
|
-
* So in 'safe' mode the cause is replaced by a shallow clone with
|
|
286
|
-
* swapped for {@link REDACTED_DETAIL}.
|
|
287
|
-
* driver's own object untouched (a
|
|
288
|
-
* what the driver produced).
|
|
433
|
+
* So in 'safe' mode the cause is replaced by a shallow clone with every
|
|
434
|
+
* {@link VALUE_BEARING_FIELDS} entry swapped for {@link REDACTED_DETAIL}.
|
|
435
|
+
* Cloning rather than mutating leaves the driver's own object untouched (a
|
|
436
|
+
* caller holding it from their own catch sees what the driver produced).
|
|
289
437
|
*
|
|
290
|
-
* The clone must remain a REAL error
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
* [[ErrorData]] slot, and that slot is not a property, so it is not copied. The
|
|
294
|
-
* result reads `cause.stack === undefined`, `util.types.isNativeError(cause) ===
|
|
295
|
-
* false` and `Object.prototype.toString.call(cause) === '[object Object]'`, i.e.
|
|
296
|
-
* every log serializer that does `err.cause.stack.split('\n')` throws a
|
|
297
|
-
* TypeError and Sentry/pino drop the cause's frames. So the clone starts life
|
|
298
|
-
* as `new Error()` (which HAS the slot), is re-prototyped to the driver error's
|
|
299
|
-
* own prototype, and takes the original's stack as a plain string. That keeps
|
|
300
|
-
* `cause instanceof pg.DatabaseError`, `cause.code === '23505'`, the native
|
|
301
|
-
* brand, and the frames.
|
|
438
|
+
* The clone must remain a REAL error (string stack, native brand,
|
|
439
|
+
* `instanceof pg.DatabaseError`); see {@link cloneErrorWithOverrides} for why
|
|
440
|
+
* `Object.create` is not enough.
|
|
302
441
|
*
|
|
303
442
|
* In 'verbose' mode the cause passes through untouched: that mode's documented
|
|
304
443
|
* job is full-fidelity debugging.
|
|
305
444
|
*
|
|
306
445
|
* ENGINES WHOSE MESSAGE CARRIES THE VALUE. MySQL and SQL Server do not have a
|
|
307
446
|
* `detail` field at all and put the conflicting value in `message`, so the
|
|
308
|
-
* early return below used to hand the raw driver error
|
|
309
|
-
* {@link markValueBearingMessage}). When the engine set that
|
|
310
|
-
* also withholds `message`, `sqlMessage` (mysql2's copy, which is
|
|
311
|
-
* mysql2 formats `message` FROM, so leaving it would put the value
|
|
312
|
-
* back) and the message text embedded in the rendered `stack` string.
|
|
313
|
-
* substitution is an exact replacement of the known message string,
|
|
314
|
-
* grammar guess, so the frames survive intact.
|
|
447
|
+
* "nothing to remove" early return below used to hand the raw driver error
|
|
448
|
+
* straight back (see {@link markValueBearingMessage}). When the engine set that
|
|
449
|
+
* flag the clone also withholds `message`, `sqlMessage` (mysql2's copy, which is
|
|
450
|
+
* the field mysql2 formats `message` FROM, so leaving it would put the value
|
|
451
|
+
* straight back) and the message text embedded in the rendered `stack` string.
|
|
452
|
+
* The stack substitution is an exact replacement of the known message string,
|
|
453
|
+
* never a grammar guess, so the frames survive intact.
|
|
315
454
|
*/
|
|
316
455
|
function redactCauseForMode(cause) {
|
|
317
456
|
if (currentErrorMessageMode() === 'verbose')
|
|
318
457
|
return cause;
|
|
319
458
|
if (!cause || typeof cause !== 'object')
|
|
320
459
|
return cause;
|
|
321
|
-
const detail = cause.detail;
|
|
322
|
-
const hasDetail = typeof detail === 'string' && detail.length > 0;
|
|
323
460
|
const valueBearingMessage = cause[VALUE_BEARING_MESSAGE] === true;
|
|
324
|
-
// Nothing value-bearing to remove: return the original object so the common
|
|
325
|
-
// case (a non-pg cause, or a pg error without a detail) allocates nothing and
|
|
326
|
-
// keeps object identity with what the driver threw.
|
|
327
|
-
if (!hasDetail && !valueBearingMessage)
|
|
328
|
-
return cause;
|
|
329
461
|
try {
|
|
330
|
-
const
|
|
331
|
-
//
|
|
332
|
-
// non-
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
value: exports.REDACTED_DETAIL,
|
|
338
|
-
writable: true,
|
|
339
|
-
enumerable: descriptors.detail?.enumerable ?? true,
|
|
340
|
-
configurable: true,
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
// The exact message strings to scrub out of the rendered stack, collected
|
|
344
|
-
// BEFORE the descriptors are overwritten.
|
|
462
|
+
const overrides = redactValueBearingFields(cause);
|
|
463
|
+
// Nothing value-bearing to remove: return the original object so the common
|
|
464
|
+
// case (a non-pg cause, or a pg error carrying none of these fields)
|
|
465
|
+
// allocates nothing and keeps object identity with what the driver threw.
|
|
466
|
+
if (!valueBearingMessage && Object.keys(overrides).length === 0)
|
|
467
|
+
return cause;
|
|
468
|
+
// The exact message strings to scrub out of the rendered stack.
|
|
345
469
|
const withheldTexts = [];
|
|
346
470
|
if (valueBearingMessage) {
|
|
347
471
|
const code = cause.code;
|
|
@@ -351,59 +475,83 @@ function redactCauseForMode(cause) {
|
|
|
351
475
|
if (typeof current !== 'string' || current.length === 0)
|
|
352
476
|
continue;
|
|
353
477
|
withheldTexts.push(current);
|
|
354
|
-
|
|
478
|
+
overrides[key] = {
|
|
355
479
|
value: replacement,
|
|
356
480
|
writable: true,
|
|
357
481
|
// `message` is non-enumerable on a native Error and mysql2's
|
|
358
482
|
// `sqlMessage` is enumerable; keep whichever the driver chose so the
|
|
359
483
|
// clone serializes with the same key set as the original.
|
|
360
|
-
enumerable:
|
|
484
|
+
enumerable: Object.getOwnPropertyDescriptor(cause, key)?.enumerable ?? false,
|
|
361
485
|
configurable: true,
|
|
362
486
|
};
|
|
363
487
|
}
|
|
364
488
|
}
|
|
365
|
-
|
|
366
|
-
// another realm (a worker, a bundled duplicate of pg) is still recognized.
|
|
367
|
-
const isError = Object.prototype.toString.call(cause) === '[object Error]';
|
|
368
|
-
if (!isError)
|
|
369
|
-
return Object.create(Object.getPrototypeOf(cause), descriptors);
|
|
370
|
-
// `new Error()` is the only way to obtain the [[ErrorData]] slot; the
|
|
371
|
-
// prototype is then pointed at the driver error's, so `instanceof` and
|
|
372
|
-
// `.name` behave exactly as before.
|
|
373
|
-
const clone = new Error();
|
|
374
|
-
Object.setPrototypeOf(clone, Object.getPrototypeOf(cause));
|
|
375
|
-
// The clone's own fresh `stack` accessor would otherwise describe THIS
|
|
376
|
-
// function's frames, and the original's accessor cannot be transplanted
|
|
377
|
-
// (it reads the receiver's slot). Copy the rendered string instead, and
|
|
378
|
-
// only when it is one: a driver that stashed a non-string there keeps its
|
|
379
|
-
// own descriptor rather than having a lie written over it.
|
|
380
|
-
const originalStack = cause.stack;
|
|
381
|
-
if (typeof originalStack === 'string') {
|
|
382
|
-
// V8 renders the stack as `<name>: <message>\n at …`, so a withheld
|
|
383
|
-
// message is still sitting in it. Substitute the exact strings that were
|
|
384
|
-
// withheld (split/join, so a message repeated in a nested frame goes
|
|
385
|
-
// too); everything else, the frames included, is untouched.
|
|
386
|
-
let stackText = originalStack;
|
|
387
|
-
for (const text of withheldTexts)
|
|
388
|
-
stackText = stackText.split(text).join(exports.REDACTED_DETAIL);
|
|
389
|
-
descriptors.stack = { value: stackText, writable: true, enumerable: false, configurable: true };
|
|
390
|
-
}
|
|
391
|
-
else if (descriptors.stack && typeof descriptors.stack.get === 'function') {
|
|
392
|
-
// An own accessor bound to the ORIGINAL receiver would return undefined
|
|
393
|
-
// here; drop it and let the clone keep its own working one.
|
|
394
|
-
delete descriptors.stack;
|
|
395
|
-
}
|
|
396
|
-
Object.defineProperties(clone, descriptors);
|
|
397
|
-
return clone;
|
|
489
|
+
return cloneErrorWithOverrides(cause, overrides, withheldTexts);
|
|
398
490
|
}
|
|
399
491
|
catch {
|
|
400
|
-
// A cause whose descriptors cannot be replayed
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
492
|
+
// A cause whose descriptors cannot be replayed, or whose properties cannot
|
|
493
|
+
// even be read (an exotic proxy, a throwing getter, a frozen prototype
|
|
494
|
+
// chain), must not turn a database error into a TypeError thrown from an
|
|
495
|
+
// error constructor. Dropping the cause entirely is the safe direction
|
|
496
|
+
// here: 'safe' mode's contract is that no row value escapes.
|
|
404
497
|
return undefined;
|
|
405
498
|
}
|
|
406
499
|
}
|
|
500
|
+
/**
|
|
501
|
+
* A copy of `original` with some own properties replaced, that is still a REAL
|
|
502
|
+
* error. Shared by {@link redactCauseForMode} and the unclassified-SQLSTATE
|
|
503
|
+
* scrub in {@link wrapPgError}, so the two cannot drift in how they clone.
|
|
504
|
+
*
|
|
505
|
+
* This is the part that is easy to get wrong. `Object.create(proto,
|
|
506
|
+
* descriptors)` looks equivalent and is not: V8 installs `stack` as an own
|
|
507
|
+
* ACCESSOR whose backing store is the internal [[ErrorData]] slot, and that
|
|
508
|
+
* slot is not a property, so it is not copied. The result reads `stack ===
|
|
509
|
+
* undefined`, `util.types.isNativeError() === false` and
|
|
510
|
+
* `Object.prototype.toString.call() === '[object Object]'`, i.e. every log
|
|
511
|
+
* serializer that does `err.stack.split('\n')` throws a TypeError and
|
|
512
|
+
* Sentry/pino drop the frames. So the clone starts life as `new Error()`
|
|
513
|
+
* (which HAS the slot), is re-prototyped to the original's own prototype, and
|
|
514
|
+
* takes the original's stack as a plain string. That keeps `instanceof
|
|
515
|
+
* pg.DatabaseError`, `.code`, the native brand, and the frames.
|
|
516
|
+
*
|
|
517
|
+
* `withheldTexts` are the exact message strings that were replaced: V8 renders
|
|
518
|
+
* the stack as `<name>: <message>\n at …`, so a withheld message is still
|
|
519
|
+
* sitting in it and is substituted out (split/join, so a message repeated in a
|
|
520
|
+
* nested frame goes too). Everything else, the frames included, is untouched.
|
|
521
|
+
* `omit` names own keys the copy must NOT carry over.
|
|
522
|
+
*/
|
|
523
|
+
function cloneErrorWithOverrides(original, overrides, withheldTexts, omit = []) {
|
|
524
|
+
const descriptors = Object.getOwnPropertyDescriptors(original);
|
|
525
|
+
for (const key of omit)
|
|
526
|
+
delete descriptors[key];
|
|
527
|
+
Object.assign(descriptors, overrides);
|
|
528
|
+
// Brand check rather than `instanceof Error`, so a driver error thrown from
|
|
529
|
+
// another realm (a worker, a bundled duplicate of pg) is still recognized.
|
|
530
|
+
const isError = Object.prototype.toString.call(original) === '[object Error]';
|
|
531
|
+
if (!isError)
|
|
532
|
+
return Object.create(Object.getPrototypeOf(original), descriptors);
|
|
533
|
+
const clone = new Error();
|
|
534
|
+
Object.setPrototypeOf(clone, Object.getPrototypeOf(original));
|
|
535
|
+
// The clone's own fresh `stack` accessor would otherwise describe THIS
|
|
536
|
+
// function's frames, and the original's accessor cannot be transplanted (it
|
|
537
|
+
// reads the receiver's slot). Copy the rendered string instead, and only when
|
|
538
|
+
// it is one: a driver that stashed a non-string there keeps its own
|
|
539
|
+
// descriptor rather than having a lie written over it.
|
|
540
|
+
const originalStack = original.stack;
|
|
541
|
+
if (typeof originalStack === 'string') {
|
|
542
|
+
let stackText = originalStack;
|
|
543
|
+
for (const text of withheldTexts)
|
|
544
|
+
stackText = stackText.split(text).join(exports.REDACTED_DETAIL);
|
|
545
|
+
descriptors.stack = { value: stackText, writable: true, enumerable: false, configurable: true };
|
|
546
|
+
}
|
|
547
|
+
else if (descriptors.stack && typeof descriptors.stack.get === 'function') {
|
|
548
|
+
// An own accessor bound to the ORIGINAL receiver would return undefined
|
|
549
|
+
// here; drop it and let the clone keep its own working one.
|
|
550
|
+
delete descriptors.stack;
|
|
551
|
+
}
|
|
552
|
+
Object.defineProperties(clone, descriptors);
|
|
553
|
+
return clone;
|
|
554
|
+
}
|
|
407
555
|
/**
|
|
408
556
|
* Render a user-supplied `where` / `connect` target for a "no row found" error
|
|
409
557
|
* message, honoring the global {@link ErrorMessageMode}. In 'safe' mode (the
|
|
@@ -475,6 +623,7 @@ function renderWhereForMessage(where, mode) {
|
|
|
475
623
|
* structured properties on the error instance regardless of mode.
|
|
476
624
|
*/
|
|
477
625
|
class NotFoundError extends TurbineError {
|
|
626
|
+
static CODE = exports.TurbineErrorCode.NOT_FOUND;
|
|
478
627
|
table;
|
|
479
628
|
where;
|
|
480
629
|
operation;
|
|
@@ -510,6 +659,7 @@ class NotFoundError extends TurbineError {
|
|
|
510
659
|
exports.NotFoundError = NotFoundError;
|
|
511
660
|
/** Thrown when a query or transaction exceeds the configured timeout */
|
|
512
661
|
class TimeoutError extends TurbineError {
|
|
662
|
+
static CODE = exports.TurbineErrorCode.TIMEOUT;
|
|
513
663
|
timeoutMs;
|
|
514
664
|
/**
|
|
515
665
|
* @param timeoutMs the client-side timeout budget in ms. Pass `0` when the
|
|
@@ -528,6 +678,23 @@ class TimeoutError extends TurbineError {
|
|
|
528
678
|
exports.TimeoutError = TimeoutError;
|
|
529
679
|
/** Thrown when query arguments fail validation (unknown column, invalid operator, etc.) */
|
|
530
680
|
class ValidationError extends TurbineError {
|
|
681
|
+
static CODE = exports.TurbineErrorCode.VALIDATION;
|
|
682
|
+
/**
|
|
683
|
+
* For an E003 that wraps a PostgreSQL data exception (SQLSTATE class 22) or a
|
|
684
|
+
* `search` operand `to_tsquery` rejected (42601): the column the server named,
|
|
685
|
+
* when it named one, and the SQLSTATE that classified the error. Undefined
|
|
686
|
+
* for validation Turbine performed itself, which never reaches the server.
|
|
687
|
+
*/
|
|
688
|
+
column;
|
|
689
|
+
sqlstate;
|
|
690
|
+
/**
|
|
691
|
+
* The driver's own text for a wrapped data exception. PostgreSQL puts the
|
|
692
|
+
* offending VALUE in that text (`invalid input syntax for type integer:
|
|
693
|
+
* "alice@example.com"`), so under `errorMessages: 'safe'` this is
|
|
694
|
+
* {@link REDACTED_DETAIL} and under `'verbose'` the driver text in full, the
|
|
695
|
+
* same split the constraint classes apply to `.cause.detail`.
|
|
696
|
+
*/
|
|
697
|
+
detail;
|
|
531
698
|
/**
|
|
532
699
|
* `options.cause` is for the engines, not for the query builder. Turbine's own
|
|
533
700
|
* E003s are raised from validation it performed itself, so there is nothing
|
|
@@ -540,11 +707,15 @@ class ValidationError extends TurbineError {
|
|
|
540
707
|
constructor(message, options) {
|
|
541
708
|
super(exports.TurbineErrorCode.VALIDATION, message, options);
|
|
542
709
|
this.name = 'ValidationError';
|
|
710
|
+
this.column = options?.column;
|
|
711
|
+
this.sqlstate = options?.sqlstate;
|
|
712
|
+
this.detail = options?.detail;
|
|
543
713
|
}
|
|
544
714
|
}
|
|
545
715
|
exports.ValidationError = ValidationError;
|
|
546
716
|
/** Thrown when a database connection fails */
|
|
547
717
|
class ConnectionError extends TurbineError {
|
|
718
|
+
static CODE = exports.TurbineErrorCode.CONNECTION;
|
|
548
719
|
/**
|
|
549
720
|
* The driver code that produced this error: a Postgres SQLSTATE (`28P01`
|
|
550
721
|
* wrong password, `3D000` no such database, `08006` connection failure, ...)
|
|
@@ -599,6 +770,7 @@ function malformedConnectionStringMessage(engine, example) {
|
|
|
599
770
|
}
|
|
600
771
|
/** Thrown when a relation reference is invalid */
|
|
601
772
|
class RelationError extends TurbineError {
|
|
773
|
+
static CODE = exports.TurbineErrorCode.RELATION;
|
|
602
774
|
constructor(message) {
|
|
603
775
|
super(exports.TurbineErrorCode.RELATION, message);
|
|
604
776
|
this.name = 'RelationError';
|
|
@@ -607,6 +779,7 @@ class RelationError extends TurbineError {
|
|
|
607
779
|
exports.RelationError = RelationError;
|
|
608
780
|
/** Thrown when a migration operation fails */
|
|
609
781
|
class MigrationError extends TurbineError {
|
|
782
|
+
static CODE = exports.TurbineErrorCode.MIGRATION;
|
|
610
783
|
constructor(message) {
|
|
611
784
|
super(exports.TurbineErrorCode.MIGRATION, message);
|
|
612
785
|
this.name = 'MigrationError';
|
|
@@ -615,6 +788,7 @@ class MigrationError extends TurbineError {
|
|
|
615
788
|
exports.MigrationError = MigrationError;
|
|
616
789
|
/** Thrown when circular relation nesting is detected */
|
|
617
790
|
class CircularRelationError extends TurbineError {
|
|
791
|
+
static CODE = exports.TurbineErrorCode.CIRCULAR_RELATION;
|
|
618
792
|
path;
|
|
619
793
|
constructor(path) {
|
|
620
794
|
super(exports.TurbineErrorCode.CIRCULAR_RELATION, `Circular or too-deep relation nesting detected: ${path.join(' → ')}. Maximum nesting depth is 10.`);
|
|
@@ -638,6 +812,7 @@ function detailFromCause(cause) {
|
|
|
638
812
|
}
|
|
639
813
|
/** Thrown when a UNIQUE constraint is violated (pg code 23505) */
|
|
640
814
|
class UniqueConstraintError extends TurbineError {
|
|
815
|
+
static CODE = exports.TurbineErrorCode.UNIQUE_VIOLATION;
|
|
641
816
|
constraint;
|
|
642
817
|
columns;
|
|
643
818
|
table;
|
|
@@ -671,6 +846,7 @@ class UniqueConstraintError extends TurbineError {
|
|
|
671
846
|
exports.UniqueConstraintError = UniqueConstraintError;
|
|
672
847
|
/** Thrown when a FOREIGN KEY constraint is violated (pg code 23503) */
|
|
673
848
|
class ForeignKeyError extends TurbineError {
|
|
849
|
+
static CODE = exports.TurbineErrorCode.FOREIGN_KEY_VIOLATION;
|
|
674
850
|
constraint;
|
|
675
851
|
table;
|
|
676
852
|
constructor(opts = {}) {
|
|
@@ -701,6 +877,7 @@ class ForeignKeyError extends TurbineError {
|
|
|
701
877
|
exports.ForeignKeyError = ForeignKeyError;
|
|
702
878
|
/** Thrown when a NOT NULL constraint is violated (pg code 23502) */
|
|
703
879
|
class NotNullViolationError extends TurbineError {
|
|
880
|
+
static CODE = exports.TurbineErrorCode.NOT_NULL_VIOLATION;
|
|
704
881
|
column;
|
|
705
882
|
table;
|
|
706
883
|
constructor(opts = {}) {
|
|
@@ -746,6 +923,7 @@ exports.NotNullViolationError = NotNullViolationError;
|
|
|
746
923
|
* ```
|
|
747
924
|
*/
|
|
748
925
|
class DeadlockError extends TurbineError {
|
|
926
|
+
static CODE = exports.TurbineErrorCode.DEADLOCK_DETECTED;
|
|
749
927
|
/** Marks this error as safe to retry */
|
|
750
928
|
isRetryable = true;
|
|
751
929
|
constraint;
|
|
@@ -780,6 +958,7 @@ exports.DeadlockError = DeadlockError;
|
|
|
780
958
|
* ```
|
|
781
959
|
*/
|
|
782
960
|
class SerializationFailureError extends TurbineError {
|
|
961
|
+
static CODE = exports.TurbineErrorCode.SERIALIZATION_FAILURE;
|
|
783
962
|
/** Marks this error as safe to retry */
|
|
784
963
|
isRetryable = true;
|
|
785
964
|
constructor(opts = {}) {
|
|
@@ -796,6 +975,7 @@ class SerializationFailureError extends TurbineError {
|
|
|
796
975
|
exports.SerializationFailureError = SerializationFailureError;
|
|
797
976
|
/** Thrown when a CHECK constraint is violated (pg code 23514) */
|
|
798
977
|
class CheckConstraintError extends TurbineError {
|
|
978
|
+
static CODE = exports.TurbineErrorCode.CHECK_VIOLATION;
|
|
799
979
|
constraint;
|
|
800
980
|
table;
|
|
801
981
|
constructor(opts = {}) {
|
|
@@ -825,6 +1005,7 @@ class CheckConstraintError extends TurbineError {
|
|
|
825
1005
|
}
|
|
826
1006
|
exports.CheckConstraintError = CheckConstraintError;
|
|
827
1007
|
class ExclusionConstraintError extends TurbineError {
|
|
1008
|
+
static CODE = exports.TurbineErrorCode.EXCLUSION_VIOLATION;
|
|
828
1009
|
constraint;
|
|
829
1010
|
table;
|
|
830
1011
|
constructor(opts = {}) {
|
|
@@ -874,6 +1055,7 @@ exports.ExclusionConstraintError = ExclusionConstraintError;
|
|
|
874
1055
|
* ```
|
|
875
1056
|
*/
|
|
876
1057
|
class PipelineError extends TurbineError {
|
|
1058
|
+
static CODE = exports.TurbineErrorCode.PIPELINE;
|
|
877
1059
|
/** Per-query results: each slot is either `{status:'ok', value}` or `{status:'error', error}` */
|
|
878
1060
|
results;
|
|
879
1061
|
/** Zero-based index of the first query that failed */
|
|
@@ -895,16 +1077,30 @@ class PipelineError extends TurbineError {
|
|
|
895
1077
|
}
|
|
896
1078
|
exports.PipelineError = PipelineError;
|
|
897
1079
|
class OptimisticLockError extends TurbineError {
|
|
1080
|
+
static CODE = exports.TurbineErrorCode.OPTIMISTIC_LOCK;
|
|
898
1081
|
table;
|
|
899
1082
|
versionField;
|
|
900
1083
|
expectedVersion;
|
|
1084
|
+
/**
|
|
1085
|
+
* The comparison that failed, value included: `expected version = 3`.
|
|
1086
|
+
* Populated in both modes, like `expectedVersion` itself; only the MESSAGE
|
|
1087
|
+
* withholds the value under `errorMessages: 'safe'`. `optimisticLock` accepts
|
|
1088
|
+
* any column as the version field (an `updated_at` timestamp, an etag
|
|
1089
|
+
* string), so the value is a stored cell like any other, and the message
|
|
1090
|
+
* treats it the way the constraint classes treat `detail`.
|
|
1091
|
+
*/
|
|
1092
|
+
detail;
|
|
901
1093
|
constructor(opts) {
|
|
902
|
-
|
|
903
|
-
|
|
1094
|
+
const detail = `expected ${opts.versionField} = ${String(opts.expectedVersion)}`;
|
|
1095
|
+
const account = currentErrorMessageMode() === 'verbose'
|
|
1096
|
+
? `${detail} but row was modified by another transaction`
|
|
1097
|
+
: `the ${opts.versionField} value did not match, the row was modified by another transaction`;
|
|
1098
|
+
super(exports.TurbineErrorCode.OPTIMISTIC_LOCK, `Optimistic lock failed on "${opts.table}", ${account}`);
|
|
904
1099
|
this.name = 'OptimisticLockError';
|
|
905
1100
|
this.table = opts.table;
|
|
906
1101
|
this.versionField = opts.versionField;
|
|
907
1102
|
this.expectedVersion = opts.expectedVersion;
|
|
1103
|
+
this.detail = detail;
|
|
908
1104
|
}
|
|
909
1105
|
}
|
|
910
1106
|
exports.OptimisticLockError = OptimisticLockError;
|
|
@@ -915,6 +1111,7 @@ exports.OptimisticLockError = OptimisticLockError;
|
|
|
915
1111
|
* clear `unsupported on <engine>` message instead of generating broken SQL.
|
|
916
1112
|
*/
|
|
917
1113
|
class UnsupportedFeatureError extends TurbineError {
|
|
1114
|
+
static CODE = exports.TurbineErrorCode.UNSUPPORTED_FEATURE;
|
|
918
1115
|
feature;
|
|
919
1116
|
dialect;
|
|
920
1117
|
constructor(feature, dialect, hint) {
|
|
@@ -940,6 +1137,7 @@ exports.UnsupportedFeatureError = UnsupportedFeatureError;
|
|
|
940
1137
|
* identically; route it to a writable primary instead.
|
|
941
1138
|
*/
|
|
942
1139
|
class ReadOnlyError extends TurbineError {
|
|
1140
|
+
static CODE = exports.TurbineErrorCode.READ_ONLY;
|
|
943
1141
|
/**
|
|
944
1142
|
* Why the write was refused. `'snapshot'`: the database itself is read-only
|
|
945
1143
|
* (snapshot serving, an embedded `readonly: true` open, or the client-level
|
|
@@ -1057,9 +1255,218 @@ const CONNECTION_ERROR_HINTS = {
|
|
|
1057
1255
|
CERT_HAS_EXPIRED: "The server's TLS certificate has expired. Renew it, or supply the correct CA via `ssl: { ca }`.",
|
|
1058
1256
|
ERR_TLS_CERT_ALTNAME_INVALID: "The server's TLS certificate does not cover the host you connected to. Check the host name in the connection string.",
|
|
1059
1257
|
};
|
|
1258
|
+
/** The SQLSTATE shape: five characters from [0-9A-Z]. */
|
|
1259
|
+
const SQLSTATE_RE = /^[0-9A-Z]{5}$/;
|
|
1260
|
+
/**
|
|
1261
|
+
* Whether `e` is a PostgreSQL SERVER error, as opposed to anything else that
|
|
1262
|
+
* reaches {@link wrapPgError} carrying a `.code`. The distinction matters for
|
|
1263
|
+
* the safe-mode scrub of an unclassified code, which rewrites the MESSAGE: a
|
|
1264
|
+
* user's own `fs` error thrown inside a `$transaction` callback travels the
|
|
1265
|
+
* same path with `code: 'EPERM'`, five uppercase characters like a SQLSTATE,
|
|
1266
|
+
* and rewriting that would turn "permission denied, open /etc/x" into
|
|
1267
|
+
* "Database error EPERM". node-postgres stamps `severity` (`ERROR`, `FATAL`,
|
|
1268
|
+
* `PANIC`) on every ErrorResponse it parses and on nothing else, so its
|
|
1269
|
+
* presence is the positive signal. The engines that reuse this function
|
|
1270
|
+
* (SQLite, MySQL, SQL Server) rewrite `.code` to a mapped SQLSTATE for the
|
|
1271
|
+
* shapes they classify and leave their native codes on the rest, so their
|
|
1272
|
+
* unclassified errors pass through exactly as before.
|
|
1273
|
+
*/
|
|
1274
|
+
function isPgServerError(e) {
|
|
1275
|
+
return typeof e.code === 'string' && SQLSTATE_RE.test(e.code) && typeof e.severity === 'string';
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Value-free accounts of the common data exceptions, so the safe-mode message
|
|
1279
|
+
* still says WHAT kind of bad input it was. PostgreSQL's own text is
|
|
1280
|
+
* `<fixed phrase>: "<the value>"`; the fixed half is what these paraphrase.
|
|
1281
|
+
* Anything else in class 22 gets the class-wide fallback. Never derived from
|
|
1282
|
+
* the driver text.
|
|
1283
|
+
*/
|
|
1284
|
+
const DATA_EXCEPTION_PHRASES = {
|
|
1285
|
+
'22P02': 'the value could not be parsed as the column type',
|
|
1286
|
+
'22003': 'the number is out of range for the column type',
|
|
1287
|
+
'22001': 'the string is too long for the column type',
|
|
1288
|
+
'22007': 'the value is not a valid date or time',
|
|
1289
|
+
'22008': 'a date or time field is out of range',
|
|
1290
|
+
'22012': 'division by zero',
|
|
1291
|
+
};
|
|
1292
|
+
const DATA_EXCEPTION_FALLBACK = "invalid input for the column's type";
|
|
1293
|
+
/**
|
|
1294
|
+
* Wrap a driver error whose MESSAGE embeds the offending value into a
|
|
1295
|
+
* ValidationError (E003): SQLSTATE class 22 (data exception) and the
|
|
1296
|
+
* `to_tsquery` parse failure. `describe` is the value-free account of what went
|
|
1297
|
+
* wrong; in 'verbose' mode the driver text replaces it, because that text IS
|
|
1298
|
+
* the precise account and 'verbose' exists to show it.
|
|
1299
|
+
*
|
|
1300
|
+
* The driver text also goes on `.detail` (redacted in 'safe' mode, full in
|
|
1301
|
+
* 'verbose'), and the driver error is marked value-bearing before it becomes
|
|
1302
|
+
* `.cause`, so the base constructor's cause redaction withholds its message in
|
|
1303
|
+
* 'safe' mode exactly as it does for the MySQL and SQL Server augmenters'
|
|
1304
|
+
* errors. There is no `detail` field to redact for these codes: the value sits
|
|
1305
|
+
* in `message`, which is why the constraint-class treatment (redact `detail`,
|
|
1306
|
+
* keep `message`) was not enough.
|
|
1307
|
+
*/
|
|
1308
|
+
function wrapValueBearingPgError(err, e, subject, describe, hint = '') {
|
|
1309
|
+
const driverText = typeof e.message === 'string' && e.message.length > 0 ? e.message : undefined;
|
|
1310
|
+
const verbose = currentErrorMessageMode() === 'verbose';
|
|
1311
|
+
const account = verbose && driverText ? driverText : describe;
|
|
1312
|
+
markValueBearingMessage(err);
|
|
1313
|
+
return new ValidationError(`${subject}: ${account} (SQLSTATE ${e.code}).${hint}`, {
|
|
1314
|
+
cause: err,
|
|
1315
|
+
column: e.column,
|
|
1316
|
+
sqlstate: e.code,
|
|
1317
|
+
detail: verbose ? driverText : exports.REDACTED_DETAIL,
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* SQLSTATE CLASSES whose MESSAGE text is structurally value-free, so 'safe'
|
|
1322
|
+
* mode keeps it instead of scrubbing it.
|
|
1323
|
+
*
|
|
1324
|
+
* WHY AN ALLOWLIST AT ALL. Scrubbing every unclassified SQLSTATE deletes the
|
|
1325
|
+
* single most useful sentence a first-run or mistyped query produces, and buys
|
|
1326
|
+
* no privacy doing it. `relation "orders" does not exist` (42P01: you forgot to
|
|
1327
|
+
* migrate) and `column "emial" of relation "users" does not exist` (42703: you
|
|
1328
|
+
* misspelled a field) name SCHEMA OBJECTS. There is no row value in either, so
|
|
1329
|
+
* withholding them protects nothing and costs the reader the entire diagnosis,
|
|
1330
|
+
* in the mode that is on by default. A redaction that removes text which cannot
|
|
1331
|
+
* leak is not a safety measure, it is a cost with nothing on the other side.
|
|
1332
|
+
*
|
|
1333
|
+
* THE RULE. A class is listed when PostgreSQL's message grammar for the codes
|
|
1334
|
+
* in it is written over schema object NAMES, over the STRUCTURE of the
|
|
1335
|
+
* statement, or over SERVER STATE, and never over row data:
|
|
1336
|
+
* - `42` syntax error or access rule violation: undefined table / column /
|
|
1337
|
+
* function / object, duplicate object, insufficient privilege, datatype
|
|
1338
|
+
* mismatch. Names and types, and the whole developer-typo population.
|
|
1339
|
+
* - `3D` invalid catalog name and `3F` invalid schema name: a database or a
|
|
1340
|
+
* schema name. (3D000 is claimed by CONNECTION_ERROR_CODES before it can
|
|
1341
|
+
* reach here; the list states the rule for the class, not which branch
|
|
1342
|
+
* happens to answer first.)
|
|
1343
|
+
* - `08` connection exception, `53` insufficient resources, `57` operator
|
|
1344
|
+
* intervention, `58` system error: transport, resource and server state. No
|
|
1345
|
+
* statement data at all.
|
|
1346
|
+
*
|
|
1347
|
+
* WHAT IS DELIBERATELY NOT LISTED, because the argument for it fails:
|
|
1348
|
+
* - `P0001` raise_exception and class `55` object_not_in_prerequisite_state
|
|
1349
|
+
* carry text written by the FUNCTION AUTHOR, and `RAISE EXCEPTION 'order %
|
|
1350
|
+
* is already shipped', order_id` is the idiomatic way to write a business
|
|
1351
|
+
* rule. What is in that text is not knowable from here, and this mode's job
|
|
1352
|
+
* is to be right about the case it cannot see. So they scrub, and the
|
|
1353
|
+
* replacement message says the text was withheld and how to see it.
|
|
1354
|
+
* - Every class nobody has argued about yet. The safe direction for an
|
|
1355
|
+
* unknown grammar is to withhold; adding a class later is cheap and needs
|
|
1356
|
+
* exactly the argument above.
|
|
1357
|
+
*
|
|
1358
|
+
* ONLY THE MESSAGE. `detail`, `hint`, `where` and `internalQuery` are redacted
|
|
1359
|
+
* on a listed class exactly as on any other, for the reasons in
|
|
1360
|
+
* {@link VALUE_BEARING_FIELDS}.
|
|
1361
|
+
*/
|
|
1362
|
+
const VALUE_FREE_MESSAGE_CLASSES = new Set(['08', '3D', '3F', '42', '53', '57', '58']);
|
|
1363
|
+
/**
|
|
1364
|
+
* The one carve-out from {@link VALUE_FREE_MESSAGE_CLASSES}. `42601`
|
|
1365
|
+
* syntax_error is the single class-42 code whose message is defined over the
|
|
1366
|
+
* statement TEXT rather than over names: PostgreSQL quotes the offending token,
|
|
1367
|
+
* and a token can be a string literal, so `syntax error at or near
|
|
1368
|
+
* "'alice@example.com'"` is a reachable message. Turbine binds every value it
|
|
1369
|
+
* emits, so its own SQL text never holds one; SQL a caller assembled by
|
|
1370
|
+
* concatenation does, and that is precisely the code path a syntax error comes
|
|
1371
|
+
* from. Keeping the rule ("scrub where a value can actually ride") true is
|
|
1372
|
+
* worth losing the text on a raw-SQL syntax error, which the replacement
|
|
1373
|
+
* message says how to get back.
|
|
1374
|
+
*
|
|
1375
|
+
* (The `to_tsquery` 42601 never reaches here; {@link wrapPgError} classifies it
|
|
1376
|
+
* as an E003 further up.)
|
|
1377
|
+
*/
|
|
1378
|
+
const VALUE_BEARING_CODES_IN_VALUE_FREE_CLASSES = new Set(['42601']);
|
|
1379
|
+
/** Whether 'safe' mode may keep the driver's message text for this SQLSTATE. */
|
|
1380
|
+
function messageTextIsValueFree(code) {
|
|
1381
|
+
return VALUE_FREE_MESSAGE_CLASSES.has(code.slice(0, 2)) && !VALUE_BEARING_CODES_IN_VALUE_FREE_CLASSES.has(code);
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* The replacement for a withheld server message. It SAYS the text was withheld
|
|
1385
|
+
* and how to see it: `Database error P0001` on its own reads like the whole of
|
|
1386
|
+
* what the server said, so a reader has no reason to look further. Same
|
|
1387
|
+
* reasoning as {@link REDACTED_DETAIL}, which marks the field rather than
|
|
1388
|
+
* deleting it.
|
|
1389
|
+
*/
|
|
1390
|
+
function withheldServerMessage(code) {
|
|
1391
|
+
return `Database error ${code} (driver text withheld by errorMessages: 'safe'; set errorMessages: 'verbose' to see it)`;
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* The 'safe'-mode treatment of a PostgreSQL server error {@link wrapPgError}
|
|
1395
|
+
* has no class for. 'verbose' mode never reaches this function: the raw error
|
|
1396
|
+
* passes through, as it always did.
|
|
1397
|
+
*
|
|
1398
|
+
* WHAT COMES BACK: a clone of the driver error, same prototype, same frames,
|
|
1399
|
+
* same `.code` (the SQLSTATE), plus `.sqlstate` carrying that same SQLSTATE
|
|
1400
|
+
* under the name every typed Turbine error uses for it
|
|
1401
|
+
* (`ValidationError.sqlstate`, `ConnectionError.sqlstate`), so one `catch`
|
|
1402
|
+
* branch reads one field whichever of the two an operation produced. Every
|
|
1403
|
+
* {@link VALUE_BEARING_FIELDS} entry is redacted. The MESSAGE survives when
|
|
1404
|
+
* {@link messageTextIsValueFree} says this SQLSTATE's grammar cannot hold a row
|
|
1405
|
+
* value, and is replaced by {@link withheldServerMessage} (and substituted out
|
|
1406
|
+
* of the rendered stack) when it can.
|
|
1407
|
+
*
|
|
1408
|
+
* The original is on `.cause` under the same rules, and that matters more than
|
|
1409
|
+
* it looks: `.cause` is the documented escape hatch, so a scrub that empties it
|
|
1410
|
+
* too leaves no way to debug at all. Its message is withheld only where the
|
|
1411
|
+
* returned error's is.
|
|
1412
|
+
*
|
|
1413
|
+
* NOT A TurbineError, deliberately. The obvious complaint about this branch is
|
|
1414
|
+
* that `err instanceof TurbineError` is false, so a caller's
|
|
1415
|
+
* `catch (e) { if (e instanceof TurbineError) ... }` misses it. Making it one is
|
|
1416
|
+
* still wrong, because 'verbose' mode returns the raw driver error UNCHANGED
|
|
1417
|
+
* here and always has: the same database failure would then be a TurbineError
|
|
1418
|
+
* under one setting and a `pg.DatabaseError` under the other, i.e. a
|
|
1419
|
+
* LOG-REDACTION setting would decide which `catch` branch runs. An error whose
|
|
1420
|
+
* TYPE depends on a logging option is a worse bug than the one that would fix.
|
|
1421
|
+
* The frozen code set (STABILITY.md) points the same way: there is no
|
|
1422
|
+
* TURBINE_E0NN for "a SQLSTATE we have no opinion about", minting one is out of
|
|
1423
|
+
* scope, and a Turbine class whose `.code` held a raw SQLSTATE instead would
|
|
1424
|
+
* break both the `TurbineErrorCode` type and the brand matching in
|
|
1425
|
+
* {@link TurbineError[Symbol.hasInstance]}. So `.code` stays the raw SQLSTATE it
|
|
1426
|
+
* has always been, `.sqlstate` is added beside it, and there is no `.docsUrl`:
|
|
1427
|
+
* that field is the anchor for a Turbine CODE, and pointing it at a docs section
|
|
1428
|
+
* that does not exist would be worse than its absence.
|
|
1429
|
+
*/
|
|
1430
|
+
function scrubUnclassifiedServerError(err, code) {
|
|
1431
|
+
const keepMessage = messageTextIsValueFree(code);
|
|
1432
|
+
try {
|
|
1433
|
+
const overrides = redactValueBearingFields(err);
|
|
1434
|
+
// The exact message string to substitute out of the rendered stack.
|
|
1435
|
+
const withheld = [];
|
|
1436
|
+
if (!keepMessage) {
|
|
1437
|
+
const original = err.message;
|
|
1438
|
+
overrides.message = {
|
|
1439
|
+
value: withheldServerMessage(code),
|
|
1440
|
+
writable: true,
|
|
1441
|
+
enumerable: false,
|
|
1442
|
+
configurable: true,
|
|
1443
|
+
};
|
|
1444
|
+
if (typeof original === 'string' && original.length > 0)
|
|
1445
|
+
withheld.push(original);
|
|
1446
|
+
}
|
|
1447
|
+
overrides.sqlstate = { value: code, writable: true, enumerable: true, configurable: true };
|
|
1448
|
+
// The value-bearing-message flag is set on `err` itself, so the omit list
|
|
1449
|
+
// strips it from the clone: the clone's own message is either the driver's
|
|
1450
|
+
// value-free text or the withheld sentence, and neither needs the flag.
|
|
1451
|
+
overrides.cause = {
|
|
1452
|
+
value: redactCauseForMode(keepMessage ? err : markValueBearingMessage(err)),
|
|
1453
|
+
writable: true,
|
|
1454
|
+
enumerable: false,
|
|
1455
|
+
configurable: true,
|
|
1456
|
+
};
|
|
1457
|
+
return cloneErrorWithOverrides(err, overrides, withheld, [VALUE_BEARING_MESSAGE]);
|
|
1458
|
+
}
|
|
1459
|
+
catch {
|
|
1460
|
+
// Same posture as redactCauseForMode: a driver error whose descriptors
|
|
1461
|
+
// cannot be replayed must not become a TypeError, and must not leak either.
|
|
1462
|
+
// The fallback withholds the text whatever the allowlist said, because
|
|
1463
|
+
// reading it off an object whose own descriptors just failed to replay is
|
|
1464
|
+
// the one thing this branch cannot safely do.
|
|
1465
|
+
return Object.assign(new Error(withheldServerMessage(code)), { code, sqlstate: code });
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1060
1468
|
/**
|
|
1061
1469
|
* Translate a pg driver error into a typed Turbine error.
|
|
1062
|
-
* If the error doesn't match a known constraint code, returns it unchanged.
|
|
1063
1470
|
*
|
|
1064
1471
|
* Maps:
|
|
1065
1472
|
* 23505 (unique_violation) -> UniqueConstraintError
|
|
@@ -1073,6 +1480,22 @@ const CONNECTION_ERROR_HINTS = {
|
|
|
1073
1480
|
* 28P01 / 28000 (auth refused) -> ConnectionError, with a remediation hint
|
|
1074
1481
|
* 3D000 (no such database) -> ConnectionError, with a remediation hint
|
|
1075
1482
|
* connection-class codes -> ConnectionError (see CONNECTION_ERROR_CODES)
|
|
1483
|
+
* 22xxx (data exception) -> ValidationError naming the column (when the
|
|
1484
|
+
* server did) and the SQLSTATE; driver text
|
|
1485
|
+
* on `.detail`, redacted in 'safe' mode
|
|
1486
|
+
* 42601 from to_tsquery -> ValidationError with a `search` hint
|
|
1487
|
+
* any other server SQLSTATE -> 'safe' mode: a clone keeping the SQLSTATE
|
|
1488
|
+
* on `.code` and repeating it on `.sqlstate`,
|
|
1489
|
+
* with the value-bearing fields redacted, the
|
|
1490
|
+
* driver message kept when the SQLSTATE
|
|
1491
|
+
* class's grammar cannot hold a row value and
|
|
1492
|
+
* withheld when it can, and the original on
|
|
1493
|
+
* `.cause` under the same rules (see
|
|
1494
|
+
* scrubUnclassifiedServerError);
|
|
1495
|
+
* 'verbose' mode: returned unchanged
|
|
1496
|
+
*
|
|
1497
|
+
* Anything that is not a PostgreSQL server error (no SQLSTATE-shaped code, or
|
|
1498
|
+
* no `severity`) is returned unchanged whatever the mode: see isPgServerError.
|
|
1076
1499
|
*
|
|
1077
1500
|
* The original pg error is preserved as `.cause` on the wrapped error.
|
|
1078
1501
|
*/
|
|
@@ -1143,6 +1566,27 @@ function wrapPgError(err) {
|
|
|
1143
1566
|
const head = pgMessage ? `Database connection error: ${pgMessage}` : `Database connection error (${e.code})`;
|
|
1144
1567
|
return new ConnectionError(hint ? `${head} (${e.code}) ${hint}` : head, { cause: err, sqlstate: e.code });
|
|
1145
1568
|
}
|
|
1569
|
+
// Class 22, data exception: a bound value did not fit the column type
|
|
1570
|
+
// (`where: { id: req.params.id }` with a non-numeric id is the common
|
|
1571
|
+
// one). Specific enough on its own that no severity check is needed:
|
|
1572
|
+
// nothing but a SQL engine produces a `22xxx` code.
|
|
1573
|
+
if (e.code.startsWith('22') && SQLSTATE_RE.test(e.code)) {
|
|
1574
|
+
const subject = e.column ? `Invalid input for column "${e.column}"` : 'Invalid input';
|
|
1575
|
+
// Own-property lookup, same reason as CONNECTION_ERROR_HINTS above.
|
|
1576
|
+
const phrase = Object.hasOwn(DATA_EXCEPTION_PHRASES, e.code)
|
|
1577
|
+
? DATA_EXCEPTION_PHRASES[e.code]
|
|
1578
|
+
: DATA_EXCEPTION_FALLBACK;
|
|
1579
|
+
return wrapValueBearingPgError(err, e, subject, phrase);
|
|
1580
|
+
}
|
|
1581
|
+
// 42601 is any syntax error. Only the to_tsquery parse failure is a
|
|
1582
|
+
// caller-input problem Turbine can name (the `search` operator), so the
|
|
1583
|
+
// message text is the discriminator; every other 42601 takes the generic
|
|
1584
|
+
// path below.
|
|
1585
|
+
if (e.code === '42601' && typeof e.message === 'string' && /tsquery/i.test(e.message)) {
|
|
1586
|
+
return wrapValueBearingPgError(err, e, 'Invalid full-text search operand', 'PostgreSQL could not parse the `search` value as a tsquery', ' `search` compiles to to_tsquery(), so `&`, `|`, `!`, `<->` and parentheses in the value are operators rather than text; quote such terms in single quotes, or strip them before searching.');
|
|
1587
|
+
}
|
|
1588
|
+
if (currentErrorMessageMode() === 'safe' && isPgServerError(e))
|
|
1589
|
+
return scrubUnclassifiedServerError(err, e.code);
|
|
1146
1590
|
return err;
|
|
1147
1591
|
}
|
|
1148
1592
|
}
|