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.
Files changed (84) hide show
  1. package/README.md +71 -27
  2. package/dist/cjs/cli/config.d.ts +40 -0
  3. package/dist/cjs/cli/config.js +74 -2
  4. package/dist/cjs/cli/index.d.ts +85 -1
  5. package/dist/cjs/cli/index.js +374 -24
  6. package/dist/cjs/cli/mcp.d.ts +8 -0
  7. package/dist/cjs/cli/mcp.js +448 -29
  8. package/dist/cjs/cli/pii-tags.d.ts +64 -9
  9. package/dist/cjs/cli/pii-tags.js +218 -39
  10. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  11. package/dist/cjs/cli/studio.d.ts +23 -0
  12. package/dist/cjs/cli/studio.js +126 -53
  13. package/dist/cjs/cli/ui.d.ts +15 -1
  14. package/dist/cjs/cli/ui.js +19 -5
  15. package/dist/cjs/client.js +248 -11
  16. package/dist/cjs/errors.d.ts +38 -1
  17. package/dist/cjs/errors.js +235 -24
  18. package/dist/cjs/index.d.ts +2 -2
  19. package/dist/cjs/index.js +7 -2
  20. package/dist/cjs/pipeline-submittable.js +26 -3
  21. package/dist/cjs/pipeline.js +15 -2
  22. package/dist/cjs/powql.d.ts +12 -0
  23. package/dist/cjs/powql.js +46 -21
  24. package/dist/cjs/prisma-compat.d.ts +15 -5
  25. package/dist/cjs/prisma-compat.js +273 -78
  26. package/dist/cjs/query/aggregates.d.ts +1 -1
  27. package/dist/cjs/query/aggregates.js +24 -10
  28. package/dist/cjs/query/batched-loader.d.ts +9 -4
  29. package/dist/cjs/query/batched-loader.js +4 -1
  30. package/dist/cjs/query/builder.d.ts +47 -0
  31. package/dist/cjs/query/builder.js +149 -21
  32. package/dist/cjs/query/index.d.ts +3 -1
  33. package/dist/cjs/query/index.js +7 -1
  34. package/dist/cjs/query/option-surface.d.ts +11 -0
  35. package/dist/cjs/query/option-surface.js +13 -0
  36. package/dist/cjs/query/relations.d.ts +8 -0
  37. package/dist/cjs/query/relations.js +21 -1
  38. package/dist/cjs/query/types.d.ts +152 -18
  39. package/dist/cjs/query/types.js +212 -1
  40. package/dist/cjs/query/where.d.ts +3 -3
  41. package/dist/cjs/query/where.js +8 -2
  42. package/dist/cjs/query/writes.js +10 -9
  43. package/dist/cli/config.d.ts +40 -0
  44. package/dist/cli/config.js +73 -2
  45. package/dist/cli/index.d.ts +85 -1
  46. package/dist/cli/index.js +373 -27
  47. package/dist/cli/mcp.d.ts +8 -0
  48. package/dist/cli/mcp.js +448 -29
  49. package/dist/cli/pii-tags.d.ts +64 -9
  50. package/dist/cli/pii-tags.js +217 -39
  51. package/dist/cli/studio-ui.generated.js +1 -1
  52. package/dist/cli/studio.d.ts +23 -0
  53. package/dist/cli/studio.js +125 -53
  54. package/dist/cli/ui.d.ts +15 -1
  55. package/dist/cli/ui.js +18 -4
  56. package/dist/client.js +250 -13
  57. package/dist/errors.d.ts +38 -1
  58. package/dist/errors.js +234 -23
  59. package/dist/index.d.ts +2 -2
  60. package/dist/index.js +5 -2
  61. package/dist/pipeline-submittable.js +26 -3
  62. package/dist/pipeline.js +15 -2
  63. package/dist/powql.d.ts +12 -0
  64. package/dist/powql.js +46 -21
  65. package/dist/prisma-compat.d.ts +15 -5
  66. package/dist/prisma-compat.js +274 -79
  67. package/dist/query/aggregates.d.ts +1 -1
  68. package/dist/query/aggregates.js +24 -10
  69. package/dist/query/batched-loader.d.ts +9 -4
  70. package/dist/query/batched-loader.js +4 -1
  71. package/dist/query/builder.d.ts +47 -0
  72. package/dist/query/builder.js +148 -21
  73. package/dist/query/index.d.ts +3 -1
  74. package/dist/query/index.js +2 -0
  75. package/dist/query/option-surface.d.ts +11 -0
  76. package/dist/query/option-surface.js +13 -0
  77. package/dist/query/relations.d.ts +8 -0
  78. package/dist/query/relations.js +21 -1
  79. package/dist/query/types.d.ts +152 -18
  80. package/dist/query/types.js +207 -2
  81. package/dist/query/where.d.ts +3 -3
  82. package/dist/query/where.js +8 -2
  83. package/dist/query/writes.js +10 -9
  84. 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
- super(formatErrorMessage(code, message), options);
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 structured
259
- // `.columns`/`.constraint`/`.column` fields and `.cause` still expose
260
- // the full detail for programmatic use.
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 structured
286
- // `.columns`/`.constraint`/`.column` fields and `.cause` still expose
287
- // the full detail for programmatic use.
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 structured
312
- // `.columns`/`.constraint`/`.column` fields and `.cause` still expose
313
- // the full detail for programmatic use.
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 structured
403
- // `.columns`/`.constraint`/`.column` fields and `.cause` still expose
404
- // the full detail for programmatic use.
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 structured
428
- // `.columns`/`.constraint`/`.column` fields and `.cause` still expose
429
- // the full detail for programmatic use.
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 both pg SQLSTATEs (class 08
560
- * connection_exception, plus a few class-53/57 admin/availability codes) and
561
- * Node driver-level error codes that arrive on the same `.code` field when the
562
- * socket never reaches Postgres. All map to {@link ConnectionError} (E004).
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
- return new ConnectionError(pgMessage
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})`, { cause: err });
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
- 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, } from './query/index.js';
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
- const client = await pool.connect();
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;