@form-engine-ts/core 4.0.0 → 4.2.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 +4 -2
- package/dist/index.cjs +121 -2
- package/dist/index.d.cts +35 -3
- package/dist/index.d.ts +35 -3
- package/dist/index.js +121 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,13 +41,15 @@ round-trips. `completionMessage` is localized with the rest of the form text.
|
|
|
41
41
|
|
|
42
42
|
`transformFieldType` changes a question's type without discarding source text, translations, conditions, or extension
|
|
43
43
|
metadata. `validateFormSchema(schema, { policy })` applies the framework-independent `FormPolicy`, including field,
|
|
44
|
-
option, text, serialized-byte, allowed-type, and
|
|
44
|
+
option, text, serialized-byte, allowed-type, locale, and per-field `fieldConstraints` rules. Rating bounds can be fixed
|
|
45
|
+
or range-limited, text lengths can have a policy maximum, and required state can be fixed. `allowedLocales` constrains the default and supported
|
|
45
46
|
locales, `maxLocales` limits their unique total, and contradictory required/allowed locale policies are reported.
|
|
46
47
|
Required locales cover every source text that exists on the form, its fields, options, and pages.
|
|
47
48
|
|
|
48
49
|
`collectSchemaLocales(schema)` scans registrations plus every form/page/field/option `translations` and
|
|
49
50
|
`translationMetadata` key. Validation reports unregistered translation locales and applies `allowedLocales` and
|
|
50
|
-
`maxLocales` to the complete collected set. `sanitizeSchema`
|
|
51
|
+
`maxLocales` to the complete collected set. `sanitizeSchema(schema, { policy })` also applies fixed field values and
|
|
52
|
+
safe maximum-length corrections while purging unregistered locale content. Pass
|
|
51
53
|
`{ policy: { allowedLocales, maxLocales } }` to `populateSchemaTranslations` to reject inadmissible targets before the
|
|
52
54
|
translation adapter runs.
|
|
53
55
|
|
package/dist/index.cjs
CHANGED
|
@@ -156,6 +156,27 @@ function sanitizePageLocales(page, registeredLocales) {
|
|
|
156
156
|
const translations = registeredEntries(page.translations, registeredLocales);
|
|
157
157
|
return { ...base, ...translations === void 0 ? {} : { translations } };
|
|
158
158
|
}
|
|
159
|
+
function sanitizeFieldConstraints(field, policy) {
|
|
160
|
+
const constraint = policy?.fieldConstraints?.[field.type];
|
|
161
|
+
if (constraint === void 0) return field;
|
|
162
|
+
const required = constraint.fixedRequired ?? field.required;
|
|
163
|
+
if (field.type === "rating") {
|
|
164
|
+
const ratingConstraint = "fixedMin" in constraint || "fixedMax" in constraint ? constraint : void 0;
|
|
165
|
+
const min = ratingConstraint !== void 0 && "fixedMin" in ratingConstraint ? ratingConstraint.fixedMin : field.min;
|
|
166
|
+
const max = ratingConstraint !== void 0 && "fixedMax" in ratingConstraint ? ratingConstraint.fixedMax : field.max;
|
|
167
|
+
return {
|
|
168
|
+
...field,
|
|
169
|
+
required,
|
|
170
|
+
...min === void 0 ? {} : { min },
|
|
171
|
+
...max === void 0 ? {} : { max }
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if ((field.type === "text" || field.type === "textarea") && "maxMaxLength" in constraint) {
|
|
175
|
+
const maxLength = field.maxLength === void 0 || constraint.maxMaxLength === void 0 ? field.maxLength : Math.min(field.maxLength, constraint.maxMaxLength);
|
|
176
|
+
return { ...field, required, ...maxLength === void 0 ? {} : { maxLength } };
|
|
177
|
+
}
|
|
178
|
+
return { ...field, required };
|
|
179
|
+
}
|
|
159
180
|
function cyclicQuestionIds(fields) {
|
|
160
181
|
const firstById = /* @__PURE__ */ new Map();
|
|
161
182
|
for (const field of fields) {
|
|
@@ -240,7 +261,7 @@ function validateSchemaStructure(schema) {
|
|
|
240
261
|
}
|
|
241
262
|
return issues;
|
|
242
263
|
}
|
|
243
|
-
function sanitizeSchema(schema) {
|
|
264
|
+
function sanitizeSchema(schema, options = {}) {
|
|
244
265
|
const existingQuestionIds = new Set(schema.fields.map((field) => field.id));
|
|
245
266
|
const registeredLocales = /* @__PURE__ */ new Set([
|
|
246
267
|
...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
|
|
@@ -248,7 +269,7 @@ function sanitizeSchema(schema) {
|
|
|
248
269
|
]);
|
|
249
270
|
const cyclic = cyclicQuestionIds(schema.fields);
|
|
250
271
|
const sanitizedFields = schema.fields.map((sourceField) => {
|
|
251
|
-
const field = sanitizeFieldLocales(sourceField, registeredLocales);
|
|
272
|
+
const field = sanitizeFieldConstraints(sanitizeFieldLocales(sourceField, registeredLocales), options.policy);
|
|
252
273
|
const sourceId = field.displayCondition?.questionId;
|
|
253
274
|
if (sourceId === void 0 || existingQuestionIds.has(sourceId) && sourceId !== field.id && !cyclic.has(field.id)) {
|
|
254
275
|
return field;
|
|
@@ -306,6 +327,17 @@ function isNonEmptyString(value) {
|
|
|
306
327
|
function issue(issues, path, code, message) {
|
|
307
328
|
issues.push({ path, code, message });
|
|
308
329
|
}
|
|
330
|
+
function fieldConstraintIssue(issues, field, fieldIndex, property, expected, message) {
|
|
331
|
+
issues.push({
|
|
332
|
+
path: `fields[${fieldIndex}].${property}`,
|
|
333
|
+
code: "field_constraint_violation",
|
|
334
|
+
type: "field_constraint_violation",
|
|
335
|
+
message,
|
|
336
|
+
fieldId: field.id,
|
|
337
|
+
property,
|
|
338
|
+
expected
|
|
339
|
+
});
|
|
340
|
+
}
|
|
309
341
|
function validateJsonValue(value, path, issues, ancestors = /* @__PURE__ */ new Set()) {
|
|
310
342
|
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
311
343
|
if (typeof value === "number") {
|
|
@@ -659,6 +691,93 @@ function validatePolicy(schema, policy, issues) {
|
|
|
659
691
|
`At most ${policy.maxOptionsPerField} options are allowed.`
|
|
660
692
|
);
|
|
661
693
|
}
|
|
694
|
+
const constraint = policy.fieldConstraints?.[field.type];
|
|
695
|
+
if (constraint === void 0) return;
|
|
696
|
+
if (constraint.fixedRequired !== void 0 && field.required !== constraint.fixedRequired) {
|
|
697
|
+
fieldConstraintIssue(
|
|
698
|
+
issues,
|
|
699
|
+
field,
|
|
700
|
+
fieldIndex,
|
|
701
|
+
"required",
|
|
702
|
+
constraint.fixedRequired,
|
|
703
|
+
`Field required must be ${String(constraint.fixedRequired)} for field type ${field.type}.`
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
if (field.type === "rating") {
|
|
707
|
+
const ratingConstraint = "fixedMin" in constraint || "fixedMax" in constraint || "allowedMinRange" in constraint || "allowedMaxRange" in constraint ? constraint : void 0;
|
|
708
|
+
if (ratingConstraint?.fixedMin !== void 0 && field.min !== ratingConstraint.fixedMin) {
|
|
709
|
+
fieldConstraintIssue(
|
|
710
|
+
issues,
|
|
711
|
+
field,
|
|
712
|
+
fieldIndex,
|
|
713
|
+
"min",
|
|
714
|
+
ratingConstraint.fixedMin,
|
|
715
|
+
`Rating minimum must be ${ratingConstraint.fixedMin}.`
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
if (ratingConstraint !== void 0 && "fixedMax" in ratingConstraint && ratingConstraint.fixedMax !== void 0 && field.max !== ratingConstraint.fixedMax) {
|
|
719
|
+
fieldConstraintIssue(
|
|
720
|
+
issues,
|
|
721
|
+
field,
|
|
722
|
+
fieldIndex,
|
|
723
|
+
"max",
|
|
724
|
+
ratingConstraint.fixedMax,
|
|
725
|
+
`Rating maximum must be ${ratingConstraint.fixedMax}.`
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
if (ratingConstraint !== void 0 && "allowedMinRange" in ratingConstraint && ratingConstraint.allowedMinRange !== void 0 && typeof field.min === "number" && (field.min < ratingConstraint.allowedMinRange[0] || field.min > ratingConstraint.allowedMinRange[1])) {
|
|
729
|
+
fieldConstraintIssue(
|
|
730
|
+
issues,
|
|
731
|
+
field,
|
|
732
|
+
fieldIndex,
|
|
733
|
+
"min",
|
|
734
|
+
ratingConstraint.allowedMinRange,
|
|
735
|
+
`Rating minimum must be between ${ratingConstraint.allowedMinRange[0]} and ${ratingConstraint.allowedMinRange[1]}.`
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
if (ratingConstraint !== void 0 && "allowedMaxRange" in ratingConstraint && ratingConstraint.allowedMaxRange !== void 0 && typeof field.max === "number" && (field.max < ratingConstraint.allowedMaxRange[0] || field.max > ratingConstraint.allowedMaxRange[1])) {
|
|
739
|
+
fieldConstraintIssue(
|
|
740
|
+
issues,
|
|
741
|
+
field,
|
|
742
|
+
fieldIndex,
|
|
743
|
+
"max",
|
|
744
|
+
ratingConstraint.allowedMaxRange,
|
|
745
|
+
`Rating maximum must be between ${ratingConstraint.allowedMaxRange[0]} and ${ratingConstraint.allowedMaxRange[1]}.`
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if ((field.type === "text" || field.type === "textarea") && "maxMaxLength" in constraint && constraint.maxMaxLength !== void 0) {
|
|
750
|
+
if (field.maxLength !== void 0 && field.maxLength > constraint.maxMaxLength) {
|
|
751
|
+
fieldConstraintIssue(
|
|
752
|
+
issues,
|
|
753
|
+
field,
|
|
754
|
+
fieldIndex,
|
|
755
|
+
"maxLength",
|
|
756
|
+
constraint.maxMaxLength,
|
|
757
|
+
`Maximum text length must be at most ${constraint.maxMaxLength}.`
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if ((field.type === "select" || field.type === "radio" || field.type === "multi-select") && "minOptions" in constraint && constraint.minOptions !== void 0 && field.options.length < constraint.minOptions) {
|
|
762
|
+
fieldConstraintIssue(
|
|
763
|
+
issues,
|
|
764
|
+
field,
|
|
765
|
+
fieldIndex,
|
|
766
|
+
"options",
|
|
767
|
+
constraint.minOptions,
|
|
768
|
+
`At least ${constraint.minOptions} options are required.`
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
if ((field.type === "select" || field.type === "radio" || field.type === "multi-select") && "maxOptions" in constraint && constraint.maxOptions !== void 0 && field.options.length > constraint.maxOptions) {
|
|
772
|
+
fieldConstraintIssue(
|
|
773
|
+
issues,
|
|
774
|
+
field,
|
|
775
|
+
fieldIndex,
|
|
776
|
+
"options",
|
|
777
|
+
constraint.maxOptions,
|
|
778
|
+
`At most ${constraint.maxOptions} options are allowed.`
|
|
779
|
+
);
|
|
780
|
+
}
|
|
662
781
|
});
|
|
663
782
|
if (policy.maxTextLength !== void 0) {
|
|
664
783
|
for (const entry of collectSchemaText(schema)) {
|
package/dist/index.d.cts
CHANGED
|
@@ -114,6 +114,28 @@ declare function createDeleteDraftTransitionPlan(state: FormVersionState, draftR
|
|
|
114
114
|
declare function assertVersionMutable(status: FormVersionStatus): void;
|
|
115
115
|
|
|
116
116
|
type FieldType = "text" | "textarea" | "number" | "rating" | "select" | "multi-select" | "checkbox" | "radio";
|
|
117
|
+
type QuestionType = FieldType;
|
|
118
|
+
interface BaseFieldConstraintRule {
|
|
119
|
+
readonly defaultRequired?: boolean;
|
|
120
|
+
readonly fixedRequired?: boolean;
|
|
121
|
+
}
|
|
122
|
+
interface RatingFieldConstraintRule extends BaseFieldConstraintRule {
|
|
123
|
+
readonly defaultMin?: number;
|
|
124
|
+
readonly defaultMax?: number;
|
|
125
|
+
readonly fixedMin?: number;
|
|
126
|
+
readonly fixedMax?: number;
|
|
127
|
+
readonly allowedMinRange?: readonly [number, number];
|
|
128
|
+
readonly allowedMaxRange?: readonly [number, number];
|
|
129
|
+
}
|
|
130
|
+
interface TextFieldConstraintRule extends BaseFieldConstraintRule {
|
|
131
|
+
readonly defaultMaxLength?: number;
|
|
132
|
+
readonly maxMaxLength?: number;
|
|
133
|
+
}
|
|
134
|
+
interface ChoiceFieldConstraintRule extends BaseFieldConstraintRule {
|
|
135
|
+
readonly minOptions?: number;
|
|
136
|
+
readonly maxOptions?: number;
|
|
137
|
+
}
|
|
138
|
+
type FieldConstraintRule = RatingFieldConstraintRule | TextFieldConstraintRule | ChoiceFieldConstraintRule | BaseFieldConstraintRule;
|
|
117
139
|
interface FormPolicy {
|
|
118
140
|
readonly allowedFieldTypes?: readonly FieldType[];
|
|
119
141
|
readonly maxFields?: number;
|
|
@@ -123,6 +145,8 @@ interface FormPolicy {
|
|
|
123
145
|
readonly maxLocales?: number;
|
|
124
146
|
readonly maxTextLength?: number;
|
|
125
147
|
readonly maxSchemaBytes?: number;
|
|
148
|
+
/** Per-question-type defaults and immutable or bounded field constraints. */
|
|
149
|
+
readonly fieldConstraints?: Partial<Record<QuestionType, FieldConstraintRule>>;
|
|
126
150
|
}
|
|
127
151
|
type ConditionOperator = "equals" | "not_equals" | "contains" | "not_empty";
|
|
128
152
|
type ConditionValue = string | number | boolean;
|
|
@@ -223,6 +247,12 @@ interface SchemaIssue {
|
|
|
223
247
|
readonly path: string;
|
|
224
248
|
readonly code: string;
|
|
225
249
|
readonly message: string;
|
|
250
|
+
/** Compatibility discriminator for structured policy issues. */
|
|
251
|
+
readonly type?: string;
|
|
252
|
+
/** Present for policy issues that identify a field property and its expected value. */
|
|
253
|
+
readonly fieldId?: string;
|
|
254
|
+
readonly property?: string;
|
|
255
|
+
readonly expected?: boolean | number | readonly [number, number];
|
|
226
256
|
}
|
|
227
257
|
type SchemaValidationResult = {
|
|
228
258
|
readonly valid: true;
|
|
@@ -417,7 +447,6 @@ interface FormAnalytics {
|
|
|
417
447
|
readonly questions: readonly QuestionAggregate[];
|
|
418
448
|
}
|
|
419
449
|
type Question = FormField;
|
|
420
|
-
type QuestionType = FieldType;
|
|
421
450
|
type ChoiceOption = FieldOption;
|
|
422
451
|
/** Translation keys reserved for the form builder UI. */
|
|
423
452
|
type BuilderTranslationKey = `builder.${string}`;
|
|
@@ -577,6 +606,9 @@ interface CollectedLocales {
|
|
|
577
606
|
/** Collects locale registrations and every locale key used by translations or translation metadata. */
|
|
578
607
|
declare function collectSchemaLocales(schema: FormSchema): CollectedLocales;
|
|
579
608
|
|
|
609
|
+
interface SanitizeSchemaOptions {
|
|
610
|
+
readonly policy?: FormPolicy;
|
|
611
|
+
}
|
|
580
612
|
type SchemaStructureIssueType = "dangling_condition_reference" | "duplicate_question_id" | "duplicate_choice_id" | "self_condition_reference" | "cyclic_condition_reference";
|
|
581
613
|
interface SchemaStructureIssue {
|
|
582
614
|
readonly type: SchemaStructureIssueType;
|
|
@@ -585,7 +617,7 @@ interface SchemaStructureIssue {
|
|
|
585
617
|
readonly message: string;
|
|
586
618
|
}
|
|
587
619
|
declare function validateSchemaStructure(schema: FormSchema): SchemaStructureIssue[];
|
|
588
|
-
declare function sanitizeSchema(schema: FormSchema): FormSchema;
|
|
620
|
+
declare function sanitizeSchema(schema: FormSchema, options?: SanitizeSchemaOptions): FormSchema;
|
|
589
621
|
|
|
590
622
|
interface ValidateFormSchemaOptions {
|
|
591
623
|
readonly policy?: FormPolicy;
|
|
@@ -639,4 +671,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
|
|
|
639
671
|
declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
|
|
640
672
|
declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
|
|
641
673
|
|
|
642
|
-
export { type AccumulatorReport, type AccumulatorResponse, type AccumulatorSkipReason, type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type BuilderTranslationKey, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type CloneVersionOptions, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvColumnContext, type CsvColumnDef, type CsvExportOptions, DEFAULT_FIELD_TYPE_DEFINITIONS, type DeleteDraftOptions, type DisplayCondition, type ExtensibleNode, type FieldOption, type FieldType, type FieldTypeDefinition, 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 FormVersionRecord, type FormVersionState, type FormVersionStatus, type JsonValue, type LocalizedText, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PaginationIteratorOptions, type PopulateTranslationOptions, type PublishDraftOptions, type PublishDraftResult, type Question, type QuestionAggregate, type QuestionType, type RatingField, type ResponseAccumulator, type ResponseAccumulatorOptions, type Result, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type StorageCommitError, type StreamCsvOptions, type SubmissionCursorValue, type SubmissionFilter, type SubmissionPage, type SubmissionPageQueryOptions, type SubmissionQueryOptions, type TextAnswerCursorValue, type TextAnswerItem, type TextAnswerPage, type TextAnswerPageQueryOptions, type TextField, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type VersionTransitionError, type VersionTransitionEvent, type VersionTransitionPlan, type VersionedFormStorageAdapter, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, createCloneTransitionPlan, createDeleteDraftTransitionPlan, createPublishTransitionPlan, createResponseAccumulator, createSubmission, decodeSubmissionCursor, decodeTextAnswerCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, encodeTextAnswerCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, isDisplayConditionSatisfied, isQuestionVisible, iterateSubmissionPages, jsonValuesEqual, matchesSubmissionFilter, matchesSubmissionPageFilters, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
|
|
674
|
+
export { type AccumulatorReport, type AccumulatorResponse, type AccumulatorSkipReason, type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type BaseFieldConstraintRule, type BuilderTranslationKey, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceFieldConstraintRule, type ChoiceOption, type ChoiceQuestionAggregate, type CloneVersionOptions, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvColumnContext, type CsvColumnDef, type CsvExportOptions, DEFAULT_FIELD_TYPE_DEFINITIONS, type DeleteDraftOptions, type DisplayCondition, type ExtensibleNode, type FieldConstraintRule, type FieldOption, type FieldType, type FieldTypeDefinition, 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 FormVersionRecord, type FormVersionState, type FormVersionStatus, type JsonValue, type LocalizedText, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PaginationIteratorOptions, type PopulateTranslationOptions, type PublishDraftOptions, type PublishDraftResult, type Question, type QuestionAggregate, type QuestionType, type RatingField, type RatingFieldConstraintRule, type ResponseAccumulator, type ResponseAccumulatorOptions, type Result, type SanitizeSchemaOptions, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type StorageCommitError, type StreamCsvOptions, type SubmissionCursorValue, type SubmissionFilter, type SubmissionPage, type SubmissionPageQueryOptions, type SubmissionQueryOptions, type TextAnswerCursorValue, type TextAnswerItem, type TextAnswerPage, type TextAnswerPageQueryOptions, type TextField, type TextFieldConstraintRule, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type VersionTransitionError, type VersionTransitionEvent, type VersionTransitionPlan, type VersionedFormStorageAdapter, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, createCloneTransitionPlan, createDeleteDraftTransitionPlan, createPublishTransitionPlan, createResponseAccumulator, createSubmission, decodeSubmissionCursor, decodeTextAnswerCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, encodeTextAnswerCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, isDisplayConditionSatisfied, isQuestionVisible, iterateSubmissionPages, jsonValuesEqual, matchesSubmissionFilter, matchesSubmissionPageFilters, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
|
package/dist/index.d.ts
CHANGED
|
@@ -114,6 +114,28 @@ declare function createDeleteDraftTransitionPlan(state: FormVersionState, draftR
|
|
|
114
114
|
declare function assertVersionMutable(status: FormVersionStatus): void;
|
|
115
115
|
|
|
116
116
|
type FieldType = "text" | "textarea" | "number" | "rating" | "select" | "multi-select" | "checkbox" | "radio";
|
|
117
|
+
type QuestionType = FieldType;
|
|
118
|
+
interface BaseFieldConstraintRule {
|
|
119
|
+
readonly defaultRequired?: boolean;
|
|
120
|
+
readonly fixedRequired?: boolean;
|
|
121
|
+
}
|
|
122
|
+
interface RatingFieldConstraintRule extends BaseFieldConstraintRule {
|
|
123
|
+
readonly defaultMin?: number;
|
|
124
|
+
readonly defaultMax?: number;
|
|
125
|
+
readonly fixedMin?: number;
|
|
126
|
+
readonly fixedMax?: number;
|
|
127
|
+
readonly allowedMinRange?: readonly [number, number];
|
|
128
|
+
readonly allowedMaxRange?: readonly [number, number];
|
|
129
|
+
}
|
|
130
|
+
interface TextFieldConstraintRule extends BaseFieldConstraintRule {
|
|
131
|
+
readonly defaultMaxLength?: number;
|
|
132
|
+
readonly maxMaxLength?: number;
|
|
133
|
+
}
|
|
134
|
+
interface ChoiceFieldConstraintRule extends BaseFieldConstraintRule {
|
|
135
|
+
readonly minOptions?: number;
|
|
136
|
+
readonly maxOptions?: number;
|
|
137
|
+
}
|
|
138
|
+
type FieldConstraintRule = RatingFieldConstraintRule | TextFieldConstraintRule | ChoiceFieldConstraintRule | BaseFieldConstraintRule;
|
|
117
139
|
interface FormPolicy {
|
|
118
140
|
readonly allowedFieldTypes?: readonly FieldType[];
|
|
119
141
|
readonly maxFields?: number;
|
|
@@ -123,6 +145,8 @@ interface FormPolicy {
|
|
|
123
145
|
readonly maxLocales?: number;
|
|
124
146
|
readonly maxTextLength?: number;
|
|
125
147
|
readonly maxSchemaBytes?: number;
|
|
148
|
+
/** Per-question-type defaults and immutable or bounded field constraints. */
|
|
149
|
+
readonly fieldConstraints?: Partial<Record<QuestionType, FieldConstraintRule>>;
|
|
126
150
|
}
|
|
127
151
|
type ConditionOperator = "equals" | "not_equals" | "contains" | "not_empty";
|
|
128
152
|
type ConditionValue = string | number | boolean;
|
|
@@ -223,6 +247,12 @@ interface SchemaIssue {
|
|
|
223
247
|
readonly path: string;
|
|
224
248
|
readonly code: string;
|
|
225
249
|
readonly message: string;
|
|
250
|
+
/** Compatibility discriminator for structured policy issues. */
|
|
251
|
+
readonly type?: string;
|
|
252
|
+
/** Present for policy issues that identify a field property and its expected value. */
|
|
253
|
+
readonly fieldId?: string;
|
|
254
|
+
readonly property?: string;
|
|
255
|
+
readonly expected?: boolean | number | readonly [number, number];
|
|
226
256
|
}
|
|
227
257
|
type SchemaValidationResult = {
|
|
228
258
|
readonly valid: true;
|
|
@@ -417,7 +447,6 @@ interface FormAnalytics {
|
|
|
417
447
|
readonly questions: readonly QuestionAggregate[];
|
|
418
448
|
}
|
|
419
449
|
type Question = FormField;
|
|
420
|
-
type QuestionType = FieldType;
|
|
421
450
|
type ChoiceOption = FieldOption;
|
|
422
451
|
/** Translation keys reserved for the form builder UI. */
|
|
423
452
|
type BuilderTranslationKey = `builder.${string}`;
|
|
@@ -577,6 +606,9 @@ interface CollectedLocales {
|
|
|
577
606
|
/** Collects locale registrations and every locale key used by translations or translation metadata. */
|
|
578
607
|
declare function collectSchemaLocales(schema: FormSchema): CollectedLocales;
|
|
579
608
|
|
|
609
|
+
interface SanitizeSchemaOptions {
|
|
610
|
+
readonly policy?: FormPolicy;
|
|
611
|
+
}
|
|
580
612
|
type SchemaStructureIssueType = "dangling_condition_reference" | "duplicate_question_id" | "duplicate_choice_id" | "self_condition_reference" | "cyclic_condition_reference";
|
|
581
613
|
interface SchemaStructureIssue {
|
|
582
614
|
readonly type: SchemaStructureIssueType;
|
|
@@ -585,7 +617,7 @@ interface SchemaStructureIssue {
|
|
|
585
617
|
readonly message: string;
|
|
586
618
|
}
|
|
587
619
|
declare function validateSchemaStructure(schema: FormSchema): SchemaStructureIssue[];
|
|
588
|
-
declare function sanitizeSchema(schema: FormSchema): FormSchema;
|
|
620
|
+
declare function sanitizeSchema(schema: FormSchema, options?: SanitizeSchemaOptions): FormSchema;
|
|
589
621
|
|
|
590
622
|
interface ValidateFormSchemaOptions {
|
|
591
623
|
readonly policy?: FormPolicy;
|
|
@@ -639,4 +671,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
|
|
|
639
671
|
declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
|
|
640
672
|
declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
|
|
641
673
|
|
|
642
|
-
export { type AccumulatorReport, type AccumulatorResponse, type AccumulatorSkipReason, type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type BuilderTranslationKey, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type CloneVersionOptions, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvColumnContext, type CsvColumnDef, type CsvExportOptions, DEFAULT_FIELD_TYPE_DEFINITIONS, type DeleteDraftOptions, type DisplayCondition, type ExtensibleNode, type FieldOption, type FieldType, type FieldTypeDefinition, 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 FormVersionRecord, type FormVersionState, type FormVersionStatus, type JsonValue, type LocalizedText, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PaginationIteratorOptions, type PopulateTranslationOptions, type PublishDraftOptions, type PublishDraftResult, type Question, type QuestionAggregate, type QuestionType, type RatingField, type ResponseAccumulator, type ResponseAccumulatorOptions, type Result, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type StorageCommitError, type StreamCsvOptions, type SubmissionCursorValue, type SubmissionFilter, type SubmissionPage, type SubmissionPageQueryOptions, type SubmissionQueryOptions, type TextAnswerCursorValue, type TextAnswerItem, type TextAnswerPage, type TextAnswerPageQueryOptions, type TextField, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type VersionTransitionError, type VersionTransitionEvent, type VersionTransitionPlan, type VersionedFormStorageAdapter, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, createCloneTransitionPlan, createDeleteDraftTransitionPlan, createPublishTransitionPlan, createResponseAccumulator, createSubmission, decodeSubmissionCursor, decodeTextAnswerCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, encodeTextAnswerCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, isDisplayConditionSatisfied, isQuestionVisible, iterateSubmissionPages, jsonValuesEqual, matchesSubmissionFilter, matchesSubmissionPageFilters, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
|
|
674
|
+
export { type AccumulatorReport, type AccumulatorResponse, type AccumulatorSkipReason, type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type BaseFieldConstraintRule, type BuilderTranslationKey, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceFieldConstraintRule, type ChoiceOption, type ChoiceQuestionAggregate, type CloneVersionOptions, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvColumnContext, type CsvColumnDef, type CsvExportOptions, DEFAULT_FIELD_TYPE_DEFINITIONS, type DeleteDraftOptions, type DisplayCondition, type ExtensibleNode, type FieldConstraintRule, type FieldOption, type FieldType, type FieldTypeDefinition, 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 FormVersionRecord, type FormVersionState, type FormVersionStatus, type JsonValue, type LocalizedText, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PaginationIteratorOptions, type PopulateTranslationOptions, type PublishDraftOptions, type PublishDraftResult, type Question, type QuestionAggregate, type QuestionType, type RatingField, type RatingFieldConstraintRule, type ResponseAccumulator, type ResponseAccumulatorOptions, type Result, type SanitizeSchemaOptions, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type StorageCommitError, type StreamCsvOptions, type SubmissionCursorValue, type SubmissionFilter, type SubmissionPage, type SubmissionPageQueryOptions, type SubmissionQueryOptions, type TextAnswerCursorValue, type TextAnswerItem, type TextAnswerPage, type TextAnswerPageQueryOptions, type TextField, type TextFieldConstraintRule, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type VersionTransitionError, type VersionTransitionEvent, type VersionTransitionPlan, type VersionedFormStorageAdapter, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, createCloneTransitionPlan, createDeleteDraftTransitionPlan, createPublishTransitionPlan, createResponseAccumulator, createSubmission, decodeSubmissionCursor, decodeTextAnswerCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, encodeTextAnswerCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, isDisplayConditionSatisfied, isQuestionVisible, iterateSubmissionPages, jsonValuesEqual, matchesSubmissionFilter, matchesSubmissionPageFilters, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
|
package/dist/index.js
CHANGED
|
@@ -87,6 +87,27 @@ function sanitizePageLocales(page, registeredLocales) {
|
|
|
87
87
|
const translations = registeredEntries(page.translations, registeredLocales);
|
|
88
88
|
return { ...base, ...translations === void 0 ? {} : { translations } };
|
|
89
89
|
}
|
|
90
|
+
function sanitizeFieldConstraints(field, policy) {
|
|
91
|
+
const constraint = policy?.fieldConstraints?.[field.type];
|
|
92
|
+
if (constraint === void 0) return field;
|
|
93
|
+
const required = constraint.fixedRequired ?? field.required;
|
|
94
|
+
if (field.type === "rating") {
|
|
95
|
+
const ratingConstraint = "fixedMin" in constraint || "fixedMax" in constraint ? constraint : void 0;
|
|
96
|
+
const min = ratingConstraint !== void 0 && "fixedMin" in ratingConstraint ? ratingConstraint.fixedMin : field.min;
|
|
97
|
+
const max = ratingConstraint !== void 0 && "fixedMax" in ratingConstraint ? ratingConstraint.fixedMax : field.max;
|
|
98
|
+
return {
|
|
99
|
+
...field,
|
|
100
|
+
required,
|
|
101
|
+
...min === void 0 ? {} : { min },
|
|
102
|
+
...max === void 0 ? {} : { max }
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if ((field.type === "text" || field.type === "textarea") && "maxMaxLength" in constraint) {
|
|
106
|
+
const maxLength = field.maxLength === void 0 || constraint.maxMaxLength === void 0 ? field.maxLength : Math.min(field.maxLength, constraint.maxMaxLength);
|
|
107
|
+
return { ...field, required, ...maxLength === void 0 ? {} : { maxLength } };
|
|
108
|
+
}
|
|
109
|
+
return { ...field, required };
|
|
110
|
+
}
|
|
90
111
|
function cyclicQuestionIds(fields) {
|
|
91
112
|
const firstById = /* @__PURE__ */ new Map();
|
|
92
113
|
for (const field of fields) {
|
|
@@ -171,7 +192,7 @@ function validateSchemaStructure(schema) {
|
|
|
171
192
|
}
|
|
172
193
|
return issues;
|
|
173
194
|
}
|
|
174
|
-
function sanitizeSchema(schema) {
|
|
195
|
+
function sanitizeSchema(schema, options = {}) {
|
|
175
196
|
const existingQuestionIds = new Set(schema.fields.map((field) => field.id));
|
|
176
197
|
const registeredLocales = /* @__PURE__ */ new Set([
|
|
177
198
|
...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
|
|
@@ -179,7 +200,7 @@ function sanitizeSchema(schema) {
|
|
|
179
200
|
]);
|
|
180
201
|
const cyclic = cyclicQuestionIds(schema.fields);
|
|
181
202
|
const sanitizedFields = schema.fields.map((sourceField) => {
|
|
182
|
-
const field = sanitizeFieldLocales(sourceField, registeredLocales);
|
|
203
|
+
const field = sanitizeFieldConstraints(sanitizeFieldLocales(sourceField, registeredLocales), options.policy);
|
|
183
204
|
const sourceId = field.displayCondition?.questionId;
|
|
184
205
|
if (sourceId === void 0 || existingQuestionIds.has(sourceId) && sourceId !== field.id && !cyclic.has(field.id)) {
|
|
185
206
|
return field;
|
|
@@ -237,6 +258,17 @@ function isNonEmptyString(value) {
|
|
|
237
258
|
function issue(issues, path, code, message) {
|
|
238
259
|
issues.push({ path, code, message });
|
|
239
260
|
}
|
|
261
|
+
function fieldConstraintIssue(issues, field, fieldIndex, property, expected, message) {
|
|
262
|
+
issues.push({
|
|
263
|
+
path: `fields[${fieldIndex}].${property}`,
|
|
264
|
+
code: "field_constraint_violation",
|
|
265
|
+
type: "field_constraint_violation",
|
|
266
|
+
message,
|
|
267
|
+
fieldId: field.id,
|
|
268
|
+
property,
|
|
269
|
+
expected
|
|
270
|
+
});
|
|
271
|
+
}
|
|
240
272
|
function validateJsonValue(value, path, issues, ancestors = /* @__PURE__ */ new Set()) {
|
|
241
273
|
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
242
274
|
if (typeof value === "number") {
|
|
@@ -590,6 +622,93 @@ function validatePolicy(schema, policy, issues) {
|
|
|
590
622
|
`At most ${policy.maxOptionsPerField} options are allowed.`
|
|
591
623
|
);
|
|
592
624
|
}
|
|
625
|
+
const constraint = policy.fieldConstraints?.[field.type];
|
|
626
|
+
if (constraint === void 0) return;
|
|
627
|
+
if (constraint.fixedRequired !== void 0 && field.required !== constraint.fixedRequired) {
|
|
628
|
+
fieldConstraintIssue(
|
|
629
|
+
issues,
|
|
630
|
+
field,
|
|
631
|
+
fieldIndex,
|
|
632
|
+
"required",
|
|
633
|
+
constraint.fixedRequired,
|
|
634
|
+
`Field required must be ${String(constraint.fixedRequired)} for field type ${field.type}.`
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
if (field.type === "rating") {
|
|
638
|
+
const ratingConstraint = "fixedMin" in constraint || "fixedMax" in constraint || "allowedMinRange" in constraint || "allowedMaxRange" in constraint ? constraint : void 0;
|
|
639
|
+
if (ratingConstraint?.fixedMin !== void 0 && field.min !== ratingConstraint.fixedMin) {
|
|
640
|
+
fieldConstraintIssue(
|
|
641
|
+
issues,
|
|
642
|
+
field,
|
|
643
|
+
fieldIndex,
|
|
644
|
+
"min",
|
|
645
|
+
ratingConstraint.fixedMin,
|
|
646
|
+
`Rating minimum must be ${ratingConstraint.fixedMin}.`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
if (ratingConstraint !== void 0 && "fixedMax" in ratingConstraint && ratingConstraint.fixedMax !== void 0 && field.max !== ratingConstraint.fixedMax) {
|
|
650
|
+
fieldConstraintIssue(
|
|
651
|
+
issues,
|
|
652
|
+
field,
|
|
653
|
+
fieldIndex,
|
|
654
|
+
"max",
|
|
655
|
+
ratingConstraint.fixedMax,
|
|
656
|
+
`Rating maximum must be ${ratingConstraint.fixedMax}.`
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
if (ratingConstraint !== void 0 && "allowedMinRange" in ratingConstraint && ratingConstraint.allowedMinRange !== void 0 && typeof field.min === "number" && (field.min < ratingConstraint.allowedMinRange[0] || field.min > ratingConstraint.allowedMinRange[1])) {
|
|
660
|
+
fieldConstraintIssue(
|
|
661
|
+
issues,
|
|
662
|
+
field,
|
|
663
|
+
fieldIndex,
|
|
664
|
+
"min",
|
|
665
|
+
ratingConstraint.allowedMinRange,
|
|
666
|
+
`Rating minimum must be between ${ratingConstraint.allowedMinRange[0]} and ${ratingConstraint.allowedMinRange[1]}.`
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
if (ratingConstraint !== void 0 && "allowedMaxRange" in ratingConstraint && ratingConstraint.allowedMaxRange !== void 0 && typeof field.max === "number" && (field.max < ratingConstraint.allowedMaxRange[0] || field.max > ratingConstraint.allowedMaxRange[1])) {
|
|
670
|
+
fieldConstraintIssue(
|
|
671
|
+
issues,
|
|
672
|
+
field,
|
|
673
|
+
fieldIndex,
|
|
674
|
+
"max",
|
|
675
|
+
ratingConstraint.allowedMaxRange,
|
|
676
|
+
`Rating maximum must be between ${ratingConstraint.allowedMaxRange[0]} and ${ratingConstraint.allowedMaxRange[1]}.`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if ((field.type === "text" || field.type === "textarea") && "maxMaxLength" in constraint && constraint.maxMaxLength !== void 0) {
|
|
681
|
+
if (field.maxLength !== void 0 && field.maxLength > constraint.maxMaxLength) {
|
|
682
|
+
fieldConstraintIssue(
|
|
683
|
+
issues,
|
|
684
|
+
field,
|
|
685
|
+
fieldIndex,
|
|
686
|
+
"maxLength",
|
|
687
|
+
constraint.maxMaxLength,
|
|
688
|
+
`Maximum text length must be at most ${constraint.maxMaxLength}.`
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if ((field.type === "select" || field.type === "radio" || field.type === "multi-select") && "minOptions" in constraint && constraint.minOptions !== void 0 && field.options.length < constraint.minOptions) {
|
|
693
|
+
fieldConstraintIssue(
|
|
694
|
+
issues,
|
|
695
|
+
field,
|
|
696
|
+
fieldIndex,
|
|
697
|
+
"options",
|
|
698
|
+
constraint.minOptions,
|
|
699
|
+
`At least ${constraint.minOptions} options are required.`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
if ((field.type === "select" || field.type === "radio" || field.type === "multi-select") && "maxOptions" in constraint && constraint.maxOptions !== void 0 && field.options.length > constraint.maxOptions) {
|
|
703
|
+
fieldConstraintIssue(
|
|
704
|
+
issues,
|
|
705
|
+
field,
|
|
706
|
+
fieldIndex,
|
|
707
|
+
"options",
|
|
708
|
+
constraint.maxOptions,
|
|
709
|
+
`At most ${constraint.maxOptions} options are allowed.`
|
|
710
|
+
);
|
|
711
|
+
}
|
|
593
712
|
});
|
|
594
713
|
if (policy.maxTextLength !== void 0) {
|
|
595
714
|
for (const entry of collectSchemaText(schema)) {
|