@nextlyhq/plugin-form-builder 0.0.2-alpha.68 → 0.0.2-alpha.69
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 +5 -5
- package/dist/admin/index.d.ts +218 -250
- package/dist/admin/index.js +4662 -4785
- package/dist/admin/index.js.map +1 -1
- package/dist/field-references-BshlgMcn.js +1048 -0
- package/dist/field-references-BshlgMcn.js.map +1 -0
- package/dist/index.d.ts +304 -513
- package/dist/index.js +3753 -2916
- package/dist/index.js.map +1 -1
- package/dist/types-BHReIA0j.d.ts +986 -0
- package/package.json +22 -23
- package/dist/chunk-X5WLJLAR.js +0 -703
- package/dist/chunk-X5WLJLAR.js.map +0 -1
- package/dist/types-Bo4lk9BD.d.ts +0 -997
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts","../src/config/removed-options.ts","../src/config/defaults.ts","../src/utils/evaluate-conditions.ts","../src/utils/export-formats.ts","../src/utils/redirect-reference.ts","../src/utils/enforced-validation.ts","../src/utils/form-settings.ts","../src/utils/field-references.ts"],"sourcesContent":["/**\n * Form Builder Plugin Types\n *\n * Defines configuration options and types for the form builder plugin.\n *\n * @packageDocumentation\n */\n\nimport type { CollectionConfig, FieldDefinition, RequestContext } from \"nextly\";\nimport type { ComponentType } from \"react\";\n\nimport type { RedirectRelationships } from \"./utils/redirect-target\";\n\n// ============================================================\n// Supporting Types\n// ============================================================\n\n/**\n * Configuration for customizing field block behavior.\n */\nexport interface FieldBlockConfig {\n /** Default label for this field type */\n label?: string;\n /** Default validation settings */\n validation?: Record<string, unknown>;\n /** Custom component to use for this field type */\n component?: ComponentType<unknown>;\n}\n\n/**\n * Email configuration for notifications.\n */\nexport interface EmailConfig {\n to: string | string[];\n from?: string;\n subject: string;\n html?: string;\n text?: string;\n replyTo?: string;\n cc?: string | string[];\n bcc?: string | string[];\n}\n\n/**\n * @experimental A resolved outgoing form notification (template-based) as built\n * by the submission handler, before it is sent. This is the value transformed by\n * the `form-builder.beforeEmail` D63 filter seam.\n */\nexport interface FormEmailNotification {\n /** Resolved recipient address. */\n to: string;\n /** Email template slug to render. */\n templateSlug: string;\n /** Template variables (submitted data + form metadata). */\n variables: Record<string, unknown>;\n /** Email provider id; undefined = system default. */\n providerId?: string;\n /**\n * Resolved sender address (rule's senderEmail, else the plugin's\n * defaultFrom); undefined = template/provider default.\n */\n from?: string;\n /** Resolved Reply-To address, if the rule configured one. */\n replyTo?: string;\n /** CC addresses, if any. */\n cc?: string[];\n /** BCC addresses, if any. */\n bcc?: string[];\n /** Source notification id; undefined for filter-injected emails. */\n notificationId?: string;\n}\n\n/** @experimental Context passed to the `form-builder.beforeEmail` filter seam. */\nexport interface BeforeEmailFilterContext {\n form: Record<string, unknown>;\n submission: Record<string, unknown>;\n}\n\n/**\n * A single email notification rule stored on a form.\n * Matches the shape saved by the form builder admin UI. This is the ONE\n * notification rule type — the admin context re-exports it rather than\n * keeping a parallel copy.\n */\nexport interface FormNotification {\n /** Unique ID for this notification */\n id: string;\n /** Display name for this notification rule */\n name: string;\n /** Whether this notification is active */\n enabled: boolean;\n /** ID of the email provider to use; undefined = system default */\n providerId?: string;\n /**\n * Sender email override. Resolution order at send time:\n * this value, else the plugin's `notifications.defaultFrom` option, else\n * the template/provider default.\n */\n senderEmail?: string;\n /** How the recipient address is determined */\n recipientType: \"static\" | \"field\";\n /** Recipient: static email address or a {{fieldName}} reference */\n to: string;\n /** CC email addresses */\n cc: string[];\n /** BCC email addresses */\n bcc: string[];\n /**\n * Reply-To address: a `{{fieldName}}` reference to one of the form's\n * email fields (resolved against the submission) or a literal address.\n */\n replyTo?: string;\n /**\n * Optional send condition evaluated against the submitted data. When set\n * and unmet, the rule is skipped for that submission. A single condition\n * covers the common case; the JSON storage leaves room to grow into a\n * multi-condition tree without a migration.\n */\n condition?: ConditionalLogicCondition;\n /** Slug of the email template to use */\n templateSlug?: string;\n}\n\n/**\n * @deprecated Use {@link FormNotification}. Alias kept so existing imports\n * keep compiling during the alpha; will be removed before a stable release.\n */\nexport type FormNotificationItem = FormNotification;\n\n/**\n * Form document structure (stored in database).\n */\nexport interface FormDocument {\n id: string;\n slug: string;\n name: string;\n fields: AnyFormField[];\n settings: FormSettings;\n notifications: FormNotification[];\n status: \"draft\" | \"published\" | \"closed\";\n /**\n * What a closed form tells a visitor who reaches it.\n *\n * Optional because the field is only written once an author has a reason to\n * give, and a row created before it existed has none. A closed form with\n * nothing here falls back to the generic refusal, which is also what a draft\n * gets — a draft has never been public, so there is no author intent to\n * relay.\n */\n closedMessage?: string;\n createdAt: Date;\n updatedAt: Date;\n}\n\n/**\n * Submission document structure (stored in database).\n */\nexport interface SubmissionDocument {\n id: string;\n form: string | FormDocument;\n data: Record<string, unknown>;\n /**\n * Where the submission sits in review.\n *\n * `spam` is one of them, not a separate flag: the stored field offers it, the\n * admin has a Spam tab and filters the other views with `not_equals: \"spam\"`,\n * the notification hook skips it, and marking something \"Not spam\" moves it\n * back to `new`. `spamReason` records WHICH check flagged it.\n */\n status: \"new\" | \"read\" | \"archived\" | \"spam\";\n ipAddress?: string;\n userAgent?: string;\n /** Which spam check flagged this, when one did. */\n spamReason?: string | null;\n submittedAt: Date;\n createdAt: Date;\n updatedAt: Date;\n}\n\n// ============================================================\n// Plugin Configuration\n// ============================================================\n\n/**\n * Configuration options for the form builder plugin.\n */\n/**\n * Which field types the builder offers, and how each one is customised.\n *\n * Declared ONCE and viewed twice: the options surface takes\n * `Partial<FormFieldToggles>` and the resolved config takes it whole, because\n * \"resolved\" means exactly \"every toggle now has a value\". Written out twice\n * the two lists agree until someone adds a field type to one of them.\n */\nexport interface FormFieldToggles {\n text: boolean | Partial<FieldBlockConfig>;\n email: boolean | Partial<FieldBlockConfig>;\n number: boolean | Partial<FieldBlockConfig>;\n phone: boolean | Partial<FieldBlockConfig>;\n url: boolean | Partial<FieldBlockConfig>;\n textarea: boolean | Partial<FieldBlockConfig>;\n select: boolean | Partial<FieldBlockConfig>;\n checkbox: boolean | Partial<FieldBlockConfig>;\n radio: boolean | Partial<FieldBlockConfig>;\n file: boolean | Partial<FieldBlockConfig>;\n date: boolean | Partial<FieldBlockConfig>;\n time: boolean | Partial<FieldBlockConfig>;\n hidden: boolean | Partial<FieldBlockConfig>;\n}\n\n/** What a host may override on a collection this plugin contributes. */\nexport type CollectionOverrides = Partial<CollectionConfig> & {\n slug?: string;\n labels?: { singular?: string; plural?: string };\n /** Fields can be an array or a function receiving defaultFields */\n fields?:\n | FieldDefinition[]\n | ((args: { defaultFields: FieldDefinition[] }) => FieldDefinition[]);\n};\n\n/** The same, once the plugin's defaults have filled in what a host omitted. */\nexport type ResolvedCollectionOverrides = {\n slug: string;\n labels: { singular: string; plural: string };\n} & Partial<CollectionConfig>;\n\nexport interface FormBuilderPluginOptions {\n /**\n * Override the default Forms collection configuration.\n *\n * Allows customizing the slug, labels, fields, access control, and other\n * collection options.\n *\n * The `fields` option can be:\n * - An array of fields (merged with defaults)\n * - A function receiving `{ defaultFields }` for full control\n *\n * @example\n * ```typescript\n * // Option 1: Array (fields merged with defaults)\n * formOverrides: {\n * slug: 'my-forms',\n * labels: { singular: 'My Form', plural: 'My Forms' },\n * access: {\n * read: () => true,\n * // Narrow the permission, never replace it: the role screen still\n * // decides who holds `create-my-forms`.\n * create: ({ permissions }) =>\n * permissions.includes('my-forms:create') && process.env.CONTENT_FREEZE !== 'on',\n * },\n * fields: [\n * text({ name: 'internalNotes', label: 'Internal Notes' }),\n * ],\n * }\n *\n * // Option 2: Function (full control over fields)\n * formOverrides: {\n * slug: 'contact-forms',\n * access: {\n * read: ({ permissions }) => permissions.includes('contact-forms:read'),\n * update: () => false,\n * },\n * fields: ({ defaultFields }) => {\n * return [\n * ...defaultFields,\n * { name: 'custom', type: 'text' },\n * ];\n * },\n * }\n * ```\n */\n formOverrides?: CollectionOverrides;\n\n /**\n * Override the default Form Submissions collection configuration.\n *\n * Allows customizing the slug, labels, fields, access control, and other\n * collection options.\n *\n * The `fields` option can be:\n * - An array of fields (merged with defaults)\n * - A function receiving `{ defaultFields }` for full control\n *\n * @example\n * ```typescript\n * // Option 1: Array (fields merged with defaults)\n * formSubmissionOverrides: {\n * slug: 'my-submissions',\n * labels: { singular: 'Response', plural: 'Responses' },\n * access: {\n * read: ({ permissions }) => permissions.includes('my-submissions:read'),\n * },\n * fields: [\n * select({\n * name: 'priority',\n * label: 'Priority',\n * options: [\n * { label: 'Low', value: 'low' },\n * { label: 'High', value: 'high' },\n * ],\n * }),\n * ],\n * }\n *\n * // Option 2: Function (full control over fields)\n * formSubmissionOverrides: {\n * fields: ({ defaultFields }) => {\n * // Add field after 'form' field\n * const formIndex = defaultFields.findIndex(f => f.name === 'form');\n * return [\n * ...defaultFields.slice(0, formIndex + 1),\n * { name: 'source', type: 'text' },\n * ...defaultFields.slice(formIndex + 1),\n * ];\n * },\n * }\n * ```\n */\n formSubmissionOverrides?: CollectionOverrides;\n\n /**\n * Configure which field types are available in the form builder.\n *\n * Set to `false` to disable a field type, or provide partial configuration\n * to customize default behavior.\n *\n * @example\n * ```typescript\n * fields: {\n * // Disable payment field\n * payment: false,\n *\n * // Customize text field defaults\n * text: {\n * maxLength: 500,\n * },\n *\n * // Disable file uploads\n * file: false,\n * }\n * ```\n */\n fields?: Partial<FormFieldToggles>;\n\n /**\n * Collections whose documents a form may redirect to after submission, and\n * how each one's documents become URLs.\n *\n * The mapping is configuration because it cannot be derived: a site's URL\n * structure belongs to the site. `{field}` placeholders are filled from the\n * document; a function covers a path this package cannot express.\n *\n * The array shorthand names the collections and takes `/{slug}` for all of\n * them, which is the routing the page builder ships.\n *\n * @example\n * ```typescript\n * redirectRelationships: ['pages']\n *\n * redirectRelationships: {\n * pages: '/{slug}',\n * posts: '/blog/{slug}',\n * docs: page => `/docs/${String(page.section)}/${String(page.slug)}`,\n * }\n * ```\n */\n redirectRelationships?: string[] | RedirectRelationships;\n\n /**\n * Hook called before sending email notifications.\n *\n * Runs as the `form-builder.beforeEmail` D63 filter: the resolved outgoing\n * notifications (template + recipient + variables) are threaded through this\n * callback before they are sent. Allows modifying, adding, or filtering the\n * outgoing notifications. Return the modified array.\n *\n * @example\n * ```typescript\n * beforeEmail: async ({ emails, form, submission }) => {\n * // Add BCC to all outgoing notifications\n * return emails.map((e) => ({ ...e, bcc: ['archive@example.com'] }));\n * }\n * ```\n */\n beforeEmail?: (args: {\n emails: FormEmailNotification[];\n form: FormDocument;\n submission: SubmissionDocument;\n }) => Promise<FormEmailNotification[]> | FormEmailNotification[];\n\n /**\n * Email notification settings.\n */\n notifications?: {\n /**\n * Default \"from\" email address.\n */\n defaultFrom?: string;\n\n /**\n * Default \"to\" email address for all form submissions.\n *\n * If specified, this email will receive all form submissions\n * regardless of form-specific notification settings.\n * @example\n * ```typescript\n * notifications: {\n * defaultToEmail: 'submissions@example.com',\n * }\n * ```\n */\n defaultToEmail?: string;\n\n /**\n * Enable/disable email notifications globally.\n * @default true\n */\n enabled?: boolean;\n };\n\n /**\n * Spam protection configuration.\n */\n spamProtection?: {\n /**\n * Enable honeypot field for spam detection.\n * @default true\n */\n honeypot?: boolean;\n\n /**\n * Enable reCAPTCHA v3.\n * @default false\n */\n recaptcha?: {\n enabled: boolean;\n siteKey?: string;\n secretKey?: string;\n scoreThreshold?: number; // 0.0 - 1.0\n };\n\n /**\n * Rate limiting configuration.\n */\n rateLimit?: {\n /**\n * Maximum submissions per window.\n * @default 10\n */\n maxSubmissions?: number;\n\n /**\n * Time window in milliseconds.\n * @default 60000 (1 minute)\n */\n windowMs?: number;\n };\n };\n\n /**\n * File upload settings.\n */\n uploads?: {\n /**\n * Maximum file size in bytes.\n * @default 10485760 (10MB)\n */\n maxFileSize?: number;\n\n /**\n * Allowed file types (MIME types).\n * @default ['image/*', 'application/pdf', 'text/*']\n */\n allowedMimeTypes?: string[];\n\n /**\n * Collection to store uploaded files.\n * @default 'media'\n */\n uploadCollection?: string;\n };\n\n /**\n * Enable/disable features.\n */\n features?: {\n /**\n * Enable visual form builder UI.\n * @default true\n */\n builder?: boolean;\n\n /**\n * Enable conditional logic.\n * @default true\n */\n conditionalLogic?: boolean;\n\n /**\n * Enable file uploads.\n * @default true\n */\n fileUploads?: boolean;\n };\n}\n\n// ============================================================\n// Form Configuration Types\n// ============================================================\n\n/**\n * Form configuration interface.\n * Similar to CollectionConfig but tailored for forms.\n */\nexport interface FormConfig {\n /**\n * Unique identifier for the form.\n */\n slug: string;\n\n /**\n * Display labels for the form.\n */\n labels?: {\n singular?: string;\n plural?: string;\n };\n\n /**\n * Form fields definition.\n */\n fields: AnyFormField[];\n\n /**\n * Form settings and behavior.\n */\n settings?: FormSettings;\n\n /**\n * Email notification configuration.\n */\n notifications?: FormNotifications;\n\n /**\n * Access control for form submission and viewing.\n */\n access?: FormAccess;\n\n /**\n * Lifecycle hooks for form submissions.\n */\n hooks?: FormHooks;\n\n /**\n * Custom metadata.\n */\n custom?: Record<string, unknown>;\n}\n\n// ============================================================\n// Form Field Types (Discriminated Union)\n// ============================================================\n\n/**\n * Form field type (discriminated union).\n * Uses the `type` property as the discriminant.\n */\nexport type FormField =\n | TextFormField\n | EmailFormField\n | NumberFormField\n | PhoneFormField\n | UrlFormField\n | TextareaFormField\n | SelectFormField\n | CheckboxFormField\n | RadioFormField\n | FileFormField\n | DateFormField\n | TimeFormField\n | HiddenFormField;\n\n/**\n * A form field of a built-in type OR a plugin-contributed type. Stored form\n * data and the builder work with this; `isKnownFormField` narrows it back to\n * the closed `FormField` union so `switch (field.type)` keeps discriminating\n * the built-ins (a `CustomFormField` in the union itself would collapse that).\n */\nexport type AnyFormField = FormField | CustomFormField;\n\n/**\n * All available built-in form field type identifiers.\n */\nexport type FormFieldType =\n | \"text\"\n | \"email\"\n | \"number\"\n | \"phone\"\n | \"url\"\n | \"textarea\"\n | \"select\"\n | \"checkbox\"\n | \"radio\"\n | \"file\"\n | \"date\"\n | \"time\"\n | \"hidden\";\n\n/**\n * A form field's type on the wire: a built-in or a plugin-contributed type id\n * that opted into the forms surface. The `(string & {})` arm admits the plugin\n * id while keeping built-in autocomplete. Built-in `case` narrowing still works\n * because every field-type switch keeps a `default` for the custom arm.\n */\nexport type FormFieldTypeId = FormFieldType | (string & {});\n\n/** Every built-in form field type, as a value list (the single source shared\n * by the field-type guard and the config validator). */\nexport const BUILT_IN_FORM_FIELD_TYPES: readonly FormFieldType[] = [\n \"text\",\n \"email\",\n \"number\",\n \"phone\",\n \"url\",\n \"textarea\",\n \"select\",\n \"checkbox\",\n \"radio\",\n \"file\",\n \"date\",\n \"time\",\n \"hidden\",\n];\n\nconst BUILT_IN_FORM_FIELD_TYPES_SET = new Set<string>(\n BUILT_IN_FORM_FIELD_TYPES\n);\n\n/**\n * Narrow an `AnyFormField` to the closed built-in `FormField` union. Returns\n * `false` for a plugin-contributed field, so callers handle it generically\n * before switching on the built-in discriminant.\n */\nexport function isKnownFormField(field: AnyFormField): field is FormField {\n return BUILT_IN_FORM_FIELD_TYPES_SET.has(field.type);\n}\n\n/**\n * Base form field properties.\n * All form field types extend this interface.\n */\nexport interface BaseFormField {\n /** Field type identifier (discriminant) */\n type: FormFieldType;\n\n /** Field name (used as key in submission data) */\n name: string;\n\n /** Display label */\n label: string;\n\n /** Placeholder text */\n placeholder?: string;\n\n /** Help text shown below field */\n helpText?: string;\n\n /** Default value */\n defaultValue?: unknown;\n\n /** Whether field is required */\n required?: boolean;\n\n /** Validation rules */\n validation?: ValidationRules;\n\n /** Conditional logic configuration */\n conditionalLogic?: ConditionalLogic;\n\n /** Admin UI configuration */\n admin?: {\n /** CSS classes */\n className?: string;\n\n /** Field width */\n width?: \"25%\" | \"33%\" | \"50%\" | \"66%\" | \"75%\" | \"100%\";\n };\n}\n\n/**\n * A plugin-contributed form field type (outside the built-in union). Carries\n * only the base properties — the plugin owns its editor component and value\n * semantics — and its `type` is the plugin's registered field-type id. Kept a\n * distinct union member so built-in `case` narrowing is unaffected while a\n * plugin field still type-checks as a `FormField`.\n */\nexport interface CustomFormField extends Omit<BaseFormField, \"type\"> {\n type: string & {};\n}\n\n/**\n * Text input field.\n */\nexport interface TextFormField extends Omit<BaseFormField, \"type\"> {\n type: \"text\";\n defaultValue?: string;\n validation?: ValidationRules & {\n minLength?: number;\n maxLength?: number;\n pattern?: string;\n };\n}\n\n/**\n * Email input field.\n */\nexport interface EmailFormField extends Omit<BaseFormField, \"type\"> {\n type: \"email\";\n defaultValue?: string;\n validation?: ValidationRules & {\n pattern?: string;\n };\n}\n\n/**\n * Number input field.\n */\nexport interface NumberFormField extends Omit<BaseFormField, \"type\"> {\n type: \"number\";\n defaultValue?: number;\n validation?: ValidationRules & {\n min?: number;\n max?: number;\n step?: number;\n };\n}\n\n/**\n * Phone input field.\n */\nexport interface PhoneFormField extends Omit<BaseFormField, \"type\"> {\n type: \"phone\";\n defaultValue?: string;\n validation?: ValidationRules & {\n pattern?: string;\n };\n}\n\n/**\n * URL input field.\n */\nexport interface UrlFormField extends Omit<BaseFormField, \"type\"> {\n type: \"url\";\n defaultValue?: string;\n validation?: ValidationRules & {\n pattern?: string;\n };\n}\n\n/**\n * Textarea field.\n */\nexport interface TextareaFormField extends Omit<BaseFormField, \"type\"> {\n type: \"textarea\";\n defaultValue?: string;\n rows?: number;\n validation?: ValidationRules & {\n minLength?: number;\n maxLength?: number;\n };\n}\n\n/**\n * Select dropdown field.\n */\nexport interface SelectFormField extends Omit<BaseFormField, \"type\"> {\n type: \"select\";\n options: Array<{ label: string; value: string }>;\n defaultValue?: string | string[];\n allowMultiple?: boolean;\n}\n\n/**\n * Single checkbox field.\n */\nexport interface CheckboxFormField extends Omit<BaseFormField, \"type\"> {\n type: \"checkbox\";\n defaultValue?: boolean;\n}\n\n/**\n * Radio group field.\n */\nexport interface RadioFormField extends Omit<BaseFormField, \"type\"> {\n type: \"radio\";\n options: Array<{ label: string; value: string }>;\n defaultValue?: string;\n}\n\n/**\n * File upload field.\n */\nexport interface FileFormField extends Omit<BaseFormField, \"type\"> {\n type: \"file\";\n accept?: string; // MIME types\n multiple?: boolean;\n maxFileSize?: number; // bytes\n /**\n * When true, files uploaded through this field are attached to all\n * notification emails for this form. Default false.\n */\n attachToEmail?: boolean;\n}\n\n/**\n * Date picker field.\n */\nexport interface DateFormField extends Omit<BaseFormField, \"type\"> {\n type: \"date\";\n defaultValue?: string; // ISO date string\n min?: string; // ISO date\n max?: string; // ISO date\n}\n\n/**\n * Time picker field.\n */\nexport interface TimeFormField extends Omit<BaseFormField, \"type\"> {\n type: \"time\";\n defaultValue?: string; // HH:mm format\n}\n\n/**\n * Hidden field.\n */\nexport interface HiddenFormField extends Omit<BaseFormField, \"type\"> {\n type: \"hidden\";\n defaultValue?: string;\n}\n\n// ============================================================\n// Validation Types\n// ============================================================\n\n/**\n * Validation rules for form fields.\n */\n/**\n * Turn a union of object types into their intersection.\n *\n * The standard conditional-type idiom: distributing the union into contravariant\n * positions makes the compiler infer the intersection that satisfies all of them.\n */\ntype UnionToIntersection<U> = (\n U extends unknown ? (k: U) => void : never\n) extends (k: infer I) => void\n ? I\n : never;\n\n/**\n * Every validation key any form field type may carry, each optional.\n *\n * For surfaces that read validation ACROSS field types rather than inside one —\n * a shared rules editor cannot narrow to `TextFormField` without becoming the\n * per-type branching it exists to remove.\n *\n * DERIVED from the field union rather than restated. A field type that declares\n * a new rule becomes readable here with nobody remembering to add it, and a rule\n * removed there stops compiling here. A hand-kept list would agree on the day it\n * was written and drift after, silently, because both would look correct.\n */\nexport type AnyFieldValidation = Partial<\n UnionToIntersection<NonNullable<FormField[\"validation\"]>>\n>;\n\nexport interface ValidationRules {\n /** Custom error message */\n errorMessage?: string;\n\n /** Custom validation function */\n validate?: (\n value: unknown,\n formData: Record<string, unknown>\n ) => boolean | string | Promise<boolean | string>;\n}\n\n// ============================================================\n// Conditional Logic Types\n// ============================================================\n\n/**\n * Conditional logic configuration.\n */\nexport interface ConditionalLogic {\n /** Whether conditional logic is enabled */\n enabled: boolean;\n\n /** Action to perform when conditions are met */\n action: \"show\" | \"hide\";\n\n /** Logical operator for multiple conditions */\n operator: \"AND\" | \"OR\";\n\n /** Conditions to evaluate */\n conditions: ConditionalLogicCondition[];\n}\n\n/**\n * A single conditional logic condition.\n */\nexport interface ConditionalLogicCondition {\n /** Field name to check */\n field: string;\n\n /** Comparison operator */\n comparison:\n | \"equals\"\n | \"notEquals\"\n | \"contains\"\n | \"isEmpty\"\n | \"isNotEmpty\"\n | \"greaterThan\"\n | \"lessThan\";\n\n /** Value to compare against (not used for isEmpty/isNotEmpty) */\n value?: unknown;\n}\n\n// ============================================================\n// Form Settings Types\n// ============================================================\n\n/**\n * Form settings and behavior.\n */\nexport interface FormSettings {\n /** Submit button text */\n submitButtonText?: string;\n\n /**\n * Confirmation type after successful submission.\n * - \"message\": Display a success message\n * - \"redirect\": Redirect to a URL\n * - \"relationship\": Redirect to a linked document (requires\n * `redirectRelationships` in the plugin options)\n *\n * @default \"message\"\n */\n confirmationType?: \"message\" | \"redirect\" | \"relationship\";\n\n /**\n * Linked document for relationship-based redirects (the collection's\n * `redirectPage` relationship value). Preserved verbatim — resolution\n * happens where the relationship is rendered.\n */\n redirectPage?: unknown;\n\n /** Success message (supports HTML) - used when confirmationType is \"message\" */\n successMessage?: string;\n\n /** Redirect URL after successful submission - used when confirmationType is \"redirect\" */\n redirectUrl?: string;\n\n /**\n * Relationship to redirect document - used when confirmationType is \"redirect\"\n * and redirectRelationships are configured in plugin options.\n */\n redirectRelation?: {\n relationTo: string;\n value: string; // Document ID\n };\n\n /** Allow multiple submissions from same IP (default true) */\n allowMultipleSubmissions?: boolean;\n\n /**\n * Per-form honeypot override. Unset inherits the plugin's\n * `spamProtection.honeypot`; the form wins where set.\n */\n honeypotEnabled?: boolean;\n\n /**\n * Per-form reCAPTCHA override. Unset inherits the plugin's\n * `spamProtection.recaptcha.enabled`; the form wins where set.\n */\n captchaEnabled?: boolean;\n\n /** reCAPTCHA site key (client-facing) when captcha is enabled per-form */\n captchaSiteKey?: string;\n}\n\n// ============================================================\n// Notification Types\n// ============================================================\n\n/**\n * Email notification configuration for a form.\n */\nexport interface FormNotifications {\n /** Enable email notifications */\n enabled: boolean;\n\n /** Recipient email addresses */\n recipients: Array<{\n email: string;\n name?: string;\n }>;\n\n /** Email subject line */\n subject?: string;\n\n /** Use submitter's email as reply-to */\n replyTo?: boolean;\n\n /** Custom email template */\n template?: string; // HTML template\n}\n\n// ============================================================\n// Access Control Types\n// ============================================================\n\n/**\n * Form access control.\n */\nexport interface FormAccess {\n /** Who can submit the form */\n submit?: (context: { req: RequestContext }) => boolean | Promise<boolean>;\n\n /** Who can view submissions */\n read?: (context: { req: RequestContext }) => boolean | Promise<boolean>;\n}\n\n// ============================================================\n// Hook Types\n// ============================================================\n\n/**\n * Form lifecycle hooks.\n */\nexport interface FormHooks {\n /** Before submission validation */\n beforeValidate?: Array<(context: FormHookContext) => void | Promise<void>>;\n\n /** Before submission is saved */\n beforeSubmit?: Array<(context: FormHookContext) => void | Promise<void>>;\n\n /** After submission is saved */\n afterSubmit?: Array<(context: FormHookContext) => void | Promise<void>>;\n}\n\n/**\n * Hook context for form hooks.\n */\nexport interface FormHookContext {\n /** Form configuration */\n form: FormConfig;\n\n /** Submission data */\n data: Record<string, unknown>;\n\n /** Request context */\n req: RequestContext;\n\n /** Submission metadata */\n metadata?: {\n ipAddress?: string;\n userAgent?: string;\n };\n}\n\n// ============================================================\n// Submission Types\n// ============================================================\n\n/**\n * Form submission data structure.\n */\nexport interface FormSubmission {\n /** Submission ID */\n id: string;\n\n /** Form slug */\n formSlug: string;\n\n /** Submission data (JSON) */\n data: Record<string, unknown>;\n\n /**\n * Submission status.\n *\n * `spam` is one of them, not a separate flag: the stored field offers it, the\n * admin has a Spam tab and filters its other views with `not_equals: \"spam\"`,\n * and marking something \"Not spam\" moves it back to `new`.\n */\n status: \"new\" | \"read\" | \"archived\" | \"spam\";\n\n /** Which spam check flagged this, when one did. */\n spamReason?: string | null;\n\n /** Internal notes (admin only) */\n notes?: string;\n\n /** Metadata */\n ipAddress?: string;\n userAgent?: string;\n submittedAt: Date;\n\n /** Created/updated timestamps */\n createdAt: Date;\n updatedAt: Date;\n}\n\n// ============================================================\n// Resolved Configuration Types (Internal)\n// ============================================================\n\n/**\n * Resolved form builder configuration after applying defaults.\n * Used internally by the plugin.\n */\nexport interface ResolvedFormBuilderConfig {\n formOverrides: ResolvedCollectionOverrides;\n\n formSubmissionOverrides: ResolvedCollectionOverrides;\n\n fields: FormFieldToggles;\n\n /**\n * The collection-to-URL map, with the array shorthand already expanded.\n *\n * ONE shape, so nothing downstream branches on which spelling was written.\n * The list of redirect collections is `Object.keys` of this rather than a\n * second field beside it, which would agree with it today and drift later.\n */\n redirectRelationships: RedirectRelationships;\n\n /**\n * Whether each redirect target collection is localized, keyed by slug.\n *\n * Resolved at `init` from the collection's own registered metadata rather\n * than asked for in options, because the collection already declares it and\n * a second declaration drifts.\n *\n * It decides whether a target's main-row `status` answers for the document:\n * a localized collection publishes on a locale companion, so the row reads\n * `draft` while a translation is public. A slug missing here is a collection\n * whose metadata could not be read — undefined, not false, because reading\n * an unknown as \"not localized\" is what marks a reachable translation as a\n * draft.\n */\n redirectTargetLocalization: Record<string, boolean>;\n\n beforeEmail?: FormBuilderPluginOptions[\"beforeEmail\"];\n\n notifications: {\n defaultFrom?: string;\n defaultToEmail?: string;\n enabled: boolean;\n };\n\n spamProtection: {\n honeypot: boolean;\n recaptcha?: {\n enabled: boolean;\n siteKey?: string;\n secretKey?: string;\n scoreThreshold?: number;\n };\n rateLimit?: {\n maxSubmissions: number;\n windowMs: number;\n };\n };\n\n uploads: {\n maxFileSize: number;\n allowedMimeTypes: string[];\n uploadCollection: string;\n };\n\n features: {\n builder: boolean;\n conditionalLogic: boolean;\n fileUploads: boolean;\n };\n}\n\n// ============================================================\n// Type Guards\n// ============================================================\n\n/**\n * Check if a value is a FormField.\n */\nexport function isFormField(value: unknown): value is FormField {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"type\" in value &&\n \"name\" in value &&\n \"label\" in value\n );\n}\n\n/**\n * Check if a FormField is a specific type.\n */\nexport function isFormFieldType<T extends FormField>(\n field: FormField,\n type: T[\"type\"]\n): field is T {\n return field.type === type;\n}\n\n/**\n * Type guard for text field.\n */\nexport function isTextFormField(field: FormField): field is TextFormField {\n return field.type === \"text\";\n}\n\n/**\n * Type guard for email field.\n */\nexport function isEmailFormField(field: FormField): field is EmailFormField {\n return field.type === \"email\";\n}\n\n/**\n * Type guard for select field.\n */\nexport function isSelectFormField(field: FormField): field is SelectFormField {\n return field.type === \"select\";\n}\n\n/**\n * Type guard for checkbox field.\n */\nexport function isCheckboxFormField(\n field: FormField\n): field is CheckboxFormField {\n return field.type === \"checkbox\";\n}\n\n/**\n * Type guard for file field.\n */\nexport function isFileFormField(field: FormField): field is FileFormField {\n return field.type === \"file\";\n}\n","/**\n * Options the plugin has removed, and the one place that says so when a form\n * config still sets one.\n *\n * Both public ways into a code-defined form, `applyFormDefaults` (through\n * `createFormConfig`) and `validateFormConfig`, call this, so a config is\n * told whichever it goes through. A JavaScript config, or one written before\n * the removal, gets no type error, and without this would lose what it set\n * without a word.\n *\n * @module config/removed-options\n */\n\nimport type { FormConfig } from \"../types\";\n\n/** Forms already told, so a config passing through both entry points hears it once. */\nconst warned = new Set<string>();\n\n/**\n * Say so when a form config still sets `notifications.webhooks`.\n *\n * The plugin sends no webhooks of its own: a submission reaches other systems\n * through Nextly's `form.submission.created` webhook, so the option sends\n * nothing and the config is told what to use instead.\n */\nexport function warnIfWebhooksConfigured(config: FormConfig): void {\n const notifications: unknown = config.notifications;\n if (typeof notifications !== \"object\" || notifications === null) return;\n if (!(\"webhooks\" in notifications) || warned.has(config.slug)) return;\n warned.add(config.slug);\n console.warn(\n `[form-builder] Form \"${config.slug}\" sets notifications.webhooks, which the Form Builder no longer reads, so those webhooks are not sent. Subscribe an endpoint to Nextly's form.submission.created webhook instead: https://nextlyhq.com/docs/plugins/form-builder#webhooks`\n );\n}\n","/**\n * Form Configuration Defaults\n *\n * Applies default values to form configurations, ensuring all required\n * properties have sensible defaults while preserving user-specified values.\n *\n * @module config/defaults\n * @since 1.0.0\n *\n * @example\n * ```typescript\n * import { applyFormDefaults } from '@nextly/plugin-form-builder';\n *\n * const config = {\n * slug: 'contact-form',\n * fields: [\n * { type: 'text', name: 'name', label: 'Name' },\n * { type: 'email', name: 'email', label: 'Email' },\n * ],\n * };\n *\n * const configWithDefaults = applyFormDefaults(config);\n * // configWithDefaults now has labels, settings, etc. with defaults applied\n * ```\n */\n\nimport type { FormConfig, FormSettings, FormNotifications } from \"../types\";\n\nimport { warnIfWebhooksConfigured } from \"./removed-options\";\n\n// ============================================================\n// Default Values\n// ============================================================\n\n/**\n * Default form settings.\n */\nexport const DEFAULT_FORM_SETTINGS: Required<\n Pick<\n FormSettings,\n | \"submitButtonText\"\n | \"confirmationType\"\n | \"successMessage\"\n | \"allowMultipleSubmissions\"\n >\n> = {\n submitButtonText: \"Submit\",\n confirmationType: \"message\",\n successMessage: \"Thank you for your submission!\",\n allowMultipleSubmissions: true,\n};\n\n/**\n * Default notification settings.\n */\nexport const DEFAULT_NOTIFICATION_SETTINGS: Pick<\n FormNotifications,\n \"enabled\" | \"replyTo\"\n> = {\n enabled: true,\n replyTo: true,\n};\n\n// ============================================================\n// Helper Functions\n// ============================================================\n\n/**\n * Converts a slug to title case.\n *\n * @param str - The slug string to convert\n * @returns Title case string\n *\n * @example\n * ```typescript\n * toTitleCase('contact-form'); // 'Contact Form'\n * toTitleCase('newsletter_signup'); // 'Newsletter Signup'\n * ```\n */\nexport function toTitleCase(str: string): string {\n return str.replace(/[-_]/g, \" \").replace(/\\b\\w/g, char => char.toUpperCase());\n}\n\n/**\n * Generates a plural form from a singular label.\n * Handles common English pluralization rules.\n *\n * @param singular - The singular form\n * @returns The plural form\n *\n * @example\n * ```typescript\n * pluralize('Form'); // 'Forms'\n * pluralize('Category'); // 'Categories'\n * pluralize('Quiz'); // 'Quizzes'\n * ```\n */\nexport function pluralize(singular: string): string {\n if (singular.endsWith(\"y\") && !/[aeiou]y$/i.test(singular)) {\n return singular.slice(0, -1) + \"ies\";\n }\n if (\n singular.endsWith(\"s\") ||\n singular.endsWith(\"x\") ||\n singular.endsWith(\"z\") ||\n singular.endsWith(\"ch\") ||\n singular.endsWith(\"sh\")\n ) {\n return singular + \"es\";\n }\n return singular + \"s\";\n}\n\n// ============================================================\n// Main Default Application Function\n// ============================================================\n\n/**\n * Apply default values to form configuration.\n *\n * This function takes a partial form configuration and returns a complete\n * configuration with all default values applied. User-specified values\n * are preserved and take precedence over defaults.\n *\n * @param config - Form configuration (may be partial)\n * @returns Configuration with defaults applied\n *\n * @example\n * ```typescript\n * import { applyFormDefaults } from '@nextly/plugin-form-builder';\n *\n * const config = {\n * slug: 'contact-form',\n * fields: [\n * { type: 'text', name: 'name', label: 'Name' },\n * { type: 'email', name: 'email', label: 'Email' },\n * ],\n * };\n *\n * const result = applyFormDefaults(config);\n *\n * // Result:\n * // {\n * // slug: 'contact-form',\n * // labels: { singular: 'Contact Form', plural: 'Contact Forms' },\n * // fields: [...],\n * // settings: {\n * // submitButtonText: 'Submit',\n * // confirmationType: 'message',\n * // successMessage: 'Thank you for your submission!',\n * // allowMultipleSubmissions: true,\n * // },\n * // access: {\n * // submit: () => true,\n * // read: ({ req }) => !!req.user,\n * // },\n * // }\n * ```\n */\nexport function applyFormDefaults(config: FormConfig): FormConfig {\n warnIfWebhooksConfigured(config);\n\n // Generate singular label from slug if not provided\n const singularLabel = config.labels?.singular || toTitleCase(config.slug);\n\n return {\n ...config,\n\n // Generate labels from slug if not provided\n labels: {\n singular: singularLabel,\n plural: config.labels?.plural || pluralize(singularLabel),\n },\n\n // Apply settings defaults\n settings: {\n submitButtonText:\n config.settings?.submitButtonText ||\n DEFAULT_FORM_SETTINGS.submitButtonText,\n confirmationType:\n config.settings?.confirmationType ||\n DEFAULT_FORM_SETTINGS.confirmationType,\n successMessage:\n config.settings?.successMessage || DEFAULT_FORM_SETTINGS.successMessage,\n allowMultipleSubmissions:\n config.settings?.allowMultipleSubmissions ??\n DEFAULT_FORM_SETTINGS.allowMultipleSubmissions,\n // Preserve other settings that don't have defaults\n redirectUrl: config.settings?.redirectUrl,\n redirectRelation: config.settings?.redirectRelation,\n honeypotEnabled: config.settings?.honeypotEnabled,\n captchaEnabled: config.settings?.captchaEnabled,\n captchaSiteKey: config.settings?.captchaSiteKey,\n },\n\n // Apply notification defaults if notifications are configured\n notifications: config.notifications\n ? {\n enabled:\n config.notifications.enabled ??\n DEFAULT_NOTIFICATION_SETTINGS.enabled,\n recipients: config.notifications.recipients || [],\n subject:\n config.notifications.subject ||\n `New submission for ${singularLabel}`,\n replyTo:\n config.notifications.replyTo ??\n DEFAULT_NOTIFICATION_SETTINGS.replyTo,\n template: config.notifications.template,\n }\n : undefined,\n\n // Apply access defaults\n access: {\n // Default: anyone can submit the form\n submit: config.access?.submit || (() => true),\n // Default: only authenticated users can view submissions\n read: config.access?.read || (({ req }) => !!req.user),\n },\n };\n}\n\n/**\n * Apply default values to individual form field.\n *\n * This is a utility function for applying defaults to a single field,\n * useful when processing fields individually (e.g., in the Schema Builder).\n *\n * @param field - The field to apply defaults to\n * @returns Field with defaults applied\n *\n * @example\n * ```typescript\n * import { applyFieldDefaults } from '@nextly/plugin-form-builder';\n *\n * const field = { type: 'text', name: 'firstName' };\n * const fieldWithDefaults = applyFieldDefaults(field);\n * // { type: 'text', name: 'firstName', label: 'First Name', required: false }\n * ```\n */\nexport function applyFieldDefaults<\n T extends { type: string; name?: string; label?: string; required?: boolean },\n>(field: T): T {\n // Generate label from name if not provided\n const label =\n field.label ||\n (field.name\n ? toTitleCase(field.name.replace(/([A-Z])/g, \" $1\").trim())\n : \"\");\n\n return {\n ...field,\n label,\n required: field.required ?? false,\n };\n}\n\n/**\n * Creates a complete form config with minimal input.\n *\n * This is a convenience function for quickly creating form configs\n * with sensible defaults. Only the slug and fields are required.\n *\n * @param slug - The form slug\n * @param fields - Array of form fields\n * @param options - Optional additional configuration\n * @returns Complete form configuration\n *\n * @example\n * ```typescript\n * import { createFormConfig } from '@nextly/plugin-form-builder';\n *\n * const config = createFormConfig('contact', [\n * { type: 'text', name: 'name', label: 'Name', required: true },\n * { type: 'email', name: 'email', label: 'Email', required: true },\n * { type: 'textarea', name: 'message', label: 'Message' },\n * ]);\n * ```\n */\nexport function createFormConfig(\n slug: string,\n fields: FormConfig[\"fields\"],\n options?: Partial<Omit<FormConfig, \"slug\" | \"fields\">>\n): FormConfig {\n return applyFormDefaults({\n slug,\n fields,\n ...options,\n });\n}\n","/**\n * Conditional Logic Evaluator\n *\n * Evaluates conditional logic rules to determine field visibility in forms.\n * Used by the FormRenderer component to show/hide fields based on user input.\n *\n * @module utils/evaluate-conditions\n * @since 1.0.0\n *\n * @example\n * ```typescript\n * import { evaluateConditions } from '@nextly/plugin-form-builder';\n *\n * const conditionalLogic = {\n * enabled: true,\n * action: 'show',\n * operator: 'AND',\n * conditions: [\n * { field: 'country', comparison: 'equals', value: 'US' },\n * { field: 'age', comparison: 'greaterThan', value: 18 },\n * ],\n * };\n *\n * const formData = { country: 'US', age: 25 };\n * const isVisible = evaluateConditions(conditionalLogic, formData);\n * // isVisible = true (all conditions met, action is 'show')\n * ```\n */\n\nimport type { ConditionalLogic, ConditionalLogicCondition } from \"../types\";\n\n// ============================================================\n// Types\n// ============================================================\n\n/**\n * Comparison operators supported by the conditional logic system.\n */\nexport type ComparisonOperator =\n | \"equals\"\n | \"notEquals\"\n | \"contains\"\n | \"isEmpty\"\n | \"isNotEmpty\"\n | \"greaterThan\"\n | \"lessThan\";\n\n// ============================================================\n// Internal Helper Functions\n// ============================================================\n\n/**\n * Checks if a value is considered \"empty\".\n * Empty values: undefined, null, empty string, empty array.\n */\nfunction isEmptyValue(value: unknown): boolean {\n if (value === undefined || value === null) {\n return true;\n }\n if (typeof value === \"string\" && value.trim() === \"\") {\n return true;\n }\n if (Array.isArray(value) && value.length === 0) {\n return true;\n }\n return false;\n}\n\n/**\n * Converts a value to a number for numeric comparisons.\n * Returns NaN if conversion is not possible.\n */\nfunction toNumber(value: unknown): number {\n if (typeof value === \"number\") {\n return value;\n }\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (trimmed === \"\") {\n return NaN;\n }\n return Number(trimmed);\n }\n return NaN;\n}\n\n/**\n * Converts a value to a string for string comparisons.\n */\nfunction toString(value: unknown): string {\n if (value === undefined || value === null) {\n return \"\";\n }\n if (typeof value === \"string\") {\n return value;\n }\n if (Array.isArray(value)) {\n return value.join(\", \");\n }\n if (typeof value === \"object\") {\n return JSON.stringify(value);\n }\n // Primitive (number, boolean, bigint, symbol) — safe to coerce.\n // eslint-disable-next-line @typescript-eslint/no-base-to-string -- value narrowed to primitive above; rule doesn't follow control flow on unknown\n return String(value);\n}\n\n/**\n * Compares two values using the specified operator.\n *\n * @param operator - The comparison operator to use\n * @param fieldValue - The actual value from the form data\n * @param conditionValue - The expected value from the condition\n * @returns Whether the comparison is true\n */\nfunction compareValues(\n operator: ComparisonOperator,\n fieldValue: unknown,\n conditionValue: unknown\n): boolean {\n switch (operator) {\n case \"equals\": {\n // Strict equality for most cases\n // Handle array values (e.g., checkbox groups, multi-selects)\n if (Array.isArray(fieldValue)) {\n // If conditionValue is also an array, check for deep equality\n if (Array.isArray(conditionValue)) {\n if (fieldValue.length !== conditionValue.length) {\n return false;\n }\n return fieldValue.every((v, i) => v === conditionValue[i]);\n }\n // Check if the array contains the condition value\n return fieldValue.includes(conditionValue);\n }\n // Standard equality - string comparison for flexibility\n return toString(fieldValue) === toString(conditionValue);\n }\n\n case \"notEquals\": {\n // Inverse of equals\n if (Array.isArray(fieldValue)) {\n if (Array.isArray(conditionValue)) {\n if (fieldValue.length !== conditionValue.length) {\n return true;\n }\n return !fieldValue.every((v, i) => v === conditionValue[i]);\n }\n return !fieldValue.includes(conditionValue);\n }\n return toString(fieldValue) !== toString(conditionValue);\n }\n\n case \"contains\": {\n // Case-insensitive substring check\n const fieldStr = toString(fieldValue).toLowerCase();\n const conditionStr = toString(conditionValue).toLowerCase();\n\n // Handle array values - check if any element contains the substring\n if (Array.isArray(fieldValue)) {\n return fieldValue.some(v =>\n toString(v).toLowerCase().includes(conditionStr)\n );\n }\n\n return fieldStr.includes(conditionStr);\n }\n\n case \"isEmpty\": {\n // Check if the field value is empty\n // conditionValue is ignored for this operator\n return isEmptyValue(fieldValue);\n }\n\n case \"isNotEmpty\": {\n // Check if the field value is not empty\n // conditionValue is ignored for this operator\n return !isEmptyValue(fieldValue);\n }\n\n case \"greaterThan\": {\n // Numeric comparison with loose coercion\n const fieldNum = toNumber(fieldValue);\n const conditionNum = toNumber(conditionValue);\n\n // If both can be converted to numbers, use numeric comparison\n if (!Number.isNaN(fieldNum) && !Number.isNaN(conditionNum)) {\n return fieldNum > conditionNum;\n }\n\n // Fall back to string comparison for non-numeric values\n return toString(fieldValue) > toString(conditionValue);\n }\n\n case \"lessThan\": {\n // Numeric comparison with loose coercion\n const fieldNum = toNumber(fieldValue);\n const conditionNum = toNumber(conditionValue);\n\n // If both can be converted to numbers, use numeric comparison\n if (!Number.isNaN(fieldNum) && !Number.isNaN(conditionNum)) {\n return fieldNum < conditionNum;\n }\n\n // Fall back to string comparison for non-numeric values\n return toString(fieldValue) < toString(conditionValue);\n }\n\n default: {\n // Unknown operator - return false to be safe\n // `operator` is `never` here because the switch is exhaustive over ComparisonOperator;\n // cast to string so the diagnostic survives if a new operator is added without a case.\n console.warn(\n `[evaluateConditions] Unknown comparison operator: ${String(operator)}`\n );\n return false;\n }\n }\n}\n\n/**\n * Evaluates a single condition against the form data.\n *\n * Exported for consumers that gate on one condition rather than a full\n * conditional-logic block — notification send-conditions use exactly this.\n *\n * @param condition - The condition to evaluate\n * @param formData - The current form data\n * @returns Whether the condition is met\n */\nexport function evaluateSingleCondition(\n condition: ConditionalLogicCondition,\n formData: Record<string, unknown>\n): boolean {\n const { field, comparison, value: conditionValue } = condition;\n\n // Get the field value from form data\n // Support nested field access with dot notation (e.g., \"address.city\")\n const fieldValue = getNestedValue(formData, field);\n\n // Evaluate the comparison\n return compareValues(comparison, fieldValue, conditionValue);\n}\n\n/**\n * Gets a nested value from an object using dot notation.\n *\n * @param obj - The object to get the value from\n * @param path - The path to the value (e.g., \"address.city\")\n * @returns The value at the path, or undefined if not found\n */\nfunction getNestedValue(obj: Record<string, unknown>, path: string): unknown {\n if (!path) {\n return undefined;\n }\n\n // Simple case - no nesting\n if (!path.includes(\".\")) {\n return obj[path];\n }\n\n // Nested case - traverse the path\n const parts = path.split(\".\");\n let current: unknown = obj;\n\n for (const part of parts) {\n if (current === undefined || current === null) {\n return undefined;\n }\n if (typeof current !== \"object\") {\n return undefined;\n }\n current = (current as Record<string, unknown>)[part];\n }\n\n return current;\n}\n\n// ============================================================\n// Main Evaluation Function\n// ============================================================\n\n/**\n * Evaluates conditional logic to determine field visibility.\n *\n * This is the main entry point for conditional logic evaluation.\n * It evaluates all conditions according to the logical operator (AND/OR)\n * and returns whether the field should be visible based on the action\n * (show/hide).\n *\n * @param logic - The conditional logic configuration\n * @param formData - The current form data (field values)\n * @returns Whether the field should be visible\n *\n * @example\n * ```typescript\n * // Show field when country is US AND age is over 18\n * const logic: ConditionalLogic = {\n * enabled: true,\n * action: 'show',\n * operator: 'AND',\n * conditions: [\n * { field: 'country', comparison: 'equals', value: 'US' },\n * { field: 'age', comparison: 'greaterThan', value: 18 },\n * ],\n * };\n *\n * evaluateConditions(logic, { country: 'US', age: 25 }); // true\n * evaluateConditions(logic, { country: 'US', age: 15 }); // false\n * evaluateConditions(logic, { country: 'CA', age: 25 }); // false\n *\n * // Hide field when status is \"completed\"\n * const hideLogic: ConditionalLogic = {\n * enabled: true,\n * action: 'hide',\n * operator: 'AND',\n * conditions: [\n * { field: 'status', comparison: 'equals', value: 'completed' },\n * ],\n * };\n *\n * evaluateConditions(hideLogic, { status: 'completed' }); // false (hidden)\n * evaluateConditions(hideLogic, { status: 'pending' }); // true (visible)\n * ```\n *\n * @example\n * ```typescript\n * // Show field when either condition is met (OR)\n * const orLogic: ConditionalLogic = {\n * enabled: true,\n * action: 'show',\n * operator: 'OR',\n * conditions: [\n * { field: 'role', comparison: 'equals', value: 'admin' },\n * { field: 'role', comparison: 'equals', value: 'editor' },\n * ],\n * };\n *\n * evaluateConditions(orLogic, { role: 'admin' }); // true\n * evaluateConditions(orLogic, { role: 'editor' }); // true\n * evaluateConditions(orLogic, { role: 'viewer' }); // false\n * ```\n */\nexport function evaluateConditions(\n logic: ConditionalLogic | undefined,\n formData: Record<string, unknown>\n): boolean {\n // If no logic provided or logic is disabled, field is visible by default\n if (!logic || !logic.enabled) {\n return true;\n }\n\n const { action, operator, conditions } = logic;\n\n // If no conditions, field is visible by default\n if (!conditions || conditions.length === 0) {\n return true;\n }\n\n // Evaluate all conditions\n const results = conditions.map(condition =>\n evaluateSingleCondition(condition, formData)\n );\n\n // Combine results based on logical operator\n let conditionsMet: boolean;\n\n if (operator === \"OR\") {\n // OR: At least one condition must be true\n conditionsMet = results.some(result => result);\n } else {\n // AND (default): All conditions must be true\n conditionsMet = results.every(result => result);\n }\n\n // Apply the action\n // - \"show\": field is visible when conditions are met\n // - \"hide\": field is visible when conditions are NOT met\n if (action === \"hide\") {\n return !conditionsMet;\n }\n\n // Default action is \"show\"\n return conditionsMet;\n}\n\n/**\n * Checks if a comparison operator is valid.\n *\n * @param operator - The operator to check\n * @returns Whether the operator is valid\n */\nexport function isValidComparisonOperator(\n operator: string\n): operator is ComparisonOperator {\n const validOperators: ComparisonOperator[] = [\n \"equals\",\n \"notEquals\",\n \"contains\",\n \"isEmpty\",\n \"isNotEmpty\",\n \"greaterThan\",\n \"lessThan\",\n ];\n return validOperators.includes(operator as ComparisonOperator);\n}\n\n/**\n * Gets the list of supported comparison operators.\n *\n * @returns Array of supported operator names\n */\nexport function getSupportedComparisonOperators(): ComparisonOperator[] {\n return [\n \"equals\",\n \"notEquals\",\n \"contains\",\n \"isEmpty\",\n \"isNotEmpty\",\n \"greaterThan\",\n \"lessThan\",\n ];\n}\n","/**\n * Export Formats Utilities\n *\n * Pure utility functions for exporting form submissions to CSV and JSON formats.\n * These functions generate formatted strings that can be used for file downloads\n * or API responses.\n *\n * @module utils/export-formats\n * @since 0.1.0\n */\n\nimport type { SubmissionDocument, FormDocument, AnyFormField } from \"../types\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Options for CSV export.\n */\nexport interface CSVExportOptions {\n /**\n * Whether to include metadata columns (_id, _status, _submittedAt, etc.).\n * @default true\n */\n includeMetadata?: boolean;\n\n /**\n * Delimiter character for CSV.\n * @default \",\"\n */\n delimiter?: string;\n\n /**\n * Whether to include UTF-8 BOM for Excel compatibility.\n * @default true\n */\n includeBOM?: boolean;\n\n /**\n * Date format for timestamps.\n * @default \"iso\" (ISO 8601)\n */\n dateFormat?: \"iso\" | \"locale\";\n}\n\n/**\n * Options for JSON export.\n */\nexport interface JSONExportOptions {\n /**\n * Whether to include metadata in each submission.\n * @default true\n */\n includeMetadata?: boolean;\n\n /**\n * Whether to include form definition in the export.\n * @default true\n */\n includeFormDefinition?: boolean;\n\n /**\n * Number of spaces for indentation (0 for minified).\n * @default 2\n */\n indent?: number;\n}\n\n/**\n * Exported JSON structure.\n */\nexport interface ExportedJSON {\n /** Export metadata */\n exportedAt: string;\n\n /** Form information (if includeFormDefinition is true) */\n form?: {\n id: string;\n name: string;\n slug: string;\n fields: Array<{\n name: string;\n label: string;\n type: string;\n }>;\n };\n\n /** Total number of submissions in export */\n totalSubmissions: number;\n\n /** Array of submission data */\n submissions: Array<Record<string, unknown>>;\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/** UTF-8 BOM character sequence */\nconst UTF8_BOM = \"\\uFEFF\";\n\n// ============================================================================\n// Value Formatting Helpers\n// ============================================================================\n\n/**\n * Stringify a value safely, falling back to JSON for plain objects so we\n * never emit the default `[object Object]` representation.\n */\nfunction safeString(value: unknown): string {\n if (typeof value === \"object\" && value !== null) {\n return JSON.stringify(value);\n }\n return String(value);\n}\n\n/**\n * Format a field value for export based on field type.\n *\n * @param value - The raw value from submission data\n * @param field - The field definition (optional, for type-specific formatting)\n * @returns Formatted string representation of the value\n *\n * @example\n * ```typescript\n * formatExportValue(true, { type: 'checkbox' }); // \"Yes\"\n * formatExportValue(['a', 'b'], { type: 'select' }); // \"a, b\"\n * formatExportValue(new Date(), { type: 'date' }); // \"2024-01-15\"\n * ```\n */\nexport function formatExportValue(\n value: unknown,\n field?: AnyFormField\n): string {\n // Handle null/undefined\n if (value === null || value === undefined) {\n return \"\";\n }\n\n // Type-specific formatting when field definition is available\n if (field) {\n switch (field.type) {\n case \"checkbox\":\n return value ? \"Yes\" : \"No\";\n\n case \"select\":\n if (Array.isArray(value)) {\n return value.join(\", \");\n }\n return safeString(value);\n\n case \"file\":\n if (Array.isArray(value)) {\n // File URLs or IDs\n return value.map(v => safeString(v)).join(\", \");\n }\n return safeString(value);\n\n case \"date\":\n if (value instanceof Date) {\n return value.toISOString().split(\"T\")[0];\n }\n if (typeof value === \"string\") {\n // Already a date string, return as-is or normalize\n const parsed = new Date(value);\n if (!isNaN(parsed.getTime())) {\n return parsed.toISOString().split(\"T\")[0];\n }\n }\n return safeString(value);\n\n case \"time\":\n return safeString(value);\n\n case \"hidden\":\n return safeString(value);\n\n default:\n // text, email, phone, url, textarea, number, radio\n break;\n }\n }\n\n // Generic formatting for values without field definition\n if (Array.isArray(value)) {\n return value.join(\", \");\n }\n\n if (typeof value === \"boolean\") {\n return value ? \"Yes\" : \"No\";\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n return safeString(value);\n}\n\n/**\n * Format a date for export.\n *\n * @param date - Date value (Date object or string)\n * @param format - Output format (\"iso\" or \"locale\")\n * @returns Formatted date string\n */\nfunction formatDate(\n date: Date | string | undefined,\n format: \"iso\" | \"locale\" = \"iso\"\n): string {\n if (!date) return \"\";\n\n const d = typeof date === \"string\" ? new Date(date) : date;\n\n if (isNaN(d.getTime())) {\n return \"\";\n }\n\n return format === \"iso\" ? d.toISOString() : d.toLocaleString();\n}\n\n// ============================================================================\n// CSV Export\n// ============================================================================\n\n/**\n * Escape a value for CSV format (RFC 4180 compliant).\n *\n * Rules:\n * - Fields containing comma, double-quote, or newline must be quoted\n * - Double-quotes within fields must be escaped with another double-quote\n *\n * @param value - The value to escape\n * @param delimiter - The delimiter being used\n * @returns Properly escaped CSV value\n */\nfunction escapeCSVValue(value: string, delimiter: string = \",\"): string {\n // CSV injection: spreadsheets execute cells starting with a formula\n // trigger character, and syntax quoting does NOT stop that. A leading\n // apostrophe forces text interpretation (the OWASP mitigation). Plain\n // negative numbers are exempt — they are data, not formulas.\n const isFormulaLike =\n /^[=+\\-@\\t\\r]/.test(value) && !/^-?\\d+(\\.\\d+)?$/.test(value);\n const escapedFormula = isFormulaLike ? `'${value}` : value;\n\n // Check if quoting is needed\n const needsQuoting =\n escapedFormula.includes(delimiter) ||\n escapedFormula.includes('\"') ||\n escapedFormula.includes(\"\\n\") ||\n escapedFormula.includes(\"\\r\");\n\n if (!needsQuoting) {\n return escapedFormula;\n }\n\n // Escape double quotes by doubling them\n const escaped = escapedFormula.replace(/\"/g, '\"\"');\n return `\"${escaped}\"`;\n}\n\n/**\n * Export submissions to CSV format.\n *\n * Generates an RFC 4180 compliant CSV string with:\n * - Form field values as columns (in form definition order)\n * - Optional metadata columns at the end (_id, _status, _submittedAt, etc.)\n * - Proper escaping of special characters\n * - Optional UTF-8 BOM for Excel compatibility\n *\n * @param submissions - Array of submission documents to export\n * @param form - Form document containing field definitions\n * @param options - Export options\n * @returns CSV formatted string\n *\n * @example\n * ```typescript\n * const csv = exportToCSV(submissions, form, {\n * includeMetadata: true,\n * includeBOM: true,\n * });\n *\n * // Download in browser\n * downloadFile(csv, 'submissions.csv', 'text/csv;charset=utf-8');\n * ```\n */\nexport function exportToCSV(\n submissions: SubmissionDocument[],\n form: FormDocument,\n options: CSVExportOptions = {}\n): string {\n const {\n includeMetadata = true,\n delimiter = \",\",\n includeBOM = true,\n dateFormat = \"iso\",\n } = options;\n\n const exportableFields = form.fields;\n\n // Build header row\n const headers: string[] = [];\n\n // Form field headers (using labels)\n for (const field of exportableFields) {\n headers.push(field.label);\n }\n\n // Metadata headers\n if (includeMetadata) {\n headers.push(\"ID\", \"Status\", \"Submitted At\", \"IP Address\", \"User Agent\");\n }\n\n // Build data rows\n const rows: string[][] = [];\n\n for (const submission of submissions) {\n const row: string[] = [];\n const data = submission.data;\n\n // Form field values\n for (const field of exportableFields) {\n const value = data[field.name];\n const formatted = formatExportValue(value, field);\n row.push(escapeCSVValue(formatted, delimiter));\n }\n\n // Metadata values\n if (includeMetadata) {\n row.push(escapeCSVValue(submission.id, delimiter));\n row.push(escapeCSVValue(submission.status, delimiter));\n row.push(\n escapeCSVValue(\n formatDate(submission.submittedAt, dateFormat),\n delimiter\n )\n );\n row.push(escapeCSVValue(submission.ipAddress || \"\", delimiter));\n row.push(escapeCSVValue(submission.userAgent || \"\", delimiter));\n }\n\n rows.push(row);\n }\n\n // Escape headers\n const escapedHeaders = headers.map(h => escapeCSVValue(h, delimiter));\n\n // Build CSV string\n const lines = [\n escapedHeaders.join(delimiter),\n ...rows.map(row => row.join(delimiter)),\n ];\n\n // Use CRLF line endings for maximum compatibility\n const csvContent = lines.join(\"\\r\\n\");\n\n // Prepend BOM if requested\n return includeBOM ? UTF8_BOM + csvContent : csvContent;\n}\n\n// ============================================================================\n// JSON Export\n// ============================================================================\n\n/**\n * Export submissions to JSON format.\n *\n * Generates a structured JSON export with:\n * - Export metadata (timestamp)\n * - Optional form definition\n * - Array of submissions with field values\n * - Optional submission metadata (_id, _status, etc.)\n *\n * @param submissions - Array of submission documents to export\n * @param form - Form document containing field definitions\n * @param options - Export options\n * @returns JSON formatted string\n *\n * @example\n * ```typescript\n * const json = exportToJSON(submissions, form, {\n * includeMetadata: true,\n * includeFormDefinition: true,\n * indent: 2,\n * });\n *\n * // Download in browser\n * downloadFile(json, 'submissions.json', 'application/json');\n * ```\n */\nexport function exportToJSON(\n submissions: SubmissionDocument[],\n form: FormDocument,\n options: JSONExportOptions = {}\n): string {\n const {\n includeMetadata = true,\n includeFormDefinition = true,\n indent = 2,\n } = options;\n\n const exportableFields = form.fields;\n\n // Build form definition\n const formDefinition = includeFormDefinition\n ? {\n id: form.id,\n name: form.name,\n slug: form.slug,\n fields: exportableFields.map(f => ({\n name: f.name,\n label: f.label,\n type: f.type,\n })),\n }\n : undefined;\n\n // Build submission data\n const exportedSubmissions = submissions.map(submission => {\n const data = submission.data;\n\n // Build field data object with labels as keys for readability\n const fieldData: Record<string, unknown> = {};\n\n for (const field of exportableFields) {\n const value = data[field.name];\n // Use field name as key (consistent with submission data)\n fieldData[field.name] = value;\n }\n\n // Add metadata if requested\n if (includeMetadata) {\n return {\n ...fieldData,\n _metadata: {\n id: submission.id,\n status: submission.status,\n submittedAt: submission.submittedAt,\n ipAddress: submission.ipAddress || null,\n userAgent: submission.userAgent || null,\n createdAt: submission.createdAt,\n updatedAt: submission.updatedAt,\n },\n };\n }\n\n return fieldData;\n });\n\n // Build export object\n const exportData: ExportedJSON = {\n exportedAt: new Date().toISOString(),\n totalSubmissions: submissions.length,\n submissions: exportedSubmissions,\n };\n\n // Add form definition if requested\n if (formDefinition) {\n exportData.form = formDefinition;\n }\n\n return JSON.stringify(exportData, null, indent);\n}\n\n// ============================================================================\n// Browser Download Helper\n// ============================================================================\n\n/**\n * Trigger a file download in the browser.\n *\n * Creates a Blob from the content and triggers a download using a temporary\n * anchor element. This function is browser-only and will throw in Node.js.\n *\n * @param content - File content as string\n * @param filename - Name for the downloaded file\n * @param mimeType - MIME type of the file\n *\n * @example\n * ```typescript\n * const csv = exportToCSV(submissions, form);\n * downloadFile(csv, 'contact-form-submissions.csv', 'text/csv;charset=utf-8');\n *\n * const json = exportToJSON(submissions, form);\n * downloadFile(json, 'submissions.json', 'application/json');\n * ```\n */\nexport function downloadFile(\n content: string,\n filename: string,\n mimeType: string\n): void {\n // Check for browser environment\n if (typeof window === \"undefined\" || typeof document === \"undefined\") {\n throw new Error(\"downloadFile is only available in browser environments\");\n }\n\n // Create blob\n const blob = new Blob([content], { type: mimeType });\n\n // Create download URL\n const url = URL.createObjectURL(blob);\n\n // Create temporary anchor element\n const link = document.createElement(\"a\");\n link.href = url;\n link.download = filename;\n\n // Trigger download\n document.body.appendChild(link);\n link.click();\n\n // Cleanup\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n}\n\n/**\n * Generate a filename for export.\n *\n * Creates a filename with form slug and current timestamp.\n *\n * @param formSlug - The form's slug\n * @param format - Export format ('csv' or 'json')\n * @returns Generated filename\n *\n * @example\n * ```typescript\n * generateExportFilename('contact-form', 'csv');\n * // Returns: \"contact-form-submissions-2024-01-15.csv\"\n * ```\n */\nexport function generateExportFilename(\n formSlug: string,\n format: \"csv\" | \"json\"\n): string {\n // The name ends up inside a Content-Disposition header — restrict it to\n // characters that can never break the header value or the filesystem.\n const safeSlug =\n formSlug.toLowerCase().replace(/[^a-z0-9_-]+/g, \"-\") || \"form\";\n const date = new Date().toISOString().split(\"T\")[0];\n return `${safeSlug}-submissions-${date}.${format}`;\n}\n\n// ============================================================================\n// Convenience Export Functions\n// ============================================================================\n\n/**\n * Export submissions and trigger browser download.\n *\n * Convenience function that combines export and download in one call.\n * Browser-only.\n *\n * @param submissions - Array of submission documents\n * @param form - Form document\n * @param format - Export format ('csv' or 'json')\n * @param options - Format-specific options\n *\n * @example\n * ```typescript\n * // In a React component\n * const handleExport = (format: 'csv' | 'json') => {\n * exportAndDownload(submissions, form, format);\n * };\n * ```\n */\nexport function exportAndDownload(\n submissions: SubmissionDocument[],\n form: FormDocument,\n format: \"csv\" | \"json\",\n options?: CSVExportOptions | JSONExportOptions\n): void {\n const filename = generateExportFilename(form.slug, format);\n\n if (format === \"csv\") {\n const content = exportToCSV(submissions, form, options);\n downloadFile(content, filename, \"text/csv;charset=utf-8\");\n } else {\n const content = exportToJSON(submissions, form, options);\n downloadFile(content, filename, \"application/json\");\n }\n}\n","/**\n * Reading the document a form's redirect setting points at.\n *\n * Separate from `redirect-target` because this half has no dependencies and\n * the other half imports `nextly/config`: the admin picker needs to read a\n * stored reference to show what is selected, and pulling the config surface\n * into the browser bundle to do it would be a real cost for no gain.\n */\n\n/** A document being turned into a URL. `id` is always present; the rest is the row. */\nexport interface RedirectTargetDocument {\n id: string;\n [field: string]: unknown;\n}\n\n/** A stored relationship value reduced to the collection and id it names. */\nexport interface RedirectReference {\n collection: string;\n id: string;\n}\n\n/**\n * The reference a `redirectPage` value holds, in any shape a read produces.\n *\n * Three arrive in practice and they are not interchangeable: a polymorphic\n * field stores `{ relationTo, value }`; a read at depth replaces `value` with\n * the populated row; and a single-target field stores the bare id. The last\n * names no collection, so it resolves only when exactly one is configured —\n * guessing which of several a bare id belongs to would send visitors to a\n * URL built from the wrong collection's pattern.\n */\nexport function parseRedirectReference(\n value: unknown,\n collections: readonly string[]\n): RedirectReference | null {\n if (value == null) return null;\n\n if (typeof value === \"string\") {\n // Trimmed, as the object branch trims its collection name. A cleared or\n // whitespace-only `redirectPage` reads as a reference otherwise, which\n // validation accepts as a usable target while the collection hook refuses\n // it as missing and the submit path resolves to nothing.\n const id = value.trim();\n return id && collections.length === 1\n ? { collection: collections[0], id }\n : null;\n }\n\n if (typeof value !== \"object\" || Array.isArray(value)) return null;\n\n const record = value as Record<string, unknown>;\n const relationTo = record.relationTo;\n const inner = record.value;\n\n if (typeof relationTo === \"string\" && relationTo.trim()) {\n // Trimmed for the TEST and in the VALUE returned. A blank or whitespace\n // name is not a collection, and a padded one is not the same string as the\n // configured key — accepting `\" pages \"` while returning it unchanged\n // makes this read valid to the public validator and unusable to every\n // consumer that looks the name up.\n const id = referenceId(inner);\n return id ? { collection: relationTo.trim(), id } : null;\n }\n\n // A populated row with no `relationTo` beside it: same ambiguity as a bare\n // id, and the same answer.\n const id = referenceId(record);\n if (!id) return null;\n return collections.length === 1 ? { collection: collections[0], id } : null;\n}\n\nfunction referenceId(value: unknown): string | null {\n if (typeof value === \"string\") return value.trim() || null;\n if (value !== null && typeof value === \"object\" && !Array.isArray(value)) {\n const { id } = value as Record<string, unknown>;\n if (typeof id === \"string\" && id) return id;\n }\n return null;\n}\n\n/**\n * The field the publish lifecycle puts on every row it manages.\n *\n * A collection without the lifecycle may legally define an ordinary field\n * named `status` — \"active\", \"archived\", anything — so the NAME `status` does\n * not identify a lifecycle and reading it as one marks live documents as\n * drafts. This field does identify it: it is written by the framework, not by\n * a schema author.\n *\n * Measured on 2026-08-25, creating one document in each kind of collection:\n * a `status: true` collection returns\n * `[createdAt, firstPublishedAt, id, slug, status, title, updatedAt]`, and a\n * plain collection carrying its own `status` text field returns the same list\n * WITHOUT `firstPublishedAt`. It is present on a never-published draft too, so\n * presence marks the lifecycle rather than the act of publishing.\n */\nconst PUBLISH_LIFECYCLE_FIELD = \"firstPublishedAt\";\n\n/**\n * Whether this document comes from a collection with the publish lifecycle.\n *\n * Callers that project their reads must request\n * {@link PUBLISH_LIFECYCLE_FIELD}; a projection that omits it makes every\n * document look unmanaged, which reads as \"always reachable\".\n */\nexport function hasPublishLifecycle(document: RedirectTargetDocument): boolean {\n return PUBLISH_LIFECYCLE_FIELD in document;\n}\n\n/**\n * How reachable a document is, as one of three answers.\n *\n * Three rather than a boolean because one of the cases is genuinely\n * undecidable from a redirect target's row, and collapsing it into either\n * answer is wrong in a different direction.\n */\nexport type Reachability = \"reachable\" | \"unreachable\" | \"unknown\";\n\n/**\n * Whether a visitor can reach this document.\n *\n * `collectionIsLocalized` is the caller's knowledge of the target collection's\n * `localized` setting, or `undefined` where the caller has no way to know.\n *\n * A localized collection publishes PER LOCALE, on a companion row that no read\n * available here returns. Measured on 2026-08-25 against a collection with\n * `localized: true` whose Spanish translation was published while the default\n * locale stayed a draft: `findByID` answers `status: \"draft\"` at the default\n * locale AND at `es`, and a `find` filtered to `status: \"published\"` returns\n * the document at no locale at all — no locale, `en` or `es`. So the main\n * row's `status` does not answer for a localized document, and reading it as\n * an answer marks a page visitors can reach as a draft, refuses a correct save\n * and drops a working redirect.\n *\n * `firstPublishedAt` is the one fact that survives that: it records whether the\n * document has ever been public in ANY language, so its absence is decisive\n * whether or not the collection is localized.\n */\nexport function documentReachability(\n document: RedirectTargetDocument,\n collectionIsLocalized: boolean | undefined\n): Reachability {\n // No lifecycle at all: there is no unpublished state to be in.\n if (!hasPublishLifecycle(document)) return \"reachable\";\n if (document.status === \"published\") return \"reachable\";\n\n // Only a caller that knows the collection is NOT localized can read the main\n // row's status as the document's own. There is no fact on the row that\n // substitutes for that knowledge, and each candidate is wrong in a real\n // configuration:\n //\n // `status` — a collection without the lifecycle may define its\n // own field of that name, so the NAME answers\n // nothing (`hasPublishLifecycle` above).\n // the row's status — a localized document publishes on a locale\n // companion, and the main row reads `draft` while a\n // translation is public.\n // `firstPublishedAt` — absent on a row published before that column\n // existed, so it records \"never public\" and \"public,\n // never recorded\" identically on an upgraded\n // database.\n //\n // So the undecided case is reported rather than guessed at.\n if (collectionIsLocalized === false) return \"unreachable\";\n\n return \"unknown\";\n}\n/** The settings keys that can carry the document a form redirects to. */\nexport type PickedDocumentField = \"redirectPage\" | \"redirectRelation\";\n\n/**\n * Which settings key names the document this form redirects to, if any.\n *\n * TWO options carry one. \"Redirect to a page\" stores `redirectPage`, and the\n * URL option falls back to `redirectRelation` when no URL is typed — the shape\n * code-first and legacy forms use, and one the submission path has always\n * resolved. One definition of the question, so the rule that guards a target\n * cannot recognise fewer shapes than the resolver that acts on it: a form the\n * rule skips is a form the resolver still redirects.\n *\n * The two are not equally strict about being EMPTY, deliberately. Naming\n * nothing under \"Redirect to a page\" contradicts the option itself, so it is\n * reported. Under the URL option an absent relation only means no destination\n * is configured yet — `validateFormConfig` already governs that, and refusing\n * it here would reject saves this rule never governed.\n */\nexport function pickedDocumentField(\n settings: Record<string, unknown>\n): PickedDocumentField | null {\n if (settings.confirmationType === \"relationship\") return \"redirectPage\";\n if (\n settings.confirmationType === \"redirect\" &&\n !settings.redirectUrl &&\n settings.redirectRelation != null\n ) {\n return \"redirectRelation\";\n }\n return null;\n}\n","/**\n * Which validation rules this runtime actually enforces, per field type.\n *\n * Core's `validationRulesForFieldType` answers what a type MEANS — what a field\n * of that type could sensibly be bounded by. This answers a narrower and quite\n * different question: which of those bounds the form's schema generator reads\n * back and applies. A rule a surface offers but the runtime ignores is worse\n * than no control at all, because the author configures a restriction, believes\n * the form is guarded, and submissions violating it still pass.\n *\n * **The answer is per type, and cannot be collapsed into one list.** A single\n * set of \"rules this runtime enforces\" only says a rule is honoured for SOME\n * type. Intersected with a type's vocabulary it then offers, for example, `min`\n * and `max` on a date — because the number schema honours them — while the date\n * schema reads its bounds from `field.min`/`field.max` and never looks at\n * `validation`. The control stores a bound nothing reads. The same collapse\n * offers length limits on email, a pattern on textarea, and all three on time\n * and hidden.\n *\n * Each row below is what its generator in `generate-schema.ts` reads, and the\n * test beside this file asserts every entry CHANGES what the generated schema\n * accepts for that specific type — so a row that stops being true fails rather\n * than quietly becoming an inert control.\n *\n * Kept free of `zod` and of the generator itself on purpose. The field editor\n * needs this metadata to decide what to draw, and importing it from the module\n * that builds schemas would ship the whole schema implementation and the\n * validation library to every admin client, before anyone opens a field.\n *\n * @module utils/enforced-validation\n */\n\nimport type { FieldValidationRule } from \"nextly/field-catalog\";\n\nimport type { FormFieldType } from \"../types\";\n\n/**\n * `message` appears on every row because it is honoured everywhere: each\n * generator passes `validation.errorMessage` to `applyRequired`, so even a type\n * with no bounds of its own uses it for the required failure.\n *\n * Typed as a total `Record` rather than a partial one, so adding a field type\n * without deciding what it enforces is a compile error instead of a type that\n * silently offers nothing.\n */\nexport const ENFORCED_VALIDATION_RULES: Record<\n FormFieldType,\n readonly FieldValidationRule[]\n> = {\n text: [\"minLength\", \"maxLength\", \"pattern\", \"message\"],\n // Length bounds are deliberately absent: the email schema applies its own\n // format check and a pattern, and reads neither `minLength` nor `maxLength`.\n email: [\"pattern\", \"message\"],\n number: [\"min\", \"max\", \"message\"],\n phone: [\"pattern\", \"message\"],\n url: [\"pattern\", \"message\"],\n // No pattern: the textarea schema reads only the two length bounds.\n textarea: [\"minLength\", \"maxLength\", \"message\"],\n select: [\"message\"],\n checkbox: [\"message\"],\n radio: [\"message\"],\n // A file's size limit is stored on the field itself, not under `validation`.\n file: [\"message\"],\n // The date schema reads `field.min`/`field.max` — top level, not `validation`\n // — so a `validation.min` written here would never be consulted.\n date: [\"message\"],\n time: [\"message\"],\n hidden: [\"message\"],\n};\n\n/**\n * The rules a field of this type both accepts and has enforced.\n *\n * A plugin-contributed type has no row, and correctly gets nothing: this\n * runtime's generator has no clause for it, so every rule would be inert.\n */\nexport function enforcedValidationRules(\n type: string\n): readonly FieldValidationRule[] {\n return ENFORCED_VALIDATION_RULES[type as FormFieldType] ?? [];\n}\n","/**\n * The ONE reader for stored form settings. Settings were historically\n * written in two shapes (the builder's old context shape and the collection\n * group's declared shape), so every consumer — the builder UI and the\n * submit handler alike — normalizes through here instead of trusting the\n * stored keys.\n */\n\nimport { DEFAULT_FORM_SETTINGS } from \"../config/defaults\";\nimport type { FormSettings } from \"../types\";\n\nexport { DEFAULT_FORM_SETTINGS };\n\nfunction asOptionalBoolean(value: unknown): boolean | undefined {\n return typeof value === \"boolean\" ? value : undefined;\n}\n\nfunction asOptionalString(value: unknown): string | undefined {\n return typeof value === \"string\" && value !== \"\" ? value : undefined;\n}\n\n/**\n * Normalize a stored settings value into the canonical {@link FormSettings}.\n *\n * Legacy keys are migrated on read (no data is lost, nothing is rewritten\n * until the form is next saved):\n * - `confirmationMessage` → `successMessage` + `confirmationType: \"message\"`\n * - nested `captcha: { enabled, siteKey }` → flat `captchaEnabled`/`captchaSiteKey`\n *\n * Keys with no consumer (`showResetButton`, `resetButtonText`,\n * `storeSubmissions`, `submissionLimit`) are dropped: a setting that does\n * nothing must not survive into the UI.\n */\nexport function normalizeFormSettings(raw: unknown): FormSettings {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) {\n return { ...DEFAULT_FORM_SETTINGS };\n }\n const source = raw as Record<string, unknown>;\n\n const legacyMessage = asOptionalString(source.confirmationMessage);\n const legacyCaptcha =\n source.captcha && typeof source.captcha === \"object\"\n ? (source.captcha as { enabled?: unknown; siteKey?: unknown })\n : undefined;\n\n // \"relationship\" is a real stored value (redirect to a linked document) —\n // coercing it to \"message\" would disable the form's redirect on the next\n // save. Only genuinely unknown values fall back to \"message\".\n const confirmationType =\n source.confirmationType === \"redirect\" ||\n source.confirmationType === \"relationship\"\n ? source.confirmationType\n : \"message\";\n\n return {\n submitButtonText:\n asOptionalString(source.submitButtonText) ??\n DEFAULT_FORM_SETTINGS.submitButtonText,\n confirmationType,\n successMessage:\n asOptionalString(source.successMessage) ??\n legacyMessage ??\n DEFAULT_FORM_SETTINGS.successMessage,\n redirectUrl: asOptionalString(source.redirectUrl),\n redirectPage: source.redirectPage ?? undefined,\n redirectRelation:\n source.redirectRelation && typeof source.redirectRelation === \"object\"\n ? (source.redirectRelation as FormSettings[\"redirectRelation\"])\n : undefined,\n allowMultipleSubmissions:\n asOptionalBoolean(source.allowMultipleSubmissions) ??\n DEFAULT_FORM_SETTINGS.allowMultipleSubmissions,\n honeypotEnabled: asOptionalBoolean(source.honeypotEnabled),\n captchaEnabled:\n asOptionalBoolean(source.captchaEnabled) ??\n asOptionalBoolean(legacyCaptcha?.enabled),\n captchaSiteKey:\n asOptionalString(source.captchaSiteKey) ??\n asOptionalString(legacyCaptcha?.siteKey),\n };\n}\n","/**\n * Find what references a form field by name, so destructive actions can be\n * guarded: a field that another field's conditional logic or a notification's\n * recipient/reply-to interpolation points at must not be silently deletable.\n */\n\nimport type { AnyFormField, FormNotification } from \"../types\";\n\n/**\n * The one spelling of a field reference this plugin recognises: `{{fieldName}}`\n * with no spaces, and `\\w` names.\n *\n * Exact, because the SEND path is exact — a value the admin accepts as a\n * reference and the send path does not is delivered with its braces intact,\n * straight to the mail provider. Both sides read this constant so the question\n * has one answer.\n */\nexport const FIELD_REF_PATTERN = /^\\{\\{(\\w+)\\}\\}$/;\n\n/** The field named by a reference, or `null` when the value is not one. */\nexport function parseFieldRef(value: string | undefined): string | null {\n if (!value) return null;\n const match = value.match(FIELD_REF_PATTERN);\n return match ? match[1] : null;\n}\n\nexport interface FieldReference {\n /** What kind of thing holds the reference. */\n kind: \"condition\" | \"notification\";\n /** Human label of the referrer (field label or notification name). */\n label: string;\n}\n\n/**\n * Match `{{ fieldName }}` interpolations (whitespace-tolerant), the syntax the\n * notification send path resolves recipients and reply-to values with.\n */\nfunction referencesInTemplate(template: string, fieldName: string): boolean {\n const pattern = new RegExp(\n `\\\\{\\\\{\\\\s*${fieldName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}\\\\s*\\\\}\\\\}`\n );\n return pattern.test(template);\n}\n\n/** All references to `fieldName` across the form's fields and notifications. */\nexport function findFieldReferences(\n fieldName: string,\n fields: readonly AnyFormField[],\n notifications: readonly FormNotification[]\n): FieldReference[] {\n const references: FieldReference[] = [];\n\n for (const field of fields) {\n if (field.name === fieldName) continue;\n // Disabled conditional logic has no runtime effect, so it must not block\n // deletion; the dormant condition just goes stale.\n if (!field.conditionalLogic?.enabled) continue;\n const conditions = field.conditionalLogic.conditions ?? [];\n if (conditions.some(condition => condition.field === fieldName)) {\n references.push({ kind: \"condition\", label: field.label || field.name });\n }\n }\n\n for (const notification of notifications) {\n // A disabled rule has no runtime effect — like disabled conditional\n // logic above, it must not block deletion; its stale reference just\n // needs re-pointing if the rule is ever re-enabled.\n if (!notification.enabled) continue;\n // Field-sourced recipients and reply-to always carry the {{fieldName}}\n // syntax (the send path's resolveFieldRef only matches that form), so\n // one template check covers to/cc/bcc/replyTo uniformly.\n const templates = [\n notification.to,\n ...(notification.cc ?? []),\n ...(notification.bcc ?? []),\n notification.replyTo,\n ].filter((value): value is string => typeof value === \"string\");\n const referencedByTemplate = templates.some(template =>\n referencesInTemplate(template, fieldName)\n );\n // Send-conditions name the field directly (no interpolation syntax).\n const referencedByCondition = notification.condition?.field === fieldName;\n if (referencedByTemplate || referencedByCondition) {\n references.push({\n kind: \"notification\",\n label: notification.name || \"Notification\",\n });\n }\n }\n\n return references;\n}\n\n/**\n * Reference lists for every field in one pass, keyed by field name, so a\n * per-card lookup is O(1) instead of re-walking fields and notifications for\n * each card on every render.\n */\nexport function buildFieldReferenceMap(\n fields: readonly AnyFormField[],\n notifications: readonly FormNotification[]\n): Map<string, FieldReference[]> {\n const map = new Map<string, FieldReference[]>();\n for (const field of fields) {\n map.set(field.name, findFieldReferences(field.name, fields, notifications));\n }\n return map;\n}\n"],"mappings":"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","names":["id","document"]}
|