turbine-orm 0.61.0 → 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 (82) hide show
  1. package/README.md +65 -21
  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 +323 -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 +186 -3
  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.js +15 -2
  21. package/dist/cjs/powql.d.ts +12 -0
  22. package/dist/cjs/powql.js +46 -21
  23. package/dist/cjs/prisma-compat.d.ts +15 -5
  24. package/dist/cjs/prisma-compat.js +273 -78
  25. package/dist/cjs/query/aggregates.d.ts +1 -1
  26. package/dist/cjs/query/aggregates.js +24 -10
  27. package/dist/cjs/query/batched-loader.d.ts +9 -4
  28. package/dist/cjs/query/batched-loader.js +4 -1
  29. package/dist/cjs/query/builder.d.ts +47 -0
  30. package/dist/cjs/query/builder.js +125 -21
  31. package/dist/cjs/query/index.d.ts +3 -1
  32. package/dist/cjs/query/index.js +7 -1
  33. package/dist/cjs/query/option-surface.d.ts +11 -0
  34. package/dist/cjs/query/option-surface.js +13 -0
  35. package/dist/cjs/query/relations.d.ts +8 -0
  36. package/dist/cjs/query/relations.js +21 -1
  37. package/dist/cjs/query/types.d.ts +152 -18
  38. package/dist/cjs/query/types.js +212 -1
  39. package/dist/cjs/query/where.d.ts +3 -3
  40. package/dist/cjs/query/where.js +8 -2
  41. package/dist/cjs/query/writes.js +10 -9
  42. package/dist/cli/config.d.ts +40 -0
  43. package/dist/cli/config.js +73 -2
  44. package/dist/cli/index.d.ts +85 -1
  45. package/dist/cli/index.js +321 -26
  46. package/dist/cli/mcp.d.ts +8 -0
  47. package/dist/cli/mcp.js +448 -29
  48. package/dist/cli/pii-tags.d.ts +64 -9
  49. package/dist/cli/pii-tags.js +217 -39
  50. package/dist/cli/studio-ui.generated.js +1 -1
  51. package/dist/cli/studio.d.ts +23 -0
  52. package/dist/cli/studio.js +125 -53
  53. package/dist/cli/ui.d.ts +15 -1
  54. package/dist/cli/ui.js +18 -4
  55. package/dist/client.js +187 -4
  56. package/dist/errors.d.ts +38 -1
  57. package/dist/errors.js +234 -23
  58. package/dist/index.d.ts +2 -2
  59. package/dist/index.js +5 -2
  60. package/dist/pipeline.js +15 -2
  61. package/dist/powql.d.ts +12 -0
  62. package/dist/powql.js +46 -21
  63. package/dist/prisma-compat.d.ts +15 -5
  64. package/dist/prisma-compat.js +274 -79
  65. package/dist/query/aggregates.d.ts +1 -1
  66. package/dist/query/aggregates.js +24 -10
  67. package/dist/query/batched-loader.d.ts +9 -4
  68. package/dist/query/batched-loader.js +4 -1
  69. package/dist/query/builder.d.ts +47 -0
  70. package/dist/query/builder.js +124 -21
  71. package/dist/query/index.d.ts +3 -1
  72. package/dist/query/index.js +2 -0
  73. package/dist/query/option-surface.d.ts +11 -0
  74. package/dist/query/option-surface.js +13 -0
  75. package/dist/query/relations.d.ts +8 -0
  76. package/dist/query/relations.js +21 -1
  77. package/dist/query/types.d.ts +152 -18
  78. package/dist/query/types.js +207 -2
  79. package/dist/query/where.d.ts +3 -3
  80. package/dist/query/where.js +8 -2
  81. package/dist/query/writes.js +10 -9
  82. package/package.json +13 -3
@@ -6,7 +6,7 @@
6
6
  * All Turbine errors extend TurbineError which includes a `code` property.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.ReadOnlyError = exports.UnsupportedFeatureError = exports.OptimisticLockError = exports.PipelineError = exports.ExclusionConstraintError = exports.CheckConstraintError = exports.SerializationFailureError = exports.DeadlockError = exports.NotNullViolationError = exports.ForeignKeyError = exports.UniqueConstraintError = exports.CircularRelationError = exports.MigrationError = exports.RelationError = exports.ConnectionError = exports.ValidationError = exports.TimeoutError = exports.NotFoundError = exports.TurbineError = exports.TurbineErrorCode = void 0;
9
+ exports.ReadOnlyError = exports.UnsupportedFeatureError = exports.OptimisticLockError = exports.PipelineError = exports.ExclusionConstraintError = exports.CheckConstraintError = exports.SerializationFailureError = exports.DeadlockError = exports.NotNullViolationError = exports.ForeignKeyError = exports.UniqueConstraintError = exports.CircularRelationError = exports.MigrationError = exports.RelationError = exports.ConnectionError = exports.ValidationError = exports.TimeoutError = exports.NotFoundError = exports.REDACTED_DETAIL = exports.TurbineError = exports.TurbineErrorCode = void 0;
10
10
  exports.setErrorMessageMode = setErrorMessageMode;
11
11
  exports.getErrorMessageMode = getErrorMessageMode;
12
12
  exports.describeTargetForMessage = describeTargetForMessage;
@@ -50,7 +50,13 @@ function formatErrorMessage(code, message) {
50
50
  class TurbineError extends Error {
51
51
  code;
52
52
  constructor(code, message, options) {
53
- super(formatErrorMessage(code, message), options);
53
+ // The cause is redacted in 'safe' mode (see redactCauseForMode). Only pass
54
+ // an options object through when the caller actually supplied a `cause`
55
+ // key: `new Error(msg, {})` defines no `cause` own property, while
56
+ // `new Error(msg, { cause: undefined })` defines one whose value is
57
+ // undefined, and error-serializing sinks tell those two apart.
58
+ const opts = options && 'cause' in options ? { ...options, cause: redactCauseForMode(options.cause) } : options;
59
+ super(formatErrorMessage(code, message), opts);
54
60
  this.name = 'TurbineError';
55
61
  this.code = code;
56
62
  }
@@ -65,6 +71,19 @@ let errorMessageMode = 'safe';
65
71
  * clause (e.g. `where: { id, email }`). Values are redacted.
66
72
  * - `'verbose'`: the message includes the full JSON-serialized where
67
73
  * clause (e.g. `where: {"id":1,"email":"alice@x.com"}`).
74
+ *
75
+ * SCOPE, stated precisely because the useful version of this contract is the
76
+ * one that is true. 'safe' mode redacts row values from the surfaces Turbine
77
+ * OWNS: its own error messages, and the `detail` field of a driver error it
78
+ * wraps and attaches as `.cause` (see redactCauseForMode).
79
+ *
80
+ * It is NOT a blanket guarantee that no row value can be reached from a thrown
81
+ * error. A driver error whose SQLSTATE {@link wrapPgError} does not classify is
82
+ * returned UNCHANGED, and some of those carry a value in the `message` field
83
+ * itself, where nothing can be removed without destroying the diagnosis:
84
+ * `22P02 invalid input syntax for type integer: "alice@example.com"` is the
85
+ * common one. Treat 'safe' mode as removing Turbine's own contribution to the
86
+ * leak, not as a log-scrubbing boundary.
68
87
  */
69
88
  function setErrorMessageMode(mode) {
70
89
  errorMessageMode = mode;
@@ -73,6 +92,107 @@ function setErrorMessageMode(mode) {
73
92
  function getErrorMessageMode() {
74
93
  return errorMessageMode;
75
94
  }
95
+ /**
96
+ * The marker left where a driver `detail` string was removed in 'safe' mode.
97
+ * Re-exported from the package root so tests and callers writing log
98
+ * assertions can match on it without hardcoding the wording.
99
+ */
100
+ exports.REDACTED_DETAIL = '[redacted by turbine errorMessages:"safe"]';
101
+ /**
102
+ * Postgres puts the CONFLICTING ROW VALUES in the `detail` field of a
103
+ * constraint error, and nowhere else: `Key (email)=(alice@example.com) already
104
+ * exists.` for 23505, `Failing row contains (7, alice@example.com, …)` for
105
+ * 23502. The `message` field carries only relation/constraint/column NAMES.
106
+ *
107
+ * 'safe' mode keeps those values out of the Turbine error's own message, but
108
+ * the raw driver error used to be attached verbatim as `.cause`, so the values
109
+ * still reached every place an error object gets rendered whole:
110
+ * - `console.error(err)` / an uncaught rejection: Node's error printer walks
111
+ * the cause chain and prints `[cause]: … detail: 'Key (email)=(…)'`. Note
112
+ * that `cause` is ALREADY non-enumerable (the Error constructor defines it
113
+ * that way) and Node prints it anyway, so hiding the property is not a fix;
114
+ * - Sentry and similar sinks link `cause` chains by default and serialize
115
+ * each link's own properties.
116
+ *
117
+ * So in 'safe' mode the cause is replaced by a shallow clone with `detail`
118
+ * swapped for {@link REDACTED_DETAIL}. Cloning rather than mutating leaves the
119
+ * driver's own object untouched (a caller holding it from their own catch sees
120
+ * what the driver produced).
121
+ *
122
+ * The clone must remain a REAL error, which is the part that is easy to get
123
+ * wrong. `Object.create(proto, descriptors)` looks equivalent and is not: V8
124
+ * installs `stack` as an own ACCESSOR whose backing store is the internal
125
+ * [[ErrorData]] slot, and that slot is not a property, so it is not copied. The
126
+ * result reads `cause.stack === undefined`, `util.types.isNativeError(cause) ===
127
+ * false` and `Object.prototype.toString.call(cause) === '[object Object]'`, i.e.
128
+ * every log serializer that does `err.cause.stack.split('\n')` throws a
129
+ * TypeError and Sentry/pino drop the cause's frames. So the clone starts life
130
+ * as `new Error()` (which HAS the slot), is re-prototyped to the driver error's
131
+ * own prototype, and takes the original's stack as a plain string. That keeps
132
+ * `cause instanceof pg.DatabaseError`, `cause.code === '23505'`, the native
133
+ * brand, and the frames.
134
+ *
135
+ * In 'verbose' mode the cause passes through untouched: that mode's documented
136
+ * job is full-fidelity debugging.
137
+ */
138
+ function redactCauseForMode(cause) {
139
+ if (errorMessageMode === 'verbose')
140
+ return cause;
141
+ if (!cause || typeof cause !== 'object')
142
+ return cause;
143
+ const detail = cause.detail;
144
+ // Nothing value-bearing to remove: return the original object so the common
145
+ // case (a non-pg cause, or a pg error without a detail) allocates nothing and
146
+ // keeps object identity with what the driver threw.
147
+ if (typeof detail !== 'string' || detail.length === 0)
148
+ return cause;
149
+ try {
150
+ const descriptors = Object.getOwnPropertyDescriptors(cause);
151
+ // Replace the descriptor rather than assigning after the clone exists: a
152
+ // non-writable `detail` would make the assignment throw in strict mode
153
+ // (every module here is ESM, so it always would), and losing the cause is
154
+ // worse than paying for one descriptor literal.
155
+ descriptors.detail = {
156
+ value: exports.REDACTED_DETAIL,
157
+ writable: true,
158
+ enumerable: descriptors.detail?.enumerable ?? true,
159
+ configurable: true,
160
+ };
161
+ // Brand check rather than `instanceof Error`, so a driver error thrown from
162
+ // another realm (a worker, a bundled duplicate of pg) is still recognized.
163
+ const isError = Object.prototype.toString.call(cause) === '[object Error]';
164
+ if (!isError)
165
+ return Object.create(Object.getPrototypeOf(cause), descriptors);
166
+ // `new Error()` is the only way to obtain the [[ErrorData]] slot; the
167
+ // prototype is then pointed at the driver error's, so `instanceof` and
168
+ // `.name` behave exactly as before.
169
+ const clone = new Error();
170
+ Object.setPrototypeOf(clone, Object.getPrototypeOf(cause));
171
+ // The clone's own fresh `stack` accessor would otherwise describe THIS
172
+ // function's frames, and the original's accessor cannot be transplanted
173
+ // (it reads the receiver's slot). Copy the rendered string instead, and
174
+ // only when it is one: a driver that stashed a non-string there keeps its
175
+ // own descriptor rather than having a lie written over it.
176
+ const originalStack = cause.stack;
177
+ if (typeof originalStack === 'string') {
178
+ descriptors.stack = { value: originalStack, writable: true, enumerable: false, configurable: true };
179
+ }
180
+ else if (descriptors.stack && typeof descriptors.stack.get === 'function') {
181
+ // An own accessor bound to the ORIGINAL receiver would return undefined
182
+ // here; drop it and let the clone keep its own working one.
183
+ delete descriptors.stack;
184
+ }
185
+ Object.defineProperties(clone, descriptors);
186
+ return clone;
187
+ }
188
+ catch {
189
+ // A cause whose descriptors cannot be replayed (an exotic proxy, a frozen
190
+ // prototype chain) must not turn a database error into a TypeError thrown
191
+ // from an error constructor. Dropping the cause entirely is the safe
192
+ // direction here: 'safe' mode's contract is that no row value escapes.
193
+ return undefined;
194
+ }
195
+ }
76
196
  /**
77
197
  * Render a user-supplied `where` / `connect` target for a "no row found" error
78
198
  * message, honoring the global {@link ErrorMessageMode}. In 'safe' mode (the
@@ -205,14 +325,30 @@ class ValidationError extends TurbineError {
205
325
  exports.ValidationError = ValidationError;
206
326
  /** Thrown when a database connection fails */
207
327
  class ConnectionError extends TurbineError {
328
+ /**
329
+ * The driver code that produced this error: a Postgres SQLSTATE (`28P01`
330
+ * wrong password, `3D000` no such database, `08006` connection failure, ...)
331
+ * or a Node socket/TLS code (`ECONNREFUSED`, `CERT_HAS_EXPIRED`, ...).
332
+ *
333
+ * Exposed because E004 covers causes with very different remedies, and the
334
+ * alternative for a caller who needs to tell "wrong password" from "server
335
+ * down" is matching on `.message` text or reaching into `.cause`, both of
336
+ * which are exactly the untyped handling this error class exists to remove.
337
+ * Undefined when Turbine raised the error itself rather than wrapping a
338
+ * driver error (a malformed connection string, a subscription on an HTTP
339
+ * pool).
340
+ */
341
+ sqlstate;
208
342
  /**
209
343
  * @param message human-readable connection failure description.
210
344
  * @param options optional pg/driver `cause` to preserve, used when wrapping a
211
- * connection-class driver error via `wrapPgError`.
345
+ * connection-class driver error via `wrapPgError`, plus the driver `code`
346
+ * that classified it.
212
347
  */
213
348
  constructor(message, options) {
214
349
  super(exports.TurbineErrorCode.CONNECTION, message, options);
215
350
  this.name = 'ConnectionError';
351
+ this.sqlstate = options?.sqlstate;
216
352
  }
217
353
  }
218
354
  exports.ConnectionError = ConnectionError;
@@ -270,9 +406,12 @@ class UniqueConstraintError extends TurbineError {
270
406
  // PII-safe by default: the raw pg `detail` string contains the
271
407
  // conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
272
408
  // exists.`). Only append it in 'verbose' mode. In 'safe' mode the
273
- // message carries keys/constraint/column names only, the structured
274
- // `.columns`/`.constraint`/`.column` fields and `.cause` still expose
275
- // the full detail for programmatic use.
409
+ // message carries keys/constraint/column names only, and the same goes
410
+ // for `.cause`, whose `detail` is redacted by the TurbineError base
411
+ // constructor (see redactCauseForMode: an unredacted cause put the
412
+ // values straight back into any log line that prints the error object).
413
+ // The structured `.columns`/`.constraint`/`.column` fields survive in
414
+ // both modes, they carry NAMES, never values.
276
415
  const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
277
416
  if (detail)
278
417
  message += `: ${detail}`;
@@ -298,9 +437,12 @@ class ForeignKeyError extends TurbineError {
298
437
  // PII-safe by default: the raw pg `detail` string contains the
299
438
  // conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
300
439
  // exists.`). Only append it in 'verbose' mode. In 'safe' mode the
301
- // message carries keys/constraint/column names only, the structured
302
- // `.columns`/`.constraint`/`.column` fields and `.cause` still expose
303
- // the full detail for programmatic use.
440
+ // message carries keys/constraint/column names only, and the same goes
441
+ // for `.cause`, whose `detail` is redacted by the TurbineError base
442
+ // constructor (see redactCauseForMode: an unredacted cause put the
443
+ // values straight back into any log line that prints the error object).
444
+ // The structured `.columns`/`.constraint`/`.column` fields survive in
445
+ // both modes, they carry NAMES, never values.
304
446
  const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
305
447
  if (detail)
306
448
  message += `: ${detail}`;
@@ -325,9 +467,12 @@ class NotNullViolationError extends TurbineError {
325
467
  // PII-safe by default: the raw pg `detail` string contains the
326
468
  // conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
327
469
  // exists.`). Only append it in 'verbose' mode. In 'safe' mode the
328
- // message carries keys/constraint/column names only, the structured
329
- // `.columns`/`.constraint`/`.column` fields and `.cause` still expose
330
- // the full detail for programmatic use.
470
+ // message carries keys/constraint/column names only, and the same goes
471
+ // for `.cause`, whose `detail` is redacted by the TurbineError base
472
+ // constructor (see redactCauseForMode: an unredacted cause put the
473
+ // values straight back into any log line that prints the error object).
474
+ // The structured `.columns`/`.constraint`/`.column` fields survive in
475
+ // both modes, they carry NAMES, never values.
331
476
  const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
332
477
  if (detail)
333
478
  message += `: ${detail}`;
@@ -419,9 +564,12 @@ class CheckConstraintError extends TurbineError {
419
564
  // PII-safe by default: the raw pg `detail` string contains the
420
565
  // conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
421
566
  // exists.`). Only append it in 'verbose' mode. In 'safe' mode the
422
- // message carries keys/constraint/column names only, the structured
423
- // `.columns`/`.constraint`/`.column` fields and `.cause` still expose
424
- // the full detail for programmatic use.
567
+ // message carries keys/constraint/column names only, and the same goes
568
+ // for `.cause`, whose `detail` is redacted by the TurbineError base
569
+ // constructor (see redactCauseForMode: an unredacted cause put the
570
+ // values straight back into any log line that prints the error object).
571
+ // The structured `.columns`/`.constraint`/`.column` fields survive in
572
+ // both modes, they carry NAMES, never values.
425
573
  const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
426
574
  if (detail)
427
575
  message += `: ${detail}`;
@@ -445,9 +593,12 @@ class ExclusionConstraintError extends TurbineError {
445
593
  // PII-safe by default: the raw pg `detail` string contains the
446
594
  // conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
447
595
  // exists.`). Only append it in 'verbose' mode. In 'safe' mode the
448
- // message carries keys/constraint/column names only, the structured
449
- // `.columns`/`.constraint`/`.column` fields and `.cause` still expose
450
- // the full detail for programmatic use.
596
+ // message carries keys/constraint/column names only, and the same goes
597
+ // for `.cause`, whose `detail` is redacted by the TurbineError base
598
+ // constructor (see redactCauseForMode: an unredacted cause put the
599
+ // values straight back into any log line that prints the error object).
600
+ // The structured `.columns`/`.constraint`/`.column` fields survive in
601
+ // both modes, they carry NAMES, never values.
451
602
  const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
452
603
  if (detail)
453
604
  message += `: ${detail}`;
@@ -582,10 +733,12 @@ function parseColumnsFromDetail(detail) {
582
733
  return m[1].split(',').map((s) => s.trim());
583
734
  }
584
735
  /**
585
- * Connection-class error codes. Covers both pg SQLSTATEs (class 08
586
- * connection_exception, plus a few class-53/57 admin/availability codes) and
587
- * Node driver-level error codes that arrive on the same `.code` field when the
588
- * socket never reaches Postgres. All map to {@link ConnectionError} (E004).
736
+ * Connection-class error codes. Covers pg SQLSTATEs (class 08
737
+ * connection_exception, class 28 authorization refusals, invalid_catalog_name,
738
+ * plus a few class-53/57 admin/availability codes) and Node driver-level socket
739
+ * and TLS error codes that arrive on the same `.code` field when the connection
740
+ * never reaches (or never gets past) Postgres. All map to
741
+ * {@link ConnectionError} (E004).
589
742
  *
590
743
  * `57014` (query_canceled, a server-side `statement_timeout` cancellation) is
591
744
  * intentionally NOT here: it maps to {@link TimeoutError} (E002) instead.
@@ -597,7 +750,22 @@ const CONNECTION_ERROR_CODES = new Set([
597
750
  '08003', // connection_does_not_exist
598
751
  '08004', // sqlserver_rejected_establishment_of_sqlconnection
599
752
  '08006', // connection_failure
753
+ '08007', // transaction_resolution_unknown
600
754
  '08P01', // protocol_violation
755
+ // pg SQLSTATE class 28: the server answered and REFUSED us. These are the
756
+ // most common first-run failures there are (wrong password, a pg_hba rule
757
+ // that does not cover this user/host), and they used to fall through
758
+ // wrapPgError untouched: the caller got a raw pg `DatabaseError` whose
759
+ // `.code` was `28P01`, i.e. a value from the SQLSTATE namespace on the SAME
760
+ // property Turbine puts `TURBINE_E0NN` in. Every `err.code.startsWith
761
+ // ('TURBINE_')` check, every `instanceof ConnectionError` catch, and the
762
+ // README's "every error is typed" guarantee silently missed the single
763
+ // failure a new user is most likely to hit.
764
+ '28000', // invalid_authorization_specification
765
+ '28P01', // invalid_password
766
+ // The server answered but the database named in the connection string does
767
+ // not exist. Same class of first-run mistake, same escape.
768
+ '3D000', // invalid_catalog_name
601
769
  // pg SQLSTATE class 53/57 (server unavailable / shutting down)
602
770
  '53300', // too_many_connections
603
771
  '57P01', // admin_shutdown
@@ -608,8 +776,44 @@ const CONNECTION_ERROR_CODES = new Set([
608
776
  'ECONNRESET',
609
777
  'ETIMEDOUT',
610
778
  'ENOTFOUND',
779
+ 'EAI_AGAIN', // transient DNS failure
780
+ 'EHOSTUNREACH',
781
+ 'ENETUNREACH',
611
782
  'EPIPE',
783
+ // Node TLS handshake failures. They can only happen while OPENING a
784
+ // connection, so classifying them as connection errors cannot mis-tag a
785
+ // query failure, and a managed Postgres with a private CA is the other
786
+ // first-run wall people hit.
787
+ 'DEPTH_ZERO_SELF_SIGNED_CERT',
788
+ 'SELF_SIGNED_CERT_IN_CHAIN',
789
+ 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
790
+ 'CERT_HAS_EXPIRED',
791
+ 'ERR_TLS_CERT_ALTNAME_INVALID',
612
792
  ]);
793
+ /**
794
+ * Actionable next step per connection-class code, appended to the driver's own
795
+ * message. The driver message states WHAT happened ("password authentication
796
+ * failed for user \"postgres\""); these state what to do about it, which is the
797
+ * whole difference between a typed error and a raw one for a first-run failure.
798
+ *
799
+ * A code with no entry here simply gets no hint appended.
800
+ */
801
+ const CONNECTION_ERROR_HINTS = {
802
+ '28P01': 'The server rejected the credentials. Check the password in your connection string (or DATABASE_URL), including any URL-encoding of special characters.',
803
+ '28000': "The server refused this user/host combination. Check the user name and the server's pg_hba.conf rules for the client address.",
804
+ '3D000': 'The database named in the connection string does not exist. Check the path segment after the host, and create the database if needed.',
805
+ '53300': "The server has no free connection slots. Lower `poolSize` or raise the server's max_connections.",
806
+ '57P03': 'The server is still starting up (or shutting down) and is not accepting connections yet.',
807
+ ECONNREFUSED: 'Nothing is listening on that host and port. Check the server is running and the port is right.',
808
+ ENOTFOUND: 'The host in the connection string could not be resolved. Check the host name, and that the connection string itself is well formed.',
809
+ EAI_AGAIN: 'DNS lookup for the host failed temporarily. Check network/DNS availability, then retry.',
810
+ ETIMEDOUT: 'The connection attempt timed out before the server answered. Check firewall/security-group rules and `connectionTimeoutMs`.',
811
+ DEPTH_ZERO_SELF_SIGNED_CERT: 'The server presented a certificate Node cannot verify. Supply the CA via `ssl: { ca }`.',
812
+ SELF_SIGNED_CERT_IN_CHAIN: 'The server presented a certificate Node cannot verify. Supply the CA via `ssl: { ca }`.',
813
+ UNABLE_TO_VERIFY_LEAF_SIGNATURE: 'The server presented a certificate Node cannot verify. Supply the CA via `ssl: { ca }`.',
814
+ CERT_HAS_EXPIRED: "The server's TLS certificate has expired. Renew it, or supply the correct CA via `ssl: { ca }`.",
815
+ 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.",
816
+ };
613
817
  /**
614
818
  * Translate a pg driver error into a typed Turbine error.
615
819
  * If the error doesn't match a known constraint code, returns it unchanged.
@@ -623,6 +827,8 @@ const CONNECTION_ERROR_CODES = new Set([
623
827
  * 40P01 (deadlock_detected) -> DeadlockError (retryable)
624
828
  * 40001 (serialization_failure) -> SerializationFailureError (retryable)
625
829
  * 57014 (query_canceled) -> TimeoutError (server-side statement_timeout)
830
+ * 28P01 / 28000 (auth refused) -> ConnectionError, with a remediation hint
831
+ * 3D000 (no such database) -> ConnectionError, with a remediation hint
626
832
  * connection-class codes -> ConnectionError (see CONNECTION_ERROR_CODES)
627
833
  *
628
834
  * The original pg error is preserved as `.cause` on the wrapped error.
@@ -687,9 +893,14 @@ function wrapPgError(err) {
687
893
  default:
688
894
  if (CONNECTION_ERROR_CODES.has(e.code)) {
689
895
  const pgMessage = typeof e.message === 'string' && e.message.length > 0 ? e.message : undefined;
690
- return new ConnectionError(pgMessage
896
+ // Own-property lookup only: `e.code` is driver-controlled text, and a
897
+ // plain-object map would happily resolve `constructor` or `toString`
898
+ // to a function and interpolate it into the message.
899
+ const hint = Object.hasOwn(CONNECTION_ERROR_HINTS, e.code) ? CONNECTION_ERROR_HINTS[e.code] : undefined;
900
+ const head = pgMessage
691
901
  ? `[turbine] Database connection error: ${pgMessage}`
692
- : `[turbine] Database connection error (${e.code})`, { cause: err });
902
+ : `[turbine] Database connection error (${e.code})`;
903
+ return new ConnectionError(hint ? `${head} (${e.code}) ${hint}` : head, { cause: err, sqlstate: e.code });
693
904
  }
694
905
  return err;
695
906
  }
@@ -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/cjs/index.js CHANGED
@@ -34,8 +34,8 @@
34
34
  * ```
35
35
  */
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
- exports.validateChannel = exports.QueryInterface = exports.AUTO_TO_ONE_JOIN_ROWS_MIN = exports.AUTO_TO_ONE_JOIN_ROWS_MAX = exports.AUTO_TO_ONE_JOIN_MAX_ROWS = exports.AUTO_JOIN_PENALTY_MS_PER_ROW = exports.AUTO_COUNT_BATCH_MIN_PARENT_ROWS = exports.AUTO_ASSUMED_ROUND_TRIP_MS = exports.fingerprintPrismaSchema = exports.pipelineSupported = exports.executePipeline = exports.PgMetricsSink = exports.HttpJsonSink = exports.hasRelationFields = exports.executeNestedUpdate = exports.executeNestedCreate = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UnsupportedFeatureError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.ReadOnlyError = exports.PipelineError = exports.OptimisticLockError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.ExclusionConstraintError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.postgresDialect = exports.withRetry = exports.TurbineClient = exports.TransactionClient = exports.yugabytedb = exports.timescale = exports.postgresql = exports.cockroachdb = exports.alloydb = void 0;
38
- exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.defineSeed = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.DestructivePushRefusal = exports.schemaDefToMetadata = exports.table = exports.isDocFieldIndexDef = exports.defineSchema = exports.column = exports.ColumnBuilder = exports.applyManyToManyRelations = exports.withDbFieldNames = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = exports.isDateType = exports.camelToSnake = void 0;
37
+ exports.QueryInterface = exports.AUTO_TO_ONE_JOIN_ROWS_MIN = exports.AUTO_TO_ONE_JOIN_ROWS_MAX = exports.AUTO_TO_ONE_JOIN_MAX_ROWS = exports.AUTO_JOIN_PENALTY_MS_PER_ROW = exports.AUTO_COUNT_BATCH_MIN_PARENT_ROWS = exports.AUTO_ASSUMED_ROUND_TRIP_MS = exports.fingerprintPrismaSchema = exports.pipelineSupported = exports.executePipeline = exports.PgMetricsSink = exports.HttpJsonSink = exports.hasRelationFields = exports.executeNestedUpdate = exports.executeNestedCreate = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UnsupportedFeatureError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.ReadOnlyError = exports.REDACTED_DETAIL = exports.PipelineError = exports.OptimisticLockError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.ExclusionConstraintError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.postgresDialect = exports.withRetry = exports.TurbineClient = exports.TransactionClient = exports.yugabytedb = exports.timescale = exports.postgresql = exports.cockroachdb = exports.alloydb = void 0;
38
+ exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.defineSeed = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.DestructivePushRefusal = exports.schemaDefToMetadata = exports.table = exports.isDocFieldIndexDef = exports.defineSchema = exports.column = exports.ColumnBuilder = exports.applyManyToManyRelations = exports.withDbFieldNames = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = exports.isDateType = exports.camelToSnake = exports.validateChannel = exports.UNSAFE = void 0;
39
39
  var index_js_1 = require("./adapters/index.js");
40
40
  Object.defineProperty(exports, "alloydb", { enumerable: true, get: function () { return index_js_1.alloydb; } });
41
41
  Object.defineProperty(exports, "cockroachdb", { enumerable: true, get: function () { return index_js_1.cockroachdb; } });
@@ -63,6 +63,7 @@ Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: functio
63
63
  Object.defineProperty(exports, "NotNullViolationError", { enumerable: true, get: function () { return errors_js_1.NotNullViolationError; } });
64
64
  Object.defineProperty(exports, "OptimisticLockError", { enumerable: true, get: function () { return errors_js_1.OptimisticLockError; } });
65
65
  Object.defineProperty(exports, "PipelineError", { enumerable: true, get: function () { return errors_js_1.PipelineError; } });
66
+ Object.defineProperty(exports, "REDACTED_DETAIL", { enumerable: true, get: function () { return errors_js_1.REDACTED_DETAIL; } });
66
67
  Object.defineProperty(exports, "ReadOnlyError", { enumerable: true, get: function () { return errors_js_1.ReadOnlyError; } });
67
68
  Object.defineProperty(exports, "RelationError", { enumerable: true, get: function () { return errors_js_1.RelationError; } });
68
69
  Object.defineProperty(exports, "SerializationFailureError", { enumerable: true, get: function () { return errors_js_1.SerializationFailureError; } });
@@ -97,6 +98,9 @@ Object.defineProperty(exports, "pipelineSupported", { enumerable: true, get: fun
97
98
  var prisma_schema_fingerprint_js_1 = require("./prisma-schema-fingerprint.js");
98
99
  Object.defineProperty(exports, "fingerprintPrismaSchema", { enumerable: true, get: function () { return prisma_schema_fingerprint_js_1.fingerprintPrismaSchema; } });
99
100
  // Query builder
101
+ // The privilege sentinel. `skipGlobalFilters` / `includePii` /
102
+ // `allowFullTableScan` are unlocked by THIS VALUE and nothing else, so a
103
+ // request body spread into query args cannot enable them (JSON has no symbols).
100
104
  var index_js_2 = require("./query/index.js");
101
105
  Object.defineProperty(exports, "AUTO_ASSUMED_ROUND_TRIP_MS", { enumerable: true, get: function () { return index_js_2.AUTO_ASSUMED_ROUND_TRIP_MS; } });
102
106
  Object.defineProperty(exports, "AUTO_COUNT_BATCH_MIN_PARENT_ROWS", { enumerable: true, get: function () { return index_js_2.AUTO_COUNT_BATCH_MIN_PARENT_ROWS; } });
@@ -105,6 +109,7 @@ Object.defineProperty(exports, "AUTO_TO_ONE_JOIN_MAX_ROWS", { enumerable: true,
105
109
  Object.defineProperty(exports, "AUTO_TO_ONE_JOIN_ROWS_MAX", { enumerable: true, get: function () { return index_js_2.AUTO_TO_ONE_JOIN_ROWS_MAX; } });
106
110
  Object.defineProperty(exports, "AUTO_TO_ONE_JOIN_ROWS_MIN", { enumerable: true, get: function () { return index_js_2.AUTO_TO_ONE_JOIN_ROWS_MIN; } });
107
111
  Object.defineProperty(exports, "QueryInterface", { enumerable: true, get: function () { return index_js_2.QueryInterface; } });
112
+ Object.defineProperty(exports, "UNSAFE", { enumerable: true, get: function () { return index_js_2.UNSAFE; } });
108
113
  // Realtime, LISTEN/NOTIFY pub/sub
109
114
  var realtime_js_1 = require("./realtime.js");
110
115
  Object.defineProperty(exports, "validateChannel", { enumerable: true, get: function () { return realtime_js_1.validateChannel; } });
@@ -90,8 +90,21 @@ async function executePipeline(pool, queries, options) {
90
90
  if (queries.length === 0) {
91
91
  return [];
92
92
  }
93
- // Acquire a single client, reused for both capability check and execution
94
- const client = await pool.connect();
93
+ // Acquire a single client, reused for both capability check and execution.
94
+ // The checkout is wrapped because a connection-level failure (28P01 wrong
95
+ // password, an unverifiable TLS cert, ECONNREFUSED) happens HERE, before any
96
+ // query runs. Unwrapped it escapes as a raw pg DatabaseError whose `.code`
97
+ // holds a SQLSTATE, which is the same property Turbine puts TURBINE_E0NN in,
98
+ // so a caller switching on `.code` silently receives a value from a foreign
99
+ // namespace. The transaction and nested-write checkouts were wrapped for this
100
+ // reason; this was the last sibling still bare.
101
+ let client;
102
+ try {
103
+ client = await pool.connect();
104
+ }
105
+ catch (err) {
106
+ throw (0, errors_js_1.wrapPgError)(err);
107
+ }
95
108
  try {
96
109
  if ((0, pipeline_submittable_js_1.supportsExtendedPipeline)(client)) {
97
110
  // Real pipeline path, uses extended-query protocol wire methods
@@ -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;