@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/dist/server.cjs CHANGED
@@ -230,6 +230,7 @@ function compactEvents(events) {
230
230
 
231
231
  // src/hidden.ts
232
232
  var MAX_HIDDEN_STRING_LENGTH = 1e3;
233
+ var DECIMAL = /^-?\d+(\.\d+)?$/;
233
234
  function coerceHiddenValue(value, type) {
234
235
  if (typeof value === "string" && value.length > MAX_HIDDEN_STRING_LENGTH) return void 0;
235
236
  if (type === "string") {
@@ -239,7 +240,7 @@ function coerceHiddenValue(value, type) {
239
240
  }
240
241
  if (type === "number") {
241
242
  if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
242
- if (typeof value !== "string" || value.trim() === "") return void 0;
243
+ if (typeof value !== "string" || !DECIMAL.test(value)) return void 0;
243
244
  const n = Number(value);
244
245
  return Number.isFinite(n) ? n : void 0;
245
246
  }
@@ -564,7 +565,7 @@ function build(definition, options, replaying = false) {
564
565
  for (const id of state.path) {
565
566
  const field = fields.get(id);
566
567
  const value = own(state.values, id);
567
- if (field && value !== void 0) out[id] = normalizeValue(value, field);
568
+ if (field && !isEmptyValue(value, field)) out[id] = normalizeValue(value, field);
568
569
  }
569
570
  return out;
570
571
  }
@@ -599,7 +600,30 @@ function replay(definition, events, options = {}) {
599
600
  }
600
601
 
601
602
  // src/parse.ts
603
+ function shown(v) {
604
+ let text;
605
+ try {
606
+ text = JSON.stringify(v);
607
+ } catch {
608
+ text = void 0;
609
+ }
610
+ text ??= typeof v === "bigint" ? `${v}n` : typeof v;
611
+ return text.length > 60 ? `${text.slice(0, 57)}...` : text;
612
+ }
602
613
  var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
614
+ var isInt = (v) => typeof v === "number" && Number.isInteger(v);
615
+ var LITERAL_TYPE = {
616
+ "short-text": "string",
617
+ "long-text": "string",
618
+ email: "string",
619
+ phone: "string",
620
+ rating: "number",
621
+ nps: "number",
622
+ "single-choice": "string",
623
+ "multi-choice": "string",
624
+ "yes-no": "boolean"
625
+ };
626
+ var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
603
627
  var OPERATORS = /* @__PURE__ */ new Set([
604
628
  "eq",
605
629
  "neq",
@@ -672,6 +696,7 @@ function parseForm(input) {
672
696
  const keys = /* @__PURE__ */ new Map();
673
697
  const fieldTypes = /* @__PURE__ */ new Map();
674
698
  const order = /* @__PURE__ */ new Map();
699
+ const choices = /* @__PURE__ */ new Map();
675
700
  input.blocks.forEach((block, i) => {
676
701
  const path = `blocks[${i}]`;
677
702
  if (!isObject(block) || typeof block.id !== "string" || !block.id) {
@@ -705,8 +730,19 @@ function parseForm(input) {
705
730
  if (!isObject(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
706
731
  else optionalText(o.label, `${at}.label`);
707
732
  });
733
+ choices.set(
734
+ block.id,
735
+ new Set(options.flatMap((o) => isObject(o) && typeof o.value === "string" ? [o.value] : []))
736
+ );
708
737
  }
709
738
  }
739
+ const { max, maxLength: maxLength2 } = isObject(block.props) ? block.props : {};
740
+ if (block.type === "rating" && max !== void 0 && !(isInt(max) && max >= 2 && max <= 10)) {
741
+ issue(`${path}.props.max`, "Must be a whole number from 2 to 10.");
742
+ }
743
+ if (maxLength2 !== void 0 && !(isInt(maxLength2) && maxLength2 > 0)) {
744
+ issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
745
+ }
710
746
  }
711
747
  if (validKey) {
712
748
  if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
@@ -723,6 +759,7 @@ function parseForm(input) {
723
759
  }
724
760
  });
725
761
  const hiddenNames = /* @__PURE__ */ new Set();
762
+ const hiddenTypes = /* @__PURE__ */ new Map();
726
763
  if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
727
764
  (Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
728
765
  if (!isObject(h) || typeof h.name !== "string" || !h.name) {
@@ -738,7 +775,9 @@ function parseForm(input) {
738
775
  issue(`hidden[${i}].type`, "Must be string, number or boolean.");
739
776
  }
740
777
  optionalBoolean(h.sensitive, `hidden[${i}].sensitive`);
778
+ optionalBoolean(h.identifying, `hidden[${i}].identifying`);
741
779
  hiddenNames.add(h.name);
780
+ if (typeof h.type === "string") hiddenTypes.set(h.name, h.type);
742
781
  });
743
782
  const endingIds = /* @__PURE__ */ new Set();
744
783
  if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
@@ -787,8 +826,45 @@ function parseForm(input) {
787
826
  if (typeof c.left.field === "string") {
788
827
  const manifest = getFieldType(fieldTypes.get(c.left.field) ?? "");
789
828
  if (manifest && OPERATORS.has(c.op) && !manifest.operators.includes(c.op)) {
790
- issue(`${path}.op`, `"${c.op}" does not apply to a ${manifest.type} field.`);
829
+ return issue(`${path}.op`, `"${c.op}" does not apply to a ${manifest.type} field.`);
830
+ }
831
+ }
832
+ checkLiterals(c.op, c.left, c.right, path);
833
+ };
834
+ const checkLiterals = (op, left, right, path) => {
835
+ if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
836
+ const literal = isObject(right) && "value" in right;
837
+ if (right !== void 0 && !literal) return;
838
+ let what = "the left side";
839
+ let type;
840
+ let options;
841
+ if (typeof left.field === "string" && fieldTypes.has(left.field)) {
842
+ const fieldType = fieldTypes.get(left.field);
843
+ what = `"${keyOf(left.field)}"`;
844
+ type = LITERAL_TYPE[fieldType];
845
+ if (fieldType === "single-choice" || fieldType === "multi-choice") options = choices.get(left.field);
846
+ } else if (typeof left.hidden === "string" && hiddenTypes.has(left.hidden)) {
847
+ what = `hidden input "${left.hidden}"`;
848
+ type = hiddenTypes.get(left.hidden);
849
+ }
850
+ const outcome = op === "neq" ? "always true" : "never true";
851
+ let values;
852
+ if (op === "in") {
853
+ if (!literal || !Array.isArray(right.value)) {
854
+ return void warnings.push({ path: `${path}.right`, message: "`in` needs a list of values; it never matches." });
855
+ }
856
+ values = right.value;
857
+ } else if (!literal) return;
858
+ else values = [right.value];
859
+ if (ORDERING.has(op)) {
860
+ if (type !== void 0 && type !== "number") {
861
+ return void warnings.push({ path: `${path}.op`, message: `"${op}" compares numbers, but ${what} holds a ${type}; it is never true.` });
791
862
  }
863
+ type = "number";
864
+ }
865
+ for (const v of values) {
866
+ 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;
867
+ if (message) warnings.push({ path: `${path}.right`, message });
792
868
  }
793
869
  };
794
870
  const keyOf = (id) => [...keys].find(([, v]) => v === id)?.[0] ?? id;
@@ -865,6 +941,18 @@ function parseForm(input) {
865
941
  }
866
942
  }
867
943
  });
944
+ const extra = [["description", input.description]];
945
+ (Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
946
+ if (isObject(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
947
+ });
948
+ for (const [path, t] of extra) {
949
+ if (typeof t !== "string") continue;
950
+ for (const ref of recallRefs(t)) {
951
+ if (ref.kind === "hidden" ? !hiddenNames.has(ref.name) : !keys.has(ref.name)) {
952
+ warnings.push({ path, message: `Recalls unknown ${ref.kind === "hidden" ? "hidden input" : "field"} "${ref.name}"; it shows its fallback text.` });
953
+ }
954
+ }
955
+ }
868
956
  return issues.length ? fail() : { ok: true, form: input, warnings };
869
957
  }
870
958
 
@@ -901,18 +989,16 @@ function validateSubmission(definition, submission, options = {}) {
901
989
  if (invalid) return { ok: false, errors: { "": invalid } };
902
990
  const input = wellFormed(submission);
903
991
  if (!input) return MALFORMED;
904
- const ignored = [];
992
+ const ignored = /* @__PURE__ */ new Set();
905
993
  const client = coerceHidden(definition, input.hidden, { sources: ["url", "host"] });
906
- for (const name of client.ignored) ignored.push(`hidden.${name}`);
994
+ for (const name of client.ignored) ignored.add(`hidden.${name}`);
907
995
  const hidden = client.values;
908
996
  const context = coerceHidden(definition, options.context, { sources: ["server"] }).values;
909
997
  const fields = fieldMap(definition);
910
- const dropped = /* @__PURE__ */ new Set();
911
998
  const accept = (id) => {
912
- if (!fields.has(id)) return void ignored.push(id);
999
+ if (!fields.has(id)) return void ignored.add(id);
913
1000
  if (!withheld(definition, fields.get(id))) return true;
914
- if (!dropped.has(id)) ignored.push(id);
915
- dropped.add(id);
1001
+ ignored.add(id);
916
1002
  return false;
917
1003
  };
918
1004
  const engineHidden = { ...hidden, ...context };
@@ -965,15 +1051,25 @@ function validateSubmission(definition, submission, options = {}) {
965
1051
  if (message === null) answers[id] = normalizeValue(value, field);
966
1052
  }
967
1053
  }
1054
+ const store = (values, report) => {
1055
+ if (definition.settings?.anonymous !== true) return values;
1056
+ const identifying = new Set((definition.hidden ?? []).filter((h) => h.identifying === true).map((h) => h.name));
1057
+ const kept = {};
1058
+ for (const [name, value] of Object.entries(values)) {
1059
+ if (!identifying.has(name)) kept[name] = value;
1060
+ else if (report) ignored.add(`hidden.${name}`);
1061
+ }
1062
+ return kept;
1063
+ };
968
1064
  return {
969
1065
  ok: true,
970
1066
  submission: {
971
1067
  status: complete ? "completed" : "draft",
972
1068
  answers,
973
- hidden,
974
- context,
1069
+ hidden: store(hidden, true),
1070
+ context: store(context, false),
975
1071
  ending: complete ? state.ending : null,
976
- ignored
1072
+ ignored: [...ignored]
977
1073
  }
978
1074
  };
979
1075
  }