@marktiderman/genesis-forms 0.8.0 → 0.9.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 +299 -10
- package/dist/destinations.cjs +202 -39
- package/dist/destinations.cjs.map +1 -1
- package/dist/destinations.d.cts +67 -21
- package/dist/destinations.d.ts +67 -21
- package/dist/destinations.js +202 -40
- package/dist/destinations.js.map +1 -1
- package/dist/{engine-Bj23YJlB.d.cts → engine-BaHPC2-1.d.cts} +2 -2
- package/dist/{engine-CzinTJph.d.ts → engine-Da99Tv-E.d.ts} +2 -2
- package/dist/index.cjs +996 -167
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +288 -11
- package/dist/index.d.ts +288 -11
- package/dist/index.js +975 -168
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +563 -173
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +3 -474
- package/dist/server.d.ts +3 -474
- package/dist/server.js +558 -174
- package/dist/server.js.map +1 -1
- package/dist/submit-protocol-CFi-0Qd0.d.ts +840 -0
- package/dist/submit-protocol-D0cOckmK.d.cts +840 -0
- package/dist/testing.cjs +250 -154
- 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 +250 -154
- package/dist/testing.js.map +1 -1
- package/dist/{types-jqDKcmP4.d.cts → types-C0pUfew3.d.cts} +7 -1
- package/dist/{types-jqDKcmP4.d.ts → types-C0pUfew3.d.ts} +7 -1
- package/dist/{validate-DvfQHYnu.d.ts → validate-DpmBssFk.d.ts} +258 -140
- package/dist/{validate-irIPubxc.d.cts → validate-U5LoW3eR.d.cts} +258 -140
- package/package.json +2 -2
- package/dist/upload-protocol-BOe018kL.d.cts +0 -122
- package/dist/upload-protocol-BUO74WkS.d.ts +0 -122
package/dist/server.cjs
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/props-check.ts
|
|
4
|
+
var RESERVED_NAMES = new Set(Object.getOwnPropertyNames(Object.prototype));
|
|
5
|
+
var isPlainRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6
|
+
var isWholeNumber = (v) => typeof v === "number" && Number.isInteger(v);
|
|
7
|
+
var isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
8
|
+
function propsChecker(prefix, report) {
|
|
9
|
+
const at = (path) => path ? `${prefix}.${path}` : prefix;
|
|
10
|
+
const issue = (path, message) => report(at(path), message);
|
|
11
|
+
return {
|
|
12
|
+
issue,
|
|
13
|
+
text(v, path, nonEmpty = false) {
|
|
14
|
+
if (typeof v !== "string" || nonEmpty && !v) issue(path, nonEmpty ? "Required." : "Required text.");
|
|
15
|
+
},
|
|
16
|
+
optionalText(v, path) {
|
|
17
|
+
if (v !== void 0 && typeof v !== "string") issue(path, "Must be text.");
|
|
18
|
+
},
|
|
19
|
+
optionalBoolean(v, path) {
|
|
20
|
+
if (v !== void 0 && typeof v !== "boolean") issue(path, "Must be true or false.");
|
|
21
|
+
},
|
|
22
|
+
optionalInt(v, path, floor) {
|
|
23
|
+
if (v !== void 0 && !(isWholeNumber(v) && v >= floor)) issue(path, `Must be a whole number of ${floor} or more.`);
|
|
24
|
+
},
|
|
25
|
+
ordered(low, high, path, what) {
|
|
26
|
+
if (isWholeNumber(low) && isWholeNumber(high) && low > high) issue(path, `Can't be more than the ${what}.`);
|
|
27
|
+
},
|
|
28
|
+
name(v, path, what) {
|
|
29
|
+
if (typeof v !== "string" || !v) {
|
|
30
|
+
issue(path, `${what} must be a non-empty string.`);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (RESERVED_NAMES.has(v)) {
|
|
34
|
+
issue(path, `"${v}" is reserved and cannot be used as ${what.toLowerCase()}.`);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
3
42
|
// src/formula.ts
|
|
4
43
|
var FORMULA_FUNCTIONS = ["sum", "avg", "min", "max"];
|
|
5
44
|
var MAX_FORMULA_LENGTH = 2e3;
|
|
@@ -426,11 +465,11 @@ function calculator(definition, work) {
|
|
|
426
465
|
for (const block of definition.blocks) {
|
|
427
466
|
if (block.kind !== "field" || !isCalculated(block)) continue;
|
|
428
467
|
const props = calculatedProps(block);
|
|
429
|
-
const
|
|
468
|
+
const parsed2 = parseFormula(props.formula);
|
|
430
469
|
const reads = [];
|
|
431
470
|
const slots = /* @__PURE__ */ new Map();
|
|
432
471
|
const byKey = /* @__PURE__ */ new Map();
|
|
433
|
-
for (const ref of
|
|
472
|
+
for (const ref of parsed2.ok ? refNodes(parsed2.ast) : []) {
|
|
434
473
|
const id = idByKey.get(ref.key);
|
|
435
474
|
const key = `${id ?? ""}\0${ref.column ?? ""}`;
|
|
436
475
|
let slot = byKey.get(key);
|
|
@@ -440,7 +479,7 @@ function calculator(definition, work) {
|
|
|
440
479
|
}
|
|
441
480
|
slots.set(ref, slot);
|
|
442
481
|
}
|
|
443
|
-
compiled.push({ field: block, props, ast:
|
|
482
|
+
compiled.push({ field: block, props, ast: parsed2.ok ? parsed2.ast : null, size: parsed2.ok ? formulaSize(parsed2.ast) : 0, reads, slots, last: null });
|
|
444
483
|
}
|
|
445
484
|
const extracted = /* @__PURE__ */ new WeakMap();
|
|
446
485
|
const cachedList = (value, column) => {
|
|
@@ -534,6 +573,17 @@ var calculatedFieldType = {
|
|
|
534
573
|
value: { json: true },
|
|
535
574
|
operators: numberOps,
|
|
536
575
|
a11y: { semantics: "control" },
|
|
576
|
+
// A number, unless the format says text; without a format it can hold either, so it isn't known.
|
|
577
|
+
literal: (props) => props.format === "text" ? "string" : props.format === "number" || props.format === "percent" ? "number" : void 0,
|
|
578
|
+
// The formula itself is checked once every key is known (`parseForm`).
|
|
579
|
+
checkProps: (props, check) => {
|
|
580
|
+
if (props.format !== void 0 && props.format !== "number" && props.format !== "percent" && props.format !== "text") {
|
|
581
|
+
check.issue("format", "Must be number, percent or text.");
|
|
582
|
+
}
|
|
583
|
+
if (props.decimals !== void 0 && !(isWholeNumber(props.decimals) && props.decimals >= 0 && props.decimals <= MAX_CALCULATED_DECIMALS)) {
|
|
584
|
+
check.issue("decimals", `Must be a whole number from 0 to ${MAX_CALCULATED_DECIMALS}.`);
|
|
585
|
+
}
|
|
586
|
+
},
|
|
537
587
|
// The engine works the value out; any number, text or true / false it made is valid.
|
|
538
588
|
validate: (value) => typeof value === "number" || typeof value === "string" || typeof value === "boolean" ? null : "This value is worked out from other answers.",
|
|
539
589
|
summarize: (value, field) => formatCalculated(value, field)
|
|
@@ -591,12 +641,42 @@ var MAX_HIGHLIGHT_NOTE_LENGTH = 1e3;
|
|
|
591
641
|
var MAX_HIGHLIGHT_QUOTE_LENGTH = 2e3;
|
|
592
642
|
var HIGHLIGHT_NOTE_MESSAGE = "Add a note to each highlight.";
|
|
593
643
|
var sourceText = (field) => typeof field.props?.text === "string" && field.props.text ? field.props.text : void 0;
|
|
644
|
+
var listProps = (props, check) => {
|
|
645
|
+
check.optionalInt(props.min, "min", 0);
|
|
646
|
+
check.optionalInt(props.max, "max", 1);
|
|
647
|
+
check.optionalInt(props.itemMaxLength, "itemMaxLength", 1);
|
|
648
|
+
check.optionalText(props.placeholder, "placeholder");
|
|
649
|
+
check.ordered(props.min, props.max ?? 10, "min", "most items");
|
|
650
|
+
};
|
|
651
|
+
var tableProps = (props, check) => {
|
|
652
|
+
const columns = props.columns;
|
|
653
|
+
if (!Array.isArray(columns) || columns.length < 1 || columns.length > MAX_TABLE_COLUMNS) {
|
|
654
|
+
check.issue("columns", `Must be a list of 1 to ${MAX_TABLE_COLUMNS} columns.`);
|
|
655
|
+
} else {
|
|
656
|
+
const seen = /* @__PURE__ */ new Set();
|
|
657
|
+
columns.forEach((c, j) => {
|
|
658
|
+
const at = `columns[${j}]`;
|
|
659
|
+
if (!isPlainRecord(c)) return check.issue(at, "Each column needs a key and a label.");
|
|
660
|
+
if (check.name(c.key, `${at}.key`, "A column key")) {
|
|
661
|
+
if (seen.has(c.key)) check.issue(`${at}.key`, `Duplicate column key "${c.key}".`);
|
|
662
|
+
seen.add(c.key);
|
|
663
|
+
}
|
|
664
|
+
check.text(c.label, `${at}.label`);
|
|
665
|
+
check.optionalBoolean(c.long, `${at}.long`);
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
check.optionalInt(props.minRows, "minRows", 0);
|
|
669
|
+
check.optionalInt(props.maxRows, "maxRows", 1);
|
|
670
|
+
check.ordered(props.minRows, props.maxRows ?? 3, "minRows", "most rows");
|
|
671
|
+
};
|
|
594
672
|
var structuredFieldTypes = [
|
|
595
673
|
{
|
|
596
674
|
type: "list",
|
|
597
675
|
label: "List",
|
|
598
676
|
category: "text",
|
|
599
677
|
value: { json: true },
|
|
678
|
+
literal: "string",
|
|
679
|
+
checkProps: listProps,
|
|
600
680
|
// logic.ts `contains` on an array is exact item equality, same as multi-choice, so it is offered.
|
|
601
681
|
operators: ["contains", "answered", "empty"],
|
|
602
682
|
a11y: { semantics: "group" },
|
|
@@ -622,6 +702,7 @@ var structuredFieldTypes = [
|
|
|
622
702
|
label: "Table",
|
|
623
703
|
category: "advanced",
|
|
624
704
|
value: { json: true },
|
|
705
|
+
checkProps: tableProps,
|
|
625
706
|
operators: ["answered", "empty"],
|
|
626
707
|
a11y: { semantics: "group" },
|
|
627
708
|
isEmpty: (value) => emptyList(value, (r) => isObject(r) && Object.values(r).every(blankText)),
|
|
@@ -665,6 +746,10 @@ var structuredFieldTypes = [
|
|
|
665
746
|
label: "Notes at moments",
|
|
666
747
|
category: "media",
|
|
667
748
|
value: { json: true },
|
|
749
|
+
checkProps: (props, check) => {
|
|
750
|
+
check.optionalInt(props.maxNotes, "maxNotes", 1);
|
|
751
|
+
check.optionalInt(props.textMaxLength, "textMaxLength", 1);
|
|
752
|
+
},
|
|
668
753
|
operators: ["answered", "empty"],
|
|
669
754
|
a11y: { semantics: "group" },
|
|
670
755
|
isEmpty: (value) => emptyList(value, (n) => isObject(n) && blankText(n.text)),
|
|
@@ -688,6 +773,10 @@ var structuredFieldTypes = [
|
|
|
688
773
|
label: "Highlights",
|
|
689
774
|
category: "media",
|
|
690
775
|
value: { json: true },
|
|
776
|
+
checkProps: (props, check) => {
|
|
777
|
+
check.optionalText(props.text, "text");
|
|
778
|
+
check.optionalInt(props.maxHighlights, "maxHighlights", 1);
|
|
779
|
+
},
|
|
691
780
|
operators: ["answered", "empty"],
|
|
692
781
|
a11y: { semantics: "group" },
|
|
693
782
|
isEmpty: (value) => emptyList(value, () => false),
|
|
@@ -780,6 +869,22 @@ var photoFieldType = {
|
|
|
780
869
|
value: { json: true },
|
|
781
870
|
operators: ["answered", "empty"],
|
|
782
871
|
a11y: { semantics: "group" },
|
|
872
|
+
checkProps: (props, check) => {
|
|
873
|
+
if (props.maxFiles !== void 0 && !(isWholeNumber(props.maxFiles) && props.maxFiles >= 1 && props.maxFiles <= MAX_PHOTO_FILES)) {
|
|
874
|
+
check.issue("maxFiles", `Must be a whole number from 1 to ${MAX_PHOTO_FILES}.`);
|
|
875
|
+
}
|
|
876
|
+
if (props.maxSizeMb !== void 0 && !(isWholeNumber(props.maxSizeMb) && props.maxSizeMb >= 1 && props.maxSizeMb <= MAX_PHOTO_SIZE_MB)) {
|
|
877
|
+
check.issue("maxSizeMb", `Must be a whole number from 1 to ${MAX_PHOTO_SIZE_MB}.`);
|
|
878
|
+
}
|
|
879
|
+
if (props.formats !== void 0) {
|
|
880
|
+
const formats = props.formats;
|
|
881
|
+
if (!Array.isArray(formats) || formats.length === 0 || !formats.every(isPhotoFormat)) {
|
|
882
|
+
check.issue("formats", "Must be a list of one or more of jpeg, png, heic and webp.");
|
|
883
|
+
} else if (new Set(formats).size !== formats.length) {
|
|
884
|
+
check.issue("formats", "Name each format once.");
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
},
|
|
783
888
|
isEmpty: (value) => value === void 0 || value === null || Array.isArray(value) && value.length === 0,
|
|
784
889
|
validate: (value, field) => {
|
|
785
890
|
if (!Array.isArray(value) || !value.every(isPhotoRef)) return "Add a photo.";
|
|
@@ -797,22 +902,59 @@ var photoFieldType = {
|
|
|
797
902
|
};
|
|
798
903
|
|
|
799
904
|
// src/fields.ts
|
|
800
|
-
var
|
|
905
|
+
var VALIDATOR_FAILED_MESSAGE = "This answer couldn't be checked.";
|
|
906
|
+
var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types@2");
|
|
907
|
+
var BUILT_IN = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/built-in-field-types@2");
|
|
801
908
|
var globalScope = globalThis;
|
|
802
909
|
var registry = globalScope[REGISTRY] ??= /* @__PURE__ */ new Map();
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
);
|
|
910
|
+
var builtIn = globalScope[BUILT_IN] ??= /* @__PURE__ */ new Set();
|
|
911
|
+
var freezeRecord = (v) => v && typeof v === "object" && !Array.isArray(v) ? Object.freeze({ ...v }) : v;
|
|
912
|
+
function frozen(manifest) {
|
|
913
|
+
const copy = { ...manifest };
|
|
914
|
+
for (const key of ["choice", "a11y", "value"]) if (key in copy) copy[key] = freezeRecord(copy[key]);
|
|
915
|
+
if (Array.isArray(copy.operators)) {
|
|
916
|
+
const operators = Object.freeze([...copy.operators]);
|
|
917
|
+
Object.defineProperty(copy, "operators", { get: () => [...operators], enumerable: true });
|
|
810
918
|
}
|
|
811
|
-
|
|
919
|
+
return Object.freeze(copy);
|
|
920
|
+
}
|
|
921
|
+
function registerBuiltIn(manifest) {
|
|
922
|
+
builtIn.add(manifest.type);
|
|
923
|
+
if (!registry.has(manifest.type)) registry.set(manifest.type, frozen(manifest));
|
|
812
924
|
}
|
|
813
925
|
function getFieldType(type) {
|
|
814
926
|
return registry.get(type);
|
|
815
927
|
}
|
|
928
|
+
function choiceKind(type) {
|
|
929
|
+
return registry.get(type)?.choice;
|
|
930
|
+
}
|
|
931
|
+
function isChoiceField(block) {
|
|
932
|
+
return !!block && block.kind === "field" && typeof block.type === "string" && choiceKind(block.type) !== void 0;
|
|
933
|
+
}
|
|
934
|
+
function literalType(type, props) {
|
|
935
|
+
const literal = registry.get(type)?.literal;
|
|
936
|
+
if (typeof literal !== "function") return literal;
|
|
937
|
+
try {
|
|
938
|
+
return literal(props);
|
|
939
|
+
} catch {
|
|
940
|
+
return void 0;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
var CONTENT_REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/content-types@1");
|
|
944
|
+
var contentScope = globalThis;
|
|
945
|
+
var contentRegistry = contentScope[CONTENT_REGISTRY] ??= /* @__PURE__ */ new Map();
|
|
946
|
+
function getContentType(type) {
|
|
947
|
+
return contentRegistry.get(type);
|
|
948
|
+
}
|
|
949
|
+
function listContentTypes() {
|
|
950
|
+
return [...contentRegistry.values()];
|
|
951
|
+
}
|
|
952
|
+
for (const manifest of [
|
|
953
|
+
{ type: "heading", label: "Heading" },
|
|
954
|
+
{ type: "text", label: "Text" }
|
|
955
|
+
]) {
|
|
956
|
+
if (!contentRegistry.has(manifest.type)) contentRegistry.set(manifest.type, frozen(manifest));
|
|
957
|
+
}
|
|
816
958
|
function isEmptyValue(value, field) {
|
|
817
959
|
const manifest = field ? registry.get(field.type) : void 0;
|
|
818
960
|
if (manifest?.isEmpty) return manifest.isEmpty(value);
|
|
@@ -995,6 +1137,18 @@ function isPhoneNumber(value) {
|
|
|
995
1137
|
}
|
|
996
1138
|
return digits >= 7 && digits <= 15;
|
|
997
1139
|
}
|
|
1140
|
+
var oneOptionValidate = (value, field) => typeof value === "string" && (choiceOptions(field).some((o) => o.value === value) || typedOther(field, value)) ? null : "Choose one of the options.";
|
|
1141
|
+
var oneOptionSummary = (value, field) => optionLabel(field, String(value));
|
|
1142
|
+
var optionsValidate = (value, field) => {
|
|
1143
|
+
const allowed = new Set(choiceOptions(field).map((o) => o.value));
|
|
1144
|
+
const others = Array.isArray(value) ? value.filter((v) => typeof v === "string" && !allowed.has(v)) : [];
|
|
1145
|
+
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.";
|
|
1146
|
+
};
|
|
1147
|
+
var optionsSummary = (value, field) => Array.isArray(value) ? value.map((v) => optionLabel(field, String(v))).join(", ") : String(value);
|
|
1148
|
+
var scaleProps = (props, check) => {
|
|
1149
|
+
check.optionalText(props.lowLabel, "lowLabel");
|
|
1150
|
+
check.optionalText(props.highLabel, "highLabel");
|
|
1151
|
+
};
|
|
998
1152
|
var builtins = [
|
|
999
1153
|
{
|
|
1000
1154
|
type: "short-text",
|
|
@@ -1003,6 +1157,7 @@ var builtins = [
|
|
|
1003
1157
|
value: { json: true },
|
|
1004
1158
|
operators: textOps,
|
|
1005
1159
|
a11y: { semantics: "control" },
|
|
1160
|
+
literal: "string",
|
|
1006
1161
|
validate: textValidate(500)
|
|
1007
1162
|
},
|
|
1008
1163
|
{
|
|
@@ -1012,6 +1167,7 @@ var builtins = [
|
|
|
1012
1167
|
value: { json: true },
|
|
1013
1168
|
operators: textOps,
|
|
1014
1169
|
a11y: { semantics: "control" },
|
|
1170
|
+
literal: "string",
|
|
1015
1171
|
validate: textValidate(5e3)
|
|
1016
1172
|
},
|
|
1017
1173
|
{
|
|
@@ -1021,6 +1177,7 @@ var builtins = [
|
|
|
1021
1177
|
value: { json: true },
|
|
1022
1178
|
operators: textOps,
|
|
1023
1179
|
a11y: { semantics: "control", autocomplete: "email", inputMode: "email" },
|
|
1180
|
+
literal: "string",
|
|
1024
1181
|
validate: (value) => typeof value === "string" && isEmailAddress(value.trim()) ? null : "Enter an email address like name@example.com.",
|
|
1025
1182
|
normalize: (value) => typeof value === "string" ? value.trim() : value
|
|
1026
1183
|
},
|
|
@@ -1031,6 +1188,7 @@ var builtins = [
|
|
|
1031
1188
|
value: { json: true },
|
|
1032
1189
|
operators: ["eq", "neq", "answered", "empty"],
|
|
1033
1190
|
a11y: { semantics: "control", autocomplete: "tel", inputMode: "tel" },
|
|
1191
|
+
literal: "string",
|
|
1034
1192
|
validate: (value) => typeof value === "string" && isPhoneNumber(value.trim()) ? null : "Enter a phone number with 7 to 15 digits.",
|
|
1035
1193
|
normalize: (value) => typeof value === "string" ? value.trim() : value,
|
|
1036
1194
|
summarize: (value) => String(value)
|
|
@@ -1042,6 +1200,13 @@ var builtins = [
|
|
|
1042
1200
|
value: { json: true },
|
|
1043
1201
|
operators: numberOps2,
|
|
1044
1202
|
a11y: { semantics: "group" },
|
|
1203
|
+
literal: "number",
|
|
1204
|
+
checkProps: (props, check) => {
|
|
1205
|
+
scaleProps(props, check);
|
|
1206
|
+
if (props.display !== void 0 && props.display !== "numbers" && props.display !== "stars") {
|
|
1207
|
+
check.issue("display", "Must be numbers or stars.");
|
|
1208
|
+
}
|
|
1209
|
+
},
|
|
1045
1210
|
validate: (value, field) => {
|
|
1046
1211
|
const { min, max } = intRange(field, 1, 5);
|
|
1047
1212
|
return Number.isInteger(value) && value >= min && value <= max ? null : `Choose a rating from ${min} to ${max}.`;
|
|
@@ -1059,6 +1224,8 @@ var builtins = [
|
|
|
1059
1224
|
value: { json: true },
|
|
1060
1225
|
operators: numberOps2,
|
|
1061
1226
|
a11y: { semantics: "group" },
|
|
1227
|
+
literal: "number",
|
|
1228
|
+
checkProps: scaleProps,
|
|
1062
1229
|
validate: (value) => Number.isInteger(value) && value >= 0 && value <= 10 ? null : "Choose a score from 0 to 10.",
|
|
1063
1230
|
summarize: (value, field) => {
|
|
1064
1231
|
const end = scaleEndLabel(field, value, 0, 10);
|
|
@@ -1072,8 +1239,10 @@ var builtins = [
|
|
|
1072
1239
|
value: { json: true },
|
|
1073
1240
|
operators: ["eq", "neq", "in", "answered", "empty"],
|
|
1074
1241
|
a11y: { semantics: "group" },
|
|
1075
|
-
|
|
1076
|
-
|
|
1242
|
+
literal: "string",
|
|
1243
|
+
choice: { multiple: false },
|
|
1244
|
+
validate: oneOptionValidate,
|
|
1245
|
+
summarize: oneOptionSummary
|
|
1077
1246
|
},
|
|
1078
1247
|
{
|
|
1079
1248
|
type: "multi-choice",
|
|
@@ -1082,12 +1251,10 @@ var builtins = [
|
|
|
1082
1251
|
value: { json: true },
|
|
1083
1252
|
operators: ["contains", "answered", "empty"],
|
|
1084
1253
|
a11y: { semantics: "group" },
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
},
|
|
1090
|
-
summarize: (value, field) => Array.isArray(value) ? value.map((v) => optionLabel(field, String(v))).join(", ") : String(value)
|
|
1254
|
+
literal: "string",
|
|
1255
|
+
choice: { multiple: true },
|
|
1256
|
+
validate: optionsValidate,
|
|
1257
|
+
summarize: optionsSummary
|
|
1091
1258
|
},
|
|
1092
1259
|
{
|
|
1093
1260
|
type: "yes-no",
|
|
@@ -1096,6 +1263,7 @@ var builtins = [
|
|
|
1096
1263
|
value: { json: true },
|
|
1097
1264
|
operators: ["eq", "neq", "answered", "empty"],
|
|
1098
1265
|
a11y: { semantics: "group" },
|
|
1266
|
+
literal: "boolean",
|
|
1099
1267
|
validate: (value) => typeof value === "boolean" ? null : "Choose yes or no.",
|
|
1100
1268
|
summarize: (value) => value ? "Yes" : "No"
|
|
1101
1269
|
},
|
|
@@ -1106,8 +1274,10 @@ var builtins = [
|
|
|
1106
1274
|
value: { json: true },
|
|
1107
1275
|
operators: ["eq", "neq", "in", "answered", "empty"],
|
|
1108
1276
|
a11y: { semantics: "control" },
|
|
1109
|
-
|
|
1110
|
-
|
|
1277
|
+
literal: "string",
|
|
1278
|
+
choice: { multiple: false },
|
|
1279
|
+
validate: oneOptionValidate,
|
|
1280
|
+
summarize: oneOptionSummary
|
|
1111
1281
|
},
|
|
1112
1282
|
{
|
|
1113
1283
|
type: "number",
|
|
@@ -1116,6 +1286,16 @@ var builtins = [
|
|
|
1116
1286
|
value: { json: true },
|
|
1117
1287
|
operators: numberOps2,
|
|
1118
1288
|
a11y: { semantics: "control", inputMode: "decimal" },
|
|
1289
|
+
literal: "number",
|
|
1290
|
+
checkProps: (props, check) => {
|
|
1291
|
+
for (const k of ["min", "max", "step"]) {
|
|
1292
|
+
if (props[k] !== void 0 && !isFiniteNumber(props[k])) check.issue(k, "Must be a number.");
|
|
1293
|
+
}
|
|
1294
|
+
if (isFiniteNumber(props.step) && props.step <= 0) check.issue("step", "Must be above 0.");
|
|
1295
|
+
if (isFiniteNumber(props.min) && isFiniteNumber(props.max) && props.min > props.max) {
|
|
1296
|
+
check.issue("min", "Can't be more than the largest number.");
|
|
1297
|
+
}
|
|
1298
|
+
},
|
|
1119
1299
|
validate: (value, field) => {
|
|
1120
1300
|
if (typeof value !== "number" || !Number.isFinite(value)) return "Enter a number.";
|
|
1121
1301
|
const { min, max, step } = numberLimits(field);
|
|
@@ -1142,6 +1322,17 @@ var builtins = [
|
|
|
1142
1322
|
value: { json: true },
|
|
1143
1323
|
operators: ["eq", "neq", "answered", "empty"],
|
|
1144
1324
|
a11y: { semantics: "control" },
|
|
1325
|
+
literal: "string",
|
|
1326
|
+
checkProps: (props, check) => {
|
|
1327
|
+
for (const k of ["min", "max"]) {
|
|
1328
|
+
if (props[k] !== void 0 && !(typeof props[k] === "string" && isIsoDate(props[k]))) {
|
|
1329
|
+
check.issue(k, "Must be a date like 2026-10-22.");
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
if (typeof props.min === "string" && typeof props.max === "string" && isIsoDate(props.min) && isIsoDate(props.max) && props.min > props.max) {
|
|
1333
|
+
check.issue("min", "Can't be after the latest date.");
|
|
1334
|
+
}
|
|
1335
|
+
},
|
|
1145
1336
|
validate: (value, field) => {
|
|
1146
1337
|
if (typeof value !== "string" || !isIsoDate(value.trim())) return "Enter a date like 2026-10-22.";
|
|
1147
1338
|
const date = value.trim();
|
|
@@ -1164,6 +1355,8 @@ var builtins = [
|
|
|
1164
1355
|
value: { json: true },
|
|
1165
1356
|
operators: ["answered", "empty"],
|
|
1166
1357
|
a11y: { semantics: "group" },
|
|
1358
|
+
literal: "boolean",
|
|
1359
|
+
checkProps: (props, check) => check.optionalText(props.text, "text"),
|
|
1167
1360
|
// Unchecked is unanswered, so a required consent must be checked.
|
|
1168
1361
|
isEmpty: (value) => value === void 0 || value === null || value === false,
|
|
1169
1362
|
validate: (value) => value === true ? null : "Check the box to agree.",
|
|
@@ -1213,7 +1406,7 @@ function dateLimits(field) {
|
|
|
1213
1406
|
if (typeof max === "string" && isIsoDate(max)) out.max = max;
|
|
1214
1407
|
return out;
|
|
1215
1408
|
}
|
|
1216
|
-
for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType, photoFieldType])
|
|
1409
|
+
for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType, photoFieldType]) registerBuiltIn(manifest);
|
|
1217
1410
|
|
|
1218
1411
|
// src/events.ts
|
|
1219
1412
|
function isPlainObject(value) {
|
|
@@ -1430,8 +1623,8 @@ function fieldMap(def) {
|
|
|
1430
1623
|
}
|
|
1431
1624
|
|
|
1432
1625
|
// src/options.ts
|
|
1433
|
-
var
|
|
1434
|
-
var
|
|
1626
|
+
var isChoice = isChoiceField;
|
|
1627
|
+
var isMultiple = (field) => choiceKind(field.type)?.multiple === true;
|
|
1435
1628
|
function put(record, key, value) {
|
|
1436
1629
|
Object.defineProperty(record, key, { value, enumerable: true, writable: true, configurable: true });
|
|
1437
1630
|
}
|
|
@@ -1512,7 +1705,7 @@ function isPrefillParam(param) {
|
|
|
1512
1705
|
return typeof param === "string" && param.length <= MAX_PREFILL_PARAM_LENGTH && /^[A-Za-z0-9_.-]+$/.test(param);
|
|
1513
1706
|
}
|
|
1514
1707
|
function prefillSetting(field) {
|
|
1515
|
-
if (!
|
|
1708
|
+
if (!choiceKind(field.type)) return void 0;
|
|
1516
1709
|
const raw = field.props?.prefill;
|
|
1517
1710
|
if (!raw || typeof raw !== "object" || Array.isArray(raw) || !isPrefillParam(raw.param)) return void 0;
|
|
1518
1711
|
return { param: raw.param, hideWhenPrefilled: raw.hideWhenPrefilled === true };
|
|
@@ -1540,7 +1733,7 @@ function prefillFromLink(definition, params, values = {}) {
|
|
|
1540
1733
|
if (!raw.length) continue;
|
|
1541
1734
|
const shown2 = new Set(visibleOptions(definition, block, seen).map((o) => o.value));
|
|
1542
1735
|
let value;
|
|
1543
|
-
if (block
|
|
1736
|
+
if (isMultiple(block)) {
|
|
1544
1737
|
const picked = [];
|
|
1545
1738
|
for (const r of raw) {
|
|
1546
1739
|
for (const v of shown2.has(r) ? [r] : r.split(",").map((s) => s.trim())) {
|
|
@@ -1583,6 +1776,21 @@ function snapshotOf(byValue, value) {
|
|
|
1583
1776
|
function answerSnapshots(definition, answers) {
|
|
1584
1777
|
const out = {};
|
|
1585
1778
|
for (const block of definition.blocks) {
|
|
1779
|
+
if (block.kind !== "field") continue;
|
|
1780
|
+
const manifest = getFieldType(block.type);
|
|
1781
|
+
const custom = manifest?.choice ? void 0 : manifest?.snapshot;
|
|
1782
|
+
if (custom) {
|
|
1783
|
+
const value2 = own(answers, block.id);
|
|
1784
|
+
if (isEmptyValue(value2, block)) continue;
|
|
1785
|
+
let snapshot2;
|
|
1786
|
+
try {
|
|
1787
|
+
snapshot2 = custom(value2, block);
|
|
1788
|
+
} catch {
|
|
1789
|
+
snapshot2 = void 0;
|
|
1790
|
+
}
|
|
1791
|
+
if (snapshot2 !== void 0) put(out, block.id, snapshot2);
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1586
1794
|
if (!isChoice(block) || !optionsSource(block)) continue;
|
|
1587
1795
|
const value = own(answers, block.id);
|
|
1588
1796
|
if (isEmptyValue(value, block)) continue;
|
|
@@ -1600,6 +1808,22 @@ function answersFromSnapshots(definition, snapshots) {
|
|
|
1600
1808
|
const answers = {};
|
|
1601
1809
|
const missing = {};
|
|
1602
1810
|
for (const block of definition.blocks) {
|
|
1811
|
+
if (block.kind !== "field") continue;
|
|
1812
|
+
const manifest = getFieldType(block.type);
|
|
1813
|
+
const custom = manifest?.choice ? void 0 : manifest?.fromSnapshot;
|
|
1814
|
+
if (custom) {
|
|
1815
|
+
const raw2 = own(snapshots, block.id);
|
|
1816
|
+
if (raw2 === void 0) continue;
|
|
1817
|
+
let restored;
|
|
1818
|
+
try {
|
|
1819
|
+
restored = custom(raw2, block);
|
|
1820
|
+
} catch {
|
|
1821
|
+
restored = void 0;
|
|
1822
|
+
}
|
|
1823
|
+
if (restored?.value !== void 0) put(answers, block.id, restored.value);
|
|
1824
|
+
if (Array.isArray(restored?.missing) && restored.missing.length) put(missing, block.id, restored.missing);
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1603
1827
|
if (!isChoice(block)) continue;
|
|
1604
1828
|
const raw = own(snapshots, block.id);
|
|
1605
1829
|
const list = (Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw]).flatMap((s) => {
|
|
@@ -1622,7 +1846,7 @@ function answersFromSnapshots(definition, snapshots) {
|
|
|
1622
1846
|
if (value === void 0) lost.push(s);
|
|
1623
1847
|
else if (!found.includes(value)) found.push(value);
|
|
1624
1848
|
}
|
|
1625
|
-
if (found.length) put(answers, block.id, block
|
|
1849
|
+
if (found.length) put(answers, block.id, isMultiple(block) ? found : found[0]);
|
|
1626
1850
|
if (lost.length) put(missing, block.id, lost);
|
|
1627
1851
|
}
|
|
1628
1852
|
return { answers, missing };
|
|
@@ -1663,8 +1887,13 @@ function validateField(field, value, options = {}) {
|
|
|
1663
1887
|
}
|
|
1664
1888
|
const manifest = getFieldType(field.type);
|
|
1665
1889
|
if (!manifest) return `Unknown field type "${field.type}".`;
|
|
1666
|
-
|
|
1667
|
-
|
|
1890
|
+
let builtIn2;
|
|
1891
|
+
try {
|
|
1892
|
+
builtIn2 = manifest.validate?.(value, field) ?? null;
|
|
1893
|
+
} catch {
|
|
1894
|
+
builtIn2 = VALIDATOR_FAILED_MESSAGE;
|
|
1895
|
+
}
|
|
1896
|
+
if (builtIn2) return builtIn2;
|
|
1668
1897
|
return options.custom ? runCustom(options.custom, value) : null;
|
|
1669
1898
|
}
|
|
1670
1899
|
|
|
@@ -1966,25 +2195,6 @@ function shown(v) {
|
|
|
1966
2195
|
}
|
|
1967
2196
|
var isObject2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
1968
2197
|
var isInt = (v) => typeof v === "number" && Number.isInteger(v);
|
|
1969
|
-
var LITERAL_TYPE = {
|
|
1970
|
-
"short-text": "string",
|
|
1971
|
-
"long-text": "string",
|
|
1972
|
-
email: "string",
|
|
1973
|
-
phone: "string",
|
|
1974
|
-
rating: "number",
|
|
1975
|
-
nps: "number",
|
|
1976
|
-
"single-choice": "string",
|
|
1977
|
-
"multi-choice": "string",
|
|
1978
|
-
"yes-no": "boolean",
|
|
1979
|
-
list: "string",
|
|
1980
|
-
dropdown: "string",
|
|
1981
|
-
number: "number",
|
|
1982
|
-
date: "string",
|
|
1983
|
-
consent: "boolean",
|
|
1984
|
-
// A calculated field holds a number unless its format says text; see `literalType`.
|
|
1985
|
-
calculated: "number"
|
|
1986
|
-
};
|
|
1987
|
-
var CHOICE_TYPES = /* @__PURE__ */ new Set(["single-choice", "multi-choice", "dropdown"]);
|
|
1988
2198
|
var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
|
|
1989
2199
|
var OPERATORS = /* @__PURE__ */ new Set([
|
|
1990
2200
|
"eq",
|
|
@@ -1998,8 +2208,11 @@ var OPERATORS = /* @__PURE__ */ new Set([
|
|
|
1998
2208
|
"answered",
|
|
1999
2209
|
"empty"
|
|
2000
2210
|
]);
|
|
2001
|
-
var RESERVED = new Set(Object.getOwnPropertyNames(Object.prototype));
|
|
2002
2211
|
var MAX_CONDITION_DEPTH = 32;
|
|
2212
|
+
function contentKinds() {
|
|
2213
|
+
const names = listContentTypes().map((c) => c.type);
|
|
2214
|
+
return names.length > 1 ? `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}` : names[0] ?? "a registered content type";
|
|
2215
|
+
}
|
|
2003
2216
|
function parseForm(input) {
|
|
2004
2217
|
const issues = [];
|
|
2005
2218
|
const warnings = [];
|
|
@@ -2029,7 +2242,7 @@ function parseForm(input) {
|
|
|
2029
2242
|
}
|
|
2030
2243
|
const fits = (x) => typeof x === "string" && x.length <= OPTION_LIMITS.attributeValue;
|
|
2031
2244
|
for (const [name2, v] of Object.entries(attributes)) {
|
|
2032
|
-
if (!name2.trim() || name2.length > OPTION_LIMITS.attributeName ||
|
|
2245
|
+
if (!name2.trim() || name2.length > OPTION_LIMITS.attributeName || RESERVED_NAMES.has(name2)) {
|
|
2033
2246
|
issue(`${at}.attributes`, `${shown(name2)} isn't a usable attribute name (at most ${OPTION_LIMITS.attributeName} characters).`);
|
|
2034
2247
|
} else if (!fits(v) && !(Array.isArray(v) && v.length <= OPTION_LIMITS.attributeItems && v.every(fits))) {
|
|
2035
2248
|
issue(
|
|
@@ -2044,104 +2257,18 @@ function parseForm(input) {
|
|
|
2044
2257
|
issue(path, `${what} must be a non-empty string.`);
|
|
2045
2258
|
return false;
|
|
2046
2259
|
}
|
|
2047
|
-
if (
|
|
2260
|
+
if (RESERVED_NAMES.has(v)) {
|
|
2048
2261
|
issue(path, `"${v}" is reserved and cannot be used as ${what.toLowerCase()}.`);
|
|
2049
2262
|
return false;
|
|
2050
2263
|
}
|
|
2051
2264
|
return true;
|
|
2052
2265
|
};
|
|
2053
|
-
const
|
|
2054
|
-
if (
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
const structuredProps = (type, props, path) => {
|
|
2060
|
-
if (type === "list") {
|
|
2061
|
-
optionalInt(props.min, `${path}.min`, 0);
|
|
2062
|
-
optionalInt(props.max, `${path}.max`, 1);
|
|
2063
|
-
optionalInt(props.itemMaxLength, `${path}.itemMaxLength`, 1);
|
|
2064
|
-
optionalText(props.placeholder, `${path}.placeholder`);
|
|
2065
|
-
ordered(props.min, props.max ?? 10, `${path}.min`, "most items");
|
|
2066
|
-
} else if (type === "table") {
|
|
2067
|
-
const columns = props.columns;
|
|
2068
|
-
if (!Array.isArray(columns) || columns.length < 1 || columns.length > MAX_TABLE_COLUMNS) {
|
|
2069
|
-
issue(`${path}.columns`, `Must be a list of 1 to ${MAX_TABLE_COLUMNS} columns.`);
|
|
2070
|
-
} else {
|
|
2071
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2072
|
-
columns.forEach((c, j) => {
|
|
2073
|
-
const at = `${path}.columns[${j}]`;
|
|
2074
|
-
if (!isObject2(c)) return issue(at, "Each column needs a key and a label.");
|
|
2075
|
-
if (name(c.key, `${at}.key`, "A column key")) {
|
|
2076
|
-
if (seen.has(c.key)) issue(`${at}.key`, `Duplicate column key "${c.key}".`);
|
|
2077
|
-
seen.add(c.key);
|
|
2078
|
-
}
|
|
2079
|
-
text(c.label, `${at}.label`);
|
|
2080
|
-
optionalBoolean(c.long, `${at}.long`);
|
|
2081
|
-
});
|
|
2082
|
-
}
|
|
2083
|
-
optionalInt(props.minRows, `${path}.minRows`, 0);
|
|
2084
|
-
optionalInt(props.maxRows, `${path}.maxRows`, 1);
|
|
2085
|
-
ordered(props.minRows, props.maxRows ?? 3, `${path}.minRows`, "most rows");
|
|
2086
|
-
} else if (type === "media-notes") {
|
|
2087
|
-
optionalInt(props.maxNotes, `${path}.maxNotes`, 1);
|
|
2088
|
-
optionalInt(props.textMaxLength, `${path}.textMaxLength`, 1);
|
|
2089
|
-
} else if (type === "text-highlights") {
|
|
2090
|
-
optionalText(props.text, `${path}.text`);
|
|
2091
|
-
optionalInt(props.maxHighlights, `${path}.maxHighlights`, 1);
|
|
2092
|
-
} else if (type === CALCULATED_TYPE) {
|
|
2093
|
-
if (props.format !== void 0 && props.format !== "number" && props.format !== "percent" && props.format !== "text") {
|
|
2094
|
-
issue(`${path}.format`, "Must be number, percent or text.");
|
|
2095
|
-
}
|
|
2096
|
-
if (props.decimals !== void 0 && !(isInt(props.decimals) && props.decimals >= 0 && props.decimals <= MAX_CALCULATED_DECIMALS)) {
|
|
2097
|
-
issue(`${path}.decimals`, `Must be a whole number from 0 to ${MAX_CALCULATED_DECIMALS}.`);
|
|
2098
|
-
}
|
|
2099
|
-
}
|
|
2100
|
-
};
|
|
2101
|
-
const isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
2102
|
-
const fieldProps = (type, props, path) => {
|
|
2103
|
-
if (type === "rating" || type === "nps") {
|
|
2104
|
-
optionalText(props.lowLabel, `${path}.lowLabel`);
|
|
2105
|
-
optionalText(props.highLabel, `${path}.highLabel`);
|
|
2106
|
-
}
|
|
2107
|
-
if (type === "rating" && props.display !== void 0 && props.display !== "numbers" && props.display !== "stars") {
|
|
2108
|
-
issue(`${path}.display`, "Must be numbers or stars.");
|
|
2109
|
-
}
|
|
2110
|
-
if (type === "number") {
|
|
2111
|
-
for (const k of ["min", "max", "step"]) {
|
|
2112
|
-
if (props[k] !== void 0 && !isFiniteNumber(props[k])) issue(`${path}.${k}`, "Must be a number.");
|
|
2113
|
-
}
|
|
2114
|
-
if (isFiniteNumber(props.step) && props.step <= 0) issue(`${path}.step`, "Must be above 0.");
|
|
2115
|
-
if (isFiniteNumber(props.min) && isFiniteNumber(props.max) && props.min > props.max) {
|
|
2116
|
-
issue(`${path}.min`, "Can't be more than the largest number.");
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
if (type === "date") {
|
|
2120
|
-
for (const k of ["min", "max"]) {
|
|
2121
|
-
if (props[k] !== void 0 && !(typeof props[k] === "string" && isIsoDate(props[k]))) {
|
|
2122
|
-
issue(`${path}.${k}`, "Must be a date like 2026-10-22.");
|
|
2123
|
-
}
|
|
2124
|
-
}
|
|
2125
|
-
if (typeof props.min === "string" && typeof props.max === "string" && isIsoDate(props.min) && isIsoDate(props.max) && props.min > props.max) {
|
|
2126
|
-
issue(`${path}.min`, "Can't be after the latest date.");
|
|
2127
|
-
}
|
|
2128
|
-
}
|
|
2129
|
-
if (type === "consent") optionalText(props.text, `${path}.text`);
|
|
2130
|
-
if (type === PHOTO_TYPE) {
|
|
2131
|
-
if (props.maxFiles !== void 0 && !(isInt(props.maxFiles) && props.maxFiles >= 1 && props.maxFiles <= MAX_PHOTO_FILES)) {
|
|
2132
|
-
issue(`${path}.maxFiles`, `Must be a whole number from 1 to ${MAX_PHOTO_FILES}.`);
|
|
2133
|
-
}
|
|
2134
|
-
if (props.maxSizeMb !== void 0 && !(isInt(props.maxSizeMb) && props.maxSizeMb >= 1 && props.maxSizeMb <= MAX_PHOTO_SIZE_MB)) {
|
|
2135
|
-
issue(`${path}.maxSizeMb`, `Must be a whole number from 1 to ${MAX_PHOTO_SIZE_MB}.`);
|
|
2136
|
-
}
|
|
2137
|
-
if (props.formats !== void 0) {
|
|
2138
|
-
const formats = props.formats;
|
|
2139
|
-
if (!Array.isArray(formats) || formats.length === 0 || !formats.every(isPhotoFormat)) {
|
|
2140
|
-
issue(`${path}.formats`, "Must be a list of one or more of jpeg, png, heic and webp.");
|
|
2141
|
-
} else if (new Set(formats).size !== formats.length) {
|
|
2142
|
-
issue(`${path}.formats`, "Name each format once.");
|
|
2143
|
-
}
|
|
2144
|
-
}
|
|
2266
|
+
const kindProps = (check, props, path) => {
|
|
2267
|
+
if (!check) return;
|
|
2268
|
+
try {
|
|
2269
|
+
check(props, propsChecker(path, issue));
|
|
2270
|
+
} catch {
|
|
2271
|
+
issue(path, "These settings couldn't be checked.");
|
|
2145
2272
|
}
|
|
2146
2273
|
};
|
|
2147
2274
|
if (!isObject2(input)) {
|
|
@@ -2180,7 +2307,7 @@ function parseForm(input) {
|
|
|
2180
2307
|
const order = /* @__PURE__ */ new Map();
|
|
2181
2308
|
const tableColumnKeys = /* @__PURE__ */ new Map();
|
|
2182
2309
|
const choices = /* @__PURE__ */ new Map();
|
|
2183
|
-
const
|
|
2310
|
+
const literals = /* @__PURE__ */ new Map();
|
|
2184
2311
|
input.blocks.forEach((block, i) => {
|
|
2185
2312
|
const path = `blocks[${i}]`;
|
|
2186
2313
|
if (!isObject2(block) || typeof block.id !== "string" || !block.id) {
|
|
@@ -2208,7 +2335,7 @@ function parseForm(input) {
|
|
|
2208
2335
|
if (from !== void 0) {
|
|
2209
2336
|
if (!isObject2(from) || from.source !== "host" || typeof from.key !== "string" || !from.key) {
|
|
2210
2337
|
issue(`${path}.props.optionsFrom`, 'Must be { source: "host", key } with a non-empty key.');
|
|
2211
|
-
} else if (typeof block.type === "string" && !
|
|
2338
|
+
} else if (typeof block.type === "string" && !choiceKind(block.type)) {
|
|
2212
2339
|
warnings.push({ path: `${path}.props.optionsFrom`, message: "Only single choice, multiple choice and dropdown questions take options from the app." });
|
|
2213
2340
|
}
|
|
2214
2341
|
const filter = isObject2(from) ? from.filter : void 0;
|
|
@@ -2230,7 +2357,7 @@ function parseForm(input) {
|
|
|
2230
2357
|
);
|
|
2231
2358
|
} else {
|
|
2232
2359
|
optionalBoolean(prefill.hideWhenPrefilled, `${path}.props.prefill.hideWhenPrefilled`);
|
|
2233
|
-
if (typeof block.type === "string" && !
|
|
2360
|
+
if (typeof block.type === "string" && !choiceKind(block.type)) {
|
|
2234
2361
|
warnings.push({ path: `${path}.props.prefill`, message: "Only single choice, multiple choice and dropdown questions are filled in from a link." });
|
|
2235
2362
|
}
|
|
2236
2363
|
}
|
|
@@ -2261,7 +2388,7 @@ function parseForm(input) {
|
|
|
2261
2388
|
if (other !== void 0) {
|
|
2262
2389
|
if (other !== true && other !== false && !(isObject2(other) && (other.label === void 0 || typeof other.label === "string"))) {
|
|
2263
2390
|
issue(`${path}.props.other`, "Must be true, false or { label }.");
|
|
2264
|
-
} else if (other !== false && typeof block.type === "string" && !
|
|
2391
|
+
} else if (other !== false && typeof block.type === "string" && !choiceKind(block.type)) {
|
|
2265
2392
|
warnings.push({ path: `${path}.props.other`, message: 'Only single choice, multiple choice and dropdown questions take an "Other" answer.' });
|
|
2266
2393
|
}
|
|
2267
2394
|
}
|
|
@@ -2274,19 +2401,14 @@ function parseForm(input) {
|
|
|
2274
2401
|
}
|
|
2275
2402
|
}
|
|
2276
2403
|
if (typeof block.type === "string" && (block.props === void 0 || isObject2(block.props))) {
|
|
2277
|
-
|
|
2278
|
-
fieldProps(block.type, block.props ?? {}, `${path}.props`);
|
|
2404
|
+
kindProps(getFieldType(block.type)?.checkProps, block.props ?? {}, `${path}.props`);
|
|
2279
2405
|
}
|
|
2280
2406
|
if (validKey) {
|
|
2281
2407
|
if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
|
|
2282
2408
|
keys.set(block.key, block.id);
|
|
2283
2409
|
}
|
|
2284
2410
|
fieldTypes.set(block.id, typeof block.type === "string" ? block.type : "");
|
|
2285
|
-
|
|
2286
|
-
const format = isObject2(block.props) ? block.props.format : void 0;
|
|
2287
|
-
if (format === "text") calculatedLiteral.set(block.id, "string");
|
|
2288
|
-
else if (format !== "number" && format !== "percent") calculatedLiteral.set(block.id, void 0);
|
|
2289
|
-
}
|
|
2411
|
+
literals.set(block.id, typeof block.type === "string" ? literalType(block.type, isObject2(block.props) ? block.props : {}) : void 0);
|
|
2290
2412
|
if (block.type === "table" && isObject2(block.props) && Array.isArray(block.props.columns)) {
|
|
2291
2413
|
tableColumnKeys.set(
|
|
2292
2414
|
block.id,
|
|
@@ -2294,8 +2416,13 @@ function parseForm(input) {
|
|
|
2294
2416
|
);
|
|
2295
2417
|
}
|
|
2296
2418
|
} else if (block.kind === "content") {
|
|
2297
|
-
|
|
2419
|
+
const content = typeof block.type === "string" ? getContentType(block.type) : void 0;
|
|
2420
|
+
if (!content) issue(`${path}.type`, `Must be ${contentKinds()}.`);
|
|
2298
2421
|
text(block.text, `${path}.text`);
|
|
2422
|
+
if (content?.checkProps) {
|
|
2423
|
+
if (block.props === void 0 || isObject2(block.props) && isJson(block.props)) kindProps(content.checkProps, block.props ?? {}, `${path}.props`);
|
|
2424
|
+
else issue(`${path}.props`, "Must be a JSON object.");
|
|
2425
|
+
}
|
|
2299
2426
|
} else if (block.kind === "page-break") {
|
|
2300
2427
|
optionalText(block.title, `${path}.title`);
|
|
2301
2428
|
} else {
|
|
@@ -2316,10 +2443,10 @@ function parseForm(input) {
|
|
|
2316
2443
|
}
|
|
2317
2444
|
const formula = isObject2(block.props) ? block.props.formula : void 0;
|
|
2318
2445
|
if (typeof formula !== "string" || !formula.trim()) return issue(path, "A calculated field needs a formula.");
|
|
2319
|
-
const
|
|
2320
|
-
if (!
|
|
2446
|
+
const parsed2 = parseFormula(formula);
|
|
2447
|
+
if (!parsed2.ok) return issue(path, `${parsed2.message} (at character ${parsed2.at + 1})`);
|
|
2321
2448
|
const reported = /* @__PURE__ */ new Set();
|
|
2322
|
-
for (const ref of
|
|
2449
|
+
for (const ref of parsed2.refs) {
|
|
2323
2450
|
const name2 = ref.column === void 0 ? ref.key : `${ref.key}.${ref.column}`;
|
|
2324
2451
|
if (reported.has(name2)) continue;
|
|
2325
2452
|
reported.add(name2);
|
|
@@ -2425,8 +2552,8 @@ function parseForm(input) {
|
|
|
2425
2552
|
if (typeof left.field === "string" && fieldTypes.has(left.field)) {
|
|
2426
2553
|
const fieldType = fieldTypes.get(left.field);
|
|
2427
2554
|
what = `"${keyOf(left.field)}"`;
|
|
2428
|
-
type =
|
|
2429
|
-
if (
|
|
2555
|
+
type = literals.get(left.field);
|
|
2556
|
+
if (choiceKind(fieldType)) options = choices.get(left.field);
|
|
2430
2557
|
} else if (typeof left.hidden === "string" && hiddenTypes.has(left.hidden)) {
|
|
2431
2558
|
what = `hidden input "${left.hidden}"`;
|
|
2432
2559
|
type = hiddenTypes.get(left.hidden);
|
|
@@ -2540,6 +2667,40 @@ function parseForm(input) {
|
|
|
2540
2667
|
return issues.length ? fail() : { ok: true, form: input, warnings };
|
|
2541
2668
|
}
|
|
2542
2669
|
|
|
2670
|
+
// src/merge.ts
|
|
2671
|
+
function put2(record, key, value) {
|
|
2672
|
+
Object.defineProperty(record, key, { value, enumerable: true, writable: true, configurable: true });
|
|
2673
|
+
}
|
|
2674
|
+
function mergeResponses(definition, base, patch, options = {}) {
|
|
2675
|
+
const def = withHostOptions(definition, options.hostOptions);
|
|
2676
|
+
const fields = fieldMap(def);
|
|
2677
|
+
const keepable = (field) => !!field && !isCalculated(field) && !withheld(def, field);
|
|
2678
|
+
let values = /* @__PURE__ */ Object.create(null);
|
|
2679
|
+
const record = (r) => r && typeof r === "object" && !Array.isArray(r) ? r : {};
|
|
2680
|
+
const from = record(base);
|
|
2681
|
+
const changes = record(patch);
|
|
2682
|
+
for (const [id, field] of fields) {
|
|
2683
|
+
if (!keepable(field)) continue;
|
|
2684
|
+
const changed = Object.prototype.hasOwnProperty.call(changes, id);
|
|
2685
|
+
const value = changed ? own(changes, id) : own(from, id);
|
|
2686
|
+
if (value === void 0 || value === null || isEmptyValue(value, field)) continue;
|
|
2687
|
+
values[id] = value;
|
|
2688
|
+
}
|
|
2689
|
+
for (const id of Object.keys(values)) values = clearFilteredAnswers(def, values, id);
|
|
2690
|
+
const initial = {};
|
|
2691
|
+
for (const id of Object.keys(values)) if (values[id] !== void 0) put2(initial, id, values[id]);
|
|
2692
|
+
const state = createForm(def, { initialValues: initial, hidden: options.hidden, clock: () => void 0 }).getState();
|
|
2693
|
+
const keepOffPath = (options.offPath ?? (def.settings?.clearHiddenValues === false ? "keep" : "drop")) === "keep";
|
|
2694
|
+
const onPath = new Set(state.path);
|
|
2695
|
+
const out = {};
|
|
2696
|
+
for (const id of fields.keys()) {
|
|
2697
|
+
if (!keepOffPath && !onPath.has(id)) continue;
|
|
2698
|
+
const value = own(initial, id);
|
|
2699
|
+
if (value !== void 0) put2(out, id, value);
|
|
2700
|
+
}
|
|
2701
|
+
return out;
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2543
2704
|
// src/edit-link.ts
|
|
2544
2705
|
var EDIT_LINK_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
2545
2706
|
var MIN_EDIT_SECRET_BYTES = 32;
|
|
@@ -2644,19 +2805,29 @@ function withEditLinkHeaders(request, response, options = {}) {
|
|
|
2644
2805
|
}
|
|
2645
2806
|
|
|
2646
2807
|
// src/rate-limit.ts
|
|
2647
|
-
var DEFAULT_TRUSTED_IP_HEADERS = ["x-vercel-forwarded-for", "
|
|
2808
|
+
var DEFAULT_TRUSTED_IP_HEADERS = ["x-vercel-forwarded-for", "cf-connecting-ip"];
|
|
2648
2809
|
var RATE_LIMIT_DEFAULTS = {
|
|
2649
2810
|
submit: { limit: 60, windowSeconds: 600 },
|
|
2650
2811
|
progress: { limit: 900, windowSeconds: 600 },
|
|
2651
2812
|
upload: { limit: 120, windowSeconds: 600 }
|
|
2652
2813
|
};
|
|
2653
|
-
function clientIp(headers,
|
|
2814
|
+
function clientIp(headers, options = DEFAULT_TRUSTED_IP_HEADERS) {
|
|
2815
|
+
const { headers: trusted = DEFAULT_TRUSTED_IP_HEADERS, forwardedForHops: hops } = isHeaderList(options) ? { headers: options } : options;
|
|
2654
2816
|
for (const name of trusted) {
|
|
2655
2817
|
const first = headers.get(name)?.split(",")[0]?.trim();
|
|
2656
2818
|
if (first) return first;
|
|
2657
2819
|
}
|
|
2820
|
+
if (hops !== void 0) {
|
|
2821
|
+
if (!Number.isInteger(hops) || hops < 1) throw new RangeError("forwardedForHops must be a whole number of at least 1");
|
|
2822
|
+
const entries = (headers.get("x-forwarded-for") ?? "").split(",").map((entry2) => entry2.trim());
|
|
2823
|
+
const entry = entries.length >= hops ? entries[entries.length - hops] : void 0;
|
|
2824
|
+
if (entry) return entry;
|
|
2825
|
+
}
|
|
2658
2826
|
return "unknown";
|
|
2659
2827
|
}
|
|
2828
|
+
function isHeaderList(options) {
|
|
2829
|
+
return Array.isArray(options);
|
|
2830
|
+
}
|
|
2660
2831
|
function addressPrefix(ip) {
|
|
2661
2832
|
const bare = ip.replace(/^\[|\](:\d+)?$/g, "").split("%")[0].toLowerCase();
|
|
2662
2833
|
if (!bare.includes(":")) return bare;
|
|
@@ -2816,9 +2987,9 @@ function createUploadHandler(options) {
|
|
|
2816
2987
|
if (text === null) return json({ error: say.tooLong }, 413);
|
|
2817
2988
|
let body;
|
|
2818
2989
|
try {
|
|
2819
|
-
const
|
|
2820
|
-
if (!
|
|
2821
|
-
body =
|
|
2990
|
+
const parsed2 = JSON.parse(text);
|
|
2991
|
+
if (!parsed2 || typeof parsed2 !== "object" || Array.isArray(parsed2)) throw new Error("not an object");
|
|
2992
|
+
body = parsed2;
|
|
2822
2993
|
} catch {
|
|
2823
2994
|
return json({ error: say.notJson }, 400);
|
|
2824
2995
|
}
|
|
@@ -2874,7 +3045,10 @@ function createUploadHandler(options) {
|
|
|
2874
3045
|
const key = await rateLimitKey({
|
|
2875
3046
|
kind: "upload",
|
|
2876
3047
|
scope: form.scope,
|
|
2877
|
-
ip: clientIp(request.headers,
|
|
3048
|
+
ip: clientIp(request.headers, {
|
|
3049
|
+
headers: limit.trustedIpHeaders ?? DEFAULT_TRUSTED_IP_HEADERS,
|
|
3050
|
+
forwardedForHops: limit.forwardedForHops
|
|
3051
|
+
}),
|
|
2878
3052
|
secret: limit.secret
|
|
2879
3053
|
});
|
|
2880
3054
|
const allowed = await allow(
|
|
@@ -2911,6 +3085,216 @@ function createUploadHandler(options) {
|
|
|
2911
3085
|
};
|
|
2912
3086
|
}
|
|
2913
3087
|
|
|
3088
|
+
// src/hash.ts
|
|
3089
|
+
function canonicalJson(value) {
|
|
3090
|
+
return JSON.stringify(sortKeys(value));
|
|
3091
|
+
}
|
|
3092
|
+
function sortKeys(value) {
|
|
3093
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
3094
|
+
if (value && typeof value === "object") {
|
|
3095
|
+
const out = {};
|
|
3096
|
+
for (const key of Object.keys(value).sort()) {
|
|
3097
|
+
const v = value[key];
|
|
3098
|
+
if (v !== void 0) out[key] = sortKeys(v);
|
|
3099
|
+
}
|
|
3100
|
+
return out;
|
|
3101
|
+
}
|
|
3102
|
+
return value;
|
|
3103
|
+
}
|
|
3104
|
+
function fingerprint(value) {
|
|
3105
|
+
const str = canonicalJson(value);
|
|
3106
|
+
let h1 = 3735928559;
|
|
3107
|
+
let h2 = 1103547991;
|
|
3108
|
+
for (let i = 0; i < str.length; i++) {
|
|
3109
|
+
const ch = str.charCodeAt(i);
|
|
3110
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
3111
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
3112
|
+
}
|
|
3113
|
+
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
3114
|
+
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
3115
|
+
return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16).padStart(14, "0");
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
// src/submit-protocol.ts
|
|
3119
|
+
var IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
|
|
3120
|
+
function isIdempotencyKey(value) {
|
|
3121
|
+
return typeof value === "string" && IDEMPOTENCY_KEY_PATTERN.test(value);
|
|
3122
|
+
}
|
|
3123
|
+
function submitOutcome(result) {
|
|
3124
|
+
if (!isSubmitResult(result)) return "retry";
|
|
3125
|
+
if (result.ok || result.reason === "duplicate") return "delivered";
|
|
3126
|
+
if (result.reason === "invalid" || result.reason === "conflict" || result.reason === "closed") return "rejected";
|
|
3127
|
+
return "retry";
|
|
3128
|
+
}
|
|
3129
|
+
function isSubmitResult(value) {
|
|
3130
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
3131
|
+
const v = value;
|
|
3132
|
+
const stored = typeof v.id === "string" && v.id !== "" && typeof v.receivedAt === "string";
|
|
3133
|
+
if (v.ok === true) return stored;
|
|
3134
|
+
if (v.ok !== false) return false;
|
|
3135
|
+
switch (v.reason) {
|
|
3136
|
+
case "duplicate":
|
|
3137
|
+
return stored;
|
|
3138
|
+
case "invalid":
|
|
3139
|
+
return !!v.errors && typeof v.errors === "object" && !Array.isArray(v.errors);
|
|
3140
|
+
case "rate_limited":
|
|
3141
|
+
return v.retryAfterSeconds === void 0 || typeof v.retryAfterSeconds === "number" && Number.isFinite(v.retryAfterSeconds);
|
|
3142
|
+
case "conflict":
|
|
3143
|
+
case "closed":
|
|
3144
|
+
case "store_failed":
|
|
3145
|
+
return true;
|
|
3146
|
+
default:
|
|
3147
|
+
return false;
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
// src/submit.ts
|
|
3152
|
+
var INVALID_KEY = {
|
|
3153
|
+
ok: false,
|
|
3154
|
+
reason: "invalid",
|
|
3155
|
+
errors: { "": "Missing or malformed idempotency key: 16 to 128 letters, digits, - or _." }
|
|
3156
|
+
};
|
|
3157
|
+
var MALFORMED = { ok: false, reason: "invalid", errors: { "": "Malformed submission." } };
|
|
3158
|
+
var CONFLICT = { ok: false, reason: "conflict" };
|
|
3159
|
+
var parsed = /* @__PURE__ */ new WeakMap();
|
|
3160
|
+
function assertPublishable(definition) {
|
|
3161
|
+
if (typeof definition === "object" && definition !== null && parsed.has(definition)) return;
|
|
3162
|
+
const result = parseForm(definition);
|
|
3163
|
+
if (!result.ok) {
|
|
3164
|
+
throw new Error(`submitResponse: the published definition is invalid: ${result.issues.map((i) => i.message).join("; ")}`);
|
|
3165
|
+
}
|
|
3166
|
+
parsed.set(definition, true);
|
|
3167
|
+
}
|
|
3168
|
+
async function sha256Hex(text) {
|
|
3169
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)));
|
|
3170
|
+
let hex = "";
|
|
3171
|
+
for (const b of digest) hex += b.toString(16).padStart(2, "0");
|
|
3172
|
+
return hex;
|
|
3173
|
+
}
|
|
3174
|
+
function hashInput(input) {
|
|
3175
|
+
return sha256Hex(canonicalJson({ answers: input.answers, events: input.events, hidden: input.hidden }));
|
|
3176
|
+
}
|
|
3177
|
+
function isoOrNull(ms) {
|
|
3178
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return null;
|
|
3179
|
+
const date = new Date(ms);
|
|
3180
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
3181
|
+
}
|
|
3182
|
+
function isRef(ref) {
|
|
3183
|
+
if (!ref || typeof ref !== "object") return false;
|
|
3184
|
+
const r = ref;
|
|
3185
|
+
return typeof r.id === "string" && r.id !== "" && typeof r.receivedAt === "string" && typeof r.inputHash === "string" && r.inputHash !== "";
|
|
3186
|
+
}
|
|
3187
|
+
function duplicateOf(ref, inputHash, log, formId) {
|
|
3188
|
+
if (!isRef(ref)) {
|
|
3189
|
+
log(`the store answered a duplicate for form ${formId} without its id, receivedAt and inputHash`);
|
|
3190
|
+
return { ok: false, reason: "store_failed" };
|
|
3191
|
+
}
|
|
3192
|
+
if (ref.inputHash !== inputHash) return CONFLICT;
|
|
3193
|
+
return { ok: false, reason: "duplicate", id: ref.id, receivedAt: ref.receivedAt };
|
|
3194
|
+
}
|
|
3195
|
+
var NO_OWNER = { attemptHash: "" };
|
|
3196
|
+
function submissionOptions(options) {
|
|
3197
|
+
const {
|
|
3198
|
+
definition: _definition,
|
|
3199
|
+
store: _store,
|
|
3200
|
+
formId: _formId,
|
|
3201
|
+
version: _version,
|
|
3202
|
+
open: _open,
|
|
3203
|
+
rateLimit: _rateLimit,
|
|
3204
|
+
uploads: _uploads,
|
|
3205
|
+
now: _now,
|
|
3206
|
+
log: _log,
|
|
3207
|
+
...rest
|
|
3208
|
+
} = options;
|
|
3209
|
+
return rest;
|
|
3210
|
+
}
|
|
3211
|
+
async function submitResponse(input, options) {
|
|
3212
|
+
const { definition, store } = options;
|
|
3213
|
+
assertPublishable(definition);
|
|
3214
|
+
if (uploadFields(definition).length > 0 && !options.uploads) {
|
|
3215
|
+
throw new Error("submitResponse: this form has upload fields; pass `uploads` (the attempt and the upload store).");
|
|
3216
|
+
}
|
|
3217
|
+
const log = options.log ?? ((m) => console.error(`[submit] ${m}`));
|
|
3218
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return MALFORMED;
|
|
3219
|
+
if (!isIdempotencyKey(input.idempotencyKey)) return INVALID_KEY;
|
|
3220
|
+
const formId = options.formId ?? definition.id;
|
|
3221
|
+
const key = input.idempotencyKey;
|
|
3222
|
+
let hash;
|
|
3223
|
+
const inputHash = () => hash ??= hashInput(input);
|
|
3224
|
+
let existing;
|
|
3225
|
+
try {
|
|
3226
|
+
existing = await store.find(formId, key);
|
|
3227
|
+
} catch (e) {
|
|
3228
|
+
log(`find failed for form ${formId}: ${e instanceof Error ? e.message : String(e)}`);
|
|
3229
|
+
return { ok: false, reason: "store_failed" };
|
|
3230
|
+
}
|
|
3231
|
+
if (existing) return duplicateOf(existing, await inputHash(), log, formId);
|
|
3232
|
+
if (options.open === false) return { ok: false, reason: "closed" };
|
|
3233
|
+
if (options.rateLimit) {
|
|
3234
|
+
let answer = true;
|
|
3235
|
+
try {
|
|
3236
|
+
answer = await options.rateLimit();
|
|
3237
|
+
} catch (e) {
|
|
3238
|
+
log(`the rate limit could not be checked; letting the submission through: ${e instanceof Error ? e.message : String(e)}`);
|
|
3239
|
+
}
|
|
3240
|
+
if (answer === false) return { ok: false, reason: "rate_limited" };
|
|
3241
|
+
if (answer !== true && answer && typeof answer === "object") {
|
|
3242
|
+
const after = answer.retryAfterSeconds;
|
|
3243
|
+
return Number.isFinite(after) && after >= 0 ? { ok: false, reason: "rate_limited", retryAfterSeconds: Math.ceil(after) } : { ok: false, reason: "rate_limited" };
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
const checked = validateSubmission(
|
|
3247
|
+
definition,
|
|
3248
|
+
{ answers: input.answers, events: input.events, hidden: input.hidden },
|
|
3249
|
+
{ ...submissionOptions(options), complete: true }
|
|
3250
|
+
);
|
|
3251
|
+
if (!checked.ok) return { ok: false, reason: "invalid", errors: checked.errors };
|
|
3252
|
+
const submission = checked.submission;
|
|
3253
|
+
let answers = submission.answers;
|
|
3254
|
+
if (options.uploads) {
|
|
3255
|
+
try {
|
|
3256
|
+
const owner = options.uploads.owner ?? (typeof input.attemptId === "string" && input.attemptId !== "" ? { attemptHash: await attemptHash(input.attemptId) } : (
|
|
3257
|
+
// No attempt: no upload can be this visit's, so any file named is refused.
|
|
3258
|
+
NO_OWNER
|
|
3259
|
+
));
|
|
3260
|
+
const files = await checkUploads(definition, answers, { formId, owner, store: options.uploads.store });
|
|
3261
|
+
if (!files.ok) return { ok: false, reason: "invalid", errors: files.errors };
|
|
3262
|
+
answers = files.answers;
|
|
3263
|
+
} catch (e) {
|
|
3264
|
+
log(`upload check failed for form ${formId}: ${e instanceof Error ? e.message : String(e)}`);
|
|
3265
|
+
return { ok: false, reason: "store_failed" };
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
const record = {
|
|
3269
|
+
formId,
|
|
3270
|
+
idempotencyKey: key,
|
|
3271
|
+
inputHash: await inputHash(),
|
|
3272
|
+
definitionVersion: options.version ?? fingerprint(definition),
|
|
3273
|
+
receivedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
3274
|
+
clientSubmittedAt: isoOrNull(input.clientSubmittedAt),
|
|
3275
|
+
answers,
|
|
3276
|
+
hidden: submission.hidden,
|
|
3277
|
+
context: submission.context,
|
|
3278
|
+
ending: submission.ending,
|
|
3279
|
+
ignored: submission.ignored,
|
|
3280
|
+
snapshots: answerSnapshots(withHostOptions(definition, options.hostOptions), answers),
|
|
3281
|
+
uploadIds: uploadFields(definition).flatMap((field) => photoRefs(answers[field.id]).map((ref) => ref.id))
|
|
3282
|
+
};
|
|
3283
|
+
let saved;
|
|
3284
|
+
try {
|
|
3285
|
+
saved = await store.save(record);
|
|
3286
|
+
} catch (e) {
|
|
3287
|
+
log(`save failed for form ${formId}: ${e instanceof Error ? e.message : String(e)}`);
|
|
3288
|
+
return { ok: false, reason: "store_failed" };
|
|
3289
|
+
}
|
|
3290
|
+
if (saved && typeof saved === "object" && "duplicate" in saved) return duplicateOf(saved.duplicate, record.inputHash, log, formId);
|
|
3291
|
+
if (!saved || typeof saved.id !== "string" || saved.id === "") {
|
|
3292
|
+
log(`save for form ${formId} answered without an id`);
|
|
3293
|
+
return { ok: false, reason: "store_failed" };
|
|
3294
|
+
}
|
|
3295
|
+
return { ok: true, id: saved.id, receivedAt: record.receivedAt };
|
|
3296
|
+
}
|
|
3297
|
+
|
|
2914
3298
|
// src/server.ts
|
|
2915
3299
|
var MAX_EVENTS = 1e4;
|
|
2916
3300
|
var MAX_SUBMISSION_VALUES = 2e5;
|
|
@@ -2948,13 +3332,13 @@ function submissionValues(input, limit) {
|
|
|
2948
3332
|
} else count += countValues(input.answers, limit - count);
|
|
2949
3333
|
return count;
|
|
2950
3334
|
}
|
|
2951
|
-
var
|
|
3335
|
+
var MALFORMED2 = { ok: false, errors: { "": "Malformed submission." } };
|
|
2952
3336
|
var parsedDefinitions = /* @__PURE__ */ new WeakMap();
|
|
2953
3337
|
function checkDefinition(definition) {
|
|
2954
3338
|
if (typeof definition === "object" && definition !== null && parsedDefinitions.has(definition)) return null;
|
|
2955
|
-
const
|
|
2956
|
-
if (!
|
|
2957
|
-
const list =
|
|
3339
|
+
const parsed2 = parseForm(definition);
|
|
3340
|
+
if (!parsed2.ok) {
|
|
3341
|
+
const list = parsed2.issues.map((i) => i.path ? `${i.path}: ${i.message}` : i.message).join("; ");
|
|
2958
3342
|
return `Invalid form definition: ${list}`;
|
|
2959
3343
|
}
|
|
2960
3344
|
parsedDefinitions.set(definition, true);
|
|
@@ -2978,7 +3362,7 @@ function validateSubmission(published, submission, options = {}) {
|
|
|
2978
3362
|
const invalid = checkDefinition(published);
|
|
2979
3363
|
if (invalid) return { ok: false, errors: { "": invalid } };
|
|
2980
3364
|
const definition = withHostOptions(published, options.hostOptions);
|
|
2981
|
-
if (!isPlainObject(submission)) return
|
|
3365
|
+
if (!isPlainObject(submission)) return MALFORMED2;
|
|
2982
3366
|
const maxEvents = options.maxEvents ?? MAX_EVENTS;
|
|
2983
3367
|
let sent = submission;
|
|
2984
3368
|
if (Array.isArray(submission.events)) {
|
|
@@ -2996,7 +3380,7 @@ function validateSubmission(published, submission, options = {}) {
|
|
|
2996
3380
|
};
|
|
2997
3381
|
}
|
|
2998
3382
|
const input = wellFormed(sent);
|
|
2999
|
-
if (!input) return
|
|
3383
|
+
if (!input) return MALFORMED2;
|
|
3000
3384
|
const ignored = /* @__PURE__ */ new Set();
|
|
3001
3385
|
const client = coerceHidden(definition, input.hidden, { sources: ["url", "host"] });
|
|
3002
3386
|
for (const name of client.ignored) ignored.add(`hidden.${name}`);
|
|
@@ -3098,6 +3482,7 @@ function publicDefinition(definition) {
|
|
|
3098
3482
|
exports.DEFAULT_TRUSTED_IP_HEADERS = DEFAULT_TRUSTED_IP_HEADERS;
|
|
3099
3483
|
exports.EDIT_LINK_PARAM = EDIT_LINK_PARAM;
|
|
3100
3484
|
exports.EDIT_LINK_TTL_SECONDS = EDIT_LINK_TTL_SECONDS;
|
|
3485
|
+
exports.IDEMPOTENCY_KEY_PATTERN = IDEMPOTENCY_KEY_PATTERN;
|
|
3101
3486
|
exports.MAX_EVENTS = MAX_EVENTS;
|
|
3102
3487
|
exports.MAX_REPLAY_WORK = MAX_REPLAY_WORK;
|
|
3103
3488
|
exports.MAX_SUBMISSION_VALUES = MAX_SUBMISSION_VALUES;
|
|
@@ -3118,10 +3503,15 @@ exports.createEditSecretReader = createEditSecretReader;
|
|
|
3118
3503
|
exports.createUploadHandler = createUploadHandler;
|
|
3119
3504
|
exports.editLinkUrl = editLinkUrl;
|
|
3120
3505
|
exports.isEditLinkRequest = isEditLinkRequest;
|
|
3506
|
+
exports.isIdempotencyKey = isIdempotencyKey;
|
|
3507
|
+
exports.isSubmitResult = isSubmitResult;
|
|
3121
3508
|
exports.isUsableEditSecret = isUsableEditSecret;
|
|
3509
|
+
exports.mergeResponses = mergeResponses;
|
|
3122
3510
|
exports.publicDefinition = publicDefinition;
|
|
3123
3511
|
exports.rateLimitKey = rateLimitKey;
|
|
3124
3512
|
exports.signEditToken = signEditToken;
|
|
3513
|
+
exports.submitOutcome = submitOutcome;
|
|
3514
|
+
exports.submitResponse = submitResponse;
|
|
3125
3515
|
exports.uploadFields = uploadFields;
|
|
3126
3516
|
exports.uploadRules = uploadRules;
|
|
3127
3517
|
exports.validateSubmission = validateSubmission;
|