@marktiderman/genesis-forms 0.1.0 → 0.2.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 +8 -4
- package/dist/{engine-LsPRTzt3.d.cts → engine-Bj8-mMVb.d.cts} +6 -6
- package/dist/{engine-Cp5t2_Am.d.ts → engine-Coc-gQIB.d.ts} +6 -6
- package/dist/index.cjs +95 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -23
- package/dist/index.d.ts +30 -23
- package/dist/index.js +95 -5
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +108 -12
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +11 -11
- package/dist/server.d.ts +11 -11
- package/dist/server.js +108 -12
- package/dist/server.js.map +1 -1
- package/dist/testing.cjs +108 -12
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +3 -3
- package/dist/testing.d.ts +3 -3
- package/dist/testing.js +108 -12
- package/dist/testing.js.map +1 -1
- package/dist/{validate-j8Z188mF.d.cts → validate-BbackAjB.d.cts} +8 -1
- package/dist/{validate-j8Z188mF.d.ts → validate-BbackAjB.d.ts} +8 -1
- package/package.json +3 -3
package/dist/server.d.cts
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import { J as Json, f as FormEvent, e as FieldValidator, a as FormDefinition } from './validate-
|
|
1
|
+
import { J as Json, f as FormEvent, e as FieldValidator, a as FormDefinition } from './validate-BbackAjB.cjs';
|
|
2
2
|
|
|
3
3
|
interface SubmissionInput {
|
|
4
4
|
/** Answers keyed by field id. Ignored when `events` is given. */
|
|
5
|
-
answers?: Record<string, Json
|
|
5
|
+
answers?: Record<string, Json> | undefined;
|
|
6
6
|
/** The respondent's event log. Replayed instead of trusting `answers`. */
|
|
7
|
-
events?: FormEvent[];
|
|
7
|
+
events?: FormEvent[] | undefined;
|
|
8
8
|
/** URL and host inputs sent by the client. Server inputs here are ignored. */
|
|
9
|
-
hidden?: Record<string, Json
|
|
9
|
+
hidden?: Record<string, Json> | undefined;
|
|
10
10
|
}
|
|
11
11
|
interface SubmissionOptions {
|
|
12
12
|
/** Values for `source: "server"` inputs, resolved by the server (class, coach, user id…). */
|
|
13
|
-
context?: Record<string, Json
|
|
13
|
+
context?: Record<string, Json> | undefined;
|
|
14
14
|
/** `false` checks a draft: formats only, nothing required. Default true. */
|
|
15
|
-
complete?: boolean;
|
|
16
|
-
validators?: Record<string, FieldValidator
|
|
15
|
+
complete?: boolean | undefined;
|
|
16
|
+
validators?: Record<string, FieldValidator> | undefined;
|
|
17
17
|
/** Most events accepted, counted after `compactEvents`. Default 10 000. */
|
|
18
|
-
maxEvents?: number;
|
|
18
|
+
maxEvents?: number | undefined;
|
|
19
19
|
}
|
|
20
20
|
interface CheckedSubmission {
|
|
21
21
|
status: "draft" | "completed";
|
|
22
22
|
/** Visible answers only, keyed by field id (kept hidden answers too, with `clearHiddenValues: false`). */
|
|
23
23
|
answers: Record<string, Json>;
|
|
24
|
-
/** Untrusted URL and host inputs, coerced to their declared types. */
|
|
24
|
+
/** Untrusted URL and host inputs, coerced to their declared types. Identifying ones are left out on anonymous forms. */
|
|
25
25
|
hidden: Record<string, Json>;
|
|
26
|
-
/** Trusted server inputs. */
|
|
26
|
+
/** Trusted server inputs. Identifying ones are left out on anonymous forms. */
|
|
27
27
|
context: Record<string, Json>;
|
|
28
28
|
ending: string | null;
|
|
29
|
-
/** Client-sent keys that were dropped: unknown fields, server inputs, undeclared hidden names. */
|
|
29
|
+
/** Client-sent keys that were dropped, each once: unknown fields, server inputs, undeclared or withheld hidden names. */
|
|
30
30
|
ignored: string[];
|
|
31
31
|
}
|
|
32
32
|
type SubmissionResult = {
|
package/dist/server.d.ts
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import { J as Json, f as FormEvent, e as FieldValidator, a as FormDefinition } from './validate-
|
|
1
|
+
import { J as Json, f as FormEvent, e as FieldValidator, a as FormDefinition } from './validate-BbackAjB.js';
|
|
2
2
|
|
|
3
3
|
interface SubmissionInput {
|
|
4
4
|
/** Answers keyed by field id. Ignored when `events` is given. */
|
|
5
|
-
answers?: Record<string, Json
|
|
5
|
+
answers?: Record<string, Json> | undefined;
|
|
6
6
|
/** The respondent's event log. Replayed instead of trusting `answers`. */
|
|
7
|
-
events?: FormEvent[];
|
|
7
|
+
events?: FormEvent[] | undefined;
|
|
8
8
|
/** URL and host inputs sent by the client. Server inputs here are ignored. */
|
|
9
|
-
hidden?: Record<string, Json
|
|
9
|
+
hidden?: Record<string, Json> | undefined;
|
|
10
10
|
}
|
|
11
11
|
interface SubmissionOptions {
|
|
12
12
|
/** Values for `source: "server"` inputs, resolved by the server (class, coach, user id…). */
|
|
13
|
-
context?: Record<string, Json
|
|
13
|
+
context?: Record<string, Json> | undefined;
|
|
14
14
|
/** `false` checks a draft: formats only, nothing required. Default true. */
|
|
15
|
-
complete?: boolean;
|
|
16
|
-
validators?: Record<string, FieldValidator
|
|
15
|
+
complete?: boolean | undefined;
|
|
16
|
+
validators?: Record<string, FieldValidator> | undefined;
|
|
17
17
|
/** Most events accepted, counted after `compactEvents`. Default 10 000. */
|
|
18
|
-
maxEvents?: number;
|
|
18
|
+
maxEvents?: number | undefined;
|
|
19
19
|
}
|
|
20
20
|
interface CheckedSubmission {
|
|
21
21
|
status: "draft" | "completed";
|
|
22
22
|
/** Visible answers only, keyed by field id (kept hidden answers too, with `clearHiddenValues: false`). */
|
|
23
23
|
answers: Record<string, Json>;
|
|
24
|
-
/** Untrusted URL and host inputs, coerced to their declared types. */
|
|
24
|
+
/** Untrusted URL and host inputs, coerced to their declared types. Identifying ones are left out on anonymous forms. */
|
|
25
25
|
hidden: Record<string, Json>;
|
|
26
|
-
/** Trusted server inputs. */
|
|
26
|
+
/** Trusted server inputs. Identifying ones are left out on anonymous forms. */
|
|
27
27
|
context: Record<string, Json>;
|
|
28
28
|
ending: string | null;
|
|
29
|
-
/** Client-sent keys that were dropped: unknown fields, server inputs, undeclared hidden names. */
|
|
29
|
+
/** Client-sent keys that were dropped, each once: unknown fields, server inputs, undeclared or withheld hidden names. */
|
|
30
30
|
ignored: string[];
|
|
31
31
|
}
|
|
32
32
|
type SubmissionResult = {
|
package/dist/server.js
CHANGED
|
@@ -228,6 +228,7 @@ function compactEvents(events) {
|
|
|
228
228
|
|
|
229
229
|
// src/hidden.ts
|
|
230
230
|
var MAX_HIDDEN_STRING_LENGTH = 1e3;
|
|
231
|
+
var DECIMAL = /^-?\d+(\.\d+)?$/;
|
|
231
232
|
function coerceHiddenValue(value, type) {
|
|
232
233
|
if (typeof value === "string" && value.length > MAX_HIDDEN_STRING_LENGTH) return void 0;
|
|
233
234
|
if (type === "string") {
|
|
@@ -237,7 +238,7 @@ function coerceHiddenValue(value, type) {
|
|
|
237
238
|
}
|
|
238
239
|
if (type === "number") {
|
|
239
240
|
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
240
|
-
if (typeof value !== "string" ||
|
|
241
|
+
if (typeof value !== "string" || !DECIMAL.test(value)) return void 0;
|
|
241
242
|
const n = Number(value);
|
|
242
243
|
return Number.isFinite(n) ? n : void 0;
|
|
243
244
|
}
|
|
@@ -562,7 +563,7 @@ function build(definition, options, replaying = false) {
|
|
|
562
563
|
for (const id of state.path) {
|
|
563
564
|
const field = fields.get(id);
|
|
564
565
|
const value = own(state.values, id);
|
|
565
|
-
if (field && value
|
|
566
|
+
if (field && !isEmptyValue(value, field)) out[id] = normalizeValue(value, field);
|
|
566
567
|
}
|
|
567
568
|
return out;
|
|
568
569
|
}
|
|
@@ -597,7 +598,30 @@ function replay(definition, events, options = {}) {
|
|
|
597
598
|
}
|
|
598
599
|
|
|
599
600
|
// src/parse.ts
|
|
601
|
+
function shown(v) {
|
|
602
|
+
let text;
|
|
603
|
+
try {
|
|
604
|
+
text = JSON.stringify(v);
|
|
605
|
+
} catch {
|
|
606
|
+
text = void 0;
|
|
607
|
+
}
|
|
608
|
+
text ??= typeof v === "bigint" ? `${v}n` : typeof v;
|
|
609
|
+
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
|
|
610
|
+
}
|
|
600
611
|
var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
612
|
+
var isInt = (v) => typeof v === "number" && Number.isInteger(v);
|
|
613
|
+
var LITERAL_TYPE = {
|
|
614
|
+
"short-text": "string",
|
|
615
|
+
"long-text": "string",
|
|
616
|
+
email: "string",
|
|
617
|
+
phone: "string",
|
|
618
|
+
rating: "number",
|
|
619
|
+
nps: "number",
|
|
620
|
+
"single-choice": "string",
|
|
621
|
+
"multi-choice": "string",
|
|
622
|
+
"yes-no": "boolean"
|
|
623
|
+
};
|
|
624
|
+
var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
|
|
601
625
|
var OPERATORS = /* @__PURE__ */ new Set([
|
|
602
626
|
"eq",
|
|
603
627
|
"neq",
|
|
@@ -670,6 +694,7 @@ function parseForm(input) {
|
|
|
670
694
|
const keys = /* @__PURE__ */ new Map();
|
|
671
695
|
const fieldTypes = /* @__PURE__ */ new Map();
|
|
672
696
|
const order = /* @__PURE__ */ new Map();
|
|
697
|
+
const choices = /* @__PURE__ */ new Map();
|
|
673
698
|
input.blocks.forEach((block, i) => {
|
|
674
699
|
const path = `blocks[${i}]`;
|
|
675
700
|
if (!isObject(block) || typeof block.id !== "string" || !block.id) {
|
|
@@ -703,8 +728,19 @@ function parseForm(input) {
|
|
|
703
728
|
if (!isObject(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
|
|
704
729
|
else optionalText(o.label, `${at}.label`);
|
|
705
730
|
});
|
|
731
|
+
choices.set(
|
|
732
|
+
block.id,
|
|
733
|
+
new Set(options.flatMap((o) => isObject(o) && typeof o.value === "string" ? [o.value] : []))
|
|
734
|
+
);
|
|
706
735
|
}
|
|
707
736
|
}
|
|
737
|
+
const { max, maxLength: maxLength2 } = isObject(block.props) ? block.props : {};
|
|
738
|
+
if (block.type === "rating" && max !== void 0 && !(isInt(max) && max >= 2 && max <= 10)) {
|
|
739
|
+
issue(`${path}.props.max`, "Must be a whole number from 2 to 10.");
|
|
740
|
+
}
|
|
741
|
+
if (maxLength2 !== void 0 && !(isInt(maxLength2) && maxLength2 > 0)) {
|
|
742
|
+
issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
|
|
743
|
+
}
|
|
708
744
|
}
|
|
709
745
|
if (validKey) {
|
|
710
746
|
if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
|
|
@@ -721,6 +757,7 @@ function parseForm(input) {
|
|
|
721
757
|
}
|
|
722
758
|
});
|
|
723
759
|
const hiddenNames = /* @__PURE__ */ new Set();
|
|
760
|
+
const hiddenTypes = /* @__PURE__ */ new Map();
|
|
724
761
|
if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
|
|
725
762
|
(Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
|
|
726
763
|
if (!isObject(h) || typeof h.name !== "string" || !h.name) {
|
|
@@ -736,7 +773,9 @@ function parseForm(input) {
|
|
|
736
773
|
issue(`hidden[${i}].type`, "Must be string, number or boolean.");
|
|
737
774
|
}
|
|
738
775
|
optionalBoolean(h.sensitive, `hidden[${i}].sensitive`);
|
|
776
|
+
optionalBoolean(h.identifying, `hidden[${i}].identifying`);
|
|
739
777
|
hiddenNames.add(h.name);
|
|
778
|
+
if (typeof h.type === "string") hiddenTypes.set(h.name, h.type);
|
|
740
779
|
});
|
|
741
780
|
const endingIds = /* @__PURE__ */ new Set();
|
|
742
781
|
if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
|
|
@@ -785,8 +824,45 @@ function parseForm(input) {
|
|
|
785
824
|
if (typeof c.left.field === "string") {
|
|
786
825
|
const manifest = getFieldType(fieldTypes.get(c.left.field) ?? "");
|
|
787
826
|
if (manifest && OPERATORS.has(c.op) && !manifest.operators.includes(c.op)) {
|
|
788
|
-
issue(`${path}.op`, `"${c.op}" does not apply to a ${manifest.type} field.`);
|
|
827
|
+
return issue(`${path}.op`, `"${c.op}" does not apply to a ${manifest.type} field.`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
checkLiterals(c.op, c.left, c.right, path);
|
|
831
|
+
};
|
|
832
|
+
const checkLiterals = (op, left, right, path) => {
|
|
833
|
+
if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
|
|
834
|
+
const literal = isObject(right) && "value" in right;
|
|
835
|
+
if (right !== void 0 && !literal) return;
|
|
836
|
+
let what = "the left side";
|
|
837
|
+
let type;
|
|
838
|
+
let options;
|
|
839
|
+
if (typeof left.field === "string" && fieldTypes.has(left.field)) {
|
|
840
|
+
const fieldType = fieldTypes.get(left.field);
|
|
841
|
+
what = `"${keyOf(left.field)}"`;
|
|
842
|
+
type = LITERAL_TYPE[fieldType];
|
|
843
|
+
if (fieldType === "single-choice" || fieldType === "multi-choice") options = choices.get(left.field);
|
|
844
|
+
} else if (typeof left.hidden === "string" && hiddenTypes.has(left.hidden)) {
|
|
845
|
+
what = `hidden input "${left.hidden}"`;
|
|
846
|
+
type = hiddenTypes.get(left.hidden);
|
|
847
|
+
}
|
|
848
|
+
const outcome = op === "neq" ? "always true" : "never true";
|
|
849
|
+
let values;
|
|
850
|
+
if (op === "in") {
|
|
851
|
+
if (!literal || !Array.isArray(right.value)) {
|
|
852
|
+
return void warnings.push({ path: `${path}.right`, message: "`in` needs a list of values; it never matches." });
|
|
853
|
+
}
|
|
854
|
+
values = right.value;
|
|
855
|
+
} else if (!literal) return;
|
|
856
|
+
else values = [right.value];
|
|
857
|
+
if (ORDERING.has(op)) {
|
|
858
|
+
if (type !== void 0 && type !== "number") {
|
|
859
|
+
return void warnings.push({ path: `${path}.op`, message: `"${op}" compares numbers, but ${what} holds a ${type}; it is never true.` });
|
|
789
860
|
}
|
|
861
|
+
type = "number";
|
|
862
|
+
}
|
|
863
|
+
for (const v of values) {
|
|
864
|
+
const message = type !== void 0 && typeof v !== type ? `${what} holds a ${type}, but ${shown(v)} is not one; this comparison is ${outcome}.` : options && typeof v === "string" && !options.has(v) ? `${shown(v)} is not an option of ${what}; this comparison is ${outcome}.` : null;
|
|
865
|
+
if (message) warnings.push({ path: `${path}.right`, message });
|
|
790
866
|
}
|
|
791
867
|
};
|
|
792
868
|
const keyOf = (id) => [...keys].find(([, v]) => v === id)?.[0] ?? id;
|
|
@@ -863,6 +939,18 @@ function parseForm(input) {
|
|
|
863
939
|
}
|
|
864
940
|
}
|
|
865
941
|
});
|
|
942
|
+
const extra = [["description", input.description]];
|
|
943
|
+
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
944
|
+
if (isObject(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
|
|
945
|
+
});
|
|
946
|
+
for (const [path, t] of extra) {
|
|
947
|
+
if (typeof t !== "string") continue;
|
|
948
|
+
for (const ref of recallRefs(t)) {
|
|
949
|
+
if (ref.kind === "hidden" ? !hiddenNames.has(ref.name) : !keys.has(ref.name)) {
|
|
950
|
+
warnings.push({ path, message: `Recalls unknown ${ref.kind === "hidden" ? "hidden input" : "field"} "${ref.name}"; it shows its fallback text.` });
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
866
954
|
return issues.length ? fail() : { ok: true, form: input, warnings };
|
|
867
955
|
}
|
|
868
956
|
|
|
@@ -899,18 +987,16 @@ function validateSubmission(definition, submission, options = {}) {
|
|
|
899
987
|
if (invalid) return { ok: false, errors: { "": invalid } };
|
|
900
988
|
const input = wellFormed(submission);
|
|
901
989
|
if (!input) return MALFORMED;
|
|
902
|
-
const ignored =
|
|
990
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
903
991
|
const client = coerceHidden(definition, input.hidden, { sources: ["url", "host"] });
|
|
904
|
-
for (const name of client.ignored) ignored.
|
|
992
|
+
for (const name of client.ignored) ignored.add(`hidden.${name}`);
|
|
905
993
|
const hidden = client.values;
|
|
906
994
|
const context = coerceHidden(definition, options.context, { sources: ["server"] }).values;
|
|
907
995
|
const fields = fieldMap(definition);
|
|
908
|
-
const dropped = /* @__PURE__ */ new Set();
|
|
909
996
|
const accept = (id) => {
|
|
910
|
-
if (!fields.has(id)) return void ignored.
|
|
997
|
+
if (!fields.has(id)) return void ignored.add(id);
|
|
911
998
|
if (!withheld(definition, fields.get(id))) return true;
|
|
912
|
-
|
|
913
|
-
dropped.add(id);
|
|
999
|
+
ignored.add(id);
|
|
914
1000
|
return false;
|
|
915
1001
|
};
|
|
916
1002
|
const engineHidden = { ...hidden, ...context };
|
|
@@ -963,15 +1049,25 @@ function validateSubmission(definition, submission, options = {}) {
|
|
|
963
1049
|
if (message === null) answers[id] = normalizeValue(value, field);
|
|
964
1050
|
}
|
|
965
1051
|
}
|
|
1052
|
+
const store = (values, report) => {
|
|
1053
|
+
if (definition.settings?.anonymous !== true) return values;
|
|
1054
|
+
const identifying = new Set((definition.hidden ?? []).filter((h) => h.identifying === true).map((h) => h.name));
|
|
1055
|
+
const kept = {};
|
|
1056
|
+
for (const [name, value] of Object.entries(values)) {
|
|
1057
|
+
if (!identifying.has(name)) kept[name] = value;
|
|
1058
|
+
else if (report) ignored.add(`hidden.${name}`);
|
|
1059
|
+
}
|
|
1060
|
+
return kept;
|
|
1061
|
+
};
|
|
966
1062
|
return {
|
|
967
1063
|
ok: true,
|
|
968
1064
|
submission: {
|
|
969
1065
|
status: complete ? "completed" : "draft",
|
|
970
1066
|
answers,
|
|
971
|
-
hidden,
|
|
972
|
-
context,
|
|
1067
|
+
hidden: store(hidden, true),
|
|
1068
|
+
context: store(context, false),
|
|
973
1069
|
ending: complete ? state.ending : null,
|
|
974
|
-
ignored
|
|
1070
|
+
ignored: [...ignored]
|
|
975
1071
|
}
|
|
976
1072
|
};
|
|
977
1073
|
}
|