botversion-sdk 2.3.0 → 2.3.1
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/package.json +1 -1
- package/undo/index.js +1 -1
- package/undo/lib/capture-mysql.js +0 -24
- package/undo/lib/capture-prisma.js +23 -170
- package/undo/lib/capture-sql.js +0 -24
- package/undo/lib/gate.js +0 -26
- package/undo/lib/hooks/prisma.js +21 -7
- package/undo/lib/loader-watch.js +0 -5
- package/undo/lib/log.js +7 -6
- package/undo/lib/prisma-structure.js +12 -29
- package/undo/lib/scanner.js +0 -23
- package/undo/lib/schema-reader.js +0 -13
- package/undo/lib/snapshot-store.js +13 -20
- package/undo/lib/tagger.js +9 -0
- package/undo/lib/undo-prisma.js +0 -9
- package/undo/lib/undo-route.js +12 -23
package/package.json
CHANGED
package/undo/index.js
CHANGED
|
@@ -50,7 +50,7 @@ function init(options) {
|
|
|
50
50
|
// 2. Look at the host project.
|
|
51
51
|
const scan = scanner.scan();
|
|
52
52
|
|
|
53
|
-
// 3. Database hooks. Only the ones that match what the host has
|
|
53
|
+
// 3. Database hooks. Only the ones that match what the host has..
|
|
54
54
|
// (Final SDK: more hooks are added to this same list.)
|
|
55
55
|
const pgCopies = pgHook.attach(scan.hostRoot);
|
|
56
56
|
const mysql2Copies = mysql2Hook.attach(scan.hostRoot);
|
|
@@ -602,13 +602,6 @@ async function runCapture(client, run, stmt, ref, kind, st, execHost) {
|
|
|
602
602
|
let phase = "before"; // "before" = the write has not run yet, so it can still be blocked
|
|
603
603
|
let failedWhy = null;
|
|
604
604
|
const notSaved = (why) => {
|
|
605
|
-
log.debug(
|
|
606
|
-
"mysql notSaved:",
|
|
607
|
-
"table=" + name,
|
|
608
|
-
"phase=" + phase,
|
|
609
|
-
"reason=" + why,
|
|
610
|
-
"confirmed=" + st.confirmed,
|
|
611
|
-
);
|
|
612
605
|
if (phase === "before") failedWhy = failedWhy || why;
|
|
613
606
|
if (phase !== "before" || st.confirmed) st.unsaved = true; // the write runs (or already ran) without a saved snapshot
|
|
614
607
|
};
|
|
@@ -640,17 +633,6 @@ async function runCapture(client, run, stmt, ref, kind, st, execHost) {
|
|
|
640
633
|
else plan = planInsert(stmt, pk);
|
|
641
634
|
}
|
|
642
635
|
} catch (e) {
|
|
643
|
-
// TEMP DEBUG: real cause behind "capture error"
|
|
644
|
-
log.debug(
|
|
645
|
-
"mysql capture threw:",
|
|
646
|
-
"table=" + name,
|
|
647
|
-
"kind=" + kind,
|
|
648
|
-
"code=" + ((e && e.code) || "none"),
|
|
649
|
-
"name=" + (e && e.name),
|
|
650
|
-
!(e && e.code)
|
|
651
|
-
? "msg=" + String((e && e.message) || "").slice(0, 150)
|
|
652
|
-
: "",
|
|
653
|
-
);
|
|
654
636
|
notSaved(
|
|
655
637
|
e instanceof Skip
|
|
656
638
|
? e.message
|
|
@@ -811,12 +793,6 @@ function maybeCapture(client, args, original, queryOriginal) {
|
|
|
811
793
|
|
|
812
794
|
// A write that cannot be saved: blocked until the user confirms (or runs unsaved if they already did).
|
|
813
795
|
const refuse = (table, why) => {
|
|
814
|
-
log.debug(
|
|
815
|
-
"mysql refuse (statement shape):",
|
|
816
|
-
"table=" + table,
|
|
817
|
-
"reason=" + why,
|
|
818
|
-
"confirmed=" + st.confirmed,
|
|
819
|
-
);
|
|
820
796
|
if (st.confirmed) {
|
|
821
797
|
st.unsaved = true; // runs without a saved snapshot, so Undo will not cover it
|
|
822
798
|
return null;
|
|
@@ -121,6 +121,8 @@ const pkWhere = (info, row) => {
|
|
|
121
121
|
|
|
122
122
|
// A top-level create or update whose relation fields only use connect (or disconnect, update only), and whose
|
|
123
123
|
// links the database undoes by itself or that are plain columns on the written row. Anything unclear = false.
|
|
124
|
+
// A top-level create or update whose relation fields only use connect (or disconnect, update only), where the
|
|
125
|
+
// link is a plain column on the written row, or (create only) a hidden join table that cascades away. Anything unclear = false.
|
|
124
126
|
async function nestedConnectOnly(
|
|
125
127
|
base,
|
|
126
128
|
model,
|
|
@@ -161,17 +163,22 @@ async function nestedConnectOnly(
|
|
|
161
163
|
!(operation === "update" && keys[0] === "disconnect")
|
|
162
164
|
)
|
|
163
165
|
return false;
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
166
|
+
// Decided per relation: a hidden join table (or an unknown answer) is safe only for a create whose join rows cascade away.
|
|
167
|
+
const joined = structure.hasHiddenJoin(base, f.relationName);
|
|
168
|
+
if (joined !== false) {
|
|
169
|
+
if (
|
|
170
|
+
joined === true &&
|
|
171
|
+
operation === "create" &&
|
|
172
|
+
structure.hiddenJoinIsCascade(
|
|
173
|
+
base,
|
|
174
|
+
f.relationName,
|
|
175
|
+
tableOf(model),
|
|
176
|
+
tableOf(f.type),
|
|
177
|
+
)
|
|
172
178
|
)
|
|
173
|
-
|
|
174
|
-
|
|
179
|
+
continue;
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
175
182
|
const here = (g.get(f.type) || []).some((l) => l.child === model);
|
|
176
183
|
const there = (g.get(model) || []).some((l) => l.child === f.type);
|
|
177
184
|
if (here && !there) continue; // link is only a column on the written row
|
|
@@ -200,24 +207,6 @@ async function capture({
|
|
|
200
207
|
}) {
|
|
201
208
|
const st = tagger.getCurrent();
|
|
202
209
|
if (!st) return null;
|
|
203
|
-
// TEMP DEBUG: what the Prisma hook handed to capture() (names and types only, never values)
|
|
204
|
-
log.debug(
|
|
205
|
-
"prisma capture called:",
|
|
206
|
-
"model=" + model,
|
|
207
|
-
"op=" + operation,
|
|
208
|
-
"dialect=" + String(dialect) + " (type " + typeof dialect + ")",
|
|
209
|
-
"nested=" + !!nested,
|
|
210
|
-
"internalType=" + typeof internal,
|
|
211
|
-
"internalKeys=" +
|
|
212
|
-
(internal && typeof internal === "object"
|
|
213
|
-
? Object.keys(internal).join("|")
|
|
214
|
-
: "none"),
|
|
215
|
-
"inHostTransaction=" + !!(internal && internal.transaction),
|
|
216
|
-
"baseActiveProvider=" + String(base && base._activeProvider),
|
|
217
|
-
"baseEngineProvider=" +
|
|
218
|
-
String(base && base._engineConfig && base._engineConfig.activeProvider),
|
|
219
|
-
"baseHasDataModel=" + !!(base && base._runtimeDataModel),
|
|
220
|
-
);
|
|
221
210
|
// FIX: take the database type from the live client when the hook did not supply one (a schema file read from disk can be missing or elsewhere).
|
|
222
211
|
if (!dialect) dialect = dialectFromPrismaClient(base);
|
|
223
212
|
// FIX: some Prisma versions carry no key markers or delete rules at runtime; ask the database (once per client) for them.
|
|
@@ -225,9 +214,7 @@ async function capture({
|
|
|
225
214
|
const m0 = modelInfo(base, model);
|
|
226
215
|
const isDelete = operation === "delete" || operation === "deleteMany";
|
|
227
216
|
if ((m0 && m0.pkFields.length === 0) || (isDelete && !getGraph(base))) {
|
|
228
|
-
log.debug("prisma structure load starting:", "model=" + model);
|
|
229
217
|
const loadedOk = await structure.load(base, dialect);
|
|
230
|
-
log.debug("prisma structure load result:", "ok=" + loadedOk);
|
|
231
218
|
}
|
|
232
219
|
}
|
|
233
220
|
// TEMP DEBUG: writes only, to avoid noise. Shows the type finally used, and whether relation rules exist on the live client.
|
|
@@ -241,13 +228,6 @@ async function capture({
|
|
|
241
228
|
rm && Array.isArray(rm.fields)
|
|
242
229
|
? rm.fields.find((f) => f.kind === "object")
|
|
243
230
|
: null;
|
|
244
|
-
log.debug(
|
|
245
|
-
"prisma write seen:",
|
|
246
|
-
"model=" + model,
|
|
247
|
-
"op=" + operation,
|
|
248
|
-
"dialectUsed=" + String(dialect),
|
|
249
|
-
"relationFieldKeys=" + (rel ? Object.keys(rel).join("|") : "none"),
|
|
250
|
-
);
|
|
251
231
|
// Logs the hook's nested flag next to the shape of the call (names only, never values).
|
|
252
232
|
try {
|
|
253
233
|
const d1 = args && args.data;
|
|
@@ -266,15 +246,6 @@ async function capture({
|
|
|
266
246
|
)
|
|
267
247
|
.map((f) => f.name)
|
|
268
248
|
: [];
|
|
269
|
-
log.debug(
|
|
270
|
-
"prisma nested flag vs call shape:",
|
|
271
|
-
"model=" + model,
|
|
272
|
-
"op=" + operation,
|
|
273
|
-
"nested=" + !!nested,
|
|
274
|
-
"relationKeysInData=" + (relKeys.join("|") || "none"),
|
|
275
|
-
"hasInclude=" + !!(args && args.include),
|
|
276
|
-
"hasSelect=" + !!(args && args.select),
|
|
277
|
-
);
|
|
278
249
|
} catch (_) {
|
|
279
250
|
/* never break the host */
|
|
280
251
|
}
|
|
@@ -285,30 +256,6 @@ async function capture({
|
|
|
285
256
|
(f) => f.kind === "scalar" || f.kind === "enum",
|
|
286
257
|
);
|
|
287
258
|
const mi = modelInfo(base, model);
|
|
288
|
-
log.debug(
|
|
289
|
-
"prisma model detail:",
|
|
290
|
-
"model=" + model,
|
|
291
|
-
"modelKeys=" + (rm ? Object.keys(rm).join("|") : "none"),
|
|
292
|
-
"primaryKeyProp=" +
|
|
293
|
-
(rm && rm.primaryKey ? JSON.stringify(rm.primaryKey) : "none"),
|
|
294
|
-
"uniqueFields=" +
|
|
295
|
-
(rm && rm.uniqueFields ? JSON.stringify(rm.uniqueFields) : "none"),
|
|
296
|
-
"fieldCount=" + fields.length,
|
|
297
|
-
"scalarCount=" + scalarFields.length,
|
|
298
|
-
"fieldsMarkedIsId=" + fields.filter((f) => f.isId).length,
|
|
299
|
-
"fieldNamedId=" + (idNamed ? Object.keys(idNamed).join("|") : "none"),
|
|
300
|
-
"firstScalarKeys=" +
|
|
301
|
-
(scalarFields[0] ? Object.keys(scalarFields[0]).join("|") : "none"),
|
|
302
|
-
"modelInfoPk=" + (mi ? JSON.stringify(mi.pkFields) : "no model info"),
|
|
303
|
-
);
|
|
304
|
-
// TEMP DEBUG: also reads the delete links now, so the next possible block shows up in this same run
|
|
305
|
-
if (operation === "delete" || operation === "deleteMany") {
|
|
306
|
-
const g0 = getGraph(base);
|
|
307
|
-
log.debug(
|
|
308
|
-
"prisma delete graph:",
|
|
309
|
-
g0 ? "readOk models=" + g0.size : "COULD NOT BE READ",
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
259
|
// TEMP DEBUG (once per process): primary-key detection for EVERY model, so no second model blocks the same action later
|
|
313
260
|
if (!capture.audited) {
|
|
314
261
|
capture.audited = true;
|
|
@@ -319,25 +266,11 @@ async function capture({
|
|
|
319
266
|
const info2 = modelInfo(base, n);
|
|
320
267
|
return !info2 || info2.pkFields.length === 0;
|
|
321
268
|
});
|
|
322
|
-
log.debug(
|
|
323
|
-
"prisma model audit:",
|
|
324
|
-
"models=" + names.length,
|
|
325
|
-
"withoutPrimaryKeyDetected=" + noPk.length,
|
|
326
|
-
"names=" + noPk.slice(0, 60).join("|"),
|
|
327
|
-
"anyFieldMarkedIsId=" +
|
|
328
|
-
names.some((n) => (all[n].fields || []).some((f) => f.isId)),
|
|
329
|
-
);
|
|
330
269
|
}
|
|
331
270
|
// TEMP DEBUG: does the Undo side find the same primary key for this table?
|
|
332
271
|
try {
|
|
333
272
|
const ui = require("./undo-prisma").infoFor(mi ? mi.table : model);
|
|
334
|
-
|
|
335
|
-
"prisma undo-side model info:",
|
|
336
|
-
ui ? "pk=" + JSON.stringify(ui.pkFields) : "NOT FOUND",
|
|
337
|
-
);
|
|
338
|
-
} catch (e) {
|
|
339
|
-
log.debug("prisma undo-side model info failed:", "name=" + (e && e.name));
|
|
340
|
-
}
|
|
273
|
+
} catch (e) {}
|
|
341
274
|
}
|
|
342
275
|
if (
|
|
343
276
|
UNSAVABLE.has(operation) ||
|
|
@@ -352,15 +285,10 @@ async function capture({
|
|
|
352
285
|
);
|
|
353
286
|
return null;
|
|
354
287
|
}
|
|
355
|
-
if (!model || !OPS.has(operation))
|
|
288
|
+
if (!model || !OPS.has(operation)) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
356
291
|
const skip = async (why) => {
|
|
357
|
-
log.debug(
|
|
358
|
-
"prisma skip:",
|
|
359
|
-
"model=" + model,
|
|
360
|
-
"op=" + operation,
|
|
361
|
-
"reason=" + why,
|
|
362
|
-
"confirmed=" + st.confirmed,
|
|
363
|
-
);
|
|
364
292
|
if (st.confirmed) st.unsaved = true; // runs without a saved snapshot, so Undo will not cover it
|
|
365
293
|
if (!st.confirmed) await gate.block(st, `${model} ${operation}`, why);
|
|
366
294
|
return null;
|
|
@@ -398,12 +326,6 @@ async function capture({
|
|
|
398
326
|
Object.keys(d[k]).some((v) => verbs.includes(v)),
|
|
399
327
|
)
|
|
400
328
|
.map((k) => k + ":" + Object.keys(d[k]).join("+"));
|
|
401
|
-
log.debug(
|
|
402
|
-
"prisma nested write shape:",
|
|
403
|
-
"model=" + model,
|
|
404
|
-
"op=" + operation,
|
|
405
|
-
shape.join(" | ") || "none found in data",
|
|
406
|
-
);
|
|
407
329
|
} catch (_) {
|
|
408
330
|
/* never break the host */
|
|
409
331
|
}
|
|
@@ -432,13 +354,6 @@ async function capture({
|
|
|
432
354
|
onOther
|
|
433
355
|
);
|
|
434
356
|
});
|
|
435
|
-
log.debug(
|
|
436
|
-
"prisma nested relation detail:",
|
|
437
|
-
"model=" + model,
|
|
438
|
-
"argKeys=" + Object.keys(args || {}).join("|"),
|
|
439
|
-
out.join(" | ") || "none",
|
|
440
|
-
"graphAvailable=" + !!g,
|
|
441
|
-
);
|
|
442
357
|
} catch (_) {
|
|
443
358
|
/* never break the host */
|
|
444
359
|
}
|
|
@@ -524,47 +439,8 @@ async function capture({
|
|
|
524
439
|
"]"
|
|
525
440
|
);
|
|
526
441
|
});
|
|
527
|
-
log.debug(
|
|
528
|
-
"prisma nested relation detail:",
|
|
529
|
-
"model=" + model,
|
|
530
|
-
"op=" + operation,
|
|
531
|
-
"graphAvailable=" + !!g,
|
|
532
|
-
"argKeys=" + Object.keys(args || {}).join("|"),
|
|
533
|
-
"includeKeys=" +
|
|
534
|
-
(plain(args && args.include)
|
|
535
|
-
? Object.keys(args.include).join("|")
|
|
536
|
-
: "none"),
|
|
537
|
-
"selectKeys=" +
|
|
538
|
-
(plain(args && args.select)
|
|
539
|
-
? Object.keys(args.select).join("|")
|
|
540
|
-
: "none"),
|
|
541
|
-
);
|
|
542
442
|
lines.forEach((l) => log.debug("prisma nested relation field:", l));
|
|
543
|
-
|
|
544
|
-
log.debug(
|
|
545
|
-
"prisma nested hook inputs:",
|
|
546
|
-
"dataPathIsArray=" + Array.isArray(internal && internal.dataPath),
|
|
547
|
-
"dataPathLength=" +
|
|
548
|
-
(Array.isArray(internal && internal.dataPath)
|
|
549
|
-
? internal.dataPath.length
|
|
550
|
-
: "n/a"),
|
|
551
|
-
"dataPath=" +
|
|
552
|
-
(Array.isArray(internal && internal.dataPath)
|
|
553
|
-
? internal.dataPath.join(">")
|
|
554
|
-
: "n/a"),
|
|
555
|
-
"action=" + String(internal && internal.action),
|
|
556
|
-
"clientMethod=" + String(internal && internal.clientMethod),
|
|
557
|
-
"internalArgsKeys=" +
|
|
558
|
-
(plain(internal && internal.args)
|
|
559
|
-
? Object.keys(internal.args).join("|")
|
|
560
|
-
: "n/a"),
|
|
561
|
-
);
|
|
562
|
-
} catch (e) {
|
|
563
|
-
log.debug(
|
|
564
|
-
"prisma nested relation detail failed:",
|
|
565
|
-
"name=" + (e && e.name),
|
|
566
|
-
);
|
|
567
|
-
}
|
|
443
|
+
} catch (e) {}
|
|
568
444
|
// Logs the foreign keys of any hidden join table behind each relation field in the write (names and rules only).
|
|
569
445
|
try {
|
|
570
446
|
const relFields = base._runtimeDataModel.models[model].fields.filter(
|
|
@@ -576,21 +452,8 @@ async function capture({
|
|
|
576
452
|
);
|
|
577
453
|
relFields.forEach((f) => {
|
|
578
454
|
const links = structure.linksOfTablesMentioning(base, f.relationName);
|
|
579
|
-
log.debug(
|
|
580
|
-
"prisma nested join table links:",
|
|
581
|
-
"field=" + f.name,
|
|
582
|
-
"relationName=" + f.relationName,
|
|
583
|
-
links.length
|
|
584
|
-
? links.join(" ; ")
|
|
585
|
-
: "no table named like this relation has foreign keys",
|
|
586
|
-
);
|
|
587
455
|
});
|
|
588
|
-
} catch (e) {
|
|
589
|
-
log.debug(
|
|
590
|
-
"prisma nested join table links failed:",
|
|
591
|
-
"name=" + (e && e.name),
|
|
592
|
-
);
|
|
593
|
-
}
|
|
456
|
+
} catch (e) {}
|
|
594
457
|
if (
|
|
595
458
|
!(await nestedConnectOnly(
|
|
596
459
|
base,
|
|
@@ -882,16 +745,6 @@ async function capture({
|
|
|
882
745
|
|
|
883
746
|
return { result: out };
|
|
884
747
|
} catch (e) {
|
|
885
|
-
// TEMP DEBUG: real cause of a capture failure (our own messages only; database messages can contain values)
|
|
886
|
-
log.debug(
|
|
887
|
-
"prisma capture threw:",
|
|
888
|
-
"model=" + model,
|
|
889
|
-
"op=" + operation,
|
|
890
|
-
"name=" + (e && e.name),
|
|
891
|
-
"code=" + ((e && e.code) || "none"),
|
|
892
|
-
e instanceof Skip ? "msg=" + e.message : "",
|
|
893
|
-
"hostError=" + !!hostError,
|
|
894
|
-
);
|
|
895
748
|
if (hostError) throw hostError; // the host's own write failed: the host must see its own error
|
|
896
749
|
return skip(e instanceof Skip ? e.message : "capture error");
|
|
897
750
|
}
|
package/undo/lib/capture-sql.js
CHANGED
|
@@ -479,13 +479,6 @@ async function runCapture(client, rawRun, stmt, values, st, hostCall) {
|
|
|
479
479
|
let phase = "before"; // "before" = the write has not run yet, so it can still be blocked
|
|
480
480
|
let failedWhy = null;
|
|
481
481
|
const notSaved = (why) => {
|
|
482
|
-
log.debug(
|
|
483
|
-
"pg notSaved:",
|
|
484
|
-
"table=" + name,
|
|
485
|
-
"phase=" + phase,
|
|
486
|
-
"reason=" + why,
|
|
487
|
-
"confirmed=" + st.confirmed,
|
|
488
|
-
);
|
|
489
482
|
if (phase === "before") failedWhy = failedWhy || why;
|
|
490
483
|
if (phase !== "before" || st.confirmed) st.unsaved = true; // the write runs (or already ran) without a saved snapshot
|
|
491
484
|
};
|
|
@@ -499,17 +492,6 @@ async function runCapture(client, rawRun, stmt, values, st, hostCall) {
|
|
|
499
492
|
else if (stmt.type === "insert")
|
|
500
493
|
await beforeInsert(rawRun, stmt, inTx, notSaved);
|
|
501
494
|
} catch (e) {
|
|
502
|
-
// TEMP DEBUG: real cause behind "capture error" (message only for non-database errors, since database messages can contain values)
|
|
503
|
-
log.debug(
|
|
504
|
-
"pg capture threw:",
|
|
505
|
-
"table=" + name,
|
|
506
|
-
"type=" + stmt.type,
|
|
507
|
-
"code=" + ((e && e.code) || "none"),
|
|
508
|
-
"name=" + (e && e.name),
|
|
509
|
-
!(e && e.code)
|
|
510
|
-
? "msg=" + String((e && e.message) || "").slice(0, 150)
|
|
511
|
-
: "",
|
|
512
|
-
);
|
|
513
495
|
notSaved("capture error " + ((e && e.code) || "unknown"));
|
|
514
496
|
}
|
|
515
497
|
|
|
@@ -586,12 +568,6 @@ function maybeCapture(client, args, original) {
|
|
|
586
568
|
|
|
587
569
|
// A write that cannot be saved: blocked until the user confirms (or runs unsaved if they already did).
|
|
588
570
|
const refuse = (table, why) => {
|
|
589
|
-
log.debug(
|
|
590
|
-
"pg refuse (statement shape):",
|
|
591
|
-
"table=" + table,
|
|
592
|
-
"reason=" + why,
|
|
593
|
-
"confirmed=" + st.confirmed,
|
|
594
|
-
);
|
|
595
571
|
if (st.confirmed) {
|
|
596
572
|
st.unsaved = true; // runs without a saved snapshot, so Undo will not cover it
|
|
597
573
|
return null;
|
package/undo/lib/gate.js
CHANGED
|
@@ -50,11 +50,6 @@ async function rollbackAction(st) {
|
|
|
50
50
|
(e.code === "unsupported" ||
|
|
51
51
|
e.code === "no-client" ||
|
|
52
52
|
e.code === "not-found");
|
|
53
|
-
log.debug(
|
|
54
|
-
"gate rollback: undoer error",
|
|
55
|
-
(e && e.code) || (e && e.name) || "unknown",
|
|
56
|
-
next ? "(moving to next database)" : "(stopping)",
|
|
57
|
-
);
|
|
58
53
|
if (!next) throw e;
|
|
59
54
|
if (e.code === "unsupported" && !unsupported) unsupported = e;
|
|
60
55
|
if (e.code === "not-found" && !notFound) notFound = e;
|
|
@@ -66,12 +61,6 @@ async function rollbackAction(st) {
|
|
|
66
61
|
|
|
67
62
|
// Answers the browser request itself, then silences the host so it cannot answer a second time.
|
|
68
63
|
function reply(st, info) {
|
|
69
|
-
log.debug(
|
|
70
|
-
"GATE REPLY:",
|
|
71
|
-
"rolledBack=" + info.rolledBack,
|
|
72
|
-
"undone=" + info.undone,
|
|
73
|
-
"headersAlreadySent=" + !!(st.res && st.res.headersSent),
|
|
74
|
-
);
|
|
75
64
|
const res = st.res;
|
|
76
65
|
if (!res || res.headersSent || res.writableEnded) return;
|
|
77
66
|
const body = JSON.stringify({
|
|
@@ -108,14 +97,6 @@ function reply(st, info) {
|
|
|
108
97
|
|
|
109
98
|
// Always throws Blocked (so the host's write does not run).
|
|
110
99
|
async function block(st, where, why) {
|
|
111
|
-
log.debug(
|
|
112
|
-
"GATE BLOCK:",
|
|
113
|
-
"table=" + where,
|
|
114
|
-
"reason=" + why,
|
|
115
|
-
"visual=" + st.visual,
|
|
116
|
-
"confirmed=" + st.confirmed,
|
|
117
|
-
"alreadyBlocked=" + !!st.blocked,
|
|
118
|
-
);
|
|
119
100
|
try {
|
|
120
101
|
if (!st.blocked) {
|
|
121
102
|
const info = {
|
|
@@ -146,13 +127,6 @@ async function block(st, where, why) {
|
|
|
146
127
|
// Only better-sqlite3 writes can be rolled back this way; writes made earlier in other databases are reported as not rolled back.
|
|
147
128
|
// Always throws Blocked (so the host's write does not run).
|
|
148
129
|
function blockSync(st, where, why) {
|
|
149
|
-
log.debug(
|
|
150
|
-
"GATE BLOCK (sync):",
|
|
151
|
-
"table=" + where,
|
|
152
|
-
"reason=" + why,
|
|
153
|
-
"visual=" + st.visual,
|
|
154
|
-
"confirmed=" + st.confirmed,
|
|
155
|
-
);
|
|
156
130
|
try {
|
|
157
131
|
if (!st.blocked) {
|
|
158
132
|
const info = {
|
package/undo/lib/hooks/prisma.js
CHANGED
|
@@ -90,6 +90,24 @@ function hasNestedWrites(node, depth) {
|
|
|
90
90
|
return false;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// True when the write's data sets a relation field of the model (read from the model itself, no word list).
|
|
94
|
+
// If the model cannot be read, falls back to the old word-list check.
|
|
95
|
+
function writesRelationFields(base, model, args) {
|
|
96
|
+
const d = args && typeof args === "object" ? args.data : null;
|
|
97
|
+
if (!d || typeof d !== "object") return false;
|
|
98
|
+
try {
|
|
99
|
+
const models = base._runtimeDataModel && base._runtimeDataModel.models;
|
|
100
|
+
const m = models && models[model];
|
|
101
|
+
if (m && Array.isArray(m.fields) && !Array.isArray(d))
|
|
102
|
+
return m.fields.some(
|
|
103
|
+
(f) => f.kind === "object" && d[f.name] !== undefined,
|
|
104
|
+
);
|
|
105
|
+
} catch (_) {
|
|
106
|
+
/* fall through */
|
|
107
|
+
}
|
|
108
|
+
return hasNestedWrites(d, 0);
|
|
109
|
+
}
|
|
110
|
+
|
|
93
111
|
function isEmptyFilter(where) {
|
|
94
112
|
return (
|
|
95
113
|
where === undefined ||
|
|
@@ -215,12 +233,7 @@ function extendClient(base, options) {
|
|
|
215
233
|
/* observation problems must never affect the host's operation */
|
|
216
234
|
}
|
|
217
235
|
// Tagged writes run inside our own transaction together with their snapshot.
|
|
218
|
-
const nested =
|
|
219
|
-
args &&
|
|
220
|
-
typeof args === "object" &&
|
|
221
|
-
args.data &&
|
|
222
|
-
hasNestedWrites(args.data, 0)
|
|
223
|
-
);
|
|
236
|
+
const nested = writesRelationFields(base, model, args);
|
|
224
237
|
const handled = await guard.runUnhooked(() =>
|
|
225
238
|
capturePrisma.capture({
|
|
226
239
|
base,
|
|
@@ -239,7 +252,8 @@ function extendClient(base, options) {
|
|
|
239
252
|
},
|
|
240
253
|
});
|
|
241
254
|
state.clients++;
|
|
242
|
-
|
|
255
|
+
// hooks every client the host creates, not only the first one
|
|
256
|
+
return extended;
|
|
243
257
|
}
|
|
244
258
|
|
|
245
259
|
// Returns a subclass of Prisma's generated client class that hooks every new instance.
|
package/undo/lib/loader-watch.js
CHANGED
|
@@ -27,11 +27,6 @@ function install() {
|
|
|
27
27
|
// TEMP DEBUG: which module requests the database hooks matched, and who asked for them
|
|
28
28
|
if (!seenLoads.has(request)) {
|
|
29
29
|
seenLoads.add(request);
|
|
30
|
-
log.debug(
|
|
31
|
-
"loader matched:",
|
|
32
|
-
request,
|
|
33
|
-
"from=" + (parent && parent.filename ? parent.filename : "?"),
|
|
34
|
-
);
|
|
35
30
|
}
|
|
36
31
|
const replacement = h.handle(exp, request, parent);
|
|
37
32
|
if (replacement !== undefined) exp = replacement; // a hook may hand back a wrapped version
|
package/undo/lib/log.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// lib/log.js
|
|
2
2
|
"use strict";
|
|
3
|
-
// Tiny logger.
|
|
4
|
-
// types, counts and ids only - NEVER row values or SQL text.
|
|
3
|
+
// Tiny logger. Logs may contain table names, operation types, counts and ids only - NEVER row values or SQL text.
|
|
5
4
|
|
|
6
5
|
const PREFIX = "[botversion-undo-exp]";
|
|
7
6
|
let quiet = false;
|
|
@@ -14,6 +13,8 @@ function info(...args) {
|
|
|
14
13
|
if (quiet) return;
|
|
15
14
|
}
|
|
16
15
|
|
|
16
|
+
function debug(...args) {}
|
|
17
|
+
|
|
17
18
|
function warn(...args) {
|
|
18
19
|
try {
|
|
19
20
|
console.warn(PREFIX, "WARNING:", ...args);
|
|
@@ -22,13 +23,13 @@ function warn(...args) {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
// TEMP
|
|
26
|
-
function
|
|
26
|
+
// TEMP DIAGNOSTIC: always prints, so we can trace why Undo is not offered. Remove after the cause is fixed.
|
|
27
|
+
function diag(...args) {
|
|
27
28
|
try {
|
|
28
|
-
console.log(
|
|
29
|
+
console.log("[bv-diag]", ...args);
|
|
29
30
|
} catch (_) {
|
|
30
31
|
/* logging must never break the host */
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
module.exports = { info, warn, debug, setQuiet, PREFIX };
|
|
35
|
+
module.exports = { info, warn, debug, diag, setQuiet, PREFIX };
|
|
@@ -129,10 +129,6 @@ const byOrd = (a, b) => a.ord - b.ord;
|
|
|
129
129
|
async function read(base, dialect, st) {
|
|
130
130
|
const sql = SQL[dialect];
|
|
131
131
|
if (!sql) {
|
|
132
|
-
log.debug(
|
|
133
|
-
"prisma-structure: reading keys from the database is not available for",
|
|
134
|
-
String(dialect),
|
|
135
|
-
);
|
|
136
132
|
return false; // stays cached: asking again would give the same answer
|
|
137
133
|
}
|
|
138
134
|
try {
|
|
@@ -183,22 +179,9 @@ async function read(base, dialect, st) {
|
|
|
183
179
|
});
|
|
184
180
|
st.badRule = badRule;
|
|
185
181
|
st.ready = true;
|
|
186
|
-
log.debug(
|
|
187
|
-
"prisma-structure loaded:",
|
|
188
|
-
"dialect=" + dialect,
|
|
189
|
-
"tablesWithKeys=" + st.pk.size,
|
|
190
|
-
"foreignKeys=" + st.fks.length,
|
|
191
|
-
"unknownRules=" + badRule,
|
|
192
|
-
);
|
|
193
182
|
return true;
|
|
194
183
|
} catch (e) {
|
|
195
184
|
st.promise = null; // try again on the next write
|
|
196
|
-
log.debug(
|
|
197
|
-
"prisma-structure failed:",
|
|
198
|
-
"name=" + (e && e.name),
|
|
199
|
-
"code=" + ((e && e.code) || "none"),
|
|
200
|
-
"dbCode=" + ((e && e.meta && e.meta.code) || "none"),
|
|
201
|
-
);
|
|
202
185
|
return false;
|
|
203
186
|
}
|
|
204
187
|
}
|
|
@@ -207,11 +190,6 @@ async function read(base, dialect, st) {
|
|
|
207
190
|
function load(base, dialect) {
|
|
208
191
|
try {
|
|
209
192
|
if (!base || typeof base.$queryRawUnsafe !== "function") {
|
|
210
|
-
log.debug(
|
|
211
|
-
"prisma-structure: client unusable:",
|
|
212
|
-
"hasBase=" + !!base,
|
|
213
|
-
"hasQueryRaw=" + !!(base && typeof base.$queryRawUnsafe === "function"),
|
|
214
|
-
);
|
|
215
193
|
return Promise.resolve(false);
|
|
216
194
|
}
|
|
217
195
|
let st = cache.get(base);
|
|
@@ -228,7 +206,6 @@ function load(base, dialect) {
|
|
|
228
206
|
if (!st.promise) st.promise = read(base, dialect, st);
|
|
229
207
|
return st.promise;
|
|
230
208
|
} catch (e) {
|
|
231
|
-
log.debug("prisma-structure: load threw:", "name=" + (e && e.name));
|
|
232
209
|
return Promise.resolve(false);
|
|
233
210
|
}
|
|
234
211
|
}
|
|
@@ -300,12 +277,6 @@ function deleteGraph(base) {
|
|
|
300
277
|
}
|
|
301
278
|
if (!st.graphLogged) {
|
|
302
279
|
st.graphLogged = true;
|
|
303
|
-
log.debug(
|
|
304
|
-
"prisma-structure delete links built:",
|
|
305
|
-
"parents=" + graph.size,
|
|
306
|
-
"links=" + links,
|
|
307
|
-
"skippedNoModel=" + skipped,
|
|
308
|
-
);
|
|
309
280
|
}
|
|
310
281
|
return graph;
|
|
311
282
|
} catch (_) {
|
|
@@ -326,6 +297,17 @@ function tablesMentioning(base, text) {
|
|
|
326
297
|
}
|
|
327
298
|
}
|
|
328
299
|
|
|
300
|
+
// True/false: is this relation stored in a hidden join table ("_" + relation name)? null = cannot tell.
|
|
301
|
+
function hasHiddenJoin(base, relationName) {
|
|
302
|
+
try {
|
|
303
|
+
const st = cache.get(base);
|
|
304
|
+
if (!st || !st.ready || !relationName) return null;
|
|
305
|
+
return st.fks.some((fk) => fk.child === "_" + relationName);
|
|
306
|
+
} catch (_) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
329
311
|
// True when the relation is stored in a hidden join table whose two foreign keys both cascade on delete.
|
|
330
312
|
function hiddenJoinIsCascade(base, relationName, tableA, tableB) {
|
|
331
313
|
try {
|
|
@@ -394,4 +376,5 @@ module.exports = {
|
|
|
394
376
|
tablesMentioning,
|
|
395
377
|
linksOfTablesMentioning,
|
|
396
378
|
hiddenJoinIsCascade,
|
|
379
|
+
hasHiddenJoin,
|
|
397
380
|
};
|
package/undo/lib/scanner.js
CHANGED
|
@@ -197,14 +197,6 @@ function detectPrisma(hostRoot) {
|
|
|
197
197
|
"Prisma's own engine - NOT visible to the pg hook (the Prisma hook covers it at model level)";
|
|
198
198
|
}
|
|
199
199
|
const ds = readPrismaDatasource(hostRoot);
|
|
200
|
-
// TEMP DEBUG: where the schema was looked for and whether a database provider was read
|
|
201
|
-
log.debug(
|
|
202
|
-
"scanner prisma schema:",
|
|
203
|
-
"hostRoot=" + hostRoot,
|
|
204
|
-
ds
|
|
205
|
-
? "provider=" + ds.provider + " file=" + ds.file
|
|
206
|
-
: "no schema file found in the usual places",
|
|
207
|
-
);
|
|
208
200
|
const database = ds
|
|
209
201
|
? {
|
|
210
202
|
name: PROVIDER_LABELS[ds.provider] || ds.provider,
|
|
@@ -354,21 +346,6 @@ function scan() {
|
|
|
354
346
|
|
|
355
347
|
const prisma = detectPrisma(hostRoot);
|
|
356
348
|
const snapshotTable = detectSnapshotDialect(prisma, drivers);
|
|
357
|
-
// TEMP DEBUG: what detection found (names and paths only)
|
|
358
|
-
log.debug(
|
|
359
|
-
"scanner result:",
|
|
360
|
-
"hostRoot=" + hostRoot,
|
|
361
|
-
"prismaFound=" + !!prisma,
|
|
362
|
-
"prismaDatabase=" +
|
|
363
|
-
(prisma && prisma.database
|
|
364
|
-
? prisma.database.provider + " from " + prisma.database.file
|
|
365
|
-
: "NOT FOUND"),
|
|
366
|
-
"snapshotDialect=" +
|
|
367
|
-
(snapshotTable
|
|
368
|
-
? snapshotTable.dialect + " via " + snapshotTable.source
|
|
369
|
-
: "NONE"),
|
|
370
|
-
"drivers=" + drivers.map((d) => d.name).join("|"),
|
|
371
|
-
);
|
|
372
349
|
|
|
373
350
|
return {
|
|
374
351
|
hostRoot,
|
|
@@ -82,12 +82,6 @@ function readDeleteGraph(hostRoot) {
|
|
|
82
82
|
const Prisma = loadPrismaNamespace(hostRoot);
|
|
83
83
|
const dm = Prisma && Prisma.dmmf && Prisma.dmmf.datamodel;
|
|
84
84
|
if (!dm || !Array.isArray(dm.models)) {
|
|
85
|
-
// TEMP DEBUG: why the delete links could not be read
|
|
86
|
-
log.debug(
|
|
87
|
-
"schema-reader delete graph: no data model",
|
|
88
|
-
"hasPrismaNamespace=" + !!Prisma,
|
|
89
|
-
"hasDmmf=" + !!(Prisma && Prisma.dmmf),
|
|
90
|
-
);
|
|
91
85
|
return null;
|
|
92
86
|
}
|
|
93
87
|
const graph = new Map();
|
|
@@ -115,13 +109,6 @@ function readDeleteGraph(hostRoot) {
|
|
|
115
109
|
}
|
|
116
110
|
return graph;
|
|
117
111
|
} catch (e) {
|
|
118
|
-
// TEMP DEBUG: why Prisma's data model could not be loaded
|
|
119
|
-
log.debug(
|
|
120
|
-
"schema-reader delete graph failed:",
|
|
121
|
-
"name=" + (e && e.name),
|
|
122
|
-
"code=" + ((e && e.code) || "none"),
|
|
123
|
-
"msg=" + String((e && e.message) || "").slice(0, 150),
|
|
124
|
-
);
|
|
125
112
|
return null;
|
|
126
113
|
}
|
|
127
114
|
}
|
|
@@ -116,14 +116,16 @@ function isOwnTable(name) {
|
|
|
116
116
|
|
|
117
117
|
// info = { rowCount, primaryKeyColumns }. Tells a hook whether a write may be captured, before it reads anything.
|
|
118
118
|
function checkCapturable(info) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
119
|
+
let r = { ok: true };
|
|
120
|
+
if (!snapshotCrypto.hasKey()) r = { ok: false, reason: "no-encryption-key" };
|
|
121
|
+
else {
|
|
122
|
+
const pk = info && info.primaryKeyColumns;
|
|
123
|
+
if (!Array.isArray(pk) || pk.length === 0)
|
|
124
|
+
r = { ok: false, reason: "no-primary-key" };
|
|
125
|
+
else if (!(info.rowCount <= ROW_LIMIT))
|
|
126
|
+
r = { ok: false, reason: "too-many-rows" };
|
|
127
|
+
}
|
|
128
|
+
return r;
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
// Order number of a write inside one AI action (1, 2, 3...). Undo reverses them last to first.
|
|
@@ -212,21 +214,12 @@ async function saveSnapshot(run, store, entry, opts) {
|
|
|
212
214
|
await run(sqlFor(dialect).insertSql, params);
|
|
213
215
|
return { ok: true };
|
|
214
216
|
});
|
|
215
|
-
if (!result.ok)
|
|
217
|
+
if (!result.ok) {
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
216
220
|
(store.savedIds || (store.savedIds = [])).push(id); // remembered so a visual run can roll back only this request
|
|
217
221
|
return { ok: true, id, seq };
|
|
218
222
|
} catch (e) {
|
|
219
|
-
// TEMP DEBUG: why saving a snapshot failed (message only for non-database errors)
|
|
220
|
-
log.debug(
|
|
221
|
-
"saveSnapshot failed:",
|
|
222
|
-
"table=" + (entry && entry.table),
|
|
223
|
-
"op=" + (entry && entry.operation),
|
|
224
|
-
"code=" + ((e && e.code) || "none"),
|
|
225
|
-
"name=" + (e && e.name),
|
|
226
|
-
!(e && e.code)
|
|
227
|
-
? "msg=" + String((e && e.message) || "").slice(0, 150)
|
|
228
|
-
: "",
|
|
229
|
-
);
|
|
230
223
|
warnOnce(
|
|
231
224
|
"save-failed",
|
|
232
225
|
`Undo setup problem: a snapshot could not be saved (database error code ${(e && e.code) || "unknown"}). Check the snapshot table and that the login can INSERT, SELECT, UPDATE and DELETE on it.`,
|
package/undo/lib/tagger.js
CHANGED
|
@@ -69,6 +69,15 @@ function buildStore(req, res) {
|
|
|
69
69
|
if (!headers) return null;
|
|
70
70
|
const raw = headers[headerName];
|
|
71
71
|
const first = Array.isArray(raw) ? raw[0] : raw;
|
|
72
|
+
if (
|
|
73
|
+
!first &&
|
|
74
|
+
Object.keys(headers).some((k) => k.indexOf("x-botversion-") === 0)
|
|
75
|
+
)
|
|
76
|
+
noteIgnored(
|
|
77
|
+
"request has x-botversion-* headers but not the action-id header named '" +
|
|
78
|
+
headerName +
|
|
79
|
+
"'",
|
|
80
|
+
);
|
|
72
81
|
// Only a plain id shape is accepted, so odd or hostile values never reach logs or later storage.
|
|
73
82
|
if (typeof first !== "string" || !/^[A-Za-z0-9_-]{8,64}$/.test(first))
|
|
74
83
|
return null;
|
package/undo/lib/undo-prisma.js
CHANGED
|
@@ -29,14 +29,6 @@ function jsonNull() {
|
|
|
29
29
|
let base = null; // the app's own PrismaClient (not the extended one, so our writes are never captured)
|
|
30
30
|
let dialect = "postgres"; // database type of the app's Prisma client
|
|
31
31
|
function remember(client, d) {
|
|
32
|
-
// TEMP DEBUG: was the Prisma client and database type remembered, and when?
|
|
33
|
-
log.debug(
|
|
34
|
-
"undoPrisma.remember called:",
|
|
35
|
-
"dialectPassed=" + String(d),
|
|
36
|
-
"hasClient=" + !!client,
|
|
37
|
-
"hasTransactionFn=" +
|
|
38
|
-
!!(client && typeof client.$transaction === "function"),
|
|
39
|
-
);
|
|
40
32
|
base = client;
|
|
41
33
|
// FIX: the hook's value first, then what the live client reports, then Postgres (the old default).
|
|
42
34
|
dialect = d || dialectFromPrismaClient(client) || "postgres";
|
|
@@ -377,7 +369,6 @@ async function statusAction(actionId, requesterHash) {
|
|
|
377
369
|
`SELECT requester_hash, status, ${LIVE()} AS live FROM ${TABLE} WHERE action_id = ${PH()}`,
|
|
378
370
|
actionId,
|
|
379
371
|
);
|
|
380
|
-
// Same safety checks as Undo itself: same browser session, still captured, still inside the window.
|
|
381
372
|
return (
|
|
382
373
|
rows.length > 0 &&
|
|
383
374
|
rows.every(
|
package/undo/lib/undo-route.js
CHANGED
|
@@ -20,18 +20,20 @@ const STATUS_PATH = "/botversion-undo-status";
|
|
|
20
20
|
// true only when something saved for this action can still be undone by this browser session.
|
|
21
21
|
// Any problem answers false, so the widget simply does not draw the button.
|
|
22
22
|
async function statusOf(id, hash) {
|
|
23
|
+
const tag = String(id).slice(0, 6);
|
|
23
24
|
try {
|
|
24
25
|
// Ask each database in turn; the first one that has something to undo answers true.
|
|
25
|
-
for (const u of [
|
|
26
|
-
undoPrisma,
|
|
27
|
-
undoPg,
|
|
28
|
-
undoMysql,
|
|
29
|
-
undoMongo,
|
|
30
|
-
undoSqlite,
|
|
31
|
-
undoMssql,
|
|
26
|
+
for (const [name, u] of [
|
|
27
|
+
["prisma", undoPrisma],
|
|
28
|
+
["pg", undoPg],
|
|
29
|
+
["mysql", undoMysql],
|
|
30
|
+
["mongo", undoMongo],
|
|
31
|
+
["sqlite", undoSqlite],
|
|
32
|
+
["mssql", undoMssql],
|
|
32
33
|
]) {
|
|
33
34
|
try {
|
|
34
|
-
|
|
35
|
+
const ok = await u.statusAction(id, hash);
|
|
36
|
+
if (ok) return true;
|
|
35
37
|
} catch (e) {
|
|
36
38
|
if (!(e instanceof undoPrisma.Refuse && e.code === "no-client"))
|
|
37
39
|
throw e;
|
|
@@ -104,7 +106,8 @@ function handle(req, res, isValidId) {
|
|
|
104
106
|
} catch (_) {
|
|
105
107
|
/* bad JSON */
|
|
106
108
|
}
|
|
107
|
-
|
|
109
|
+
const idOk = typeof id === "string" && isValidId(id);
|
|
110
|
+
if (!idOk) {
|
|
108
111
|
return send(res, 403, { ok: false, message: "Invalid action id." });
|
|
109
112
|
}
|
|
110
113
|
// Try Prisma first; if the saved change is not a Prisma model (or Prisma is not in use), try pg.
|
|
@@ -138,11 +141,6 @@ function handle(req, res, isValidId) {
|
|
|
138
141
|
(e.code === "unsupported" ||
|
|
139
142
|
e.code === "no-client" ||
|
|
140
143
|
e.code === "not-found");
|
|
141
|
-
log.debug(
|
|
142
|
-
"undo route: undoer said",
|
|
143
|
-
(e && e.code) || (e && e.name) || "unknown",
|
|
144
|
-
next ? "(trying next database)" : "(stopping)",
|
|
145
|
-
);
|
|
146
144
|
if (!next) throw e;
|
|
147
145
|
if (e.code === "unsupported" && !unsupported) unsupported = e;
|
|
148
146
|
if (e.code === "not-found" && !notFound) notFound = e;
|
|
@@ -161,15 +159,6 @@ function handle(req, res, isValidId) {
|
|
|
161
159
|
NOTE,
|
|
162
160
|
});
|
|
163
161
|
} catch (e) {
|
|
164
|
-
log.debug(
|
|
165
|
-
"undo route FINAL error:",
|
|
166
|
-
e instanceof undoPrisma.Refuse
|
|
167
|
-
? "refuse code=" + e.code
|
|
168
|
-
: "unexpected " +
|
|
169
|
-
(e && e.name) +
|
|
170
|
-
" code=" +
|
|
171
|
-
((e && e.code) || "none"),
|
|
172
|
-
);
|
|
173
162
|
if (e instanceof undoPrisma.Refuse) {
|
|
174
163
|
const status =
|
|
175
164
|
e.code === "forbidden"
|