@form-engine-ts/core 1.0.0 → 2.0.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 +34 -0
- package/dist/index.cjs +586 -57
- package/dist/index.d.cts +107 -9
- package/dist/index.d.ts +107 -9
- package/dist/index.js +579 -57
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -23,17 +23,24 @@ __export(index_exports, {
|
|
|
23
23
|
aggregateResponses: () => aggregateResponses,
|
|
24
24
|
assertValidFormSchema: () => assertValidFormSchema,
|
|
25
25
|
calculateChoiceDistribution: () => calculateChoiceDistribution,
|
|
26
|
+
calculateCrossTabulation: () => calculateCrossTabulation,
|
|
26
27
|
calculateFieldVisibility: () => calculateFieldVisibility,
|
|
27
28
|
calculateNumericSummary: () => calculateNumericSummary,
|
|
29
|
+
calculatePageVisibility: () => calculatePageVisibility,
|
|
28
30
|
createSubmission: () => createSubmission,
|
|
31
|
+
dispatchWebhook: () => dispatchWebhook,
|
|
29
32
|
escapeCsvCell: () => escapeCsvCell,
|
|
30
33
|
exportResponsesToCsv: () => exportResponsesToCsv,
|
|
34
|
+
isDisplayConditionSatisfied: () => isDisplayConditionSatisfied,
|
|
31
35
|
isQuestionVisible: () => isQuestionVisible,
|
|
36
|
+
populateSchemaTranslations: () => populateSchemaTranslations,
|
|
32
37
|
resolveFormTranslation: () => resolveFormTranslation,
|
|
38
|
+
resolveLocalizedSchema: () => resolveLocalizedSchema,
|
|
33
39
|
sanitizeSchema: () => sanitizeSchema,
|
|
34
40
|
selectVisibleAnswers: () => selectVisibleAnswers,
|
|
35
41
|
validateAnswers: () => validateAnswers,
|
|
36
42
|
validateFormSchema: () => validateFormSchema,
|
|
43
|
+
validatePageAnswers: () => validatePageAnswers,
|
|
37
44
|
validateSchemaStructure: () => validateSchemaStructure
|
|
38
45
|
});
|
|
39
46
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -126,17 +133,46 @@ function validateSchemaStructure(schema) {
|
|
|
126
133
|
function sanitizeSchema(schema) {
|
|
127
134
|
const existingQuestionIds = new Set(schema.fields.map((field) => field.id));
|
|
128
135
|
const cyclic = cyclicQuestionIds(schema.fields);
|
|
129
|
-
|
|
136
|
+
const sanitizedFields = schema.fields.map((field) => {
|
|
137
|
+
const sourceId = field.displayCondition?.questionId;
|
|
138
|
+
if (sourceId === void 0 || existingQuestionIds.has(sourceId) && sourceId !== field.id && !cyclic.has(field.id)) {
|
|
139
|
+
return field;
|
|
140
|
+
}
|
|
141
|
+
const { displayCondition: _displayCondition, ...sanitized } = field;
|
|
142
|
+
return sanitized;
|
|
143
|
+
});
|
|
144
|
+
const base = {
|
|
130
145
|
...schema,
|
|
131
|
-
fields:
|
|
132
|
-
const sourceId = field.displayCondition?.questionId;
|
|
133
|
-
if (sourceId === void 0 || existingQuestionIds.has(sourceId) && sourceId !== field.id && !cyclic.has(field.id)) {
|
|
134
|
-
return field;
|
|
135
|
-
}
|
|
136
|
-
const { displayCondition: _displayCondition, ...sanitized } = field;
|
|
137
|
-
return sanitized;
|
|
138
|
-
})
|
|
146
|
+
fields: sanitizedFields
|
|
139
147
|
};
|
|
148
|
+
if (schema.pages === void 0) return base;
|
|
149
|
+
const assigned = /* @__PURE__ */ new Set();
|
|
150
|
+
const pages = schema.pages.map((page) => ({
|
|
151
|
+
...page,
|
|
152
|
+
questionIds: page.questionIds.filter((id) => {
|
|
153
|
+
if (!existingQuestionIds.has(id) || assigned.has(id)) return false;
|
|
154
|
+
assigned.add(id);
|
|
155
|
+
return true;
|
|
156
|
+
})
|
|
157
|
+
})).filter((page) => page.questionIds.length > 0);
|
|
158
|
+
if (pages.length === 0) {
|
|
159
|
+
const { pages: _pages, ...singlePage } = base;
|
|
160
|
+
return singlePage;
|
|
161
|
+
}
|
|
162
|
+
const unassigned = schema.fields.map((field) => field.id).filter((id) => !assigned.has(id));
|
|
163
|
+
const completePages = pages.map(
|
|
164
|
+
(page, index) => index === pages.length - 1 ? { ...page, questionIds: [...page.questionIds, ...unassigned] } : page
|
|
165
|
+
);
|
|
166
|
+
const pageIndexByQuestion = new Map(
|
|
167
|
+
completePages.flatMap((page, pageIndex) => page.questionIds.map((id) => [id, pageIndex]))
|
|
168
|
+
);
|
|
169
|
+
const safePages = completePages.map((page, pageIndex) => {
|
|
170
|
+
const sourceIndex = page.displayCondition === void 0 ? void 0 : pageIndexByQuestion.get(page.displayCondition.questionId);
|
|
171
|
+
if (page.displayCondition === void 0 || sourceIndex !== void 0 && sourceIndex < pageIndex) return page;
|
|
172
|
+
const { displayCondition: _displayCondition, ...safePage } = page;
|
|
173
|
+
return safePage;
|
|
174
|
+
});
|
|
175
|
+
return { ...base, pages: safePages };
|
|
140
176
|
}
|
|
141
177
|
|
|
142
178
|
// src/schema.ts
|
|
@@ -151,6 +187,80 @@ function isNonEmptyString(value) {
|
|
|
151
187
|
function issue(issues, path, code, message) {
|
|
152
188
|
issues.push({ path, code, message });
|
|
153
189
|
}
|
|
190
|
+
function validateJsonValue(value, path, issues, ancestors = /* @__PURE__ */ new Set()) {
|
|
191
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
192
|
+
if (typeof value === "number") {
|
|
193
|
+
if (!Number.isFinite(value)) issue(issues, path, "invalid_metadata", "Metadata numbers must be finite.");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (typeof value !== "object") {
|
|
197
|
+
issue(issues, path, "invalid_metadata", "Expected JSON-serializable metadata.");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (ancestors.has(value)) {
|
|
201
|
+
issue(issues, path, "invalid_metadata", "Metadata must not contain cycles.");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const nextAncestors = new Set(ancestors).add(value);
|
|
205
|
+
if (Array.isArray(value)) {
|
|
206
|
+
value.forEach((item, index) => {
|
|
207
|
+
validateJsonValue(item, `${path}[${index}]`, issues, nextAncestors);
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const prototype = Object.getPrototypeOf(value);
|
|
212
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
213
|
+
issue(issues, path, "invalid_metadata", "Metadata objects must be plain JSON objects.");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
for (const [key, item] of Object.entries(value)) {
|
|
217
|
+
validateJsonValue(item, `${path}.${key}`, issues, nextAncestors);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function validateExtensibleNode(value, path, issues) {
|
|
221
|
+
for (const property of ["metadata", "translationMetadata"]) {
|
|
222
|
+
const candidate = value[property];
|
|
223
|
+
if (candidate === void 0) continue;
|
|
224
|
+
if (!isRecord(candidate)) {
|
|
225
|
+
issue(
|
|
226
|
+
issues,
|
|
227
|
+
path.length === 0 ? property : `${path}.${property}`,
|
|
228
|
+
"invalid_metadata",
|
|
229
|
+
"Expected a metadata object."
|
|
230
|
+
);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
validateJsonValue(candidate, path.length === 0 ? property : `${path}.${property}`, issues);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function validateLocalizedTextMap(value, path, issues) {
|
|
237
|
+
if (!isRecord(value)) {
|
|
238
|
+
issue(issues, path, "invalid_translations", "Expected a locale-to-translation object.");
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
for (const [locale, translation] of Object.entries(value)) {
|
|
242
|
+
if (!isNonEmptyString(locale) || !isRecord(translation)) {
|
|
243
|
+
issue(issues, `${path}.${locale}`, "invalid_translation", "Expected a translation object.");
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
for (const key of ["title", "description", "completionMessage"]) {
|
|
247
|
+
if (translation[key] !== void 0 && !isNonEmptyString(translation[key])) {
|
|
248
|
+
issue(issues, `${path}.${locale}.${key}`, "invalid_translation", "Expected non-empty translated text.");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function validateOptionTranslations(value, path, issues) {
|
|
254
|
+
if (!isRecord(value)) {
|
|
255
|
+
issue(issues, path, "invalid_translations", "Expected a locale-to-label object.");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
for (const [locale, label] of Object.entries(value)) {
|
|
259
|
+
if (!isNonEmptyString(locale) || !isNonEmptyString(label)) {
|
|
260
|
+
issue(issues, `${path}.${locale}`, "invalid_translation", "Expected a non-empty translated label.");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
154
264
|
function rejectLegacyProperties(value, path, properties, issues) {
|
|
155
265
|
for (const property of properties) {
|
|
156
266
|
if (Object.hasOwn(value, property)) {
|
|
@@ -184,6 +294,7 @@ function validateOptions(value, path, issues) {
|
|
|
184
294
|
return;
|
|
185
295
|
}
|
|
186
296
|
rejectLegacyProperties(option, optionPath, ["value", "labelKey"], issues);
|
|
297
|
+
validateExtensibleNode(option, optionPath, issues);
|
|
187
298
|
if (!isNonEmptyString(option.id)) {
|
|
188
299
|
issue(issues, `${optionPath}.id`, "invalid_option_id", "Expected a non-empty option ID.");
|
|
189
300
|
} else if (seen.has(option.id)) {
|
|
@@ -194,6 +305,8 @@ function validateOptions(value, path, issues) {
|
|
|
194
305
|
if (!isNonEmptyString(option.label)) {
|
|
195
306
|
issue(issues, `${optionPath}.label`, "invalid_label", "Expected a non-empty option label.");
|
|
196
307
|
}
|
|
308
|
+
if (option.translations !== void 0)
|
|
309
|
+
validateOptionTranslations(option.translations, `${optionPath}.translations`, issues);
|
|
197
310
|
});
|
|
198
311
|
return true;
|
|
199
312
|
}
|
|
@@ -227,6 +340,7 @@ function validateField(value, path, issues) {
|
|
|
227
340
|
return false;
|
|
228
341
|
}
|
|
229
342
|
rejectLegacyProperties(value, path, ["titleKey", "labelKey", "helpTextKey", "descriptionKey"], issues);
|
|
343
|
+
validateExtensibleNode(value, path, issues);
|
|
230
344
|
if (!isNonEmptyString(value.id)) issue(issues, `${path}.id`, "invalid_id", "Expected a non-empty ID.");
|
|
231
345
|
if (!isNonEmptyString(value.title)) {
|
|
232
346
|
issue(issues, `${path}.title`, "invalid_title", "Expected a non-empty question title.");
|
|
@@ -243,6 +357,7 @@ function validateField(value, path, issues) {
|
|
|
243
357
|
if (value.displayCondition !== void 0) {
|
|
244
358
|
validateDisplayCondition(value.displayCondition, `${path}.displayCondition`, issues);
|
|
245
359
|
}
|
|
360
|
+
if (value.translations !== void 0) validateLocalizedTextMap(value.translations, `${path}.translations`, issues);
|
|
246
361
|
if (typeof value.type !== "string" || !FIELD_TYPES.has(value.type)) {
|
|
247
362
|
issue(issues, `${path}.type`, "invalid_field_type", "Unsupported field type.");
|
|
248
363
|
return false;
|
|
@@ -311,6 +426,7 @@ function validateFormSchema(input) {
|
|
|
311
426
|
return { valid: false, issues: [{ path: "$", code: "invalid_schema", message: "Expected a schema object." }] };
|
|
312
427
|
}
|
|
313
428
|
rejectLegacyProperties(input, "", ["titleKey", "descriptionKey"], issues);
|
|
429
|
+
validateExtensibleNode(input, "", issues);
|
|
314
430
|
if (!isNonEmptyString(input.id)) issue(issues, "id", "invalid_id", "Expected a non-empty ID.");
|
|
315
431
|
if (!Number.isInteger(input.version) || input.version < 1) {
|
|
316
432
|
issue(issues, "version", "invalid_version", "Expected a positive integer version.");
|
|
@@ -318,16 +434,36 @@ function validateFormSchema(input) {
|
|
|
318
434
|
if (!isNonEmptyString(input.title)) {
|
|
319
435
|
issue(issues, "title", "invalid_title", "Expected a non-empty form title.");
|
|
320
436
|
}
|
|
321
|
-
for (const key of ["description", "submitLabelKey"]) {
|
|
437
|
+
for (const key of ["description", "completionMessage", "submitLabelKey"]) {
|
|
322
438
|
if (input[key] !== void 0 && !isNonEmptyString(input[key])) {
|
|
323
439
|
issue(
|
|
324
440
|
issues,
|
|
325
441
|
key,
|
|
326
|
-
key === "
|
|
327
|
-
key === "
|
|
442
|
+
key === "submitLabelKey" ? "invalid_translation_key" : "invalid_description",
|
|
443
|
+
key === "submitLabelKey" ? "Expected a translation key." : "Expected non-empty form text."
|
|
328
444
|
);
|
|
329
445
|
}
|
|
330
446
|
}
|
|
447
|
+
if (input.defaultLocale !== void 0 && !isNonEmptyString(input.defaultLocale)) {
|
|
448
|
+
issue(issues, "defaultLocale", "invalid_locale", "Expected a non-empty default locale.");
|
|
449
|
+
}
|
|
450
|
+
if (input.supportedLocales !== void 0) {
|
|
451
|
+
if (!Array.isArray(input.supportedLocales) || input.supportedLocales.length === 0) {
|
|
452
|
+
issue(issues, "supportedLocales", "invalid_locales", "Expected at least one supported locale.");
|
|
453
|
+
} else {
|
|
454
|
+
const locales = /* @__PURE__ */ new Set();
|
|
455
|
+
input.supportedLocales.forEach((locale, index) => {
|
|
456
|
+
if (!isNonEmptyString(locale)) {
|
|
457
|
+
issue(issues, `supportedLocales[${index}]`, "invalid_locale", "Expected a non-empty locale.");
|
|
458
|
+
} else if (locales.has(locale)) {
|
|
459
|
+
issue(issues, `supportedLocales[${index}]`, "duplicate_locale", "Locales must be unique.");
|
|
460
|
+
} else {
|
|
461
|
+
locales.add(locale);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (input.translations !== void 0) validateLocalizedTextMap(input.translations, "translations", issues);
|
|
331
467
|
if (!Array.isArray(input.fields) || input.fields.length === 0) {
|
|
332
468
|
issue(issues, "fields", "invalid_fields", "Expected at least one field.");
|
|
333
469
|
} else {
|
|
@@ -354,6 +490,81 @@ function validateFormSchema(input) {
|
|
|
354
490
|
const code = structuralIssue.type === "dangling_condition_reference" ? "unknown_condition_source" : structuralIssue.type === "self_condition_reference" ? "self_condition" : "condition_cycle";
|
|
355
491
|
issue(issues, `fields[${fieldIndex}].displayCondition`, code, structuralIssue.message);
|
|
356
492
|
}
|
|
493
|
+
if (input.pages !== void 0) {
|
|
494
|
+
if (!Array.isArray(input.pages) || input.pages.length === 0) {
|
|
495
|
+
issue(issues, "pages", "invalid_pages", "Expected at least one page when pages is defined.");
|
|
496
|
+
} else {
|
|
497
|
+
const pageIds = /* @__PURE__ */ new Set();
|
|
498
|
+
const assigned = /* @__PURE__ */ new Map();
|
|
499
|
+
input.pages.forEach((page, pageIndex) => {
|
|
500
|
+
const pagePath = `pages[${pageIndex}]`;
|
|
501
|
+
if (!isRecord(page)) {
|
|
502
|
+
issue(issues, pagePath, "invalid_page", "Expected a page object.");
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
validateExtensibleNode(page, pagePath, issues);
|
|
506
|
+
if (!isNonEmptyString(page.id)) {
|
|
507
|
+
issue(issues, `${pagePath}.id`, "invalid_page_id", "Expected a non-empty page ID.");
|
|
508
|
+
} else if (pageIds.has(page.id)) {
|
|
509
|
+
issue(issues, `${pagePath}.id`, "duplicate_page", "Page IDs must be unique.");
|
|
510
|
+
} else {
|
|
511
|
+
pageIds.add(page.id);
|
|
512
|
+
}
|
|
513
|
+
for (const key of ["title", "description"]) {
|
|
514
|
+
if (page[key] !== void 0 && !isNonEmptyString(page[key])) {
|
|
515
|
+
issue(issues, `${pagePath}.${key}`, "invalid_page_text", "Expected non-empty page text.");
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (page.translations !== void 0) {
|
|
519
|
+
validateLocalizedTextMap(page.translations, `${pagePath}.translations`, issues);
|
|
520
|
+
}
|
|
521
|
+
if (page.displayCondition !== void 0) {
|
|
522
|
+
validateDisplayCondition(page.displayCondition, `${pagePath}.displayCondition`, issues);
|
|
523
|
+
}
|
|
524
|
+
if (!Array.isArray(page.questionIds) || page.questionIds.length === 0) {
|
|
525
|
+
issue(issues, `${pagePath}.questionIds`, "invalid_page_questions", "Expected at least one question ID.");
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
page.questionIds.forEach((questionId, questionIndex) => {
|
|
529
|
+
const questionPath = `${pagePath}.questionIds[${questionIndex}]`;
|
|
530
|
+
if (!isNonEmptyString(questionId)) {
|
|
531
|
+
issue(issues, questionPath, "invalid_question_reference", "Expected a question ID.");
|
|
532
|
+
} else if (!ids.has(questionId)) {
|
|
533
|
+
issue(issues, questionPath, "unknown_page_question", "Page references an unknown question.");
|
|
534
|
+
} else if (assigned.has(questionId)) {
|
|
535
|
+
issue(issues, questionPath, "duplicate_page_question", "A question may belong to only one page.");
|
|
536
|
+
} else {
|
|
537
|
+
assigned.set(questionId, pageIndex);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
for (const fieldId of ids) {
|
|
542
|
+
if (!assigned.has(fieldId))
|
|
543
|
+
issue(issues, "pages", "unassigned_page_question", `Question ${fieldId} has no page.`);
|
|
544
|
+
}
|
|
545
|
+
input.pages.forEach((page, pageIndex) => {
|
|
546
|
+
if (!isRecord(page) || !isRecord(page.displayCondition)) return;
|
|
547
|
+
const sourceId = page.displayCondition.questionId;
|
|
548
|
+
if (typeof sourceId !== "string") return;
|
|
549
|
+
const sourcePageIndex = assigned.get(sourceId);
|
|
550
|
+
if (sourcePageIndex === void 0) {
|
|
551
|
+
issue(
|
|
552
|
+
issues,
|
|
553
|
+
`pages[${pageIndex}].displayCondition.questionId`,
|
|
554
|
+
"unknown_page_condition_source",
|
|
555
|
+
"Page condition references an unknown question."
|
|
556
|
+
);
|
|
557
|
+
} else if (sourcePageIndex >= pageIndex) {
|
|
558
|
+
issue(
|
|
559
|
+
issues,
|
|
560
|
+
`pages[${pageIndex}].displayCondition.questionId`,
|
|
561
|
+
"forward_page_condition",
|
|
562
|
+
"Page conditions must reference a question on an earlier page."
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
357
568
|
}
|
|
358
569
|
return issues.length === 0 ? { valid: true, value: input, issues: [] } : { valid: false, issues };
|
|
359
570
|
}
|
|
@@ -381,7 +592,9 @@ function valuesEqual(left, right) {
|
|
|
381
592
|
return typeof left === "string" && typeof right === "string" ? normalizeString(left) === normalizeString(right) : left === right;
|
|
382
593
|
}
|
|
383
594
|
function isQuestionVisible(question, currentAnswers) {
|
|
384
|
-
|
|
595
|
+
return isDisplayConditionSatisfied(question.displayCondition, currentAnswers);
|
|
596
|
+
}
|
|
597
|
+
function isDisplayConditionSatisfied(condition, currentAnswers) {
|
|
385
598
|
if (condition === void 0) return true;
|
|
386
599
|
const answer = currentAnswers[condition.questionId];
|
|
387
600
|
if (isEmpty(answer)) return false;
|
|
@@ -394,7 +607,7 @@ function isQuestionVisible(question, currentAnswers) {
|
|
|
394
607
|
}
|
|
395
608
|
return Array.isArray(answer) && answer.some((item) => valuesEqual(item, condition.value));
|
|
396
609
|
}
|
|
397
|
-
function
|
|
610
|
+
function calculateBaseFieldVisibility(schema, currentAnswers) {
|
|
398
611
|
const fields = new Map(schema.fields.map((field) => [field.id, field]));
|
|
399
612
|
const resolved = /* @__PURE__ */ new Map();
|
|
400
613
|
const resolving = /* @__PURE__ */ new Set();
|
|
@@ -412,7 +625,34 @@ function calculateFieldVisibility(schema, currentAnswers) {
|
|
|
412
625
|
return visible;
|
|
413
626
|
};
|
|
414
627
|
for (const field of schema.fields) resolve(field);
|
|
415
|
-
return Object.
|
|
628
|
+
return Object.fromEntries(resolved);
|
|
629
|
+
}
|
|
630
|
+
function calculatePageVisibility(schema, currentAnswers) {
|
|
631
|
+
if (schema.pages === void 0 || schema.pages.length === 0) return {};
|
|
632
|
+
const baseFieldVisibility = calculateBaseFieldVisibility(schema, currentAnswers);
|
|
633
|
+
const pageByQuestion = new Map(schema.pages.flatMap((page) => page.questionIds.map((id) => [id, page.id])));
|
|
634
|
+
const pageVisibility = {};
|
|
635
|
+
for (const page of schema.pages) {
|
|
636
|
+
const sourceId = page.displayCondition?.questionId;
|
|
637
|
+
const sourcePageId = sourceId === void 0 ? void 0 : pageByQuestion.get(sourceId);
|
|
638
|
+
const sourceVisible = sourceId === void 0 || baseFieldVisibility[sourceId] === true && sourcePageId !== void 0 && pageVisibility[sourcePageId] === true;
|
|
639
|
+
pageVisibility[page.id] = sourceVisible && isDisplayConditionSatisfied(page.displayCondition, currentAnswers);
|
|
640
|
+
}
|
|
641
|
+
return Object.freeze(pageVisibility);
|
|
642
|
+
}
|
|
643
|
+
function calculateFieldVisibility(schema, currentAnswers) {
|
|
644
|
+
const baseVisibility = calculateBaseFieldVisibility(schema, currentAnswers);
|
|
645
|
+
if (schema.pages === void 0 || schema.pages.length === 0) return Object.freeze(baseVisibility);
|
|
646
|
+
const pageVisibility = calculatePageVisibility(schema, currentAnswers);
|
|
647
|
+
const pageByQuestion = new Map(schema.pages.flatMap((page) => page.questionIds.map((id) => [id, page.id])));
|
|
648
|
+
return Object.freeze(
|
|
649
|
+
Object.fromEntries(
|
|
650
|
+
schema.fields.map((field) => {
|
|
651
|
+
const pageId = pageByQuestion.get(field.id);
|
|
652
|
+
return [field.id, baseVisibility[field.id] === true && pageId !== void 0 && pageVisibility[pageId] === true];
|
|
653
|
+
})
|
|
654
|
+
)
|
|
655
|
+
);
|
|
416
656
|
}
|
|
417
657
|
function selectVisibleAnswers(schema, currentAnswers) {
|
|
418
658
|
const visibility = calculateFieldVisibility(schema, currentAnswers);
|
|
@@ -446,6 +686,23 @@ function calculateNumericSummary(responses, questionId) {
|
|
|
446
686
|
max: numbers.length === 0 ? null : Math.max(...numbers)
|
|
447
687
|
};
|
|
448
688
|
}
|
|
689
|
+
function calculateCrossTabulation(responses, rowQuestionId, colQuestionId) {
|
|
690
|
+
const matrix = {};
|
|
691
|
+
const rowTotals = {};
|
|
692
|
+
const colTotals = {};
|
|
693
|
+
let grandTotal = 0;
|
|
694
|
+
for (const response of responses) {
|
|
695
|
+
const row = response.values[rowQuestionId];
|
|
696
|
+
const col = response.values[colQuestionId];
|
|
697
|
+
if (typeof row !== "string" || row.length === 0 || typeof col !== "string" || col.length === 0) continue;
|
|
698
|
+
matrix[row] ??= {};
|
|
699
|
+
matrix[row][col] = (matrix[row][col] ?? 0) + 1;
|
|
700
|
+
rowTotals[row] = (rowTotals[row] ?? 0) + 1;
|
|
701
|
+
colTotals[col] = (colTotals[col] ?? 0) + 1;
|
|
702
|
+
grandTotal += 1;
|
|
703
|
+
}
|
|
704
|
+
return { rowQuestionId, colQuestionId, matrix, rowTotals, colTotals, grandTotal };
|
|
705
|
+
}
|
|
449
706
|
function valueIsValid(field, value) {
|
|
450
707
|
if (value === void 0 || value === "") return false;
|
|
451
708
|
if (field.type === "text" || field.type === "textarea") {
|
|
@@ -537,15 +794,20 @@ function aggregateResponses(schema, submissions) {
|
|
|
537
794
|
questions: schema.fields.map((field) => aggregateField(schema, field, submissions))
|
|
538
795
|
};
|
|
539
796
|
}
|
|
540
|
-
function escapeCsvCell(value) {
|
|
797
|
+
function escapeCsvCell(value, neutralizeFormulas = true) {
|
|
541
798
|
if (value === null || value === void 0) return "";
|
|
542
|
-
|
|
799
|
+
let stringValue = String(value);
|
|
800
|
+
if (neutralizeFormulas && typeof value === "string") {
|
|
801
|
+
const trimmed = stringValue.trimStart();
|
|
802
|
+
if (trimmed.length > 0 && ["=", "+", "-", "@"].includes(trimmed[0] ?? "")) {
|
|
803
|
+
stringValue = `'${stringValue}`;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
543
806
|
return /[",\r\n]/.test(stringValue) ? `"${stringValue.replaceAll('"', '""')}"` : stringValue;
|
|
544
807
|
}
|
|
545
808
|
function serializeValue(value) {
|
|
546
809
|
if (value === void 0) return "";
|
|
547
|
-
|
|
548
|
-
return String(value);
|
|
810
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value);
|
|
549
811
|
}
|
|
550
812
|
function exportResponsesToCsv(schema, responses, options = {}) {
|
|
551
813
|
assertValidFormSchema(schema);
|
|
@@ -566,10 +828,52 @@ function exportResponsesToCsv(schema, responses, options = {}) {
|
|
|
566
828
|
];
|
|
567
829
|
})
|
|
568
830
|
];
|
|
569
|
-
const
|
|
831
|
+
const neutralizeFormulas = options.neutralizeFormulas ?? true;
|
|
832
|
+
const csv = rows.map((row) => row.map((cell) => escapeCsvCell(cell, neutralizeFormulas)).join(",")).join("\r\n");
|
|
570
833
|
return options.withBom ?? true ? `\uFEFF${csv}` : csv;
|
|
571
834
|
}
|
|
572
835
|
|
|
836
|
+
// src/events.ts
|
|
837
|
+
function bytesToHex(bytes) {
|
|
838
|
+
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
839
|
+
}
|
|
840
|
+
async function signPayload(payload, secret) {
|
|
841
|
+
if (globalThis.crypto?.subtle === void 0) throw new Error("Web Crypto is unavailable for webhook signing.");
|
|
842
|
+
const encoder = new TextEncoder();
|
|
843
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
844
|
+
"raw",
|
|
845
|
+
encoder.encode(secret),
|
|
846
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
847
|
+
false,
|
|
848
|
+
["sign"]
|
|
849
|
+
);
|
|
850
|
+
return bytesToHex(await globalThis.crypto.subtle.sign("HMAC", key, encoder.encode(payload)));
|
|
851
|
+
}
|
|
852
|
+
async function dispatchWebhook(event, config, fetchImpl = globalThis.fetch) {
|
|
853
|
+
const body = JSON.stringify(event);
|
|
854
|
+
const controller = new AbortController();
|
|
855
|
+
const timeoutMs = config.timeoutMs ?? 5e3;
|
|
856
|
+
const timeout = globalThis.setTimeout(() => controller.abort(), timeoutMs);
|
|
857
|
+
try {
|
|
858
|
+
const headers = { "content-type": "application/json", ...config.headers };
|
|
859
|
+
if (config.secret !== void 0) {
|
|
860
|
+
headers["X-Form-Engine-Signature"] = await signPayload(body, config.secret);
|
|
861
|
+
}
|
|
862
|
+
const response = await fetchImpl(config.url, {
|
|
863
|
+
method: "POST",
|
|
864
|
+
headers,
|
|
865
|
+
body,
|
|
866
|
+
signal: controller.signal
|
|
867
|
+
});
|
|
868
|
+
return response.ok ? { success: true, status: response.status } : { success: false, status: response.status, error: `Webhook returned HTTP ${response.status}.` };
|
|
869
|
+
} catch (cause) {
|
|
870
|
+
const error = controller.signal.aborted ? `Webhook request timed out after ${timeoutMs}ms.` : cause instanceof Error ? cause.message : String(cause);
|
|
871
|
+
return { success: false, error };
|
|
872
|
+
} finally {
|
|
873
|
+
globalThis.clearTimeout(timeout);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
573
877
|
// src/validation.ts
|
|
574
878
|
var DEFAULT_MESSAGES = {
|
|
575
879
|
required: "validation.required",
|
|
@@ -696,6 +1000,18 @@ function validateAnswers(schema, values) {
|
|
|
696
1000
|
}
|
|
697
1001
|
return issues.length === 0 ? { valid: true, issues: [] } : { valid: false, issues };
|
|
698
1002
|
}
|
|
1003
|
+
function validatePageAnswers(schema, pageIndex, values) {
|
|
1004
|
+
if (schema.pages === void 0 || schema.pages.length === 0) return validateAnswers(schema, values);
|
|
1005
|
+
const page = schema.pages[pageIndex];
|
|
1006
|
+
if (page === void 0) return { valid: true, issues: [] };
|
|
1007
|
+
const targetIds = new Set(page.questionIds);
|
|
1008
|
+
const visibility = calculateFieldVisibility(schema, values);
|
|
1009
|
+
const issues = [];
|
|
1010
|
+
for (const field of schema.fields) {
|
|
1011
|
+
if (targetIds.has(field.id) && visibility[field.id] === true) validateField2(field, values[field.id], issues);
|
|
1012
|
+
}
|
|
1013
|
+
return issues.length === 0 ? { valid: true, issues: [] } : { valid: false, issues };
|
|
1014
|
+
}
|
|
699
1015
|
|
|
700
1016
|
// src/submission.ts
|
|
701
1017
|
function cloneValues(values) {
|
|
@@ -723,68 +1039,281 @@ function createSubmission(schema, values, options) {
|
|
|
723
1039
|
formVersion: schema.version,
|
|
724
1040
|
locale: options.locale,
|
|
725
1041
|
values: Object.freeze(cloneValues(visibleValues)),
|
|
726
|
-
submittedAt: options.submittedAt
|
|
1042
|
+
submittedAt: options.submittedAt,
|
|
1043
|
+
...options.metadata === void 0 ? {} : { metadata: Object.freeze({ ...options.metadata }) },
|
|
1044
|
+
...options.translationMetadata === void 0 ? {} : { translationMetadata: Object.freeze({ ...options.translationMetadata }) }
|
|
727
1045
|
});
|
|
728
1046
|
}
|
|
729
1047
|
|
|
730
1048
|
// src/translation.ts
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const next = () => {
|
|
746
|
-
const value = translated[index];
|
|
747
|
-
index += 1;
|
|
748
|
-
if (value === void 0) throw new Error("Translation adapter returned an incomplete result.");
|
|
749
|
-
return value;
|
|
1049
|
+
function mergeLocalizedText(translations, locale, property, value) {
|
|
1050
|
+
return { ...translations, [locale]: { ...translations?.[locale], [property]: value } };
|
|
1051
|
+
}
|
|
1052
|
+
function withTranslationMetadata(node, locale, property, metadata) {
|
|
1053
|
+
if (metadata === void 0) return node;
|
|
1054
|
+
return {
|
|
1055
|
+
...node,
|
|
1056
|
+
translationMetadata: {
|
|
1057
|
+
...node.translationMetadata,
|
|
1058
|
+
[locale]: {
|
|
1059
|
+
...node.translationMetadata?.[locale],
|
|
1060
|
+
[property]: metadata
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
750
1063
|
};
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
1064
|
+
}
|
|
1065
|
+
function createSlot(kind, nodeId, property, locale, sourceText, existingText, metadata) {
|
|
1066
|
+
return {
|
|
1067
|
+
kind,
|
|
1068
|
+
nodeId,
|
|
1069
|
+
property,
|
|
1070
|
+
locale,
|
|
1071
|
+
sourceText,
|
|
1072
|
+
...existingText === void 0 ? {} : { existingText },
|
|
1073
|
+
...metadata === void 0 ? {} : { metadata }
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
function translationSlots(schema, locale) {
|
|
1077
|
+
const descriptors = [];
|
|
1078
|
+
const addFormSlot = (property, sourceText) => {
|
|
1079
|
+
const slot = createSlot(
|
|
1080
|
+
"form",
|
|
1081
|
+
schema.id,
|
|
1082
|
+
property,
|
|
1083
|
+
locale,
|
|
1084
|
+
sourceText,
|
|
1085
|
+
schema.translations?.[locale]?.[property],
|
|
1086
|
+
schema.metadata
|
|
1087
|
+
);
|
|
1088
|
+
descriptors.push({
|
|
1089
|
+
slot,
|
|
1090
|
+
apply: (current, value, metadata) => withTranslationMetadata(
|
|
1091
|
+
{ ...current, translations: mergeLocalizedText(current.translations, locale, property, value) },
|
|
1092
|
+
locale,
|
|
1093
|
+
property,
|
|
1094
|
+
metadata
|
|
1095
|
+
)
|
|
1096
|
+
});
|
|
1097
|
+
};
|
|
1098
|
+
addFormSlot("title", schema.title);
|
|
1099
|
+
if (schema.description !== void 0) addFormSlot("description", schema.description);
|
|
1100
|
+
if (schema.completionMessage !== void 0) addFormSlot("completionMessage", schema.completionMessage);
|
|
1101
|
+
schema.fields.forEach((field, fieldIndex) => {
|
|
1102
|
+
const addFieldSlot = (property, sourceText) => {
|
|
1103
|
+
const slot = createSlot(
|
|
1104
|
+
"field",
|
|
1105
|
+
field.id,
|
|
1106
|
+
property,
|
|
1107
|
+
locale,
|
|
1108
|
+
sourceText,
|
|
1109
|
+
field.translations?.[locale]?.[property],
|
|
1110
|
+
field.metadata
|
|
1111
|
+
);
|
|
1112
|
+
descriptors.push({
|
|
1113
|
+
slot,
|
|
1114
|
+
apply: (current, value, metadata) => ({
|
|
1115
|
+
...current,
|
|
1116
|
+
fields: current.fields.map(
|
|
1117
|
+
(candidate, index) => index === fieldIndex ? withTranslationMetadata(
|
|
1118
|
+
{
|
|
1119
|
+
...candidate,
|
|
1120
|
+
translations: mergeLocalizedText(candidate.translations, locale, property, value)
|
|
1121
|
+
},
|
|
1122
|
+
locale,
|
|
1123
|
+
property,
|
|
1124
|
+
metadata
|
|
1125
|
+
) : candidate
|
|
1126
|
+
)
|
|
1127
|
+
})
|
|
1128
|
+
});
|
|
1129
|
+
};
|
|
1130
|
+
addFieldSlot("title", field.title);
|
|
1131
|
+
if (field.description !== void 0) addFieldSlot("description", field.description);
|
|
1132
|
+
if ("options" in field) {
|
|
1133
|
+
field.options.forEach((option, optionIndex) => {
|
|
1134
|
+
const slot = createSlot(
|
|
1135
|
+
"option",
|
|
1136
|
+
option.id,
|
|
1137
|
+
"label",
|
|
1138
|
+
locale,
|
|
1139
|
+
option.label,
|
|
1140
|
+
option.translations?.[locale],
|
|
1141
|
+
option.metadata
|
|
1142
|
+
);
|
|
1143
|
+
descriptors.push({
|
|
1144
|
+
slot,
|
|
1145
|
+
apply: (current, value, metadata) => ({
|
|
1146
|
+
...current,
|
|
1147
|
+
fields: current.fields.map((candidate, candidateIndex) => {
|
|
1148
|
+
if (candidateIndex !== fieldIndex || !("options" in candidate)) return candidate;
|
|
1149
|
+
return {
|
|
1150
|
+
...candidate,
|
|
1151
|
+
options: candidate.options.map(
|
|
1152
|
+
(candidateOption, candidateOptionIndex) => candidateOptionIndex === optionIndex ? withTranslationMetadata(
|
|
1153
|
+
{
|
|
1154
|
+
...candidateOption,
|
|
1155
|
+
translations: { ...candidateOption.translations, [locale]: value }
|
|
1156
|
+
},
|
|
1157
|
+
locale,
|
|
1158
|
+
"label",
|
|
1159
|
+
metadata
|
|
1160
|
+
) : candidateOption
|
|
1161
|
+
)
|
|
1162
|
+
};
|
|
1163
|
+
})
|
|
1164
|
+
})
|
|
1165
|
+
});
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
schema.pages?.forEach((page, pageIndex) => {
|
|
1170
|
+
const addPageSlot = (property, sourceText) => {
|
|
1171
|
+
const slot = createSlot(
|
|
1172
|
+
"page",
|
|
1173
|
+
page.id,
|
|
1174
|
+
property,
|
|
1175
|
+
locale,
|
|
1176
|
+
sourceText,
|
|
1177
|
+
page.translations?.[locale]?.[property],
|
|
1178
|
+
page.metadata
|
|
1179
|
+
);
|
|
1180
|
+
descriptors.push({
|
|
1181
|
+
slot,
|
|
1182
|
+
apply: (current, value, metadata) => ({
|
|
1183
|
+
...current,
|
|
1184
|
+
...current.pages === void 0 ? {} : {
|
|
1185
|
+
pages: current.pages.map(
|
|
1186
|
+
(candidate, index) => index === pageIndex ? withTranslationMetadata(
|
|
1187
|
+
{
|
|
1188
|
+
...candidate,
|
|
1189
|
+
translations: mergeLocalizedText(candidate.translations, locale, property, value)
|
|
1190
|
+
},
|
|
1191
|
+
locale,
|
|
1192
|
+
property,
|
|
1193
|
+
metadata
|
|
1194
|
+
) : candidate
|
|
1195
|
+
)
|
|
1196
|
+
}
|
|
1197
|
+
})
|
|
1198
|
+
});
|
|
760
1199
|
};
|
|
761
|
-
if (
|
|
762
|
-
|
|
1200
|
+
if (page.title !== void 0) addPageSlot("title", page.title);
|
|
1201
|
+
if (page.description !== void 0) addPageSlot("description", page.description);
|
|
763
1202
|
});
|
|
764
|
-
|
|
1203
|
+
return descriptors;
|
|
1204
|
+
}
|
|
1205
|
+
function resolveLocalizedSchema(schema, targetLocale) {
|
|
1206
|
+
if (targetLocale.length === 0 || targetLocale === schema.defaultLocale) return schema;
|
|
1207
|
+
const formTranslation = schema.translations?.[targetLocale];
|
|
1208
|
+
const completionMessage = formTranslation?.completionMessage ?? schema.completionMessage;
|
|
1209
|
+
return {
|
|
765
1210
|
...schema,
|
|
766
|
-
title,
|
|
767
|
-
...description === void 0 ? {} : { description },
|
|
768
|
-
|
|
1211
|
+
title: formTranslation?.title ?? schema.title,
|
|
1212
|
+
...(formTranslation?.description ?? schema.description) === void 0 ? {} : { description: formTranslation?.description ?? schema.description },
|
|
1213
|
+
...completionMessage === void 0 ? {} : { completionMessage },
|
|
1214
|
+
fields: schema.fields.map((field) => {
|
|
1215
|
+
const translation = field.translations?.[targetLocale];
|
|
1216
|
+
const localized = {
|
|
1217
|
+
...field,
|
|
1218
|
+
title: translation?.title ?? field.title,
|
|
1219
|
+
...(translation?.description ?? field.description) === void 0 ? {} : { description: translation?.description ?? field.description }
|
|
1220
|
+
};
|
|
1221
|
+
if (!("options" in field)) return localized;
|
|
1222
|
+
return {
|
|
1223
|
+
...localized,
|
|
1224
|
+
options: field.options.map((option) => ({
|
|
1225
|
+
...option,
|
|
1226
|
+
label: option.translations?.[targetLocale] ?? option.label
|
|
1227
|
+
}))
|
|
1228
|
+
};
|
|
1229
|
+
}),
|
|
1230
|
+
...schema.pages === void 0 ? {} : {
|
|
1231
|
+
pages: schema.pages.map((page) => {
|
|
1232
|
+
const translation = page.translations?.[targetLocale];
|
|
1233
|
+
const title = translation?.title ?? page.title;
|
|
1234
|
+
const description = translation?.description ?? page.description;
|
|
1235
|
+
return {
|
|
1236
|
+
...page,
|
|
1237
|
+
...title === void 0 ? {} : { title },
|
|
1238
|
+
...description === void 0 ? {} : { description }
|
|
1239
|
+
};
|
|
1240
|
+
})
|
|
1241
|
+
}
|
|
769
1242
|
};
|
|
770
|
-
|
|
771
|
-
|
|
1243
|
+
}
|
|
1244
|
+
async function populateSchemaTranslations(schema, targetLocales, adapter, options = {}) {
|
|
1245
|
+
assertValidFormSchema(schema);
|
|
1246
|
+
const locales = [...new Set(targetLocales.filter((locale) => locale.length > 0 && locale !== schema.defaultLocale))];
|
|
1247
|
+
const updatedSlots = [];
|
|
1248
|
+
const skippedSlots = [];
|
|
1249
|
+
let result = schema;
|
|
1250
|
+
for (const locale of locales) {
|
|
1251
|
+
const descriptors = translationSlots(schema, locale);
|
|
1252
|
+
const selected = [];
|
|
1253
|
+
for (const descriptor of descriptors) {
|
|
1254
|
+
const shouldTranslate = options.shouldOverwrite?.(descriptor.slot) ?? (options.overwrite === "all" || descriptor.slot.existingText === void 0);
|
|
1255
|
+
if (shouldTranslate) selected.push(descriptor);
|
|
1256
|
+
else skippedSlots.push(descriptor.slot);
|
|
1257
|
+
}
|
|
1258
|
+
if (selected.length === 0) continue;
|
|
1259
|
+
const translated = await adapter.translateBatch(
|
|
1260
|
+
selected.map((descriptor) => descriptor.slot.sourceText),
|
|
1261
|
+
locale,
|
|
1262
|
+
schema.defaultLocale
|
|
1263
|
+
);
|
|
1264
|
+
if (translated.length !== selected.length) {
|
|
1265
|
+
throw new Error(`Translation adapter returned ${translated.length} texts for ${selected.length} inputs.`);
|
|
1266
|
+
}
|
|
1267
|
+
selected.forEach((descriptor, index) => {
|
|
1268
|
+
const translatedText = translated[index];
|
|
1269
|
+
if (translatedText === void 0) throw new Error("Translation adapter returned an unexpected result.");
|
|
1270
|
+
const metadata = options.createMetadata?.(descriptor.slot, translatedText);
|
|
1271
|
+
result = descriptor.apply(result, translatedText, metadata);
|
|
1272
|
+
updatedSlots.push(descriptor.slot);
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
const supportedLocales = [
|
|
1276
|
+
.../* @__PURE__ */ new Set([
|
|
1277
|
+
...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
|
|
1278
|
+
...schema.supportedLocales ?? [],
|
|
1279
|
+
...locales
|
|
1280
|
+
])
|
|
1281
|
+
];
|
|
1282
|
+
if (supportedLocales.length > 0) result = { ...result, supportedLocales };
|
|
1283
|
+
assertValidFormSchema(result);
|
|
1284
|
+
return { schema: result, report: { updatedSlots, skippedSlots } };
|
|
1285
|
+
}
|
|
1286
|
+
async function resolveFormTranslation(schema, adapter, targetLocale, sourceLocale) {
|
|
1287
|
+
const populated = await populateSchemaTranslations(
|
|
1288
|
+
sourceLocale === void 0 ? schema : { ...schema, defaultLocale: sourceLocale },
|
|
1289
|
+
[targetLocale],
|
|
1290
|
+
adapter,
|
|
1291
|
+
{ overwrite: "all" }
|
|
1292
|
+
);
|
|
1293
|
+
return resolveLocalizedSchema(populated.schema, targetLocale);
|
|
772
1294
|
}
|
|
773
1295
|
// Annotate the CommonJS export names for ESM import in node:
|
|
774
1296
|
0 && (module.exports = {
|
|
775
1297
|
aggregateResponses,
|
|
776
1298
|
assertValidFormSchema,
|
|
777
1299
|
calculateChoiceDistribution,
|
|
1300
|
+
calculateCrossTabulation,
|
|
778
1301
|
calculateFieldVisibility,
|
|
779
1302
|
calculateNumericSummary,
|
|
1303
|
+
calculatePageVisibility,
|
|
780
1304
|
createSubmission,
|
|
1305
|
+
dispatchWebhook,
|
|
781
1306
|
escapeCsvCell,
|
|
782
1307
|
exportResponsesToCsv,
|
|
1308
|
+
isDisplayConditionSatisfied,
|
|
783
1309
|
isQuestionVisible,
|
|
1310
|
+
populateSchemaTranslations,
|
|
784
1311
|
resolveFormTranslation,
|
|
1312
|
+
resolveLocalizedSchema,
|
|
785
1313
|
sanitizeSchema,
|
|
786
1314
|
selectVisibleAnswers,
|
|
787
1315
|
validateAnswers,
|
|
788
1316
|
validateFormSchema,
|
|
1317
|
+
validatePageAnswers,
|
|
789
1318
|
validateSchemaStructure
|
|
790
1319
|
});
|