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/cjs/errors.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* turbine-orm, Error types
|
|
4
|
-
*
|
|
5
|
-
* Typed errors with error codes for programmatic handling.
|
|
6
|
-
* All Turbine errors extend TurbineError which includes a `code` property.
|
|
7
|
-
*/
|
|
8
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
3
|
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;
|
|
4
|
+
exports.registerClientErrorMessageMode = registerClientErrorMessageMode;
|
|
5
|
+
exports.errorMessageModesDiverged = errorMessageModesDiverged;
|
|
6
|
+
exports.runWithErrorMessageMode = runWithErrorMessageMode;
|
|
10
7
|
exports.setErrorMessageMode = setErrorMessageMode;
|
|
11
8
|
exports.getErrorMessageMode = getErrorMessageMode;
|
|
9
|
+
exports.markValueBearingMessage = markValueBearingMessage;
|
|
12
10
|
exports.describeTargetForMessage = describeTargetForMessage;
|
|
11
|
+
exports.malformedConnectionStringMessage = malformedConnectionStringMessage;
|
|
13
12
|
exports.wrapPgError = wrapPgError;
|
|
13
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
14
|
+
/**
|
|
15
|
+
* turbine-orm, Error types
|
|
16
|
+
*
|
|
17
|
+
* Typed errors with error codes for programmatic handling.
|
|
18
|
+
* All Turbine errors extend TurbineError which includes a `code` property.
|
|
19
|
+
*/
|
|
14
20
|
/** Error codes for all Turbine errors */
|
|
15
21
|
exports.TurbineErrorCode = {
|
|
16
22
|
NOT_FOUND: 'TURBINE_E001',
|
|
@@ -85,6 +91,76 @@ class TurbineError extends Error {
|
|
|
85
91
|
}
|
|
86
92
|
exports.TurbineError = TurbineError;
|
|
87
93
|
let errorMessageMode = 'safe';
|
|
94
|
+
/**
|
|
95
|
+
* The mode in force for the operation currently executing, when a client has
|
|
96
|
+
* established one. See {@link runWithErrorMessageMode}.
|
|
97
|
+
*/
|
|
98
|
+
const errorMessageModeScope = new node_async_hooks_1.AsyncLocalStorage();
|
|
99
|
+
/** The first mode a TurbineClient registered, used to detect divergence. */
|
|
100
|
+
let firstRegisteredClientMode;
|
|
101
|
+
let clientModesDiverged = false;
|
|
102
|
+
/**
|
|
103
|
+
* The mode that applies right now: the scope a client established for this
|
|
104
|
+
* operation, else the process-wide default.
|
|
105
|
+
*
|
|
106
|
+
* The default is the fallback rather than the authority, which is the whole
|
|
107
|
+
* point. `errorMessageMode` is a module-level `let` that every TurbineClient
|
|
108
|
+
* constructor overwrote, so in a process with a primary client and an analytics
|
|
109
|
+
* or replica client, or a test harness building one client per suite, the LAST
|
|
110
|
+
* client constructed decided the mode for ALL of them, order-dependently. A
|
|
111
|
+
* client asking for `'safe'` could silently be running in `'verbose'`, which is
|
|
112
|
+
* the direction that leaks. (The dual ESM+CJS build makes it worse: the two
|
|
113
|
+
* copies hold separate `let`s.)
|
|
114
|
+
*
|
|
115
|
+
* An error constructed outside any client, directly or via
|
|
116
|
+
* {@link setErrorMessageMode}, still formats by the default, unchanged.
|
|
117
|
+
*/
|
|
118
|
+
function currentErrorMessageMode() {
|
|
119
|
+
return errorMessageModeScope.getStore() ?? errorMessageMode;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Register a client's configured mode and report whether per-operation scoping
|
|
123
|
+
* is now required.
|
|
124
|
+
*
|
|
125
|
+
* The gate is DIVERGENCE, not "has a client been built": while every client in
|
|
126
|
+
* the process agrees (the overwhelmingly common case, including every
|
|
127
|
+
* single-client app), the module default is already the right answer for all of
|
|
128
|
+
* them and no scope has to be established, so the query path pays exactly
|
|
129
|
+
* nothing. The moment two clients disagree, every client starts scoping, which
|
|
130
|
+
* is why this is read per call rather than latched per client.
|
|
131
|
+
*
|
|
132
|
+
* @internal Used by TurbineClient; not part of the public surface.
|
|
133
|
+
*/
|
|
134
|
+
function registerClientErrorMessageMode(mode) {
|
|
135
|
+
if (firstRegisteredClientMode === undefined)
|
|
136
|
+
firstRegisteredClientMode = mode;
|
|
137
|
+
else if (firstRegisteredClientMode !== mode)
|
|
138
|
+
clientModesDiverged = true;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Whether any two clients in this process have registered different modes, so
|
|
142
|
+
* the module default can no longer speak for all of them.
|
|
143
|
+
*
|
|
144
|
+
* @internal
|
|
145
|
+
*/
|
|
146
|
+
function errorMessageModesDiverged() {
|
|
147
|
+
return clientModesDiverged;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Run `fn` with `mode` in force for everything it does, awaits included.
|
|
151
|
+
*
|
|
152
|
+
* Establishing the scope around the call (rather than threading a mode
|
|
153
|
+
* parameter into every error constructor) is what makes this reachable at all:
|
|
154
|
+
* the mode-sensitive errors are built deep inside the query executor, the write
|
|
155
|
+
* builders and the nested-write engine, none of which are handed the client.
|
|
156
|
+
* The async context is captured when the operation's promise chain is created
|
|
157
|
+
* INSIDE this call, so every continuation of it resolves the same mode.
|
|
158
|
+
*
|
|
159
|
+
* @internal
|
|
160
|
+
*/
|
|
161
|
+
function runWithErrorMessageMode(mode, fn) {
|
|
162
|
+
return errorMessageModeScope.run(mode, fn);
|
|
163
|
+
}
|
|
88
164
|
/**
|
|
89
165
|
* Set the global NotFoundError message mode. Called from the TurbineClient
|
|
90
166
|
* constructor when `TurbineConfig.errorMessages` is provided.
|
|
@@ -110,9 +186,16 @@ let errorMessageMode = 'safe';
|
|
|
110
186
|
function setErrorMessageMode(mode) {
|
|
111
187
|
errorMessageMode = mode;
|
|
112
188
|
}
|
|
113
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* Returns the NotFoundError message mode in effect right now.
|
|
191
|
+
*
|
|
192
|
+
* Called inside an operation issued through a client whose mode differs from
|
|
193
|
+
* another client's in the same process, that is THAT client's mode; anywhere
|
|
194
|
+
* else it is the process default {@link setErrorMessageMode} last set, which is
|
|
195
|
+
* what it has always returned. Exported for tests.
|
|
196
|
+
*/
|
|
114
197
|
function getErrorMessageMode() {
|
|
115
|
-
return
|
|
198
|
+
return currentErrorMessageMode();
|
|
116
199
|
}
|
|
117
200
|
/**
|
|
118
201
|
* The marker left where a driver `detail` string was removed in 'safe' mode.
|
|
@@ -120,6 +203,69 @@ function getErrorMessageMode() {
|
|
|
120
203
|
* assertions can match on it without hardcoding the wording.
|
|
121
204
|
*/
|
|
122
205
|
exports.REDACTED_DETAIL = '[redacted by turbine errorMessages:"safe"]';
|
|
206
|
+
/**
|
|
207
|
+
* Marks a driver error whose `message` (not only its `detail`) embeds row
|
|
208
|
+
* VALUES, so 'safe' mode has to withhold the message text too.
|
|
209
|
+
*
|
|
210
|
+
* Postgres splits this cleanly: `message` carries relation / constraint /
|
|
211
|
+
* column NAMES and `detail` carries the conflicting values, so redacting
|
|
212
|
+
* `detail` was enough. MySQL and SQL Server do not split it at all. mysql2's
|
|
213
|
+
* ER_DUP_ENTRY reads `Duplicate entry 'alice@example.com' for key
|
|
214
|
+
* 'users.email'` and SQL Server's 2627 ends `The duplicate key value is
|
|
215
|
+
* (alice@example.com).`, both on `message`, with no `detail` field anywhere.
|
|
216
|
+
* So `redactCauseForMode` took its "nothing value-bearing to remove" early
|
|
217
|
+
* return and handed back the driver error untouched, and the row value reached
|
|
218
|
+
* every log line, Sentry event and uncaught-rejection dump that renders the
|
|
219
|
+
* cause chain, in the mode whose entire job is to prevent exactly that.
|
|
220
|
+
*
|
|
221
|
+
* A flag rather than a code list, and set by the ENGINE that knows its own
|
|
222
|
+
* message grammar, for two reasons. It keeps Postgres byte-identical (pg never
|
|
223
|
+
* sets it, so its cause is returned exactly as before), and it puts "this
|
|
224
|
+
* engine's message embeds values" next to the code that reads that engine's
|
|
225
|
+
* messages instead of in a table here that would silently rot.
|
|
226
|
+
*
|
|
227
|
+
* `Symbol.for` so the ESM and CJS copies of this module agree on the key: a
|
|
228
|
+
* dual-package consumer can hand an error marked by one build to the other.
|
|
229
|
+
*/
|
|
230
|
+
const VALUE_BEARING_MESSAGE = Symbol.for('turbine.error.valueBearingMessage');
|
|
231
|
+
/**
|
|
232
|
+
* Mark a driver error as carrying row values in its `message` (see
|
|
233
|
+
* {@link VALUE_BEARING_MESSAGE}). Called by the engine error augmenters for the
|
|
234
|
+
* exact driver codes whose message grammar embeds a value; every other code is
|
|
235
|
+
* left alone so nothing is withheld that did not need to be.
|
|
236
|
+
*
|
|
237
|
+
* Non-enumerable so the flag itself never shows up in a serialized error, and
|
|
238
|
+
* best-effort so a frozen or exotic driver error cannot turn a constraint
|
|
239
|
+
* violation into a TypeError.
|
|
240
|
+
*/
|
|
241
|
+
function markValueBearingMessage(err) {
|
|
242
|
+
if (!err || typeof err !== 'object')
|
|
243
|
+
return err;
|
|
244
|
+
try {
|
|
245
|
+
Object.defineProperty(err, VALUE_BEARING_MESSAGE, {
|
|
246
|
+
value: true,
|
|
247
|
+
enumerable: false,
|
|
248
|
+
writable: true,
|
|
249
|
+
configurable: true,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
/* a frozen driver error keeps its message; the Turbine message is still safe */
|
|
254
|
+
}
|
|
255
|
+
return err;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The replacement for a withheld driver message. The whole message goes, not a
|
|
259
|
+
* pattern-matched part of it: three engines with three grammars and no
|
|
260
|
+
* guarantee a future driver keeps either, so "withhold it" is the only rule
|
|
261
|
+
* that stays true. Nothing diagnostic is lost, the constraint/column/table
|
|
262
|
+
* names are already on the typed Turbine error as structured fields and in its
|
|
263
|
+
* own message, and `errorMessages: 'verbose'` returns the driver text in full.
|
|
264
|
+
*/
|
|
265
|
+
function redactedDriverMessage(code) {
|
|
266
|
+
const codePart = typeof code === 'string' && code.length > 0 ? ` (driver code ${code})` : '';
|
|
267
|
+
return `${exports.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.`;
|
|
268
|
+
}
|
|
123
269
|
/**
|
|
124
270
|
* Postgres puts the CONFLICTING ROW VALUES in the `detail` field of a
|
|
125
271
|
* constraint error, and nowhere else: `Key (email)=(alice@example.com) already
|
|
@@ -156,17 +302,29 @@ exports.REDACTED_DETAIL = '[redacted by turbine errorMessages:"safe"]';
|
|
|
156
302
|
*
|
|
157
303
|
* In 'verbose' mode the cause passes through untouched: that mode's documented
|
|
158
304
|
* job is full-fidelity debugging.
|
|
305
|
+
*
|
|
306
|
+
* ENGINES WHOSE MESSAGE CARRIES THE VALUE. MySQL and SQL Server do not have a
|
|
307
|
+
* `detail` field at all and put the conflicting value in `message`, so the
|
|
308
|
+
* early return below used to hand the raw driver error straight back (see
|
|
309
|
+
* {@link markValueBearingMessage}). When the engine set that flag the clone
|
|
310
|
+
* also withholds `message`, `sqlMessage` (mysql2's copy, which is the field
|
|
311
|
+
* mysql2 formats `message` FROM, so leaving it would put the value straight
|
|
312
|
+
* back) and the message text embedded in the rendered `stack` string. The stack
|
|
313
|
+
* substitution is an exact replacement of the known message string, never a
|
|
314
|
+
* grammar guess, so the frames survive intact.
|
|
159
315
|
*/
|
|
160
316
|
function redactCauseForMode(cause) {
|
|
161
|
-
if (
|
|
317
|
+
if (currentErrorMessageMode() === 'verbose')
|
|
162
318
|
return cause;
|
|
163
319
|
if (!cause || typeof cause !== 'object')
|
|
164
320
|
return cause;
|
|
165
321
|
const detail = cause.detail;
|
|
322
|
+
const hasDetail = typeof detail === 'string' && detail.length > 0;
|
|
323
|
+
const valueBearingMessage = cause[VALUE_BEARING_MESSAGE] === true;
|
|
166
324
|
// Nothing value-bearing to remove: return the original object so the common
|
|
167
325
|
// case (a non-pg cause, or a pg error without a detail) allocates nothing and
|
|
168
326
|
// keeps object identity with what the driver threw.
|
|
169
|
-
if (
|
|
327
|
+
if (!hasDetail && !valueBearingMessage)
|
|
170
328
|
return cause;
|
|
171
329
|
try {
|
|
172
330
|
const descriptors = Object.getOwnPropertyDescriptors(cause);
|
|
@@ -174,12 +332,36 @@ function redactCauseForMode(cause) {
|
|
|
174
332
|
// non-writable `detail` would make the assignment throw in strict mode
|
|
175
333
|
// (every module here is ESM, so it always would), and losing the cause is
|
|
176
334
|
// worse than paying for one descriptor literal.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
335
|
+
if (hasDetail) {
|
|
336
|
+
descriptors.detail = {
|
|
337
|
+
value: exports.REDACTED_DETAIL,
|
|
338
|
+
writable: true,
|
|
339
|
+
enumerable: descriptors.detail?.enumerable ?? true,
|
|
340
|
+
configurable: true,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
// The exact message strings to scrub out of the rendered stack, collected
|
|
344
|
+
// BEFORE the descriptors are overwritten.
|
|
345
|
+
const withheldTexts = [];
|
|
346
|
+
if (valueBearingMessage) {
|
|
347
|
+
const code = cause.code;
|
|
348
|
+
const replacement = redactedDriverMessage(code);
|
|
349
|
+
for (const key of ['message', 'sqlMessage']) {
|
|
350
|
+
const current = cause[key];
|
|
351
|
+
if (typeof current !== 'string' || current.length === 0)
|
|
352
|
+
continue;
|
|
353
|
+
withheldTexts.push(current);
|
|
354
|
+
descriptors[key] = {
|
|
355
|
+
value: replacement,
|
|
356
|
+
writable: true,
|
|
357
|
+
// `message` is non-enumerable on a native Error and mysql2's
|
|
358
|
+
// `sqlMessage` is enumerable; keep whichever the driver chose so the
|
|
359
|
+
// clone serializes with the same key set as the original.
|
|
360
|
+
enumerable: descriptors[key]?.enumerable ?? false,
|
|
361
|
+
configurable: true,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
183
365
|
// Brand check rather than `instanceof Error`, so a driver error thrown from
|
|
184
366
|
// another realm (a worker, a bundled duplicate of pg) is still recognized.
|
|
185
367
|
const isError = Object.prototype.toString.call(cause) === '[object Error]';
|
|
@@ -197,7 +379,14 @@ function redactCauseForMode(cause) {
|
|
|
197
379
|
// own descriptor rather than having a lie written over it.
|
|
198
380
|
const originalStack = cause.stack;
|
|
199
381
|
if (typeof originalStack === 'string') {
|
|
200
|
-
|
|
382
|
+
// V8 renders the stack as `<name>: <message>\n at …`, so a withheld
|
|
383
|
+
// message is still sitting in it. Substitute the exact strings that were
|
|
384
|
+
// withheld (split/join, so a message repeated in a nested frame goes
|
|
385
|
+
// too); everything else, the frames included, is untouched.
|
|
386
|
+
let stackText = originalStack;
|
|
387
|
+
for (const text of withheldTexts)
|
|
388
|
+
stackText = stackText.split(text).join(exports.REDACTED_DETAIL);
|
|
389
|
+
descriptors.stack = { value: stackText, writable: true, enumerable: false, configurable: true };
|
|
201
390
|
}
|
|
202
391
|
else if (descriptors.stack && typeof descriptors.stack.get === 'function') {
|
|
203
392
|
// An own accessor bound to the ORIGINAL receiver would return undefined
|
|
@@ -226,7 +415,7 @@ function redactCauseForMode(cause) {
|
|
|
226
415
|
* connect/update failures which historically embedded the raw values.
|
|
227
416
|
*/
|
|
228
417
|
function describeTargetForMessage(target) {
|
|
229
|
-
if (
|
|
418
|
+
if (currentErrorMessageMode() === 'verbose') {
|
|
230
419
|
try {
|
|
231
420
|
return JSON.stringify(target);
|
|
232
421
|
}
|
|
@@ -300,11 +489,11 @@ class NotFoundError extends TurbineError {
|
|
|
300
489
|
let message = input.message;
|
|
301
490
|
if (!message) {
|
|
302
491
|
if (operation && table) {
|
|
303
|
-
const wherePart = where !== undefined ? ` matching where: ${renderWhereForMessage(where,
|
|
492
|
+
const wherePart = where !== undefined ? ` matching where: ${renderWhereForMessage(where, currentErrorMessageMode())}` : '';
|
|
304
493
|
message = `[turbine] ${operation} on "${table}" found no record${wherePart}`;
|
|
305
494
|
}
|
|
306
495
|
else if (table) {
|
|
307
|
-
const wherePart = where !== undefined ? ` matching where ${renderWhereForMessage(where,
|
|
496
|
+
const wherePart = where !== undefined ? ` matching where ${renderWhereForMessage(where, currentErrorMessageMode())}` : '';
|
|
308
497
|
message = `[turbine] No record found in "${table}"${wherePart}`;
|
|
309
498
|
}
|
|
310
499
|
else {
|
|
@@ -374,6 +563,31 @@ class ConnectionError extends TurbineError {
|
|
|
374
563
|
}
|
|
375
564
|
}
|
|
376
565
|
exports.ConnectionError = ConnectionError;
|
|
566
|
+
/**
|
|
567
|
+
* The message for "this engine's connection string could not be parsed".
|
|
568
|
+
*
|
|
569
|
+
* The one rule it exists to enforce: NEVER echo the value. A DSN carries a
|
|
570
|
+
* password, and the trigger for this error is a MALFORMED DSN, which is exactly
|
|
571
|
+
* when someone pastes the failure into a bug report, a CI log, or an error
|
|
572
|
+
* tracker. No redaction written against the URL grammar can be trusted on a
|
|
573
|
+
* string that just failed to parse as a URL, so the only safe amount of it to
|
|
574
|
+
* include is none: not the password, not the host, not a prefix.
|
|
575
|
+
*
|
|
576
|
+
* This mirrors the Postgres path (`assertUsableConnectionString` in client.ts),
|
|
577
|
+
* which reached the same conclusion first and stated the same reason; the
|
|
578
|
+
* engines used to interpolate the raw string instead, so SECURITY.md's claim of
|
|
579
|
+
* redaction "in all CLI error output" was true for one engine out of four.
|
|
580
|
+
* Shared rather than copied so the three engines cannot drift back apart.
|
|
581
|
+
*
|
|
582
|
+
* @param engine the human engine label ("MySQL", "SQL Server", "PowDB").
|
|
583
|
+
* @param example a well-formed connection string for that engine, with a
|
|
584
|
+
* placeholder password. Never derived from the caller's value.
|
|
585
|
+
*/
|
|
586
|
+
function malformedConnectionStringMessage(engine, example) {
|
|
587
|
+
return (`[turbine] The ${engine} connection string could not be parsed as a URL. Expected something like "${example}". ` +
|
|
588
|
+
'(Check for a missing "//", a stray quote copied out of a .env file, or a shell-truncated value.) ' +
|
|
589
|
+
'The value is not included here because it may contain a password.');
|
|
590
|
+
}
|
|
377
591
|
/** Thrown when a relation reference is invalid */
|
|
378
592
|
class RelationError extends TurbineError {
|
|
379
593
|
constructor(message) {
|
|
@@ -434,7 +648,7 @@ class UniqueConstraintError extends TurbineError {
|
|
|
434
648
|
// values straight back into any log line that prints the error object).
|
|
435
649
|
// The structured `.columns`/`.constraint`/`.column` fields survive in
|
|
436
650
|
// both modes, they carry NAMES, never values.
|
|
437
|
-
const detail =
|
|
651
|
+
const detail = currentErrorMessageMode() === 'verbose' ? detailFromCause(cause) : undefined;
|
|
438
652
|
if (detail)
|
|
439
653
|
message += `: ${detail}`;
|
|
440
654
|
}
|
|
@@ -465,7 +679,7 @@ class ForeignKeyError extends TurbineError {
|
|
|
465
679
|
// values straight back into any log line that prints the error object).
|
|
466
680
|
// The structured `.columns`/`.constraint`/`.column` fields survive in
|
|
467
681
|
// both modes, they carry NAMES, never values.
|
|
468
|
-
const detail =
|
|
682
|
+
const detail = currentErrorMessageMode() === 'verbose' ? detailFromCause(cause) : undefined;
|
|
469
683
|
if (detail)
|
|
470
684
|
message += `: ${detail}`;
|
|
471
685
|
}
|
|
@@ -495,7 +709,7 @@ class NotNullViolationError extends TurbineError {
|
|
|
495
709
|
// values straight back into any log line that prints the error object).
|
|
496
710
|
// The structured `.columns`/`.constraint`/`.column` fields survive in
|
|
497
711
|
// both modes, they carry NAMES, never values.
|
|
498
|
-
const detail =
|
|
712
|
+
const detail = currentErrorMessageMode() === 'verbose' ? detailFromCause(cause) : undefined;
|
|
499
713
|
if (detail)
|
|
500
714
|
message += `: ${detail}`;
|
|
501
715
|
}
|
|
@@ -592,7 +806,7 @@ class CheckConstraintError extends TurbineError {
|
|
|
592
806
|
// values straight back into any log line that prints the error object).
|
|
593
807
|
// The structured `.columns`/`.constraint`/`.column` fields survive in
|
|
594
808
|
// both modes, they carry NAMES, never values.
|
|
595
|
-
const detail =
|
|
809
|
+
const detail = currentErrorMessageMode() === 'verbose' ? detailFromCause(cause) : undefined;
|
|
596
810
|
if (detail)
|
|
597
811
|
message += `: ${detail}`;
|
|
598
812
|
}
|
|
@@ -621,7 +835,7 @@ class ExclusionConstraintError extends TurbineError {
|
|
|
621
835
|
// values straight back into any log line that prints the error object).
|
|
622
836
|
// The structured `.columns`/`.constraint`/`.column` fields survive in
|
|
623
837
|
// both modes, they carry NAMES, never values.
|
|
624
|
-
const detail =
|
|
838
|
+
const detail = currentErrorMessageMode() === 'verbose' ? detailFromCause(cause) : undefined;
|
|
625
839
|
if (detail)
|
|
626
840
|
message += `: ${detail}`;
|
|
627
841
|
}
|
|
@@ -46,7 +46,19 @@ export interface CreateIndexSqlOptions {
|
|
|
46
46
|
* it must also carry the `-- turbine:no-transaction` directive.
|
|
47
47
|
*/
|
|
48
48
|
concurrently?: boolean;
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Emit `IF NOT EXISTS`. Defaults to true for the plain (in-transaction) form
|
|
51
|
+
* and FALSE when `concurrently` is set.
|
|
52
|
+
*
|
|
53
|
+
* `IF NOT EXISTS` matches on the index NAME, never on its validity, so over
|
|
54
|
+
* the INVALID index a failed concurrent build leaves behind it silently
|
|
55
|
+
* no-ops: the migration records as applied, the index doctor reported is
|
|
56
|
+
* still missing, and the documented remedy (DROP INDEX CONCURRENTLY, then
|
|
57
|
+
* rerun) is unreachable through `migrate up` because the rerun no-ops too.
|
|
58
|
+
* The concurrent form gets its idempotency from a preceding
|
|
59
|
+
* `DROP INDEX CONCURRENTLY IF EXISTS` instead; see
|
|
60
|
+
* {@link buildCreateIndexStatements}, which is what the CLI emits.
|
|
61
|
+
*/
|
|
50
62
|
ifNotExists?: boolean;
|
|
51
63
|
/**
|
|
52
64
|
* Emit a partial index `... WHERE <col> IS NOT NULL`. Only applied for a
|
|
@@ -63,6 +75,26 @@ export interface CreateIndexSqlOptions {
|
|
|
63
75
|
* module topology-only.
|
|
64
76
|
*/
|
|
65
77
|
export declare function buildCreateIndexSql(table: string, columns: string[], indexName: string, options?: CreateIndexSqlOptions): string;
|
|
78
|
+
/**
|
|
79
|
+
* The statement SEQUENCE that builds one fix index and converges on a VALID
|
|
80
|
+
* index however many times it is rerun.
|
|
81
|
+
*
|
|
82
|
+
* A no-transaction migration is recorded only after ALL its statements succeed,
|
|
83
|
+
* so a mid-file failure leaves earlier indexes built and the migration
|
|
84
|
+
* unrecorded: a rerun must be safe. The old answer was `CREATE INDEX
|
|
85
|
+
* CONCURRENTLY IF NOT EXISTS`, which is safe but not CONVERGENT: a concurrent
|
|
86
|
+
* build that fails partway leaves an INVALID index with the right name, and
|
|
87
|
+
* every subsequent run skips it. Measured: the index was INVALID before the fix
|
|
88
|
+
* migration, `migrate up` reported 1 applied and 0 errors, and the index was
|
|
89
|
+
* still INVALID after.
|
|
90
|
+
*
|
|
91
|
+
* So the concurrent form drops first instead. `DROP INDEX CONCURRENTLY IF
|
|
92
|
+
* EXISTS` is a no-op on the first run (the index is missing, which is why
|
|
93
|
+
* doctor proposed it), and on a rerun it clears the corpse so the CREATE
|
|
94
|
+
* actually rebuilds. Neither statement takes a blocking lock, and both are
|
|
95
|
+
* legal only outside a transaction, which this file already is.
|
|
96
|
+
*/
|
|
97
|
+
export declare function buildCreateIndexStatements(table: string, columns: string[], indexName: string, options?: CreateIndexSqlOptions): string[];
|
|
66
98
|
/** Build the matching `DROP INDEX` statement. `concurrently` requires no-transaction execution. */
|
|
67
99
|
export declare function buildDropIndexSql(indexName: string, options?: {
|
|
68
100
|
concurrently?: boolean;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
20
|
exports.buildCreateIndexSql = buildCreateIndexSql;
|
|
21
|
+
exports.buildCreateIndexStatements = buildCreateIndexStatements;
|
|
21
22
|
exports.buildDropIndexSql = buildDropIndexSql;
|
|
22
23
|
exports.isProbeIndexed = isProbeIndexed;
|
|
23
24
|
exports.doctorIndexName = doctorIndexName;
|
|
@@ -36,7 +37,12 @@ const schema_js_1 = require("./schema.js");
|
|
|
36
37
|
*/
|
|
37
38
|
function buildCreateIndexSql(table, columns, indexName, options = {}) {
|
|
38
39
|
const concurrently = options.concurrently ? 'CONCURRENTLY ' : '';
|
|
39
|
-
|
|
40
|
+
// Default differs by form: the plain statement runs inside a transaction and
|
|
41
|
+
// can never leave an INVALID corpse, so IF NOT EXISTS is a pure win there.
|
|
42
|
+
// The CONCURRENTLY form can, and IF NOT EXISTS would then match that corpse
|
|
43
|
+
// by name and skip the rebuild forever (see CreateIndexSqlOptions).
|
|
44
|
+
const wantIfNotExists = options.ifNotExists ?? !options.concurrently;
|
|
45
|
+
const ifNotExists = wantIfNotExists ? 'IF NOT EXISTS ' : '';
|
|
40
46
|
const cols = columns.map(utils_js_1.quoteIdent).join(', ');
|
|
41
47
|
let sql = `CREATE INDEX ${concurrently}${ifNotExists}${(0, utils_js_1.quoteIdent)(indexName)} ON ${(0, utils_js_1.quoteIdent)(table)} (${cols})`;
|
|
42
48
|
if (options.partialNotNull && columns.length === 1 && columns[0] !== undefined) {
|
|
@@ -44,6 +50,31 @@ function buildCreateIndexSql(table, columns, indexName, options = {}) {
|
|
|
44
50
|
}
|
|
45
51
|
return `${sql};`;
|
|
46
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* The statement SEQUENCE that builds one fix index and converges on a VALID
|
|
55
|
+
* index however many times it is rerun.
|
|
56
|
+
*
|
|
57
|
+
* A no-transaction migration is recorded only after ALL its statements succeed,
|
|
58
|
+
* so a mid-file failure leaves earlier indexes built and the migration
|
|
59
|
+
* unrecorded: a rerun must be safe. The old answer was `CREATE INDEX
|
|
60
|
+
* CONCURRENTLY IF NOT EXISTS`, which is safe but not CONVERGENT: a concurrent
|
|
61
|
+
* build that fails partway leaves an INVALID index with the right name, and
|
|
62
|
+
* every subsequent run skips it. Measured: the index was INVALID before the fix
|
|
63
|
+
* migration, `migrate up` reported 1 applied and 0 errors, and the index was
|
|
64
|
+
* still INVALID after.
|
|
65
|
+
*
|
|
66
|
+
* So the concurrent form drops first instead. `DROP INDEX CONCURRENTLY IF
|
|
67
|
+
* EXISTS` is a no-op on the first run (the index is missing, which is why
|
|
68
|
+
* doctor proposed it), and on a rerun it clears the corpse so the CREATE
|
|
69
|
+
* actually rebuilds. Neither statement takes a blocking lock, and both are
|
|
70
|
+
* legal only outside a transaction, which this file already is.
|
|
71
|
+
*/
|
|
72
|
+
function buildCreateIndexStatements(table, columns, indexName, options = {}) {
|
|
73
|
+
const create = buildCreateIndexSql(table, columns, indexName, options);
|
|
74
|
+
if (!options.concurrently)
|
|
75
|
+
return [create];
|
|
76
|
+
return [buildDropIndexSql(indexName, { concurrently: true, ifExists: true }), create];
|
|
77
|
+
}
|
|
47
78
|
/** Build the matching `DROP INDEX` statement. `concurrently` requires no-transaction execution. */
|
|
48
79
|
function buildDropIndexSql(indexName, options = {}) {
|
|
49
80
|
const concurrently = options.concurrently ? 'CONCURRENTLY ' : '';
|
package/dist/cjs/introspect.d.ts
CHANGED
|
@@ -283,6 +283,54 @@ export declare function buildRelationsFromForeignKeys(foreignKeys: ForeignKeyEnt
|
|
|
283
283
|
* instead of silently dropping the relation.
|
|
284
284
|
*/
|
|
285
285
|
export declare function addAutoManyToManyRelations(tableNames: Iterable<string>, foreignKeys: ForeignKeyEntry[], pkByTable: Map<string, string[]>, columnNamesByTable: Map<string, string[]>, relationsByTable: Map<string, Record<string, RelationDef>>, columnFieldsByTable?: Map<string, Set<string>>, unknownTypedFieldsByTable?: Map<string, Set<string>>, uniqueIndexColsByTable?: Map<string, string[][]>): void;
|
|
286
|
+
/** Everything the catalog relation derivation reads. See {@link deriveCatalogRelations}. */
|
|
287
|
+
export interface CatalogRelationInputs {
|
|
288
|
+
/** The introspected table set, post include/exclude filtering. */
|
|
289
|
+
tableNames: string[];
|
|
290
|
+
/** FK rows already grouped per constraint (one entry per declared constraint). */
|
|
291
|
+
foreignKeys: ForeignKeyEntry[];
|
|
292
|
+
/** Primary-key columns per table. */
|
|
293
|
+
pkByTable: Map<string, string[]>;
|
|
294
|
+
/** Column metadata per table (only name/field/tsType/pgType are read). */
|
|
295
|
+
columnsByTable: Map<string, Pick<ColumnMetadata, 'name' | 'field' | 'tsType' | 'pgType'>[]>;
|
|
296
|
+
/** UNIQUE-constraint column sets per table. */
|
|
297
|
+
uniqueByTable: Map<string, string[][]>;
|
|
298
|
+
/** Indexes per table, `definition` must be the raw `pg_indexes.indexdef`. */
|
|
299
|
+
indexesByTable: Map<string, IndexMetadata[]>;
|
|
300
|
+
/** Enum types in the schema, so an enum column is not mistaken for a json shadow. */
|
|
301
|
+
enums: Record<string, string[]>;
|
|
302
|
+
/** Referential actions keyed "<table>::<constraint>". Optional. */
|
|
303
|
+
fkActions?: Map<string, {
|
|
304
|
+
onDelete: ReferentialAction;
|
|
305
|
+
onUpdate: ReferentialAction;
|
|
306
|
+
}>;
|
|
307
|
+
/** Opt out of the unique-FK → `hasOne` flip (F2), see {@link IntrospectOptions.legacyToManyUniques}. */
|
|
308
|
+
legacyToManyUniques?: boolean;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* One-stop relation derivation for every surface that reads a live PostgreSQL
|
|
312
|
+
* CATALOG: `turbine generate` (via {@link introspectPostgresCatalog}) and the
|
|
313
|
+
* MCP server, which introspects for itself because it cannot assume generated
|
|
314
|
+
* metadata exists.
|
|
315
|
+
*
|
|
316
|
+
* THE REASON THIS IS ONE FUNCTION and not two call sites: the pipeline it drives
|
|
317
|
+
* is `buildRelationsFromForeignKeys` (five parameters, two optional) plus
|
|
318
|
+
* `addAutoManyToManyRelations` (eight parameters, three optional), and every
|
|
319
|
+
* optional one CHANGES THE ANSWER while omitting it stays silently type-correct.
|
|
320
|
+
* Hand-mirroring them drifted exactly that way: MCP passed four arguments and so
|
|
321
|
+
* never received `uniqueSetsByTable`, which meant a UNIQUE foreign key produced
|
|
322
|
+
* `users.profile` (hasOne) under `turbine generate` and `users.profiles`
|
|
323
|
+
* (hasMany) under `turbine mcp`, against the same database. An MCP client
|
|
324
|
+
* following its own schema tool then queried `with: { profiles: true }` and got
|
|
325
|
+
* `TURBINE_E005 Unknown relation`. MCP also omitted `uniqueIndexColsByTable`,
|
|
326
|
+
* losing every auto-m2m relation through a Prisma-style PK-less junction. Adding
|
|
327
|
+
* an argument here now reaches both surfaces or neither.
|
|
328
|
+
*
|
|
329
|
+
* The engine introspectors (SQLite / MySQL / MSSQL) keep their own
|
|
330
|
+
* {@link deriveEngineRelations} because they deliberately do NOT do the
|
|
331
|
+
* unique-FK → `hasOne` flip.
|
|
332
|
+
*/
|
|
333
|
+
export declare function deriveCatalogRelations(inputs: CatalogRelationInputs): Map<string, Record<string, RelationDef>>;
|
|
286
334
|
/**
|
|
287
335
|
* One-stop relation derivation for the engine introspectors (SQLite / MySQL /
|
|
288
336
|
* MSSQL): filters the FK list to the introspected table set, seeds the
|