@marktiderman/genesis-forms 0.4.1 → 0.5.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 +89 -5
- package/dist/destinations.cjs +92 -4
- package/dist/destinations.cjs.map +1 -1
- package/dist/destinations.d.cts +1 -1
- package/dist/destinations.d.ts +1 -1
- package/dist/destinations.js +92 -4
- package/dist/destinations.js.map +1 -1
- package/dist/{engine-Dz9J5ej2.d.ts → engine-BDq0XJ21.d.cts} +25 -2
- package/dist/{engine-BkNuzlna.d.cts → engine-Qzp6bX6O.d.ts} +25 -2
- package/dist/index.cjs +383 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +121 -76
- package/dist/index.d.ts +121 -76
- package/dist/index.js +360 -60
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +387 -62
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +39 -5
- package/dist/server.d.ts +39 -5
- package/dist/server.js +385 -63
- package/dist/server.js.map +1 -1
- package/dist/testing.cjs +386 -61
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +4 -4
- package/dist/testing.d.ts +4 -4
- package/dist/testing.js +386 -61
- package/dist/testing.js.map +1 -1
- package/dist/{types-BBKBQ8sp.d.cts → types-BhYUbLJ-.d.cts} +21 -1
- package/dist/{types-BBKBQ8sp.d.ts → types-BhYUbLJ-.d.ts} +21 -1
- package/dist/validate-Bs1lyoOl.d.ts +131 -0
- package/dist/validate-BsZ0YTQS.d.cts +131 -0
- package/package.json +1 -1
- package/dist/validate-BXXqi0fq.d.cts +0 -30
- package/dist/validate-BjNwF1UE.d.ts +0 -30
package/dist/testing.js
CHANGED
|
@@ -352,8 +352,7 @@ var round = (n, decimals) => {
|
|
|
352
352
|
const r = Number(n.toFixed(decimals));
|
|
353
353
|
return Object.is(r, -0) ? 0 : r;
|
|
354
354
|
};
|
|
355
|
-
function
|
|
356
|
-
const { format, decimals } = calculatedProps(field);
|
|
355
|
+
function valueAs(result, { format, decimals }) {
|
|
357
356
|
if (result === null || Array.isArray(result)) return void 0;
|
|
358
357
|
if (format === "number" || format === "percent") {
|
|
359
358
|
const n = formulaNumber(result);
|
|
@@ -380,19 +379,6 @@ var extract = (value, column) => column !== void 0 ? value.flatMap((row) => {
|
|
|
380
379
|
const cell = row[column];
|
|
381
380
|
return typeof cell === "string" || typeof cell === "number" ? [cell] : [];
|
|
382
381
|
}) : value.flatMap((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? [v] : []);
|
|
383
|
-
var extracted = /* @__PURE__ */ new WeakMap();
|
|
384
|
-
function cachedList(value, column) {
|
|
385
|
-
let byColumn = extracted.get(value);
|
|
386
|
-
if (!byColumn) extracted.set(value, byColumn = /* @__PURE__ */ new Map());
|
|
387
|
-
const key = column ?? "";
|
|
388
|
-
let list = byColumn.get(key);
|
|
389
|
-
if (!list) byColumn.set(key, list = ownFormulaList(extract(value, column)));
|
|
390
|
-
return list;
|
|
391
|
-
}
|
|
392
|
-
function readAnswer(value, column) {
|
|
393
|
-
if (Array.isArray(value)) return cachedList(value, column);
|
|
394
|
-
return column !== void 0 ? null : formulaInput(value);
|
|
395
|
-
}
|
|
396
382
|
function formulaInput(value, column) {
|
|
397
383
|
if (value === void 0 || value === null) return null;
|
|
398
384
|
if (Array.isArray(value) && value.length > MAX_FORMULA_ITEMS) return null;
|
|
@@ -407,9 +393,28 @@ function refNodes(node, out = []) {
|
|
|
407
393
|
else if (node.kind === "call") for (const a of node.args) refNodes(a, out);
|
|
408
394
|
return out;
|
|
409
395
|
}
|
|
396
|
+
function formulaSize(node) {
|
|
397
|
+
if (node.kind === "unary") return 1 + formulaSize(node.arg);
|
|
398
|
+
if (node.kind === "binary") return 1 + formulaSize(node.left) + formulaSize(node.right);
|
|
399
|
+
if (node.kind === "if") return 1 + formulaSize(node.test) + formulaSize(node.then) + formulaSize(node.else);
|
|
400
|
+
if (node.kind === "call") return node.args.reduce((n, a) => n + formulaSize(a), 1);
|
|
401
|
+
return 1;
|
|
402
|
+
}
|
|
403
|
+
var ReplayWorkExceeded = class extends Error {
|
|
404
|
+
constructor(limit) {
|
|
405
|
+
super(`Replaying the response would take more than ${limit} steps of work.`);
|
|
406
|
+
this.limit = limit;
|
|
407
|
+
}
|
|
408
|
+
limit;
|
|
409
|
+
};
|
|
410
|
+
function spend(budget, amount) {
|
|
411
|
+
if (!budget) return;
|
|
412
|
+
budget.used += amount;
|
|
413
|
+
if (budget.used > budget.max) throw new ReplayWorkExceeded(budget.max);
|
|
414
|
+
}
|
|
410
415
|
var UNREAD = /* @__PURE__ */ Symbol("unread");
|
|
411
416
|
var OVER = /* @__PURE__ */ Symbol("over");
|
|
412
|
-
function calculator(definition) {
|
|
417
|
+
function calculator(definition, work) {
|
|
413
418
|
const compiled = [];
|
|
414
419
|
const idByKey = /* @__PURE__ */ new Map();
|
|
415
420
|
for (const block of definition.blocks) {
|
|
@@ -418,7 +423,8 @@ function calculator(definition) {
|
|
|
418
423
|
}
|
|
419
424
|
for (const block of definition.blocks) {
|
|
420
425
|
if (block.kind !== "field" || !isCalculated(block)) continue;
|
|
421
|
-
const
|
|
426
|
+
const props = calculatedProps(block);
|
|
427
|
+
const parsed = parseFormula(props.formula);
|
|
422
428
|
const reads = [];
|
|
423
429
|
const slots = /* @__PURE__ */ new Map();
|
|
424
430
|
const byKey = /* @__PURE__ */ new Map();
|
|
@@ -432,33 +438,65 @@ function calculator(definition) {
|
|
|
432
438
|
}
|
|
433
439
|
slots.set(ref, slot);
|
|
434
440
|
}
|
|
435
|
-
compiled.push({ field: block, ast: parsed.ok ? parsed.ast : null, reads, slots, last: null });
|
|
441
|
+
compiled.push({ field: block, props, ast: parsed.ok ? parsed.ast : null, size: parsed.ok ? formulaSize(parsed.ast) : 0, reads, slots, last: null });
|
|
436
442
|
}
|
|
443
|
+
const extracted = /* @__PURE__ */ new WeakMap();
|
|
444
|
+
const cachedList = (value, column) => {
|
|
445
|
+
let byColumn = extracted.get(value);
|
|
446
|
+
if (!byColumn) extracted.set(value, byColumn = /* @__PURE__ */ new Map());
|
|
447
|
+
const key = column ?? "";
|
|
448
|
+
let list = byColumn.get(key);
|
|
449
|
+
if (!list) {
|
|
450
|
+
byColumn.set(key, list = ownFormulaList(extract(value, column)));
|
|
451
|
+
}
|
|
452
|
+
return list;
|
|
453
|
+
};
|
|
454
|
+
const readAnswer = (value, column) => {
|
|
455
|
+
if (Array.isArray(value)) return cachedList(value, column);
|
|
456
|
+
return column !== void 0 ? null : formulaInput(value);
|
|
457
|
+
};
|
|
458
|
+
const forget = (value) => {
|
|
459
|
+
if (!value || typeof value !== "object") return;
|
|
460
|
+
if (Array.isArray(value)) extracted.delete(value);
|
|
461
|
+
for (const entry of compiled) if (entry.last?.inputs.includes(value)) entry.last = null;
|
|
462
|
+
};
|
|
437
463
|
const withCalculated = (values, path) => {
|
|
438
464
|
if (!compiled.length) return values;
|
|
439
465
|
const visible = new Set(path);
|
|
440
466
|
let next = values;
|
|
467
|
+
let copied = false;
|
|
441
468
|
let budget = MAX_SETTLE_ITEMS;
|
|
442
469
|
const within = /* @__PURE__ */ new Map();
|
|
443
|
-
const input = ({ id
|
|
470
|
+
const input = ({ id }) => {
|
|
444
471
|
if (id === void 0 || !visible.has(id) || !Object.prototype.hasOwnProperty.call(next, id)) return UNREAD;
|
|
445
472
|
const value = next[id];
|
|
446
473
|
if (!Array.isArray(value)) return value;
|
|
447
|
-
let ok = within.get(
|
|
474
|
+
let ok = within.get(id);
|
|
448
475
|
if (ok === void 0) {
|
|
449
476
|
ok = value.length <= MAX_FORMULA_ITEMS && value.length <= budget;
|
|
450
477
|
if (ok) budget -= value.length;
|
|
451
|
-
within.set(
|
|
478
|
+
within.set(id, ok);
|
|
452
479
|
}
|
|
453
480
|
return ok ? value : OVER;
|
|
454
481
|
};
|
|
455
482
|
for (const entry of compiled) {
|
|
456
483
|
const { field, ast } = entry;
|
|
457
|
-
const
|
|
484
|
+
const { reads } = entry;
|
|
485
|
+
spend(work, 1);
|
|
486
|
+
const inputs = new Array(reads.length);
|
|
487
|
+
let over = false;
|
|
488
|
+
let same2 = entry.last !== null;
|
|
489
|
+
for (let i = 0; i < reads.length; i++) {
|
|
490
|
+
const v = input(reads[i]);
|
|
491
|
+
inputs[i] = v;
|
|
492
|
+
if (v === OVER) over = true;
|
|
493
|
+
if (same2 && entry.last.inputs[i] !== v) same2 = false;
|
|
494
|
+
}
|
|
458
495
|
let value;
|
|
459
|
-
if (
|
|
496
|
+
if (same2) {
|
|
460
497
|
value = entry.last.value;
|
|
461
498
|
} else {
|
|
499
|
+
spend(work, entry.size);
|
|
462
500
|
const resolved = new Array(inputs.length);
|
|
463
501
|
const read = (ref) => {
|
|
464
502
|
const slot = entry.slots.get(ref);
|
|
@@ -466,19 +504,25 @@ function calculator(definition) {
|
|
|
466
504
|
let v = resolved[slot];
|
|
467
505
|
if (v === void 0) {
|
|
468
506
|
const raw = inputs[slot];
|
|
469
|
-
v = resolved[slot] = raw === UNREAD ? null : readAnswer(raw,
|
|
507
|
+
v = resolved[slot] = raw === UNREAD ? null : readAnswer(raw, reads[slot].column);
|
|
470
508
|
}
|
|
471
509
|
return v;
|
|
472
510
|
};
|
|
473
|
-
value = ast && !
|
|
511
|
+
value = ast && !over ? valueAs(evaluateFormula(ast, read), entry.props) : void 0;
|
|
474
512
|
entry.last = { inputs, value };
|
|
475
513
|
}
|
|
476
514
|
const current = Object.prototype.hasOwnProperty.call(next, field.id) ? next[field.id] : void 0;
|
|
477
|
-
if (current !== value)
|
|
515
|
+
if (current !== value) {
|
|
516
|
+
if (!copied) {
|
|
517
|
+
next = { ...next };
|
|
518
|
+
copied = true;
|
|
519
|
+
}
|
|
520
|
+
next[field.id] = value;
|
|
521
|
+
}
|
|
478
522
|
}
|
|
479
523
|
return next;
|
|
480
524
|
};
|
|
481
|
-
return { active: compiled.length > 0, withCalculated };
|
|
525
|
+
return { active: compiled.length > 0, ids: new Set(compiled.map((c) => c.field.id)), withCalculated, forget };
|
|
482
526
|
}
|
|
483
527
|
var numberOps = ["eq", "neq", "lt", "lte", "gt", "gte", "in", "answered", "empty"];
|
|
484
528
|
var calculatedFieldType = {
|
|
@@ -675,6 +719,80 @@ var structuredFieldTypes = [
|
|
|
675
719
|
}
|
|
676
720
|
];
|
|
677
721
|
|
|
722
|
+
// src/photo.ts
|
|
723
|
+
var PHOTO_TYPE = "photo";
|
|
724
|
+
var PHOTO_FORMATS = {
|
|
725
|
+
jpeg: ["image/jpeg"],
|
|
726
|
+
png: ["image/png"],
|
|
727
|
+
heic: ["image/heic", "image/heif"],
|
|
728
|
+
webp: ["image/webp"]
|
|
729
|
+
};
|
|
730
|
+
var FORMAT_NAMES = { jpeg: "JPEG", png: "PNG", heic: "HEIC", webp: "WebP" };
|
|
731
|
+
var ALL_FORMATS = Object.keys(PHOTO_FORMATS);
|
|
732
|
+
var DEFAULT_PHOTO_FILES = 3;
|
|
733
|
+
var MAX_PHOTO_FILES = 5;
|
|
734
|
+
var DEFAULT_PHOTO_SIZE_MB = 10;
|
|
735
|
+
var MAX_PHOTO_SIZE_MB = 25;
|
|
736
|
+
var MAX_PHOTO_ID_LENGTH = 200;
|
|
737
|
+
var MAX_PHOTO_NAME_LENGTH = 255;
|
|
738
|
+
var isPhotoFormat = (v) => typeof v === "string" && Object.prototype.hasOwnProperty.call(PHOTO_FORMATS, v);
|
|
739
|
+
var wholeIn = (v, min, max) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
|
|
740
|
+
function photoLimits(field) {
|
|
741
|
+
const maxFiles = wholeIn(field.props?.maxFiles, 1, MAX_PHOTO_FILES) ? field.props.maxFiles : DEFAULT_PHOTO_FILES;
|
|
742
|
+
const maxSizeMb = wholeIn(field.props?.maxSizeMb, 1, MAX_PHOTO_SIZE_MB) ? field.props.maxSizeMb : DEFAULT_PHOTO_SIZE_MB;
|
|
743
|
+
const raw = field.props?.formats;
|
|
744
|
+
const picked = Array.isArray(raw) ? ALL_FORMATS.filter((f) => raw.includes(f)) : [];
|
|
745
|
+
const formats = picked.length ? picked : [...ALL_FORMATS];
|
|
746
|
+
return {
|
|
747
|
+
maxFiles,
|
|
748
|
+
maxSizeMb,
|
|
749
|
+
maxBytes: maxSizeMb * 1024 * 1024,
|
|
750
|
+
formats,
|
|
751
|
+
mimeTypes: formats.flatMap((f) => PHOTO_FORMATS[f])
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function isPhotoRef(v) {
|
|
755
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return false;
|
|
756
|
+
const r = v;
|
|
757
|
+
return typeof r.id === "string" && r.id.length > 0 && r.id.length <= MAX_PHOTO_ID_LENGTH && typeof r.name === "string" && r.name.length <= MAX_PHOTO_NAME_LENGTH && typeof r.type === "string" && typeof r.size === "number" && Number.isInteger(r.size) && r.size > 0;
|
|
758
|
+
}
|
|
759
|
+
function photoRefs(value) {
|
|
760
|
+
if (!Array.isArray(value)) return [];
|
|
761
|
+
const refs = [];
|
|
762
|
+
for (const r of value) if (isPhotoRef(r)) refs.push({ id: r.id, name: r.name, type: r.type, size: r.size });
|
|
763
|
+
return refs;
|
|
764
|
+
}
|
|
765
|
+
var plural2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
766
|
+
function photoFormatList(formats) {
|
|
767
|
+
const names = formats.map((f) => FORMAT_NAMES[f]);
|
|
768
|
+
return names.length > 1 ? `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}` : names[0] ?? "";
|
|
769
|
+
}
|
|
770
|
+
var photoTypeMessage = (field) => `Use a ${photoFormatList(photoLimits(field).formats)} photo.`;
|
|
771
|
+
var photoSizeMessage = (field) => `Each photo must be ${photoLimits(field).maxSizeMb} MB or smaller.`;
|
|
772
|
+
var photoCountMessage = (field) => `Add at most ${plural2(photoLimits(field).maxFiles, "photo")}.`;
|
|
773
|
+
var photoFieldType = {
|
|
774
|
+
type: PHOTO_TYPE,
|
|
775
|
+
label: "Photo",
|
|
776
|
+
category: "media",
|
|
777
|
+
value: { json: true },
|
|
778
|
+
operators: ["answered", "empty"],
|
|
779
|
+
a11y: { semantics: "group" },
|
|
780
|
+
isEmpty: (value) => value === void 0 || value === null || Array.isArray(value) && value.length === 0,
|
|
781
|
+
validate: (value, field) => {
|
|
782
|
+
if (!Array.isArray(value) || !value.every(isPhotoRef)) return "Add a photo.";
|
|
783
|
+
const limits = photoLimits(field);
|
|
784
|
+
if (value.length > limits.maxFiles) return photoCountMessage(field);
|
|
785
|
+
const refs = value;
|
|
786
|
+
if (new Set(refs.map((r) => r.id)).size !== refs.length) return "Each photo can be added once.";
|
|
787
|
+
if (refs.some((r) => !limits.mimeTypes.includes(r.type.toLowerCase()))) return photoTypeMessage(field);
|
|
788
|
+
if (refs.some((r) => r.size > limits.maxBytes)) return photoSizeMessage(field);
|
|
789
|
+
return null;
|
|
790
|
+
},
|
|
791
|
+
normalize: (value) => Array.isArray(value) && value.every(isPhotoRef) ? photoRefs(value) : value,
|
|
792
|
+
summarize: (value) => photoRefs(value).map((r) => r.name || "Photo").join(", "),
|
|
793
|
+
samples: () => [[{ id: "sample-photo", name: "photo.jpg", type: "image/jpeg", size: 1024 }]]
|
|
794
|
+
};
|
|
795
|
+
|
|
678
796
|
// src/fields.ts
|
|
679
797
|
var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types");
|
|
680
798
|
var globalScope = globalThis;
|
|
@@ -711,6 +829,20 @@ function summarizeValue(value, field) {
|
|
|
711
829
|
if (Array.isArray(value)) return value.join(", ");
|
|
712
830
|
return String(value);
|
|
713
831
|
}
|
|
832
|
+
var OTHER_MAX_LENGTH = 200;
|
|
833
|
+
function otherOption(field) {
|
|
834
|
+
const other = field.props?.other;
|
|
835
|
+
if (other === true) return { label: "Other" };
|
|
836
|
+
if (other && typeof other === "object" && !Array.isArray(other)) {
|
|
837
|
+
const label = typeof other.label === "string" && other.label.trim() ? other.label.trim() : "Other";
|
|
838
|
+
return { label };
|
|
839
|
+
}
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
function isOtherValue(field, value) {
|
|
843
|
+
return otherOption(field) !== null && typeof value === "string" && !choiceOptions(field).some((o) => o.value === value);
|
|
844
|
+
}
|
|
845
|
+
var typedOther = (field, value) => isOtherValue(field, value) && value.trim() !== "" && value.length <= OTHER_MAX_LENGTH;
|
|
714
846
|
function choiceOptions(field) {
|
|
715
847
|
const raw = field.props?.options;
|
|
716
848
|
if (!Array.isArray(raw)) return [];
|
|
@@ -718,6 +850,42 @@ function choiceOptions(field) {
|
|
|
718
850
|
(o) => o && typeof o === "object" && !Array.isArray(o) && typeof o.value === "string" ? [{ value: o.value, label: typeof o.label === "string" ? o.label : void 0 }] : []
|
|
719
851
|
);
|
|
720
852
|
}
|
|
853
|
+
function optionsSource(field) {
|
|
854
|
+
const raw = field.props?.optionsFrom;
|
|
855
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
856
|
+
return raw.source === "host" && typeof raw.key === "string" && raw.key ? { source: "host", key: raw.key } : void 0;
|
|
857
|
+
}
|
|
858
|
+
function hostList(hostOptions, key) {
|
|
859
|
+
if (!hostOptions || !Object.prototype.hasOwnProperty.call(hostOptions, key)) return [];
|
|
860
|
+
const list = hostOptions[key];
|
|
861
|
+
if (!Array.isArray(list)) return [];
|
|
862
|
+
const seen = /* @__PURE__ */ new Set();
|
|
863
|
+
const out = [];
|
|
864
|
+
for (const o of list) {
|
|
865
|
+
if (!o || typeof o !== "object" || Array.isArray(o)) continue;
|
|
866
|
+
const { value, label } = o;
|
|
867
|
+
if (typeof value !== "string" || !value || seen.has(value)) continue;
|
|
868
|
+
seen.add(value);
|
|
869
|
+
out.push(typeof label === "string" ? { value, label } : { value });
|
|
870
|
+
}
|
|
871
|
+
return out;
|
|
872
|
+
}
|
|
873
|
+
function withHostOptions(definition, hostOptions) {
|
|
874
|
+
let changed = false;
|
|
875
|
+
const blocks = definition.blocks.map((block) => {
|
|
876
|
+
if (block.kind !== "field") return block;
|
|
877
|
+
const from = optionsSource(block);
|
|
878
|
+
if (!from) return block;
|
|
879
|
+
const own2 = choiceOptions(block);
|
|
880
|
+
const taken = new Set(own2.map((o) => o.value));
|
|
881
|
+
const added = hostList(hostOptions, from.key).filter((o) => !taken.has(o.value));
|
|
882
|
+
if (!added.length) return block;
|
|
883
|
+
changed = true;
|
|
884
|
+
const options = [...own2, ...added].map((o) => o.label === void 0 ? { value: o.value } : { value: o.value, label: o.label });
|
|
885
|
+
return { ...block, props: { ...block.props, options } };
|
|
886
|
+
});
|
|
887
|
+
return changed ? { ...definition, blocks } : definition;
|
|
888
|
+
}
|
|
721
889
|
function optionLabel(field, value) {
|
|
722
890
|
const option = choiceOptions(field).find((o) => o.value === value);
|
|
723
891
|
return option?.label ?? value;
|
|
@@ -836,7 +1004,7 @@ var builtins = [
|
|
|
836
1004
|
value: { json: true },
|
|
837
1005
|
operators: ["eq", "neq", "in", "answered", "empty"],
|
|
838
1006
|
a11y: { semantics: "group" },
|
|
839
|
-
validate: (value, field) => typeof value === "string" && choiceOptions(field).some((o) => o.value === value) ? null : "Choose one of the options.",
|
|
1007
|
+
validate: (value, field) => typeof value === "string" && (choiceOptions(field).some((o) => o.value === value) || typedOther(field, value)) ? null : "Choose one of the options.",
|
|
840
1008
|
summarize: (value, field) => optionLabel(field, String(value))
|
|
841
1009
|
},
|
|
842
1010
|
{
|
|
@@ -848,7 +1016,8 @@ var builtins = [
|
|
|
848
1016
|
a11y: { semantics: "group" },
|
|
849
1017
|
validate: (value, field) => {
|
|
850
1018
|
const allowed = new Set(choiceOptions(field).map((o) => o.value));
|
|
851
|
-
|
|
1019
|
+
const others = Array.isArray(value) ? value.filter((v) => typeof v === "string" && !allowed.has(v)) : [];
|
|
1020
|
+
return Array.isArray(value) && new Set(value).size === value.length && value.every((v) => typeof v === "string") && (others.length === 0 || others.length === 1 && typedOther(field, others[0])) ? null : "Choose from the options.";
|
|
852
1021
|
},
|
|
853
1022
|
summarize: (value, field) => Array.isArray(value) ? value.map((v) => optionLabel(field, String(v))).join(", ") : String(value)
|
|
854
1023
|
},
|
|
@@ -869,7 +1038,7 @@ var builtins = [
|
|
|
869
1038
|
value: { json: true },
|
|
870
1039
|
operators: ["eq", "neq", "in", "answered", "empty"],
|
|
871
1040
|
a11y: { semantics: "control" },
|
|
872
|
-
validate: (value, field) => typeof value === "string" && choiceOptions(field).some((o) => o.value === value) ? null : "Choose one of the options.",
|
|
1041
|
+
validate: (value, field) => typeof value === "string" && (choiceOptions(field).some((o) => o.value === value) || typedOther(field, value)) ? null : "Choose one of the options.",
|
|
873
1042
|
summarize: (value, field) => optionLabel(field, String(value))
|
|
874
1043
|
},
|
|
875
1044
|
{
|
|
@@ -976,7 +1145,7 @@ function dateLimits(field) {
|
|
|
976
1145
|
if (typeof max === "string" && isIsoDate(max)) out.max = max;
|
|
977
1146
|
return out;
|
|
978
1147
|
}
|
|
979
|
-
for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType]) registerFieldType(manifest);
|
|
1148
|
+
for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType, photoFieldType]) registerFieldType(manifest);
|
|
980
1149
|
|
|
981
1150
|
// src/events.ts
|
|
982
1151
|
function isPlainObject(value) {
|
|
@@ -1135,8 +1304,8 @@ function rulesAllow(def, blockId, scope) {
|
|
|
1135
1304
|
}
|
|
1136
1305
|
function walkPath(def, scope) {
|
|
1137
1306
|
const list = steps(def);
|
|
1138
|
-
|
|
1139
|
-
|
|
1307
|
+
let index;
|
|
1308
|
+
let position;
|
|
1140
1309
|
const jumps = (def.logic ?? []).filter(
|
|
1141
1310
|
(r) => r.effect === "jump"
|
|
1142
1311
|
);
|
|
@@ -1160,6 +1329,7 @@ function walkPath(def, scope) {
|
|
|
1160
1329
|
ending = jump.to.ending;
|
|
1161
1330
|
break;
|
|
1162
1331
|
}
|
|
1332
|
+
index ??= new Map(list.map((b, at) => [b.id, at]));
|
|
1163
1333
|
const target = index.get(jump.to);
|
|
1164
1334
|
if (target !== void 0 && target > i) {
|
|
1165
1335
|
i = nextVisible(target);
|
|
@@ -1169,6 +1339,7 @@ function walkPath(def, scope) {
|
|
|
1169
1339
|
i = nextVisible(i + 1);
|
|
1170
1340
|
}
|
|
1171
1341
|
const scopeAt = (blockId, inclusive = false) => {
|
|
1342
|
+
position ??= new Map(def.blocks.map((b, i2) => [b.id, i2]));
|
|
1172
1343
|
const at = position.get(blockId) ?? -1;
|
|
1173
1344
|
const values = /* @__PURE__ */ Object.create(null);
|
|
1174
1345
|
for (const id of path) {
|
|
@@ -1231,10 +1402,20 @@ function validateField(field, value, options = {}) {
|
|
|
1231
1402
|
}
|
|
1232
1403
|
|
|
1233
1404
|
// src/engine.ts
|
|
1405
|
+
function readsAny(node, ids) {
|
|
1406
|
+
if (Array.isArray(node)) return node.some((n) => readsAny(n, ids));
|
|
1407
|
+
if (!node || typeof node !== "object") return false;
|
|
1408
|
+
for (const [k, v] of Object.entries(node)) {
|
|
1409
|
+
if (k === "field" && typeof v === "string" && ids.has(v)) return true;
|
|
1410
|
+
if (typeof v === "object" && readsAny(v, ids)) return true;
|
|
1411
|
+
}
|
|
1412
|
+
return false;
|
|
1413
|
+
}
|
|
1234
1414
|
function createForm(definition, options = {}) {
|
|
1235
1415
|
return build(definition, options).engine;
|
|
1236
1416
|
}
|
|
1237
|
-
function build(
|
|
1417
|
+
function build(given, options, replaying = false, budget) {
|
|
1418
|
+
const definition = withHostOptions(given, options.hostOptions);
|
|
1238
1419
|
const fields = fieldMap(definition);
|
|
1239
1420
|
const byKey = new Map([...fields.values()].map((f) => [f.key, f]));
|
|
1240
1421
|
const blocks = new Map(definition.blocks.map((b) => [b.id, b]));
|
|
@@ -1252,15 +1433,24 @@ function build(definition, options, replaying = false) {
|
|
|
1252
1433
|
if (!declared.has(name)) initialHidden[name] = value;
|
|
1253
1434
|
}
|
|
1254
1435
|
Object.assign(initialHidden, coerceHidden(definition, options.hidden).values);
|
|
1255
|
-
const calc = calculator(definition);
|
|
1436
|
+
const calc = calculator(definition, budget);
|
|
1437
|
+
const pathOf = (values, hidden) => {
|
|
1438
|
+
spend(budget, definition.blocks.length);
|
|
1439
|
+
return computePath(definition, scopeOf(values, hidden));
|
|
1440
|
+
};
|
|
1441
|
+
const logicReadsCalculated = calc.active && readsAny(definition.logic, calc.ids);
|
|
1256
1442
|
const settle = (values, hidden) => {
|
|
1257
|
-
let { path } =
|
|
1443
|
+
let { path } = pathOf(values, hidden);
|
|
1258
1444
|
if (!calc.active) return { values, path };
|
|
1445
|
+
if (!logicReadsCalculated) return { values: calc.withCalculated(values, path), path };
|
|
1259
1446
|
for (let round2 = 0; round2 < 4; round2++) {
|
|
1260
1447
|
const next = calc.withCalculated(values, path);
|
|
1261
1448
|
if (next === values) break;
|
|
1262
1449
|
values = next;
|
|
1263
|
-
|
|
1450
|
+
const again = pathOf(values, hidden).path;
|
|
1451
|
+
const same2 = again.length === path.length && again.every((id, i) => id === path[i]);
|
|
1452
|
+
path = again;
|
|
1453
|
+
if (same2) break;
|
|
1264
1454
|
}
|
|
1265
1455
|
return { values, path };
|
|
1266
1456
|
};
|
|
@@ -1271,6 +1461,7 @@ function build(definition, options, replaying = false) {
|
|
|
1271
1461
|
path: first.path,
|
|
1272
1462
|
current: first.path[0] ?? null,
|
|
1273
1463
|
history: [],
|
|
1464
|
+
furthest: first.path[0] ?? null,
|
|
1274
1465
|
errors: {},
|
|
1275
1466
|
status: "in_progress",
|
|
1276
1467
|
ending: null,
|
|
@@ -1302,14 +1493,30 @@ function build(definition, options, replaying = false) {
|
|
|
1302
1493
|
return errors;
|
|
1303
1494
|
};
|
|
1304
1495
|
let unsettled = false;
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
unsettled
|
|
1308
|
-
|
|
1309
|
-
|
|
1496
|
+
let formulasStale = false;
|
|
1497
|
+
const settleNow = (needValues = true) => {
|
|
1498
|
+
if (unsettled && calc.active && !logicReadsCalculated && !needValues) {
|
|
1499
|
+
unsettled = false;
|
|
1500
|
+
formulasStale = true;
|
|
1501
|
+
state = { ...state, path: pathOf(state.values, state.hidden).path };
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (unsettled) {
|
|
1505
|
+
unsettled = false;
|
|
1506
|
+
formulasStale = false;
|
|
1507
|
+
const { values, path } = settle(state.values, state.hidden);
|
|
1508
|
+
state = { ...state, values, path };
|
|
1509
|
+
} else if (formulasStale && needValues) {
|
|
1510
|
+
formulasStale = false;
|
|
1511
|
+
state = { ...state, values: calc.withCalculated(state.values, state.path) };
|
|
1512
|
+
}
|
|
1310
1513
|
};
|
|
1311
1514
|
const answer = (id, value, event) => {
|
|
1312
|
-
if (state.status === "completed" ||
|
|
1515
|
+
if (state.status === "completed" || isCalculated(fields.get(id))) return;
|
|
1516
|
+
if (own(state.values, id) === value) {
|
|
1517
|
+
if (value === null || typeof value !== "object") return;
|
|
1518
|
+
calc.forget(value);
|
|
1519
|
+
}
|
|
1313
1520
|
const errors = { ...state.errors };
|
|
1314
1521
|
delete errors[id];
|
|
1315
1522
|
if (batch) {
|
|
@@ -1355,7 +1562,8 @@ function build(definition, options, replaying = false) {
|
|
|
1355
1562
|
emit({ ...state, events });
|
|
1356
1563
|
return true;
|
|
1357
1564
|
}
|
|
1358
|
-
|
|
1565
|
+
const furthest = state.furthest && state.path.indexOf(state.furthest) >= state.path.indexOf(target) ? state.furthest : target;
|
|
1566
|
+
emit({ ...state, current: target, history: [...state.history, state.current], furthest, events });
|
|
1359
1567
|
return true;
|
|
1360
1568
|
},
|
|
1361
1569
|
back() {
|
|
@@ -1375,7 +1583,7 @@ function build(definition, options, replaying = false) {
|
|
|
1375
1583
|
emit({ ...state, errors, current: firstError, events });
|
|
1376
1584
|
return false;
|
|
1377
1585
|
}
|
|
1378
|
-
const { ending } =
|
|
1586
|
+
const { ending } = pathOf(state.values, state.hidden);
|
|
1379
1587
|
emit({ ...state, errors: {}, status: "completed", ending, events });
|
|
1380
1588
|
return true;
|
|
1381
1589
|
},
|
|
@@ -1400,6 +1608,15 @@ function build(definition, options, replaying = false) {
|
|
|
1400
1608
|
settleNow();
|
|
1401
1609
|
batch = wasBatch;
|
|
1402
1610
|
}
|
|
1611
|
+
const resumeAt = options.resumeAt;
|
|
1612
|
+
if (resumeAt && !replaying && state.path.includes(resumeAt)) {
|
|
1613
|
+
for (let i = 0; i < state.path.length && state.current !== resumeAt; i++) {
|
|
1614
|
+
if (!engine.next()) {
|
|
1615
|
+
state = { ...state, errors: {} };
|
|
1616
|
+
break;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1403
1620
|
const apply = (event) => {
|
|
1404
1621
|
if (event.type === "answer") {
|
|
1405
1622
|
if (fields.has(event.field)) {
|
|
@@ -1407,10 +1624,11 @@ function build(definition, options, replaying = false) {
|
|
|
1407
1624
|
answer(event.field, event.value, logged);
|
|
1408
1625
|
}
|
|
1409
1626
|
} else if (event.type === "next") {
|
|
1410
|
-
settleNow();
|
|
1627
|
+
settleNow(false);
|
|
1628
|
+
if (formulasStale && isCalculated(fields.get(state.current ?? ""))) settleNow(true);
|
|
1411
1629
|
engine.next();
|
|
1412
1630
|
} else if (event.type === "back") {
|
|
1413
|
-
settleNow();
|
|
1631
|
+
settleNow(false);
|
|
1414
1632
|
engine.back();
|
|
1415
1633
|
} else if (event.type === "submit") {
|
|
1416
1634
|
settleNow();
|
|
@@ -1425,9 +1643,18 @@ function build(definition, options, replaying = false) {
|
|
|
1425
1643
|
return { engine, apply, finish };
|
|
1426
1644
|
}
|
|
1427
1645
|
function replay(definition, events, options = {}) {
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1646
|
+
return replayWithin(definition, events, options);
|
|
1647
|
+
}
|
|
1648
|
+
function replayWithin(definition, events, options, maxWork) {
|
|
1649
|
+
const budget = maxWork === void 0 ? void 0 : { used: 0, max: maxWork };
|
|
1650
|
+
const { engine, apply, finish } = build(definition, { ...options, initialValues: void 0, clock: () => void 0 }, true, budget);
|
|
1651
|
+
try {
|
|
1652
|
+
for (const event of events) apply(event);
|
|
1653
|
+
finish();
|
|
1654
|
+
} finally {
|
|
1655
|
+
if (budget) budget.used;
|
|
1656
|
+
}
|
|
1657
|
+
if (budget) budget.max = Infinity;
|
|
1431
1658
|
return engine;
|
|
1432
1659
|
}
|
|
1433
1660
|
|
|
@@ -1759,6 +1986,22 @@ function parseForm(input) {
|
|
|
1759
1986
|
}
|
|
1760
1987
|
}
|
|
1761
1988
|
if (type === "consent") optionalText(props.text, `${path}.text`);
|
|
1989
|
+
if (type === PHOTO_TYPE) {
|
|
1990
|
+
if (props.maxFiles !== void 0 && !(isInt(props.maxFiles) && props.maxFiles >= 1 && props.maxFiles <= MAX_PHOTO_FILES)) {
|
|
1991
|
+
issue(`${path}.maxFiles`, `Must be a whole number from 1 to ${MAX_PHOTO_FILES}.`);
|
|
1992
|
+
}
|
|
1993
|
+
if (props.maxSizeMb !== void 0 && !(isInt(props.maxSizeMb) && props.maxSizeMb >= 1 && props.maxSizeMb <= MAX_PHOTO_SIZE_MB)) {
|
|
1994
|
+
issue(`${path}.maxSizeMb`, `Must be a whole number from 1 to ${MAX_PHOTO_SIZE_MB}.`);
|
|
1995
|
+
}
|
|
1996
|
+
if (props.formats !== void 0) {
|
|
1997
|
+
const formats = props.formats;
|
|
1998
|
+
if (!Array.isArray(formats) || formats.length === 0 || !formats.every(isPhotoFormat)) {
|
|
1999
|
+
issue(`${path}.formats`, "Must be a list of one or more of jpeg, png, heic and webp.");
|
|
2000
|
+
} else if (new Set(formats).size !== formats.length) {
|
|
2001
|
+
issue(`${path}.formats`, "Name each format once.");
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
1762
2005
|
};
|
|
1763
2006
|
if (!isObject2(input)) {
|
|
1764
2007
|
issue("", "A form definition must be an object.");
|
|
@@ -1779,6 +2022,7 @@ function parseForm(input) {
|
|
|
1779
2022
|
else {
|
|
1780
2023
|
optionalBoolean(input.settings.anonymous, "settings.anonymous");
|
|
1781
2024
|
optionalBoolean(input.settings.clearHiddenValues, "settings.clearHiddenValues");
|
|
2025
|
+
optionalBoolean(input.settings.saveProgress, "settings.saveProgress");
|
|
1782
2026
|
}
|
|
1783
2027
|
}
|
|
1784
2028
|
if (input.meta !== void 0 && (!isObject2(input.meta) || !isJson(input.meta))) {
|
|
@@ -1819,6 +2063,14 @@ function parseForm(input) {
|
|
|
1819
2063
|
issue(`${path}.type`, `Unknown field type "${String(block.type)}".`);
|
|
1820
2064
|
}
|
|
1821
2065
|
if (block.props !== void 0) {
|
|
2066
|
+
const from = isObject2(block.props) ? block.props.optionsFrom : void 0;
|
|
2067
|
+
if (from !== void 0) {
|
|
2068
|
+
if (!isObject2(from) || from.source !== "host" || typeof from.key !== "string" || !from.key) {
|
|
2069
|
+
issue(`${path}.props.optionsFrom`, 'Must be { source: "host", key } with a non-empty key.');
|
|
2070
|
+
} else if (typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
|
|
2071
|
+
warnings.push({ path: `${path}.props.optionsFrom`, message: "Only single choice, multiple choice and dropdown questions take options from the app." });
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
1822
2074
|
if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
|
|
1823
2075
|
else if (block.props.options !== void 0) {
|
|
1824
2076
|
const options = block.props.options;
|
|
@@ -1829,10 +2081,20 @@ function parseForm(input) {
|
|
|
1829
2081
|
if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
|
|
1830
2082
|
else optionalText(o.label, `${at}.label`);
|
|
1831
2083
|
});
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
2084
|
+
if (from === void 0) {
|
|
2085
|
+
choices.set(
|
|
2086
|
+
block.id,
|
|
2087
|
+
new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
const other = isObject2(block.props) ? block.props.other : void 0;
|
|
2093
|
+
if (other !== void 0) {
|
|
2094
|
+
if (other !== true && other !== false && !(isObject2(other) && (other.label === void 0 || typeof other.label === "string"))) {
|
|
2095
|
+
issue(`${path}.props.other`, "Must be true, false or { label }.");
|
|
2096
|
+
} else if (other !== false && typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
|
|
2097
|
+
warnings.push({ path: `${path}.props.other`, message: 'Only single choice, multiple choice and dropdown questions take an "Other" answer.' });
|
|
1836
2098
|
}
|
|
1837
2099
|
}
|
|
1838
2100
|
const { max, maxLength: maxLength2 } = isObject2(block.props) ? block.props : {};
|
|
@@ -1907,6 +2169,8 @@ function parseForm(input) {
|
|
|
1907
2169
|
} else if (ref.column !== void 0) issue(path, `"${ref.key}" is not a table, so it has no column "${ref.column}".`);
|
|
1908
2170
|
else if (targetType === "media-notes" || targetType === "text-highlights") {
|
|
1909
2171
|
warnings.push({ path, message: `"${ref.key}" holds notes, not numbers; the formula reads it as blank.` });
|
|
2172
|
+
} else if (targetType === PHOTO_TYPE) {
|
|
2173
|
+
warnings.push({ path, message: `"${ref.key}" holds photos, not numbers; the formula reads it as blank.` });
|
|
1910
2174
|
}
|
|
1911
2175
|
}
|
|
1912
2176
|
});
|
|
@@ -2110,6 +2374,41 @@ function parseForm(input) {
|
|
|
2110
2374
|
|
|
2111
2375
|
// src/server.ts
|
|
2112
2376
|
var MAX_EVENTS = 1e4;
|
|
2377
|
+
var MAX_SUBMISSION_VALUES = 2e5;
|
|
2378
|
+
var MAX_REPLAY_WORK = 1e6;
|
|
2379
|
+
var RAW_EVENTS_PER_EVENT = 10;
|
|
2380
|
+
function countValues(value, limit) {
|
|
2381
|
+
let count = 0;
|
|
2382
|
+
const stack = [value];
|
|
2383
|
+
while (stack.length && count <= limit) {
|
|
2384
|
+
const v = stack.pop();
|
|
2385
|
+
if (v === void 0) continue;
|
|
2386
|
+
count++;
|
|
2387
|
+
if (Array.isArray(v)) for (const item of v) stack.push(item);
|
|
2388
|
+
else if (v !== null && typeof v === "object") for (const k in v) stack.push(v[k]);
|
|
2389
|
+
}
|
|
2390
|
+
return count;
|
|
2391
|
+
}
|
|
2392
|
+
var answerTo = (e) => isPlainObject(e) && e.type === "answer" && typeof e.field === "string" ? e.field : void 0;
|
|
2393
|
+
function leanEvents(events) {
|
|
2394
|
+
const out = [];
|
|
2395
|
+
for (let i = 0; i < events.length; i++) {
|
|
2396
|
+
const field = answerTo(events[i]);
|
|
2397
|
+
if (field !== void 0 && answerTo(events[i + 1]) === field) continue;
|
|
2398
|
+
out.push(events[i]);
|
|
2399
|
+
}
|
|
2400
|
+
return out;
|
|
2401
|
+
}
|
|
2402
|
+
function submissionValues(input, limit) {
|
|
2403
|
+
let count = countValues(input.hidden, limit);
|
|
2404
|
+
if (Array.isArray(input.events)) {
|
|
2405
|
+
for (const e of input.events) {
|
|
2406
|
+
if (count > limit) break;
|
|
2407
|
+
if (answerTo(e) !== void 0) count += countValues(e.value, limit - count);
|
|
2408
|
+
}
|
|
2409
|
+
} else count += countValues(input.answers, limit - count);
|
|
2410
|
+
return count;
|
|
2411
|
+
}
|
|
2113
2412
|
var MALFORMED = { ok: false, errors: { "": "Malformed submission." } };
|
|
2114
2413
|
var parsedDefinitions = /* @__PURE__ */ new WeakMap();
|
|
2115
2414
|
function checkDefinition(definition) {
|
|
@@ -2136,10 +2435,28 @@ function wellFormed(input) {
|
|
|
2136
2435
|
}
|
|
2137
2436
|
return { answers, events: known, hidden };
|
|
2138
2437
|
}
|
|
2139
|
-
function validateSubmission(
|
|
2140
|
-
const invalid = checkDefinition(
|
|
2438
|
+
function validateSubmission(published, submission, options = {}) {
|
|
2439
|
+
const invalid = checkDefinition(published);
|
|
2141
2440
|
if (invalid) return { ok: false, errors: { "": invalid } };
|
|
2142
|
-
const
|
|
2441
|
+
const definition = withHostOptions(published, options.hostOptions);
|
|
2442
|
+
if (!isPlainObject(submission)) return MALFORMED;
|
|
2443
|
+
const maxEvents = options.maxEvents ?? MAX_EVENTS;
|
|
2444
|
+
let sent = submission;
|
|
2445
|
+
if (Array.isArray(submission.events)) {
|
|
2446
|
+
const maxRaw = maxEvents * RAW_EVENTS_PER_EVENT;
|
|
2447
|
+
if (submission.events.length > maxRaw) {
|
|
2448
|
+
return { ok: false, errors: { "": `Too many events: ${submission.events.length} sent; the limit is ${maxRaw} before compaction.` } };
|
|
2449
|
+
}
|
|
2450
|
+
sent = { ...submission, events: leanEvents(submission.events) };
|
|
2451
|
+
}
|
|
2452
|
+
const maxValues = options.maxValues ?? MAX_SUBMISSION_VALUES;
|
|
2453
|
+
if (submissionValues(sent, maxValues) > maxValues) {
|
|
2454
|
+
return {
|
|
2455
|
+
ok: false,
|
|
2456
|
+
errors: { "": `Too much data: the answers hold more than ${maxValues} values (items, rows and cells); the limit is ${maxValues}.` }
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
const input = wellFormed(sent);
|
|
2143
2460
|
if (!input) return MALFORMED;
|
|
2144
2461
|
const ignored = /* @__PURE__ */ new Set();
|
|
2145
2462
|
const client = coerceHidden(definition, input.hidden, { sources: ["url", "host"] });
|
|
@@ -2158,12 +2475,20 @@ function validateSubmission(definition, submission, options = {}) {
|
|
|
2158
2475
|
let engine;
|
|
2159
2476
|
if (input.events) {
|
|
2160
2477
|
const events = compactEvents(input.events);
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
return { ok: false, errors: { "": `Too many events: ${events.length} after compaction; the limit is ${max}.` } };
|
|
2478
|
+
if (events.length > maxEvents) {
|
|
2479
|
+
return { ok: false, errors: { "": `Too many events: ${events.length} after compaction; the limit is ${maxEvents}.` } };
|
|
2164
2480
|
}
|
|
2165
2481
|
const kept = events.filter((e) => e.type !== "answer" || accept(e.field) !== false);
|
|
2166
|
-
|
|
2482
|
+
const maxWork = options.maxReplayWork ?? MAX_REPLAY_WORK;
|
|
2483
|
+
try {
|
|
2484
|
+
engine = replayWithin(definition, kept, { hidden: engineHidden, validators: options.validators }, maxWork);
|
|
2485
|
+
} catch (e) {
|
|
2486
|
+
if (!(e instanceof ReplayWorkExceeded)) throw e;
|
|
2487
|
+
return {
|
|
2488
|
+
ok: false,
|
|
2489
|
+
errors: { "": `Too much work: replaying these events (the path and calculated fields) would take more than ${maxWork} steps; the limit is ${maxWork}.` }
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2167
2492
|
} else {
|
|
2168
2493
|
const answers2 = {};
|
|
2169
2494
|
for (const id of Object.keys(input.answers ?? {})) {
|