@10x-media/form-builder 0.1.0-beta.11 → 0.1.0-beta.13

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.
Files changed (151) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/aggregation/resolveResultsRequest.d.ts +6 -0
  3. package/dist/aggregation/resolveResultsRequest.js +32 -3
  4. package/dist/aggregation/resolveResultsRequest.js.map +1 -1
  5. package/dist/calc/computeCalcFields.d.ts +5 -3
  6. package/dist/calc/computeCalcFields.js +4 -3
  7. package/dist/calc/computeCalcFields.js.map +1 -1
  8. package/dist/calc/evaluate.d.ts +26 -3
  9. package/dist/calc/evaluate.js +33 -11
  10. package/dist/calc/evaluate.js.map +1 -1
  11. package/dist/calc/formatCalc.d.ts +11 -0
  12. package/dist/calc/formatCalc.js +35 -0
  13. package/dist/calc/formatCalc.js.map +1 -0
  14. package/dist/calc/formatCalcValue.d.ts +17 -0
  15. package/dist/calc/formatCalcValue.js +17 -0
  16. package/dist/calc/formatCalcValue.js.map +1 -0
  17. package/dist/calc/normalizeCalc.d.ts +17 -3
  18. package/dist/calc/normalizeCalc.js +39 -17
  19. package/dist/calc/normalizeCalc.js.map +1 -1
  20. package/dist/calc/registry.d.ts +43 -0
  21. package/dist/calc/registry.js +27 -0
  22. package/dist/calc/registry.js.map +1 -0
  23. package/dist/calc/resolveCalcContext.d.ts +36 -0
  24. package/dist/calc/resolveCalcContext.js +87 -0
  25. package/dist/calc/resolveCalcContext.js.map +1 -0
  26. package/dist/calc/types.d.ts +18 -3
  27. package/dist/calc/types.js +2 -1
  28. package/dist/calc/types.js.map +1 -1
  29. package/dist/client/CalcExpressionBuilder.d.ts +20 -0
  30. package/dist/client/CalcExpressionBuilder.js +890 -0
  31. package/dist/client/CalcExpressionBuilder.js.map +1 -0
  32. package/dist/client/admin.css +282 -0
  33. package/dist/collections/formSubmissions.js +7 -9
  34. package/dist/collections/formSubmissions.js.map +1 -1
  35. package/dist/collections/forms.js +59 -34
  36. package/dist/collections/forms.js.map +1 -1
  37. package/dist/collections/formsEndpoints.js +4 -2
  38. package/dist/collections/formsEndpoints.js.map +1 -1
  39. package/dist/collections/settingsFields.d.ts +31 -0
  40. package/dist/collections/settingsFields.js +45 -0
  41. package/dist/collections/settingsFields.js.map +1 -0
  42. package/dist/exports/client.d.ts +2 -1
  43. package/dist/exports/client.js +2 -1
  44. package/dist/exports/react.d.ts +7 -3
  45. package/dist/exports/react.js +6 -2
  46. package/dist/exports/rsc.js +1 -1
  47. package/dist/exports/types.d.ts +1 -1
  48. package/dist/fields/buildFieldBlocks.js +2 -1
  49. package/dist/fields/buildFieldBlocks.js.map +1 -1
  50. package/dist/fields/builtin/calculation.d.ts +17 -0
  51. package/dist/fields/builtin/calculation.js +170 -115
  52. package/dist/fields/builtin/calculation.js.map +1 -1
  53. package/dist/fields/builtin/checkbox.js +15 -0
  54. package/dist/fields/builtin/checkbox.js.map +1 -1
  55. package/dist/fields/builtin/email.js +7 -0
  56. package/dist/fields/builtin/email.js.map +1 -1
  57. package/dist/fields/builtin/index.d.ts +6 -2
  58. package/dist/fields/builtin/index.js +9 -4
  59. package/dist/fields/builtin/index.js.map +1 -1
  60. package/dist/fields/builtin/select.js +20 -1
  61. package/dist/fields/builtin/select.js.map +1 -1
  62. package/dist/fields/builtin/text.js +7 -0
  63. package/dist/fields/builtin/text.js.map +1 -1
  64. package/dist/fields/instanceOptions.js +1 -1
  65. package/dist/fields/instanceOptions.js.map +1 -1
  66. package/dist/fields/sharedConfig.js +15 -11
  67. package/dist/fields/sharedConfig.js.map +1 -1
  68. package/dist/fields/types.d.ts +8 -2
  69. package/dist/form/toFormDocument.d.ts +7 -0
  70. package/dist/form/toFormDocument.js +2 -1
  71. package/dist/form/toFormDocument.js.map +1 -1
  72. package/dist/form/types.d.ts +7 -0
  73. package/dist/index.d.ts +12 -4
  74. package/dist/index.js +35 -8
  75. package/dist/index.js.map +1 -1
  76. package/dist/options.d.ts +35 -0
  77. package/dist/plugin/registerCollections.js +29 -19
  78. package/dist/plugin/registerCollections.js.map +1 -1
  79. package/dist/poll/closeJob.d.ts +4 -3
  80. package/dist/poll/closeJob.js +6 -4
  81. package/dist/poll/closeJob.js.map +1 -1
  82. package/dist/poll/pollTypeRegistry.js +1 -1
  83. package/dist/poll/resolvePollCloseRequest.js +2 -1
  84. package/dist/poll/resolvePollCloseRequest.js.map +1 -1
  85. package/dist/poll/resolvePollOutcome.d.ts +6 -0
  86. package/dist/poll/resolvePollOutcome.js +17 -3
  87. package/dist/poll/resolvePollOutcome.js.map +1 -1
  88. package/dist/poll/votes/aggregateFromVotes.d.ts +27 -0
  89. package/dist/poll/votes/aggregateFromVotes.js +53 -0
  90. package/dist/poll/votes/aggregateFromVotes.js.map +1 -0
  91. package/dist/poll/votes/bumpPollVote.js +88 -0
  92. package/dist/poll/votes/bumpPollVote.js.map +1 -0
  93. package/dist/poll/votes/recountPollVotes.d.ts +20 -0
  94. package/dist/poll/votes/recountPollVotes.js +63 -0
  95. package/dist/poll/votes/recountPollVotes.js.map +1 -0
  96. package/dist/poll/votes/voteTallyHook.js +58 -0
  97. package/dist/poll/votes/voteTallyHook.js.map +1 -0
  98. package/dist/poll/votes/votesCollection.d.ts +19 -0
  99. package/dist/poll/votes/votesCollection.js +89 -0
  100. package/dist/poll/votes/votesCollection.js.map +1 -0
  101. package/dist/react/Form.d.ts +7 -0
  102. package/dist/react/Form.js +20 -3
  103. package/dist/react/Form.js.map +1 -1
  104. package/dist/react/FormContext.d.ts +8 -1
  105. package/dist/react/FormContext.js.map +1 -1
  106. package/dist/react/primitives/Checkbox.d.ts +4 -2
  107. package/dist/react/primitives/Checkbox.js +3 -2
  108. package/dist/react/primitives/Checkbox.js.map +1 -1
  109. package/dist/react/primitives/ChoiceGroup.d.ts +40 -0
  110. package/dist/react/primitives/ChoiceGroup.js +33 -0
  111. package/dist/react/primitives/ChoiceGroup.js.map +1 -0
  112. package/dist/react/primitives/FieldShell.d.ts +11 -1
  113. package/dist/react/primitives/FieldShell.js +43 -33
  114. package/dist/react/primitives/FieldShell.js.map +1 -1
  115. package/dist/react/primitives/Input.d.ts +3 -1
  116. package/dist/react/primitives/Input.js +2 -1
  117. package/dist/react/primitives/Input.js.map +1 -1
  118. package/dist/react/renderers/calculation.js +6 -1
  119. package/dist/react/renderers/calculation.js.map +1 -1
  120. package/dist/react/renderers/checkbox.js +2 -1
  121. package/dist/react/renderers/checkbox.js.map +1 -1
  122. package/dist/react/renderers/email.js +3 -1
  123. package/dist/react/renderers/email.js.map +1 -1
  124. package/dist/react/renderers/select.js +20 -2
  125. package/dist/react/renderers/select.js.map +1 -1
  126. package/dist/react/renderers/text.js +3 -1
  127. package/dist/react/renderers/text.js.map +1 -1
  128. package/dist/react/useCalcValues.d.ts +10 -0
  129. package/dist/react/useCalcValues.js +13 -0
  130. package/dist/react/useCalcValues.js.map +1 -0
  131. package/dist/recall/resolver.js +1 -1
  132. package/dist/recall/resolver.js.map +1 -1
  133. package/dist/submissions/formIdOf.js +13 -0
  134. package/dist/submissions/formIdOf.js.map +1 -0
  135. package/dist/submissions/runSubmission.js +3 -3
  136. package/dist/submissions/runSubmission.js.map +1 -1
  137. package/dist/submissions/validateSubmission.js +26 -2
  138. package/dist/submissions/validateSubmission.js.map +1 -1
  139. package/dist/translations/de.js +58 -3
  140. package/dist/translations/de.js.map +1 -1
  141. package/dist/translations/en.js +58 -3
  142. package/dist/translations/en.js.map +1 -1
  143. package/dist/translations/keys.d.ts +56 -1
  144. package/dist/translations/keys.js +56 -1
  145. package/dist/translations/keys.js.map +1 -1
  146. package/dist/validation/builtin/index.js +2 -0
  147. package/dist/validation/builtin/index.js.map +1 -1
  148. package/dist/validation/builtin/integer.js +15 -0
  149. package/dist/validation/builtin/integer.js.map +1 -0
  150. package/package.json +10 -4
  151. package/styles/form-builder.css +84 -0
@@ -1 +1 @@
1
- {"version":3,"file":"Input.js","names":[],"sources":["../../../src/react/primitives/Input.tsx"],"sourcesContent":["'use client'\n\nexport type InputProps = {\n\tid: string\n\tname: string\n\ttype?: 'text' | 'email' | 'number' | 'date'\n\tvalue: string\n\tonChange: (value: string) => void\n\tonBlur?: () => void\n\tplaceholder?: string\n\trequired?: boolean\n\tdisabled?: boolean\n\tinvalid?: boolean\n\tdescribedById?: string\n}\n\nexport const Input = ({\n\tid,\n\tname,\n\ttype = 'text',\n\tvalue,\n\tonChange,\n\tonBlur,\n\tplaceholder,\n\trequired,\n\tdisabled,\n\tinvalid,\n\tdescribedById,\n}: InputProps) => (\n\t<input\n\t\tid={id}\n\t\tname={name}\n\t\ttype={type}\n\t\tclassName=\"fb-input\"\n\t\tvalue={value}\n\t\tplaceholder={placeholder}\n\t\trequired={required}\n\t\tdisabled={disabled}\n\t\taria-invalid={invalid || undefined}\n\t\taria-describedby={describedById}\n\t\tonChange={(event) => onChange(event.target.value)}\n\t\tonBlur={onBlur}\n\t/>\n)\n"],"mappings":";;;AAgBA,MAAa,SAAS,EACrB,IACA,MACA,OAAO,QACP,OACA,UACA,QACA,aACA,UACA,UACA,SACA,oBAEA,oBAAC,SAAD;CACK;CACE;CACA;CACN,WAAU;CACH;CACM;CACH;CACA;CACV,gBAAc,WAAW,KAAA;CACzB,oBAAkB;CAClB,WAAW,UAAU,SAAS,MAAM,OAAO,KAAK;CACxC;AACR,CAAA"}
1
+ {"version":3,"file":"Input.js","names":[],"sources":["../../../src/react/primitives/Input.tsx"],"sourcesContent":["'use client'\n\nexport type InputProps = {\n\tid: string\n\tname: string\n\ttype?: 'text' | 'email' | 'number' | 'date'\n\tvalue: string\n\tonChange: (value: string) => void\n\tonBlur?: () => void\n\tplaceholder?: string\n\trequired?: boolean\n\tdisabled?: boolean\n\tinvalid?: boolean\n\tdescribedById?: string\n\tautoComplete?: string\n}\n\nexport const Input = ({\n\tid,\n\tname,\n\ttype = 'text',\n\tvalue,\n\tonChange,\n\tonBlur,\n\tplaceholder,\n\trequired,\n\tdisabled,\n\tinvalid,\n\tdescribedById,\n\tautoComplete,\n}: InputProps) => (\n\t<input\n\t\tid={id}\n\t\tname={name}\n\t\ttype={type}\n\t\tclassName=\"fb-input\"\n\t\tvalue={value}\n\t\tplaceholder={placeholder}\n\t\trequired={required}\n\t\tdisabled={disabled}\n\t\taria-invalid={invalid || undefined}\n\t\taria-describedby={describedById}\n\t\tautoComplete={autoComplete}\n\t\tonChange={(event) => onChange(event.target.value)}\n\t\tonBlur={onBlur}\n\t/>\n)\n"],"mappings":";;;AAiBA,MAAa,SAAS,EACrB,IACA,MACA,OAAO,QACP,OACA,UACA,QACA,aACA,UACA,UACA,SACA,eACA,mBAEA,oBAAC,SAAD;CACK;CACE;CACA;CACN,WAAU;CACH;CACM;CACH;CACA;CACV,gBAAc,WAAW,KAAA;CACzB,oBAAkB;CACJ;CACd,WAAW,UAAU,SAAS,MAAM,OAAO,KAAK;CACxC;AACR,CAAA"}
@@ -1,4 +1,5 @@
1
1
  "use client";
2
+ import { formatCalcValue } from "../../calc/formatCalcValue.js";
2
3
  import { defineFieldRenderer } from "../contract.js";
3
4
  import { FieldShell } from "../primitives/FieldShell.js";
4
5
  import { jsx } from "react/jsx-runtime";
@@ -17,7 +18,11 @@ const calculationRenderer = defineFieldRenderer(({ field, id, value, errors, req
17
18
  id,
18
19
  className: "fb-field__calc",
19
20
  "aria-describedby": describedById,
20
- children: value == null ? "" : String(value)
21
+ children: value == null ? "" : formatCalcValue(value, {
22
+ decimals: field.decimals,
23
+ prefix: field.prefix,
24
+ suffix: field.suffix
25
+ })
21
26
  })
22
27
  });
23
28
  });
@@ -1 +1 @@
1
- {"version":3,"file":"calculation.js","names":[],"sources":["../../../src/react/renderers/calculation.tsx"],"sourcesContent":["'use client'\n\nimport { defineFieldRenderer } from '../contract'\nimport { FieldShell } from '../primitives/FieldShell'\n\n/** Read-only renderer for a calculation field: the value is derived, never user-editable, so it shows in an `<output>` rather than an input. */\nexport const calculationRenderer = defineFieldRenderer<number | undefined>(\n\t({ field, id, value, errors, required }) => {\n\t\tconst describedById = `${id}-desc`\n\t\treturn (\n\t\t\t<FieldShell\n\t\t\t\tid={id}\n\t\t\t\tlabel={field.label}\n\t\t\t\tdescription={typeof field.description === 'string' ? field.description : undefined}\n\t\t\t\trequired={required}\n\t\t\t\terrors={errors}\n\t\t\t\tdescribedById={describedById}\n\t\t\t>\n\t\t\t\t<output id={id} className=\"fb-field__calc\" aria-describedby={describedById}>\n\t\t\t\t\t{value == null ? '' : String(value)}\n\t\t\t\t</output>\n\t\t\t</FieldShell>\n\t\t)\n\t}\n)\n"],"mappings":";;;;;;AAMA,MAAa,sBAAsB,qBACjC,EAAE,OAAO,IAAI,OAAO,QAAQ,eAAe;CAC3C,MAAM,gBAAgB,GAAG,GAAG;CAC5B,OACC,oBAAC,YAAD;EACK;EACJ,OAAO,MAAM;EACb,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;EAC/D;EACF;EACO;YAEf,oBAAC,UAAD;GAAY;GAAI,WAAU;GAAiB,oBAAkB;aAC3D,SAAS,OAAO,KAAK,OAAO,KAAK;EAC3B,CAAA;CACG,CAAA;AAEd,CACD"}
1
+ {"version":3,"file":"calculation.js","names":[],"sources":["../../../src/react/renderers/calculation.tsx"],"sourcesContent":["'use client'\n\nimport { formatCalcValue } from '../../calc/formatCalcValue'\nimport { defineFieldRenderer } from '../contract'\nimport { FieldShell } from '../primitives/FieldShell'\n\n/** Read-only renderer for a calculation field: the value is derived, never user-editable, so it shows in an `<output>` rather than an input. */\nexport const calculationRenderer = defineFieldRenderer<number | undefined>(\n\t({ field, id, value, errors, required }) => {\n\t\tconst describedById = `${id}-desc`\n\t\treturn (\n\t\t\t<FieldShell\n\t\t\t\tid={id}\n\t\t\t\tlabel={field.label}\n\t\t\t\tdescription={typeof field.description === 'string' ? field.description : undefined}\n\t\t\t\trequired={required}\n\t\t\t\terrors={errors}\n\t\t\t\tdescribedById={describedById}\n\t\t\t>\n\t\t\t\t<output id={id} className=\"fb-field__calc\" aria-describedby={describedById}>\n\t\t\t\t\t{value == null\n\t\t\t\t\t\t? ''\n\t\t\t\t\t\t: formatCalcValue(value, {\n\t\t\t\t\t\t\t\tdecimals: field.decimals,\n\t\t\t\t\t\t\t\tprefix: field.prefix,\n\t\t\t\t\t\t\t\tsuffix: field.suffix,\n\t\t\t\t\t\t\t})}\n\t\t\t\t</output>\n\t\t\t</FieldShell>\n\t\t)\n\t}\n)\n"],"mappings":";;;;;;;AAOA,MAAa,sBAAsB,qBACjC,EAAE,OAAO,IAAI,OAAO,QAAQ,eAAe;CAC3C,MAAM,gBAAgB,GAAG,GAAG;CAC5B,OACC,oBAAC,YAAD;EACK;EACJ,OAAO,MAAM;EACb,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;EAC/D;EACF;EACO;YAEf,oBAAC,UAAD;GAAY;GAAI,WAAU;GAAiB,oBAAkB;aAC3D,SAAS,OACP,KACA,gBAAgB,OAAO;IACvB,UAAU,MAAM;IAChB,QAAQ,MAAM;IACd,QAAQ,MAAM;GACf,CAAC;EACI,CAAA;CACG,CAAA;AAEd,CACD"}
@@ -22,7 +22,8 @@ const checkboxRenderer = defineFieldRenderer(({ field, id, name, value, onChange
22
22
  required,
23
23
  disabled,
24
24
  invalid: errors.length > 0,
25
- describedById
25
+ describedById,
26
+ variant: field.display === "switch" ? "switch" : void 0
26
27
  })
27
28
  });
28
29
  });
@@ -1 +1 @@
1
- {"version":3,"file":"checkbox.js","names":[],"sources":["../../../src/react/renderers/checkbox.tsx"],"sourcesContent":["'use client'\n\nimport { defineFieldRenderer } from '../contract'\nimport { Checkbox } from '../primitives/Checkbox'\nimport { FieldShell } from '../primitives/FieldShell'\n\nexport const checkboxRenderer = defineFieldRenderer<boolean>(\n\t({ field, id, name, value, onChange, onBlur, errors, required, disabled }) => {\n\t\tconst describedById = `${id}-desc`\n\t\treturn (\n\t\t\t<FieldShell\n\t\t\t\tid={id}\n\t\t\t\tlabel={field.label}\n\t\t\t\tdescription={typeof field.description === 'string' ? field.description : undefined}\n\t\t\t\trequired={required}\n\t\t\t\terrors={errors}\n\t\t\t\tdescribedById={describedById}\n\t\t\t>\n\t\t\t\t<Checkbox\n\t\t\t\t\tid={id}\n\t\t\t\t\tname={name}\n\t\t\t\t\tchecked={value ?? false}\n\t\t\t\t\tonChange={onChange}\n\t\t\t\t\tonBlur={onBlur}\n\t\t\t\t\trequired={required}\n\t\t\t\t\tdisabled={disabled}\n\t\t\t\t\tinvalid={errors.length > 0}\n\t\t\t\t\tdescribedById={describedById}\n\t\t\t\t/>\n\t\t\t</FieldShell>\n\t\t)\n\t}\n)\n"],"mappings":";;;;;;AAMA,MAAa,mBAAmB,qBAC9B,EAAE,OAAO,IAAI,MAAM,OAAO,UAAU,QAAQ,QAAQ,UAAU,eAAe;CAC7E,MAAM,gBAAgB,GAAG,GAAG;CAC5B,OACC,oBAAC,YAAD;EACK;EACJ,OAAO,MAAM;EACb,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;EAC/D;EACF;EACO;YAEf,oBAAC,UAAD;GACK;GACE;GACN,SAAS,SAAS;GACR;GACF;GACE;GACA;GACV,SAAS,OAAO,SAAS;GACV;EACf,CAAA;CACU,CAAA;AAEd,CACD"}
1
+ {"version":3,"file":"checkbox.js","names":[],"sources":["../../../src/react/renderers/checkbox.tsx"],"sourcesContent":["'use client'\n\nimport { defineFieldRenderer } from '../contract'\nimport { Checkbox } from '../primitives/Checkbox'\nimport { FieldShell } from '../primitives/FieldShell'\n\nexport const checkboxRenderer = defineFieldRenderer<boolean>(\n\t({ field, id, name, value, onChange, onBlur, errors, required, disabled }) => {\n\t\tconst describedById = `${id}-desc`\n\t\treturn (\n\t\t\t<FieldShell\n\t\t\t\tid={id}\n\t\t\t\tlabel={field.label}\n\t\t\t\tdescription={typeof field.description === 'string' ? field.description : undefined}\n\t\t\t\trequired={required}\n\t\t\t\terrors={errors}\n\t\t\t\tdescribedById={describedById}\n\t\t\t>\n\t\t\t\t<Checkbox\n\t\t\t\t\tid={id}\n\t\t\t\t\tname={name}\n\t\t\t\t\tchecked={value ?? false}\n\t\t\t\t\tonChange={onChange}\n\t\t\t\t\tonBlur={onBlur}\n\t\t\t\t\trequired={required}\n\t\t\t\t\tdisabled={disabled}\n\t\t\t\t\tinvalid={errors.length > 0}\n\t\t\t\t\tdescribedById={describedById}\n\t\t\t\t\tvariant={field.display === 'switch' ? 'switch' : undefined}\n\t\t\t\t/>\n\t\t\t</FieldShell>\n\t\t)\n\t}\n)\n"],"mappings":";;;;;;AAMA,MAAa,mBAAmB,qBAC9B,EAAE,OAAO,IAAI,MAAM,OAAO,UAAU,QAAQ,QAAQ,UAAU,eAAe;CAC7E,MAAM,gBAAgB,GAAG,GAAG;CAC5B,OACC,oBAAC,YAAD;EACK;EACJ,OAAO,MAAM;EACb,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;EAC/D;EACF;EACO;YAEf,oBAAC,UAAD;GACK;GACE;GACN,SAAS,SAAS;GACR;GACF;GACE;GACA;GACV,SAAS,OAAO,SAAS;GACV;GACf,SAAS,MAAM,YAAY,WAAW,WAAW,KAAA;EACjD,CAAA;CACU,CAAA;AAEd,CACD"}
@@ -6,6 +6,7 @@ import { jsx } from "react/jsx-runtime";
6
6
  //#region src/react/renderers/email.tsx
7
7
  const emailRenderer = defineFieldRenderer(({ field, id, name, value, onChange, onBlur, errors, required, disabled }) => {
8
8
  const describedById = `${id}-desc`;
9
+ const autoComplete = typeof field.autocomplete === "string" && field.autocomplete.trim() ? field.autocomplete : void 0;
9
10
  return /* @__PURE__ */ jsx(FieldShell, {
10
11
  id,
11
12
  label: field.label,
@@ -24,7 +25,8 @@ const emailRenderer = defineFieldRenderer(({ field, id, name, value, onChange, o
24
25
  required,
25
26
  disabled,
26
27
  invalid: errors.length > 0,
27
- describedById
28
+ describedById,
29
+ autoComplete
28
30
  })
29
31
  });
30
32
  });
@@ -1 +1 @@
1
- {"version":3,"file":"email.js","names":[],"sources":["../../../src/react/renderers/email.tsx"],"sourcesContent":["'use client'\n\nimport { defineFieldRenderer } from '../contract'\nimport { FieldShell } from '../primitives/FieldShell'\nimport { Input } from '../primitives/Input'\n\nexport const emailRenderer = defineFieldRenderer<string>(\n\t({ field, id, name, value, onChange, onBlur, errors, required, disabled }) => {\n\t\tconst describedById = `${id}-desc`\n\t\treturn (\n\t\t\t<FieldShell\n\t\t\t\tid={id}\n\t\t\t\tlabel={field.label}\n\t\t\t\tdescription={typeof field.description === 'string' ? field.description : undefined}\n\t\t\t\trequired={required}\n\t\t\t\terrors={errors}\n\t\t\t\tdescribedById={describedById}\n\t\t\t>\n\t\t\t\t<Input\n\t\t\t\t\tid={id}\n\t\t\t\t\tname={name}\n\t\t\t\t\ttype=\"email\"\n\t\t\t\t\tvalue={value ?? ''}\n\t\t\t\t\tonChange={onChange}\n\t\t\t\t\tonBlur={onBlur}\n\t\t\t\t\tplaceholder={typeof field.placeholder === 'string' ? field.placeholder : undefined}\n\t\t\t\t\trequired={required}\n\t\t\t\t\tdisabled={disabled}\n\t\t\t\t\tinvalid={errors.length > 0}\n\t\t\t\t\tdescribedById={describedById}\n\t\t\t\t/>\n\t\t\t</FieldShell>\n\t\t)\n\t}\n)\n"],"mappings":";;;;;;AAMA,MAAa,gBAAgB,qBAC3B,EAAE,OAAO,IAAI,MAAM,OAAO,UAAU,QAAQ,QAAQ,UAAU,eAAe;CAC7E,MAAM,gBAAgB,GAAG,GAAG;CAC5B,OACC,oBAAC,YAAD;EACK;EACJ,OAAO,MAAM;EACb,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;EAC/D;EACF;EACO;YAEf,oBAAC,OAAD;GACK;GACE;GACN,MAAK;GACL,OAAO,SAAS;GACN;GACF;GACR,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;GAC/D;GACA;GACV,SAAS,OAAO,SAAS;GACV;EACf,CAAA;CACU,CAAA;AAEd,CACD"}
1
+ {"version":3,"file":"email.js","names":[],"sources":["../../../src/react/renderers/email.tsx"],"sourcesContent":["'use client'\n\nimport { defineFieldRenderer } from '../contract'\nimport { FieldShell } from '../primitives/FieldShell'\nimport { Input } from '../primitives/Input'\n\nexport const emailRenderer = defineFieldRenderer<string>(\n\t({ field, id, name, value, onChange, onBlur, errors, required, disabled }) => {\n\t\tconst describedById = `${id}-desc`\n\t\tconst autoComplete =\n\t\t\ttypeof field.autocomplete === 'string' && field.autocomplete.trim()\n\t\t\t\t? field.autocomplete\n\t\t\t\t: undefined\n\t\treturn (\n\t\t\t<FieldShell\n\t\t\t\tid={id}\n\t\t\t\tlabel={field.label}\n\t\t\t\tdescription={typeof field.description === 'string' ? field.description : undefined}\n\t\t\t\trequired={required}\n\t\t\t\terrors={errors}\n\t\t\t\tdescribedById={describedById}\n\t\t\t>\n\t\t\t\t<Input\n\t\t\t\t\tid={id}\n\t\t\t\t\tname={name}\n\t\t\t\t\ttype=\"email\"\n\t\t\t\t\tvalue={value ?? ''}\n\t\t\t\t\tonChange={onChange}\n\t\t\t\t\tonBlur={onBlur}\n\t\t\t\t\tplaceholder={typeof field.placeholder === 'string' ? field.placeholder : undefined}\n\t\t\t\t\trequired={required}\n\t\t\t\t\tdisabled={disabled}\n\t\t\t\t\tinvalid={errors.length > 0}\n\t\t\t\t\tdescribedById={describedById}\n\t\t\t\t\tautoComplete={autoComplete}\n\t\t\t\t/>\n\t\t\t</FieldShell>\n\t\t)\n\t}\n)\n"],"mappings":";;;;;;AAMA,MAAa,gBAAgB,qBAC3B,EAAE,OAAO,IAAI,MAAM,OAAO,UAAU,QAAQ,QAAQ,UAAU,eAAe;CAC7E,MAAM,gBAAgB,GAAG,GAAG;CAC5B,MAAM,eACL,OAAO,MAAM,iBAAiB,YAAY,MAAM,aAAa,KAAK,IAC/D,MAAM,eACN,KAAA;CACJ,OACC,oBAAC,YAAD;EACK;EACJ,OAAO,MAAM;EACb,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;EAC/D;EACF;EACO;YAEf,oBAAC,OAAD;GACK;GACE;GACN,MAAK;GACL,OAAO,SAAS;GACN;GACF;GACR,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;GAC/D;GACA;GACV,SAAS,OAAO,SAAS;GACV;GACD;EACd,CAAA;CACU,CAAA;AAEd,CACD"}
@@ -2,11 +2,16 @@
2
2
  import { defineFieldRenderer } from "../contract.js";
3
3
  import { FieldShell } from "../primitives/FieldShell.js";
4
4
  import { Select } from "../primitives/Select.js";
5
+ import { ChoiceGroup } from "../primitives/ChoiceGroup.js";
5
6
  import { jsx } from "react/jsx-runtime";
6
7
  //#region src/react/renderers/select.tsx
7
8
  const selectRenderer = defineFieldRenderer(({ field, id, name, value, onChange, onBlur, errors, required, disabled }) => {
8
9
  const describedById = `${id}-desc`;
9
- const options = Array.isArray(field.options) ? field.options : [];
10
+ const options = (Array.isArray(field.options) ? field.options : []).map((option) => ({
11
+ value: option.value,
12
+ label: option.label?.trim() ? option.label : option.value
13
+ }));
14
+ const display = field.display === "radio" || field.display === "buttons" ? field.display : "dropdown";
10
15
  return /* @__PURE__ */ jsx(FieldShell, {
11
16
  id,
12
17
  label: field.label,
@@ -14,7 +19,8 @@ const selectRenderer = defineFieldRenderer(({ field, id, name, value, onChange,
14
19
  required,
15
20
  errors,
16
21
  describedById,
17
- children: /* @__PURE__ */ jsx(Select, {
22
+ group: display !== "dropdown",
23
+ children: display === "dropdown" ? /* @__PURE__ */ jsx(Select, {
18
24
  id,
19
25
  name,
20
26
  value: value ?? "",
@@ -26,6 +32,18 @@ const selectRenderer = defineFieldRenderer(({ field, id, name, value, onChange,
26
32
  disabled,
27
33
  invalid: errors.length > 0,
28
34
  describedById
35
+ }) : /* @__PURE__ */ jsx(ChoiceGroup, {
36
+ name,
37
+ value: value ?? "",
38
+ options,
39
+ onChange,
40
+ onBlur,
41
+ required,
42
+ disabled,
43
+ invalid: errors.length > 0,
44
+ describedById,
45
+ labelledById: field.label ? `${id}-label` : void 0,
46
+ variant: display
29
47
  })
30
48
  });
31
49
  });
@@ -1 +1 @@
1
- {"version":3,"file":"select.js","names":[],"sources":["../../../src/react/renderers/select.tsx"],"sourcesContent":["'use client'\n\nimport { defineFieldRenderer } from '../contract'\nimport { FieldShell } from '../primitives/FieldShell'\nimport { Select } from '../primitives/Select'\n\nexport const selectRenderer = defineFieldRenderer<string>(\n\t({ field, id, name, value, onChange, onBlur, errors, required, disabled }) => {\n\t\tconst describedById = `${id}-desc`\n\t\tconst options = Array.isArray(field.options)\n\t\t\t? (field.options as { label: string; value: string }[])\n\t\t\t: []\n\t\treturn (\n\t\t\t<FieldShell\n\t\t\t\tid={id}\n\t\t\t\tlabel={field.label}\n\t\t\t\tdescription={typeof field.description === 'string' ? field.description : undefined}\n\t\t\t\trequired={required}\n\t\t\t\terrors={errors}\n\t\t\t\tdescribedById={describedById}\n\t\t\t>\n\t\t\t\t<Select\n\t\t\t\t\tid={id}\n\t\t\t\t\tname={name}\n\t\t\t\t\tvalue={value ?? ''}\n\t\t\t\t\toptions={options}\n\t\t\t\t\tonChange={onChange}\n\t\t\t\t\tonBlur={onBlur}\n\t\t\t\t\tplaceholder={typeof field.placeholder === 'string' ? field.placeholder : undefined}\n\t\t\t\t\trequired={required}\n\t\t\t\t\tdisabled={disabled}\n\t\t\t\t\tinvalid={errors.length > 0}\n\t\t\t\t\tdescribedById={describedById}\n\t\t\t\t/>\n\t\t\t</FieldShell>\n\t\t)\n\t}\n)\n"],"mappings":";;;;;;AAMA,MAAa,iBAAiB,qBAC5B,EAAE,OAAO,IAAI,MAAM,OAAO,UAAU,QAAQ,QAAQ,UAAU,eAAe;CAC7E,MAAM,gBAAgB,GAAG,GAAG;CAC5B,MAAM,UAAU,MAAM,QAAQ,MAAM,OAAO,IACvC,MAAM,UACP,CAAC;CACJ,OACC,oBAAC,YAAD;EACK;EACJ,OAAO,MAAM;EACb,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;EAC/D;EACF;EACO;YAEf,oBAAC,QAAD;GACK;GACE;GACN,OAAO,SAAS;GACP;GACC;GACF;GACR,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;GAC/D;GACA;GACV,SAAS,OAAO,SAAS;GACV;EACf,CAAA;CACU,CAAA;AAEd,CACD"}
1
+ {"version":3,"file":"select.js","names":[],"sources":["../../../src/react/renderers/select.tsx"],"sourcesContent":["'use client'\n\nimport { defineFieldRenderer } from '../contract'\nimport { ChoiceGroup } from '../primitives/ChoiceGroup'\nimport { FieldShell } from '../primitives/FieldShell'\nimport { Select } from '../primitives/Select'\n\nexport const selectRenderer = defineFieldRenderer<string>(\n\t({ field, id, name, value, onChange, onBlur, errors, required, disabled }) => {\n\t\tconst describedById = `${id}-desc`\n\t\tconst options = (\n\t\t\tArray.isArray(field.options) ? (field.options as { label?: string; value: string }[]) : []\n\t\t).map((option) => ({\n\t\t\tvalue: option.value,\n\t\t\tlabel: option.label?.trim() ? option.label : option.value,\n\t\t}))\n\t\tconst display =\n\t\t\tfield.display === 'radio' || field.display === 'buttons' ? field.display : 'dropdown'\n\t\treturn (\n\t\t\t<FieldShell\n\t\t\t\tid={id}\n\t\t\t\tlabel={field.label}\n\t\t\t\tdescription={typeof field.description === 'string' ? field.description : undefined}\n\t\t\t\trequired={required}\n\t\t\t\terrors={errors}\n\t\t\t\tdescribedById={describedById}\n\t\t\t\tgroup={display !== 'dropdown'}\n\t\t\t>\n\t\t\t\t{display === 'dropdown' ? (\n\t\t\t\t\t<Select\n\t\t\t\t\t\tid={id}\n\t\t\t\t\t\tname={name}\n\t\t\t\t\t\tvalue={value ?? ''}\n\t\t\t\t\t\toptions={options}\n\t\t\t\t\t\tonChange={onChange}\n\t\t\t\t\t\tonBlur={onBlur}\n\t\t\t\t\t\tplaceholder={typeof field.placeholder === 'string' ? field.placeholder : undefined}\n\t\t\t\t\t\trequired={required}\n\t\t\t\t\t\tdisabled={disabled}\n\t\t\t\t\t\tinvalid={errors.length > 0}\n\t\t\t\t\t\tdescribedById={describedById}\n\t\t\t\t\t/>\n\t\t\t\t) : (\n\t\t\t\t\t<ChoiceGroup\n\t\t\t\t\t\tname={name}\n\t\t\t\t\t\tvalue={value ?? ''}\n\t\t\t\t\t\toptions={options}\n\t\t\t\t\t\tonChange={onChange}\n\t\t\t\t\t\tonBlur={onBlur}\n\t\t\t\t\t\trequired={required}\n\t\t\t\t\t\tdisabled={disabled}\n\t\t\t\t\t\tinvalid={errors.length > 0}\n\t\t\t\t\t\tdescribedById={describedById}\n\t\t\t\t\t\tlabelledById={field.label ? `${id}-label` : undefined}\n\t\t\t\t\t\tvariant={display}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t</FieldShell>\n\t\t)\n\t}\n)\n"],"mappings":";;;;;;;AAOA,MAAa,iBAAiB,qBAC5B,EAAE,OAAO,IAAI,MAAM,OAAO,UAAU,QAAQ,QAAQ,UAAU,eAAe;CAC7E,MAAM,gBAAgB,GAAG,GAAG;CAC5B,MAAM,WACL,MAAM,QAAQ,MAAM,OAAO,IAAK,MAAM,UAAkD,CAAC,GACxF,KAAK,YAAY;EAClB,OAAO,OAAO;EACd,OAAO,OAAO,OAAO,KAAK,IAAI,OAAO,QAAQ,OAAO;CACrD,EAAE;CACF,MAAM,UACL,MAAM,YAAY,WAAW,MAAM,YAAY,YAAY,MAAM,UAAU;CAC5E,OACC,oBAAC,YAAD;EACK;EACJ,OAAO,MAAM;EACb,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;EAC/D;EACF;EACO;EACf,OAAO,YAAY;YAElB,YAAY,aACZ,oBAAC,QAAD;GACK;GACE;GACN,OAAO,SAAS;GACP;GACC;GACF;GACR,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;GAC/D;GACA;GACV,SAAS,OAAO,SAAS;GACV;EACf,CAAA,IAED,oBAAC,aAAD;GACO;GACN,OAAO,SAAS;GACP;GACC;GACF;GACE;GACA;GACV,SAAS,OAAO,SAAS;GACV;GACf,cAAc,MAAM,QAAQ,GAAG,GAAG,UAAU,KAAA;GAC5C,SAAS;EACT,CAAA;CAES,CAAA;AAEd,CACD"}
@@ -6,6 +6,7 @@ import { jsx } from "react/jsx-runtime";
6
6
  //#region src/react/renderers/text.tsx
7
7
  const textRenderer = defineFieldRenderer(({ field, id, name, value, onChange, onBlur, errors, required, disabled }) => {
8
8
  const describedById = `${id}-desc`;
9
+ const autoComplete = typeof field.autocomplete === "string" && field.autocomplete.trim() ? field.autocomplete : void 0;
9
10
  return /* @__PURE__ */ jsx(FieldShell, {
10
11
  id,
11
12
  label: field.label,
@@ -24,7 +25,8 @@ const textRenderer = defineFieldRenderer(({ field, id, name, value, onChange, on
24
25
  required,
25
26
  disabled,
26
27
  invalid: errors.length > 0,
27
- describedById
28
+ describedById,
29
+ autoComplete
28
30
  })
29
31
  });
30
32
  });
@@ -1 +1 @@
1
- {"version":3,"file":"text.js","names":[],"sources":["../../../src/react/renderers/text.tsx"],"sourcesContent":["'use client'\n\nimport { defineFieldRenderer } from '../contract'\nimport { FieldShell } from '../primitives/FieldShell'\nimport { Input } from '../primitives/Input'\n\nexport const textRenderer = defineFieldRenderer<string>(\n\t({ field, id, name, value, onChange, onBlur, errors, required, disabled }) => {\n\t\tconst describedById = `${id}-desc`\n\t\treturn (\n\t\t\t<FieldShell\n\t\t\t\tid={id}\n\t\t\t\tlabel={field.label}\n\t\t\t\tdescription={typeof field.description === 'string' ? field.description : undefined}\n\t\t\t\trequired={required}\n\t\t\t\terrors={errors}\n\t\t\t\tdescribedById={describedById}\n\t\t\t>\n\t\t\t\t<Input\n\t\t\t\t\tid={id}\n\t\t\t\t\tname={name}\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t\tvalue={value ?? ''}\n\t\t\t\t\tonChange={onChange}\n\t\t\t\t\tonBlur={onBlur}\n\t\t\t\t\tplaceholder={typeof field.placeholder === 'string' ? field.placeholder : undefined}\n\t\t\t\t\trequired={required}\n\t\t\t\t\tdisabled={disabled}\n\t\t\t\t\tinvalid={errors.length > 0}\n\t\t\t\t\tdescribedById={describedById}\n\t\t\t\t/>\n\t\t\t</FieldShell>\n\t\t)\n\t}\n)\n"],"mappings":";;;;;;AAMA,MAAa,eAAe,qBAC1B,EAAE,OAAO,IAAI,MAAM,OAAO,UAAU,QAAQ,QAAQ,UAAU,eAAe;CAC7E,MAAM,gBAAgB,GAAG,GAAG;CAC5B,OACC,oBAAC,YAAD;EACK;EACJ,OAAO,MAAM;EACb,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;EAC/D;EACF;EACO;YAEf,oBAAC,OAAD;GACK;GACE;GACN,MAAK;GACL,OAAO,SAAS;GACN;GACF;GACR,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;GAC/D;GACA;GACV,SAAS,OAAO,SAAS;GACV;EACf,CAAA;CACU,CAAA;AAEd,CACD"}
1
+ {"version":3,"file":"text.js","names":[],"sources":["../../../src/react/renderers/text.tsx"],"sourcesContent":["'use client'\n\nimport { defineFieldRenderer } from '../contract'\nimport { FieldShell } from '../primitives/FieldShell'\nimport { Input } from '../primitives/Input'\n\nexport const textRenderer = defineFieldRenderer<string>(\n\t({ field, id, name, value, onChange, onBlur, errors, required, disabled }) => {\n\t\tconst describedById = `${id}-desc`\n\t\tconst autoComplete =\n\t\t\ttypeof field.autocomplete === 'string' && field.autocomplete.trim()\n\t\t\t\t? field.autocomplete\n\t\t\t\t: undefined\n\t\treturn (\n\t\t\t<FieldShell\n\t\t\t\tid={id}\n\t\t\t\tlabel={field.label}\n\t\t\t\tdescription={typeof field.description === 'string' ? field.description : undefined}\n\t\t\t\trequired={required}\n\t\t\t\terrors={errors}\n\t\t\t\tdescribedById={describedById}\n\t\t\t>\n\t\t\t\t<Input\n\t\t\t\t\tid={id}\n\t\t\t\t\tname={name}\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t\tvalue={value ?? ''}\n\t\t\t\t\tonChange={onChange}\n\t\t\t\t\tonBlur={onBlur}\n\t\t\t\t\tplaceholder={typeof field.placeholder === 'string' ? field.placeholder : undefined}\n\t\t\t\t\trequired={required}\n\t\t\t\t\tdisabled={disabled}\n\t\t\t\t\tinvalid={errors.length > 0}\n\t\t\t\t\tdescribedById={describedById}\n\t\t\t\t\tautoComplete={autoComplete}\n\t\t\t\t/>\n\t\t\t</FieldShell>\n\t\t)\n\t}\n)\n"],"mappings":";;;;;;AAMA,MAAa,eAAe,qBAC1B,EAAE,OAAO,IAAI,MAAM,OAAO,UAAU,QAAQ,QAAQ,UAAU,eAAe;CAC7E,MAAM,gBAAgB,GAAG,GAAG;CAC5B,MAAM,eACL,OAAO,MAAM,iBAAiB,YAAY,MAAM,aAAa,KAAK,IAC/D,MAAM,eACN,KAAA;CACJ,OACC,oBAAC,YAAD;EACK;EACJ,OAAO,MAAM;EACb,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;EAC/D;EACF;EACO;YAEf,oBAAC,OAAD;GACK;GACE;GACN,MAAK;GACL,OAAO,SAAS;GACN;GACF;GACR,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;GAC/D;GACA;GACV,SAAS,OAAO,SAAS;GACV;GACD;EACd,CAAA;CACU,CAAA;AAEd,CACD"}
@@ -0,0 +1,10 @@
1
+ //#region src/react/useCalcValues.d.ts
2
+ /**
3
+ * Live computed calculation values (field name -> number), recomputed as answers change. For
4
+ * composed frontends (custom layouts over `useFormContext`/`FormFields`) that render running
5
+ * totals without re-evaluating expressions themselves.
6
+ */
7
+ declare const useCalcValues: () => Record<string, number>;
8
+ //#endregion
9
+ export { useCalcValues };
10
+ //# sourceMappingURL=useCalcValues.d.ts.map
@@ -0,0 +1,13 @@
1
+ "use client";
2
+ import { useFormContext } from "./FormContext.js";
3
+ //#region src/react/useCalcValues.ts
4
+ /**
5
+ * Live computed calculation values (field name -> number), recomputed as answers change. For
6
+ * composed frontends (custom layouts over `useFormContext`/`FormFields`) that render running
7
+ * totals without re-evaluating expressions themselves.
8
+ */
9
+ const useCalcValues = () => useFormContext().calcValues ?? {};
10
+ //#endregion
11
+ export { useCalcValues };
12
+
13
+ //# sourceMappingURL=useCalcValues.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCalcValues.js","names":[],"sources":["../../src/react/useCalcValues.ts"],"sourcesContent":["'use client'\n\nimport { useFormContext } from './FormContext'\n\n/**\n * Live computed calculation values (field name -> number), recomputed as answers change. For\n * composed frontends (custom layouts over `useFormContext`/`FormFields`) that render running\n * totals without re-evaluating expressions themselves.\n */\nexport const useCalcValues = (): Record<string, number> => useFormContext().calcValues ?? {}\n"],"mappings":";;;;;;;;AASA,MAAa,sBAA8C,eAAe,EAAE,cAAc,CAAC"}
@@ -5,7 +5,7 @@ const optionLabelsFor = (field) => {
5
5
  const options = field.options;
6
6
  if (!Array.isArray(options)) return;
7
7
  const map = {};
8
- for (const option of options) if (option && typeof option.value === "string") map[option.value] = option.label ?? option.value;
8
+ for (const option of options) if (option && typeof option.value === "string") map[option.value] = option.label?.trim() ? option.label : option.value;
9
9
  return Object.keys(map).length > 0 ? map : void 0;
10
10
  };
11
11
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"resolver.js","names":[],"sources":["../../src/recall/resolver.ts"],"sourcesContent":["import { isNamedField } from '../fields/fieldKey'\nimport type { AnyFormFieldDefinition } from '../fields/types'\nimport type { FormFieldInstance, SubmissionDescriptor } from '../submissions/types'\n\nexport type RecallResolver = (name: string) => string\n\ntype Option = { label?: string; value?: string }\n\n/** Derive a value->label map from a select-like field instance's `options`. */\nexport const optionLabelsFor = (field: FormFieldInstance): Record<string, string> | undefined => {\n\tconst options = field.options\n\tif (!Array.isArray(options)) {\n\t\treturn undefined\n\t}\n\tconst map: Record<string, string> = {}\n\tfor (const option of options as Option[]) {\n\t\tif (option && typeof option.value === 'string') {\n\t\t\tmap[option.value] = option.label ?? option.value\n\t\t}\n\t}\n\treturn Object.keys(map).length > 0 ? map : undefined\n}\n\n/**\n * Build a recall resolver: `{{name}}` -> the field's current value, formatted via its type's `format`\n * (so a select shows its option label, a checkbox Yes/No, a date localized). Missing field or empty\n * value -> ''. Pure and isomorphic; the renderer and server templates share it.\n */\nexport const buildRecallResolver = (args: {\n\tfields: FormFieldInstance[]\n\tvalues: Record<string, unknown>\n\tregistry: Map<string, AnyFormFieldDefinition>\n\tlocale: string\n\tt: (key: string) => string\n}): RecallResolver => {\n\tconst byName = new Map(args.fields.filter(isNamedField).map((field) => [field.name, field]))\n\treturn (name) => {\n\t\tconst field = byName.get(name)\n\t\tif (!field) {\n\t\t\treturn ''\n\t\t}\n\t\tconst value = args.values[name]\n\t\tif (value == null || value === '' || (Array.isArray(value) && value.length === 0)) {\n\t\t\treturn ''\n\t\t}\n\t\tconst definition = args.registry.get(field.blockType)\n\t\tif (definition?.format) {\n\t\t\treturn definition.format({\n\t\t\t\tvalue,\n\t\t\t\tconfig: {},\n\t\t\t\toptionLabels: optionLabelsFor(field),\n\t\t\t\tlocale: args.locale,\n\t\t\t\tt: args.t,\n\t\t\t})\n\t\t}\n\t\treturn Array.isArray(value) ? value.join(', ') : String(value)\n\t}\n}\n\n/**\n * Build a submission-shaped descriptor per named field: the client-side counterpart to the\n * per-field descriptors `runSubmission` snapshots server-side. Lets client-rendered templates\n * (the response message) label rows the same way server-rendered ones (email-team) do. `optionLabels`\n * is omitted: pair with values already formatted via `buildRecallResolver`, not raw stored values.\n */\nexport const descriptorsFor = (fields: FormFieldInstance[]): SubmissionDescriptor[] =>\n\tfields.filter(isNamedField).map((field) => ({\n\t\tfield: field.name,\n\t\tlabel: field.label ?? field.name,\n\t\tfieldType: field.blockType,\n\t}))\n"],"mappings":";;;AASA,MAAa,mBAAmB,UAAiE;CAChG,MAAM,UAAU,MAAM;CACtB,IAAI,CAAC,MAAM,QAAQ,OAAO,GACzB;CAED,MAAM,MAA8B,CAAC;CACrC,KAAK,MAAM,UAAU,SACpB,IAAI,UAAU,OAAO,OAAO,UAAU,UACrC,IAAI,OAAO,SAAS,OAAO,SAAS,OAAO;CAG7C,OAAO,OAAO,KAAK,GAAG,EAAE,SAAS,IAAI,MAAM,KAAA;AAC5C;;;;;;AAOA,MAAa,uBAAuB,SAMd;CACrB,MAAM,SAAS,IAAI,IAAI,KAAK,OAAO,OAAO,YAAY,EAAE,KAAK,UAAU,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;CAC3F,QAAQ,SAAS;EAChB,MAAM,QAAQ,OAAO,IAAI,IAAI;EAC7B,IAAI,CAAC,OACJ,OAAO;EAER,MAAM,QAAQ,KAAK,OAAO;EAC1B,IAAI,SAAS,QAAQ,UAAU,MAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,GAC9E,OAAO;EAER,MAAM,aAAa,KAAK,SAAS,IAAI,MAAM,SAAS;EACpD,IAAI,YAAY,QACf,OAAO,WAAW,OAAO;GACxB;GACA,QAAQ,CAAC;GACT,cAAc,gBAAgB,KAAK;GACnC,QAAQ,KAAK;GACb,GAAG,KAAK;EACT,CAAC;EAEF,OAAO,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,KAAK;CAC9D;AACD;;;;;;;AAQA,MAAa,kBAAkB,WAC9B,OAAO,OAAO,YAAY,EAAE,KAAK,WAAW;CAC3C,OAAO,MAAM;CACb,OAAO,MAAM,SAAS,MAAM;CAC5B,WAAW,MAAM;AAClB,EAAE"}
1
+ {"version":3,"file":"resolver.js","names":[],"sources":["../../src/recall/resolver.ts"],"sourcesContent":["import { isNamedField } from '../fields/fieldKey'\nimport type { AnyFormFieldDefinition } from '../fields/types'\nimport type { FormFieldInstance, SubmissionDescriptor } from '../submissions/types'\n\nexport type RecallResolver = (name: string) => string\n\ntype Option = { label?: string; value?: string }\n\n/** Derive a value->label map from a select-like field instance's `options`. */\nexport const optionLabelsFor = (field: FormFieldInstance): Record<string, string> | undefined => {\n\tconst options = field.options\n\tif (!Array.isArray(options)) {\n\t\treturn undefined\n\t}\n\tconst map: Record<string, string> = {}\n\tfor (const option of options as Option[]) {\n\t\tif (option && typeof option.value === 'string') {\n\t\t\tmap[option.value] = option.label?.trim() ? option.label : option.value\n\t\t}\n\t}\n\treturn Object.keys(map).length > 0 ? map : undefined\n}\n\n/**\n * Build a recall resolver: `{{name}}` -> the field's current value, formatted via its type's `format`\n * (so a select shows its option label, a checkbox Yes/No, a date localized). Missing field or empty\n * value -> ''. Pure and isomorphic; the renderer and server templates share it.\n */\nexport const buildRecallResolver = (args: {\n\tfields: FormFieldInstance[]\n\tvalues: Record<string, unknown>\n\tregistry: Map<string, AnyFormFieldDefinition>\n\tlocale: string\n\tt: (key: string) => string\n}): RecallResolver => {\n\tconst byName = new Map(args.fields.filter(isNamedField).map((field) => [field.name, field]))\n\treturn (name) => {\n\t\tconst field = byName.get(name)\n\t\tif (!field) {\n\t\t\treturn ''\n\t\t}\n\t\tconst value = args.values[name]\n\t\tif (value == null || value === '' || (Array.isArray(value) && value.length === 0)) {\n\t\t\treturn ''\n\t\t}\n\t\tconst definition = args.registry.get(field.blockType)\n\t\tif (definition?.format) {\n\t\t\treturn definition.format({\n\t\t\t\tvalue,\n\t\t\t\tconfig: {},\n\t\t\t\toptionLabels: optionLabelsFor(field),\n\t\t\t\tlocale: args.locale,\n\t\t\t\tt: args.t,\n\t\t\t})\n\t\t}\n\t\treturn Array.isArray(value) ? value.join(', ') : String(value)\n\t}\n}\n\n/**\n * Build a submission-shaped descriptor per named field: the client-side counterpart to the\n * per-field descriptors `runSubmission` snapshots server-side. Lets client-rendered templates\n * (the response message) label rows the same way server-rendered ones (email-team) do. `optionLabels`\n * is omitted: pair with values already formatted via `buildRecallResolver`, not raw stored values.\n */\nexport const descriptorsFor = (fields: FormFieldInstance[]): SubmissionDescriptor[] =>\n\tfields.filter(isNamedField).map((field) => ({\n\t\tfield: field.name,\n\t\tlabel: field.label ?? field.name,\n\t\tfieldType: field.blockType,\n\t}))\n"],"mappings":";;;AASA,MAAa,mBAAmB,UAAiE;CAChG,MAAM,UAAU,MAAM;CACtB,IAAI,CAAC,MAAM,QAAQ,OAAO,GACzB;CAED,MAAM,MAA8B,CAAC;CACrC,KAAK,MAAM,UAAU,SACpB,IAAI,UAAU,OAAO,OAAO,UAAU,UACrC,IAAI,OAAO,SAAS,OAAO,OAAO,KAAK,IAAI,OAAO,QAAQ,OAAO;CAGnE,OAAO,OAAO,KAAK,GAAG,EAAE,SAAS,IAAI,MAAM,KAAA;AAC5C;;;;;;AAOA,MAAa,uBAAuB,SAMd;CACrB,MAAM,SAAS,IAAI,IAAI,KAAK,OAAO,OAAO,YAAY,EAAE,KAAK,UAAU,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;CAC3F,QAAQ,SAAS;EAChB,MAAM,QAAQ,OAAO,IAAI,IAAI;EAC7B,IAAI,CAAC,OACJ,OAAO;EAER,MAAM,QAAQ,KAAK,OAAO;EAC1B,IAAI,SAAS,QAAQ,UAAU,MAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,GAC9E,OAAO;EAER,MAAM,aAAa,KAAK,SAAS,IAAI,MAAM,SAAS;EACpD,IAAI,YAAY,QACf,OAAO,WAAW,OAAO;GACxB;GACA,QAAQ,CAAC;GACT,cAAc,gBAAgB,KAAK;GACnC,QAAQ,KAAK;GACb,GAAG,KAAK;EACT,CAAC;EAEF,OAAO,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,KAAK;CAC9D;AACD;;;;;;;AAQA,MAAa,kBAAkB,WAC9B,OAAO,OAAO,YAAY,EAAE,KAAK,WAAW;CAC3C,OAAO,MAAM;CACb,OAAO,MAAM,SAAS,MAAM;CAC5B,WAAW,MAAM;AAClB,EAAE"}
@@ -0,0 +1,13 @@
1
+ //#region src/submissions/formIdOf.ts
2
+ /** Narrow a submission's `form` relationship value (raw id or a populated relationship doc) to its id. */
3
+ const formIdOf = (form) => {
4
+ if (typeof form === "number" || typeof form === "string") return form;
5
+ if (form && typeof form === "object" && "id" in form) {
6
+ const id = form.id;
7
+ if (typeof id === "number" || typeof id === "string") return id;
8
+ }
9
+ };
10
+ //#endregion
11
+ export { formIdOf };
12
+
13
+ //# sourceMappingURL=formIdOf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formIdOf.js","names":[],"sources":["../../src/submissions/formIdOf.ts"],"sourcesContent":["/** Narrow a submission's `form` relationship value (raw id or a populated relationship doc) to its id. */\nexport const formIdOf = (form: unknown): number | string | undefined => {\n\tif (typeof form === 'number' || typeof form === 'string') {\n\t\treturn form\n\t}\n\tif (form && typeof form === 'object' && 'id' in form) {\n\t\tconst id = (form as { id: unknown }).id\n\t\tif (typeof id === 'number' || typeof id === 'string') {\n\t\t\treturn id\n\t\t}\n\t}\n\treturn undefined\n}\n"],"mappings":";;AACA,MAAa,YAAY,SAA+C;CACvE,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAC/C,OAAO;CAER,IAAI,QAAQ,OAAO,SAAS,YAAY,QAAQ,MAAM;EACrD,MAAM,KAAM,KAAyB;EACrC,IAAI,OAAO,OAAO,YAAY,OAAO,OAAO,UAC3C,OAAO;CAET;AAED"}
@@ -48,7 +48,7 @@ const optionLabelsFor = (instance) => {
48
48
  const options = instance.options;
49
49
  if (!Array.isArray(options)) return;
50
50
  const map = {};
51
- for (const option of options) if (typeof option?.value === "string") map[option.value] = option.label ?? option.value;
51
+ for (const option of options) if (typeof option?.value === "string") map[option.value] = option.label?.trim() ? option.label : option.value;
52
52
  return Object.keys(map).length > 0 ? map : void 0;
53
53
  };
54
54
  /**
@@ -65,7 +65,7 @@ const optionLabelsFor = (instance) => {
65
65
  * validated, never stored, and a client-sent value under their name is dropped.
66
66
  */
67
67
  const runSubmission = async (input) => {
68
- const { fields, values, registry, ruleRegistry, consentEntries, consentSnapshot, locale, t, operation, req, payload, formId, uploadSlug, expectedOwner, strictUploadOwnership } = input;
68
+ const { fields, values, registry, ruleRegistry, consentEntries, consentSnapshot, locale, t, operation, req, payload, formId, uploadSlug, expectedOwner, strictUploadOwnership, calcResolved } = input;
69
69
  const incoming = new Map(values.map((entry) => [entry.field, entry.value]));
70
70
  const coercedAnswers = {};
71
71
  const coercedByName = /* @__PURE__ */ new Map();
@@ -79,7 +79,7 @@ const runSubmission = async (input) => {
79
79
  coercedAnswers[instance.name] = value;
80
80
  coercedByName.set(instance.name, value);
81
81
  }
82
- const effective = computeCalcFields(fields, coercedAnswers);
82
+ const effective = computeCalcFields(fields, coercedAnswers, calcResolved);
83
83
  const errors = [];
84
84
  const outValues = [];
85
85
  const descriptors = [];
@@ -1 +1 @@
1
- {"version":3,"file":"runSubmission.js","names":[],"sources":["../../src/submissions/runSubmission.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { calcExpressionOf, computeCalcFields } from '../calc/computeCalcFields'\nimport { evaluateCondition } from '../conditions/evaluate'\nimport type { ConsentProof, ConsentSnapshotMode } from '../consent/captureConsent'\nimport { captureConsent } from '../consent/captureConsent'\nimport type { ConsentSourceEntry } from '../consent/types'\nimport { isNamedField } from '../fields/fieldKey'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport type { Translate } from '../fields/types'\nimport { keys } from '../translations/keys'\nimport { captureFileRef } from '../uploads/captureFileRef'\nimport type { FileFieldConfig, FileRefError } from '../uploads/types'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport { runValidation } from '../validation/runValidation'\nimport { isTruthyString } from './coerceBoolean'\nimport type {\n\tFormFieldInstance,\n\tSubmissionDescriptor,\n\tSubmissionFieldError,\n\tSubmissionValue,\n\tSubmissionWidth,\n} from './types'\n\nconst SUBMISSION_WIDTHS: ReadonlySet<string> = new Set<SubmissionWidth>([\n\t'full',\n\t'half',\n\t'third',\n\t'twoThirds',\n])\n\n/** The field instance's authored layout width, or undefined if unset/unrecognized (renders full). */\nexport const widthOf = (instance: FormFieldInstance): SubmissionWidth | undefined =>\n\ttypeof instance.width === 'string' && SUBMISSION_WIDTHS.has(instance.width)\n\t\t? (instance.width as SubmissionWidth)\n\t\t: undefined\n\nconst errorKeyFor = (code: FileRefError): string => {\n\tif (code === 'mimeType') {\n\t\treturn keys.validationFileMimeType\n\t}\n\tif (code === 'tooLarge') {\n\t\treturn keys.validationFileTooLarge\n\t}\n\treturn keys.validationFileMissing\n}\n\n/** Normalize the authored `mimeTypes` (a `hasMany` text field) to a `string[]`. */\nconst mimeTypesOf = (raw: unknown): string[] | undefined => {\n\tif (!Array.isArray(raw)) {\n\t\treturn undefined\n\t}\n\tconst out = (raw as unknown[]).filter((entry): entry is string => typeof entry === 'string')\n\treturn out.length > 0 ? out : undefined\n}\n\nconst fileFieldConfigOf = (instance: FormFieldInstance): FileFieldConfig => ({\n\tmimeTypes: mimeTypesOf(instance.mimeTypes),\n\tmaxSize: typeof instance.maxSize === 'number' ? instance.maxSize : undefined,\n})\n\nconst isEmpty = (value: unknown): boolean =>\n\tvalue == null || value === '' || (Array.isArray(value) && value.length === 0)\n\nconst coerce = (kind: string, value: unknown): unknown => {\n\tif (value == null) {\n\t\treturn value\n\t}\n\tif (kind === 'number') {\n\t\tconst next = typeof value === 'number' ? value : Number(value)\n\t\treturn Number.isNaN(next) ? value : next\n\t}\n\tif (kind === 'boolean') {\n\t\t// A genuine boolean from the renderer passes through; a raw client string is parsed against the\n\t\t// shared truthy allow-list (also used by prefill), so only affirmative tokens read as true. The\n\t\t// server is the trust boundary for consent: a stray string must never count as agreement.\n\t\tif (typeof value === 'string') {\n\t\t\treturn isTruthyString(value)\n\t\t}\n\t\treturn Boolean(value)\n\t}\n\tif (kind === 'text') {\n\t\treturn typeof value === 'string' ? value : String(value)\n\t}\n\treturn value\n}\n\nconst optionLabelsFor = (instance: FormFieldInstance): Record<string, string> | undefined => {\n\tconst options = instance.options\n\tif (!Array.isArray(options)) {\n\t\treturn undefined\n\t}\n\tconst map: Record<string, string> = {}\n\tfor (const option of options as Array<{ label?: string; value?: string }>) {\n\t\tif (typeof option?.value === 'string') {\n\t\t\tmap[option.value] = option.label ?? option.value\n\t\t}\n\t}\n\treturn Object.keys(map).length > 0 ? map : undefined\n}\n\nexport type ConsentProofEntry = { field: string } & ConsentProof\n\nexport type RunSubmissionInput = {\n\tfields: FormFieldInstance[]\n\tvalues: SubmissionValue[]\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\t/**\n\t * The host's consent sources, resolved once by the caller (which owns the request-scoped\n\t * resolver and how a failure surfaces) and read here to build each consent proof.\n\t */\n\tconsentEntries?: ConsentSourceEntry[]\n\t/** What each consent proof snapshots of the agreed wording (plugin option `consent.snapshot`). */\n\tconsentSnapshot?: ConsentSnapshotMode\n\tlocale: string\n\tt: Translate\n\toperation: 'create' | 'update'\n\treq?: PayloadRequest\n\tpayload?: Payload\n\tformId?: number | string\n\t/**\n\t * The plugin-configured uploads collection slug. The block's stamped `collection` is for the\n\t * client only; the server resolves the slug from plugin config and fails closed without one.\n\t */\n\tuploadSlug?: string\n\t/** Resolved request identity, verified against an upload's `owner` stamp when a file field is captured. */\n\texpectedOwner?: string\n\t/** Plugin `spam.uploadOwnership: 'strict'`: reject an owned upload when the submitter is unidentifiable. */\n\tstrictUploadOwnership?: boolean\n}\n\nexport type RunSubmissionResult = {\n\terrors: SubmissionFieldError[]\n\tvalues: SubmissionValue[]\n\tdescriptors: SubmissionDescriptor[]\n\tconsent: ConsentProofEntry[]\n}\n\n/**\n * Pure submission core, two-pass: first coerce every answered field to its typed kind (so cross-field\n * rules see coerced siblings), then validate each field through `runValidation` (required, intrinsic\n * facet, declarative rules), snapshotting a localized descriptor per answered field. Calc fields are\n * the trust boundary: their client-sent values are never seeded, their values are derived from their\n * expressions over the coerced answers, and those derived values are authoritative everywhere downstream\n * (conditions, validation, storage). Conditions gate the second pass against these effective answers: a\n * field whose `visibleWhen` is false is skipped entirely (never validated, never stored, so a client-sent\n * value for it is ignored), and a visible field whose `validateWhen` is false stores its value but skips\n * validation. A visible calc field stores its derived value and is never validated. Display-only field\n * types (value kind 'none', e.g. message) and nameless (bare) rows are skipped in both passes: never\n * validated, never stored, and a client-sent value under their name is dropped.\n */\nexport const runSubmission = async (input: RunSubmissionInput): Promise<RunSubmissionResult> => {\n\tconst {\n\t\tfields,\n\t\tvalues,\n\t\tregistry,\n\t\truleRegistry,\n\t\tconsentEntries,\n\t\tconsentSnapshot,\n\t\tlocale,\n\t\tt,\n\t\toperation,\n\t\treq,\n\t\tpayload,\n\t\tformId,\n\t\tuploadSlug,\n\t\texpectedOwner,\n\t\tstrictUploadOwnership,\n\t} = input\n\tconst incoming = new Map(values.map((entry) => [entry.field, entry.value]))\n\n\tconst coercedAnswers: Record<string, unknown> = {}\n\tconst coercedByName = new Map<string, unknown>()\n\tfor (const instance of fields) {\n\t\tconst definition = registry.get(instance.blockType)\n\t\t// Never seed a calc field's client value: its value is derived below, so the client cannot influence it (even for a self-referencing expression).\n\t\t// A display-only ('none' kind) field carries no value at all, so a client-sent value under its name is dropped here.\n\t\t// A nameless (bare) row has no key to read a value under.\n\t\tif (\n\t\t\t!definition ||\n\t\t\tdefinition.value === 'none' ||\n\t\t\tcalcExpressionOf(instance) ||\n\t\t\t!isNamedField(instance)\n\t\t) {\n\t\t\tcontinue\n\t\t}\n\t\tconst raw = incoming.get(instance.name)\n\t\t// A consent field's \"not agreed\" state is semantically meaningful: treat a missing value as\n\t\t// `false` so the intrinsic validate can enforce required-agreement (not optional = must be true).\n\t\t// A repeater with no rows is coerced to [] so validate() can check minRows. The empty-guard\n\t\t// below must not skip repeaters: isEmpty([]) is true, but [] still needs to reach validate()\n\t\t// so a minRows > 0 constraint correctly rejects a zero-row submission.\n\t\tconst effectiveRaw =\n\t\t\tinstance.blockType === 'consent' && isEmpty(raw)\n\t\t\t\t? false\n\t\t\t\t: instance.blockType === 'repeater' && isEmpty(raw)\n\t\t\t\t\t? []\n\t\t\t\t\t: raw\n\t\tif (isEmpty(effectiveRaw) && instance.blockType !== 'repeater') {\n\t\t\tcontinue\n\t\t}\n\t\tconst value = coerce(definition.value, effectiveRaw)\n\t\tcoercedAnswers[instance.name] = value\n\t\tcoercedByName.set(instance.name, value)\n\t}\n\n\t// Calc values are authoritative everywhere downstream (conditions, validation, storage), never the client-sent value.\n\tconst effective = computeCalcFields(fields, coercedAnswers)\n\n\tconst errors: SubmissionFieldError[] = []\n\tconst outValues: SubmissionValue[] = []\n\tconst descriptors: SubmissionDescriptor[] = []\n\tconst consentProofs: ConsentProofEntry[] = []\n\tconst now = new Date().toISOString()\n\n\tfor (const instance of fields) {\n\t\tconst definition = registry.get(instance.blockType)\n\t\t// A 'none'-kind (display-only) field is never validated and never stored: no value, no descriptor.\n\t\t// A nameless (bare) row has no key to store under, so it is skipped the same way.\n\t\tif (!definition || definition.value === 'none' || !isNamedField(instance)) {\n\t\t\tcontinue\n\t\t}\n\t\tconst raw = incoming.get(instance.name)\n\t\tconst value = coercedByName.has(instance.name) ? coercedByName.get(instance.name) : raw\n\n\t\tif (!evaluateCondition(instance.visibleWhen, effective)) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// A calc field's value is server-derived and always valid: skip validation and the client value entirely, storing the computed result.\n\t\tif (calcExpressionOf(instance)) {\n\t\t\toutValues.push({ field: instance.name, value: effective[instance.name] })\n\t\t\tdescriptors.push({\n\t\t\t\tfield: instance.name,\n\t\t\t\tlabel: instance.label ?? instance.name,\n\t\t\t\tfieldType: instance.blockType,\n\t\t\t\t...(widthOf(instance) ? { width: widthOf(instance) } : {}),\n\t\t\t})\n\t\t\tcontinue\n\t\t}\n\n\t\tif (evaluateCondition(instance.validateWhen, effective)) {\n\t\t\tconst { errors: issues } = await runValidation({\n\t\t\t\tfield: instance,\n\t\t\t\tfieldDefinition: definition,\n\t\t\t\tvalue,\n\t\t\t\tfieldType: instance.blockType,\n\t\t\t\truleRegistry,\n\t\t\t\tanswers: effective,\n\t\t\t\tlocale,\n\t\t\t\tt,\n\t\t\t\toperation,\n\t\t\t\tevent: 'submit',\n\t\t\t\tmode: 'server',\n\t\t\t\treq,\n\t\t\t\tpayload,\n\t\t\t\tformId,\n\t\t\t})\n\t\t\tif (issues.length > 0) {\n\t\t\t\tfor (const message of issues) {\n\t\t\t\t\terrors.push({ path: instance.name, message })\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\t// Gate on the field-type definition's value kind, not the blockType literal, so a custom\n\t\t// type registered with value:'file' gets the same server-side capture and enforcement as\n\t\t// the built-in file field (a blockType check would leave it storing the raw client id).\n\t\tif (definition.value === 'file') {\n\t\t\tif (isEmpty(value)) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif (payload) {\n\t\t\t\tif (!uploadSlug) {\n\t\t\t\t\terrors.push({ path: instance.name, message: t(errorKeyFor('missing')) })\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst fileConfig = fileFieldConfigOf(instance)\n\t\t\t\tconst captured = await captureFileRef({\n\t\t\t\t\tpayload,\n\t\t\t\t\tcollectionSlug: uploadSlug,\n\t\t\t\t\tuploadId: value as string | number,\n\t\t\t\t\tconfig: fileConfig,\n\t\t\t\t\treq,\n\t\t\t\t\texpectedOwner,\n\t\t\t\t\tstrict: strictUploadOwnership,\n\t\t\t\t})\n\t\t\t\tif (!captured.ok) {\n\t\t\t\t\terrors.push({ path: instance.name, message: t(errorKeyFor(captured.code)) })\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\toutValues.push({ field: instance.name, value: captured.ref })\n\t\t\t\tdescriptors.push({\n\t\t\t\t\tfield: instance.name,\n\t\t\t\t\tlabel: instance.label ?? instance.name,\n\t\t\t\t\tfieldType: instance.blockType,\n\t\t\t\t\t...(widthOf(instance) ? { width: widthOf(instance) } : {}),\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\toutValues.push({ field: instance.name, value })\n\t\t\tdescriptors.push({\n\t\t\t\tfield: instance.name,\n\t\t\t\tlabel: instance.label ?? instance.name,\n\t\t\t\tfieldType: instance.blockType,\n\t\t\t\t...(widthOf(instance) ? { width: widthOf(instance) } : {}),\n\t\t\t})\n\t\t\tcontinue\n\t\t}\n\n\t\tif (instance.blockType === 'consent' && payload) {\n\t\t\tconst proof = await captureConsent({\n\t\t\t\tfield: instance,\n\t\t\t\tagreed: value === true,\n\t\t\t\tentries: consentEntries ?? [],\n\t\t\t\tpayload,\n\t\t\t\treq,\n\t\t\t\tnow,\n\t\t\t\tsnapshot: consentSnapshot,\n\t\t\t})\n\t\t\tconsentProofs.push({ field: instance.name, ...proof })\n\t\t\tcontinue\n\t\t}\n\n\t\tif (instance.blockType === 'repeater') {\n\t\t\tconst rows = Array.isArray(value) ? (value as Array<Record<string, unknown>>) : []\n\t\t\t// Nameless sub-rows are dropped like top-level ones (bare blocks are excluded from the\n\t\t\t// repeater's subFields config, so any encountered here is stray data).\n\t\t\tconst subFields = (\n\t\t\t\tArray.isArray(instance.subFields) ? (instance.subFields as FormFieldInstance[]) : []\n\t\t\t).filter(isNamedField)\n\n\t\t\t// Per-row sub-field processing. Validation is gated by the sub-field's visibleWhen and\n\t\t\t// validateWhen against the row's own values; errors carry the path\n\t\t\t// fieldName[rowIndex].subFieldName so the client maps them to the right input. File\n\t\t\t// sub-fields additionally cross the server trust boundary: they are captured\n\t\t\t// unconditionally (a crafted request could carry an id for a conditionally-hidden field),\n\t\t\t// so a stored row never holds a raw, unenforced upload id.\n\t\t\tfor (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {\n\t\t\t\tconst row = rows[rowIndex] ?? {}\n\t\t\t\tfor (const subField of subFields) {\n\t\t\t\t\tconst subDef = registry.get(subField.blockType)\n\t\t\t\t\tif (!subDef) continue\n\t\t\t\t\t// Display-only ('none' kind) sub-fields mirror the top-level skip: never validated,\n\t\t\t\t\t// never captured, and stripped from the stored row below so a client-injected value\n\t\t\t\t\t// under their name cannot ride along.\n\t\t\t\t\tif (subDef.value === 'none') continue\n\t\t\t\t\tconst subPath = `${instance.name}[${rowIndex}].${subField.name}`\n\t\t\t\t\tif (\n\t\t\t\t\t\tevaluateCondition(subField.visibleWhen, row) &&\n\t\t\t\t\t\tevaluateCondition(subField.validateWhen, row)\n\t\t\t\t\t) {\n\t\t\t\t\t\tconst { errors: subErrors } = await runValidation({\n\t\t\t\t\t\t\tfield: subField,\n\t\t\t\t\t\t\tfieldDefinition: subDef,\n\t\t\t\t\t\t\tvalue: row[subField.name],\n\t\t\t\t\t\t\tfieldType: subField.blockType,\n\t\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\t\tanswers: row,\n\t\t\t\t\t\t\tlocale,\n\t\t\t\t\t\t\tt,\n\t\t\t\t\t\t\toperation,\n\t\t\t\t\t\t\tevent: 'submit',\n\t\t\t\t\t\t\tmode: 'server',\n\t\t\t\t\t\t\treq,\n\t\t\t\t\t\t\tpayload,\n\t\t\t\t\t\t\tformId,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tfor (const message of subErrors) {\n\t\t\t\t\t\t\terrors.push({ path: subPath, message })\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Mirror the top-level file capture: the client submits only an upload id, so\n\t\t\t\t\t// re-derive filename/mimeType/filesize from the stored doc and enforce\n\t\t\t\t\t// owner/mime/size against the sub-field's config, failing closed on a missing\n\t\t\t\t\t// uploads slug or any capture rejection. The captured FileRef replaces the raw\n\t\t\t\t\t// id in the row. Without a payload (pure unit context) the value stays verbatim,\n\t\t\t\t\t// exactly like the top-level path.\n\t\t\t\t\tif (subDef.value === 'file' && payload) {\n\t\t\t\t\t\tconst subValue = row[subField.name]\n\t\t\t\t\t\tif (isEmpty(subValue)) {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!uploadSlug) {\n\t\t\t\t\t\t\terrors.push({ path: subPath, message: t(errorKeyFor('missing')) })\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst captured = await captureFileRef({\n\t\t\t\t\t\t\tpayload,\n\t\t\t\t\t\t\tcollectionSlug: uploadSlug,\n\t\t\t\t\t\t\tuploadId: subValue as string | number,\n\t\t\t\t\t\t\tconfig: fileFieldConfigOf(subField),\n\t\t\t\t\t\t\treq,\n\t\t\t\t\t\t\texpectedOwner,\n\t\t\t\t\t\t\tstrict: strictUploadOwnership,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tif (!captured.ok) {\n\t\t\t\t\t\t\terrors.push({ path: subPath, message: t(errorKeyFor(captured.code)) })\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\trow[subField.name] = captured.ref\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (errors.length > 0) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tconst subFieldDescriptors: SubmissionDescriptor[] = subFields.map((sf) => ({\n\t\t\t\tfield: sf.name,\n\t\t\t\tlabel: sf.label ?? sf.name,\n\t\t\t\tfieldType: sf.blockType,\n\t\t\t\t...((sf.options as unknown) ? { optionLabels: optionLabelsFor(sf) ?? undefined } : {}),\n\t\t\t}))\n\n\t\t\t// Strip display-only sub-fields from the stored rows: like top-level 'none' fields they\n\t\t\t// carry no answer, so a client-injected value under their name must never persist.\n\t\t\tconst displayOnly = new Set(\n\t\t\t\tsubFields.filter((sf) => registry.get(sf.blockType)?.value === 'none').map((sf) => sf.name)\n\t\t\t)\n\t\t\tconst storedRows =\n\t\t\t\tdisplayOnly.size === 0\n\t\t\t\t\t? rows\n\t\t\t\t\t: rows.map((row) =>\n\t\t\t\t\t\t\tObject.fromEntries(Object.entries(row).filter(([key]) => !displayOnly.has(key)))\n\t\t\t\t\t\t)\n\n\t\t\toutValues.push({ field: instance.name, value: storedRows })\n\t\t\tdescriptors.push({\n\t\t\t\tfield: instance.name,\n\t\t\t\tlabel: instance.label ?? instance.name,\n\t\t\t\tfieldType: instance.blockType,\n\t\t\t\t...(widthOf(instance) ? { width: widthOf(instance) } : {}),\n\t\t\t\t...(subFieldDescriptors.length > 0 ? { subFieldDescriptors } : {}),\n\t\t\t})\n\t\t\tcontinue\n\t\t}\n\n\t\tif (isEmpty(raw)) {\n\t\t\tcontinue\n\t\t}\n\n\t\toutValues.push({ field: instance.name, value })\n\t\tconst optionLabels = optionLabelsFor(instance)\n\t\tdescriptors.push({\n\t\t\tfield: instance.name,\n\t\t\tlabel: instance.label ?? instance.name,\n\t\t\tfieldType: instance.blockType,\n\t\t\t...(widthOf(instance) ? { width: widthOf(instance) } : {}),\n\t\t\t...(optionLabels ? { optionLabels } : {}),\n\t\t})\n\t}\n\n\treturn { errors, values: outValues, descriptors, consent: consentProofs }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"runSubmission.js","names":[],"sources":["../../src/submissions/runSubmission.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { calcExpressionOf, computeCalcFields } from '../calc/computeCalcFields'\nimport type { CalcResolved } from '../calc/evaluate'\nimport { evaluateCondition } from '../conditions/evaluate'\nimport type { ConsentProof, ConsentSnapshotMode } from '../consent/captureConsent'\nimport { captureConsent } from '../consent/captureConsent'\nimport type { ConsentSourceEntry } from '../consent/types'\nimport { isNamedField } from '../fields/fieldKey'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport type { Translate } from '../fields/types'\nimport { keys } from '../translations/keys'\nimport { captureFileRef } from '../uploads/captureFileRef'\nimport type { FileFieldConfig, FileRefError } from '../uploads/types'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport { runValidation } from '../validation/runValidation'\nimport { isTruthyString } from './coerceBoolean'\nimport type {\n\tFormFieldInstance,\n\tSubmissionDescriptor,\n\tSubmissionFieldError,\n\tSubmissionValue,\n\tSubmissionWidth,\n} from './types'\n\nconst SUBMISSION_WIDTHS: ReadonlySet<string> = new Set<SubmissionWidth>([\n\t'full',\n\t'half',\n\t'third',\n\t'twoThirds',\n])\n\n/** The field instance's authored layout width, or undefined if unset/unrecognized (renders full). */\nexport const widthOf = (instance: FormFieldInstance): SubmissionWidth | undefined =>\n\ttypeof instance.width === 'string' && SUBMISSION_WIDTHS.has(instance.width)\n\t\t? (instance.width as SubmissionWidth)\n\t\t: undefined\n\nconst errorKeyFor = (code: FileRefError): string => {\n\tif (code === 'mimeType') {\n\t\treturn keys.validationFileMimeType\n\t}\n\tif (code === 'tooLarge') {\n\t\treturn keys.validationFileTooLarge\n\t}\n\treturn keys.validationFileMissing\n}\n\n/** Normalize the authored `mimeTypes` (a `hasMany` text field) to a `string[]`. */\nconst mimeTypesOf = (raw: unknown): string[] | undefined => {\n\tif (!Array.isArray(raw)) {\n\t\treturn undefined\n\t}\n\tconst out = (raw as unknown[]).filter((entry): entry is string => typeof entry === 'string')\n\treturn out.length > 0 ? out : undefined\n}\n\nconst fileFieldConfigOf = (instance: FormFieldInstance): FileFieldConfig => ({\n\tmimeTypes: mimeTypesOf(instance.mimeTypes),\n\tmaxSize: typeof instance.maxSize === 'number' ? instance.maxSize : undefined,\n})\n\nconst isEmpty = (value: unknown): boolean =>\n\tvalue == null || value === '' || (Array.isArray(value) && value.length === 0)\n\nconst coerce = (kind: string, value: unknown): unknown => {\n\tif (value == null) {\n\t\treturn value\n\t}\n\tif (kind === 'number') {\n\t\tconst next = typeof value === 'number' ? value : Number(value)\n\t\treturn Number.isNaN(next) ? value : next\n\t}\n\tif (kind === 'boolean') {\n\t\t// A genuine boolean from the renderer passes through; a raw client string is parsed against the\n\t\t// shared truthy allow-list (also used by prefill), so only affirmative tokens read as true. The\n\t\t// server is the trust boundary for consent: a stray string must never count as agreement.\n\t\tif (typeof value === 'string') {\n\t\t\treturn isTruthyString(value)\n\t\t}\n\t\treturn Boolean(value)\n\t}\n\tif (kind === 'text') {\n\t\treturn typeof value === 'string' ? value : String(value)\n\t}\n\treturn value\n}\n\nconst optionLabelsFor = (instance: FormFieldInstance): Record<string, string> | undefined => {\n\tconst options = instance.options\n\tif (!Array.isArray(options)) {\n\t\treturn undefined\n\t}\n\tconst map: Record<string, string> = {}\n\tfor (const option of options as Array<{ label?: string; value?: string }>) {\n\t\tif (typeof option?.value === 'string') {\n\t\t\tmap[option.value] = option.label?.trim() ? option.label : option.value\n\t\t}\n\t}\n\treturn Object.keys(map).length > 0 ? map : undefined\n}\n\nexport type ConsentProofEntry = { field: string } & ConsentProof\n\nexport type RunSubmissionInput = {\n\tfields: FormFieldInstance[]\n\tvalues: SubmissionValue[]\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\t/**\n\t * The host's consent sources, resolved once by the caller (which owns the request-scoped\n\t * resolver and how a failure surfaces) and read here to build each consent proof.\n\t */\n\tconsentEntries?: ConsentSourceEntry[]\n\t/** What each consent proof snapshots of the agreed wording (plugin option `consent.snapshot`). */\n\tconsentSnapshot?: ConsentSnapshotMode\n\tlocale: string\n\tt: Translate\n\toperation: 'create' | 'update'\n\treq?: PayloadRequest\n\tpayload?: Payload\n\tformId?: number | string\n\t/**\n\t * The plugin-configured uploads collection slug. The block's stamped `collection` is for the\n\t * client only; the server resolves the slug from plugin config and fails closed without one.\n\t */\n\tuploadSlug?: string\n\t/** Resolved request identity, verified against an upload's `owner` stamp when a file field is captured. */\n\texpectedOwner?: string\n\t/** Plugin `spam.uploadOwnership: 'strict'`: reject an owned upload when the submitter is unidentifiable. */\n\tstrictUploadOwnership?: boolean\n\t/** Server-resolved calc context (sources/weights/functions); absent, extension nodes evaluate to 0. */\n\tcalcResolved?: CalcResolved\n}\n\nexport type RunSubmissionResult = {\n\terrors: SubmissionFieldError[]\n\tvalues: SubmissionValue[]\n\tdescriptors: SubmissionDescriptor[]\n\tconsent: ConsentProofEntry[]\n}\n\n/**\n * Pure submission core, two-pass: first coerce every answered field to its typed kind (so cross-field\n * rules see coerced siblings), then validate each field through `runValidation` (required, intrinsic\n * facet, declarative rules), snapshotting a localized descriptor per answered field. Calc fields are\n * the trust boundary: their client-sent values are never seeded, their values are derived from their\n * expressions over the coerced answers, and those derived values are authoritative everywhere downstream\n * (conditions, validation, storage). Conditions gate the second pass against these effective answers: a\n * field whose `visibleWhen` is false is skipped entirely (never validated, never stored, so a client-sent\n * value for it is ignored), and a visible field whose `validateWhen` is false stores its value but skips\n * validation. A visible calc field stores its derived value and is never validated. Display-only field\n * types (value kind 'none', e.g. message) and nameless (bare) rows are skipped in both passes: never\n * validated, never stored, and a client-sent value under their name is dropped.\n */\nexport const runSubmission = async (input: RunSubmissionInput): Promise<RunSubmissionResult> => {\n\tconst {\n\t\tfields,\n\t\tvalues,\n\t\tregistry,\n\t\truleRegistry,\n\t\tconsentEntries,\n\t\tconsentSnapshot,\n\t\tlocale,\n\t\tt,\n\t\toperation,\n\t\treq,\n\t\tpayload,\n\t\tformId,\n\t\tuploadSlug,\n\t\texpectedOwner,\n\t\tstrictUploadOwnership,\n\t\tcalcResolved,\n\t} = input\n\tconst incoming = new Map(values.map((entry) => [entry.field, entry.value]))\n\n\tconst coercedAnswers: Record<string, unknown> = {}\n\tconst coercedByName = new Map<string, unknown>()\n\tfor (const instance of fields) {\n\t\tconst definition = registry.get(instance.blockType)\n\t\t// Never seed a calc field's client value: its value is derived below, so the client cannot influence it (even for a self-referencing expression).\n\t\t// A display-only ('none' kind) field carries no value at all, so a client-sent value under its name is dropped here.\n\t\t// A nameless (bare) row has no key to read a value under.\n\t\tif (\n\t\t\t!definition ||\n\t\t\tdefinition.value === 'none' ||\n\t\t\tcalcExpressionOf(instance) ||\n\t\t\t!isNamedField(instance)\n\t\t) {\n\t\t\tcontinue\n\t\t}\n\t\tconst raw = incoming.get(instance.name)\n\t\t// A consent field's \"not agreed\" state is semantically meaningful: treat a missing value as\n\t\t// `false` so the intrinsic validate can enforce required-agreement (not optional = must be true).\n\t\t// A repeater with no rows is coerced to [] so validate() can check minRows. The empty-guard\n\t\t// below must not skip repeaters: isEmpty([]) is true, but [] still needs to reach validate()\n\t\t// so a minRows > 0 constraint correctly rejects a zero-row submission.\n\t\tconst effectiveRaw =\n\t\t\tinstance.blockType === 'consent' && isEmpty(raw)\n\t\t\t\t? false\n\t\t\t\t: instance.blockType === 'repeater' && isEmpty(raw)\n\t\t\t\t\t? []\n\t\t\t\t\t: raw\n\t\tif (isEmpty(effectiveRaw) && instance.blockType !== 'repeater') {\n\t\t\tcontinue\n\t\t}\n\t\tconst value = coerce(definition.value, effectiveRaw)\n\t\tcoercedAnswers[instance.name] = value\n\t\tcoercedByName.set(instance.name, value)\n\t}\n\n\t// Calc values are authoritative everywhere downstream (conditions, validation, storage), never the client-sent value.\n\tconst effective = computeCalcFields(fields, coercedAnswers, calcResolved)\n\n\tconst errors: SubmissionFieldError[] = []\n\tconst outValues: SubmissionValue[] = []\n\tconst descriptors: SubmissionDescriptor[] = []\n\tconst consentProofs: ConsentProofEntry[] = []\n\tconst now = new Date().toISOString()\n\n\tfor (const instance of fields) {\n\t\tconst definition = registry.get(instance.blockType)\n\t\t// A 'none'-kind (display-only) field is never validated and never stored: no value, no descriptor.\n\t\t// A nameless (bare) row has no key to store under, so it is skipped the same way.\n\t\tif (!definition || definition.value === 'none' || !isNamedField(instance)) {\n\t\t\tcontinue\n\t\t}\n\t\tconst raw = incoming.get(instance.name)\n\t\tconst value = coercedByName.has(instance.name) ? coercedByName.get(instance.name) : raw\n\n\t\tif (!evaluateCondition(instance.visibleWhen, effective)) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// A calc field's value is server-derived and always valid: skip validation and the client value entirely, storing the computed result.\n\t\tif (calcExpressionOf(instance)) {\n\t\t\toutValues.push({ field: instance.name, value: effective[instance.name] })\n\t\t\tdescriptors.push({\n\t\t\t\tfield: instance.name,\n\t\t\t\tlabel: instance.label ?? instance.name,\n\t\t\t\tfieldType: instance.blockType,\n\t\t\t\t...(widthOf(instance) ? { width: widthOf(instance) } : {}),\n\t\t\t})\n\t\t\tcontinue\n\t\t}\n\n\t\tif (evaluateCondition(instance.validateWhen, effective)) {\n\t\t\tconst { errors: issues } = await runValidation({\n\t\t\t\tfield: instance,\n\t\t\t\tfieldDefinition: definition,\n\t\t\t\tvalue,\n\t\t\t\tfieldType: instance.blockType,\n\t\t\t\truleRegistry,\n\t\t\t\tanswers: effective,\n\t\t\t\tlocale,\n\t\t\t\tt,\n\t\t\t\toperation,\n\t\t\t\tevent: 'submit',\n\t\t\t\tmode: 'server',\n\t\t\t\treq,\n\t\t\t\tpayload,\n\t\t\t\tformId,\n\t\t\t})\n\t\t\tif (issues.length > 0) {\n\t\t\t\tfor (const message of issues) {\n\t\t\t\t\terrors.push({ path: instance.name, message })\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\t// Gate on the field-type definition's value kind, not the blockType literal, so a custom\n\t\t// type registered with value:'file' gets the same server-side capture and enforcement as\n\t\t// the built-in file field (a blockType check would leave it storing the raw client id).\n\t\tif (definition.value === 'file') {\n\t\t\tif (isEmpty(value)) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif (payload) {\n\t\t\t\tif (!uploadSlug) {\n\t\t\t\t\terrors.push({ path: instance.name, message: t(errorKeyFor('missing')) })\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst fileConfig = fileFieldConfigOf(instance)\n\t\t\t\tconst captured = await captureFileRef({\n\t\t\t\t\tpayload,\n\t\t\t\t\tcollectionSlug: uploadSlug,\n\t\t\t\t\tuploadId: value as string | number,\n\t\t\t\t\tconfig: fileConfig,\n\t\t\t\t\treq,\n\t\t\t\t\texpectedOwner,\n\t\t\t\t\tstrict: strictUploadOwnership,\n\t\t\t\t})\n\t\t\t\tif (!captured.ok) {\n\t\t\t\t\terrors.push({ path: instance.name, message: t(errorKeyFor(captured.code)) })\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\toutValues.push({ field: instance.name, value: captured.ref })\n\t\t\t\tdescriptors.push({\n\t\t\t\t\tfield: instance.name,\n\t\t\t\t\tlabel: instance.label ?? instance.name,\n\t\t\t\t\tfieldType: instance.blockType,\n\t\t\t\t\t...(widthOf(instance) ? { width: widthOf(instance) } : {}),\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\toutValues.push({ field: instance.name, value })\n\t\t\tdescriptors.push({\n\t\t\t\tfield: instance.name,\n\t\t\t\tlabel: instance.label ?? instance.name,\n\t\t\t\tfieldType: instance.blockType,\n\t\t\t\t...(widthOf(instance) ? { width: widthOf(instance) } : {}),\n\t\t\t})\n\t\t\tcontinue\n\t\t}\n\n\t\tif (instance.blockType === 'consent' && payload) {\n\t\t\tconst proof = await captureConsent({\n\t\t\t\tfield: instance,\n\t\t\t\tagreed: value === true,\n\t\t\t\tentries: consentEntries ?? [],\n\t\t\t\tpayload,\n\t\t\t\treq,\n\t\t\t\tnow,\n\t\t\t\tsnapshot: consentSnapshot,\n\t\t\t})\n\t\t\tconsentProofs.push({ field: instance.name, ...proof })\n\t\t\tcontinue\n\t\t}\n\n\t\tif (instance.blockType === 'repeater') {\n\t\t\tconst rows = Array.isArray(value) ? (value as Array<Record<string, unknown>>) : []\n\t\t\t// Nameless sub-rows are dropped like top-level ones (bare blocks are excluded from the\n\t\t\t// repeater's subFields config, so any encountered here is stray data).\n\t\t\tconst subFields = (\n\t\t\t\tArray.isArray(instance.subFields) ? (instance.subFields as FormFieldInstance[]) : []\n\t\t\t).filter(isNamedField)\n\n\t\t\t// Per-row sub-field processing. Validation is gated by the sub-field's visibleWhen and\n\t\t\t// validateWhen against the row's own values; errors carry the path\n\t\t\t// fieldName[rowIndex].subFieldName so the client maps them to the right input. File\n\t\t\t// sub-fields additionally cross the server trust boundary: they are captured\n\t\t\t// unconditionally (a crafted request could carry an id for a conditionally-hidden field),\n\t\t\t// so a stored row never holds a raw, unenforced upload id.\n\t\t\tfor (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {\n\t\t\t\tconst row = rows[rowIndex] ?? {}\n\t\t\t\tfor (const subField of subFields) {\n\t\t\t\t\tconst subDef = registry.get(subField.blockType)\n\t\t\t\t\tif (!subDef) continue\n\t\t\t\t\t// Display-only ('none' kind) sub-fields mirror the top-level skip: never validated,\n\t\t\t\t\t// never captured, and stripped from the stored row below so a client-injected value\n\t\t\t\t\t// under their name cannot ride along.\n\t\t\t\t\tif (subDef.value === 'none') continue\n\t\t\t\t\tconst subPath = `${instance.name}[${rowIndex}].${subField.name}`\n\t\t\t\t\tif (\n\t\t\t\t\t\tevaluateCondition(subField.visibleWhen, row) &&\n\t\t\t\t\t\tevaluateCondition(subField.validateWhen, row)\n\t\t\t\t\t) {\n\t\t\t\t\t\tconst { errors: subErrors } = await runValidation({\n\t\t\t\t\t\t\tfield: subField,\n\t\t\t\t\t\t\tfieldDefinition: subDef,\n\t\t\t\t\t\t\tvalue: row[subField.name],\n\t\t\t\t\t\t\tfieldType: subField.blockType,\n\t\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\t\tanswers: row,\n\t\t\t\t\t\t\tlocale,\n\t\t\t\t\t\t\tt,\n\t\t\t\t\t\t\toperation,\n\t\t\t\t\t\t\tevent: 'submit',\n\t\t\t\t\t\t\tmode: 'server',\n\t\t\t\t\t\t\treq,\n\t\t\t\t\t\t\tpayload,\n\t\t\t\t\t\t\tformId,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tfor (const message of subErrors) {\n\t\t\t\t\t\t\terrors.push({ path: subPath, message })\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Mirror the top-level file capture: the client submits only an upload id, so\n\t\t\t\t\t// re-derive filename/mimeType/filesize from the stored doc and enforce\n\t\t\t\t\t// owner/mime/size against the sub-field's config, failing closed on a missing\n\t\t\t\t\t// uploads slug or any capture rejection. The captured FileRef replaces the raw\n\t\t\t\t\t// id in the row. Without a payload (pure unit context) the value stays verbatim,\n\t\t\t\t\t// exactly like the top-level path.\n\t\t\t\t\tif (subDef.value === 'file' && payload) {\n\t\t\t\t\t\tconst subValue = row[subField.name]\n\t\t\t\t\t\tif (isEmpty(subValue)) {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!uploadSlug) {\n\t\t\t\t\t\t\terrors.push({ path: subPath, message: t(errorKeyFor('missing')) })\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst captured = await captureFileRef({\n\t\t\t\t\t\t\tpayload,\n\t\t\t\t\t\t\tcollectionSlug: uploadSlug,\n\t\t\t\t\t\t\tuploadId: subValue as string | number,\n\t\t\t\t\t\t\tconfig: fileFieldConfigOf(subField),\n\t\t\t\t\t\t\treq,\n\t\t\t\t\t\t\texpectedOwner,\n\t\t\t\t\t\t\tstrict: strictUploadOwnership,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tif (!captured.ok) {\n\t\t\t\t\t\t\terrors.push({ path: subPath, message: t(errorKeyFor(captured.code)) })\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\trow[subField.name] = captured.ref\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (errors.length > 0) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tconst subFieldDescriptors: SubmissionDescriptor[] = subFields.map((sf) => ({\n\t\t\t\tfield: sf.name,\n\t\t\t\tlabel: sf.label ?? sf.name,\n\t\t\t\tfieldType: sf.blockType,\n\t\t\t\t...((sf.options as unknown) ? { optionLabels: optionLabelsFor(sf) ?? undefined } : {}),\n\t\t\t}))\n\n\t\t\t// Strip display-only sub-fields from the stored rows: like top-level 'none' fields they\n\t\t\t// carry no answer, so a client-injected value under their name must never persist.\n\t\t\tconst displayOnly = new Set(\n\t\t\t\tsubFields.filter((sf) => registry.get(sf.blockType)?.value === 'none').map((sf) => sf.name)\n\t\t\t)\n\t\t\tconst storedRows =\n\t\t\t\tdisplayOnly.size === 0\n\t\t\t\t\t? rows\n\t\t\t\t\t: rows.map((row) =>\n\t\t\t\t\t\t\tObject.fromEntries(Object.entries(row).filter(([key]) => !displayOnly.has(key)))\n\t\t\t\t\t\t)\n\n\t\t\toutValues.push({ field: instance.name, value: storedRows })\n\t\t\tdescriptors.push({\n\t\t\t\tfield: instance.name,\n\t\t\t\tlabel: instance.label ?? instance.name,\n\t\t\t\tfieldType: instance.blockType,\n\t\t\t\t...(widthOf(instance) ? { width: widthOf(instance) } : {}),\n\t\t\t\t...(subFieldDescriptors.length > 0 ? { subFieldDescriptors } : {}),\n\t\t\t})\n\t\t\tcontinue\n\t\t}\n\n\t\tif (isEmpty(raw)) {\n\t\t\tcontinue\n\t\t}\n\n\t\toutValues.push({ field: instance.name, value })\n\t\tconst optionLabels = optionLabelsFor(instance)\n\t\tdescriptors.push({\n\t\t\tfield: instance.name,\n\t\t\tlabel: instance.label ?? instance.name,\n\t\t\tfieldType: instance.blockType,\n\t\t\t...(widthOf(instance) ? { width: widthOf(instance) } : {}),\n\t\t\t...(optionLabels ? { optionLabels } : {}),\n\t\t})\n\t}\n\n\treturn { errors, values: outValues, descriptors, consent: consentProofs }\n}\n"],"mappings":"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"}
@@ -1,10 +1,12 @@
1
1
  import { keys } from "../translations/keys.js";
2
2
  import { asFieldTranslate, asTranslate } from "../translations/server.js";
3
3
  import { resolveConsentEntries } from "../consent/resolveConsentEntries.js";
4
- import { IDENTITY_CONTEXT_KEY } from "../spam/constants.js";
4
+ import { calcExpressionOf } from "../calc/computeCalcFields.js";
5
+ import { resolveCalcContext } from "../calc/resolveCalcContext.js";
5
6
  import { isPollClosed, pollConfigOf } from "../form/pollState.js";
6
7
  import { resolveEffectivePollOptions } from "../poll/effectivePollOptions.js";
7
8
  import { FORMS_SLUG } from "../collections/forms.js";
9
+ import { IDENTITY_CONTEXT_KEY } from "../spam/constants.js";
8
10
  import { applyPollOptions } from "../poll/applyPollOptions.js";
9
11
  import { runSubmission } from "./runSubmission.js";
10
12
  import { POLL_CONTEXT_KEY } from "./votedCookie.js";
@@ -19,7 +21,7 @@ import { APIError, ValidationError } from "payload";
19
21
  * Consent fields are captured into `result.consent` (array of proofs, one per visible consent field),
20
22
  * built from the host's sources re-resolved here rather than from anything the form or client carries.
21
23
  */
22
- const validateSubmission = ({ registry, ruleRegistry, consentSources, consentSnapshot, uploadSlug, pollSourceRegistry, strictUploadOwnership }) => async ({ data, operation, req }) => {
24
+ const validateSubmission = ({ registry, ruleRegistry, calcSources, calcFunctions, consentSources, consentSnapshot, uploadSlug, pollSourceRegistry, strictUploadOwnership }) => async ({ data, operation, req }) => {
23
25
  if (operation !== "create" || !data) return data;
24
26
  const formId = data.form;
25
27
  if (formId == null) return data;
@@ -76,9 +78,31 @@ const validateSubmission = ({ registry, ruleRegistry, consentSources, consentSna
76
78
  } catch {
77
79
  throw new APIError(asTranslate(req.i18n.t)(keys.consentSourcesUnavailable), 503);
78
80
  }
81
+ let calcResolved;
82
+ if (fields.some((instance) => calcExpressionOf(instance) !== void 0)) {
83
+ let resolved;
84
+ try {
85
+ resolved = await resolveCalcContext({
86
+ fields,
87
+ sources: calcSources ?? {},
88
+ form,
89
+ payload: req.payload,
90
+ req
91
+ });
92
+ } catch (error) {
93
+ req.payload.logger?.error(`@10x-media/form-builder: calc source resolution failed for form ${String(formId)}: ${error instanceof Error ? error.message : String(error)}`);
94
+ throw new APIError(asTranslate(req.i18n.t)(keys.calcSourcesUnavailable), 503);
95
+ }
96
+ const functions = calcFunctions && Object.keys(calcFunctions).length > 0 ? Object.fromEntries(Object.entries(calcFunctions).map(([name, definition]) => [name, definition.apply])) : void 0;
97
+ calcResolved = {
98
+ ...resolved,
99
+ ...functions ? { functions } : {}
100
+ };
101
+ }
79
102
  const result = await runSubmission({
80
103
  fields,
81
104
  values: incoming,
105
+ calcResolved,
82
106
  registry,
83
107
  ruleRegistry,
84
108
  consentEntries,
@@ -1 +1 @@
1
- {"version":3,"file":"validateSubmission.js","names":[],"sources":["../../src/submissions/validateSubmission.ts"],"sourcesContent":["import { APIError, type CollectionBeforeValidateHook, ValidationError } from 'payload'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport { FORMS_SLUG } from '../collections/forms'\nimport type { ConsentSnapshotMode } from '../consent/captureConsent'\nimport { resolveConsentEntries } from '../consent/resolveConsentEntries'\nimport type { ConsentSourceEntry, ConsentSourcesResolver } from '../consent/types'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport { isPollClosed, pollConfigOf } from '../form/pollState'\nimport { applyPollOptions } from '../poll/applyPollOptions'\nimport type { PollOption } from '../poll/definePollOptionSource'\nimport { resolveEffectivePollOptions } from '../poll/effectivePollOptions'\nimport type { PollOptionSourceRegistry } from '../poll/registry'\nimport { IDENTITY_CONTEXT_KEY } from '../spam/constants'\nimport { keys } from '../translations/keys'\nimport { asFieldTranslate, asTranslate } from '../translations/server'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport { runSubmission } from './runSubmission'\nimport type { FormFieldInstance, SubmissionValue } from './types'\nimport { POLL_CONTEXT_KEY } from './votedCookie'\n\nexport type ValidateSubmissionArgs = {\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\t/** The host's consent sources resolver (plugin option `consent.sources`); absent when no sources are configured. */\n\tconsentSources?: ConsentSourcesResolver\n\t/** What each consent proof snapshots of the agreed wording (plugin option `consent.snapshot`). */\n\tconsentSnapshot?: ConsentSnapshotMode\n\t/** The plugin-configured uploads collection slug; absent when uploads are disabled. */\n\tuploadSlug?: string\n\t/** Registered poll option sources; a form's configured `optionSource` resolves through this at validation time. */\n\tpollSourceRegistry?: PollOptionSourceRegistry\n\t/** Plugin `spam.uploadOwnership: 'strict'`: reject an owned upload when the submitter is unidentifiable. */\n\tstrictUploadOwnership?: boolean\n}\n\n/**\n * Server-authoritative submission validation. On create it loads the referenced form, re-runs every\n * field's required check, intrinsic validator, and declarative rules through `runSubmission`, threading\n * `req`/`payload` so server-only async rules can hit the DB, and throws a Payload `ValidationError` with\n * per-field paths on any failure. The client is never trusted.\n * Consent fields are captured into `result.consent` (array of proofs, one per visible consent field),\n * built from the host's sources re-resolved here rather than from anything the form or client carries.\n */\nexport const validateSubmission =\n\t({\n\t\tregistry,\n\t\truleRegistry,\n\t\tconsentSources,\n\t\tconsentSnapshot,\n\t\tuploadSlug,\n\t\tpollSourceRegistry,\n\t\tstrictUploadOwnership,\n\t}: ValidateSubmissionArgs): CollectionBeforeValidateHook =>\n\tasync ({ data, operation, req }) => {\n\t\tif (operation !== 'create' || !data) {\n\t\t\treturn data\n\t\t}\n\t\tconst formId = data.form\n\t\tif (formId == null) {\n\t\t\treturn data\n\t\t}\n\n\t\tconst form = await req.payload.findByID({\n\t\t\tcollection: FORMS_SLUG,\n\t\t\tid: formId as string | number,\n\t\t\tdepth: 0,\n\t\t\tlocale: req.locale,\n\t\t\treq,\n\t\t})\n\n\t\t// Stash whether the form is poll-enabled (its top-level `pollEnabled` flag) so the voted-cookie\n\t\t// afterChange hook can skip a second form fetch on the same request.\n\t\tconst poll = pollConfigOf(form.poll)\n\t\tconst pollEnabled = form.pollEnabled === true\n\t\treq.context[POLL_CONTEXT_KEY] = pollEnabled\n\n\t\t// Form-level lifecycle guard, before any field work: a closed poll accepts no submissions,\n\t\t// regardless of what the client rendered.\n\t\tif (pollEnabled && isPollClosed(poll)) {\n\t\t\tthrow new APIError(asTranslate(req.i18n.t)(keys.pollClosed), 403)\n\t\t}\n\n\t\tlet fields = ((form.fields as FormFieldInstance[] | undefined) ?? []) as FormFieldInstance[]\n\t\tconst incoming = ((data.values as SubmissionValue[] | undefined) ?? []) as SubmissionValue[]\n\t\tconst locale = req.locale ?? 'en'\n\t\tconst t = asFieldTranslate(req.i18n.t)\n\n\t\t// With a resolved choice set for the results field (a poll `optionSource`, or the field type's\n\t\t// own `resolveOptions`) the resolved values are the only accepted answers: options are injected\n\t\t// into the field instance (so the select's membership check and the stored option labels use\n\t\t// them) and membership is also enforced directly, so an empty resolution or a non-select results\n\t\t// field still fails closed. A resolve failure rejects the whole submission rather than skipping\n\t\t// the check. Static authored polls are unaffected: their field validates against its own options\n\t\t// through the normal field pipeline.\n\t\tconst resultsField =\n\t\t\tpollEnabled && typeof poll?.resultsField === 'string' && poll.resultsField.length > 0\n\t\t\t\t? poll.resultsField\n\t\t\t\t: undefined\n\t\tconst resultsInstance = resultsField\n\t\t\t? fields.find((instance) => instance.name === resultsField)\n\t\t\t: undefined\n\t\tconst usesResolver =\n\t\t\t(typeof poll?.optionSource === 'string' && poll.optionSource.length > 0) ||\n\t\t\tBoolean(resultsInstance && registry.get(resultsInstance.blockType)?.resolveOptions)\n\t\tif (resultsField && usesResolver) {\n\t\t\tlet resolved: PollOption[]\n\t\t\ttry {\n\t\t\t\tresolved = await resolveEffectivePollOptions({\n\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\treq,\n\t\t\t\t\tform,\n\t\t\t\t\tsources: pollSourceRegistry ?? new Map(),\n\t\t\t\t\tfieldTypes: registry,\n\t\t\t\t})\n\t\t\t} catch {\n\t\t\t\tthrow new APIError(asTranslate(req.i18n.t)(keys.pollOptionsUnavailable), 503)\n\t\t\t}\n\t\t\tfields = applyPollOptions(fields, resultsField, resolved)\n\t\t\tconst answer = incoming.find((entry) => entry.field === resultsField)?.value\n\t\t\tconst isAnswered = answer != null && answer !== ''\n\t\t\tif (isAnswered && !resolved.some((option) => option.value === answer)) {\n\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t{\n\t\t\t\t\t\tcollection: FORM_SUBMISSIONS_SLUG,\n\t\t\t\t\t\terrors: [\n\t\t\t\t\t\t\t{ path: resultsField, message: asTranslate(req.i18n.t)(keys.validationSelect) },\n\t\t\t\t\t\t],\n\t\t\t\t\t},\n\t\t\t\t\treq.t\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t\tconst expectedOwner =\n\t\t\ttypeof req.context?.[IDENTITY_CONTEXT_KEY] === 'string'\n\t\t\t\t? (req.context[IDENTITY_CONTEXT_KEY] as string)\n\t\t\t\t: undefined\n\n\t\t// Every consent proof is built from the source as it reads right now, not from anything the\n\t\t// form or the client carries, so a resolver failure rejects the submission rather than\n\t\t// recording an agreement to a statement the server cannot vouch for.\n\t\tlet consentEntries: ConsentSourceEntry[] = []\n\t\tif (consentSources && fields.some((field) => field.blockType === 'consent')) {\n\t\t\ttry {\n\t\t\t\tconsentEntries = await resolveConsentEntries({\n\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\treq,\n\t\t\t\t\tform,\n\t\t\t\t\tsources: consentSources,\n\t\t\t\t})\n\t\t\t} catch {\n\t\t\t\tthrow new APIError(asTranslate(req.i18n.t)(keys.consentSourcesUnavailable), 503)\n\t\t\t}\n\t\t}\n\n\t\tconst result = await runSubmission({\n\t\t\tfields,\n\t\t\tvalues: incoming,\n\t\t\tregistry,\n\t\t\truleRegistry,\n\t\t\tconsentEntries,\n\t\t\tconsentSnapshot,\n\t\t\tlocale,\n\t\t\tt,\n\t\t\toperation: 'create',\n\t\t\treq,\n\t\t\tpayload: req.payload,\n\t\t\tformId: formId as number | string,\n\t\t\tuploadSlug,\n\t\t\texpectedOwner,\n\t\t\tstrictUploadOwnership,\n\t\t})\n\n\t\tif (result.errors.length > 0) {\n\t\t\tthrow new ValidationError({ collection: FORM_SUBMISSIONS_SLUG, errors: result.errors }, req.t)\n\t\t}\n\n\t\tdata.values = result.values\n\t\tdata.descriptors = result.descriptors\n\t\tdata.consent = result.consent.length > 0 ? result.consent : undefined\n\t\tdata.locale = locale\n\t\t// Unauthenticated submits are always 'complete'. A client-supplied 'partial' would\n\t\t// silently skip all post-submit actions and events. Authenticated callers (e.g. an\n\t\t// admin draft-save flow) may set status themselves.\n\t\tif (!req.user) {\n\t\t\tdata.status = 'complete'\n\t\t}\n\t\treturn data\n\t}\n"],"mappings":"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"}
1
+ {"version":3,"file":"validateSubmission.js","names":[],"sources":["../../src/submissions/validateSubmission.ts"],"sourcesContent":["import { APIError, type CollectionBeforeValidateHook, ValidationError } from 'payload'\nimport { calcExpressionOf } from '../calc/computeCalcFields'\nimport type { CalcResolved } from '../calc/evaluate'\nimport type { CalcFunction, CalcSource } from '../calc/registry'\nimport { resolveCalcContext } from '../calc/resolveCalcContext'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport { FORMS_SLUG } from '../collections/forms'\nimport type { ConsentSnapshotMode } from '../consent/captureConsent'\nimport { resolveConsentEntries } from '../consent/resolveConsentEntries'\nimport type { ConsentSourceEntry, ConsentSourcesResolver } from '../consent/types'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport { isPollClosed, pollConfigOf } from '../form/pollState'\nimport { applyPollOptions } from '../poll/applyPollOptions'\nimport type { PollOption } from '../poll/definePollOptionSource'\nimport { resolveEffectivePollOptions } from '../poll/effectivePollOptions'\nimport type { PollOptionSourceRegistry } from '../poll/registry'\nimport { IDENTITY_CONTEXT_KEY } from '../spam/constants'\nimport { keys } from '../translations/keys'\nimport { asFieldTranslate, asTranslate } from '../translations/server'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport { runSubmission } from './runSubmission'\nimport type { FormFieldInstance, SubmissionValue } from './types'\nimport { POLL_CONTEXT_KEY } from './votedCookie'\n\nexport type ValidateSubmissionArgs = {\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\t/** Registered calc sources (plugin option `calc.sources`); re-resolved fresh per submission. */\n\tcalcSources?: Record<string, CalcSource>\n\t/** Registered calc functions (plugin option `calc.functions`); threaded into calc evaluation. */\n\tcalcFunctions?: Record<string, CalcFunction>\n\t/** The host's consent sources resolver (plugin option `consent.sources`); absent when no sources are configured. */\n\tconsentSources?: ConsentSourcesResolver\n\t/** What each consent proof snapshots of the agreed wording (plugin option `consent.snapshot`). */\n\tconsentSnapshot?: ConsentSnapshotMode\n\t/** The plugin-configured uploads collection slug; absent when uploads are disabled. */\n\tuploadSlug?: string\n\t/** Registered poll option sources; a form's configured `optionSource` resolves through this at validation time. */\n\tpollSourceRegistry?: PollOptionSourceRegistry\n\t/** Plugin `spam.uploadOwnership: 'strict'`: reject an owned upload when the submitter is unidentifiable. */\n\tstrictUploadOwnership?: boolean\n}\n\n/**\n * Server-authoritative submission validation. On create it loads the referenced form, re-runs every\n * field's required check, intrinsic validator, and declarative rules through `runSubmission`, threading\n * `req`/`payload` so server-only async rules can hit the DB, and throws a Payload `ValidationError` with\n * per-field paths on any failure. The client is never trusted.\n * Consent fields are captured into `result.consent` (array of proofs, one per visible consent field),\n * built from the host's sources re-resolved here rather than from anything the form or client carries.\n */\nexport const validateSubmission =\n\t({\n\t\tregistry,\n\t\truleRegistry,\n\t\tcalcSources,\n\t\tcalcFunctions,\n\t\tconsentSources,\n\t\tconsentSnapshot,\n\t\tuploadSlug,\n\t\tpollSourceRegistry,\n\t\tstrictUploadOwnership,\n\t}: ValidateSubmissionArgs): CollectionBeforeValidateHook =>\n\tasync ({ data, operation, req }) => {\n\t\tif (operation !== 'create' || !data) {\n\t\t\treturn data\n\t\t}\n\t\tconst formId = data.form\n\t\tif (formId == null) {\n\t\t\treturn data\n\t\t}\n\n\t\tconst form = await req.payload.findByID({\n\t\t\tcollection: FORMS_SLUG,\n\t\t\tid: formId as string | number,\n\t\t\tdepth: 0,\n\t\t\tlocale: req.locale,\n\t\t\treq,\n\t\t})\n\n\t\t// Stash whether the form is poll-enabled (its top-level `pollEnabled` flag) so the voted-cookie\n\t\t// afterChange hook can skip a second form fetch on the same request.\n\t\tconst poll = pollConfigOf(form.poll)\n\t\tconst pollEnabled = form.pollEnabled === true\n\t\treq.context[POLL_CONTEXT_KEY] = pollEnabled\n\n\t\t// Form-level lifecycle guard, before any field work: a closed poll accepts no submissions,\n\t\t// regardless of what the client rendered.\n\t\tif (pollEnabled && isPollClosed(poll)) {\n\t\t\tthrow new APIError(asTranslate(req.i18n.t)(keys.pollClosed), 403)\n\t\t}\n\n\t\tlet fields = ((form.fields as FormFieldInstance[] | undefined) ?? []) as FormFieldInstance[]\n\t\tconst incoming = ((data.values as SubmissionValue[] | undefined) ?? []) as SubmissionValue[]\n\t\tconst locale = req.locale ?? 'en'\n\t\tconst t = asFieldTranslate(req.i18n.t)\n\n\t\t// With a resolved choice set for the results field (a poll `optionSource`, or the field type's\n\t\t// own `resolveOptions`) the resolved values are the only accepted answers: options are injected\n\t\t// into the field instance (so the select's membership check and the stored option labels use\n\t\t// them) and membership is also enforced directly, so an empty resolution or a non-select results\n\t\t// field still fails closed. A resolve failure rejects the whole submission rather than skipping\n\t\t// the check. Static authored polls are unaffected: their field validates against its own options\n\t\t// through the normal field pipeline.\n\t\tconst resultsField =\n\t\t\tpollEnabled && typeof poll?.resultsField === 'string' && poll.resultsField.length > 0\n\t\t\t\t? poll.resultsField\n\t\t\t\t: undefined\n\t\tconst resultsInstance = resultsField\n\t\t\t? fields.find((instance) => instance.name === resultsField)\n\t\t\t: undefined\n\t\tconst usesResolver =\n\t\t\t(typeof poll?.optionSource === 'string' && poll.optionSource.length > 0) ||\n\t\t\tBoolean(resultsInstance && registry.get(resultsInstance.blockType)?.resolveOptions)\n\t\tif (resultsField && usesResolver) {\n\t\t\tlet resolved: PollOption[]\n\t\t\ttry {\n\t\t\t\tresolved = await resolveEffectivePollOptions({\n\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\treq,\n\t\t\t\t\tform,\n\t\t\t\t\tsources: pollSourceRegistry ?? new Map(),\n\t\t\t\t\tfieldTypes: registry,\n\t\t\t\t})\n\t\t\t} catch {\n\t\t\t\tthrow new APIError(asTranslate(req.i18n.t)(keys.pollOptionsUnavailable), 503)\n\t\t\t}\n\t\t\tfields = applyPollOptions(fields, resultsField, resolved)\n\t\t\tconst answer = incoming.find((entry) => entry.field === resultsField)?.value\n\t\t\tconst isAnswered = answer != null && answer !== ''\n\t\t\tif (isAnswered && !resolved.some((option) => option.value === answer)) {\n\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t{\n\t\t\t\t\t\tcollection: FORM_SUBMISSIONS_SLUG,\n\t\t\t\t\t\terrors: [\n\t\t\t\t\t\t\t{ path: resultsField, message: asTranslate(req.i18n.t)(keys.validationSelect) },\n\t\t\t\t\t\t],\n\t\t\t\t\t},\n\t\t\t\t\treq.t\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t\tconst expectedOwner =\n\t\t\ttypeof req.context?.[IDENTITY_CONTEXT_KEY] === 'string'\n\t\t\t\t? (req.context[IDENTITY_CONTEXT_KEY] as string)\n\t\t\t\t: undefined\n\n\t\t// Every consent proof is built from the source as it reads right now, not from anything the\n\t\t// form or the client carries, so a resolver failure rejects the submission rather than\n\t\t// recording an agreement to a statement the server cannot vouch for.\n\t\tlet consentEntries: ConsentSourceEntry[] = []\n\t\tif (consentSources && fields.some((field) => field.blockType === 'consent')) {\n\t\t\ttry {\n\t\t\t\tconsentEntries = await resolveConsentEntries({\n\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\treq,\n\t\t\t\t\tform,\n\t\t\t\t\tsources: consentSources,\n\t\t\t\t})\n\t\t\t} catch {\n\t\t\t\tthrow new APIError(asTranslate(req.i18n.t)(keys.consentSourcesUnavailable), 503)\n\t\t\t}\n\t\t}\n\n\t\t// Calc extension values re-resolve fresh here, never trusting the render-time embedding, and a\n\t\t// resolver failure rejects the submission: a calc total is money math, so an outage must not\n\t\t// silently evaluate to 0 (the render path fails open instead; see `calcAfterRead`). Surfaced as\n\t\t// a translated 503 (the consent-sources precedent above), with the cause logged server-side\n\t\t// rather than leaked to the anonymous caller.\n\t\tlet calcResolved: CalcResolved | undefined\n\t\tif (fields.some((instance) => calcExpressionOf(instance) !== undefined)) {\n\t\t\tlet resolved: CalcResolved\n\t\t\ttry {\n\t\t\t\tresolved = await resolveCalcContext({\n\t\t\t\t\tfields,\n\t\t\t\t\tsources: calcSources ?? {},\n\t\t\t\t\t// Double cast: a host's generated Form interface has no index signature, so the direct\n\t\t\t\t\t// cast fails under a consumer tsconfig even though the shape is a plain document.\n\t\t\t\t\tform: form as unknown as { id: number | string } & Record<string, unknown>,\n\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\treq,\n\t\t\t\t})\n\t\t\t} catch (error) {\n\t\t\t\treq.payload.logger?.error(\n\t\t\t\t\t`@10x-media/form-builder: calc source resolution failed for form ${String(formId)}: ${\n\t\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t\t}`\n\t\t\t\t)\n\t\t\t\tthrow new APIError(asTranslate(req.i18n.t)(keys.calcSourcesUnavailable), 503)\n\t\t\t}\n\t\t\tconst functions =\n\t\t\t\tcalcFunctions && Object.keys(calcFunctions).length > 0\n\t\t\t\t\t? Object.fromEntries(\n\t\t\t\t\t\t\tObject.entries(calcFunctions).map(([name, definition]) => [name, definition.apply])\n\t\t\t\t\t\t)\n\t\t\t\t\t: undefined\n\t\t\tcalcResolved = { ...resolved, ...(functions ? { functions } : {}) }\n\t\t}\n\n\t\tconst result = await runSubmission({\n\t\t\tfields,\n\t\t\tvalues: incoming,\n\t\t\tcalcResolved,\n\t\t\tregistry,\n\t\t\truleRegistry,\n\t\t\tconsentEntries,\n\t\t\tconsentSnapshot,\n\t\t\tlocale,\n\t\t\tt,\n\t\t\toperation: 'create',\n\t\t\treq,\n\t\t\tpayload: req.payload,\n\t\t\tformId: formId as number | string,\n\t\t\tuploadSlug,\n\t\t\texpectedOwner,\n\t\t\tstrictUploadOwnership,\n\t\t})\n\n\t\tif (result.errors.length > 0) {\n\t\t\tthrow new ValidationError({ collection: FORM_SUBMISSIONS_SLUG, errors: result.errors }, req.t)\n\t\t}\n\n\t\tdata.values = result.values\n\t\tdata.descriptors = result.descriptors\n\t\tdata.consent = result.consent.length > 0 ? result.consent : undefined\n\t\tdata.locale = locale\n\t\t// Unauthenticated submits are always 'complete'. A client-supplied 'partial' would\n\t\t// silently skip all post-submit actions and events. Authenticated callers (e.g. an\n\t\t// admin draft-save flow) may set status themselves.\n\t\tif (!req.user) {\n\t\t\tdata.status = 'complete'\n\t\t}\n\t\treturn data\n\t}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAmDA,MAAa,sBACX,EACA,UACA,cACA,aACA,eACA,gBACA,iBACA,YACA,oBACA,4BAED,OAAO,EAAE,MAAM,WAAW,UAAU;CACnC,IAAI,cAAc,YAAY,CAAC,MAC9B,OAAO;CAER,MAAM,SAAS,KAAK;CACpB,IAAI,UAAU,MACb,OAAO;CAGR,MAAM,OAAO,MAAM,IAAI,QAAQ,SAAS;EACvC,YAAY;EACZ,IAAI;EACJ,OAAO;EACP,QAAQ,IAAI;EACZ;CACD,CAAC;CAID,MAAM,OAAO,aAAa,KAAK,IAAI;CACnC,MAAM,cAAc,KAAK,gBAAgB;CACzC,IAAI,QAAQ,oBAAoB;CAIhC,IAAI,eAAe,aAAa,IAAI,GACnC,MAAM,IAAI,SAAS,YAAY,IAAI,KAAK,CAAC,EAAE,KAAK,UAAU,GAAG,GAAG;CAGjE,IAAI,SAAW,KAAK,UAA8C,CAAC;CACnE,MAAM,WAAa,KAAK,UAA4C,CAAC;CACrE,MAAM,SAAS,IAAI,UAAU;CAC7B,MAAM,IAAI,iBAAiB,IAAI,KAAK,CAAC;CASrC,MAAM,eACL,eAAe,OAAO,MAAM,iBAAiB,YAAY,KAAK,aAAa,SAAS,IACjF,KAAK,eACL,KAAA;CACJ,MAAM,kBAAkB,eACrB,OAAO,MAAM,aAAa,SAAS,SAAS,YAAY,IACxD,KAAA;CACH,MAAM,eACJ,OAAO,MAAM,iBAAiB,YAAY,KAAK,aAAa,SAAS,KACtE,QAAQ,mBAAmB,SAAS,IAAI,gBAAgB,SAAS,GAAG,cAAc;CACnF,IAAI,gBAAgB,cAAc;EACjC,IAAI;EACJ,IAAI;GACH,WAAW,MAAM,4BAA4B;IAC5C,SAAS,IAAI;IACb;IACA;IACA,SAAS,sCAAsB,IAAI,IAAI;IACvC,YAAY;GACb,CAAC;EACF,QAAQ;GACP,MAAM,IAAI,SAAS,YAAY,IAAI,KAAK,CAAC,EAAE,KAAK,sBAAsB,GAAG,GAAG;EAC7E;EACA,SAAS,iBAAiB,QAAQ,cAAc,QAAQ;EACxD,MAAM,SAAS,SAAS,MAAM,UAAU,MAAM,UAAU,YAAY,GAAG;EAEvE,IADmB,UAAU,QAAQ,WAAW,MAC9B,CAAC,SAAS,MAAM,WAAW,OAAO,UAAU,MAAM,GACnE,MAAM,IAAI,gBACT;GACC,YAAY;GACZ,QAAQ,CACP;IAAE,MAAM;IAAc,SAAS,YAAY,IAAI,KAAK,CAAC,EAAE,KAAK,gBAAgB;GAAE,CAC/E;EACD,GACA,IAAI,CACL;CAEF;CACA,MAAM,gBACL,OAAO,IAAI,UAAA,+BAAoC,WAC3C,IAAI,QAAQ,wBACb,KAAA;CAKJ,IAAI,iBAAuC,CAAC;CAC5C,IAAI,kBAAkB,OAAO,MAAM,UAAU,MAAM,cAAc,SAAS,GACzE,IAAI;EACH,iBAAiB,MAAM,sBAAsB;GAC5C,SAAS,IAAI;GACb;GACA;GACA,SAAS;EACV,CAAC;CACF,QAAQ;EACP,MAAM,IAAI,SAAS,YAAY,IAAI,KAAK,CAAC,EAAE,KAAK,yBAAyB,GAAG,GAAG;CAChF;CAQD,IAAI;CACJ,IAAI,OAAO,MAAM,aAAa,iBAAiB,QAAQ,MAAM,KAAA,CAAS,GAAG;EACxE,IAAI;EACJ,IAAI;GACH,WAAW,MAAM,mBAAmB;IACnC;IACA,SAAS,eAAe,CAAC;IAGnB;IACN,SAAS,IAAI;IACb;GACD,CAAC;EACF,SAAS,OAAO;GACf,IAAI,QAAQ,QAAQ,MACnB,mEAAmE,OAAO,MAAM,EAAE,IACjF,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAEvD;GACA,MAAM,IAAI,SAAS,YAAY,IAAI,KAAK,CAAC,EAAE,KAAK,sBAAsB,GAAG,GAAG;EAC7E;EACA,MAAM,YACL,iBAAiB,OAAO,KAAK,aAAa,EAAE,SAAS,IAClD,OAAO,YACP,OAAO,QAAQ,aAAa,EAAE,KAAK,CAAC,MAAM,gBAAgB,CAAC,MAAM,WAAW,KAAK,CAAC,CACnF,IACC,KAAA;EACJ,eAAe;GAAE,GAAG;GAAU,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;EAAG;CACnE;CAEA,MAAM,SAAS,MAAM,cAAc;EAClC;EACA,QAAQ;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA,WAAW;EACX;EACA,SAAS,IAAI;EACL;EACR;EACA;EACA;CACD,CAAC;CAED,IAAI,OAAO,OAAO,SAAS,GAC1B,MAAM,IAAI,gBAAgB;EAAE,YAAY;EAAuB,QAAQ,OAAO;CAAO,GAAG,IAAI,CAAC;CAG9F,KAAK,SAAS,OAAO;CACrB,KAAK,cAAc,OAAO;CAC1B,KAAK,UAAU,OAAO,QAAQ,SAAS,IAAI,OAAO,UAAU,KAAA;CAC5D,KAAK,SAAS;CAId,IAAI,CAAC,IAAI,MACR,KAAK,SAAS;CAEf,OAAO;AACR"}