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