turbine-orm 0.60.1 → 0.62.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 +71 -27
- package/dist/cjs/cli/config.d.ts +40 -0
- package/dist/cjs/cli/config.js +74 -2
- package/dist/cjs/cli/index.d.ts +85 -1
- package/dist/cjs/cli/index.js +374 -24
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +448 -29
- package/dist/cjs/cli/pii-tags.d.ts +64 -9
- package/dist/cjs/cli/pii-tags.js +218 -39
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +23 -0
- package/dist/cjs/cli/studio.js +126 -53
- package/dist/cjs/cli/ui.d.ts +15 -1
- package/dist/cjs/cli/ui.js +19 -5
- package/dist/cjs/client.js +248 -11
- package/dist/cjs/errors.d.ts +38 -1
- package/dist/cjs/errors.js +235 -24
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/pipeline-submittable.js +26 -3
- package/dist/cjs/pipeline.js +15 -2
- package/dist/cjs/powql.d.ts +12 -0
- package/dist/cjs/powql.js +46 -21
- package/dist/cjs/prisma-compat.d.ts +15 -5
- package/dist/cjs/prisma-compat.js +273 -78
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +24 -10
- package/dist/cjs/query/batched-loader.d.ts +9 -4
- package/dist/cjs/query/batched-loader.js +4 -1
- package/dist/cjs/query/builder.d.ts +47 -0
- package/dist/cjs/query/builder.js +149 -21
- package/dist/cjs/query/index.d.ts +3 -1
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/option-surface.d.ts +11 -0
- package/dist/cjs/query/option-surface.js +13 -0
- package/dist/cjs/query/relations.d.ts +8 -0
- package/dist/cjs/query/relations.js +21 -1
- package/dist/cjs/query/types.d.ts +152 -18
- package/dist/cjs/query/types.js +212 -1
- package/dist/cjs/query/where.d.ts +3 -3
- package/dist/cjs/query/where.js +8 -2
- package/dist/cjs/query/writes.js +10 -9
- package/dist/cli/config.d.ts +40 -0
- package/dist/cli/config.js +73 -2
- package/dist/cli/index.d.ts +85 -1
- package/dist/cli/index.js +373 -27
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +448 -29
- package/dist/cli/pii-tags.d.ts +64 -9
- package/dist/cli/pii-tags.js +217 -39
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +23 -0
- package/dist/cli/studio.js +125 -53
- package/dist/cli/ui.d.ts +15 -1
- package/dist/cli/ui.js +18 -4
- package/dist/client.js +250 -13
- package/dist/errors.d.ts +38 -1
- package/dist/errors.js +234 -23
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -2
- package/dist/pipeline-submittable.js +26 -3
- package/dist/pipeline.js +15 -2
- package/dist/powql.d.ts +12 -0
- package/dist/powql.js +46 -21
- package/dist/prisma-compat.d.ts +15 -5
- package/dist/prisma-compat.js +274 -79
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +24 -10
- package/dist/query/batched-loader.d.ts +9 -4
- package/dist/query/batched-loader.js +4 -1
- package/dist/query/builder.d.ts +47 -0
- package/dist/query/builder.js +148 -21
- package/dist/query/index.d.ts +3 -1
- package/dist/query/index.js +2 -0
- package/dist/query/option-surface.d.ts +11 -0
- package/dist/query/option-surface.js +13 -0
- package/dist/query/relations.d.ts +8 -0
- package/dist/query/relations.js +21 -1
- package/dist/query/types.d.ts +152 -18
- package/dist/query/types.js +207 -2
- package/dist/query/where.d.ts +3 -3
- package/dist/query/where.js +8 -2
- package/dist/query/writes.js +10 -9
- package/package.json +13 -3
package/dist/errors.js
CHANGED
|
@@ -43,7 +43,13 @@ function formatErrorMessage(code, message) {
|
|
|
43
43
|
export class TurbineError extends Error {
|
|
44
44
|
code;
|
|
45
45
|
constructor(code, message, options) {
|
|
46
|
-
|
|
46
|
+
// The cause is redacted in 'safe' mode (see redactCauseForMode). Only pass
|
|
47
|
+
// an options object through when the caller actually supplied a `cause`
|
|
48
|
+
// key: `new Error(msg, {})` defines no `cause` own property, while
|
|
49
|
+
// `new Error(msg, { cause: undefined })` defines one whose value is
|
|
50
|
+
// undefined, and error-serializing sinks tell those two apart.
|
|
51
|
+
const opts = options && 'cause' in options ? { ...options, cause: redactCauseForMode(options.cause) } : options;
|
|
52
|
+
super(formatErrorMessage(code, message), opts);
|
|
47
53
|
this.name = 'TurbineError';
|
|
48
54
|
this.code = code;
|
|
49
55
|
}
|
|
@@ -57,6 +63,19 @@ let errorMessageMode = 'safe';
|
|
|
57
63
|
* clause (e.g. `where: { id, email }`). Values are redacted.
|
|
58
64
|
* - `'verbose'`: the message includes the full JSON-serialized where
|
|
59
65
|
* clause (e.g. `where: {"id":1,"email":"alice@x.com"}`).
|
|
66
|
+
*
|
|
67
|
+
* SCOPE, stated precisely because the useful version of this contract is the
|
|
68
|
+
* one that is true. 'safe' mode redacts row values from the surfaces Turbine
|
|
69
|
+
* OWNS: its own error messages, and the `detail` field of a driver error it
|
|
70
|
+
* wraps and attaches as `.cause` (see redactCauseForMode).
|
|
71
|
+
*
|
|
72
|
+
* It is NOT a blanket guarantee that no row value can be reached from a thrown
|
|
73
|
+
* error. A driver error whose SQLSTATE {@link wrapPgError} does not classify is
|
|
74
|
+
* returned UNCHANGED, and some of those carry a value in the `message` field
|
|
75
|
+
* itself, where nothing can be removed without destroying the diagnosis:
|
|
76
|
+
* `22P02 invalid input syntax for type integer: "alice@example.com"` is the
|
|
77
|
+
* common one. Treat 'safe' mode as removing Turbine's own contribution to the
|
|
78
|
+
* leak, not as a log-scrubbing boundary.
|
|
60
79
|
*/
|
|
61
80
|
export function setErrorMessageMode(mode) {
|
|
62
81
|
errorMessageMode = mode;
|
|
@@ -65,6 +84,107 @@ export function setErrorMessageMode(mode) {
|
|
|
65
84
|
export function getErrorMessageMode() {
|
|
66
85
|
return errorMessageMode;
|
|
67
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* The marker left where a driver `detail` string was removed in 'safe' mode.
|
|
89
|
+
* Re-exported from the package root so tests and callers writing log
|
|
90
|
+
* assertions can match on it without hardcoding the wording.
|
|
91
|
+
*/
|
|
92
|
+
export const REDACTED_DETAIL = '[redacted by turbine errorMessages:"safe"]';
|
|
93
|
+
/**
|
|
94
|
+
* Postgres puts the CONFLICTING ROW VALUES in the `detail` field of a
|
|
95
|
+
* constraint error, and nowhere else: `Key (email)=(alice@example.com) already
|
|
96
|
+
* exists.` for 23505, `Failing row contains (7, alice@example.com, …)` for
|
|
97
|
+
* 23502. The `message` field carries only relation/constraint/column NAMES.
|
|
98
|
+
*
|
|
99
|
+
* 'safe' mode keeps those values out of the Turbine error's own message, but
|
|
100
|
+
* the raw driver error used to be attached verbatim as `.cause`, so the values
|
|
101
|
+
* still reached every place an error object gets rendered whole:
|
|
102
|
+
* - `console.error(err)` / an uncaught rejection: Node's error printer walks
|
|
103
|
+
* the cause chain and prints `[cause]: … detail: 'Key (email)=(…)'`. Note
|
|
104
|
+
* that `cause` is ALREADY non-enumerable (the Error constructor defines it
|
|
105
|
+
* that way) and Node prints it anyway, so hiding the property is not a fix;
|
|
106
|
+
* - Sentry and similar sinks link `cause` chains by default and serialize
|
|
107
|
+
* each link's own properties.
|
|
108
|
+
*
|
|
109
|
+
* So in 'safe' mode the cause is replaced by a shallow clone with `detail`
|
|
110
|
+
* swapped for {@link REDACTED_DETAIL}. Cloning rather than mutating leaves the
|
|
111
|
+
* driver's own object untouched (a caller holding it from their own catch sees
|
|
112
|
+
* what the driver produced).
|
|
113
|
+
*
|
|
114
|
+
* The clone must remain a REAL error, which is the part that is easy to get
|
|
115
|
+
* wrong. `Object.create(proto, descriptors)` looks equivalent and is not: V8
|
|
116
|
+
* installs `stack` as an own ACCESSOR whose backing store is the internal
|
|
117
|
+
* [[ErrorData]] slot, and that slot is not a property, so it is not copied. The
|
|
118
|
+
* result reads `cause.stack === undefined`, `util.types.isNativeError(cause) ===
|
|
119
|
+
* false` and `Object.prototype.toString.call(cause) === '[object Object]'`, i.e.
|
|
120
|
+
* every log serializer that does `err.cause.stack.split('\n')` throws a
|
|
121
|
+
* TypeError and Sentry/pino drop the cause's frames. So the clone starts life
|
|
122
|
+
* as `new Error()` (which HAS the slot), is re-prototyped to the driver error's
|
|
123
|
+
* own prototype, and takes the original's stack as a plain string. That keeps
|
|
124
|
+
* `cause instanceof pg.DatabaseError`, `cause.code === '23505'`, the native
|
|
125
|
+
* brand, and the frames.
|
|
126
|
+
*
|
|
127
|
+
* In 'verbose' mode the cause passes through untouched: that mode's documented
|
|
128
|
+
* job is full-fidelity debugging.
|
|
129
|
+
*/
|
|
130
|
+
function redactCauseForMode(cause) {
|
|
131
|
+
if (errorMessageMode === 'verbose')
|
|
132
|
+
return cause;
|
|
133
|
+
if (!cause || typeof cause !== 'object')
|
|
134
|
+
return cause;
|
|
135
|
+
const detail = cause.detail;
|
|
136
|
+
// Nothing value-bearing to remove: return the original object so the common
|
|
137
|
+
// case (a non-pg cause, or a pg error without a detail) allocates nothing and
|
|
138
|
+
// keeps object identity with what the driver threw.
|
|
139
|
+
if (typeof detail !== 'string' || detail.length === 0)
|
|
140
|
+
return cause;
|
|
141
|
+
try {
|
|
142
|
+
const descriptors = Object.getOwnPropertyDescriptors(cause);
|
|
143
|
+
// Replace the descriptor rather than assigning after the clone exists: a
|
|
144
|
+
// non-writable `detail` would make the assignment throw in strict mode
|
|
145
|
+
// (every module here is ESM, so it always would), and losing the cause is
|
|
146
|
+
// worse than paying for one descriptor literal.
|
|
147
|
+
descriptors.detail = {
|
|
148
|
+
value: REDACTED_DETAIL,
|
|
149
|
+
writable: true,
|
|
150
|
+
enumerable: descriptors.detail?.enumerable ?? true,
|
|
151
|
+
configurable: true,
|
|
152
|
+
};
|
|
153
|
+
// Brand check rather than `instanceof Error`, so a driver error thrown from
|
|
154
|
+
// another realm (a worker, a bundled duplicate of pg) is still recognized.
|
|
155
|
+
const isError = Object.prototype.toString.call(cause) === '[object Error]';
|
|
156
|
+
if (!isError)
|
|
157
|
+
return Object.create(Object.getPrototypeOf(cause), descriptors);
|
|
158
|
+
// `new Error()` is the only way to obtain the [[ErrorData]] slot; the
|
|
159
|
+
// prototype is then pointed at the driver error's, so `instanceof` and
|
|
160
|
+
// `.name` behave exactly as before.
|
|
161
|
+
const clone = new Error();
|
|
162
|
+
Object.setPrototypeOf(clone, Object.getPrototypeOf(cause));
|
|
163
|
+
// The clone's own fresh `stack` accessor would otherwise describe THIS
|
|
164
|
+
// function's frames, and the original's accessor cannot be transplanted
|
|
165
|
+
// (it reads the receiver's slot). Copy the rendered string instead, and
|
|
166
|
+
// only when it is one: a driver that stashed a non-string there keeps its
|
|
167
|
+
// own descriptor rather than having a lie written over it.
|
|
168
|
+
const originalStack = cause.stack;
|
|
169
|
+
if (typeof originalStack === 'string') {
|
|
170
|
+
descriptors.stack = { value: originalStack, writable: true, enumerable: false, configurable: true };
|
|
171
|
+
}
|
|
172
|
+
else if (descriptors.stack && typeof descriptors.stack.get === 'function') {
|
|
173
|
+
// An own accessor bound to the ORIGINAL receiver would return undefined
|
|
174
|
+
// here; drop it and let the clone keep its own working one.
|
|
175
|
+
delete descriptors.stack;
|
|
176
|
+
}
|
|
177
|
+
Object.defineProperties(clone, descriptors);
|
|
178
|
+
return clone;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// A cause whose descriptors cannot be replayed (an exotic proxy, a frozen
|
|
182
|
+
// prototype chain) must not turn a database error into a TypeError thrown
|
|
183
|
+
// from an error constructor. Dropping the cause entirely is the safe
|
|
184
|
+
// direction here: 'safe' mode's contract is that no row value escapes.
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
68
188
|
/**
|
|
69
189
|
* Render a user-supplied `where` / `connect` target for a "no row found" error
|
|
70
190
|
* message, honoring the global {@link ErrorMessageMode}. In 'safe' mode (the
|
|
@@ -194,14 +314,30 @@ export class ValidationError extends TurbineError {
|
|
|
194
314
|
}
|
|
195
315
|
/** Thrown when a database connection fails */
|
|
196
316
|
export class ConnectionError extends TurbineError {
|
|
317
|
+
/**
|
|
318
|
+
* The driver code that produced this error: a Postgres SQLSTATE (`28P01`
|
|
319
|
+
* wrong password, `3D000` no such database, `08006` connection failure, ...)
|
|
320
|
+
* or a Node socket/TLS code (`ECONNREFUSED`, `CERT_HAS_EXPIRED`, ...).
|
|
321
|
+
*
|
|
322
|
+
* Exposed because E004 covers causes with very different remedies, and the
|
|
323
|
+
* alternative for a caller who needs to tell "wrong password" from "server
|
|
324
|
+
* down" is matching on `.message` text or reaching into `.cause`, both of
|
|
325
|
+
* which are exactly the untyped handling this error class exists to remove.
|
|
326
|
+
* Undefined when Turbine raised the error itself rather than wrapping a
|
|
327
|
+
* driver error (a malformed connection string, a subscription on an HTTP
|
|
328
|
+
* pool).
|
|
329
|
+
*/
|
|
330
|
+
sqlstate;
|
|
197
331
|
/**
|
|
198
332
|
* @param message human-readable connection failure description.
|
|
199
333
|
* @param options optional pg/driver `cause` to preserve, used when wrapping a
|
|
200
|
-
* connection-class driver error via `wrapPgError
|
|
334
|
+
* connection-class driver error via `wrapPgError`, plus the driver `code`
|
|
335
|
+
* that classified it.
|
|
201
336
|
*/
|
|
202
337
|
constructor(message, options) {
|
|
203
338
|
super(TurbineErrorCode.CONNECTION, message, options);
|
|
204
339
|
this.name = 'ConnectionError';
|
|
340
|
+
this.sqlstate = options?.sqlstate;
|
|
205
341
|
}
|
|
206
342
|
}
|
|
207
343
|
/** Thrown when a relation reference is invalid */
|
|
@@ -255,9 +391,12 @@ export class UniqueConstraintError extends TurbineError {
|
|
|
255
391
|
// PII-safe by default: the raw pg `detail` string contains the
|
|
256
392
|
// conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
|
|
257
393
|
// exists.`). Only append it in 'verbose' mode. In 'safe' mode the
|
|
258
|
-
// message carries keys/constraint/column names only, the
|
|
259
|
-
// `.
|
|
260
|
-
//
|
|
394
|
+
// message carries keys/constraint/column names only, and the same goes
|
|
395
|
+
// for `.cause`, whose `detail` is redacted by the TurbineError base
|
|
396
|
+
// constructor (see redactCauseForMode: an unredacted cause put the
|
|
397
|
+
// values straight back into any log line that prints the error object).
|
|
398
|
+
// The structured `.columns`/`.constraint`/`.column` fields survive in
|
|
399
|
+
// both modes, they carry NAMES, never values.
|
|
261
400
|
const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
|
|
262
401
|
if (detail)
|
|
263
402
|
message += `: ${detail}`;
|
|
@@ -282,9 +421,12 @@ export class ForeignKeyError extends TurbineError {
|
|
|
282
421
|
// PII-safe by default: the raw pg `detail` string contains the
|
|
283
422
|
// conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
|
|
284
423
|
// exists.`). Only append it in 'verbose' mode. In 'safe' mode the
|
|
285
|
-
// message carries keys/constraint/column names only, the
|
|
286
|
-
// `.
|
|
287
|
-
//
|
|
424
|
+
// message carries keys/constraint/column names only, and the same goes
|
|
425
|
+
// for `.cause`, whose `detail` is redacted by the TurbineError base
|
|
426
|
+
// constructor (see redactCauseForMode: an unredacted cause put the
|
|
427
|
+
// values straight back into any log line that prints the error object).
|
|
428
|
+
// The structured `.columns`/`.constraint`/`.column` fields survive in
|
|
429
|
+
// both modes, they carry NAMES, never values.
|
|
288
430
|
const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
|
|
289
431
|
if (detail)
|
|
290
432
|
message += `: ${detail}`;
|
|
@@ -308,9 +450,12 @@ export class NotNullViolationError extends TurbineError {
|
|
|
308
450
|
// PII-safe by default: the raw pg `detail` string contains the
|
|
309
451
|
// conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
|
|
310
452
|
// exists.`). Only append it in 'verbose' mode. In 'safe' mode the
|
|
311
|
-
// message carries keys/constraint/column names only, the
|
|
312
|
-
// `.
|
|
313
|
-
//
|
|
453
|
+
// message carries keys/constraint/column names only, and the same goes
|
|
454
|
+
// for `.cause`, whose `detail` is redacted by the TurbineError base
|
|
455
|
+
// constructor (see redactCauseForMode: an unredacted cause put the
|
|
456
|
+
// values straight back into any log line that prints the error object).
|
|
457
|
+
// The structured `.columns`/`.constraint`/`.column` fields survive in
|
|
458
|
+
// both modes, they carry NAMES, never values.
|
|
314
459
|
const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
|
|
315
460
|
if (detail)
|
|
316
461
|
message += `: ${detail}`;
|
|
@@ -399,9 +544,12 @@ export class CheckConstraintError extends TurbineError {
|
|
|
399
544
|
// PII-safe by default: the raw pg `detail` string contains the
|
|
400
545
|
// conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
|
|
401
546
|
// exists.`). Only append it in 'verbose' mode. In 'safe' mode the
|
|
402
|
-
// message carries keys/constraint/column names only, the
|
|
403
|
-
// `.
|
|
404
|
-
//
|
|
547
|
+
// message carries keys/constraint/column names only, and the same goes
|
|
548
|
+
// for `.cause`, whose `detail` is redacted by the TurbineError base
|
|
549
|
+
// constructor (see redactCauseForMode: an unredacted cause put the
|
|
550
|
+
// values straight back into any log line that prints the error object).
|
|
551
|
+
// The structured `.columns`/`.constraint`/`.column` fields survive in
|
|
552
|
+
// both modes, they carry NAMES, never values.
|
|
405
553
|
const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
|
|
406
554
|
if (detail)
|
|
407
555
|
message += `: ${detail}`;
|
|
@@ -424,9 +572,12 @@ export class ExclusionConstraintError extends TurbineError {
|
|
|
424
572
|
// PII-safe by default: the raw pg `detail` string contains the
|
|
425
573
|
// conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
|
|
426
574
|
// exists.`). Only append it in 'verbose' mode. In 'safe' mode the
|
|
427
|
-
// message carries keys/constraint/column names only, the
|
|
428
|
-
// `.
|
|
429
|
-
//
|
|
575
|
+
// message carries keys/constraint/column names only, and the same goes
|
|
576
|
+
// for `.cause`, whose `detail` is redacted by the TurbineError base
|
|
577
|
+
// constructor (see redactCauseForMode: an unredacted cause put the
|
|
578
|
+
// values straight back into any log line that prints the error object).
|
|
579
|
+
// The structured `.columns`/`.constraint`/`.column` fields survive in
|
|
580
|
+
// both modes, they carry NAMES, never values.
|
|
430
581
|
const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
|
|
431
582
|
if (detail)
|
|
432
583
|
message += `: ${detail}`;
|
|
@@ -556,10 +707,12 @@ function parseColumnsFromDetail(detail) {
|
|
|
556
707
|
return m[1].split(',').map((s) => s.trim());
|
|
557
708
|
}
|
|
558
709
|
/**
|
|
559
|
-
* Connection-class error codes. Covers
|
|
560
|
-
* connection_exception,
|
|
561
|
-
*
|
|
562
|
-
*
|
|
710
|
+
* Connection-class error codes. Covers pg SQLSTATEs (class 08
|
|
711
|
+
* connection_exception, class 28 authorization refusals, invalid_catalog_name,
|
|
712
|
+
* plus a few class-53/57 admin/availability codes) and Node driver-level socket
|
|
713
|
+
* and TLS error codes that arrive on the same `.code` field when the connection
|
|
714
|
+
* never reaches (or never gets past) Postgres. All map to
|
|
715
|
+
* {@link ConnectionError} (E004).
|
|
563
716
|
*
|
|
564
717
|
* `57014` (query_canceled, a server-side `statement_timeout` cancellation) is
|
|
565
718
|
* intentionally NOT here: it maps to {@link TimeoutError} (E002) instead.
|
|
@@ -571,7 +724,22 @@ const CONNECTION_ERROR_CODES = new Set([
|
|
|
571
724
|
'08003', // connection_does_not_exist
|
|
572
725
|
'08004', // sqlserver_rejected_establishment_of_sqlconnection
|
|
573
726
|
'08006', // connection_failure
|
|
727
|
+
'08007', // transaction_resolution_unknown
|
|
574
728
|
'08P01', // protocol_violation
|
|
729
|
+
// pg SQLSTATE class 28: the server answered and REFUSED us. These are the
|
|
730
|
+
// most common first-run failures there are (wrong password, a pg_hba rule
|
|
731
|
+
// that does not cover this user/host), and they used to fall through
|
|
732
|
+
// wrapPgError untouched: the caller got a raw pg `DatabaseError` whose
|
|
733
|
+
// `.code` was `28P01`, i.e. a value from the SQLSTATE namespace on the SAME
|
|
734
|
+
// property Turbine puts `TURBINE_E0NN` in. Every `err.code.startsWith
|
|
735
|
+
// ('TURBINE_')` check, every `instanceof ConnectionError` catch, and the
|
|
736
|
+
// README's "every error is typed" guarantee silently missed the single
|
|
737
|
+
// failure a new user is most likely to hit.
|
|
738
|
+
'28000', // invalid_authorization_specification
|
|
739
|
+
'28P01', // invalid_password
|
|
740
|
+
// The server answered but the database named in the connection string does
|
|
741
|
+
// not exist. Same class of first-run mistake, same escape.
|
|
742
|
+
'3D000', // invalid_catalog_name
|
|
575
743
|
// pg SQLSTATE class 53/57 (server unavailable / shutting down)
|
|
576
744
|
'53300', // too_many_connections
|
|
577
745
|
'57P01', // admin_shutdown
|
|
@@ -582,8 +750,44 @@ const CONNECTION_ERROR_CODES = new Set([
|
|
|
582
750
|
'ECONNRESET',
|
|
583
751
|
'ETIMEDOUT',
|
|
584
752
|
'ENOTFOUND',
|
|
753
|
+
'EAI_AGAIN', // transient DNS failure
|
|
754
|
+
'EHOSTUNREACH',
|
|
755
|
+
'ENETUNREACH',
|
|
585
756
|
'EPIPE',
|
|
757
|
+
// Node TLS handshake failures. They can only happen while OPENING a
|
|
758
|
+
// connection, so classifying them as connection errors cannot mis-tag a
|
|
759
|
+
// query failure, and a managed Postgres with a private CA is the other
|
|
760
|
+
// first-run wall people hit.
|
|
761
|
+
'DEPTH_ZERO_SELF_SIGNED_CERT',
|
|
762
|
+
'SELF_SIGNED_CERT_IN_CHAIN',
|
|
763
|
+
'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
|
|
764
|
+
'CERT_HAS_EXPIRED',
|
|
765
|
+
'ERR_TLS_CERT_ALTNAME_INVALID',
|
|
586
766
|
]);
|
|
767
|
+
/**
|
|
768
|
+
* Actionable next step per connection-class code, appended to the driver's own
|
|
769
|
+
* message. The driver message states WHAT happened ("password authentication
|
|
770
|
+
* failed for user \"postgres\""); these state what to do about it, which is the
|
|
771
|
+
* whole difference between a typed error and a raw one for a first-run failure.
|
|
772
|
+
*
|
|
773
|
+
* A code with no entry here simply gets no hint appended.
|
|
774
|
+
*/
|
|
775
|
+
const CONNECTION_ERROR_HINTS = {
|
|
776
|
+
'28P01': 'The server rejected the credentials. Check the password in your connection string (or DATABASE_URL), including any URL-encoding of special characters.',
|
|
777
|
+
'28000': "The server refused this user/host combination. Check the user name and the server's pg_hba.conf rules for the client address.",
|
|
778
|
+
'3D000': 'The database named in the connection string does not exist. Check the path segment after the host, and create the database if needed.',
|
|
779
|
+
'53300': "The server has no free connection slots. Lower `poolSize` or raise the server's max_connections.",
|
|
780
|
+
'57P03': 'The server is still starting up (or shutting down) and is not accepting connections yet.',
|
|
781
|
+
ECONNREFUSED: 'Nothing is listening on that host and port. Check the server is running and the port is right.',
|
|
782
|
+
ENOTFOUND: 'The host in the connection string could not be resolved. Check the host name, and that the connection string itself is well formed.',
|
|
783
|
+
EAI_AGAIN: 'DNS lookup for the host failed temporarily. Check network/DNS availability, then retry.',
|
|
784
|
+
ETIMEDOUT: 'The connection attempt timed out before the server answered. Check firewall/security-group rules and `connectionTimeoutMs`.',
|
|
785
|
+
DEPTH_ZERO_SELF_SIGNED_CERT: 'The server presented a certificate Node cannot verify. Supply the CA via `ssl: { ca }`.',
|
|
786
|
+
SELF_SIGNED_CERT_IN_CHAIN: 'The server presented a certificate Node cannot verify. Supply the CA via `ssl: { ca }`.',
|
|
787
|
+
UNABLE_TO_VERIFY_LEAF_SIGNATURE: 'The server presented a certificate Node cannot verify. Supply the CA via `ssl: { ca }`.',
|
|
788
|
+
CERT_HAS_EXPIRED: "The server's TLS certificate has expired. Renew it, or supply the correct CA via `ssl: { ca }`.",
|
|
789
|
+
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.",
|
|
790
|
+
};
|
|
587
791
|
/**
|
|
588
792
|
* Translate a pg driver error into a typed Turbine error.
|
|
589
793
|
* If the error doesn't match a known constraint code, returns it unchanged.
|
|
@@ -597,6 +801,8 @@ const CONNECTION_ERROR_CODES = new Set([
|
|
|
597
801
|
* 40P01 (deadlock_detected) -> DeadlockError (retryable)
|
|
598
802
|
* 40001 (serialization_failure) -> SerializationFailureError (retryable)
|
|
599
803
|
* 57014 (query_canceled) -> TimeoutError (server-side statement_timeout)
|
|
804
|
+
* 28P01 / 28000 (auth refused) -> ConnectionError, with a remediation hint
|
|
805
|
+
* 3D000 (no such database) -> ConnectionError, with a remediation hint
|
|
600
806
|
* connection-class codes -> ConnectionError (see CONNECTION_ERROR_CODES)
|
|
601
807
|
*
|
|
602
808
|
* The original pg error is preserved as `.cause` on the wrapped error.
|
|
@@ -661,9 +867,14 @@ export function wrapPgError(err) {
|
|
|
661
867
|
default:
|
|
662
868
|
if (CONNECTION_ERROR_CODES.has(e.code)) {
|
|
663
869
|
const pgMessage = typeof e.message === 'string' && e.message.length > 0 ? e.message : undefined;
|
|
664
|
-
|
|
870
|
+
// Own-property lookup only: `e.code` is driver-controlled text, and a
|
|
871
|
+
// plain-object map would happily resolve `constructor` or `toString`
|
|
872
|
+
// to a function and interpolate it into the message.
|
|
873
|
+
const hint = Object.hasOwn(CONNECTION_ERROR_HINTS, e.code) ? CONNECTION_ERROR_HINTS[e.code] : undefined;
|
|
874
|
+
const head = pgMessage
|
|
665
875
|
? `[turbine] Database connection error: ${pgMessage}`
|
|
666
|
-
: `[turbine] Database connection error (${e.code})
|
|
876
|
+
: `[turbine] Database connection error (${e.code})`;
|
|
877
|
+
return new ConnectionError(hint ? `${head} (${e.code}) ${hint}` : head, { cause: err, sqlstate: e.code });
|
|
667
878
|
}
|
|
668
879
|
return err;
|
|
669
880
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -37,14 +37,14 @@ export { alloydb, cockroachdb, postgresql, timescale, yugabytedb } from './adapt
|
|
|
37
37
|
export { type Middleware, type MiddlewareNext, type MiddlewareParams, type PgCompatPool, type PgCompatPoolClient, type PgCompatQueryResult, type PlanCacheMode, type RetryOptions, TransactionClient, type TransactionOptions, TurbineClient, type TurbineConfig, type TurbineDriver, withRetry, } from './client.js';
|
|
38
38
|
export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, ColumnTypeInput, CreateIndexStatementInput, CreateTableStatementInput, Dialect, DialectIntrospector, DialectMigrator, DialectName, InsertStatementInput, IntrospectOptions as DialectIntrospectOptions, ResultStrategy, StreamableConnection, UpsertStatementInput, } from './dialect.js';
|
|
39
39
|
export { postgresDialect } from './dialect.js';
|
|
40
|
-
export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, type ErrorMessageMode, ExclusionConstraintError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, OptimisticLockError, PipelineError, type PipelineResultSlot, ReadOnlyError, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, UnsupportedFeatureError, ValidationError, wrapPgError, } from './errors.js';
|
|
40
|
+
export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, type ErrorMessageMode, ExclusionConstraintError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, OptimisticLockError, PipelineError, type PipelineResultSlot, REDACTED_DETAIL, ReadOnlyError, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, UnsupportedFeatureError, ValidationError, wrapPgError, } from './errors.js';
|
|
41
41
|
export { type GenerateOptions, generate } from './generate.js';
|
|
42
42
|
export { type IntrospectOptions, introspect } from './introspect.js';
|
|
43
43
|
export { executeNestedCreate, executeNestedUpdate, hasRelationFields, type NestedWriteContext, } from './nested-write.js';
|
|
44
44
|
export { HttpJsonSink, type HttpJsonSinkOptions, type MetricsFlushBatch, type MetricsFlushRow, type ObserveConfig, type ObserveHandle, type ObserveSink, PgMetricsSink, type PgMetricsSinkOptions, } from './observe.js';
|
|
45
45
|
export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
|
|
46
46
|
export { fingerprintPrismaSchema } from './prisma-schema-fingerprint.js';
|
|
47
|
-
export { type AggregateArgs, type AggregateResult, type ArrayFilter, AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type GroupByResult, type HavingClause, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderByObject, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TemporalInfinityReading, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithOrderByObject, type WithResult, } from './query/index.js';
|
|
47
|
+
export { type AggregateArgs, type AggregateResult, type ArrayFilter, AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type GroupByResult, type HavingClause, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderByObject, type OrderDirection, type PrivilegeOption, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TemporalInfinityReading, type TextSearchFilter, type TypedWithClause, UNSAFE, type Unsafe, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithOrderByObject, type WithResult, } from './query/index.js';
|
|
48
48
|
export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
|
|
49
49
|
export type { CheckMetadata, ColumnMetadata, IndexMetadata, PrismaCompatMap, PrismaModelMap, PrismaRelationMap, PrismaSchemaSource, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
50
50
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, withDbFieldNames, } from './schema.js';
|
package/dist/index.js
CHANGED
|
@@ -37,7 +37,7 @@ export { alloydb, cockroachdb, postgresql, timescale, yugabytedb } from './adapt
|
|
|
37
37
|
export { TransactionClient, TurbineClient, withRetry, } from './client.js';
|
|
38
38
|
export { postgresDialect } from './dialect.js';
|
|
39
39
|
// Error types
|
|
40
|
-
export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, ExclusionConstraintError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, OptimisticLockError, PipelineError, ReadOnlyError, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, UnsupportedFeatureError, ValidationError, wrapPgError, } from './errors.js';
|
|
40
|
+
export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, ExclusionConstraintError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, OptimisticLockError, PipelineError, REDACTED_DETAIL, ReadOnlyError, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, UnsupportedFeatureError, ValidationError, wrapPgError, } from './errors.js';
|
|
41
41
|
// Code generation
|
|
42
42
|
export { generate } from './generate.js';
|
|
43
43
|
// Introspection
|
|
@@ -51,7 +51,10 @@ export { executePipeline, pipelineSupported } from './pipeline.js';
|
|
|
51
51
|
// Prisma-schema fingerprint (provenance on a generated PRISMA_MAP)
|
|
52
52
|
export { fingerprintPrismaSchema } from './prisma-schema-fingerprint.js';
|
|
53
53
|
// Query builder
|
|
54
|
-
|
|
54
|
+
// The privilege sentinel. `skipGlobalFilters` / `includePii` /
|
|
55
|
+
// `allowFullTableScan` are unlocked by THIS VALUE and nothing else, so a
|
|
56
|
+
// request body spread into query args cannot enable them (JSON has no symbols).
|
|
57
|
+
export { AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, QueryInterface, UNSAFE, } from './query/index.js';
|
|
55
58
|
// Realtime, LISTEN/NOTIFY pub/sub
|
|
56
59
|
export { validateChannel } from './realtime.js';
|
|
57
60
|
// Schema utilities
|
|
@@ -357,14 +357,37 @@ export async function runPipelined(client, queries, options = {}) {
|
|
|
357
357
|
connection.sync();
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
|
-
if (connection.stream.uncork) {
|
|
361
|
-
connection.stream.uncork();
|
|
362
|
-
}
|
|
363
360
|
}
|
|
364
361
|
catch (err) {
|
|
362
|
+
// The send path threw PART WAY THROUGH an extended-query sequence, so the
|
|
363
|
+
// backend has Parse/Bind bytes for the preceding queries and will never
|
|
364
|
+
// receive their Sync. `valueMapper: prepareValue` runs synchronously
|
|
365
|
+
// inside `bind`, so any param with a throwing `toPostgres`/`toJSON`, or a
|
|
366
|
+
// circular reference, lands here.
|
|
367
|
+
//
|
|
368
|
+
// Returning that connection to the pool is what made this severe: the pool
|
|
369
|
+
// reports it idle and healthy, the NEXT borrower checks it out, and its
|
|
370
|
+
// first query hangs forever waiting on a ReadyForQuery for a sequence that
|
|
371
|
+
// was never completed. The failure surfaces in an unrelated query with
|
|
372
|
+
// nothing pointing back here. Destroy the connection instead: one dead
|
|
373
|
+
// socket is recoverable, a wedged pool slot is not.
|
|
374
|
+
//
|
|
375
|
+
// Destroying the socket is the same move the timeout path above already
|
|
376
|
+
// makes, and node-postgres drops a client whose stream errored rather than
|
|
377
|
+
// returning it to the pool.
|
|
378
|
+
if (connection.stream.destroy) {
|
|
379
|
+
connection.stream.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
380
|
+
}
|
|
365
381
|
cleanup();
|
|
366
382
|
reject(err);
|
|
367
383
|
}
|
|
384
|
+
finally {
|
|
385
|
+
// Always uncork, including on the throw path: leaving the stream corked
|
|
386
|
+
// strands the buffered bytes and the socket with them.
|
|
387
|
+
if (connection.stream.uncork) {
|
|
388
|
+
connection.stream.uncork();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
368
391
|
});
|
|
369
392
|
}
|
|
370
393
|
// ---------------------------------------------------------------------------
|
package/dist/pipeline.js
CHANGED
|
@@ -86,8 +86,21 @@ export async function executePipeline(pool, queries, options) {
|
|
|
86
86
|
if (queries.length === 0) {
|
|
87
87
|
return [];
|
|
88
88
|
}
|
|
89
|
-
// Acquire a single client, reused for both capability check and execution
|
|
90
|
-
|
|
89
|
+
// Acquire a single client, reused for both capability check and execution.
|
|
90
|
+
// The checkout is wrapped because a connection-level failure (28P01 wrong
|
|
91
|
+
// password, an unverifiable TLS cert, ECONNREFUSED) happens HERE, before any
|
|
92
|
+
// query runs. Unwrapped it escapes as a raw pg DatabaseError whose `.code`
|
|
93
|
+
// holds a SQLSTATE, which is the same property Turbine puts TURBINE_E0NN in,
|
|
94
|
+
// so a caller switching on `.code` silently receives a value from a foreign
|
|
95
|
+
// namespace. The transaction and nested-write checkouts were wrapped for this
|
|
96
|
+
// reason; this was the last sibling still bare.
|
|
97
|
+
let client;
|
|
98
|
+
try {
|
|
99
|
+
client = await pool.connect();
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
throw wrapPgError(err);
|
|
103
|
+
}
|
|
91
104
|
try {
|
|
92
105
|
if (supportsExtendedPipeline(client)) {
|
|
93
106
|
// Real pipeline path, uses extended-query protocol wire methods
|
package/dist/powql.d.ts
CHANGED
|
@@ -349,6 +349,18 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
349
349
|
* contract): for identical cross-engine results pass `nulls: 'last'`
|
|
350
350
|
* explicitly on Postgres, which defaults nulls-first for `desc`.
|
|
351
351
|
*/
|
|
352
|
+
/**
|
|
353
|
+
* Validate one orderBy direction token and return the PowQL keyword.
|
|
354
|
+
*
|
|
355
|
+
* Every direction site on this engine used to be spelled
|
|
356
|
+
* `x === 'desc' ? 'desc' : 'asc'`, so `'DESC'`, a token the CORE explicitly
|
|
357
|
+
* accepts and honours, compiled to `asc` here: identical application code
|
|
358
|
+
* sorted one way on Postgres and silently the opposite way on PowDB. The rule
|
|
359
|
+
* is core's {@link assertDirectionToken}, reused rather than re-derived, so
|
|
360
|
+
* the two engines cannot drift again. `undefined` stays "not specified"
|
|
361
|
+
* (defaults asc); `null` is a VALUE and is refused like any other bad token.
|
|
362
|
+
*/
|
|
363
|
+
private dirKeyword;
|
|
352
364
|
private buildOrder;
|
|
353
365
|
/** Compile one {@link JsonPathOrderBy} entry to `order .col->$n asc` (+ optional numeric cast). */
|
|
354
366
|
private buildJsonPathOrder;
|