@form-engine-ts/core 2.1.1 → 2.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 +9 -2
- package/dist/index.cjs +162 -5
- package/dist/index.d.cts +16 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +161 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,8 +40,15 @@ round-trips. `completionMessage` is localized with the rest of the form text.
|
|
|
40
40
|
|
|
41
41
|
`transformFieldType` changes a question's type without discarding source text, translations, conditions, or extension
|
|
42
42
|
metadata. `validateFormSchema(schema, { policy })` applies the framework-independent `FormPolicy`, including field,
|
|
43
|
-
option, text, serialized-byte, allowed-type, and
|
|
44
|
-
|
|
43
|
+
option, text, serialized-byte, allowed-type, and locale constraints. `allowedLocales` constrains the default and supported
|
|
44
|
+
locales, `maxLocales` limits their unique total, and contradictory required/allowed locale policies are reported.
|
|
45
|
+
Required locales cover every source text that exists on the form, its fields, options, and pages.
|
|
46
|
+
|
|
47
|
+
`collectSchemaLocales(schema)` scans registrations plus every form/page/field/option `translations` and
|
|
48
|
+
`translationMetadata` key. Validation reports unregistered translation locales and applies `allowedLocales` and
|
|
49
|
+
`maxLocales` to the complete collected set. `sanitizeSchema` purges unregistered locale content. Pass
|
|
50
|
+
`{ policy: { allowedLocales, maxLocales } }` to `populateSchemaTranslations` to reject inadmissible targets before the
|
|
51
|
+
translation adapter runs.
|
|
45
52
|
|
|
46
53
|
Translation callbacks receive `nodeMetadata` and `existingTranslationMetadata` separately. The deprecated `metadata`
|
|
47
54
|
slot property remains an alias for `nodeMetadata` during migration.
|
package/dist/index.cjs
CHANGED
|
@@ -27,6 +27,7 @@ __export(index_exports, {
|
|
|
27
27
|
calculateFieldVisibility: () => calculateFieldVisibility,
|
|
28
28
|
calculateNumericSummary: () => calculateNumericSummary,
|
|
29
29
|
calculatePageVisibility: () => calculatePageVisibility,
|
|
30
|
+
collectSchemaLocales: () => collectSchemaLocales,
|
|
30
31
|
createSubmission: () => createSubmission,
|
|
31
32
|
dispatchWebhook: () => dispatchWebhook,
|
|
32
33
|
escapeCsvCell: () => escapeCsvCell,
|
|
@@ -46,7 +47,95 @@ __export(index_exports, {
|
|
|
46
47
|
});
|
|
47
48
|
module.exports = __toCommonJS(index_exports);
|
|
48
49
|
|
|
50
|
+
// src/policy.ts
|
|
51
|
+
function collectRecordKeys(value, path, pathsByLocale) {
|
|
52
|
+
for (const locale of Object.keys(value ?? {})) {
|
|
53
|
+
const paths = pathsByLocale.get(locale) ?? [];
|
|
54
|
+
paths.push(`${path}.${locale}`);
|
|
55
|
+
pathsByLocale.set(locale, paths);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function collectSchemaLocales(schema) {
|
|
59
|
+
const pathsByLocale = /* @__PURE__ */ new Map();
|
|
60
|
+
collectRecordKeys(schema.translations, "translations", pathsByLocale);
|
|
61
|
+
collectRecordKeys(schema.translationMetadata, "translationMetadata", pathsByLocale);
|
|
62
|
+
schema.fields.forEach((field, fieldIndex) => {
|
|
63
|
+
collectRecordKeys(field.translations, `fields[${fieldIndex}].translations`, pathsByLocale);
|
|
64
|
+
collectRecordKeys(field.translationMetadata, `fields[${fieldIndex}].translationMetadata`, pathsByLocale);
|
|
65
|
+
if (!("options" in field)) return;
|
|
66
|
+
field.options.forEach((option, optionIndex) => {
|
|
67
|
+
collectRecordKeys(
|
|
68
|
+
option.translations,
|
|
69
|
+
`fields[${fieldIndex}].options[${optionIndex}].translations`,
|
|
70
|
+
pathsByLocale
|
|
71
|
+
);
|
|
72
|
+
collectRecordKeys(
|
|
73
|
+
option.translationMetadata,
|
|
74
|
+
`fields[${fieldIndex}].options[${optionIndex}].translationMetadata`,
|
|
75
|
+
pathsByLocale
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
schema.pages?.forEach((page, pageIndex) => {
|
|
80
|
+
collectRecordKeys(page.translations, `pages[${pageIndex}].translations`, pathsByLocale);
|
|
81
|
+
collectRecordKeys(page.translationMetadata, `pages[${pageIndex}].translationMetadata`, pathsByLocale);
|
|
82
|
+
});
|
|
83
|
+
const translationLocales = new Set(pathsByLocale.keys());
|
|
84
|
+
const allUniqueLocales = /* @__PURE__ */ new Set([
|
|
85
|
+
...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
|
|
86
|
+
...schema.supportedLocales ?? [],
|
|
87
|
+
...translationLocales
|
|
88
|
+
]);
|
|
89
|
+
return {
|
|
90
|
+
...schema.defaultLocale === void 0 ? {} : { defaultLocale: schema.defaultLocale },
|
|
91
|
+
supportedLocales: schema.supportedLocales ?? [],
|
|
92
|
+
translationLocales,
|
|
93
|
+
allUniqueLocales,
|
|
94
|
+
translationLocalePaths: pathsByLocale
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
49
98
|
// src/sanitization.ts
|
|
99
|
+
function registeredEntries(value, registeredLocales) {
|
|
100
|
+
if (value === void 0) return void 0;
|
|
101
|
+
const entries = Object.entries(value).filter(([locale]) => registeredLocales.has(locale));
|
|
102
|
+
return entries.length === 0 ? void 0 : Object.fromEntries(entries);
|
|
103
|
+
}
|
|
104
|
+
function sanitizeNodeLocales(node, registeredLocales) {
|
|
105
|
+
const { translationMetadata: _translationMetadata, ...base } = node;
|
|
106
|
+
const translationMetadata = registeredEntries(node.translationMetadata, registeredLocales);
|
|
107
|
+
return {
|
|
108
|
+
...base,
|
|
109
|
+
...translationMetadata === void 0 ? {} : { translationMetadata }
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function sanitizeOptionLocales(option, registeredLocales) {
|
|
113
|
+
const { translations: _translations, ...base } = sanitizeNodeLocales(option, registeredLocales);
|
|
114
|
+
const translations = registeredEntries(option.translations, registeredLocales);
|
|
115
|
+
return { ...base, ...translations === void 0 ? {} : { translations } };
|
|
116
|
+
}
|
|
117
|
+
function sanitizeFieldLocales(field, registeredLocales) {
|
|
118
|
+
const localizedNode = sanitizeNodeLocales(field, registeredLocales);
|
|
119
|
+
const { translations: _translations, ...base } = localizedNode;
|
|
120
|
+
const translations = registeredEntries(field.translations, registeredLocales);
|
|
121
|
+
const localized = {
|
|
122
|
+
...base,
|
|
123
|
+
...translations === void 0 ? {} : { translations }
|
|
124
|
+
};
|
|
125
|
+
if (!("options" in localizedNode)) return localized;
|
|
126
|
+
const { translations: _choiceTranslations, ...choiceBase } = localizedNode;
|
|
127
|
+
return {
|
|
128
|
+
...choiceBase,
|
|
129
|
+
...translations === void 0 ? {} : { translations },
|
|
130
|
+
options: localizedNode.options.map((option) => sanitizeOptionLocales(option, registeredLocales))
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function sanitizePageLocales(page, registeredLocales) {
|
|
134
|
+
const localizedNode = sanitizeNodeLocales(page, registeredLocales);
|
|
135
|
+
const { translations: _translations, ...base } = localizedNode;
|
|
136
|
+
const translations = registeredEntries(page.translations, registeredLocales);
|
|
137
|
+
return { ...base, ...translations === void 0 ? {} : { translations } };
|
|
138
|
+
}
|
|
50
139
|
function cyclicQuestionIds(fields) {
|
|
51
140
|
const firstById = /* @__PURE__ */ new Map();
|
|
52
141
|
for (const field of fields) {
|
|
@@ -133,8 +222,13 @@ function validateSchemaStructure(schema) {
|
|
|
133
222
|
}
|
|
134
223
|
function sanitizeSchema(schema) {
|
|
135
224
|
const existingQuestionIds = new Set(schema.fields.map((field) => field.id));
|
|
225
|
+
const registeredLocales = /* @__PURE__ */ new Set([
|
|
226
|
+
...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
|
|
227
|
+
...schema.supportedLocales ?? []
|
|
228
|
+
]);
|
|
136
229
|
const cyclic = cyclicQuestionIds(schema.fields);
|
|
137
|
-
const sanitizedFields = schema.fields.map((
|
|
230
|
+
const sanitizedFields = schema.fields.map((sourceField) => {
|
|
231
|
+
const field = sanitizeFieldLocales(sourceField, registeredLocales);
|
|
138
232
|
const sourceId = field.displayCondition?.questionId;
|
|
139
233
|
if (sourceId === void 0 || existingQuestionIds.has(sourceId) && sourceId !== field.id && !cyclic.has(field.id)) {
|
|
140
234
|
return field;
|
|
@@ -142,15 +236,19 @@ function sanitizeSchema(schema) {
|
|
|
142
236
|
const { displayCondition: _displayCondition, ...sanitized } = field;
|
|
143
237
|
return sanitized;
|
|
144
238
|
});
|
|
239
|
+
const localizedSchema = sanitizeNodeLocales(schema, registeredLocales);
|
|
240
|
+
const { translations: _translations, ...schemaWithoutLocaleContent } = localizedSchema;
|
|
241
|
+
const translations = registeredEntries(schema.translations, registeredLocales);
|
|
145
242
|
const base = {
|
|
146
|
-
...
|
|
243
|
+
...schemaWithoutLocaleContent,
|
|
244
|
+
...translations === void 0 ? {} : { translations },
|
|
147
245
|
fields: sanitizedFields
|
|
148
246
|
};
|
|
149
247
|
if (schema.pages === void 0) return base;
|
|
150
248
|
const assigned = /* @__PURE__ */ new Set();
|
|
151
|
-
const pages = schema.pages.map((
|
|
152
|
-
...
|
|
153
|
-
questionIds:
|
|
249
|
+
const pages = schema.pages.map((sourcePage) => ({
|
|
250
|
+
...sanitizePageLocales(sourcePage, registeredLocales),
|
|
251
|
+
questionIds: sourcePage.questionIds.filter((id) => {
|
|
154
252
|
if (!existingQuestionIds.has(id) || assigned.has(id)) return false;
|
|
155
253
|
assigned.add(id);
|
|
156
254
|
return true;
|
|
@@ -554,6 +652,52 @@ function validatePolicy(schema, policy, issues) {
|
|
|
554
652
|
}
|
|
555
653
|
}
|
|
556
654
|
}
|
|
655
|
+
const collectedLocales = collectSchemaLocales(schema);
|
|
656
|
+
const registeredLocales = /* @__PURE__ */ new Set([
|
|
657
|
+
...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
|
|
658
|
+
...schema.supportedLocales ?? []
|
|
659
|
+
]);
|
|
660
|
+
for (const locale of collectedLocales.translationLocales) {
|
|
661
|
+
if (registeredLocales.has(locale)) continue;
|
|
662
|
+
for (const path of collectedLocales.translationLocalePaths.get(locale) ?? []) {
|
|
663
|
+
issue(
|
|
664
|
+
issues,
|
|
665
|
+
path,
|
|
666
|
+
"unregistered_translation_locale",
|
|
667
|
+
`Translation locale ${locale} is not registered by defaultLocale or supportedLocales.`
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (policy.allowedLocales !== void 0) {
|
|
672
|
+
const pathsByLocale = /* @__PURE__ */ new Map();
|
|
673
|
+
if (schema.defaultLocale !== void 0) pathsByLocale.set(schema.defaultLocale, ["defaultLocale"]);
|
|
674
|
+
schema.supportedLocales?.forEach((locale, index) => {
|
|
675
|
+
pathsByLocale.set(locale, [...pathsByLocale.get(locale) ?? [], `supportedLocales[${index}]`]);
|
|
676
|
+
});
|
|
677
|
+
for (const [locale, paths] of collectedLocales.translationLocalePaths) {
|
|
678
|
+
pathsByLocale.set(locale, [...pathsByLocale.get(locale) ?? [], ...paths]);
|
|
679
|
+
}
|
|
680
|
+
for (const [locale, paths] of pathsByLocale) {
|
|
681
|
+
if (!policy.allowedLocales.includes(locale)) {
|
|
682
|
+
for (const path of paths) {
|
|
683
|
+
issue(issues, path, "disallowed_locale", `Locale ${locale} is not allowed by the form policy.`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
for (const locale of policy.requiredLocales ?? []) {
|
|
688
|
+
if (!policy.allowedLocales.includes(locale)) {
|
|
689
|
+
issue(
|
|
690
|
+
issues,
|
|
691
|
+
"policy.requiredLocales",
|
|
692
|
+
"required_locale_not_allowed",
|
|
693
|
+
`Required locale ${locale} is not included in allowedLocales.`
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (policy.maxLocales !== void 0 && collectedLocales.allUniqueLocales.size > policy.maxLocales) {
|
|
699
|
+
issue(issues, "supportedLocales", "max_locales_exceeded", `At most ${policy.maxLocales} locales are allowed.`);
|
|
700
|
+
}
|
|
557
701
|
for (const locale of policy.requiredLocales ?? []) addRequiredTranslationIssues(schema, locale, issues);
|
|
558
702
|
if (policy.maxSchemaBytes !== void 0) {
|
|
559
703
|
try {
|
|
@@ -1454,6 +1598,18 @@ function resolveLocalizedSchema(schema, targetLocale) {
|
|
|
1454
1598
|
async function populateSchemaTranslations(schema, targetLocales, adapter, options = {}) {
|
|
1455
1599
|
assertValidFormSchema(schema);
|
|
1456
1600
|
const locales = [...new Set(targetLocales.filter((locale) => locale.length > 0 && locale !== schema.defaultLocale))];
|
|
1601
|
+
const allowedLocales = options.policy?.allowedLocales;
|
|
1602
|
+
const collectedLocales = collectSchemaLocales(schema);
|
|
1603
|
+
const disallowedLocale = [...collectedLocales.allUniqueLocales, ...locales].find(
|
|
1604
|
+
(locale) => allowedLocales !== void 0 && !allowedLocales.includes(locale)
|
|
1605
|
+
);
|
|
1606
|
+
if (disallowedLocale !== void 0) {
|
|
1607
|
+
throw new RangeError(`Translation locale ${disallowedLocale} is not allowed by the form policy.`);
|
|
1608
|
+
}
|
|
1609
|
+
const projectedLocales = /* @__PURE__ */ new Set([...collectedLocales.allUniqueLocales, ...locales]);
|
|
1610
|
+
if (options.policy?.maxLocales !== void 0 && projectedLocales.size > options.policy.maxLocales) {
|
|
1611
|
+
throw new RangeError(`At most ${options.policy.maxLocales} locales are allowed by the form policy.`);
|
|
1612
|
+
}
|
|
1457
1613
|
const updatedSlots = [];
|
|
1458
1614
|
const skippedSlots = [];
|
|
1459
1615
|
let result = schema;
|
|
@@ -1511,6 +1667,7 @@ async function resolveFormTranslation(schema, adapter, targetLocale, sourceLocal
|
|
|
1511
1667
|
calculateFieldVisibility,
|
|
1512
1668
|
calculateNumericSummary,
|
|
1513
1669
|
calculatePageVisibility,
|
|
1670
|
+
collectSchemaLocales,
|
|
1514
1671
|
createSubmission,
|
|
1515
1672
|
dispatchWebhook,
|
|
1516
1673
|
escapeCsvCell,
|
package/dist/index.d.cts
CHANGED
|
@@ -4,6 +4,8 @@ interface FormPolicy {
|
|
|
4
4
|
readonly maxFields?: number;
|
|
5
5
|
readonly maxOptionsPerField?: number;
|
|
6
6
|
readonly requiredLocales?: readonly string[];
|
|
7
|
+
readonly allowedLocales?: readonly string[];
|
|
8
|
+
readonly maxLocales?: number;
|
|
7
9
|
readonly maxTextLength?: number;
|
|
8
10
|
readonly maxSchemaBytes?: number;
|
|
9
11
|
}
|
|
@@ -266,6 +268,17 @@ declare function dispatchWebhook<T>(event: FormEvent<T>, config: WebhookConfig,
|
|
|
266
268
|
*/
|
|
267
269
|
declare function transformFieldType(field: FormField, nextType: QuestionType): FormField;
|
|
268
270
|
|
|
271
|
+
interface CollectedLocales {
|
|
272
|
+
readonly defaultLocale?: string;
|
|
273
|
+
readonly supportedLocales: readonly string[];
|
|
274
|
+
readonly translationLocales: ReadonlySet<string>;
|
|
275
|
+
readonly allUniqueLocales: ReadonlySet<string>;
|
|
276
|
+
/** Every translation or translation-metadata locale and the schema paths where it occurs. */
|
|
277
|
+
readonly translationLocalePaths: ReadonlyMap<string, readonly string[]>;
|
|
278
|
+
}
|
|
279
|
+
/** Collects locale registrations and every locale key used by translations or translation metadata. */
|
|
280
|
+
declare function collectSchemaLocales(schema: FormSchema): CollectedLocales;
|
|
281
|
+
|
|
269
282
|
type SchemaStructureIssueType = "dangling_condition_reference" | "duplicate_question_id" | "duplicate_choice_id" | "self_condition_reference" | "cyclic_condition_reference";
|
|
270
283
|
interface SchemaStructureIssue {
|
|
271
284
|
readonly type: SchemaStructureIssueType;
|
|
@@ -305,6 +318,8 @@ interface PopulateTranslationOptions {
|
|
|
305
318
|
readonly overwrite?: "missing-only" | "all";
|
|
306
319
|
readonly shouldOverwrite?: (slot: TranslationSlot) => boolean;
|
|
307
320
|
readonly createMetadata?: (slot: TranslationSlot, translatedText: string) => Readonly<Record<string, JsonValue>>;
|
|
321
|
+
/** Applies locale admission and count limits before the adapter is called. */
|
|
322
|
+
readonly policy?: Pick<FormPolicy, "allowedLocales" | "maxLocales">;
|
|
308
323
|
}
|
|
309
324
|
interface TranslationReport {
|
|
310
325
|
readonly updatedSlots: readonly TranslationSlot[];
|
|
@@ -326,4 +341,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
|
|
|
326
341
|
declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
|
|
327
342
|
declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
|
|
328
343
|
|
|
329
|
-
export { type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvExportOptions, type DisplayCondition, type ExtensibleNode, type FieldOption, type FieldType, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormPolicy, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormValue, type FormValues, type JsonValue, type LocalizedText, type MultiSelectField, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PopulateTranslationOptions, type Question, type QuestionAggregate, type QuestionType, type RatingField, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type SubmissionQueryOptions, type TextField, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, createSubmission, dispatchWebhook, escapeCsvCell, exportResponsesToCsv, isDisplayConditionSatisfied, isQuestionVisible, populateSchemaTranslations, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
|
|
344
|
+
export { type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvExportOptions, type DisplayCondition, type ExtensibleNode, type FieldOption, type FieldType, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormPolicy, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormValue, type FormValues, type JsonValue, type LocalizedText, type MultiSelectField, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PopulateTranslationOptions, type Question, type QuestionAggregate, type QuestionType, type RatingField, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type SubmissionQueryOptions, type TextField, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, collectSchemaLocales, createSubmission, dispatchWebhook, escapeCsvCell, exportResponsesToCsv, isDisplayConditionSatisfied, isQuestionVisible, populateSchemaTranslations, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ interface FormPolicy {
|
|
|
4
4
|
readonly maxFields?: number;
|
|
5
5
|
readonly maxOptionsPerField?: number;
|
|
6
6
|
readonly requiredLocales?: readonly string[];
|
|
7
|
+
readonly allowedLocales?: readonly string[];
|
|
8
|
+
readonly maxLocales?: number;
|
|
7
9
|
readonly maxTextLength?: number;
|
|
8
10
|
readonly maxSchemaBytes?: number;
|
|
9
11
|
}
|
|
@@ -266,6 +268,17 @@ declare function dispatchWebhook<T>(event: FormEvent<T>, config: WebhookConfig,
|
|
|
266
268
|
*/
|
|
267
269
|
declare function transformFieldType(field: FormField, nextType: QuestionType): FormField;
|
|
268
270
|
|
|
271
|
+
interface CollectedLocales {
|
|
272
|
+
readonly defaultLocale?: string;
|
|
273
|
+
readonly supportedLocales: readonly string[];
|
|
274
|
+
readonly translationLocales: ReadonlySet<string>;
|
|
275
|
+
readonly allUniqueLocales: ReadonlySet<string>;
|
|
276
|
+
/** Every translation or translation-metadata locale and the schema paths where it occurs. */
|
|
277
|
+
readonly translationLocalePaths: ReadonlyMap<string, readonly string[]>;
|
|
278
|
+
}
|
|
279
|
+
/** Collects locale registrations and every locale key used by translations or translation metadata. */
|
|
280
|
+
declare function collectSchemaLocales(schema: FormSchema): CollectedLocales;
|
|
281
|
+
|
|
269
282
|
type SchemaStructureIssueType = "dangling_condition_reference" | "duplicate_question_id" | "duplicate_choice_id" | "self_condition_reference" | "cyclic_condition_reference";
|
|
270
283
|
interface SchemaStructureIssue {
|
|
271
284
|
readonly type: SchemaStructureIssueType;
|
|
@@ -305,6 +318,8 @@ interface PopulateTranslationOptions {
|
|
|
305
318
|
readonly overwrite?: "missing-only" | "all";
|
|
306
319
|
readonly shouldOverwrite?: (slot: TranslationSlot) => boolean;
|
|
307
320
|
readonly createMetadata?: (slot: TranslationSlot, translatedText: string) => Readonly<Record<string, JsonValue>>;
|
|
321
|
+
/** Applies locale admission and count limits before the adapter is called. */
|
|
322
|
+
readonly policy?: Pick<FormPolicy, "allowedLocales" | "maxLocales">;
|
|
308
323
|
}
|
|
309
324
|
interface TranslationReport {
|
|
310
325
|
readonly updatedSlots: readonly TranslationSlot[];
|
|
@@ -326,4 +341,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
|
|
|
326
341
|
declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
|
|
327
342
|
declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
|
|
328
343
|
|
|
329
|
-
export { type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvExportOptions, type DisplayCondition, type ExtensibleNode, type FieldOption, type FieldType, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormPolicy, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormValue, type FormValues, type JsonValue, type LocalizedText, type MultiSelectField, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PopulateTranslationOptions, type Question, type QuestionAggregate, type QuestionType, type RatingField, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type SubmissionQueryOptions, type TextField, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, createSubmission, dispatchWebhook, escapeCsvCell, exportResponsesToCsv, isDisplayConditionSatisfied, isQuestionVisible, populateSchemaTranslations, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
|
|
344
|
+
export { type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvExportOptions, type DisplayCondition, type ExtensibleNode, type FieldOption, type FieldType, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormPolicy, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormValue, type FormValues, type JsonValue, type LocalizedText, type MultiSelectField, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PopulateTranslationOptions, type Question, type QuestionAggregate, type QuestionType, type RatingField, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type SubmissionQueryOptions, type TextField, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, collectSchemaLocales, createSubmission, dispatchWebhook, escapeCsvCell, exportResponsesToCsv, isDisplayConditionSatisfied, isQuestionVisible, populateSchemaTranslations, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,92 @@
|
|
|
1
|
+
// src/policy.ts
|
|
2
|
+
function collectRecordKeys(value, path, pathsByLocale) {
|
|
3
|
+
for (const locale of Object.keys(value ?? {})) {
|
|
4
|
+
const paths = pathsByLocale.get(locale) ?? [];
|
|
5
|
+
paths.push(`${path}.${locale}`);
|
|
6
|
+
pathsByLocale.set(locale, paths);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function collectSchemaLocales(schema) {
|
|
10
|
+
const pathsByLocale = /* @__PURE__ */ new Map();
|
|
11
|
+
collectRecordKeys(schema.translations, "translations", pathsByLocale);
|
|
12
|
+
collectRecordKeys(schema.translationMetadata, "translationMetadata", pathsByLocale);
|
|
13
|
+
schema.fields.forEach((field, fieldIndex) => {
|
|
14
|
+
collectRecordKeys(field.translations, `fields[${fieldIndex}].translations`, pathsByLocale);
|
|
15
|
+
collectRecordKeys(field.translationMetadata, `fields[${fieldIndex}].translationMetadata`, pathsByLocale);
|
|
16
|
+
if (!("options" in field)) return;
|
|
17
|
+
field.options.forEach((option, optionIndex) => {
|
|
18
|
+
collectRecordKeys(
|
|
19
|
+
option.translations,
|
|
20
|
+
`fields[${fieldIndex}].options[${optionIndex}].translations`,
|
|
21
|
+
pathsByLocale
|
|
22
|
+
);
|
|
23
|
+
collectRecordKeys(
|
|
24
|
+
option.translationMetadata,
|
|
25
|
+
`fields[${fieldIndex}].options[${optionIndex}].translationMetadata`,
|
|
26
|
+
pathsByLocale
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
schema.pages?.forEach((page, pageIndex) => {
|
|
31
|
+
collectRecordKeys(page.translations, `pages[${pageIndex}].translations`, pathsByLocale);
|
|
32
|
+
collectRecordKeys(page.translationMetadata, `pages[${pageIndex}].translationMetadata`, pathsByLocale);
|
|
33
|
+
});
|
|
34
|
+
const translationLocales = new Set(pathsByLocale.keys());
|
|
35
|
+
const allUniqueLocales = /* @__PURE__ */ new Set([
|
|
36
|
+
...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
|
|
37
|
+
...schema.supportedLocales ?? [],
|
|
38
|
+
...translationLocales
|
|
39
|
+
]);
|
|
40
|
+
return {
|
|
41
|
+
...schema.defaultLocale === void 0 ? {} : { defaultLocale: schema.defaultLocale },
|
|
42
|
+
supportedLocales: schema.supportedLocales ?? [],
|
|
43
|
+
translationLocales,
|
|
44
|
+
allUniqueLocales,
|
|
45
|
+
translationLocalePaths: pathsByLocale
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
1
49
|
// src/sanitization.ts
|
|
50
|
+
function registeredEntries(value, registeredLocales) {
|
|
51
|
+
if (value === void 0) return void 0;
|
|
52
|
+
const entries = Object.entries(value).filter(([locale]) => registeredLocales.has(locale));
|
|
53
|
+
return entries.length === 0 ? void 0 : Object.fromEntries(entries);
|
|
54
|
+
}
|
|
55
|
+
function sanitizeNodeLocales(node, registeredLocales) {
|
|
56
|
+
const { translationMetadata: _translationMetadata, ...base } = node;
|
|
57
|
+
const translationMetadata = registeredEntries(node.translationMetadata, registeredLocales);
|
|
58
|
+
return {
|
|
59
|
+
...base,
|
|
60
|
+
...translationMetadata === void 0 ? {} : { translationMetadata }
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function sanitizeOptionLocales(option, registeredLocales) {
|
|
64
|
+
const { translations: _translations, ...base } = sanitizeNodeLocales(option, registeredLocales);
|
|
65
|
+
const translations = registeredEntries(option.translations, registeredLocales);
|
|
66
|
+
return { ...base, ...translations === void 0 ? {} : { translations } };
|
|
67
|
+
}
|
|
68
|
+
function sanitizeFieldLocales(field, registeredLocales) {
|
|
69
|
+
const localizedNode = sanitizeNodeLocales(field, registeredLocales);
|
|
70
|
+
const { translations: _translations, ...base } = localizedNode;
|
|
71
|
+
const translations = registeredEntries(field.translations, registeredLocales);
|
|
72
|
+
const localized = {
|
|
73
|
+
...base,
|
|
74
|
+
...translations === void 0 ? {} : { translations }
|
|
75
|
+
};
|
|
76
|
+
if (!("options" in localizedNode)) return localized;
|
|
77
|
+
const { translations: _choiceTranslations, ...choiceBase } = localizedNode;
|
|
78
|
+
return {
|
|
79
|
+
...choiceBase,
|
|
80
|
+
...translations === void 0 ? {} : { translations },
|
|
81
|
+
options: localizedNode.options.map((option) => sanitizeOptionLocales(option, registeredLocales))
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function sanitizePageLocales(page, registeredLocales) {
|
|
85
|
+
const localizedNode = sanitizeNodeLocales(page, registeredLocales);
|
|
86
|
+
const { translations: _translations, ...base } = localizedNode;
|
|
87
|
+
const translations = registeredEntries(page.translations, registeredLocales);
|
|
88
|
+
return { ...base, ...translations === void 0 ? {} : { translations } };
|
|
89
|
+
}
|
|
2
90
|
function cyclicQuestionIds(fields) {
|
|
3
91
|
const firstById = /* @__PURE__ */ new Map();
|
|
4
92
|
for (const field of fields) {
|
|
@@ -85,8 +173,13 @@ function validateSchemaStructure(schema) {
|
|
|
85
173
|
}
|
|
86
174
|
function sanitizeSchema(schema) {
|
|
87
175
|
const existingQuestionIds = new Set(schema.fields.map((field) => field.id));
|
|
176
|
+
const registeredLocales = /* @__PURE__ */ new Set([
|
|
177
|
+
...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
|
|
178
|
+
...schema.supportedLocales ?? []
|
|
179
|
+
]);
|
|
88
180
|
const cyclic = cyclicQuestionIds(schema.fields);
|
|
89
|
-
const sanitizedFields = schema.fields.map((
|
|
181
|
+
const sanitizedFields = schema.fields.map((sourceField) => {
|
|
182
|
+
const field = sanitizeFieldLocales(sourceField, registeredLocales);
|
|
90
183
|
const sourceId = field.displayCondition?.questionId;
|
|
91
184
|
if (sourceId === void 0 || existingQuestionIds.has(sourceId) && sourceId !== field.id && !cyclic.has(field.id)) {
|
|
92
185
|
return field;
|
|
@@ -94,15 +187,19 @@ function sanitizeSchema(schema) {
|
|
|
94
187
|
const { displayCondition: _displayCondition, ...sanitized } = field;
|
|
95
188
|
return sanitized;
|
|
96
189
|
});
|
|
190
|
+
const localizedSchema = sanitizeNodeLocales(schema, registeredLocales);
|
|
191
|
+
const { translations: _translations, ...schemaWithoutLocaleContent } = localizedSchema;
|
|
192
|
+
const translations = registeredEntries(schema.translations, registeredLocales);
|
|
97
193
|
const base = {
|
|
98
|
-
...
|
|
194
|
+
...schemaWithoutLocaleContent,
|
|
195
|
+
...translations === void 0 ? {} : { translations },
|
|
99
196
|
fields: sanitizedFields
|
|
100
197
|
};
|
|
101
198
|
if (schema.pages === void 0) return base;
|
|
102
199
|
const assigned = /* @__PURE__ */ new Set();
|
|
103
|
-
const pages = schema.pages.map((
|
|
104
|
-
...
|
|
105
|
-
questionIds:
|
|
200
|
+
const pages = schema.pages.map((sourcePage) => ({
|
|
201
|
+
...sanitizePageLocales(sourcePage, registeredLocales),
|
|
202
|
+
questionIds: sourcePage.questionIds.filter((id) => {
|
|
106
203
|
if (!existingQuestionIds.has(id) || assigned.has(id)) return false;
|
|
107
204
|
assigned.add(id);
|
|
108
205
|
return true;
|
|
@@ -506,6 +603,52 @@ function validatePolicy(schema, policy, issues) {
|
|
|
506
603
|
}
|
|
507
604
|
}
|
|
508
605
|
}
|
|
606
|
+
const collectedLocales = collectSchemaLocales(schema);
|
|
607
|
+
const registeredLocales = /* @__PURE__ */ new Set([
|
|
608
|
+
...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
|
|
609
|
+
...schema.supportedLocales ?? []
|
|
610
|
+
]);
|
|
611
|
+
for (const locale of collectedLocales.translationLocales) {
|
|
612
|
+
if (registeredLocales.has(locale)) continue;
|
|
613
|
+
for (const path of collectedLocales.translationLocalePaths.get(locale) ?? []) {
|
|
614
|
+
issue(
|
|
615
|
+
issues,
|
|
616
|
+
path,
|
|
617
|
+
"unregistered_translation_locale",
|
|
618
|
+
`Translation locale ${locale} is not registered by defaultLocale or supportedLocales.`
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (policy.allowedLocales !== void 0) {
|
|
623
|
+
const pathsByLocale = /* @__PURE__ */ new Map();
|
|
624
|
+
if (schema.defaultLocale !== void 0) pathsByLocale.set(schema.defaultLocale, ["defaultLocale"]);
|
|
625
|
+
schema.supportedLocales?.forEach((locale, index) => {
|
|
626
|
+
pathsByLocale.set(locale, [...pathsByLocale.get(locale) ?? [], `supportedLocales[${index}]`]);
|
|
627
|
+
});
|
|
628
|
+
for (const [locale, paths] of collectedLocales.translationLocalePaths) {
|
|
629
|
+
pathsByLocale.set(locale, [...pathsByLocale.get(locale) ?? [], ...paths]);
|
|
630
|
+
}
|
|
631
|
+
for (const [locale, paths] of pathsByLocale) {
|
|
632
|
+
if (!policy.allowedLocales.includes(locale)) {
|
|
633
|
+
for (const path of paths) {
|
|
634
|
+
issue(issues, path, "disallowed_locale", `Locale ${locale} is not allowed by the form policy.`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
for (const locale of policy.requiredLocales ?? []) {
|
|
639
|
+
if (!policy.allowedLocales.includes(locale)) {
|
|
640
|
+
issue(
|
|
641
|
+
issues,
|
|
642
|
+
"policy.requiredLocales",
|
|
643
|
+
"required_locale_not_allowed",
|
|
644
|
+
`Required locale ${locale} is not included in allowedLocales.`
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (policy.maxLocales !== void 0 && collectedLocales.allUniqueLocales.size > policy.maxLocales) {
|
|
650
|
+
issue(issues, "supportedLocales", "max_locales_exceeded", `At most ${policy.maxLocales} locales are allowed.`);
|
|
651
|
+
}
|
|
509
652
|
for (const locale of policy.requiredLocales ?? []) addRequiredTranslationIssues(schema, locale, issues);
|
|
510
653
|
if (policy.maxSchemaBytes !== void 0) {
|
|
511
654
|
try {
|
|
@@ -1406,6 +1549,18 @@ function resolveLocalizedSchema(schema, targetLocale) {
|
|
|
1406
1549
|
async function populateSchemaTranslations(schema, targetLocales, adapter, options = {}) {
|
|
1407
1550
|
assertValidFormSchema(schema);
|
|
1408
1551
|
const locales = [...new Set(targetLocales.filter((locale) => locale.length > 0 && locale !== schema.defaultLocale))];
|
|
1552
|
+
const allowedLocales = options.policy?.allowedLocales;
|
|
1553
|
+
const collectedLocales = collectSchemaLocales(schema);
|
|
1554
|
+
const disallowedLocale = [...collectedLocales.allUniqueLocales, ...locales].find(
|
|
1555
|
+
(locale) => allowedLocales !== void 0 && !allowedLocales.includes(locale)
|
|
1556
|
+
);
|
|
1557
|
+
if (disallowedLocale !== void 0) {
|
|
1558
|
+
throw new RangeError(`Translation locale ${disallowedLocale} is not allowed by the form policy.`);
|
|
1559
|
+
}
|
|
1560
|
+
const projectedLocales = /* @__PURE__ */ new Set([...collectedLocales.allUniqueLocales, ...locales]);
|
|
1561
|
+
if (options.policy?.maxLocales !== void 0 && projectedLocales.size > options.policy.maxLocales) {
|
|
1562
|
+
throw new RangeError(`At most ${options.policy.maxLocales} locales are allowed by the form policy.`);
|
|
1563
|
+
}
|
|
1409
1564
|
const updatedSlots = [];
|
|
1410
1565
|
const skippedSlots = [];
|
|
1411
1566
|
let result = schema;
|
|
@@ -1462,6 +1617,7 @@ export {
|
|
|
1462
1617
|
calculateFieldVisibility,
|
|
1463
1618
|
calculateNumericSummary,
|
|
1464
1619
|
calculatePageVisibility,
|
|
1620
|
+
collectSchemaLocales,
|
|
1465
1621
|
createSubmission,
|
|
1466
1622
|
dispatchWebhook,
|
|
1467
1623
|
escapeCsvCell,
|