@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/testing.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
  }
@@ -772,7 +773,30 @@ function coverage(form, opts = {}) {
772
773
  }
773
774
 
774
775
  // src/parse.ts
776
+ function shown(v) {
777
+ let text;
778
+ try {
779
+ text = JSON.stringify(v);
780
+ } catch {
781
+ text = void 0;
782
+ }
783
+ text ??= typeof v === "bigint" ? `${v}n` : typeof v;
784
+ return text.length > 60 ? `${text.slice(0, 57)}...` : text;
785
+ }
775
786
  var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
787
+ var isInt = (v) => typeof v === "number" && Number.isInteger(v);
788
+ var LITERAL_TYPE = {
789
+ "short-text": "string",
790
+ "long-text": "string",
791
+ email: "string",
792
+ phone: "string",
793
+ rating: "number",
794
+ nps: "number",
795
+ "single-choice": "string",
796
+ "multi-choice": "string",
797
+ "yes-no": "boolean"
798
+ };
799
+ var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
776
800
  var OPERATORS = /* @__PURE__ */ new Set([
777
801
  "eq",
778
802
  "neq",
@@ -845,6 +869,7 @@ function parseForm(input) {
845
869
  const keys = /* @__PURE__ */ new Map();
846
870
  const fieldTypes = /* @__PURE__ */ new Map();
847
871
  const order = /* @__PURE__ */ new Map();
872
+ const choices = /* @__PURE__ */ new Map();
848
873
  input.blocks.forEach((block, i) => {
849
874
  const path = `blocks[${i}]`;
850
875
  if (!isObject(block) || typeof block.id !== "string" || !block.id) {
@@ -878,8 +903,19 @@ function parseForm(input) {
878
903
  if (!isObject(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
879
904
  else optionalText(o.label, `${at}.label`);
880
905
  });
906
+ choices.set(
907
+ block.id,
908
+ new Set(options.flatMap((o) => isObject(o) && typeof o.value === "string" ? [o.value] : []))
909
+ );
881
910
  }
882
911
  }
912
+ const { max, maxLength: maxLength2 } = isObject(block.props) ? block.props : {};
913
+ if (block.type === "rating" && max !== void 0 && !(isInt(max) && max >= 2 && max <= 10)) {
914
+ issue(`${path}.props.max`, "Must be a whole number from 2 to 10.");
915
+ }
916
+ if (maxLength2 !== void 0 && !(isInt(maxLength2) && maxLength2 > 0)) {
917
+ issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
918
+ }
883
919
  }
884
920
  if (validKey) {
885
921
  if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
@@ -896,6 +932,7 @@ function parseForm(input) {
896
932
  }
897
933
  });
898
934
  const hiddenNames = /* @__PURE__ */ new Set();
935
+ const hiddenTypes = /* @__PURE__ */ new Map();
899
936
  if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
900
937
  (Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
901
938
  if (!isObject(h) || typeof h.name !== "string" || !h.name) {
@@ -911,7 +948,9 @@ function parseForm(input) {
911
948
  issue(`hidden[${i}].type`, "Must be string, number or boolean.");
912
949
  }
913
950
  optionalBoolean(h.sensitive, `hidden[${i}].sensitive`);
951
+ optionalBoolean(h.identifying, `hidden[${i}].identifying`);
914
952
  hiddenNames.add(h.name);
953
+ if (typeof h.type === "string") hiddenTypes.set(h.name, h.type);
915
954
  });
916
955
  const endingIds = /* @__PURE__ */ new Set();
917
956
  if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
@@ -960,8 +999,45 @@ function parseForm(input) {
960
999
  if (typeof c.left.field === "string") {
961
1000
  const manifest = getFieldType(fieldTypes.get(c.left.field) ?? "");
962
1001
  if (manifest && OPERATORS.has(c.op) && !manifest.operators.includes(c.op)) {
963
- issue(`${path}.op`, `"${c.op}" does not apply to a ${manifest.type} field.`);
1002
+ return issue(`${path}.op`, `"${c.op}" does not apply to a ${manifest.type} field.`);
1003
+ }
1004
+ }
1005
+ checkLiterals(c.op, c.left, c.right, path);
1006
+ };
1007
+ const checkLiterals = (op, left, right, path) => {
1008
+ if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
1009
+ const literal = isObject(right) && "value" in right;
1010
+ if (right !== void 0 && !literal) return;
1011
+ let what = "the left side";
1012
+ let type;
1013
+ let options;
1014
+ if (typeof left.field === "string" && fieldTypes.has(left.field)) {
1015
+ const fieldType = fieldTypes.get(left.field);
1016
+ what = `"${keyOf(left.field)}"`;
1017
+ type = LITERAL_TYPE[fieldType];
1018
+ if (fieldType === "single-choice" || fieldType === "multi-choice") options = choices.get(left.field);
1019
+ } else if (typeof left.hidden === "string" && hiddenTypes.has(left.hidden)) {
1020
+ what = `hidden input "${left.hidden}"`;
1021
+ type = hiddenTypes.get(left.hidden);
1022
+ }
1023
+ const outcome = op === "neq" ? "always true" : "never true";
1024
+ let values;
1025
+ if (op === "in") {
1026
+ if (!literal || !Array.isArray(right.value)) {
1027
+ return void warnings.push({ path: `${path}.right`, message: "`in` needs a list of values; it never matches." });
1028
+ }
1029
+ values = right.value;
1030
+ } else if (!literal) return;
1031
+ else values = [right.value];
1032
+ if (ORDERING.has(op)) {
1033
+ if (type !== void 0 && type !== "number") {
1034
+ return void warnings.push({ path: `${path}.op`, message: `"${op}" compares numbers, but ${what} holds a ${type}; it is never true.` });
964
1035
  }
1036
+ type = "number";
1037
+ }
1038
+ for (const v of values) {
1039
+ 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;
1040
+ if (message) warnings.push({ path: `${path}.right`, message });
965
1041
  }
966
1042
  };
967
1043
  const keyOf = (id) => [...keys].find(([, v]) => v === id)?.[0] ?? id;
@@ -1038,6 +1114,18 @@ function parseForm(input) {
1038
1114
  }
1039
1115
  }
1040
1116
  });
1117
+ const extra = [["description", input.description]];
1118
+ (Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
1119
+ if (isObject(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
1120
+ });
1121
+ for (const [path, t] of extra) {
1122
+ if (typeof t !== "string") continue;
1123
+ for (const ref of recallRefs(t)) {
1124
+ if (ref.kind === "hidden" ? !hiddenNames.has(ref.name) : !keys.has(ref.name)) {
1125
+ warnings.push({ path, message: `Recalls unknown ${ref.kind === "hidden" ? "hidden input" : "field"} "${ref.name}"; it shows its fallback text.` });
1126
+ }
1127
+ }
1128
+ }
1041
1129
  return issues.length ? fail() : { ok: true, form: input, warnings };
1042
1130
  }
1043
1131
 
@@ -1074,18 +1162,16 @@ function validateSubmission(definition, submission, options = {}) {
1074
1162
  if (invalid) return { ok: false, errors: { "": invalid } };
1075
1163
  const input = wellFormed(submission);
1076
1164
  if (!input) return MALFORMED;
1077
- const ignored = [];
1165
+ const ignored = /* @__PURE__ */ new Set();
1078
1166
  const client = coerceHidden(definition, input.hidden, { sources: ["url", "host"] });
1079
- for (const name of client.ignored) ignored.push(`hidden.${name}`);
1167
+ for (const name of client.ignored) ignored.add(`hidden.${name}`);
1080
1168
  const hidden = client.values;
1081
1169
  const context = coerceHidden(definition, options.context, { sources: ["server"] }).values;
1082
1170
  const fields = fieldMap(definition);
1083
- const dropped = /* @__PURE__ */ new Set();
1084
1171
  const accept = (id) => {
1085
- if (!fields.has(id)) return void ignored.push(id);
1172
+ if (!fields.has(id)) return void ignored.add(id);
1086
1173
  if (!withheld(definition, fields.get(id))) return true;
1087
- if (!dropped.has(id)) ignored.push(id);
1088
- dropped.add(id);
1174
+ ignored.add(id);
1089
1175
  return false;
1090
1176
  };
1091
1177
  const engineHidden = { ...hidden, ...context };
@@ -1138,15 +1224,25 @@ function validateSubmission(definition, submission, options = {}) {
1138
1224
  if (message === null) answers[id] = normalizeValue(value, field);
1139
1225
  }
1140
1226
  }
1227
+ const store = (values, report) => {
1228
+ if (definition.settings?.anonymous !== true) return values;
1229
+ const identifying = new Set((definition.hidden ?? []).filter((h) => h.identifying === true).map((h) => h.name));
1230
+ const kept = {};
1231
+ for (const [name, value] of Object.entries(values)) {
1232
+ if (!identifying.has(name)) kept[name] = value;
1233
+ else if (report) ignored.add(`hidden.${name}`);
1234
+ }
1235
+ return kept;
1236
+ };
1141
1237
  return {
1142
1238
  ok: true,
1143
1239
  submission: {
1144
1240
  status: complete ? "completed" : "draft",
1145
1241
  answers,
1146
- hidden,
1147
- context,
1242
+ hidden: store(hidden, true),
1243
+ context: store(context, false),
1148
1244
  ending: complete ? state.ending : null,
1149
- ignored
1245
+ ignored: [...ignored]
1150
1246
  }
1151
1247
  };
1152
1248
  }