@form-engine-ts/core 2.2.0 → 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 +6 -0
- package/dist/index.cjs +143 -25
- package/dist/index.d.cts +14 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +142 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,6 +44,12 @@ option, text, serialized-byte, allowed-type, and locale constraints. `allowedLoc
|
|
|
44
44
|
locales, `maxLocales` limits their unique total, and contradictory required/allowed locale policies are reported.
|
|
45
45
|
Required locales cover every source text that exists on the form, its fields, options, and pages.
|
|
46
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.
|
|
52
|
+
|
|
47
53
|
Translation callbacks receive `nodeMetadata` and `existingTranslationMetadata` separately. The deprecated `metadata`
|
|
48
54
|
slot property remains an alias for `nodeMetadata` during migration.
|
|
49
55
|
|
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,31 +652,38 @@ function validatePolicy(schema, policy, issues) {
|
|
|
554
652
|
}
|
|
555
653
|
}
|
|
556
654
|
}
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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) ?? []) {
|
|
565
663
|
issue(
|
|
566
664
|
issues,
|
|
567
|
-
|
|
568
|
-
"
|
|
569
|
-
`
|
|
665
|
+
path,
|
|
666
|
+
"unregistered_translation_locale",
|
|
667
|
+
`Translation locale ${locale} is not registered by defaultLocale or supportedLocales.`
|
|
570
668
|
);
|
|
571
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"]);
|
|
572
674
|
schema.supportedLocales?.forEach((locale, index) => {
|
|
573
|
-
|
|
574
|
-
issue(
|
|
575
|
-
issues,
|
|
576
|
-
`supportedLocales[${index}]`,
|
|
577
|
-
"disallowed_locale",
|
|
578
|
-
`Locale ${locale} is not allowed by the form policy.`
|
|
579
|
-
);
|
|
580
|
-
}
|
|
675
|
+
pathsByLocale.set(locale, [...pathsByLocale.get(locale) ?? [], `supportedLocales[${index}]`]);
|
|
581
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
|
+
}
|
|
582
687
|
for (const locale of policy.requiredLocales ?? []) {
|
|
583
688
|
if (!policy.allowedLocales.includes(locale)) {
|
|
584
689
|
issue(
|
|
@@ -590,7 +695,7 @@ function validatePolicy(schema, policy, issues) {
|
|
|
590
695
|
}
|
|
591
696
|
}
|
|
592
697
|
}
|
|
593
|
-
if (policy.maxLocales !== void 0 &&
|
|
698
|
+
if (policy.maxLocales !== void 0 && collectedLocales.allUniqueLocales.size > policy.maxLocales) {
|
|
594
699
|
issue(issues, "supportedLocales", "max_locales_exceeded", `At most ${policy.maxLocales} locales are allowed.`);
|
|
595
700
|
}
|
|
596
701
|
for (const locale of policy.requiredLocales ?? []) addRequiredTranslationIssues(schema, locale, issues);
|
|
@@ -1493,6 +1598,18 @@ function resolveLocalizedSchema(schema, targetLocale) {
|
|
|
1493
1598
|
async function populateSchemaTranslations(schema, targetLocales, adapter, options = {}) {
|
|
1494
1599
|
assertValidFormSchema(schema);
|
|
1495
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
|
+
}
|
|
1496
1613
|
const updatedSlots = [];
|
|
1497
1614
|
const skippedSlots = [];
|
|
1498
1615
|
let result = schema;
|
|
@@ -1550,6 +1667,7 @@ async function resolveFormTranslation(schema, adapter, targetLocale, sourceLocal
|
|
|
1550
1667
|
calculateFieldVisibility,
|
|
1551
1668
|
calculateNumericSummary,
|
|
1552
1669
|
calculatePageVisibility,
|
|
1670
|
+
collectSchemaLocales,
|
|
1553
1671
|
createSubmission,
|
|
1554
1672
|
dispatchWebhook,
|
|
1555
1673
|
escapeCsvCell,
|
package/dist/index.d.cts
CHANGED
|
@@ -268,6 +268,17 @@ declare function dispatchWebhook<T>(event: FormEvent<T>, config: WebhookConfig,
|
|
|
268
268
|
*/
|
|
269
269
|
declare function transformFieldType(field: FormField, nextType: QuestionType): FormField;
|
|
270
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
|
+
|
|
271
282
|
type SchemaStructureIssueType = "dangling_condition_reference" | "duplicate_question_id" | "duplicate_choice_id" | "self_condition_reference" | "cyclic_condition_reference";
|
|
272
283
|
interface SchemaStructureIssue {
|
|
273
284
|
readonly type: SchemaStructureIssueType;
|
|
@@ -307,6 +318,8 @@ interface PopulateTranslationOptions {
|
|
|
307
318
|
readonly overwrite?: "missing-only" | "all";
|
|
308
319
|
readonly shouldOverwrite?: (slot: TranslationSlot) => boolean;
|
|
309
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">;
|
|
310
323
|
}
|
|
311
324
|
interface TranslationReport {
|
|
312
325
|
readonly updatedSlots: readonly TranslationSlot[];
|
|
@@ -328,4 +341,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
|
|
|
328
341
|
declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
|
|
329
342
|
declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
|
|
330
343
|
|
|
331
|
-
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
|
@@ -268,6 +268,17 @@ declare function dispatchWebhook<T>(event: FormEvent<T>, config: WebhookConfig,
|
|
|
268
268
|
*/
|
|
269
269
|
declare function transformFieldType(field: FormField, nextType: QuestionType): FormField;
|
|
270
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
|
+
|
|
271
282
|
type SchemaStructureIssueType = "dangling_condition_reference" | "duplicate_question_id" | "duplicate_choice_id" | "self_condition_reference" | "cyclic_condition_reference";
|
|
272
283
|
interface SchemaStructureIssue {
|
|
273
284
|
readonly type: SchemaStructureIssueType;
|
|
@@ -307,6 +318,8 @@ interface PopulateTranslationOptions {
|
|
|
307
318
|
readonly overwrite?: "missing-only" | "all";
|
|
308
319
|
readonly shouldOverwrite?: (slot: TranslationSlot) => boolean;
|
|
309
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">;
|
|
310
323
|
}
|
|
311
324
|
interface TranslationReport {
|
|
312
325
|
readonly updatedSlots: readonly TranslationSlot[];
|
|
@@ -328,4 +341,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
|
|
|
328
341
|
declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
|
|
329
342
|
declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
|
|
330
343
|
|
|
331
|
-
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,31 +603,38 @@ function validatePolicy(schema, policy, issues) {
|
|
|
506
603
|
}
|
|
507
604
|
}
|
|
508
605
|
}
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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) ?? []) {
|
|
517
614
|
issue(
|
|
518
615
|
issues,
|
|
519
|
-
|
|
520
|
-
"
|
|
521
|
-
`
|
|
616
|
+
path,
|
|
617
|
+
"unregistered_translation_locale",
|
|
618
|
+
`Translation locale ${locale} is not registered by defaultLocale or supportedLocales.`
|
|
522
619
|
);
|
|
523
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"]);
|
|
524
625
|
schema.supportedLocales?.forEach((locale, index) => {
|
|
525
|
-
|
|
526
|
-
issue(
|
|
527
|
-
issues,
|
|
528
|
-
`supportedLocales[${index}]`,
|
|
529
|
-
"disallowed_locale",
|
|
530
|
-
`Locale ${locale} is not allowed by the form policy.`
|
|
531
|
-
);
|
|
532
|
-
}
|
|
626
|
+
pathsByLocale.set(locale, [...pathsByLocale.get(locale) ?? [], `supportedLocales[${index}]`]);
|
|
533
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
|
+
}
|
|
534
638
|
for (const locale of policy.requiredLocales ?? []) {
|
|
535
639
|
if (!policy.allowedLocales.includes(locale)) {
|
|
536
640
|
issue(
|
|
@@ -542,7 +646,7 @@ function validatePolicy(schema, policy, issues) {
|
|
|
542
646
|
}
|
|
543
647
|
}
|
|
544
648
|
}
|
|
545
|
-
if (policy.maxLocales !== void 0 &&
|
|
649
|
+
if (policy.maxLocales !== void 0 && collectedLocales.allUniqueLocales.size > policy.maxLocales) {
|
|
546
650
|
issue(issues, "supportedLocales", "max_locales_exceeded", `At most ${policy.maxLocales} locales are allowed.`);
|
|
547
651
|
}
|
|
548
652
|
for (const locale of policy.requiredLocales ?? []) addRequiredTranslationIssues(schema, locale, issues);
|
|
@@ -1445,6 +1549,18 @@ function resolveLocalizedSchema(schema, targetLocale) {
|
|
|
1445
1549
|
async function populateSchemaTranslations(schema, targetLocales, adapter, options = {}) {
|
|
1446
1550
|
assertValidFormSchema(schema);
|
|
1447
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
|
+
}
|
|
1448
1564
|
const updatedSlots = [];
|
|
1449
1565
|
const skippedSlots = [];
|
|
1450
1566
|
let result = schema;
|
|
@@ -1501,6 +1617,7 @@ export {
|
|
|
1501
1617
|
calculateFieldVisibility,
|
|
1502
1618
|
calculateNumericSummary,
|
|
1503
1619
|
calculatePageVisibility,
|
|
1620
|
+
collectSchemaLocales,
|
|
1504
1621
|
createSubmission,
|
|
1505
1622
|
dispatchWebhook,
|
|
1506
1623
|
escapeCsvCell,
|