@embeddables/forms 0.2.0 → 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.
- package/README.md +56 -7
- package/dist/{form-Bt5pwVP6.js → form-CR5xJ_nQ.js} +299 -68
- package/dist/form-CR5xJ_nQ.js.map +1 -0
- package/dist/{form-Br23yPDS.d.ts → form-DKPfOrTG.d.ts} +91 -44
- package/dist/form-DKPfOrTG.d.ts.map +1 -0
- package/dist/{form-BOHdxeuO.cjs → form-lmr-O7j7.cjs} +299 -68
- package/dist/form-lmr-O7j7.cjs.map +1 -0
- package/dist/{index-BB698udY.d.ts → index-FePzwfQE.d.ts} +13 -13
- package/dist/index-FePzwfQE.d.ts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/react.cjs +95 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts +31 -3
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +95 -2
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/package.json +3 -3
- package/dist/form-BOHdxeuO.cjs.map +0 -1
- package/dist/form-Br23yPDS.d.ts.map +0 -1
- package/dist/form-Bt5pwVP6.js.map +0 -1
- package/dist/index-BB698udY.d.ts.map +0 -1
|
@@ -32,6 +32,21 @@ var ValidatorError = class extends FormsError {
|
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
//#endregion
|
|
35
|
+
//#region src/types/form-file.ts
|
|
36
|
+
function contentTypeMatchesAccept({ contentType, accept }) {
|
|
37
|
+
const normalized = contentType.split(";", 1)[0]?.trim().toLowerCase() || "";
|
|
38
|
+
return accept.some((entry) => {
|
|
39
|
+
const pattern = entry.trim().toLowerCase();
|
|
40
|
+
if (pattern.endsWith("/*")) return normalized.startsWith(pattern.slice(0, -1));
|
|
41
|
+
return normalized === pattern;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function isFormFileRef(value) {
|
|
45
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
46
|
+
const record = value;
|
|
47
|
+
return typeof record.file_id === "string" && typeof record.name === "string" && typeof record.content_type === "string" && typeof record.size === "number" && Number.isFinite(record.size) && (record.status === "uploading" || record.status === "done" || record.status === "error");
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
35
50
|
//#region src/core/validation.ts
|
|
36
51
|
/** Runtime counterpart to `FieldType`. Exhaustive by construction. */
|
|
37
52
|
const FIELD_TYPE_PREDICATES = {
|
|
@@ -41,7 +56,8 @@ const FIELD_TYPE_PREDICATES = {
|
|
|
41
56
|
boolean: (value) => typeof value === "boolean",
|
|
42
57
|
select: (value) => typeof value === "string",
|
|
43
58
|
multiselect: (value) => Array.isArray(value),
|
|
44
|
-
json: () => true
|
|
59
|
+
json: () => true,
|
|
60
|
+
file: (value) => value === null || isFormFileRef(value)
|
|
45
61
|
};
|
|
46
62
|
const EMAIL_PATTERN = /^[\w.!#$%&'*+/=?^`{|}~-]+@[a-zA-Z\d](?:[a-zA-Z\d-]{0,61}[a-zA-Z\d])?(?:\.[a-zA-Z\d](?:[a-zA-Z\d-]{0,61}[a-zA-Z\d])?)*$/;
|
|
47
63
|
/**
|
|
@@ -56,9 +72,9 @@ function validateValue({ field, value, values, pattern, validator }) {
|
|
|
56
72
|
const isBlank = isAbsent || value === "" || Array.isArray(value) && value.length === 0;
|
|
57
73
|
if (rules?.required === true && isBlank) messages.push(`${field.label} is required`);
|
|
58
74
|
if (isAbsent) return messages;
|
|
59
|
-
if (!FIELD_TYPE_PREDICATES[field.
|
|
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`);
|
|
60
76
|
if (typeof value === "string") {
|
|
61
|
-
if (field.
|
|
77
|
+
if (field.value_type === "email" && value !== "" && !EMAIL_PATTERN.test(value)) messages.push(`${field.label} must be a valid email address`);
|
|
62
78
|
if (rules?.minLength !== void 0 && value.length < rules.minLength) messages.push(`${field.label} must be at least ${rules.minLength} characters`);
|
|
63
79
|
if (rules?.maxLength !== void 0 && value.length > rules.maxLength) messages.push(`${field.label} must be at most ${rules.maxLength} characters`);
|
|
64
80
|
if (pattern && !pattern.test(value)) messages.push(`${field.label} is not in the expected format`);
|
|
@@ -71,12 +87,12 @@ function validateValue({ field, value, values, pattern, validator }) {
|
|
|
71
87
|
if (rules?.min !== void 0 && value < rules.min) messages.push(`${field.label} must be at least ${rules.min}`);
|
|
72
88
|
if (rules?.max !== void 0 && value > rules.max) messages.push(`${field.label} must be at most ${rules.max}`);
|
|
73
89
|
}
|
|
74
|
-
if (field.
|
|
90
|
+
if (field.value_type === "select" || field.value_type === "multiselect") {
|
|
75
91
|
const options = field.options;
|
|
76
92
|
if (options) {
|
|
77
|
-
const allowed = new Set(options.map((option) => option.
|
|
78
|
-
if (field.
|
|
79
|
-
if (field.
|
|
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)) {
|
|
80
96
|
if (value.some((entry) => typeof entry !== "string" || !allowed.has(entry))) messages.push(`${field.label} has values that are not allowed options`);
|
|
81
97
|
}
|
|
82
98
|
}
|
|
@@ -85,6 +101,14 @@ function validateValue({ field, value, values, pattern, validator }) {
|
|
|
85
101
|
const encoded = canonicalize(value);
|
|
86
102
|
if (!rules.oneOf.some((option) => canonicalize(option) === encoded)) messages.push(`${field.label} must be one of the allowed options`);
|
|
87
103
|
}
|
|
104
|
+
if (field.value_type === "file" && isFormFileRef(value)) {
|
|
105
|
+
if (value.status !== "done") messages.push(`${field.label} upload is not complete`);
|
|
106
|
+
if (rules?.maxSize !== void 0 && value.size > rules.maxSize) messages.push(`${field.label} must be at most ${rules.maxSize} bytes`);
|
|
107
|
+
if (rules?.accept !== void 0 && !contentTypeMatchesAccept({
|
|
108
|
+
contentType: value.content_type,
|
|
109
|
+
accept: rules.accept
|
|
110
|
+
})) messages.push(`${field.label} must be one of the allowed file types`);
|
|
111
|
+
}
|
|
88
112
|
if (!validator || messages.length > 0) return messages;
|
|
89
113
|
return normalizeValidatorResult({
|
|
90
114
|
result: validator({
|
|
@@ -127,7 +151,8 @@ const FIELD_TYPES$2 = [
|
|
|
127
151
|
"boolean",
|
|
128
152
|
"select",
|
|
129
153
|
"multiselect",
|
|
130
|
-
"json"
|
|
154
|
+
"json",
|
|
155
|
+
"file"
|
|
131
156
|
];
|
|
132
157
|
const LIVE_DOCUMENTS = /* @__PURE__ */ new WeakMap();
|
|
133
158
|
function readDocumentFromStorage({ storage }) {
|
|
@@ -198,7 +223,7 @@ function writeFields({ storage, formKey, fields, fieldDefinitions }) {
|
|
|
198
223
|
if (value === void 0) continue;
|
|
199
224
|
nextForm[field.key] = {
|
|
200
225
|
value,
|
|
201
|
-
type: field.
|
|
226
|
+
type: field.value_type,
|
|
202
227
|
label: field.label,
|
|
203
228
|
...field.registryId === void 0 ? {} : { registryId: field.registryId },
|
|
204
229
|
...field.protocolFieldId === void 0 ? {} : { protocolFieldId: field.protocolFieldId }
|
|
@@ -280,7 +305,7 @@ function buildCookiePayloadFromBag({ bag, fieldDefinitions }) {
|
|
|
280
305
|
if (value === void 0) continue;
|
|
281
306
|
payload[field.key] = {
|
|
282
307
|
value,
|
|
283
|
-
type: field.
|
|
308
|
+
type: field.value_type,
|
|
284
309
|
label: field.label,
|
|
285
310
|
...field.registryId === void 0 ? {} : { registryId: field.registryId },
|
|
286
311
|
...field.protocolFieldId === void 0 ? {} : { protocolFieldId: field.protocolFieldId }
|
|
@@ -300,10 +325,10 @@ function isStoredField(value) {
|
|
|
300
325
|
return true;
|
|
301
326
|
}
|
|
302
327
|
function entryMatchesFieldDefinition(entry, field) {
|
|
303
|
-
if (entry.type !== field.
|
|
328
|
+
if (entry.type !== field.value_type || entry.label !== field.label) return false;
|
|
304
329
|
if ((entry.registryId ?? void 0) !== (field.registryId ?? void 0)) return false;
|
|
305
330
|
if ((entry.protocolFieldId ?? void 0) !== (field.protocolFieldId ?? void 0)) return false;
|
|
306
|
-
return FIELD_TYPE_PREDICATES[field.
|
|
331
|
+
return FIELD_TYPE_PREDICATES[field.value_type](entry.value);
|
|
307
332
|
}
|
|
308
333
|
function serializeBrowserCookie(key, value) {
|
|
309
334
|
const locationRef = globalThis.location;
|
|
@@ -436,7 +461,7 @@ function createNoopPersistence() {
|
|
|
436
461
|
}
|
|
437
462
|
//#endregion
|
|
438
463
|
//#region src/storage/persistence-client.ts
|
|
439
|
-
const PUBLISHABLE_KEY_HEADER = "x-publishable-key";
|
|
464
|
+
const PUBLISHABLE_KEY_HEADER$1 = "x-publishable-key";
|
|
440
465
|
const hcWithType = (...args) => (0, hono_client.hc)(...args);
|
|
441
466
|
function withTimeout(fetchImpl, timeoutMs) {
|
|
442
467
|
return async (input, init) => {
|
|
@@ -469,7 +494,7 @@ function createApiPersistence(config) {
|
|
|
469
494
|
const root = config.baseUrl.replace(/\/+$/, "");
|
|
470
495
|
const rpc = hcWithType(`${root}/forms`, {
|
|
471
496
|
fetch: withTimeout(config.fetch, config.timeoutMs),
|
|
472
|
-
headers: { [PUBLISHABLE_KEY_HEADER]: config.publishableKey }
|
|
497
|
+
headers: { [PUBLISHABLE_KEY_HEADER$1]: config.publishableKey }
|
|
473
498
|
});
|
|
474
499
|
return {
|
|
475
500
|
savePartial({ formKey, values }) {
|
|
@@ -498,14 +523,88 @@ function resolveDefaultPersistence(config) {
|
|
|
498
523
|
return createApiPersistence(resolved);
|
|
499
524
|
}
|
|
500
525
|
//#endregion
|
|
526
|
+
//#region src/storage/upload-client.ts
|
|
527
|
+
const PUBLISHABLE_KEY_HEADER = "x-publishable-key";
|
|
528
|
+
function mapUploadErrorMessage(problem, status) {
|
|
529
|
+
const code = problem?.code;
|
|
530
|
+
if (code === "validation.file_too_large") return problem?.detail ?? "Maximum upload size is 25 MiB.";
|
|
531
|
+
if (code === "validation.unsupported_content_type") return problem?.detail ?? "Unsupported file type.";
|
|
532
|
+
if (code === "validation.upload_failed") return problem?.detail ?? "Upload could not be completed.";
|
|
533
|
+
if (code === "service.form_uploads_not_configured") return problem?.detail ?? "File uploads are not configured for this project.";
|
|
534
|
+
if (code === "validation.failed") return problem?.detail ?? "Upload metadata is invalid.";
|
|
535
|
+
return problem?.detail ?? problem?.title ?? `File upload failed (${status}).`;
|
|
536
|
+
}
|
|
537
|
+
function uploadResultToFormFileRef(result) {
|
|
538
|
+
return {
|
|
539
|
+
file_id: result.file_id,
|
|
540
|
+
name: result.name,
|
|
541
|
+
content_type: result.content_type,
|
|
542
|
+
size: result.size,
|
|
543
|
+
status: "done",
|
|
544
|
+
uploaded_at: result.uploaded_at
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function toUploadFetchError(error, path, timeoutMs) {
|
|
548
|
+
if (error instanceof FormsError) return error;
|
|
549
|
+
if (error instanceof Error && error.name === "AbortError") return new FormsError(`Request to ${path} timed out after ${timeoutMs}ms`, { cause: error });
|
|
550
|
+
return new FormsError(`Request to ${path} failed to reach the API`, { cause: error });
|
|
551
|
+
}
|
|
552
|
+
function toUploadDecodeError(error, path) {
|
|
553
|
+
return new FormsError(`Upload response from ${path} could not be decoded`, { cause: error });
|
|
554
|
+
}
|
|
555
|
+
async function uploadFormFile(config, input) {
|
|
556
|
+
const url = `${config.baseUrl.replace(/\/+$/, "")}/forms/v1/public/uploads`;
|
|
557
|
+
const formData = new FormData();
|
|
558
|
+
formData.append("project_id", config.projectId);
|
|
559
|
+
formData.append("app_user_id", config.appUserId);
|
|
560
|
+
formData.append("form_id", input.formId);
|
|
561
|
+
formData.append("field_key", input.fieldKey);
|
|
562
|
+
const fileName = input.fileName ?? (typeof File !== "undefined" && input.file instanceof File ? input.file.name : "upload");
|
|
563
|
+
formData.append("file", input.file, fileName);
|
|
564
|
+
const controller = new AbortController();
|
|
565
|
+
const timer = setTimeout(() => controller.abort(), config.timeoutMs);
|
|
566
|
+
try {
|
|
567
|
+
const res = await config.fetch(url, {
|
|
568
|
+
method: "POST",
|
|
569
|
+
headers: { [PUBLISHABLE_KEY_HEADER]: config.publishableKey },
|
|
570
|
+
body: formData,
|
|
571
|
+
signal: controller.signal
|
|
572
|
+
});
|
|
573
|
+
clearTimeout(timer);
|
|
574
|
+
if (!res.ok) throw new FormsError(mapUploadErrorMessage(await res.json().catch(() => null), res.status));
|
|
575
|
+
try {
|
|
576
|
+
return uploadResultToFormFileRef(await res.json());
|
|
577
|
+
} catch (error) {
|
|
578
|
+
throw toUploadDecodeError(error, url);
|
|
579
|
+
}
|
|
580
|
+
} catch (error) {
|
|
581
|
+
throw toUploadFetchError(error, url, config.timeoutMs);
|
|
582
|
+
} finally {
|
|
583
|
+
clearTimeout(timer);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
//#endregion
|
|
501
587
|
//#region src/core/analytics.ts
|
|
502
588
|
/** The ingest bound on a `data:updated` entry's `value`. */
|
|
503
589
|
const MAX_VALUE_LENGTH = 1024;
|
|
504
590
|
/** The ingest bound on a `data:updated` entry's `label`. */
|
|
505
591
|
const MAX_LABEL_LENGTH = 256;
|
|
506
|
-
/** Maps a form field's declared type to the analytics `field:updated` class. */
|
|
507
|
-
function mapFieldUpdatedType(
|
|
508
|
-
return
|
|
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
|
+
});
|
|
509
608
|
}
|
|
510
609
|
/**
|
|
511
610
|
* Stringifies values for `data:updated` entries. `field:updated` carries the
|
|
@@ -518,9 +617,13 @@ function formatFieldValue({ value }) {
|
|
|
518
617
|
/** One event carrying every key in one `.set()` call. */
|
|
519
618
|
function buildDataUpdatedEvent({ fields, patch, formId }) {
|
|
520
619
|
const byKey = new Map(fields.map((field) => [field.key, field]));
|
|
620
|
+
const entries = analyticsPatchEntries({
|
|
621
|
+
fields,
|
|
622
|
+
patch
|
|
623
|
+
});
|
|
521
624
|
return {
|
|
522
625
|
event_name: "data:updated",
|
|
523
|
-
data: Object.fromEntries(
|
|
626
|
+
data: Object.fromEntries(entries.map(([key, value]) => {
|
|
524
627
|
const field = byKey.get(key);
|
|
525
628
|
return [key, {
|
|
526
629
|
value: formatFieldValue({
|
|
@@ -536,12 +639,15 @@ function buildDataUpdatedEvent({ fields, patch, formId }) {
|
|
|
536
639
|
/** One `field:updated` per changed key, emitted alongside `data:updated`. */
|
|
537
640
|
function buildFieldUpdatedEvents({ fields, patch, formId }) {
|
|
538
641
|
const byKey = new Map(fields.map((field) => [field.key, field]));
|
|
539
|
-
return
|
|
642
|
+
return analyticsPatchEntries({
|
|
643
|
+
fields,
|
|
644
|
+
patch
|
|
645
|
+
}).map(([key, value]) => {
|
|
540
646
|
const field = byKey.get(key);
|
|
541
647
|
const event = {
|
|
542
648
|
event_name: "field:updated",
|
|
543
649
|
field_key: key,
|
|
544
|
-
field_type: mapFieldUpdatedType(field?.
|
|
650
|
+
field_type: mapFieldUpdatedType(field?.value_type ?? "text"),
|
|
545
651
|
form_id: formId
|
|
546
652
|
};
|
|
547
653
|
if (value !== void 0) event.field_value = value;
|
|
@@ -559,7 +665,7 @@ function buildFieldUpdatedEvents({ fields, patch, formId }) {
|
|
|
559
665
|
* values passes through.
|
|
560
666
|
*/
|
|
561
667
|
function normalizeExclusiveSelection({ next, previous, options }) {
|
|
562
|
-
const exclusive = new Set(options.filter((option) => option.exclusive === true).map((option) => option.
|
|
668
|
+
const exclusive = new Set(options.filter((option) => option.exclusive === true).map((option) => option.key));
|
|
563
669
|
if (exclusive.size === 0) return next;
|
|
564
670
|
const previousValues = new Set(previous);
|
|
565
671
|
const added = next.filter((entry) => !previousValues.has(entry));
|
|
@@ -577,7 +683,8 @@ const FIELD_TYPES = [
|
|
|
577
683
|
"boolean",
|
|
578
684
|
"select",
|
|
579
685
|
"multiselect",
|
|
580
|
-
"json"
|
|
686
|
+
"json",
|
|
687
|
+
"file"
|
|
581
688
|
];
|
|
582
689
|
const VALIDATION_RULES = [
|
|
583
690
|
"required",
|
|
@@ -587,17 +694,29 @@ const VALIDATION_RULES = [
|
|
|
587
694
|
"max",
|
|
588
695
|
"pattern",
|
|
589
696
|
"oneOf",
|
|
697
|
+
"accept",
|
|
698
|
+
"maxSize",
|
|
590
699
|
"custom"
|
|
591
700
|
];
|
|
592
701
|
const NUMERIC_RULES = [
|
|
593
702
|
"minLength",
|
|
594
703
|
"maxLength",
|
|
595
704
|
"min",
|
|
596
|
-
"max"
|
|
705
|
+
"max",
|
|
706
|
+
"maxSize"
|
|
707
|
+
];
|
|
708
|
+
const FILE_ONLY_RULES = ["accept", "maxSize"];
|
|
709
|
+
const FIELD_CLASSIFICATIONS = [
|
|
710
|
+
"user_input",
|
|
711
|
+
"computed",
|
|
712
|
+
"local_only"
|
|
597
713
|
];
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
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",
|
|
601
720
|
"exclusive"
|
|
602
721
|
];
|
|
603
722
|
/** The two field types whose choices are declared through a field-level `options`. */
|
|
@@ -633,82 +752,148 @@ function validateAndCompile({ schema }) {
|
|
|
633
752
|
if (fields.length === 0) throw new SchemaError("schema.fields: must declare at least one field");
|
|
634
753
|
const patterns = /* @__PURE__ */ new Map();
|
|
635
754
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
755
|
+
const compiledFields = [];
|
|
636
756
|
fields.forEach((field, index) => {
|
|
637
|
-
|
|
757
|
+
compiledFields.push(compileField({
|
|
638
758
|
field,
|
|
639
759
|
path: `schema.fields[${index}]`,
|
|
640
760
|
seenKeys,
|
|
641
761
|
patterns
|
|
642
|
-
});
|
|
762
|
+
}));
|
|
643
763
|
});
|
|
644
764
|
return {
|
|
645
765
|
formKey,
|
|
646
|
-
fields:
|
|
766
|
+
fields: compiledFields,
|
|
647
767
|
patterns
|
|
648
768
|
};
|
|
649
769
|
}
|
|
650
|
-
function
|
|
770
|
+
function compileField({ field, path, seenKeys, patterns }) {
|
|
651
771
|
if (!isObjectLike(field)) throw new SchemaError(`${path}: must be an object`);
|
|
652
772
|
const key = field["key"];
|
|
653
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`);
|
|
654
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})`);
|
|
655
776
|
if (seenKeys.has(key)) throw new SchemaError(`${path}.key: duplicate field key "${key}" in this form`);
|
|
656
777
|
seenKeys.add(key);
|
|
657
778
|
const label = field["label"];
|
|
658
779
|
if (typeof label !== "string" || label.trim() === "") throw new SchemaError(`${path}.label: must be a non-empty string`);
|
|
659
|
-
|
|
660
|
-
|
|
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
|
+
}
|
|
661
799
|
const registryId = field["registryId"];
|
|
662
800
|
if (registryId !== void 0 && typeof registryId !== "string") throw new SchemaError(`${path}.registryId: must be a string`);
|
|
663
801
|
const protocolFieldId = field["protocolFieldId"];
|
|
664
802
|
if (protocolFieldId !== void 0 && typeof protocolFieldId !== "string") throw new SchemaError(`${path}.protocolFieldId: must be a string`);
|
|
665
803
|
const includeInCookies = field["includeInCookies"];
|
|
666
804
|
if (includeInCookies !== void 0 && typeof includeInCookies !== "boolean") throw new SchemaError(`${path}.includeInCookies: must be a boolean`);
|
|
667
|
-
|
|
805
|
+
const normalizedOptions = normalizeOptions({
|
|
668
806
|
field,
|
|
669
|
-
|
|
807
|
+
valueType,
|
|
670
808
|
path
|
|
671
809
|
});
|
|
672
810
|
const validations = field["validations"];
|
|
673
|
-
if (validations
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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;
|
|
686
854
|
}
|
|
687
|
-
function
|
|
855
|
+
function normalizeOptions({ field, valueType, path }) {
|
|
688
856
|
const options = field["options"];
|
|
689
|
-
if (options === void 0) return;
|
|
690
|
-
if (!OPTION_FIELD_TYPES.includes(
|
|
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`);
|
|
691
859
|
if (!(Array.isArray(options) && options.length > 0)) throw new SchemaError(`${path}.options: must be a non-empty array`);
|
|
692
860
|
const entries = options;
|
|
693
|
-
const
|
|
861
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
862
|
+
const normalized = [];
|
|
694
863
|
entries.forEach((option, index) => {
|
|
695
864
|
const at = `${path}.options[${index}]`;
|
|
696
865
|
if (!isObjectLike(option)) throw new SchemaError(`${at}: must be an object`);
|
|
697
|
-
for (const optionKey of Object.keys(option)) if (!
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
const
|
|
701
|
-
if (
|
|
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
|
+
}
|
|
702
877
|
const exclusive = option["exclusive"];
|
|
703
878
|
if (exclusive !== void 0 && typeof exclusive !== "boolean") throw new SchemaError(`${at}.exclusive: must be a boolean`);
|
|
704
|
-
if (exclusive
|
|
705
|
-
if (
|
|
706
|
-
|
|
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
|
+
});
|
|
707
888
|
});
|
|
889
|
+
return normalized;
|
|
708
890
|
}
|
|
709
891
|
function validateValidations({ validations, type, path }) {
|
|
710
892
|
if (!isObjectLike(validations)) throw new SchemaError(`${path}: must be an object`);
|
|
711
|
-
for (const rule of Object.keys(validations))
|
|
893
|
+
for (const rule of Object.keys(validations)) {
|
|
894
|
+
if (!VALIDATION_RULES.includes(rule)) throw new SchemaError(`${path}.${rule}: unknown validation rule; expected one of ${VALIDATION_RULES.join(", ")}`);
|
|
895
|
+
if (type !== "file" && FILE_ONLY_RULES.includes(rule)) throw new SchemaError(`${path}.${rule}: is only valid for file fields`);
|
|
896
|
+
}
|
|
712
897
|
const required = validations["required"];
|
|
713
898
|
if (required !== void 0 && typeof required !== "boolean") throw new SchemaError(`${path}.required: must be a boolean`);
|
|
714
899
|
for (const rule of NUMERIC_RULES) {
|
|
@@ -724,6 +909,10 @@ function validateValidations({ validations, type, path }) {
|
|
|
724
909
|
const oneOf = validations["oneOf"];
|
|
725
910
|
if (oneOf !== void 0 && OPTION_FIELD_TYPES.includes(type)) throw new SchemaError(`${path}.oneOf: not allowed on a ${type} field; declare choices through the field's options instead`);
|
|
726
911
|
if (oneOf !== void 0 && !(Array.isArray(oneOf) && oneOf.length > 0)) throw new SchemaError(`${path}.oneOf: must be a non-empty array`);
|
|
912
|
+
const accept = validations["accept"];
|
|
913
|
+
if (accept !== void 0 && !(Array.isArray(accept) && accept.length > 0 && accept.every((entry) => typeof entry === "string" && entry.trim() !== ""))) throw new SchemaError(`${path}.accept: must be a non-empty array of strings`);
|
|
914
|
+
const maxSize = validations["maxSize"];
|
|
915
|
+
if (maxSize !== void 0 && !(typeof maxSize === "number" && Number.isFinite(maxSize) && maxSize > 0)) throw new SchemaError(`${path}.maxSize: must be a positive finite number`);
|
|
727
916
|
const pattern = validations["pattern"];
|
|
728
917
|
if (pattern !== void 0 && typeof pattern !== "string") throw new SchemaError(`${path}.pattern: must be a string`);
|
|
729
918
|
const custom = validations["custom"];
|
|
@@ -957,16 +1146,22 @@ function resolveInitConfig({ core, customValidations, serverFormData }) {
|
|
|
957
1146
|
}
|
|
958
1147
|
return registry;
|
|
959
1148
|
}
|
|
960
|
-
function createFormsClient({ core, customValidations, serverFormData, analyticsInstance, baseUrl, storage, persistence }) {
|
|
1149
|
+
function createFormsClient({ core, customValidations, serverFormData, analyticsInstance, baseUrl, fetch: fetchImpl, storage, persistence }) {
|
|
961
1150
|
if (!hasRequiredCoreMethods(core)) throw new FormsError("initForms requires an initialized Embeddables core instance.");
|
|
962
1151
|
const registry = resolveInitConfig({
|
|
963
1152
|
core,
|
|
964
1153
|
customValidations,
|
|
965
1154
|
serverFormData
|
|
966
1155
|
});
|
|
1156
|
+
const uploadConfig = resolvePersistenceConfig({
|
|
1157
|
+
core,
|
|
1158
|
+
baseUrl,
|
|
1159
|
+
fetch: fetchImpl
|
|
1160
|
+
});
|
|
967
1161
|
const resolvedPersistence = persistence ?? resolveDefaultPersistence({
|
|
968
1162
|
core,
|
|
969
|
-
baseUrl
|
|
1163
|
+
baseUrl,
|
|
1164
|
+
fetch: fetchImpl
|
|
970
1165
|
});
|
|
971
1166
|
const instances = /* @__PURE__ */ new Map();
|
|
972
1167
|
return { getForm({ formId }) {
|
|
@@ -978,6 +1173,7 @@ function createFormsClient({ core, customValidations, serverFormData, analyticsI
|
|
|
978
1173
|
analyticsInstance,
|
|
979
1174
|
storage,
|
|
980
1175
|
persistence: resolvedPersistence,
|
|
1176
|
+
uploadConfig,
|
|
981
1177
|
schema: entry.schema,
|
|
982
1178
|
customValidations: entry.customValidations,
|
|
983
1179
|
serverFormData: entry.serverFormData,
|
|
@@ -994,7 +1190,7 @@ function mergeInitialBag({ fromStorage, serverFormData }) {
|
|
|
994
1190
|
...serverFormData
|
|
995
1191
|
};
|
|
996
1192
|
}
|
|
997
|
-
function createFormInstance({ analyticsInstance, storage, persistence, schema, customValidations, serverFormData, projectId }) {
|
|
1193
|
+
function createFormInstance({ analyticsInstance, storage, persistence, uploadConfig, schema, customValidations, serverFormData, projectId }) {
|
|
998
1194
|
const resolved = resolveForm({ schema: mergeCustomValidations({
|
|
999
1195
|
schema,
|
|
1000
1196
|
customValidations
|
|
@@ -1100,7 +1296,7 @@ function createFormInstance({ analyticsInstance, storage, persistence, schema, c
|
|
|
1100
1296
|
const previousBag = readBag();
|
|
1101
1297
|
for (const [key, value] of Object.entries(changes)) {
|
|
1102
1298
|
const field = declared.get(key);
|
|
1103
|
-
if (!field || field.
|
|
1299
|
+
if (!field || field.value_type !== "multiselect" || !field.options) continue;
|
|
1104
1300
|
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) continue;
|
|
1105
1301
|
const stored = previousBag[key];
|
|
1106
1302
|
changes[key] = [...normalizeExclusiveSelection({
|
|
@@ -1186,15 +1382,22 @@ function createFormInstance({ analyticsInstance, storage, persistence, schema, c
|
|
|
1186
1382
|
ok: true,
|
|
1187
1383
|
errors: noErrors()
|
|
1188
1384
|
});
|
|
1189
|
-
|
|
1385
|
+
const dataUpdated = buildDataUpdatedEvent({
|
|
1190
1386
|
fields: resolved.fields,
|
|
1191
1387
|
patch: changes,
|
|
1192
1388
|
formId: schema.id
|
|
1193
|
-
})
|
|
1389
|
+
});
|
|
1390
|
+
const fieldUpdated = buildFieldUpdatedEvents({
|
|
1194
1391
|
fields: resolved.fields,
|
|
1195
1392
|
patch: changes,
|
|
1196
1393
|
formId: schema.id
|
|
1197
|
-
})
|
|
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(() => ({
|
|
1198
1401
|
ok: true,
|
|
1199
1402
|
errors: noErrors()
|
|
1200
1403
|
})).catch((error) => ({
|
|
@@ -1228,6 +1431,11 @@ function createFormInstance({ analyticsInstance, storage, persistence, schema, c
|
|
|
1228
1431
|
return get(fieldKey);
|
|
1229
1432
|
});
|
|
1230
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
|
+
};
|
|
1231
1439
|
const submit = () => {
|
|
1232
1440
|
const snapshot = narrow(readBag());
|
|
1233
1441
|
const values = snapshot;
|
|
@@ -1370,16 +1578,39 @@ function createFormInstance({ analyticsInstance, storage, persistence, schema, c
|
|
|
1370
1578
|
listeners.delete(listener);
|
|
1371
1579
|
};
|
|
1372
1580
|
};
|
|
1581
|
+
const uploadFile = async ({ key, file, fileName }) => {
|
|
1582
|
+
const field = declared.get(key);
|
|
1583
|
+
if (!field) throw new FormsError(`Unknown field: ${key}`);
|
|
1584
|
+
if (field.value_type !== "file") throw new FormsError(`Field "${key}" is not a file field`);
|
|
1585
|
+
if (!uploadConfig) throw new FormsError("File uploads require a valid publishable key and initialized Embeddables core instance.");
|
|
1586
|
+
const rules = field.validations;
|
|
1587
|
+
const contentType = file.type || "application/octet-stream";
|
|
1588
|
+
const byteLength = file.size;
|
|
1589
|
+
if (byteLength > 26214400) throw new FormsError("Maximum upload size is 25 MiB.");
|
|
1590
|
+
if (rules?.maxSize !== void 0 && byteLength > rules.maxSize) throw new FormsError(`${field.label} must be at most ${rules.maxSize} bytes`);
|
|
1591
|
+
if (rules?.accept !== void 0 && !contentTypeMatchesAccept({
|
|
1592
|
+
contentType,
|
|
1593
|
+
accept: rules.accept
|
|
1594
|
+
})) throw new FormsError(`${field.label} must be one of the allowed file types`);
|
|
1595
|
+
return uploadFormFile(uploadConfig, {
|
|
1596
|
+
formId: resolved.formKey,
|
|
1597
|
+
fieldKey: key,
|
|
1598
|
+
file,
|
|
1599
|
+
fileName
|
|
1600
|
+
});
|
|
1601
|
+
};
|
|
1373
1602
|
return {
|
|
1374
1603
|
key: schema.id,
|
|
1375
1604
|
set,
|
|
1376
1605
|
get,
|
|
1606
|
+
getFieldByKey,
|
|
1377
1607
|
getValueByProtocolFieldId,
|
|
1378
1608
|
getAll,
|
|
1379
1609
|
submit,
|
|
1380
1610
|
validate,
|
|
1381
1611
|
errors: () => freeze(state.errors),
|
|
1382
1612
|
clear,
|
|
1613
|
+
uploadFile,
|
|
1383
1614
|
subscribe
|
|
1384
1615
|
};
|
|
1385
1616
|
}
|
|
@@ -1439,4 +1670,4 @@ Object.defineProperty(exports, "seedServerFormsStorageFromCookies", {
|
|
|
1439
1670
|
}
|
|
1440
1671
|
});
|
|
1441
1672
|
|
|
1442
|
-
//# sourceMappingURL=form-
|
|
1673
|
+
//# sourceMappingURL=form-lmr-O7j7.cjs.map
|