turbine-orm 0.65.0 → 0.66.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 +34 -32
- package/dist/adapters/cockroachdb.js +21 -3
- package/dist/adapters/index.d.ts +15 -0
- package/dist/adapters/yugabytedb.js +20 -3
- package/dist/cjs/adapters/cockroachdb.js +21 -3
- package/dist/cjs/adapters/index.d.ts +15 -0
- package/dist/cjs/adapters/yugabytedb.js +20 -3
- package/dist/cjs/cli/destructive.d.ts +18 -4
- package/dist/cjs/cli/destructive.js +230 -122
- package/dist/cjs/cli/index.d.ts +21 -4
- package/dist/cjs/cli/index.js +119 -22
- package/dist/cjs/cli/mcp.d.ts +28 -8
- package/dist/cjs/cli/mcp.js +170 -127
- package/dist/cjs/cli/migrate.d.ts +134 -13
- package/dist/cjs/cli/migrate.js +349 -241
- package/dist/cjs/cli/pii-predicate-guard.d.ts +112 -0
- package/dist/cjs/cli/pii-predicate-guard.js +390 -0
- package/dist/cjs/cli/prisma-resolve.js +75 -4
- package/dist/cjs/cli/prisma-schema.d.ts +17 -1
- package/dist/cjs/cli/prisma-schema.js +83 -17
- package/dist/cjs/cli/sql-statements.d.ts +125 -0
- package/dist/cjs/cli/sql-statements.js +378 -0
- package/dist/cjs/cli/studio.js +49 -118
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +43 -0
- package/dist/cjs/client.js +125 -6
- package/dist/cjs/dialect.d.ts +123 -0
- package/dist/cjs/dialect.js +33 -0
- package/dist/cjs/errors.d.ts +74 -1
- package/dist/cjs/errors.js +239 -25
- package/dist/cjs/index-advisor.d.ts +33 -1
- package/dist/cjs/index-advisor.js +32 -1
- package/dist/cjs/introspect.d.ts +48 -0
- package/dist/cjs/introspect.js +222 -91
- package/dist/cjs/mssql.js +43 -1
- package/dist/cjs/mysql.d.ts +5 -2
- package/dist/cjs/mysql.js +202 -17
- package/dist/cjs/nested-write.js +6 -1
- package/dist/cjs/pipeline-submittable.js +17 -3
- package/dist/cjs/pipeline.js +75 -9
- package/dist/cjs/powdb.d.ts +23 -0
- package/dist/cjs/powdb.js +33 -1
- package/dist/cjs/powql.d.ts +61 -9
- package/dist/cjs/powql.js +186 -49
- package/dist/cjs/prisma-compat.js +160 -41
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +80 -18
- package/dist/cjs/query/batched-loader.d.ts +10 -0
- package/dist/cjs/query/batched-loader.js +268 -7
- package/dist/cjs/query/builder.d.ts +73 -0
- package/dist/cjs/query/builder.js +225 -28
- package/dist/cjs/query/filters.d.ts +162 -0
- package/dist/cjs/query/filters.js +250 -1
- package/dist/cjs/query/relations.d.ts +10 -10
- package/dist/cjs/query/relations.js +93 -12
- package/dist/cjs/query/types.d.ts +14 -1
- package/dist/cjs/query/utils.d.ts +146 -2
- package/dist/cjs/query/utils.js +210 -4
- package/dist/cjs/query/warn-registry.d.ts +10 -0
- package/dist/cjs/query/warn-registry.js +10 -0
- package/dist/cjs/query/where-compile.d.ts +30 -0
- package/dist/cjs/query/where-compile.js +41 -0
- package/dist/cjs/query/where.d.ts +128 -13
- package/dist/cjs/query/where.js +215 -77
- package/dist/cjs/query/writes.d.ts +1 -1
- package/dist/cjs/query/writes.js +39 -15
- package/dist/cjs/schema-builder.d.ts +2 -1
- package/dist/cjs/schema-sql.d.ts +94 -4
- package/dist/cjs/schema-sql.js +506 -30
- package/dist/cjs/schema.d.ts +3 -1
- package/dist/cjs/sqlite.d.ts +6 -0
- package/dist/cjs/sqlite.js +151 -10
- package/dist/cjs/typed-sql.d.ts +29 -1
- package/dist/cjs/typed-sql.js +30 -12
- package/dist/cli/destructive.d.ts +18 -4
- package/dist/cli/destructive.js +229 -121
- package/dist/cli/index.d.ts +21 -4
- package/dist/cli/index.js +120 -24
- package/dist/cli/mcp.d.ts +28 -8
- package/dist/cli/mcp.js +172 -129
- package/dist/cli/migrate.d.ts +134 -13
- package/dist/cli/migrate.js +347 -238
- package/dist/cli/pii-predicate-guard.d.ts +112 -0
- package/dist/cli/pii-predicate-guard.js +386 -0
- package/dist/cli/prisma-resolve.js +75 -4
- package/dist/cli/prisma-schema.d.ts +17 -1
- package/dist/cli/prisma-schema.js +83 -17
- package/dist/cli/sql-statements.d.ts +125 -0
- package/dist/cli/sql-statements.js +373 -0
- package/dist/cli/studio.js +49 -118
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +43 -0
- package/dist/client.js +126 -7
- package/dist/dialect.d.ts +123 -0
- package/dist/dialect.js +33 -0
- package/dist/errors.d.ts +74 -1
- package/dist/errors.js +228 -19
- package/dist/index-advisor.d.ts +33 -1
- package/dist/index-advisor.js +31 -1
- package/dist/introspect.d.ts +48 -0
- package/dist/introspect.js +221 -91
- package/dist/mssql.js +44 -2
- package/dist/mysql.d.ts +5 -2
- package/dist/mysql.js +203 -18
- package/dist/nested-write.js +7 -2
- package/dist/pipeline-submittable.js +18 -4
- package/dist/pipeline.js +76 -10
- package/dist/powdb.d.ts +23 -0
- package/dist/powdb.js +33 -2
- package/dist/powql.d.ts +61 -9
- package/dist/powql.js +187 -50
- package/dist/prisma-compat.js +160 -41
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +82 -20
- package/dist/query/batched-loader.d.ts +10 -0
- package/dist/query/batched-loader.js +270 -9
- package/dist/query/builder.d.ts +73 -0
- package/dist/query/builder.js +226 -30
- package/dist/query/filters.d.ts +162 -0
- package/dist/query/filters.js +246 -1
- package/dist/query/relations.d.ts +10 -10
- package/dist/query/relations.js +94 -14
- package/dist/query/types.d.ts +14 -1
- package/dist/query/utils.d.ts +146 -2
- package/dist/query/utils.js +204 -3
- package/dist/query/warn-registry.d.ts +10 -0
- package/dist/query/warn-registry.js +10 -0
- package/dist/query/where-compile.d.ts +30 -0
- package/dist/query/where-compile.js +40 -1
- package/dist/query/where.d.ts +128 -13
- package/dist/query/where.js +216 -80
- package/dist/query/writes.d.ts +1 -1
- package/dist/query/writes.js +40 -16
- package/dist/schema-builder.d.ts +2 -1
- package/dist/schema-sql.d.ts +94 -4
- package/dist/schema-sql.js +505 -30
- package/dist/schema.d.ts +3 -1
- package/dist/sqlite.d.ts +6 -0
- package/dist/sqlite.js +151 -10
- package/dist/typed-sql.d.ts +29 -1
- package/dist/typed-sql.js +30 -12
- package/package.json +6 -4
package/dist/dialect.d.ts
CHANGED
|
@@ -220,6 +220,51 @@ export interface LimitOffsetInput {
|
|
|
220
220
|
/** Whether the outer SELECT already carries an ORDER BY (so none must be injected). */
|
|
221
221
|
hasOrderBy: boolean;
|
|
222
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Inputs for {@link Dialect.buildPartitionLimit}, the per-correlation-key row
|
|
225
|
+
* bound the batched relation loader pushes into its follow-up query.
|
|
226
|
+
*
|
|
227
|
+
* The follow-up is one flat `WHERE fk = ANY($1)` over EVERY parent's children,
|
|
228
|
+
* so a trailing `LIMIT n` would cap the TOTAL rather than the per-parent count.
|
|
229
|
+
* A window function is the portable-in-shape way to say "at most n rows per
|
|
230
|
+
* key": rank inside the partition, then keep the ranks at or below the bound.
|
|
231
|
+
*
|
|
232
|
+
* The loader hands over the compiled child SELECT and the pieces it cannot
|
|
233
|
+
* spell engine-independently; the dialect owns the SQL text (and its aliases),
|
|
234
|
+
* exactly like {@link buildLimitOffset} and {@link buildRelationSubquery}.
|
|
235
|
+
*/
|
|
236
|
+
export interface PartitionLimitInput {
|
|
237
|
+
/** The compiled child SELECT, to be wrapped as a derived table. */
|
|
238
|
+
innerSql: string;
|
|
239
|
+
/** RAW (unquoted) correlation column to partition by. */
|
|
240
|
+
partitionColumn: string;
|
|
241
|
+
/**
|
|
242
|
+
* The window's (and the outer result's) ordering, as RAW column names.
|
|
243
|
+
*
|
|
244
|
+
* NEVER EMPTY, and never a partial order: the loader only asks for this
|
|
245
|
+
* rewrite when the relation's `orderBy` covers a NOT NULL unique key of the
|
|
246
|
+
* target table, so no two rows of a partition can tie. That precondition is
|
|
247
|
+
* the whole licence for the rewrite. The join plan takes its `limit` with a
|
|
248
|
+
* per-parent `ORDER BY … LIMIT n` and this takes it with a rank over one flat
|
|
249
|
+
* result, and where ties exist the two are each free to keep different rows,
|
|
250
|
+
* which was measured happening (see `partitionOrderBy` in
|
|
251
|
+
* query/batched-loader.ts). Anything short of a total order therefore keeps
|
|
252
|
+
* the client-side slice instead of arriving here.
|
|
253
|
+
*/
|
|
254
|
+
orderBy: readonly {
|
|
255
|
+
column: string;
|
|
256
|
+
direction: 'ASC' | 'DESC';
|
|
257
|
+
nulls?: 'FIRST' | 'LAST';
|
|
258
|
+
}[];
|
|
259
|
+
/** SQL-ready placeholder for the bound (`$3`, `?`, …). */
|
|
260
|
+
limitPlaceholder: string;
|
|
261
|
+
/**
|
|
262
|
+
* RAW name of the rank column the wrapper adds. It is part of the projection
|
|
263
|
+
* (`SELECT <outer>.*`), so the CALLER must remove it from each raw row before
|
|
264
|
+
* parsing, or it would surface as an extra field on every child entity.
|
|
265
|
+
*/
|
|
266
|
+
rankColumn: string;
|
|
267
|
+
}
|
|
223
268
|
/**
|
|
224
269
|
* Everything an engine needs to OVERRIDE nested-relation subquery generation, for
|
|
225
270
|
* dialects whose JSON-aggregation shape is fundamentally different from PostgreSQL's
|
|
@@ -345,6 +390,37 @@ export interface Dialect {
|
|
|
345
390
|
readonly supportsUpsertUpdateWhere?: boolean;
|
|
346
391
|
/** Whether this dialect/engine supports pgvector distance ops (KNN / distance WHERE). */
|
|
347
392
|
readonly supportsVector: boolean;
|
|
393
|
+
/**
|
|
394
|
+
* Whether this dialect/engine can actually answer JSON CONTAINMENT, i.e. the
|
|
395
|
+
* pathless `{ contains }` and `{ equals }` JSON filters that compile through
|
|
396
|
+
* {@link buildJsonContains}.
|
|
397
|
+
*
|
|
398
|
+
* A capability flag rather than a per-operand hook, because the answer turned
|
|
399
|
+
* out not to vary by operand. SQLite emulates containment as
|
|
400
|
+
* `EXISTS (SELECT 1 FROM json_each(col) WHERE value = $1)` while the param is
|
|
401
|
+
* bound as JSON TEXT, and `json_each.value` yields the DECODED SQL value, so
|
|
402
|
+
* the comparison is between two different encodings and never holds. Measured
|
|
403
|
+
* against a real in-process SQLite, every operand type returned zero rows:
|
|
404
|
+
*
|
|
405
|
+
* stored filter PG SQLite
|
|
406
|
+
* ["gold"] contains: 'gold' match none ('gold' = '"gold"')
|
|
407
|
+
* [1] contains: 1 match none (INTEGER 1 = TEXT '1')
|
|
408
|
+
* {"a":1,"t":"gold"} contains: { a: 1 } match none (no structural walk)
|
|
409
|
+
*
|
|
410
|
+
* So the feature has never worked on that engine at all, and refusing it
|
|
411
|
+
* removes nothing that functioned. Fewer-rows-with-no-error is the exact
|
|
412
|
+
* degrade the capability contract exists to convert into a refusal.
|
|
413
|
+
*
|
|
414
|
+
* Repairing it rather than refusing it is possible and deliberately not done
|
|
415
|
+
* here: `json_each` exposes a `type` column ('text' / 'integer' / 'real' /
|
|
416
|
+
* 'true' / 'false' / 'null'), so a faithful scalar test is
|
|
417
|
+
* `type = $1 AND value IS $2`. That is a param-COUNT change on a path whose
|
|
418
|
+
* build and collect sides must stay in lockstep, which is a different change
|
|
419
|
+
* from this one. Until then the flag is honest and the error names the
|
|
420
|
+
* alternative that is exact today (`{ path: [...], equals }`, which compiles
|
|
421
|
+
* to `json_extract`).
|
|
422
|
+
*/
|
|
423
|
+
readonly supportsJsonContains: boolean;
|
|
348
424
|
/**
|
|
349
425
|
* Whether this dialect/engine supports the PostgreSQL full-text `search`
|
|
350
426
|
* filter (`to_tsvector(...) @@ to_tsquery(...)`). Optional: absent is treated
|
|
@@ -454,6 +530,28 @@ export interface Dialect {
|
|
|
454
530
|
readonly supportsILike: boolean;
|
|
455
531
|
/** Build a case-insensitive LIKE equivalent. */
|
|
456
532
|
buildInsensitiveLike(column: string, paramRef: string): string;
|
|
533
|
+
/**
|
|
534
|
+
* Escape the LIKE metacharacters of this engine in a bound `contains` /
|
|
535
|
+
* `startsWith` / `endsWith` / `stringContains` operand. Optional: when a
|
|
536
|
+
* dialect omits it the shared {@link escapeLike} is used, which escapes the
|
|
537
|
+
* SQL-standard set (`\`, `%`, `_`) and pairs with the `ESCAPE '\'` clause the
|
|
538
|
+
* builders always emit.
|
|
539
|
+
*
|
|
540
|
+
* It exists because the standard set is not the whole set everywhere:
|
|
541
|
+
* T-SQL's `LIKE` also treats `[` as opening a character CLASS, so
|
|
542
|
+
* `{ contains: '[draft]' }` becomes "contains any one of d, r, a, f, t" on
|
|
543
|
+
* SQL Server, a silently over-broad predicate on an engine where every other
|
|
544
|
+
* dialect agrees the value is a literal. That is not injection (the operand
|
|
545
|
+
* is still bound), but it is a wrong answer, and the difference is a property
|
|
546
|
+
* of the ENGINE's pattern grammar, not of the SQL text, which is why it is a
|
|
547
|
+
* dialect hook rather than a branch in the where builder.
|
|
548
|
+
*
|
|
549
|
+
* Whatever a dialect returns MUST be escaped for the same `ESCAPE '\'`
|
|
550
|
+
* clause, i.e. a backslash prefix, and MUST be identical on the SQL-build and
|
|
551
|
+
* the cache-hit param-collect paths (both call this one hook, so they cannot
|
|
552
|
+
* drift).
|
|
553
|
+
*/
|
|
554
|
+
escapeLikePattern?(value: string): string;
|
|
457
555
|
/** JSON operator support level for this dialect. */
|
|
458
556
|
readonly jsonPathSupport: 'native' | 'function' | 'limited';
|
|
459
557
|
/** Build a JSON containment check. */
|
|
@@ -601,6 +699,31 @@ export interface Dialect {
|
|
|
601
699
|
* param-push-ordering contract the override must honor.
|
|
602
700
|
*/
|
|
603
701
|
buildRelationSubquery?(ctx: RelationSubqueryContext): string;
|
|
702
|
+
/**
|
|
703
|
+
* Bound a batched relation follow-up to at most N rows per correlation key,
|
|
704
|
+
* by wrapping the compiled child SELECT in a `ROW_NUMBER() OVER (PARTITION
|
|
705
|
+
* BY …)` filter. See {@link PartitionLimitInput} for why a trailing `LIMIT`
|
|
706
|
+
* cannot express this.
|
|
707
|
+
*
|
|
708
|
+
* OPTIONAL, and absent means "no pushdown": the batched loader then fetches
|
|
709
|
+
* every matching child and applies the per-relation `limit` client-side, the
|
|
710
|
+
* behaviour every engine had before this hook existed. That fallback is
|
|
711
|
+
* correct but unbounded in bytes over the wire (200 parents x ~505 children
|
|
712
|
+
* with `limit: 3` measured 101,000 rows fetched to keep 600, +52.9 MB peak
|
|
713
|
+
* heap against +0.5 MB for the join plan), which is why PostgreSQL
|
|
714
|
+
* implements it. Only PostgreSQL does in this release; the other engines keep
|
|
715
|
+
* the client-side slice, so their emitted SQL is byte-identical to before.
|
|
716
|
+
*
|
|
717
|
+
* The client-side slice is NOT removed when a dialect implements this: it
|
|
718
|
+
* stays as the belt-and-braces bound, and is a no-op once the engine has
|
|
719
|
+
* already limited each partition. It is also still the whole mechanism for
|
|
720
|
+
* any relation whose `orderBy` does not TOTALLY order the target table, on
|
|
721
|
+
* every engine including this one: a rewrite that reaches here has to return
|
|
722
|
+
* the same rows the join plan's per-parent `ORDER BY … LIMIT n` returns, and
|
|
723
|
+
* with ties present neither plan's choice is forced. See
|
|
724
|
+
* {@link PartitionLimitInput.orderBy}.
|
|
725
|
+
*/
|
|
726
|
+
buildPartitionLimit?(input: PartitionLimitInput): string;
|
|
604
727
|
}
|
|
605
728
|
export interface DialectIntrospector {
|
|
606
729
|
introspect(options: IntrospectOptions): Promise<SchemaMetadata>;
|
package/dist/dialect.js
CHANGED
|
@@ -20,6 +20,7 @@ export const postgresDialect = {
|
|
|
20
20
|
nullJsonLiteral: 'NULL',
|
|
21
21
|
aggSupportsInlineOrderBy: true,
|
|
22
22
|
supportsVector: true,
|
|
23
|
+
supportsJsonContains: true,
|
|
23
24
|
supportsFullTextSearch: true,
|
|
24
25
|
supportsArrayColumns: true,
|
|
25
26
|
supportsListenNotify: true,
|
|
@@ -258,6 +259,38 @@ export const postgresDialect = {
|
|
|
258
259
|
params: [name, value],
|
|
259
260
|
};
|
|
260
261
|
},
|
|
262
|
+
buildPartitionLimit(input) {
|
|
263
|
+
// Two wrapper levels, and both are needed: `ROW_NUMBER()` is a window
|
|
264
|
+
// function, so it cannot appear in the same WHERE that filters on it.
|
|
265
|
+
// Aliases are deliberately not in the `t<n>` family the relation builder
|
|
266
|
+
// allocates, so a future caller that wraps a query carrying those aliases
|
|
267
|
+
// cannot shadow one.
|
|
268
|
+
const inner = this.quoteIdentifier('turbine_pl_src');
|
|
269
|
+
const outer = this.quoteIdentifier('turbine_pl_rank');
|
|
270
|
+
const rank = this.quoteIdentifier(input.rankColumn);
|
|
271
|
+
const order = (ref) => input.orderBy
|
|
272
|
+
.map((o) => `${ref}.${this.quoteIdentifier(o.column)} ${o.direction}${o.nulls ? ` NULLS ${o.nulls}` : ''}`)
|
|
273
|
+
.join(', ');
|
|
274
|
+
// The empty-list branches are defensive only: the loader never asks for
|
|
275
|
+
// this rewrite without a total order (see the note on
|
|
276
|
+
// `PartitionLimitInput.orderBy`). An unordered window numbers each
|
|
277
|
+
// partition arbitrarily, and so does the join plan's ORDER-BY-less
|
|
278
|
+
// per-parent `LIMIT`, but they are different plans over different row sets
|
|
279
|
+
// and were measured choosing differently, so that shape keeps the loader's
|
|
280
|
+
// client-side slice rather than arriving here.
|
|
281
|
+
const windowOrder = input.orderBy.length > 0 ? ` ORDER BY ${order(inner)}` : '';
|
|
282
|
+
// The OUTER ordering is not cosmetic: the loader buckets children in result
|
|
283
|
+
// order, so without re-applying it here the relation array would come back
|
|
284
|
+
// in whatever order the rank filter happened to emit. It agrees with the
|
|
285
|
+
// window's own ordering by construction, since both are this one list and
|
|
286
|
+
// that list admits no ties, so the outer sort cannot pick a different order
|
|
287
|
+
// among the ranked rows than the rank did.
|
|
288
|
+
const outerOrder = input.orderBy.length > 0 ? ` ORDER BY ${order(outer)}` : '';
|
|
289
|
+
return (`SELECT ${outer}.* FROM (SELECT ${inner}.*, ROW_NUMBER() OVER (PARTITION BY ` +
|
|
290
|
+
`${inner}.${this.quoteIdentifier(input.partitionColumn)}${windowOrder}) AS ${rank} ` +
|
|
291
|
+
`FROM (${input.innerSql}) AS ${inner}) AS ${outer} ` +
|
|
292
|
+
`WHERE ${outer}.${rank} <= ${input.limitPlaceholder}${outerOrder}`);
|
|
293
|
+
},
|
|
261
294
|
async *openStream(connection, sql, params, batchSize, opts) {
|
|
262
295
|
// Cursors require a single connection inside a transaction: BEGIN → DECLARE
|
|
263
296
|
// … NO SCROLL CURSOR FOR → FETCH n (loop) → CLOSE → COMMIT; ROLLBACK on
|
package/dist/errors.d.ts
CHANGED
|
@@ -47,6 +47,40 @@ export declare class TurbineError extends Error {
|
|
|
47
47
|
* with `{ errorMessages: 'verbose' }`.
|
|
48
48
|
*/
|
|
49
49
|
export type ErrorMessageMode = 'safe' | 'verbose';
|
|
50
|
+
/**
|
|
51
|
+
* Register a client's configured mode and report whether per-operation scoping
|
|
52
|
+
* is now required.
|
|
53
|
+
*
|
|
54
|
+
* The gate is DIVERGENCE, not "has a client been built": while every client in
|
|
55
|
+
* the process agrees (the overwhelmingly common case, including every
|
|
56
|
+
* single-client app), the module default is already the right answer for all of
|
|
57
|
+
* them and no scope has to be established, so the query path pays exactly
|
|
58
|
+
* nothing. The moment two clients disagree, every client starts scoping, which
|
|
59
|
+
* is why this is read per call rather than latched per client.
|
|
60
|
+
*
|
|
61
|
+
* @internal Used by TurbineClient; not part of the public surface.
|
|
62
|
+
*/
|
|
63
|
+
export declare function registerClientErrorMessageMode(mode: ErrorMessageMode): void;
|
|
64
|
+
/**
|
|
65
|
+
* Whether any two clients in this process have registered different modes, so
|
|
66
|
+
* the module default can no longer speak for all of them.
|
|
67
|
+
*
|
|
68
|
+
* @internal
|
|
69
|
+
*/
|
|
70
|
+
export declare function errorMessageModesDiverged(): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Run `fn` with `mode` in force for everything it does, awaits included.
|
|
73
|
+
*
|
|
74
|
+
* Establishing the scope around the call (rather than threading a mode
|
|
75
|
+
* parameter into every error constructor) is what makes this reachable at all:
|
|
76
|
+
* the mode-sensitive errors are built deep inside the query executor, the write
|
|
77
|
+
* builders and the nested-write engine, none of which are handed the client.
|
|
78
|
+
* The async context is captured when the operation's promise chain is created
|
|
79
|
+
* INSIDE this call, so every continuation of it resolves the same mode.
|
|
80
|
+
*
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
export declare function runWithErrorMessageMode<R>(mode: ErrorMessageMode, fn: () => R): R;
|
|
50
84
|
/**
|
|
51
85
|
* Set the global NotFoundError message mode. Called from the TurbineClient
|
|
52
86
|
* constructor when `TurbineConfig.errorMessages` is provided.
|
|
@@ -70,7 +104,14 @@ export type ErrorMessageMode = 'safe' | 'verbose';
|
|
|
70
104
|
* leak, not as a log-scrubbing boundary.
|
|
71
105
|
*/
|
|
72
106
|
export declare function setErrorMessageMode(mode: ErrorMessageMode): void;
|
|
73
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Returns the NotFoundError message mode in effect right now.
|
|
109
|
+
*
|
|
110
|
+
* Called inside an operation issued through a client whose mode differs from
|
|
111
|
+
* another client's in the same process, that is THAT client's mode; anywhere
|
|
112
|
+
* else it is the process default {@link setErrorMessageMode} last set, which is
|
|
113
|
+
* what it has always returned. Exported for tests.
|
|
114
|
+
*/
|
|
74
115
|
export declare function getErrorMessageMode(): ErrorMessageMode;
|
|
75
116
|
/**
|
|
76
117
|
* The marker left where a driver `detail` string was removed in 'safe' mode.
|
|
@@ -78,6 +119,17 @@ export declare function getErrorMessageMode(): ErrorMessageMode;
|
|
|
78
119
|
* assertions can match on it without hardcoding the wording.
|
|
79
120
|
*/
|
|
80
121
|
export declare const REDACTED_DETAIL = "[redacted by turbine errorMessages:\"safe\"]";
|
|
122
|
+
/**
|
|
123
|
+
* Mark a driver error as carrying row values in its `message` (see
|
|
124
|
+
* {@link VALUE_BEARING_MESSAGE}). Called by the engine error augmenters for the
|
|
125
|
+
* exact driver codes whose message grammar embeds a value; every other code is
|
|
126
|
+
* left alone so nothing is withheld that did not need to be.
|
|
127
|
+
*
|
|
128
|
+
* Non-enumerable so the flag itself never shows up in a serialized error, and
|
|
129
|
+
* best-effort so a frozen or exotic driver error cannot turn a constraint
|
|
130
|
+
* violation into a TypeError.
|
|
131
|
+
*/
|
|
132
|
+
export declare function markValueBearingMessage<T>(err: T): T;
|
|
81
133
|
/**
|
|
82
134
|
* Render a user-supplied `where` / `connect` target for a "no row found" error
|
|
83
135
|
* message, honoring the global {@link ErrorMessageMode}. In 'safe' mode (the
|
|
@@ -168,6 +220,27 @@ export declare class ConnectionError extends TurbineError {
|
|
|
168
220
|
sqlstate?: string;
|
|
169
221
|
});
|
|
170
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* The message for "this engine's connection string could not be parsed".
|
|
225
|
+
*
|
|
226
|
+
* The one rule it exists to enforce: NEVER echo the value. A DSN carries a
|
|
227
|
+
* password, and the trigger for this error is a MALFORMED DSN, which is exactly
|
|
228
|
+
* when someone pastes the failure into a bug report, a CI log, or an error
|
|
229
|
+
* tracker. No redaction written against the URL grammar can be trusted on a
|
|
230
|
+
* string that just failed to parse as a URL, so the only safe amount of it to
|
|
231
|
+
* include is none: not the password, not the host, not a prefix.
|
|
232
|
+
*
|
|
233
|
+
* This mirrors the Postgres path (`assertUsableConnectionString` in client.ts),
|
|
234
|
+
* which reached the same conclusion first and stated the same reason; the
|
|
235
|
+
* engines used to interpolate the raw string instead, so SECURITY.md's claim of
|
|
236
|
+
* redaction "in all CLI error output" was true for one engine out of four.
|
|
237
|
+
* Shared rather than copied so the three engines cannot drift back apart.
|
|
238
|
+
*
|
|
239
|
+
* @param engine the human engine label ("MySQL", "SQL Server", "PowDB").
|
|
240
|
+
* @param example a well-formed connection string for that engine, with a
|
|
241
|
+
* placeholder password. Never derived from the caller's value.
|
|
242
|
+
*/
|
|
243
|
+
export declare function malformedConnectionStringMessage(engine: string, example: string): string;
|
|
171
244
|
/** Thrown when a relation reference is invalid */
|
|
172
245
|
export declare class RelationError extends TurbineError {
|
|
173
246
|
constructor(message: string);
|
package/dist/errors.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
1
2
|
/**
|
|
2
3
|
* turbine-orm, Error types
|
|
3
4
|
*
|
|
@@ -77,6 +78,76 @@ export class TurbineError extends Error {
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
let errorMessageMode = 'safe';
|
|
81
|
+
/**
|
|
82
|
+
* The mode in force for the operation currently executing, when a client has
|
|
83
|
+
* established one. See {@link runWithErrorMessageMode}.
|
|
84
|
+
*/
|
|
85
|
+
const errorMessageModeScope = new AsyncLocalStorage();
|
|
86
|
+
/** The first mode a TurbineClient registered, used to detect divergence. */
|
|
87
|
+
let firstRegisteredClientMode;
|
|
88
|
+
let clientModesDiverged = false;
|
|
89
|
+
/**
|
|
90
|
+
* The mode that applies right now: the scope a client established for this
|
|
91
|
+
* operation, else the process-wide default.
|
|
92
|
+
*
|
|
93
|
+
* The default is the fallback rather than the authority, which is the whole
|
|
94
|
+
* point. `errorMessageMode` is a module-level `let` that every TurbineClient
|
|
95
|
+
* constructor overwrote, so in a process with a primary client and an analytics
|
|
96
|
+
* or replica client, or a test harness building one client per suite, the LAST
|
|
97
|
+
* client constructed decided the mode for ALL of them, order-dependently. A
|
|
98
|
+
* client asking for `'safe'` could silently be running in `'verbose'`, which is
|
|
99
|
+
* the direction that leaks. (The dual ESM+CJS build makes it worse: the two
|
|
100
|
+
* copies hold separate `let`s.)
|
|
101
|
+
*
|
|
102
|
+
* An error constructed outside any client, directly or via
|
|
103
|
+
* {@link setErrorMessageMode}, still formats by the default, unchanged.
|
|
104
|
+
*/
|
|
105
|
+
function currentErrorMessageMode() {
|
|
106
|
+
return errorMessageModeScope.getStore() ?? errorMessageMode;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Register a client's configured mode and report whether per-operation scoping
|
|
110
|
+
* is now required.
|
|
111
|
+
*
|
|
112
|
+
* The gate is DIVERGENCE, not "has a client been built": while every client in
|
|
113
|
+
* the process agrees (the overwhelmingly common case, including every
|
|
114
|
+
* single-client app), the module default is already the right answer for all of
|
|
115
|
+
* them and no scope has to be established, so the query path pays exactly
|
|
116
|
+
* nothing. The moment two clients disagree, every client starts scoping, which
|
|
117
|
+
* is why this is read per call rather than latched per client.
|
|
118
|
+
*
|
|
119
|
+
* @internal Used by TurbineClient; not part of the public surface.
|
|
120
|
+
*/
|
|
121
|
+
export function registerClientErrorMessageMode(mode) {
|
|
122
|
+
if (firstRegisteredClientMode === undefined)
|
|
123
|
+
firstRegisteredClientMode = mode;
|
|
124
|
+
else if (firstRegisteredClientMode !== mode)
|
|
125
|
+
clientModesDiverged = true;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Whether any two clients in this process have registered different modes, so
|
|
129
|
+
* the module default can no longer speak for all of them.
|
|
130
|
+
*
|
|
131
|
+
* @internal
|
|
132
|
+
*/
|
|
133
|
+
export function errorMessageModesDiverged() {
|
|
134
|
+
return clientModesDiverged;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Run `fn` with `mode` in force for everything it does, awaits included.
|
|
138
|
+
*
|
|
139
|
+
* Establishing the scope around the call (rather than threading a mode
|
|
140
|
+
* parameter into every error constructor) is what makes this reachable at all:
|
|
141
|
+
* the mode-sensitive errors are built deep inside the query executor, the write
|
|
142
|
+
* builders and the nested-write engine, none of which are handed the client.
|
|
143
|
+
* The async context is captured when the operation's promise chain is created
|
|
144
|
+
* INSIDE this call, so every continuation of it resolves the same mode.
|
|
145
|
+
*
|
|
146
|
+
* @internal
|
|
147
|
+
*/
|
|
148
|
+
export function runWithErrorMessageMode(mode, fn) {
|
|
149
|
+
return errorMessageModeScope.run(mode, fn);
|
|
150
|
+
}
|
|
80
151
|
/**
|
|
81
152
|
* Set the global NotFoundError message mode. Called from the TurbineClient
|
|
82
153
|
* constructor when `TurbineConfig.errorMessages` is provided.
|
|
@@ -102,9 +173,16 @@ let errorMessageMode = 'safe';
|
|
|
102
173
|
export function setErrorMessageMode(mode) {
|
|
103
174
|
errorMessageMode = mode;
|
|
104
175
|
}
|
|
105
|
-
/**
|
|
176
|
+
/**
|
|
177
|
+
* Returns the NotFoundError message mode in effect right now.
|
|
178
|
+
*
|
|
179
|
+
* Called inside an operation issued through a client whose mode differs from
|
|
180
|
+
* another client's in the same process, that is THAT client's mode; anywhere
|
|
181
|
+
* else it is the process default {@link setErrorMessageMode} last set, which is
|
|
182
|
+
* what it has always returned. Exported for tests.
|
|
183
|
+
*/
|
|
106
184
|
export function getErrorMessageMode() {
|
|
107
|
-
return
|
|
185
|
+
return currentErrorMessageMode();
|
|
108
186
|
}
|
|
109
187
|
/**
|
|
110
188
|
* The marker left where a driver `detail` string was removed in 'safe' mode.
|
|
@@ -112,6 +190,69 @@ export function getErrorMessageMode() {
|
|
|
112
190
|
* assertions can match on it without hardcoding the wording.
|
|
113
191
|
*/
|
|
114
192
|
export const REDACTED_DETAIL = '[redacted by turbine errorMessages:"safe"]';
|
|
193
|
+
/**
|
|
194
|
+
* Marks a driver error whose `message` (not only its `detail`) embeds row
|
|
195
|
+
* VALUES, so 'safe' mode has to withhold the message text too.
|
|
196
|
+
*
|
|
197
|
+
* Postgres splits this cleanly: `message` carries relation / constraint /
|
|
198
|
+
* column NAMES and `detail` carries the conflicting values, so redacting
|
|
199
|
+
* `detail` was enough. MySQL and SQL Server do not split it at all. mysql2's
|
|
200
|
+
* ER_DUP_ENTRY reads `Duplicate entry 'alice@example.com' for key
|
|
201
|
+
* 'users.email'` and SQL Server's 2627 ends `The duplicate key value is
|
|
202
|
+
* (alice@example.com).`, both on `message`, with no `detail` field anywhere.
|
|
203
|
+
* So `redactCauseForMode` took its "nothing value-bearing to remove" early
|
|
204
|
+
* return and handed back the driver error untouched, and the row value reached
|
|
205
|
+
* every log line, Sentry event and uncaught-rejection dump that renders the
|
|
206
|
+
* cause chain, in the mode whose entire job is to prevent exactly that.
|
|
207
|
+
*
|
|
208
|
+
* A flag rather than a code list, and set by the ENGINE that knows its own
|
|
209
|
+
* message grammar, for two reasons. It keeps Postgres byte-identical (pg never
|
|
210
|
+
* sets it, so its cause is returned exactly as before), and it puts "this
|
|
211
|
+
* engine's message embeds values" next to the code that reads that engine's
|
|
212
|
+
* messages instead of in a table here that would silently rot.
|
|
213
|
+
*
|
|
214
|
+
* `Symbol.for` so the ESM and CJS copies of this module agree on the key: a
|
|
215
|
+
* dual-package consumer can hand an error marked by one build to the other.
|
|
216
|
+
*/
|
|
217
|
+
const VALUE_BEARING_MESSAGE = Symbol.for('turbine.error.valueBearingMessage');
|
|
218
|
+
/**
|
|
219
|
+
* Mark a driver error as carrying row values in its `message` (see
|
|
220
|
+
* {@link VALUE_BEARING_MESSAGE}). Called by the engine error augmenters for the
|
|
221
|
+
* exact driver codes whose message grammar embeds a value; every other code is
|
|
222
|
+
* left alone so nothing is withheld that did not need to be.
|
|
223
|
+
*
|
|
224
|
+
* Non-enumerable so the flag itself never shows up in a serialized error, and
|
|
225
|
+
* best-effort so a frozen or exotic driver error cannot turn a constraint
|
|
226
|
+
* violation into a TypeError.
|
|
227
|
+
*/
|
|
228
|
+
export function markValueBearingMessage(err) {
|
|
229
|
+
if (!err || typeof err !== 'object')
|
|
230
|
+
return err;
|
|
231
|
+
try {
|
|
232
|
+
Object.defineProperty(err, VALUE_BEARING_MESSAGE, {
|
|
233
|
+
value: true,
|
|
234
|
+
enumerable: false,
|
|
235
|
+
writable: true,
|
|
236
|
+
configurable: true,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
/* a frozen driver error keeps its message; the Turbine message is still safe */
|
|
241
|
+
}
|
|
242
|
+
return err;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The replacement for a withheld driver message. The whole message goes, not a
|
|
246
|
+
* pattern-matched part of it: three engines with three grammars and no
|
|
247
|
+
* guarantee a future driver keeps either, so "withhold it" is the only rule
|
|
248
|
+
* that stays true. Nothing diagnostic is lost, the constraint/column/table
|
|
249
|
+
* names are already on the typed Turbine error as structured fields and in its
|
|
250
|
+
* own message, and `errorMessages: 'verbose'` returns the driver text in full.
|
|
251
|
+
*/
|
|
252
|
+
function redactedDriverMessage(code) {
|
|
253
|
+
const codePart = typeof code === 'string' && code.length > 0 ? ` (driver code ${code})` : '';
|
|
254
|
+
return `${REDACTED_DETAIL}${codePart}: this engine reports the conflicting row values in the message text, so errorMessages:"safe" withholds it. Use errorMessages:"verbose" to see it.`;
|
|
255
|
+
}
|
|
115
256
|
/**
|
|
116
257
|
* Postgres puts the CONFLICTING ROW VALUES in the `detail` field of a
|
|
117
258
|
* constraint error, and nowhere else: `Key (email)=(alice@example.com) already
|
|
@@ -148,17 +289,29 @@ export const REDACTED_DETAIL = '[redacted by turbine errorMessages:"safe"]';
|
|
|
148
289
|
*
|
|
149
290
|
* In 'verbose' mode the cause passes through untouched: that mode's documented
|
|
150
291
|
* job is full-fidelity debugging.
|
|
292
|
+
*
|
|
293
|
+
* ENGINES WHOSE MESSAGE CARRIES THE VALUE. MySQL and SQL Server do not have a
|
|
294
|
+
* `detail` field at all and put the conflicting value in `message`, so the
|
|
295
|
+
* early return below used to hand the raw driver error straight back (see
|
|
296
|
+
* {@link markValueBearingMessage}). When the engine set that flag the clone
|
|
297
|
+
* also withholds `message`, `sqlMessage` (mysql2's copy, which is the field
|
|
298
|
+
* mysql2 formats `message` FROM, so leaving it would put the value straight
|
|
299
|
+
* back) and the message text embedded in the rendered `stack` string. The stack
|
|
300
|
+
* substitution is an exact replacement of the known message string, never a
|
|
301
|
+
* grammar guess, so the frames survive intact.
|
|
151
302
|
*/
|
|
152
303
|
function redactCauseForMode(cause) {
|
|
153
|
-
if (
|
|
304
|
+
if (currentErrorMessageMode() === 'verbose')
|
|
154
305
|
return cause;
|
|
155
306
|
if (!cause || typeof cause !== 'object')
|
|
156
307
|
return cause;
|
|
157
308
|
const detail = cause.detail;
|
|
309
|
+
const hasDetail = typeof detail === 'string' && detail.length > 0;
|
|
310
|
+
const valueBearingMessage = cause[VALUE_BEARING_MESSAGE] === true;
|
|
158
311
|
// Nothing value-bearing to remove: return the original object so the common
|
|
159
312
|
// case (a non-pg cause, or a pg error without a detail) allocates nothing and
|
|
160
313
|
// keeps object identity with what the driver threw.
|
|
161
|
-
if (
|
|
314
|
+
if (!hasDetail && !valueBearingMessage)
|
|
162
315
|
return cause;
|
|
163
316
|
try {
|
|
164
317
|
const descriptors = Object.getOwnPropertyDescriptors(cause);
|
|
@@ -166,12 +319,36 @@ function redactCauseForMode(cause) {
|
|
|
166
319
|
// non-writable `detail` would make the assignment throw in strict mode
|
|
167
320
|
// (every module here is ESM, so it always would), and losing the cause is
|
|
168
321
|
// worse than paying for one descriptor literal.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
322
|
+
if (hasDetail) {
|
|
323
|
+
descriptors.detail = {
|
|
324
|
+
value: REDACTED_DETAIL,
|
|
325
|
+
writable: true,
|
|
326
|
+
enumerable: descriptors.detail?.enumerable ?? true,
|
|
327
|
+
configurable: true,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
// The exact message strings to scrub out of the rendered stack, collected
|
|
331
|
+
// BEFORE the descriptors are overwritten.
|
|
332
|
+
const withheldTexts = [];
|
|
333
|
+
if (valueBearingMessage) {
|
|
334
|
+
const code = cause.code;
|
|
335
|
+
const replacement = redactedDriverMessage(code);
|
|
336
|
+
for (const key of ['message', 'sqlMessage']) {
|
|
337
|
+
const current = cause[key];
|
|
338
|
+
if (typeof current !== 'string' || current.length === 0)
|
|
339
|
+
continue;
|
|
340
|
+
withheldTexts.push(current);
|
|
341
|
+
descriptors[key] = {
|
|
342
|
+
value: replacement,
|
|
343
|
+
writable: true,
|
|
344
|
+
// `message` is non-enumerable on a native Error and mysql2's
|
|
345
|
+
// `sqlMessage` is enumerable; keep whichever the driver chose so the
|
|
346
|
+
// clone serializes with the same key set as the original.
|
|
347
|
+
enumerable: descriptors[key]?.enumerable ?? false,
|
|
348
|
+
configurable: true,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
175
352
|
// Brand check rather than `instanceof Error`, so a driver error thrown from
|
|
176
353
|
// another realm (a worker, a bundled duplicate of pg) is still recognized.
|
|
177
354
|
const isError = Object.prototype.toString.call(cause) === '[object Error]';
|
|
@@ -189,7 +366,14 @@ function redactCauseForMode(cause) {
|
|
|
189
366
|
// own descriptor rather than having a lie written over it.
|
|
190
367
|
const originalStack = cause.stack;
|
|
191
368
|
if (typeof originalStack === 'string') {
|
|
192
|
-
|
|
369
|
+
// V8 renders the stack as `<name>: <message>\n at …`, so a withheld
|
|
370
|
+
// message is still sitting in it. Substitute the exact strings that were
|
|
371
|
+
// withheld (split/join, so a message repeated in a nested frame goes
|
|
372
|
+
// too); everything else, the frames included, is untouched.
|
|
373
|
+
let stackText = originalStack;
|
|
374
|
+
for (const text of withheldTexts)
|
|
375
|
+
stackText = stackText.split(text).join(REDACTED_DETAIL);
|
|
376
|
+
descriptors.stack = { value: stackText, writable: true, enumerable: false, configurable: true };
|
|
193
377
|
}
|
|
194
378
|
else if (descriptors.stack && typeof descriptors.stack.get === 'function') {
|
|
195
379
|
// An own accessor bound to the ORIGINAL receiver would return undefined
|
|
@@ -218,7 +402,7 @@ function redactCauseForMode(cause) {
|
|
|
218
402
|
* connect/update failures which historically embedded the raw values.
|
|
219
403
|
*/
|
|
220
404
|
export function describeTargetForMessage(target) {
|
|
221
|
-
if (
|
|
405
|
+
if (currentErrorMessageMode() === 'verbose') {
|
|
222
406
|
try {
|
|
223
407
|
return JSON.stringify(target);
|
|
224
408
|
}
|
|
@@ -292,11 +476,11 @@ export class NotFoundError extends TurbineError {
|
|
|
292
476
|
let message = input.message;
|
|
293
477
|
if (!message) {
|
|
294
478
|
if (operation && table) {
|
|
295
|
-
const wherePart = where !== undefined ? ` matching where: ${renderWhereForMessage(where,
|
|
479
|
+
const wherePart = where !== undefined ? ` matching where: ${renderWhereForMessage(where, currentErrorMessageMode())}` : '';
|
|
296
480
|
message = `[turbine] ${operation} on "${table}" found no record${wherePart}`;
|
|
297
481
|
}
|
|
298
482
|
else if (table) {
|
|
299
|
-
const wherePart = where !== undefined ? ` matching where ${renderWhereForMessage(where,
|
|
483
|
+
const wherePart = where !== undefined ? ` matching where ${renderWhereForMessage(where, currentErrorMessageMode())}` : '';
|
|
300
484
|
message = `[turbine] No record found in "${table}"${wherePart}`;
|
|
301
485
|
}
|
|
302
486
|
else {
|
|
@@ -362,6 +546,31 @@ export class ConnectionError extends TurbineError {
|
|
|
362
546
|
this.sqlstate = options?.sqlstate;
|
|
363
547
|
}
|
|
364
548
|
}
|
|
549
|
+
/**
|
|
550
|
+
* The message for "this engine's connection string could not be parsed".
|
|
551
|
+
*
|
|
552
|
+
* The one rule it exists to enforce: NEVER echo the value. A DSN carries a
|
|
553
|
+
* password, and the trigger for this error is a MALFORMED DSN, which is exactly
|
|
554
|
+
* when someone pastes the failure into a bug report, a CI log, or an error
|
|
555
|
+
* tracker. No redaction written against the URL grammar can be trusted on a
|
|
556
|
+
* string that just failed to parse as a URL, so the only safe amount of it to
|
|
557
|
+
* include is none: not the password, not the host, not a prefix.
|
|
558
|
+
*
|
|
559
|
+
* This mirrors the Postgres path (`assertUsableConnectionString` in client.ts),
|
|
560
|
+
* which reached the same conclusion first and stated the same reason; the
|
|
561
|
+
* engines used to interpolate the raw string instead, so SECURITY.md's claim of
|
|
562
|
+
* redaction "in all CLI error output" was true for one engine out of four.
|
|
563
|
+
* Shared rather than copied so the three engines cannot drift back apart.
|
|
564
|
+
*
|
|
565
|
+
* @param engine the human engine label ("MySQL", "SQL Server", "PowDB").
|
|
566
|
+
* @param example a well-formed connection string for that engine, with a
|
|
567
|
+
* placeholder password. Never derived from the caller's value.
|
|
568
|
+
*/
|
|
569
|
+
export function malformedConnectionStringMessage(engine, example) {
|
|
570
|
+
return (`[turbine] The ${engine} connection string could not be parsed as a URL. Expected something like "${example}". ` +
|
|
571
|
+
'(Check for a missing "//", a stray quote copied out of a .env file, or a shell-truncated value.) ' +
|
|
572
|
+
'The value is not included here because it may contain a password.');
|
|
573
|
+
}
|
|
365
574
|
/** Thrown when a relation reference is invalid */
|
|
366
575
|
export class RelationError extends TurbineError {
|
|
367
576
|
constructor(message) {
|
|
@@ -419,7 +628,7 @@ export class UniqueConstraintError extends TurbineError {
|
|
|
419
628
|
// values straight back into any log line that prints the error object).
|
|
420
629
|
// The structured `.columns`/`.constraint`/`.column` fields survive in
|
|
421
630
|
// both modes, they carry NAMES, never values.
|
|
422
|
-
const detail =
|
|
631
|
+
const detail = currentErrorMessageMode() === 'verbose' ? detailFromCause(cause) : undefined;
|
|
423
632
|
if (detail)
|
|
424
633
|
message += `: ${detail}`;
|
|
425
634
|
}
|
|
@@ -449,7 +658,7 @@ export class ForeignKeyError extends TurbineError {
|
|
|
449
658
|
// values straight back into any log line that prints the error object).
|
|
450
659
|
// The structured `.columns`/`.constraint`/`.column` fields survive in
|
|
451
660
|
// both modes, they carry NAMES, never values.
|
|
452
|
-
const detail =
|
|
661
|
+
const detail = currentErrorMessageMode() === 'verbose' ? detailFromCause(cause) : undefined;
|
|
453
662
|
if (detail)
|
|
454
663
|
message += `: ${detail}`;
|
|
455
664
|
}
|
|
@@ -478,7 +687,7 @@ export class NotNullViolationError extends TurbineError {
|
|
|
478
687
|
// values straight back into any log line that prints the error object).
|
|
479
688
|
// The structured `.columns`/`.constraint`/`.column` fields survive in
|
|
480
689
|
// both modes, they carry NAMES, never values.
|
|
481
|
-
const detail =
|
|
690
|
+
const detail = currentErrorMessageMode() === 'verbose' ? detailFromCause(cause) : undefined;
|
|
482
691
|
if (detail)
|
|
483
692
|
message += `: ${detail}`;
|
|
484
693
|
}
|
|
@@ -572,7 +781,7 @@ export class CheckConstraintError extends TurbineError {
|
|
|
572
781
|
// values straight back into any log line that prints the error object).
|
|
573
782
|
// The structured `.columns`/`.constraint`/`.column` fields survive in
|
|
574
783
|
// both modes, they carry NAMES, never values.
|
|
575
|
-
const detail =
|
|
784
|
+
const detail = currentErrorMessageMode() === 'verbose' ? detailFromCause(cause) : undefined;
|
|
576
785
|
if (detail)
|
|
577
786
|
message += `: ${detail}`;
|
|
578
787
|
}
|
|
@@ -600,7 +809,7 @@ export class ExclusionConstraintError extends TurbineError {
|
|
|
600
809
|
// values straight back into any log line that prints the error object).
|
|
601
810
|
// The structured `.columns`/`.constraint`/`.column` fields survive in
|
|
602
811
|
// both modes, they carry NAMES, never values.
|
|
603
|
-
const detail =
|
|
812
|
+
const detail = currentErrorMessageMode() === 'verbose' ? detailFromCause(cause) : undefined;
|
|
604
813
|
if (detail)
|
|
605
814
|
message += `: ${detail}`;
|
|
606
815
|
}
|