@embeddables/forms 0.2.1 → 0.3.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.
@@ -72,9 +72,9 @@ function validateValue({ field, value, values, pattern, validator }) {
72
72
  const isBlank = isAbsent || value === "" || Array.isArray(value) && value.length === 0;
73
73
  if (rules?.required === true && isBlank) messages.push(`${field.label} is required`);
74
74
  if (isAbsent) return messages;
75
- if (!FIELD_TYPE_PREDICATES[field.type](value)) messages.push(`${field.label} expects ${/^[aeiou]/.test(field.type) ? "an" : "a"} ${field.type} value`);
75
+ if (!FIELD_TYPE_PREDICATES[field.value_type](value)) messages.push(`${field.label} expects ${/^[aeiou]/.test(field.value_type) ? "an" : "a"} ${field.value_type} value`);
76
76
  if (typeof value === "string") {
77
- if (field.type === "email" && value !== "" && !EMAIL_PATTERN.test(value)) messages.push(`${field.label} must be a valid email address`);
77
+ if (field.value_type === "email" && value !== "" && !EMAIL_PATTERN.test(value)) messages.push(`${field.label} must be a valid email address`);
78
78
  if (rules?.minLength !== void 0 && value.length < rules.minLength) messages.push(`${field.label} must be at least ${rules.minLength} characters`);
79
79
  if (rules?.maxLength !== void 0 && value.length > rules.maxLength) messages.push(`${field.label} must be at most ${rules.maxLength} characters`);
80
80
  if (pattern && !pattern.test(value)) messages.push(`${field.label} is not in the expected format`);
@@ -87,12 +87,12 @@ function validateValue({ field, value, values, pattern, validator }) {
87
87
  if (rules?.min !== void 0 && value < rules.min) messages.push(`${field.label} must be at least ${rules.min}`);
88
88
  if (rules?.max !== void 0 && value > rules.max) messages.push(`${field.label} must be at most ${rules.max}`);
89
89
  }
90
- if (field.type === "select" || field.type === "multiselect") {
90
+ if (field.value_type === "select" || field.value_type === "multiselect") {
91
91
  const options = field.options;
92
92
  if (options) {
93
- const allowed = new Set(options.map((option) => option.value));
94
- if (field.type === "select" && typeof value === "string" && !allowed.has(value)) messages.push(`${field.label} must be one of the allowed options`);
95
- if (field.type === "multiselect" && Array.isArray(value)) {
93
+ const allowed = new Set(options.map((option) => option.key));
94
+ if (field.value_type === "select" && typeof value === "string" && !allowed.has(value)) messages.push(`${field.label} must be one of the allowed options`);
95
+ if (field.value_type === "multiselect" && Array.isArray(value)) {
96
96
  if (value.some((entry) => typeof entry !== "string" || !allowed.has(entry))) messages.push(`${field.label} has values that are not allowed options`);
97
97
  }
98
98
  }
@@ -101,7 +101,7 @@ function validateValue({ field, value, values, pattern, validator }) {
101
101
  const encoded = canonicalize(value);
102
102
  if (!rules.oneOf.some((option) => canonicalize(option) === encoded)) messages.push(`${field.label} must be one of the allowed options`);
103
103
  }
104
- if (field.type === "file" && isFormFileRef(value)) {
104
+ if (field.value_type === "file" && isFormFileRef(value)) {
105
105
  if (value.status !== "done") messages.push(`${field.label} upload is not complete`);
106
106
  if (rules?.maxSize !== void 0 && value.size > rules.maxSize) messages.push(`${field.label} must be at most ${rules.maxSize} bytes`);
107
107
  if (rules?.accept !== void 0 && !contentTypeMatchesAccept({
@@ -223,7 +223,7 @@ function writeFields({ storage, formKey, fields, fieldDefinitions }) {
223
223
  if (value === void 0) continue;
224
224
  nextForm[field.key] = {
225
225
  value,
226
- type: field.type,
226
+ type: field.value_type,
227
227
  label: field.label,
228
228
  ...field.registryId === void 0 ? {} : { registryId: field.registryId },
229
229
  ...field.protocolFieldId === void 0 ? {} : { protocolFieldId: field.protocolFieldId }
@@ -305,7 +305,7 @@ function buildCookiePayloadFromBag({ bag, fieldDefinitions }) {
305
305
  if (value === void 0) continue;
306
306
  payload[field.key] = {
307
307
  value,
308
- type: field.type,
308
+ type: field.value_type,
309
309
  label: field.label,
310
310
  ...field.registryId === void 0 ? {} : { registryId: field.registryId },
311
311
  ...field.protocolFieldId === void 0 ? {} : { protocolFieldId: field.protocolFieldId }
@@ -325,10 +325,10 @@ function isStoredField(value) {
325
325
  return true;
326
326
  }
327
327
  function entryMatchesFieldDefinition(entry, field) {
328
- if (entry.type !== field.type || entry.label !== field.label) return false;
328
+ if (entry.type !== field.value_type || entry.label !== field.label) return false;
329
329
  if ((entry.registryId ?? void 0) !== (field.registryId ?? void 0)) return false;
330
330
  if ((entry.protocolFieldId ?? void 0) !== (field.protocolFieldId ?? void 0)) return false;
331
- return FIELD_TYPE_PREDICATES[field.type](entry.value);
331
+ return FIELD_TYPE_PREDICATES[field.value_type](entry.value);
332
332
  }
333
333
  function serializeBrowserCookie(key, value) {
334
334
  const locationRef = globalThis.location;
@@ -589,9 +589,22 @@ async function uploadFormFile(config, input) {
589
589
  const MAX_VALUE_LENGTH = 1024;
590
590
  /** The ingest bound on a `data:updated` entry's `label`. */
591
591
  const MAX_LABEL_LENGTH = 256;
592
- /** Maps a form field's declared type to the analytics `field:updated` class. */
593
- function mapFieldUpdatedType(type) {
594
- return type;
592
+ /** Maps a form field's declared value type to the analytics `field:updated` class. */
593
+ function mapFieldUpdatedType(valueType) {
594
+ return valueType;
595
+ }
596
+ function resolvedFieldClassification(field) {
597
+ return field.field_type ?? "user_input";
598
+ }
599
+ function isAnalyticsExcludedField(field) {
600
+ return resolvedFieldClassification(field) === "local_only";
601
+ }
602
+ function analyticsPatchEntries({ fields, patch }) {
603
+ const byKey = new Map(fields.map((field) => [field.key, field]));
604
+ return Object.entries(patch).filter(([key]) => {
605
+ const field = byKey.get(key);
606
+ return field === void 0 || !isAnalyticsExcludedField(field);
607
+ });
595
608
  }
596
609
  /**
597
610
  * Stringifies values for `data:updated` entries. `field:updated` carries the
@@ -604,9 +617,13 @@ function formatFieldValue({ value }) {
604
617
  /** One event carrying every key in one `.set()` call. */
605
618
  function buildDataUpdatedEvent({ fields, patch, formId }) {
606
619
  const byKey = new Map(fields.map((field) => [field.key, field]));
620
+ const entries = analyticsPatchEntries({
621
+ fields,
622
+ patch
623
+ });
607
624
  return {
608
625
  event_name: "data:updated",
609
- data: Object.fromEntries(Object.entries(patch).map(([key, value]) => {
626
+ data: Object.fromEntries(entries.map(([key, value]) => {
610
627
  const field = byKey.get(key);
611
628
  return [key, {
612
629
  value: formatFieldValue({
@@ -622,12 +639,15 @@ function buildDataUpdatedEvent({ fields, patch, formId }) {
622
639
  /** One `field:updated` per changed key, emitted alongside `data:updated`. */
623
640
  function buildFieldUpdatedEvents({ fields, patch, formId }) {
624
641
  const byKey = new Map(fields.map((field) => [field.key, field]));
625
- return Object.entries(patch).map(([key, value]) => {
642
+ return analyticsPatchEntries({
643
+ fields,
644
+ patch
645
+ }).map(([key, value]) => {
626
646
  const field = byKey.get(key);
627
647
  const event = {
628
648
  event_name: "field:updated",
629
649
  field_key: key,
630
- field_type: mapFieldUpdatedType(field?.type ?? "text"),
650
+ field_type: mapFieldUpdatedType(field?.value_type ?? "text"),
631
651
  form_id: formId
632
652
  };
633
653
  if (value !== void 0) event.field_value = value;
@@ -645,7 +665,7 @@ function buildFieldUpdatedEvents({ fields, patch, formId }) {
645
665
  * values passes through.
646
666
  */
647
667
  function normalizeExclusiveSelection({ next, previous, options }) {
648
- const exclusive = new Set(options.filter((option) => option.exclusive === true).map((option) => option.value));
668
+ const exclusive = new Set(options.filter((option) => option.exclusive === true).map((option) => option.key));
649
669
  if (exclusive.size === 0) return next;
650
670
  const previousValues = new Set(previous);
651
671
  const added = next.filter((entry) => !previousValues.has(entry));
@@ -686,9 +706,17 @@ const NUMERIC_RULES = [
686
706
  "maxSize"
687
707
  ];
688
708
  const FILE_ONLY_RULES = ["accept", "maxSize"];
689
- const OPTION_KEYS = [
690
- "value",
691
- "label",
709
+ const FIELD_CLASSIFICATIONS = [
710
+ "user_input",
711
+ "computed",
712
+ "local_only"
713
+ ];
714
+ const SNAKE_CASE_FIELD_KEY = /^[a-z][a-z0-9_]*$/;
715
+ /** Keys allowed on a raw option object before normalization. */
716
+ const OPTION_INPUT_KEYS = [
717
+ "key",
718
+ "display_text",
719
+ "display_description",
692
720
  "exclusive"
693
721
  ];
694
722
  /** The two field types whose choices are declared through a field-level `options`. */
@@ -724,78 +752,141 @@ function validateAndCompile({ schema }) {
724
752
  if (fields.length === 0) throw new SchemaError("schema.fields: must declare at least one field");
725
753
  const patterns = /* @__PURE__ */ new Map();
726
754
  const seenKeys = /* @__PURE__ */ new Set();
755
+ const compiledFields = [];
727
756
  fields.forEach((field, index) => {
728
- validateField({
757
+ compiledFields.push(compileField({
729
758
  field,
730
759
  path: `schema.fields[${index}]`,
731
760
  seenKeys,
732
761
  patterns
733
- });
762
+ }));
734
763
  });
735
764
  return {
736
765
  formKey,
737
- fields: [...fields],
766
+ fields: compiledFields,
738
767
  patterns
739
768
  };
740
769
  }
741
- function validateField({ field, path, seenKeys, patterns }) {
770
+ function compileField({ field, path, seenKeys, patterns }) {
742
771
  if (!isObjectLike(field)) throw new SchemaError(`${path}: must be an object`);
743
772
  const key = field["key"];
744
773
  if (typeof key !== "string" || key.trim() === "") throw new SchemaError(`${path}.key: must be a non-empty string`);
774
+ if (!SNAKE_CASE_FIELD_KEY.test(key)) throw new SchemaError(`${path}.key: must be lowercase snake_case`);
745
775
  if (key.length > MAX_FIELD_KEY_LENGTH) throw new SchemaError(`${path}.key: must be at most ${MAX_FIELD_KEY_LENGTH} characters (received ${key.length})`);
746
776
  if (seenKeys.has(key)) throw new SchemaError(`${path}.key: duplicate field key "${key}" in this form`);
747
777
  seenKeys.add(key);
748
778
  const label = field["label"];
749
779
  if (typeof label !== "string" || label.trim() === "") throw new SchemaError(`${path}.label: must be a non-empty string`);
750
- const type = field["type"];
751
- if (typeof type !== "string" || !FIELD_TYPES.includes(type)) throw new SchemaError(`${path}.type: must be one of ${FIELD_TYPES.join(", ")}`);
780
+ if (field["type"] !== void 0) throw new SchemaError(`${path}.type: is not supported; use value_type`);
781
+ const valueTypeRaw = field["value_type"];
782
+ if (typeof valueTypeRaw !== "string" || valueTypeRaw.trim() === "") throw new SchemaError(`${path}.value_type: must be a non-empty string`);
783
+ const valueType = valueTypeRaw;
784
+ if (!FIELD_TYPES.includes(valueType)) throw new SchemaError(`${path}.value_type: must be one of ${FIELD_TYPES.join(", ")}`);
785
+ const displayText = field["display_text"];
786
+ if (displayText !== void 0) {
787
+ if (typeof displayText !== "string" || displayText.trim() === "") throw new SchemaError(`${path}.display_text: must be a non-empty string`);
788
+ }
789
+ const displayDescription = field["display_description"];
790
+ if (displayDescription !== void 0) {
791
+ if (typeof displayDescription !== "string" || displayDescription.trim() === "") throw new SchemaError(`${path}.display_description: must be a non-empty string`);
792
+ }
793
+ const fieldTypeRaw = field["field_type"];
794
+ let fieldType = "user_input";
795
+ if (fieldTypeRaw !== void 0) {
796
+ if (typeof fieldTypeRaw !== "string" || !FIELD_CLASSIFICATIONS.includes(fieldTypeRaw)) throw new SchemaError(`${path}.field_type: must be one of ${FIELD_CLASSIFICATIONS.join(", ")}`);
797
+ fieldType = fieldTypeRaw;
798
+ }
752
799
  const registryId = field["registryId"];
753
800
  if (registryId !== void 0 && typeof registryId !== "string") throw new SchemaError(`${path}.registryId: must be a string`);
754
801
  const protocolFieldId = field["protocolFieldId"];
755
802
  if (protocolFieldId !== void 0 && typeof protocolFieldId !== "string") throw new SchemaError(`${path}.protocolFieldId: must be a string`);
756
803
  const includeInCookies = field["includeInCookies"];
757
804
  if (includeInCookies !== void 0 && typeof includeInCookies !== "boolean") throw new SchemaError(`${path}.includeInCookies: must be a boolean`);
758
- validateOptions({
805
+ const normalizedOptions = normalizeOptions({
759
806
  field,
760
- type,
807
+ valueType,
761
808
  path
762
809
  });
763
810
  const validations = field["validations"];
764
- if (validations === void 0) return;
765
- validateValidations({
766
- validations,
767
- type,
768
- path: `${path}.validations`
769
- });
770
- if (!isObjectLike(validations)) return;
771
- const pattern = validations["pattern"];
772
- if (typeof pattern !== "string") return;
773
- patterns.set(key, compilePattern({
774
- pattern,
775
- path: `${path}.validations.pattern`
776
- }));
811
+ if (validations !== void 0) {
812
+ validateValidations({
813
+ validations,
814
+ type: valueType,
815
+ path: `${path}.validations`
816
+ });
817
+ if (isObjectLike(validations)) {
818
+ const pattern = validations["pattern"];
819
+ if (typeof pattern === "string") patterns.set(key, compilePattern({
820
+ pattern,
821
+ path: `${path}.validations.pattern`
822
+ }));
823
+ }
824
+ }
825
+ const compiled = {
826
+ ...field,
827
+ key,
828
+ label,
829
+ value_type: valueType
830
+ };
831
+ delete compiled["type"];
832
+ compiled["field_type"] = fieldType;
833
+ if (displayText !== void 0) compiled["display_text"] = displayText;
834
+ if (displayDescription !== void 0) compiled["display_description"] = displayDescription;
835
+ if (normalizedOptions !== void 0) compiled["options"] = normalizedOptions;
836
+ if (validations !== void 0) compiled["validations"] = cloneValidations(validations);
837
+ return compiled;
838
+ }
839
+ function cloneValidations(validations) {
840
+ if (!isObjectLike(validations)) return validations;
841
+ const cloned = {};
842
+ for (const [ruleKey, ruleValue] of Object.entries(validations)) {
843
+ if (ruleKey === "custom") {
844
+ cloned[ruleKey] = ruleValue;
845
+ continue;
846
+ }
847
+ if (ruleKey === "oneOf" || ruleKey === "accept") {
848
+ cloned[ruleKey] = Array.isArray(ruleValue) ? [...ruleValue] : ruleValue;
849
+ continue;
850
+ }
851
+ cloned[ruleKey] = ruleValue;
852
+ }
853
+ return cloned;
777
854
  }
778
- function validateOptions({ field, type, path }) {
855
+ function normalizeOptions({ field, valueType, path }) {
779
856
  const options = field["options"];
780
- if (options === void 0) return;
781
- if (!OPTION_FIELD_TYPES.includes(type)) throw new SchemaError(`${path}.options: only a select or multiselect field may declare options`);
857
+ if (options === void 0) return void 0;
858
+ if (!OPTION_FIELD_TYPES.includes(valueType)) throw new SchemaError(`${path}.options: only a select or multiselect field may declare options`);
782
859
  if (!(Array.isArray(options) && options.length > 0)) throw new SchemaError(`${path}.options: must be a non-empty array`);
783
860
  const entries = options;
784
- const seenValues = /* @__PURE__ */ new Set();
861
+ const seenKeys = /* @__PURE__ */ new Set();
862
+ const normalized = [];
785
863
  entries.forEach((option, index) => {
786
864
  const at = `${path}.options[${index}]`;
787
865
  if (!isObjectLike(option)) throw new SchemaError(`${at}: must be an object`);
788
- for (const optionKey of Object.keys(option)) if (!OPTION_KEYS.includes(optionKey)) throw new SchemaError(`${at}.${optionKey}: unknown option key; expected one of ${OPTION_KEYS.join(", ")}`);
789
- const value = option["value"];
790
- if (typeof value !== "string" || value.trim() === "") throw new SchemaError(`${at}.value: must be a non-empty string`);
791
- const label = option["label"];
792
- if (label !== void 0 && typeof label !== "string") throw new SchemaError(`${at}.label: must be a string`);
866
+ for (const optionKey of Object.keys(option)) if (!OPTION_INPUT_KEYS.includes(optionKey)) throw new SchemaError(`${at}.${optionKey}: unknown option key; expected one of ${OPTION_INPUT_KEYS.join(", ")}`);
867
+ const optionKey = option["key"];
868
+ const displayTextRaw = option["display_text"];
869
+ const displayDescription = option["display_description"];
870
+ if (typeof optionKey !== "string" || optionKey.trim() === "") throw new SchemaError(`${at}.key: must be a non-empty string`);
871
+ if (typeof displayTextRaw !== "string" || displayTextRaw.trim() === "") throw new SchemaError(`${at}.display_text: must be a non-empty string`);
872
+ const key = optionKey;
873
+ const displayText = displayTextRaw;
874
+ if (displayDescription !== void 0) {
875
+ if (typeof displayDescription !== "string" || displayDescription.trim() === "") throw new SchemaError(`${at}.display_description: must be a non-empty string`);
876
+ }
793
877
  const exclusive = option["exclusive"];
794
878
  if (exclusive !== void 0 && typeof exclusive !== "boolean") throw new SchemaError(`${at}.exclusive: must be a boolean`);
795
- if (exclusive === true && type === "select") throw new SchemaError(`${at}.exclusive: only a multiselect field may declare an exclusive option`);
796
- if (seenValues.has(value)) throw new SchemaError(`${at}.value: duplicate option value "${value}" on this field`);
797
- seenValues.add(value);
879
+ if (exclusive !== void 0 && valueType === "select") throw new SchemaError(`${at}.exclusive: only a multiselect field may declare an exclusive option`);
880
+ if (seenKeys.has(key)) throw new SchemaError(`${at}.key: duplicate option key "${key}" on this field`);
881
+ seenKeys.add(key);
882
+ normalized.push({
883
+ key,
884
+ display_text: displayText,
885
+ ...displayDescription === void 0 ? {} : { display_description: displayDescription },
886
+ ...exclusive === void 0 ? {} : { exclusive }
887
+ });
798
888
  });
889
+ return normalized;
799
890
  }
800
891
  function validateValidations({ validations, type, path }) {
801
892
  if (!isObjectLike(validations)) throw new SchemaError(`${path}: must be an object`);
@@ -1205,7 +1296,7 @@ function createFormInstance({ analyticsInstance, storage, persistence, uploadCon
1205
1296
  const previousBag = readBag();
1206
1297
  for (const [key, value] of Object.entries(changes)) {
1207
1298
  const field = declared.get(key);
1208
- if (!field || field.type !== "multiselect" || !field.options) continue;
1299
+ if (!field || field.value_type !== "multiselect" || !field.options) continue;
1209
1300
  if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) continue;
1210
1301
  const stored = previousBag[key];
1211
1302
  changes[key] = [...normalizeExclusiveSelection({
@@ -1291,15 +1382,22 @@ function createFormInstance({ analyticsInstance, storage, persistence, uploadCon
1291
1382
  ok: true,
1292
1383
  errors: noErrors()
1293
1384
  });
1294
- return analyticsInstance.trackEvent([buildDataUpdatedEvent({
1385
+ const dataUpdated = buildDataUpdatedEvent({
1295
1386
  fields: resolved.fields,
1296
1387
  patch: changes,
1297
1388
  formId: schema.id
1298
- }), ...buildFieldUpdatedEvents({
1389
+ });
1390
+ const fieldUpdated = buildFieldUpdatedEvents({
1299
1391
  fields: resolved.fields,
1300
1392
  patch: changes,
1301
1393
  formId: schema.id
1302
- })]).then(() => ({
1394
+ });
1395
+ const analyticsEvents = [...Object.keys(dataUpdated.data).length > 0 ? [dataUpdated] : [], ...fieldUpdated];
1396
+ if (analyticsEvents.length === 0) return Promise.resolve({
1397
+ ok: true,
1398
+ errors: noErrors()
1399
+ });
1400
+ return analyticsInstance.trackEvent(analyticsEvents).then(() => ({
1303
1401
  ok: true,
1304
1402
  errors: noErrors()
1305
1403
  })).catch((error) => ({
@@ -1333,6 +1431,11 @@ function createFormInstance({ analyticsInstance, storage, persistence, uploadCon
1333
1431
  return get(fieldKey);
1334
1432
  });
1335
1433
  const getAll = () => narrow(state.bag);
1434
+ const getFieldByKey = (key) => {
1435
+ const fieldKey = key;
1436
+ if (!declared.has(fieldKey)) return void 0;
1437
+ return declared.get(fieldKey);
1438
+ };
1336
1439
  const submit = () => {
1337
1440
  const snapshot = narrow(readBag());
1338
1441
  const values = snapshot;
@@ -1478,7 +1581,7 @@ function createFormInstance({ analyticsInstance, storage, persistence, uploadCon
1478
1581
  const uploadFile = async ({ key, file, fileName }) => {
1479
1582
  const field = declared.get(key);
1480
1583
  if (!field) throw new FormsError(`Unknown field: ${key}`);
1481
- if (field.type !== "file") throw new FormsError(`Field "${key}" is not a file field`);
1584
+ if (field.value_type !== "file") throw new FormsError(`Field "${key}" is not a file field`);
1482
1585
  if (!uploadConfig) throw new FormsError("File uploads require a valid publishable key and initialized Embeddables core instance.");
1483
1586
  const rules = field.validations;
1484
1587
  const contentType = file.type || "application/octet-stream";
@@ -1500,6 +1603,7 @@ function createFormInstance({ analyticsInstance, storage, persistence, uploadCon
1500
1603
  key: schema.id,
1501
1604
  set,
1502
1605
  get,
1606
+ getFieldByKey,
1503
1607
  getValueByProtocolFieldId,
1504
1608
  getAll,
1505
1609
  submit,
@@ -1566,4 +1670,4 @@ Object.defineProperty(exports, "seedServerFormsStorageFromCookies", {
1566
1670
  }
1567
1671
  });
1568
1672
 
1569
- //# sourceMappingURL=form-CdIfHqNc.cjs.map
1673
+ //# sourceMappingURL=form-lmr-O7j7.cjs.map