@10x-media/form-builder 0.1.0-beta.2 → 0.1.0-beta.21

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 (619) hide show
  1. package/CHANGELOG.md +360 -0
  2. package/README.md +7 -5
  3. package/dist/actions/assertNoBlockCollision.js +18 -0
  4. package/dist/actions/assertNoBlockCollision.js.map +1 -0
  5. package/dist/actions/body/converters.d.ts +20 -0
  6. package/dist/actions/body/converters.js +57 -0
  7. package/dist/actions/body/converters.js.map +1 -0
  8. package/dist/actions/body/escapeHtml.d.ts +6 -0
  9. package/dist/actions/body/escapeHtml.js +14 -0
  10. package/dist/actions/body/escapeHtml.js.map +1 -0
  11. package/dist/actions/body/serializeBody.d.ts +55 -0
  12. package/dist/actions/body/serializeBody.js +78 -0
  13. package/dist/actions/body/serializeBody.js.map +1 -0
  14. package/dist/actions/body/serializeSlate.js +31 -0
  15. package/dist/actions/body/serializeSlate.js.map +1 -0
  16. package/dist/actions/body/textOfBody.d.ts +13 -0
  17. package/dist/actions/body/textOfBody.js +35 -0
  18. package/dist/actions/body/textOfBody.js.map +1 -0
  19. package/dist/actions/body/wildcards.d.ts +10 -0
  20. package/dist/actions/body/wildcards.js +33 -0
  21. package/dist/actions/body/wildcards.js.map +1 -0
  22. package/dist/actions/buildActionBlocks.js +12 -9
  23. package/dist/actions/buildActionBlocks.js.map +1 -1
  24. package/dist/actions/builtin/confirmation.js +47 -43
  25. package/dist/actions/builtin/confirmation.js.map +1 -1
  26. package/dist/actions/builtin/emailAction.d.ts +20 -0
  27. package/dist/actions/builtin/emailAction.js +120 -0
  28. package/dist/actions/builtin/emailAction.js.map +1 -0
  29. package/dist/actions/builtin/emailTeam.js +20 -38
  30. package/dist/actions/builtin/emailTeam.js.map +1 -1
  31. package/dist/actions/builtin/index.d.ts +16 -1
  32. package/dist/actions/builtin/index.js +21 -7
  33. package/dist/actions/builtin/index.js.map +1 -1
  34. package/dist/actions/builtin/signedWebhook.js +6 -2
  35. package/dist/actions/builtin/signedWebhook.js.map +1 -1
  36. package/dist/actions/defineAction.d.ts +17 -5
  37. package/dist/actions/defineAction.js.map +1 -1
  38. package/dist/actions/dispatch.js +51 -6
  39. package/dist/actions/dispatch.js.map +1 -1
  40. package/dist/actions/dispatchContext.js +13 -0
  41. package/dist/actions/dispatchContext.js.map +1 -0
  42. package/dist/actions/emailRecipients.d.ts +12 -0
  43. package/dist/actions/emailRecipients.js +146 -0
  44. package/dist/actions/emailRecipients.js.map +1 -0
  45. package/dist/actions/fromAddresses.d.ts +37 -0
  46. package/dist/actions/fromAddresses.js +131 -0
  47. package/dist/actions/fromAddresses.js.map +1 -0
  48. package/dist/actions/recipientSources.d.ts +34 -0
  49. package/dist/actions/recipientSources.js +11 -0
  50. package/dist/actions/recipientSources.js.map +1 -0
  51. package/dist/actions/registry.d.ts +2 -1
  52. package/dist/actions/registry.js +5 -7
  53. package/dist/actions/registry.js.map +1 -1
  54. package/dist/actions/runActions.js +10 -1
  55. package/dist/actions/runActions.js.map +1 -1
  56. package/dist/actions/task.js +47 -16
  57. package/dist/actions/task.js.map +1 -1
  58. package/dist/aggregation/aggregateResponses.d.ts +9 -0
  59. package/dist/aggregation/aggregateResponses.js +8 -14
  60. package/dist/aggregation/aggregateResponses.js.map +1 -1
  61. package/dist/aggregation/resolveResultsRequest.d.ts +41 -6
  62. package/dist/aggregation/resolveResultsRequest.js +97 -11
  63. package/dist/aggregation/resolveResultsRequest.js.map +1 -1
  64. package/dist/calc/computeCalcFields.d.ts +5 -3
  65. package/dist/calc/computeCalcFields.js +6 -4
  66. package/dist/calc/computeCalcFields.js.map +1 -1
  67. package/dist/calc/evaluate.d.ts +26 -3
  68. package/dist/calc/evaluate.js +34 -13
  69. package/dist/calc/evaluate.js.map +1 -1
  70. package/dist/calc/formatCalc.d.ts +11 -0
  71. package/dist/calc/formatCalc.js +35 -0
  72. package/dist/calc/formatCalc.js.map +1 -0
  73. package/dist/calc/formatCalcValue.d.ts +17 -0
  74. package/dist/calc/formatCalcValue.js +17 -0
  75. package/dist/calc/formatCalcValue.js.map +1 -0
  76. package/dist/calc/normalizeCalc.d.ts +17 -3
  77. package/dist/calc/normalizeCalc.js +42 -33
  78. package/dist/calc/normalizeCalc.js.map +1 -1
  79. package/dist/calc/registry.d.ts +43 -0
  80. package/dist/calc/registry.js +27 -0
  81. package/dist/calc/registry.js.map +1 -0
  82. package/dist/calc/resolveCalcContext.d.ts +36 -0
  83. package/dist/calc/resolveCalcContext.js +87 -0
  84. package/dist/calc/resolveCalcContext.js.map +1 -0
  85. package/dist/calc/types.d.ts +25 -4
  86. package/dist/calc/types.js +23 -0
  87. package/dist/calc/types.js.map +1 -0
  88. package/dist/client/ByteSizeField.d.ts +29 -0
  89. package/dist/client/ByteSizeField.js +58 -0
  90. package/dist/client/ByteSizeField.js.map +1 -0
  91. package/dist/client/CalcExpressionBuilder.d.ts +20 -0
  92. package/dist/client/CalcExpressionBuilder.js +890 -0
  93. package/dist/client/CalcExpressionBuilder.js.map +1 -0
  94. package/dist/client/ClosePollButton.d.ts +15 -0
  95. package/dist/client/ClosePollButton.js +80 -0
  96. package/dist/client/ClosePollButton.js.map +1 -0
  97. package/dist/client/CondensedArray.d.ts +15 -0
  98. package/dist/client/CondensedArray.js +97 -0
  99. package/dist/client/CondensedArray.js.map +1 -0
  100. package/dist/client/ConditionBuilder.js +2 -1
  101. package/dist/client/ConditionBuilder.js.map +1 -1
  102. package/dist/client/ConsentBlockLabel.d.ts +15 -0
  103. package/dist/client/ConsentBlockLabel.js +71 -0
  104. package/dist/client/ConsentBlockLabel.js.map +1 -0
  105. package/dist/client/ConsentSourceRowLabel.d.ts +9 -0
  106. package/dist/client/ConsentSourceRowLabel.js +21 -0
  107. package/dist/client/ConsentSourceRowLabel.js.map +1 -0
  108. package/dist/client/EndpointOptionsSelect.d.ts +55 -0
  109. package/dist/client/EndpointOptionsSelect.js +112 -0
  110. package/dist/client/EndpointOptionsSelect.js.map +1 -0
  111. package/dist/client/FieldBlockLabel.d.ts +18 -0
  112. package/dist/client/FieldBlockLabel.js +34 -0
  113. package/dist/client/FieldBlockLabel.js.map +1 -0
  114. package/dist/client/FieldNameSelect.d.ts +37 -0
  115. package/dist/client/FieldNameSelect.js +72 -0
  116. package/dist/client/FieldNameSelect.js.map +1 -0
  117. package/dist/client/FlowBuilder.d.ts +2 -2
  118. package/dist/client/FlowBuilder.js +137 -88
  119. package/dist/client/FlowBuilder.js.map +1 -1
  120. package/dist/client/FormConditionField.js +1 -4
  121. package/dist/client/FormConditionField.js.map +1 -1
  122. package/dist/client/RecipientsSelect.d.ts +37 -0
  123. package/dist/client/RecipientsSelect.js +197 -0
  124. package/dist/client/RecipientsSelect.js.map +1 -0
  125. package/dist/client/RuleDescription.d.ts +21 -0
  126. package/dist/client/RuleDescription.js +19 -0
  127. package/dist/client/RuleDescription.js.map +1 -0
  128. package/dist/client/admin.css +557 -0
  129. package/dist/client/endpointOptions.d.ts +11 -0
  130. package/dist/client/endpointOptions.js +38 -0
  131. package/dist/client/endpointOptions.js.map +1 -0
  132. package/dist/client/flowAuthoring.js +142 -0
  133. package/dist/client/flowAuthoring.js.map +1 -0
  134. package/dist/client/synthesizeClientField.js +21 -10
  135. package/dist/client/synthesizeClientField.js.map +1 -1
  136. package/dist/client/toStaticLabel.js +13 -0
  137. package/dist/client/toStaticLabel.js.map +1 -0
  138. package/dist/collections/buttonFields.d.ts +34 -0
  139. package/dist/collections/buttonFields.js +35 -0
  140. package/dist/collections/buttonFields.js.map +1 -0
  141. package/dist/collections/cells/FieldCountCell.d.ts +15 -0
  142. package/dist/collections/cells/FieldCountCell.js +18 -0
  143. package/dist/collections/cells/FieldCountCell.js.map +1 -0
  144. package/dist/collections/cells/FlowStepsCell.d.ts +14 -0
  145. package/dist/collections/cells/FlowStepsCell.js +17 -0
  146. package/dist/collections/cells/FlowStepsCell.js.map +1 -0
  147. package/dist/collections/formSubmissions.js +114 -28
  148. package/dist/collections/formSubmissions.js.map +1 -1
  149. package/dist/collections/forms.js +449 -112
  150. package/dist/collections/forms.js.map +1 -1
  151. package/dist/collections/formsEndpoints.js +106 -0
  152. package/dist/collections/formsEndpoints.js.map +1 -0
  153. package/dist/collections/redirectFields.d.ts +29 -0
  154. package/dist/collections/settingsFields.d.ts +31 -0
  155. package/dist/collections/settingsFields.js +45 -0
  156. package/dist/collections/settingsFields.js.map +1 -0
  157. package/dist/conditions/conditionType.js +9 -2
  158. package/dist/conditions/conditionType.js.map +1 -1
  159. package/dist/conditions/evaluate.js +1 -1
  160. package/dist/conditions/evaluate.js.map +1 -1
  161. package/dist/conditions/fieldTypes.d.ts +3 -1
  162. package/dist/conditions/fieldTypes.js +4 -1
  163. package/dist/conditions/fieldTypes.js.map +1 -1
  164. package/dist/conditions/normalizeConditions.js +20 -6
  165. package/dist/conditions/normalizeConditions.js.map +1 -1
  166. package/dist/consent/applyConsentStatements.d.ts +14 -0
  167. package/dist/consent/applyConsentStatements.js +17 -0
  168. package/dist/consent/applyConsentStatements.js.map +1 -0
  169. package/dist/consent/captureConsent.d.ts +39 -11
  170. package/dist/consent/captureConsent.js +63 -13
  171. package/dist/consent/captureConsent.js.map +1 -1
  172. package/dist/consent/consentSourcesField.d.ts +66 -0
  173. package/dist/consent/consentSourcesField.js +98 -0
  174. package/dist/consent/consentSourcesField.js.map +1 -0
  175. package/dist/consent/effectiveStatement.js +16 -0
  176. package/dist/consent/effectiveStatement.js.map +1 -0
  177. package/dist/consent/resolveConsentEntries.d.ts +24 -0
  178. package/dist/consent/resolveConsentEntries.js +40 -0
  179. package/dist/consent/resolveConsentEntries.js.map +1 -0
  180. package/dist/consent/resolveConsentSourcesRequest.d.ts +37 -0
  181. package/dist/consent/resolveConsentSourcesRequest.js +54 -0
  182. package/dist/consent/resolveConsentSourcesRequest.js.map +1 -0
  183. package/dist/consent/resolveConsentStatements.d.ts +45 -0
  184. package/dist/consent/resolveConsentStatements.js +46 -0
  185. package/dist/consent/resolveConsentStatements.js.map +1 -0
  186. package/dist/consent/resolvePublishedVersionRef.d.ts +17 -11
  187. package/dist/consent/resolvePublishedVersionRef.js +13 -10
  188. package/dist/consent/resolvePublishedVersionRef.js.map +1 -1
  189. package/dist/consent/types.d.ts +69 -0
  190. package/dist/context/formContext.d.ts +30 -0
  191. package/dist/context/formContext.js +54 -0
  192. package/dist/context/formContext.js.map +1 -0
  193. package/dist/email/departments.d.ts +59 -0
  194. package/dist/email/departments.js +87 -0
  195. package/dist/email/departments.js.map +1 -0
  196. package/dist/email/departmentsField.d.ts +53 -0
  197. package/dist/email/departmentsField.js +73 -0
  198. package/dist/email/departmentsField.js.map +1 -0
  199. package/dist/exports/client.d.ts +14 -1
  200. package/dist/exports/client.js +14 -1
  201. package/dist/exports/i18n.d.ts +5 -2
  202. package/dist/exports/i18n.js +5 -2
  203. package/dist/exports/react.d.ts +34 -10
  204. package/dist/exports/react.js +24 -4
  205. package/dist/exports/rsc.d.ts +10 -1
  206. package/dist/exports/rsc.js +10 -1
  207. package/dist/exports/types.d.ts +20 -12
  208. package/dist/fields/buildFieldBlocks.js +46 -25
  209. package/dist/fields/buildFieldBlocks.js.map +1 -1
  210. package/dist/fields/builtin/calculation.d.ts +17 -0
  211. package/dist/fields/builtin/calculation.js +193 -18
  212. package/dist/fields/builtin/calculation.js.map +1 -1
  213. package/dist/fields/builtin/checkbox.js +23 -0
  214. package/dist/fields/builtin/checkbox.js.map +1 -1
  215. package/dist/fields/builtin/consent.js +49 -14
  216. package/dist/fields/builtin/consent.js.map +1 -1
  217. package/dist/fields/builtin/country.d.ts +13 -0
  218. package/dist/fields/builtin/country.js +30 -0
  219. package/dist/fields/builtin/country.js.map +1 -0
  220. package/dist/fields/builtin/date.js +31 -0
  221. package/dist/fields/builtin/date.js.map +1 -0
  222. package/dist/fields/builtin/email.js +8 -1
  223. package/dist/fields/builtin/email.js.map +1 -1
  224. package/dist/fields/builtin/file.d.ts +15 -0
  225. package/dist/fields/builtin/file.js +95 -8
  226. package/dist/fields/builtin/file.js.map +1 -1
  227. package/dist/fields/builtin/index.d.ts +30 -0
  228. package/dist/fields/builtin/index.js +37 -10
  229. package/dist/fields/builtin/index.js.map +1 -1
  230. package/dist/fields/builtin/message.js +30 -0
  231. package/dist/fields/builtin/message.js.map +1 -0
  232. package/dist/fields/builtin/repeater.js +8 -3
  233. package/dist/fields/builtin/repeater.js.map +1 -1
  234. package/dist/fields/builtin/select.js +28 -4
  235. package/dist/fields/builtin/select.js.map +1 -1
  236. package/dist/fields/builtin/state.d.ts +13 -0
  237. package/dist/fields/builtin/state.js +30 -0
  238. package/dist/fields/builtin/state.js.map +1 -0
  239. package/dist/fields/builtin/text.js +7 -0
  240. package/dist/fields/builtin/text.js.map +1 -1
  241. package/dist/fields/data/regions.d.ts +21 -0
  242. package/dist/fields/data/regions.js +1238 -0
  243. package/dist/fields/data/regions.js.map +1 -0
  244. package/dist/fields/fieldKey.d.ts +15 -0
  245. package/dist/fields/fieldKey.js +16 -0
  246. package/dist/fields/fieldKey.js.map +1 -0
  247. package/dist/fields/fieldNamesOfType.js +30 -0
  248. package/dist/fields/fieldNamesOfType.js.map +1 -0
  249. package/dist/fields/instanceOptions.js +20 -0
  250. package/dist/fields/instanceOptions.js.map +1 -0
  251. package/dist/fields/localizedIf.d.ts +13 -0
  252. package/dist/fields/localizedIf.js +12 -0
  253. package/dist/fields/localizedIf.js.map +1 -0
  254. package/dist/fields/registry.d.ts +3 -1
  255. package/dist/fields/registry.js +23 -8
  256. package/dist/fields/registry.js.map +1 -1
  257. package/dist/fields/sharedConfig.js +125 -59
  258. package/dist/fields/sharedConfig.js.map +1 -1
  259. package/dist/fields/types.d.ts +78 -9
  260. package/dist/flow/engine.d.ts +6 -4
  261. package/dist/flow/engine.js +11 -6
  262. package/dist/flow/engine.js.map +1 -1
  263. package/dist/flow/normalizeFlow.js +28 -9
  264. package/dist/flow/normalizeFlow.js.map +1 -1
  265. package/dist/flow/stepLabel.js +15 -0
  266. package/dist/flow/stepLabel.js.map +1 -0
  267. package/dist/flow/types.d.ts +12 -3
  268. package/dist/flow/types.js +11 -0
  269. package/dist/flow/types.js.map +1 -0
  270. package/dist/form/pollState.d.ts +23 -0
  271. package/dist/form/pollState.js +17 -0
  272. package/dist/form/pollState.js.map +1 -0
  273. package/dist/form/toFormDocument.d.ts +110 -0
  274. package/dist/form/toFormDocument.js +89 -0
  275. package/dist/form/toFormDocument.js.map +1 -0
  276. package/dist/form/types.d.ts +73 -0
  277. package/dist/index.d.ts +67 -61
  278. package/dist/index.js +120 -25
  279. package/dist/index.js.map +1 -1
  280. package/dist/options.d.ts +281 -0
  281. package/dist/plugin/applyRegistryConfig.d.ts +11 -0
  282. package/dist/plugin/applyRegistryConfig.js +20 -0
  283. package/dist/plugin/applyRegistryConfig.js.map +1 -0
  284. package/dist/plugin/customState.js +26 -0
  285. package/dist/plugin/customState.js.map +1 -0
  286. package/dist/plugin/registerCollections.js +43 -40
  287. package/dist/plugin/registerCollections.js.map +1 -1
  288. package/dist/plugin/uploadsCollection.d.ts +16 -0
  289. package/dist/plugin/uploadsCollection.js +53 -0
  290. package/dist/plugin/uploadsCollection.js.map +1 -0
  291. package/dist/poll/applyPollOptions.js +18 -0
  292. package/dist/poll/applyPollOptions.js.map +1 -0
  293. package/dist/poll/buildPollOptionSourceFields.js +50 -0
  294. package/dist/poll/buildPollOptionSourceFields.js.map +1 -0
  295. package/dist/poll/closeJob.d.ts +53 -0
  296. package/dist/poll/closeJob.js +121 -0
  297. package/dist/poll/closeJob.js.map +1 -0
  298. package/dist/poll/definePollOptionSource.d.ts +44 -0
  299. package/dist/poll/definePollOptionSource.js +6 -0
  300. package/dist/poll/definePollOptionSource.js.map +1 -0
  301. package/dist/poll/definePollType.d.ts +34 -0
  302. package/dist/poll/definePollType.js +6 -0
  303. package/dist/poll/definePollType.js.map +1 -0
  304. package/dist/poll/effectivePollOptions.d.ts +38 -0
  305. package/dist/poll/effectivePollOptions.js +64 -0
  306. package/dist/poll/effectivePollOptions.js.map +1 -0
  307. package/dist/poll/mostVoted.d.ts +21 -0
  308. package/dist/poll/mostVoted.js +32 -0
  309. package/dist/poll/mostVoted.js.map +1 -0
  310. package/dist/poll/outcomeBeforeChange.js +91 -0
  311. package/dist/poll/outcomeBeforeChange.js.map +1 -0
  312. package/dist/poll/outcomeFields.d.ts +38 -0
  313. package/dist/poll/outcomeFields.js +52 -0
  314. package/dist/poll/outcomeFields.js.map +1 -0
  315. package/dist/poll/pollTypeRegistry.d.ts +34 -0
  316. package/dist/poll/pollTypeRegistry.js +69 -0
  317. package/dist/poll/pollTypeRegistry.js.map +1 -0
  318. package/dist/poll/registry.d.ts +17 -0
  319. package/dist/poll/registry.js +12 -0
  320. package/dist/poll/registry.js.map +1 -0
  321. package/dist/poll/resolvePollCloseRequest.js +106 -0
  322. package/dist/poll/resolvePollCloseRequest.js.map +1 -0
  323. package/dist/poll/resolvePollOptions.d.ts +29 -0
  324. package/dist/poll/resolvePollOptions.js +52 -0
  325. package/dist/poll/resolvePollOptions.js.map +1 -0
  326. package/dist/poll/resolvePollOptionsRequest.js +50 -0
  327. package/dist/poll/resolvePollOptionsRequest.js.map +1 -0
  328. package/dist/poll/resolvePollOutcome.d.ts +40 -0
  329. package/dist/poll/resolvePollOutcome.js +90 -0
  330. package/dist/poll/resolvePollOutcome.js.map +1 -0
  331. package/dist/poll/resultsField.js +21 -0
  332. package/dist/poll/resultsField.js.map +1 -0
  333. package/dist/poll/votes/aggregateFromVotes.d.ts +27 -0
  334. package/dist/poll/votes/aggregateFromVotes.js +54 -0
  335. package/dist/poll/votes/aggregateFromVotes.js.map +1 -0
  336. package/dist/poll/votes/answerValues.js +16 -0
  337. package/dist/poll/votes/answerValues.js.map +1 -0
  338. package/dist/poll/votes/bumpPollVote.js +88 -0
  339. package/dist/poll/votes/bumpPollVote.js.map +1 -0
  340. package/dist/poll/votes/recountPollVotes.d.ts +20 -0
  341. package/dist/poll/votes/recountPollVotes.js +63 -0
  342. package/dist/poll/votes/recountPollVotes.js.map +1 -0
  343. package/dist/poll/votes/voteTallyHook.js +76 -0
  344. package/dist/poll/votes/voteTallyHook.js.map +1 -0
  345. package/dist/poll/votes/votesCollection.d.ts +19 -0
  346. package/dist/poll/votes/votesCollection.js +89 -0
  347. package/dist/poll/votes/votesCollection.js.map +1 -0
  348. package/dist/prefill/valuesFromSearchParams.js +5 -9
  349. package/dist/prefill/valuesFromSearchParams.js.map +1 -1
  350. package/dist/presentations/types.d.ts +3 -2
  351. package/dist/react/Form.d.ts +77 -35
  352. package/dist/react/Form.js +368 -222
  353. package/dist/react/Form.js.map +1 -1
  354. package/dist/react/FormContext.d.ts +51 -1
  355. package/dist/react/FormContext.js.map +1 -1
  356. package/dist/react/FormControls.d.ts +55 -0
  357. package/dist/react/FormControls.js +56 -0
  358. package/dist/react/FormControls.js.map +1 -0
  359. package/dist/react/FormFields.d.ts +18 -0
  360. package/dist/react/FormFields.js +114 -0
  361. package/dist/react/FormFields.js.map +1 -0
  362. package/dist/react/FormLayout.d.ts +4 -2
  363. package/dist/react/FormLayout.js +3 -2
  364. package/dist/react/FormLayout.js.map +1 -1
  365. package/dist/react/FormResults.d.ts +8 -3
  366. package/dist/react/FormResults.js +50 -29
  367. package/dist/react/FormResults.js.map +1 -1
  368. package/dist/react/FormSteps.d.ts +17 -0
  369. package/dist/react/FormSteps.js +35 -0
  370. package/dist/react/FormSteps.js.map +1 -0
  371. package/dist/react/Poll.d.ts +40 -6
  372. package/dist/react/Poll.js +137 -33
  373. package/dist/react/Poll.js.map +1 -1
  374. package/dist/react/adapters.d.ts +35 -0
  375. package/dist/react/adapters.js +11 -0
  376. package/dist/react/adapters.js.map +1 -0
  377. package/dist/react/captcha/HcaptchaCaptcha.d.ts +31 -0
  378. package/dist/react/captcha/HcaptchaCaptcha.js +53 -0
  379. package/dist/react/captcha/HcaptchaCaptcha.js.map +1 -0
  380. package/dist/react/captcha/RecaptchaCaptcha.d.ts +44 -0
  381. package/dist/react/captcha/RecaptchaCaptcha.js +93 -0
  382. package/dist/react/captcha/RecaptchaCaptcha.js.map +1 -0
  383. package/dist/react/captcha/TurnstileCaptcha.d.ts +31 -0
  384. package/dist/react/captcha/TurnstileCaptcha.js +53 -0
  385. package/dist/react/captcha/TurnstileCaptcha.js.map +1 -0
  386. package/dist/react/captcha/types.d.ts +8 -0
  387. package/dist/react/captcha/useCaptchaScript.js +63 -0
  388. package/dist/react/captcha/useCaptchaScript.js.map +1 -0
  389. package/dist/react/captcha/vendors.js +8 -0
  390. package/dist/react/captcha/vendors.js.map +1 -0
  391. package/dist/react/contract.d.ts +2 -3
  392. package/dist/react/contract.js.map +1 -1
  393. package/dist/react/fetchResults.d.ts +1 -1
  394. package/dist/react/fetchResults.js +1 -1
  395. package/dist/react/fetchResults.js.map +1 -1
  396. package/dist/react/presentation/DialogSurface.js +1 -4
  397. package/dist/react/presentation/DialogSurface.js.map +1 -1
  398. package/dist/react/primitives/Checkbox.d.ts +6 -2
  399. package/dist/react/primitives/Checkbox.js +4 -2
  400. package/dist/react/primitives/Checkbox.js.map +1 -1
  401. package/dist/react/primitives/ChoiceGroup.d.ts +40 -0
  402. package/dist/react/primitives/ChoiceGroup.js +33 -0
  403. package/dist/react/primitives/ChoiceGroup.js.map +1 -0
  404. package/dist/react/primitives/FieldShell.d.ts +13 -5
  405. package/dist/react/primitives/FieldShell.js +35 -32
  406. package/dist/react/primitives/FieldShell.js.map +1 -1
  407. package/dist/react/primitives/Input.d.ts +4 -2
  408. package/dist/react/primitives/Input.js +2 -1
  409. package/dist/react/primitives/Input.js.map +1 -1
  410. package/dist/react/renderers/calculation.js +7 -5
  411. package/dist/react/renderers/calculation.js.map +1 -1
  412. package/dist/react/renderers/checkbox.js +3 -5
  413. package/dist/react/renderers/checkbox.js.map +1 -1
  414. package/dist/react/renderers/consent.js +84 -36
  415. package/dist/react/renderers/consent.js.map +1 -1
  416. package/dist/react/renderers/date.js +33 -0
  417. package/dist/react/renderers/date.js.map +1 -0
  418. package/dist/react/renderers/email.js +4 -5
  419. package/dist/react/renderers/email.js.map +1 -1
  420. package/dist/react/renderers/file.js +29 -14
  421. package/dist/react/renderers/file.js.map +1 -1
  422. package/dist/react/renderers/index.js +7 -0
  423. package/dist/react/renderers/index.js.map +1 -1
  424. package/dist/react/renderers/message.js +41 -0
  425. package/dist/react/renderers/message.js.map +1 -0
  426. package/dist/react/renderers/number.js +1 -4
  427. package/dist/react/renderers/number.js.map +1 -1
  428. package/dist/react/renderers/region.js +43 -0
  429. package/dist/react/renderers/region.js.map +1 -0
  430. package/dist/react/renderers/repeater.js +32 -14
  431. package/dist/react/renderers/repeater.js.map +1 -1
  432. package/dist/react/renderers/select.js +21 -6
  433. package/dist/react/renderers/select.js.map +1 -1
  434. package/dist/react/renderers/text.js +4 -5
  435. package/dist/react/renderers/text.js.map +1 -1
  436. package/dist/react/renderers/textarea.js +1 -4
  437. package/dist/react/renderers/textarea.js.map +1 -1
  438. package/dist/react/state.d.ts +46 -3
  439. package/dist/react/state.js +73 -10
  440. package/dist/react/state.js.map +1 -1
  441. package/dist/react/submitForm.js +1 -1
  442. package/dist/react/submitForm.js.map +1 -1
  443. package/dist/react/uploadFile.d.ts +2 -2
  444. package/dist/react/uploadFile.js +1 -1
  445. package/dist/react/uploadFile.js.map +1 -1
  446. package/dist/react/useCalcValues.d.ts +10 -0
  447. package/dist/react/useCalcValues.js +13 -0
  448. package/dist/react/useCalcValues.js.map +1 -0
  449. package/dist/react/useField.d.ts +0 -1
  450. package/dist/react/useField.js +7 -6
  451. package/dist/react/useField.js.map +1 -1
  452. package/dist/react/useFormState.d.ts +1 -1
  453. package/dist/react/useFormState.js +1 -1
  454. package/dist/react/useFormState.js.map +1 -1
  455. package/dist/react/validateField.js +3 -6
  456. package/dist/react/validateField.js.map +1 -1
  457. package/dist/recall/interpolate.d.ts +4 -3
  458. package/dist/recall/interpolate.js +5 -4
  459. package/dist/recall/interpolate.js.map +1 -1
  460. package/dist/recall/resolver.d.ts +1 -1
  461. package/dist/recall/resolver.js +15 -3
  462. package/dist/recall/resolver.js.map +1 -1
  463. package/dist/spam/captcha.d.ts +4 -3
  464. package/dist/spam/captcha.js +4 -3
  465. package/dist/spam/captcha.js.map +1 -1
  466. package/dist/spam/clientIp.js +9 -0
  467. package/dist/spam/clientIp.js.map +1 -0
  468. package/dist/spam/constants.js +13 -1
  469. package/dist/spam/constants.js.map +1 -1
  470. package/dist/spam/identify.js +3 -6
  471. package/dist/spam/identify.js.map +1 -1
  472. package/dist/spam/providers/hcaptcha.d.ts +17 -0
  473. package/dist/spam/providers/hcaptcha.js +18 -0
  474. package/dist/spam/providers/hcaptcha.js.map +1 -0
  475. package/dist/spam/providers/recaptcha.d.ts +23 -0
  476. package/dist/spam/providers/recaptcha.js +23 -0
  477. package/dist/spam/providers/recaptcha.js.map +1 -0
  478. package/dist/spam/providers/siteverify.js +34 -0
  479. package/dist/spam/providers/siteverify.js.map +1 -0
  480. package/dist/spam/providers/turnstile.d.ts +17 -0
  481. package/dist/spam/providers/turnstile.js +18 -0
  482. package/dist/spam/providers/turnstile.js.map +1 -0
  483. package/dist/spam/resolveSpam.js +2 -1
  484. package/dist/spam/resolveSpam.js.map +1 -1
  485. package/dist/spam/spamGuard.js +25 -11
  486. package/dist/spam/spamGuard.js.map +1 -1
  487. package/dist/spam/types.d.ts +19 -4
  488. package/dist/submissions/SubmissionAnswers.js +6 -2
  489. package/dist/submissions/SubmissionAnswers.js.map +1 -1
  490. package/dist/submissions/SubmissionAnswersClient.js +100 -64
  491. package/dist/submissions/SubmissionAnswersClient.js.map +1 -1
  492. package/dist/submissions/coerceBoolean.js +14 -0
  493. package/dist/submissions/coerceBoolean.js.map +1 -0
  494. package/dist/submissions/createSubmission.d.ts +36 -0
  495. package/dist/submissions/createSubmission.js +26 -0
  496. package/dist/submissions/createSubmission.js.map +1 -0
  497. package/dist/submissions/formIdOf.js +13 -0
  498. package/dist/submissions/formIdOf.js.map +1 -0
  499. package/dist/submissions/resolveVotedSubmission.d.ts +27 -0
  500. package/dist/submissions/resolveVotedSubmission.js +51 -0
  501. package/dist/submissions/resolveVotedSubmission.js.map +1 -0
  502. package/dist/submissions/runSubmission.d.ts +10 -0
  503. package/dist/submissions/runSubmission.js +104 -54
  504. package/dist/submissions/runSubmission.js.map +1 -1
  505. package/dist/submissions/types.d.ts +11 -4
  506. package/dist/submissions/validateSubmission.js +99 -13
  507. package/dist/submissions/validateSubmission.js.map +1 -1
  508. package/dist/submissions/verifyContext.js +35 -0
  509. package/dist/submissions/verifyContext.js.map +1 -0
  510. package/dist/submissions/voteChangeEndpoint.js +105 -0
  511. package/dist/submissions/voteChangeEndpoint.js.map +1 -0
  512. package/dist/submissions/votedCookie.d.ts +15 -0
  513. package/dist/submissions/votedCookie.js +74 -0
  514. package/dist/submissions/votedCookie.js.map +1 -0
  515. package/dist/translations/clientRuntimeKeys.d.ts +17 -0
  516. package/dist/translations/clientRuntimeKeys.js +48 -0
  517. package/dist/translations/clientRuntimeKeys.js.map +1 -0
  518. package/dist/translations/de.d.ts +12 -0
  519. package/dist/translations/de.js +380 -0
  520. package/dist/translations/de.js.map +1 -0
  521. package/dist/translations/en.js +250 -28
  522. package/dist/translations/en.js.map +1 -1
  523. package/dist/translations/index.d.ts +14 -5
  524. package/dist/translations/index.js +14 -3
  525. package/dist/translations/index.js.map +1 -1
  526. package/dist/translations/keys.d.ts +245 -23
  527. package/dist/translations/keys.js +246 -24
  528. package/dist/translations/keys.js.map +1 -1
  529. package/dist/translations/makeTranslate.d.ts +7 -3
  530. package/dist/translations/makeTranslate.js +12 -3
  531. package/dist/translations/makeTranslate.js.map +1 -1
  532. package/dist/translations/server.js +8 -1
  533. package/dist/translations/server.js.map +1 -1
  534. package/dist/uploads/captureFileRef.d.ts +10 -3
  535. package/dist/uploads/captureFileRef.js +18 -9
  536. package/dist/uploads/captureFileRef.js.map +1 -1
  537. package/dist/uploads/formatBytes.d.ts +9 -0
  538. package/dist/uploads/formatBytes.js +25 -0
  539. package/dist/uploads/formatBytes.js.map +1 -0
  540. package/dist/uploads/resolveFileRef.js +3 -2
  541. package/dist/uploads/resolveFileRef.js.map +1 -1
  542. package/dist/uploads/types.d.ts +1 -2
  543. package/dist/validation/buildRuleBlocks.js +18 -17
  544. package/dist/validation/buildRuleBlocks.js.map +1 -1
  545. package/dist/validation/builtin/email.js +2 -1
  546. package/dist/validation/builtin/email.js.map +1 -1
  547. package/dist/validation/builtin/index.d.ts +14 -0
  548. package/dist/validation/builtin/index.js +14 -1
  549. package/dist/validation/builtin/index.js.map +1 -1
  550. package/dist/validation/builtin/integer.js +15 -0
  551. package/dist/validation/builtin/integer.js.map +1 -0
  552. package/dist/validation/builtin/matchesField.js +3 -7
  553. package/dist/validation/builtin/matchesField.js.map +1 -1
  554. package/dist/validation/builtin/max.js +1 -0
  555. package/dist/validation/builtin/max.js.map +1 -1
  556. package/dist/validation/builtin/maxDate.js +24 -0
  557. package/dist/validation/builtin/maxDate.js.map +1 -0
  558. package/dist/validation/builtin/maxLength.js +1 -0
  559. package/dist/validation/builtin/maxLength.js.map +1 -1
  560. package/dist/validation/builtin/min.js +1 -0
  561. package/dist/validation/builtin/min.js.map +1 -1
  562. package/dist/validation/builtin/minDate.js +24 -0
  563. package/dist/validation/builtin/minDate.js.map +1 -0
  564. package/dist/validation/builtin/minLength.js +1 -0
  565. package/dist/validation/builtin/minLength.js.map +1 -1
  566. package/dist/validation/builtin/notAlreadySubmitted.js +5 -1
  567. package/dist/validation/builtin/notAlreadySubmitted.js.map +1 -1
  568. package/dist/validation/builtin/oneOf.js +1 -0
  569. package/dist/validation/builtin/oneOf.js.map +1 -1
  570. package/dist/validation/builtin/pattern.js +11 -2
  571. package/dist/validation/builtin/pattern.js.map +1 -1
  572. package/dist/validation/builtin/url.js +1 -0
  573. package/dist/validation/builtin/url.js.map +1 -1
  574. package/dist/validation/builtin/validateDateBound.js +18 -0
  575. package/dist/validation/builtin/validateDateBound.js.map +1 -0
  576. package/dist/validation/builtin/validateRegexParams.js +44 -0
  577. package/dist/validation/builtin/validateRegexParams.js.map +1 -0
  578. package/dist/validation/emailPattern.js +10 -0
  579. package/dist/validation/emailPattern.js.map +1 -0
  580. package/dist/validation/fieldTargetParam.d.ts +22 -0
  581. package/dist/validation/fieldTargetParam.js +38 -0
  582. package/dist/validation/fieldTargetParam.js.map +1 -0
  583. package/dist/validation/registry.d.ts +2 -1
  584. package/dist/validation/registry.js +2 -9
  585. package/dist/validation/registry.js.map +1 -1
  586. package/dist/validation/runValidation.js +8 -29
  587. package/dist/validation/runValidation.js.map +1 -1
  588. package/dist/validation/types.d.ts +5 -11
  589. package/dist/validation/validateUrl.js +22 -0
  590. package/dist/validation/validateUrl.js.map +1 -0
  591. package/package.json +14 -6
  592. package/styles/form-builder.css +94 -190
  593. package/dist/collections/uploads.d.ts +0 -30
  594. package/dist/collections/uploads.js +0 -55
  595. package/dist/collections/uploads.js.map +0 -1
  596. package/dist/consent/buildConsentSourceConfig.js +0 -49
  597. package/dist/consent/buildConsentSourceConfig.js.map +0 -1
  598. package/dist/consent/builtin/index.d.ts +0 -7
  599. package/dist/consent/builtin/index.js +0 -11
  600. package/dist/consent/builtin/index.js.map +0 -1
  601. package/dist/consent/builtin/pageReference.js +0 -75
  602. package/dist/consent/builtin/pageReference.js.map +0 -1
  603. package/dist/consent/builtin/static.js +0 -44
  604. package/dist/consent/builtin/static.js.map +0 -1
  605. package/dist/consent/defineConsentSource.d.ts +0 -35
  606. package/dist/consent/defineConsentSource.js +0 -6
  607. package/dist/consent/defineConsentSource.js.map +0 -1
  608. package/dist/consent/registry.d.ts +0 -16
  609. package/dist/consent/registry.js +0 -16
  610. package/dist/consent/registry.js.map +0 -1
  611. package/dist/consent/resolveConsentLinks.d.ts +0 -19
  612. package/dist/consent/resolveConsentLinks.js +0 -26
  613. package/dist/consent/resolveConsentLinks.js.map +0 -1
  614. package/dist/presentations/registry.d.ts +0 -15
  615. package/dist/presentations/registry.js +0 -15
  616. package/dist/presentations/registry.js.map +0 -1
  617. package/dist/react/toFormDocument.d.ts +0 -24
  618. package/dist/react/toFormDocument.js +0 -22
  619. package/dist/react/toFormDocument.js.map +0 -1
@@ -2,12 +2,39 @@ import { FieldAggregation } from "./types.js";
2
2
  import { Payload, PayloadRequest } from "payload";
3
3
 
4
4
  //#region src/aggregation/resolveResultsRequest.d.ts
5
+ type FormResultsAccessArgs = {
6
+ req: PayloadRequest; /** The loaded forms document (depth 0). Untyped beyond `id`: hosts read their own fields (e.g. `form.tenant`). */
7
+ form: {
8
+ id: number | string;
9
+ } & Record<string, unknown>;
10
+ };
11
+ /**
12
+ * Host seam gating anonymous results reads, evaluated after the form is loaded and before anything
13
+ * is aggregated. Multi-tenant recipe: compare `form.tenant` against the tenant derived from `req`
14
+ * (host header, cookie, or auth context) and return `false` for a cross-tenant read. Authenticated
15
+ * callers bypass this seam (they are admin-trusted, like the rest of the results endpoint).
16
+ */
17
+ type FormResultsAccess = (args: FormResultsAccessArgs) => boolean | Promise<boolean>;
5
18
  type ResolveResultsRequestArgs = {
6
19
  payload: Payload;
7
20
  formId: number | string | undefined; /** The requested field (query param). */
8
21
  field?: string; /** Whether the caller is authenticated (an admin/user). */
9
22
  isAuthed: boolean;
10
- req?: PayloadRequest;
23
+ req?: PayloadRequest; /** Optional host seam for anonymous reads; absent means plugin-default gating only. */
24
+ access?: FormResultsAccess;
25
+ /**
26
+ * Poll-eligible field types (registry-derived). When set, an anonymous read of an
27
+ * option-source poll also requires the results field's instance to be one of these types,
28
+ * so legacy or db-written docs pointing at a free-text field can never expose stored
29
+ * answers as leftover result buckets.
30
+ */
31
+ eligibleTypes?: readonly string[];
32
+ /**
33
+ * Whether the hidden tally store backs poll reads. When true, the poll results field is served
34
+ * from `aggregateFromVotes` (anonymous reads, and an authed read naming exactly that field)
35
+ * instead of the submission scan; every authorization gate stays identical either way.
36
+ */
37
+ pollVotesEnabled?: boolean;
11
38
  };
12
39
  type ResolveResultsRequestResult = {
13
40
  status: number;
@@ -21,12 +48,20 @@ type ResolveResultsRequestResult = {
21
48
  };
22
49
  /**
23
50
  * Authorize and resolve a poll/survey results request. Authed callers may aggregate any field (or all
24
- * enumerable fields). Anonymous callers are allowed only when the form opted in (`showResults`), and then
25
- * only for the configured `resultsField`, and only if that field is enumerable (has options) so a
26
- * misconfigured `resultsField` pointing at a free-text or PII field can never be dumped publicly. Returns
27
- * only aggregate counts, never raw submissions.
51
+ * enumerable fields) and bypass the `access` seam; for a poll-enabled form they also get the results
52
+ * field's effective options injected so sourced/resolver-backed polls render labels (best-effort, a
53
+ * resolve failure degrades to raw values rather than blocking the trusted read). Anonymous callers are
54
+ * allowed only when the form's poll is enabled, the poll's `resultsVisibility` permits it (`afterVote`:
55
+ * any time; `afterClose`: only once `closesAt` has passed), and the optional host `access` seam
56
+ * approves; and then only for the configured `poll.resultsField`, and only if that field is enumerable
57
+ * so a misconfigured `resultsField` pointing at a free-text or PII field can never be dumped publicly. A
58
+ * static poll is enumerable through its authored options; a poll whose options come from an
59
+ * `optionSource` or the field type's own `resolveOptions` resolves them here (registry via
60
+ * `config.custom`, per-request cache), gated by `eligibleTypes` and enumerable only when resolution
61
+ * yields any, with the resolved options driving bucket order and labels. Resolution failure fails
62
+ * closed (503) on the anonymous path. Returns only aggregate counts, never raw submissions.
28
63
  */
29
64
  declare const resolveFormResultsRequest: (args: ResolveResultsRequestArgs) => Promise<ResolveResultsRequestResult>;
30
65
  //#endregion
31
- export { ResolveResultsRequestArgs, ResolveResultsRequestResult, resolveFormResultsRequest };
66
+ export { FormResultsAccess, FormResultsAccessArgs, ResolveResultsRequestArgs, ResolveResultsRequestResult, resolveFormResultsRequest };
32
67
  //# sourceMappingURL=resolveResultsRequest.d.ts.map
@@ -1,19 +1,41 @@
1
+ import { isPollClosed, pollConfigOf } from "../form/pollState.js";
1
2
  import { aggregateFormResponses, fieldHasOptions } from "./aggregateResponses.js";
3
+ import { resolveEffectivePollOptions } from "../poll/effectivePollOptions.js";
4
+ import { aggregateFromVotes } from "../poll/votes/aggregateFromVotes.js";
5
+ import { resolvePollOutcome } from "../poll/resolvePollOutcome.js";
6
+ import { shouldAutoResolvePoll } from "../poll/closeJob.js";
2
7
  import { FORMS_SLUG } from "../collections/forms.js";
3
8
  //#region src/aggregation/resolveResultsRequest.ts
4
9
  const forbidden = {
5
10
  status: 403,
6
11
  body: { errors: [{ message: "Forbidden" }] }
7
12
  };
13
+ const unavailable = {
14
+ status: 503,
15
+ body: { errors: [{ message: "Poll options unavailable" }] }
16
+ };
17
+ /** Scan-shape parity for a tally-served field: label from the live instance, else the field name. */
18
+ const tallyMetaOf = (instance, field) => ({
19
+ label: typeof instance?.label === "string" && instance.label.length > 0 ? instance.label : field,
20
+ fieldType: instance?.blockType
21
+ });
8
22
  /**
9
23
  * Authorize and resolve a poll/survey results request. Authed callers may aggregate any field (or all
10
- * enumerable fields). Anonymous callers are allowed only when the form opted in (`showResults`), and then
11
- * only for the configured `resultsField`, and only if that field is enumerable (has options) so a
12
- * misconfigured `resultsField` pointing at a free-text or PII field can never be dumped publicly. Returns
13
- * only aggregate counts, never raw submissions.
24
+ * enumerable fields) and bypass the `access` seam; for a poll-enabled form they also get the results
25
+ * field's effective options injected so sourced/resolver-backed polls render labels (best-effort, a
26
+ * resolve failure degrades to raw values rather than blocking the trusted read). Anonymous callers are
27
+ * allowed only when the form's poll is enabled, the poll's `resultsVisibility` permits it (`afterVote`:
28
+ * any time; `afterClose`: only once `closesAt` has passed), and the optional host `access` seam
29
+ * approves; and then only for the configured `poll.resultsField`, and only if that field is enumerable
30
+ * so a misconfigured `resultsField` pointing at a free-text or PII field can never be dumped publicly. A
31
+ * static poll is enumerable through its authored options; a poll whose options come from an
32
+ * `optionSource` or the field type's own `resolveOptions` resolves them here (registry via
33
+ * `config.custom`, per-request cache), gated by `eligibleTypes` and enumerable only when resolution
34
+ * yields any, with the resolved options driving bucket order and labels. Resolution failure fails
35
+ * closed (503) on the anonymous path. Returns only aggregate counts, never raw submissions.
14
36
  */
15
37
  const resolveFormResultsRequest = async (args) => {
16
- const { payload, formId, field, isAuthed, req } = args;
38
+ const { payload, formId, field, isAuthed, req, access, eligibleTypes, pollVotesEnabled } = args;
17
39
  if (formId == null) return {
18
40
  status: 400,
19
41
  body: { errors: [{ message: "Missing form id" }] }
@@ -29,15 +51,78 @@ const resolveFormResultsRequest = async (args) => {
29
51
  status: 404,
30
52
  body: { errors: [{ message: "Not found" }] }
31
53
  };
54
+ if (shouldAutoResolvePoll(form)) await resolvePollOutcome({
55
+ payload,
56
+ formId,
57
+ req,
58
+ pollVotesEnabled
59
+ }).catch(() => void 0);
32
60
  let fields;
33
- if (isAuthed) fields = field ? [field] : void 0;
34
- else {
35
- if (form.showResults !== true) return forbidden;
36
- const publicField = typeof form.resultsField === "string" && form.resultsField.length > 0 ? form.resultsField : void 0;
61
+ let resolvedOptions;
62
+ if (isAuthed) {
63
+ fields = field ? [field] : void 0;
64
+ const poll = pollConfigOf(form.poll);
65
+ const resultsField = typeof poll?.resultsField === "string" && poll.resultsField.length > 0 ? poll.resultsField : void 0;
66
+ if (form.pollEnabled === true && resultsField && (!field || field === resultsField)) {
67
+ const options = await resolveEffectivePollOptions({
68
+ payload,
69
+ req,
70
+ form
71
+ }).catch(() => []);
72
+ if (options.length > 0) resolvedOptions = { [resultsField]: options };
73
+ if (pollVotesEnabled === true && field === resultsField) return {
74
+ status: 200,
75
+ body: { results: [await aggregateFromVotes({
76
+ payload,
77
+ formId,
78
+ field: resultsField,
79
+ meta: tallyMetaOf((Array.isArray(form.fields) ? form.fields : []).find((entry) => entry.name === resultsField), resultsField),
80
+ options: resolvedOptions?.[resultsField] ?? [],
81
+ req
82
+ })] }
83
+ };
84
+ }
85
+ } else {
86
+ const poll = pollConfigOf(form.poll);
87
+ if (form.pollEnabled !== true || !poll) return forbidden;
88
+ if (poll.resultsVisibility === "afterClose" && !isPollClosed(poll)) return forbidden;
89
+ if (access) {
90
+ if (!(req ? await access({
91
+ req,
92
+ form
93
+ }) : false)) return forbidden;
94
+ }
95
+ const publicField = typeof poll.resultsField === "string" && poll.resultsField.length > 0 ? poll.resultsField : void 0;
37
96
  if (!publicField) return forbidden;
38
97
  if (field && field !== publicField) return forbidden;
39
98
  const instance = (Array.isArray(form.fields) ? form.fields : []).find((entry) => entry.name === publicField);
40
- if (!instance || !fieldHasOptions(instance)) return forbidden;
99
+ if (!instance) return forbidden;
100
+ const hasSource = typeof poll.optionSource === "string" && poll.optionSource.length > 0;
101
+ const authored = fieldHasOptions(instance);
102
+ if ((hasSource || !authored) && eligibleTypes && !eligibleTypes.includes(instance.blockType)) return forbidden;
103
+ let options;
104
+ try {
105
+ options = await resolveEffectivePollOptions({
106
+ payload,
107
+ req,
108
+ form
109
+ });
110
+ } catch {
111
+ return unavailable;
112
+ }
113
+ if (options.length === 0) return forbidden;
114
+ if (pollVotesEnabled === true) return {
115
+ status: 200,
116
+ body: { results: [await aggregateFromVotes({
117
+ payload,
118
+ formId,
119
+ field: publicField,
120
+ meta: tallyMetaOf(instance, publicField),
121
+ options,
122
+ req
123
+ })] }
124
+ };
125
+ resolvedOptions = { [publicField]: options };
41
126
  fields = [publicField];
42
127
  }
43
128
  return {
@@ -46,7 +131,8 @@ const resolveFormResultsRequest = async (args) => {
46
131
  payload,
47
132
  formId,
48
133
  fields,
49
- req
134
+ req,
135
+ resolvedOptions
50
136
  }) }
51
137
  };
52
138
  };
@@ -1 +1 @@
1
- {"version":3,"file":"resolveResultsRequest.js","names":[],"sources":["../../src/aggregation/resolveResultsRequest.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { FORMS_SLUG } from '../collections/forms'\nimport type { FormFieldInstance } from '../submissions/types'\nimport { aggregateFormResponses, fieldHasOptions } from './aggregateResponses'\nimport type { FieldAggregation } from './types'\n\nexport type ResolveResultsRequestArgs = {\n\tpayload: Payload\n\tformId: number | string | undefined\n\t/** The requested field (query param). */\n\tfield?: string\n\t/** Whether the caller is authenticated (an admin/user). */\n\tisAuthed: boolean\n\treq?: PayloadRequest\n}\n\nexport type ResolveResultsRequestResult = {\n\tstatus: number\n\tbody: { results: FieldAggregation[] } | { errors: { message: string }[] }\n}\n\nconst forbidden: ResolveResultsRequestResult = {\n\tstatus: 403,\n\tbody: { errors: [{ message: 'Forbidden' }] },\n}\n\n/**\n * Authorize and resolve a poll/survey results request. Authed callers may aggregate any field (or all\n * enumerable fields). Anonymous callers are allowed only when the form opted in (`showResults`), and then\n * only for the configured `resultsField`, and only if that field is enumerable (has options) so a\n * misconfigured `resultsField` pointing at a free-text or PII field can never be dumped publicly. Returns\n * only aggregate counts, never raw submissions.\n */\nexport const resolveFormResultsRequest = async (\n\targs: ResolveResultsRequestArgs\n): Promise<ResolveResultsRequestResult> => {\n\tconst { payload, formId, field, isAuthed, req } = args\n\tif (formId == null) {\n\t\treturn { status: 400, body: { errors: [{ message: 'Missing form id' }] } }\n\t}\n\tconst form = await payload\n\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, overrideAccess: true, req })\n\t\t.catch(() => null)\n\tif (!form) {\n\t\treturn { status: 404, body: { errors: [{ message: 'Not found' }] } }\n\t}\n\n\tlet fields: string[] | undefined\n\tif (isAuthed) {\n\t\tfields = field ? [field] : undefined\n\t} else {\n\t\tif (form.showResults !== true) {\n\t\t\treturn forbidden\n\t\t}\n\t\tconst publicField =\n\t\t\ttypeof form.resultsField === 'string' && form.resultsField.length > 0\n\t\t\t\t? form.resultsField\n\t\t\t\t: undefined\n\t\tif (!publicField) {\n\t\t\treturn forbidden\n\t\t}\n\t\tif (field && field !== publicField) {\n\t\t\treturn forbidden\n\t\t}\n\t\tconst instances = Array.isArray(form.fields) ? (form.fields as FormFieldInstance[]) : []\n\t\tconst instance = instances.find((entry) => entry.name === publicField)\n\t\tif (!instance || !fieldHasOptions(instance)) {\n\t\t\treturn forbidden\n\t\t}\n\t\tfields = [publicField]\n\t}\n\n\tconst results = await aggregateFormResponses({ payload, formId, fields, req })\n\treturn { status: 200, body: { results } }\n}\n"],"mappings":";;;AAqBA,MAAM,YAAyC;CAC9C,QAAQ;CACR,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;AAC5C;;;;;;;;AASA,MAAa,4BAA4B,OACxC,SAC0C;CAC1C,MAAM,EAAE,SAAS,QAAQ,OAAO,UAAU,QAAQ;CAClD,IAAI,UAAU,MACb,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,kBAAkB,CAAC,EAAE;CAAE;CAE1E,MAAM,OAAO,MAAM,QACjB,SAAS;EAAE,YAAY;EAAY,IAAI;EAAQ,OAAO;EAAG,gBAAgB;EAAM;CAAI,CAAC,EACpF,YAAY,IAAI;CAClB,IAAI,CAAC,MACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAGpE,IAAI;CACJ,IAAI,UACH,SAAS,QAAQ,CAAC,KAAK,IAAI,KAAA;MACrB;EACN,IAAI,KAAK,gBAAgB,MACxB,OAAO;EAER,MAAM,cACL,OAAO,KAAK,iBAAiB,YAAY,KAAK,aAAa,SAAS,IACjE,KAAK,eACL,KAAA;EACJ,IAAI,CAAC,aACJ,OAAO;EAER,IAAI,SAAS,UAAU,aACtB,OAAO;EAGR,MAAM,YADY,MAAM,QAAQ,KAAK,MAAM,IAAK,KAAK,SAAiC,CAAC,GAC5D,MAAM,UAAU,MAAM,SAAS,WAAW;EACrE,IAAI,CAAC,YAAY,CAAC,gBAAgB,QAAQ,GACzC,OAAO;EAER,SAAS,CAAC,WAAW;CACtB;CAGA,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,SAAA,MADR,uBAAuB;GAAE;GAAS;GAAQ;GAAQ;EAAI,CAAC,EACvC;CAAE;AACzC"}
1
+ {"version":3,"file":"resolveResultsRequest.js","names":[],"sources":["../../src/aggregation/resolveResultsRequest.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { FORMS_SLUG } from '../collections/forms'\nimport { isPollClosed, pollConfigOf } from '../form/pollState'\nimport { type PollFormLike, shouldAutoResolvePoll } from '../poll/closeJob'\nimport type { PollOption } from '../poll/definePollOptionSource'\nimport { resolveEffectivePollOptions } from '../poll/effectivePollOptions'\nimport { resolvePollOutcome } from '../poll/resolvePollOutcome'\nimport { aggregateFromVotes } from '../poll/votes/aggregateFromVotes'\nimport type { FormFieldInstance } from '../submissions/types'\nimport { aggregateFormResponses, fieldHasOptions } from './aggregateResponses'\nimport type { FieldAggregation } from './types'\n\nexport type FormResultsAccessArgs = {\n\treq: PayloadRequest\n\t/** The loaded forms document (depth 0). Untyped beyond `id`: hosts read their own fields (e.g. `form.tenant`). */\n\tform: { id: number | string } & Record<string, unknown>\n}\n\n/**\n * Host seam gating anonymous results reads, evaluated after the form is loaded and before anything\n * is aggregated. Multi-tenant recipe: compare `form.tenant` against the tenant derived from `req`\n * (host header, cookie, or auth context) and return `false` for a cross-tenant read. Authenticated\n * callers bypass this seam (they are admin-trusted, like the rest of the results endpoint).\n */\nexport type FormResultsAccess = (args: FormResultsAccessArgs) => boolean | Promise<boolean>\n\nexport type ResolveResultsRequestArgs = {\n\tpayload: Payload\n\tformId: number | string | undefined\n\t/** The requested field (query param). */\n\tfield?: string\n\t/** Whether the caller is authenticated (an admin/user). */\n\tisAuthed: boolean\n\treq?: PayloadRequest\n\t/** Optional host seam for anonymous reads; absent means plugin-default gating only. */\n\taccess?: FormResultsAccess\n\t/**\n\t * Poll-eligible field types (registry-derived). When set, an anonymous read of an\n\t * option-source poll also requires the results field's instance to be one of these types,\n\t * so legacy or db-written docs pointing at a free-text field can never expose stored\n\t * answers as leftover result buckets.\n\t */\n\teligibleTypes?: readonly string[]\n\t/**\n\t * Whether the hidden tally store backs poll reads. When true, the poll results field is served\n\t * from `aggregateFromVotes` (anonymous reads, and an authed read naming exactly that field)\n\t * instead of the submission scan; every authorization gate stays identical either way.\n\t */\n\tpollVotesEnabled?: boolean\n}\n\nexport type ResolveResultsRequestResult = {\n\tstatus: number\n\tbody: { results: FieldAggregation[] } | { errors: { message: string }[] }\n}\n\nconst forbidden: ResolveResultsRequestResult = {\n\tstatus: 403,\n\tbody: { errors: [{ message: 'Forbidden' }] },\n}\n\n// 503 over 403 for a failed option-source resolve: the form and its poll config are already\n// publicly readable, so \"temporarily unavailable\" leaks nothing new, matches the submission\n// path's precedent, and does not mislabel a transient server fault as an authorization denial.\nconst unavailable: ResolveResultsRequestResult = {\n\tstatus: 503,\n\tbody: { errors: [{ message: 'Poll options unavailable' }] },\n}\n\n/** Scan-shape parity for a tally-served field: label from the live instance, else the field name. */\nconst tallyMetaOf = (\n\tinstance: FormFieldInstance | undefined,\n\tfield: string\n): { label: string; fieldType?: string } => ({\n\tlabel: typeof instance?.label === 'string' && instance.label.length > 0 ? instance.label : field,\n\tfieldType: instance?.blockType,\n})\n\n/**\n * Authorize and resolve a poll/survey results request. Authed callers may aggregate any field (or all\n * enumerable fields) and bypass the `access` seam; for a poll-enabled form they also get the results\n * field's effective options injected so sourced/resolver-backed polls render labels (best-effort, a\n * resolve failure degrades to raw values rather than blocking the trusted read). Anonymous callers are\n * allowed only when the form's poll is enabled, the poll's `resultsVisibility` permits it (`afterVote`:\n * any time; `afterClose`: only once `closesAt` has passed), and the optional host `access` seam\n * approves; and then only for the configured `poll.resultsField`, and only if that field is enumerable\n * so a misconfigured `resultsField` pointing at a free-text or PII field can never be dumped publicly. A\n * static poll is enumerable through its authored options; a poll whose options come from an\n * `optionSource` or the field type's own `resolveOptions` resolves them here (registry via\n * `config.custom`, per-request cache), gated by `eligibleTypes` and enumerable only when resolution\n * yields any, with the resolved options driving bucket order and labels. Resolution failure fails\n * closed (503) on the anonymous path. Returns only aggregate counts, never raw submissions.\n */\nexport const resolveFormResultsRequest = async (\n\targs: ResolveResultsRequestArgs\n): Promise<ResolveResultsRequestResult> => {\n\tconst { payload, formId, field, isAuthed, req, access, eligibleTypes, pollVotesEnabled } = args\n\tif (formId == null) {\n\t\treturn { status: 400, body: { errors: [{ message: 'Missing form id' }] } }\n\t}\n\tconst form = await payload\n\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, overrideAccess: true, req })\n\t\t.catch(() => null)\n\tif (!form) {\n\t\treturn { status: 404, body: { errors: [{ message: 'Not found' }] } }\n\t}\n\n\t// No-runner safety net: when a closed poll's `mostVoted`/`source` outcome was never auto-resolved\n\t// (no wired job runner fired the close task), a results read heals it. Idempotent and gate-neutral,\n\t// it runs on the already-loaded form via overrideAccess and writes only through the outcome hook, so\n\t// it cannot relax the anonymous authorization below. A failure degrades silently to the normal path.\n\tif (shouldAutoResolvePoll(form as PollFormLike)) {\n\t\tawait resolvePollOutcome({ payload, formId, req, pollVotesEnabled }).catch(() => undefined)\n\t}\n\n\tlet fields: string[] | undefined\n\tlet resolvedOptions: Record<string, { value: string; label: string }[]> | undefined\n\tif (isAuthed) {\n\t\tfields = field ? [field] : undefined\n\t\t// Label a poll's results field for admins: resolve its effective options (a source, the\n\t\t// field type's own resolver, or authored options) and inject them so a sourced/resolved poll\n\t\t// shows labels instead of raw stored values, and so an all-fields read still surfaces a\n\t\t// resolver-backed field that carries no authored options. Best-effort: a resolver/source\n\t\t// failure degrades to raw values rather than blocking the trusted read (the anonymous path\n\t\t// below fails closed instead). Only run when the read would actually surface the results field.\n\t\tconst poll = pollConfigOf(form.poll)\n\t\tconst resultsField =\n\t\t\ttypeof poll?.resultsField === 'string' && poll.resultsField.length > 0\n\t\t\t\t? poll.resultsField\n\t\t\t\t: undefined\n\t\tif (form.pollEnabled === true && resultsField && (!field || field === resultsField)) {\n\t\t\tconst options: PollOption[] = await resolveEffectivePollOptions({\n\t\t\t\tpayload,\n\t\t\t\treq,\n\t\t\t\tform,\n\t\t\t}).catch(() => [])\n\t\t\tif (options.length > 0) {\n\t\t\t\tresolvedOptions = { [resultsField]: options }\n\t\t\t}\n\t\t\t// Only the explicit single-field poll read switches to the tally store; an all-fields\n\t\t\t// (or multi-field) read keeps the scan, which is the only source for non-poll fields.\n\t\t\tif (pollVotesEnabled === true && field === resultsField) {\n\t\t\t\tconst instances = Array.isArray(form.fields) ? (form.fields as FormFieldInstance[]) : []\n\t\t\t\tconst instance = instances.find((entry) => entry.name === resultsField)\n\t\t\t\tconst aggregation = await aggregateFromVotes({\n\t\t\t\t\tpayload,\n\t\t\t\t\tformId,\n\t\t\t\t\tfield: resultsField,\n\t\t\t\t\tmeta: tallyMetaOf(instance, resultsField),\n\t\t\t\t\toptions: resolvedOptions?.[resultsField] ?? [],\n\t\t\t\t\treq,\n\t\t\t\t})\n\t\t\t\treturn { status: 200, body: { results: [aggregation] } }\n\t\t\t}\n\t\t}\n\t} else {\n\t\tconst poll = pollConfigOf(form.poll)\n\t\tif (form.pollEnabled !== true || !poll) {\n\t\t\treturn forbidden\n\t\t}\n\t\tif (poll.resultsVisibility === 'afterClose' && !isPollClosed(poll)) {\n\t\t\treturn forbidden\n\t\t}\n\t\tif (access) {\n\t\t\t// No req means the seam cannot be evaluated; fail closed rather than skip a configured gate.\n\t\t\t// The concrete generated Form doc has no index signature; the seam's Record<string, unknown>\n\t\t\t// is the ergonomic host-facing contract, so widen the doc to it here.\n\t\t\tconst allowed = req\n\t\t\t\t? await access({ req, form: form as unknown as FormResultsAccessArgs['form'] })\n\t\t\t\t: false\n\t\t\tif (!allowed) {\n\t\t\t\treturn forbidden\n\t\t\t}\n\t\t}\n\t\tconst publicField =\n\t\t\ttypeof poll.resultsField === 'string' && poll.resultsField.length > 0\n\t\t\t\t? poll.resultsField\n\t\t\t\t: undefined\n\t\tif (!publicField) {\n\t\t\treturn forbidden\n\t\t}\n\t\tif (field && field !== publicField) {\n\t\t\treturn forbidden\n\t\t}\n\t\tconst instances = Array.isArray(form.fields) ? (form.fields as FormFieldInstance[]) : []\n\t\tconst instance = instances.find((entry) => entry.name === publicField)\n\t\tif (!instance) {\n\t\t\treturn forbidden\n\t\t}\n\t\tconst hasSource = typeof poll.optionSource === 'string' && poll.optionSource.length > 0\n\t\tconst authored = fieldHasOptions(instance)\n\t\t// Options that don't come from the author's own literal `options` list (a source or a\n\t\t// field-typed resolver) require the results field to be a poll-eligible type, so a legacy or\n\t\t// db-written doc pointing `resultsField` at a free-text field can never dump raw answers as\n\t\t// leftover buckets. A static authored poll is a choice field already, so it keeps serving.\n\t\tif ((hasSource || !authored) && eligibleTypes && !eligibleTypes.includes(instance.blockType)) {\n\t\t\treturn forbidden\n\t\t}\n\t\tlet options: PollOption[]\n\t\ttry {\n\t\t\toptions = await resolveEffectivePollOptions({ payload, req, form })\n\t\t} catch {\n\t\t\treturn unavailable\n\t\t}\n\t\tif (options.length === 0) {\n\t\t\treturn forbidden\n\t\t}\n\t\tif (pollVotesEnabled === true) {\n\t\t\tconst aggregation = await aggregateFromVotes({\n\t\t\t\tpayload,\n\t\t\t\tformId,\n\t\t\t\tfield: publicField,\n\t\t\t\tmeta: tallyMetaOf(instance, publicField),\n\t\t\t\toptions,\n\t\t\t\treq,\n\t\t\t})\n\t\t\treturn { status: 200, body: { results: [aggregation] } }\n\t\t}\n\t\tresolvedOptions = { [publicField]: options }\n\t\tfields = [publicField]\n\t}\n\n\tconst results = await aggregateFormResponses({ payload, formId, fields, req, resolvedOptions })\n\treturn { status: 200, body: { results } }\n}\n"],"mappings":";;;;;;;;AAwDA,MAAM,YAAyC;CAC9C,QAAQ;CACR,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;AAC5C;AAKA,MAAM,cAA2C;CAChD,QAAQ;CACR,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,2BAA2B,CAAC,EAAE;AAC3D;;AAGA,MAAM,eACL,UACA,WAC4C;CAC5C,OAAO,OAAO,UAAU,UAAU,YAAY,SAAS,MAAM,SAAS,IAAI,SAAS,QAAQ;CAC3F,WAAW,UAAU;AACtB;;;;;;;;;;;;;;;;AAiBA,MAAa,4BAA4B,OACxC,SAC0C;CAC1C,MAAM,EAAE,SAAS,QAAQ,OAAO,UAAU,KAAK,QAAQ,eAAe,qBAAqB;CAC3F,IAAI,UAAU,MACb,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,kBAAkB,CAAC,EAAE;CAAE;CAE1E,MAAM,OAAO,MAAM,QACjB,SAAS;EAAE,YAAY;EAAY,IAAI;EAAQ,OAAO;EAAG,gBAAgB;EAAM;CAAI,CAAC,EACpF,YAAY,IAAI;CAClB,IAAI,CAAC,MACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAOpE,IAAI,sBAAsB,IAAoB,GAC7C,MAAM,mBAAmB;EAAE;EAAS;EAAQ;EAAK;CAAiB,CAAC,EAAE,YAAY,KAAA,CAAS;CAG3F,IAAI;CACJ,IAAI;CACJ,IAAI,UAAU;EACb,SAAS,QAAQ,CAAC,KAAK,IAAI,KAAA;EAO3B,MAAM,OAAO,aAAa,KAAK,IAAI;EACnC,MAAM,eACL,OAAO,MAAM,iBAAiB,YAAY,KAAK,aAAa,SAAS,IAClE,KAAK,eACL,KAAA;EACJ,IAAI,KAAK,gBAAgB,QAAQ,iBAAiB,CAAC,SAAS,UAAU,eAAe;GACpF,MAAM,UAAwB,MAAM,4BAA4B;IAC/D;IACA;IACA;GACD,CAAC,EAAE,YAAY,CAAC,CAAC;GACjB,IAAI,QAAQ,SAAS,GACpB,kBAAkB,GAAG,eAAe,QAAQ;GAI7C,IAAI,qBAAqB,QAAQ,UAAU,cAW1C,OAAO;IAAE,QAAQ;IAAK,MAAM,EAAE,SAAS,CAAC,MARd,mBAAmB;KAC5C;KACA;KACA,OAAO;KACP,MAAM,aANW,MAAM,QAAQ,KAAK,MAAM,IAAK,KAAK,SAAiC,CAAC,GAC5D,MAAM,UAAU,MAAM,SAAS,YAKhC,GAAG,YAAY;KACxC,SAAS,kBAAkB,iBAAiB,CAAC;KAC7C;IACD,CAAC,CACkD,EAAE;GAAE;EAEzD;CACD,OAAO;EACN,MAAM,OAAO,aAAa,KAAK,IAAI;EACnC,IAAI,KAAK,gBAAgB,QAAQ,CAAC,MACjC,OAAO;EAER,IAAI,KAAK,sBAAsB,gBAAgB,CAAC,aAAa,IAAI,GAChE,OAAO;EAER,IAAI;OAOC,EAHY,MACb,MAAM,OAAO;IAAE;IAAW;GAAiD,CAAC,IAC5E,QAEF,OAAO;EAAA;EAGT,MAAM,cACL,OAAO,KAAK,iBAAiB,YAAY,KAAK,aAAa,SAAS,IACjE,KAAK,eACL,KAAA;EACJ,IAAI,CAAC,aACJ,OAAO;EAER,IAAI,SAAS,UAAU,aACtB,OAAO;EAGR,MAAM,YADY,MAAM,QAAQ,KAAK,MAAM,IAAK,KAAK,SAAiC,CAAC,GAC5D,MAAM,UAAU,MAAM,SAAS,WAAW;EACrE,IAAI,CAAC,UACJ,OAAO;EAER,MAAM,YAAY,OAAO,KAAK,iBAAiB,YAAY,KAAK,aAAa,SAAS;EACtF,MAAM,WAAW,gBAAgB,QAAQ;EAKzC,KAAK,aAAa,CAAC,aAAa,iBAAiB,CAAC,cAAc,SAAS,SAAS,SAAS,GAC1F,OAAO;EAER,IAAI;EACJ,IAAI;GACH,UAAU,MAAM,4BAA4B;IAAE;IAAS;IAAK;GAAK,CAAC;EACnE,QAAQ;GACP,OAAO;EACR;EACA,IAAI,QAAQ,WAAW,GACtB,OAAO;EAER,IAAI,qBAAqB,MASxB,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,SAAS,CAAC,MARd,mBAAmB;IAC5C;IACA;IACA,OAAO;IACP,MAAM,YAAY,UAAU,WAAW;IACvC;IACA;GACD,CAAC,CACkD,EAAE;EAAE;EAExD,kBAAkB,GAAG,cAAc,QAAQ;EAC3C,SAAS,CAAC,WAAW;CACtB;CAGA,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,SAAA,MADR,uBAAuB;GAAE;GAAS;GAAQ;GAAQ;GAAK;EAAgB,CAAC,EACxD;CAAE;AACzC"}
@@ -1,5 +1,6 @@
1
- import { FormFieldInstance } from "../submissions/types.js";
2
1
  import { CalcExpression } from "./types.js";
2
+ import { FormFieldInstance } from "../submissions/types.js";
3
+ import { CalcResolved } from "./evaluate.js";
3
4
 
4
5
  //#region src/calc/computeCalcFields.d.ts
5
6
  /** Returns the calc expression if the field carries one (non-null object), otherwise undefined. */
@@ -7,9 +8,10 @@ declare const calcExpressionOf: (field: FormFieldInstance) => CalcExpression | u
7
8
  /**
8
9
  * Returns answers with every calc field's value derived from its expression,
9
10
  * folded in declaration order so a calc may reference an earlier calc's result.
10
- * Identity when no field carries an expression.
11
+ * Identity when no field carries an expression. `resolved` threads server-resolved
12
+ * source values, weight maps, and custom functions into each evaluation.
11
13
  */
12
- declare const computeCalcFields: (fields: FormFieldInstance[], answers: Record<string, unknown>) => Record<string, unknown>;
14
+ declare const computeCalcFields: (fields: FormFieldInstance[], answers: Record<string, unknown>, resolved?: CalcResolved) => Record<string, unknown>;
13
15
  //#endregion
14
16
  export { calcExpressionOf, computeCalcFields };
15
17
  //# sourceMappingURL=computeCalcFields.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { isNamedField } from "../fields/fieldKey.js";
1
2
  import { evaluateCalc } from "./evaluate.js";
2
3
  //#region src/calc/computeCalcFields.ts
3
4
  /** Returns the calc expression if the field carries one (non-null object), otherwise undefined. */
@@ -8,15 +9,16 @@ const calcExpressionOf = (field) => {
8
9
  /**
9
10
  * Returns answers with every calc field's value derived from its expression,
10
11
  * folded in declaration order so a calc may reference an earlier calc's result.
11
- * Identity when no field carries an expression.
12
+ * Identity when no field carries an expression. `resolved` threads server-resolved
13
+ * source values, weight maps, and custom functions into each evaluation.
12
14
  */
13
- const computeCalcFields = (fields, answers) => {
15
+ const computeCalcFields = (fields, answers, resolved) => {
14
16
  let next = answers;
15
17
  for (const field of fields) {
16
18
  const expr = calcExpressionOf(field);
17
- if (expr) next = {
19
+ if (expr && isNamedField(field)) next = {
18
20
  ...next,
19
- [field.name]: evaluateCalc(expr, next)
21
+ [field.name]: evaluateCalc(expr, next, resolved)
20
22
  };
21
23
  }
22
24
  return next;
@@ -1 +1 @@
1
- {"version":3,"file":"computeCalcFields.js","names":[],"sources":["../../src/calc/computeCalcFields.ts"],"sourcesContent":["import type { FormFieldInstance } from '../submissions/types'\nimport { evaluateCalc } from './evaluate'\nimport type { CalcExpression } from './types'\n\n/** Returns the calc expression if the field carries one (non-null object), otherwise undefined. */\nexport const calcExpressionOf = (field: FormFieldInstance): CalcExpression | undefined => {\n\tconst expr = field.expression\n\treturn expr !== null && typeof expr === 'object' && !Array.isArray(expr)\n\t\t? (expr as CalcExpression)\n\t\t: undefined\n}\n\n/**\n * Returns answers with every calc field's value derived from its expression,\n * folded in declaration order so a calc may reference an earlier calc's result.\n * Identity when no field carries an expression.\n */\nexport const computeCalcFields = (\n\tfields: FormFieldInstance[],\n\tanswers: Record<string, unknown>\n): Record<string, unknown> => {\n\tlet next = answers\n\tfor (const field of fields) {\n\t\tconst expr = calcExpressionOf(field)\n\t\tif (expr) {\n\t\t\tnext = { ...next, [field.name]: evaluateCalc(expr, next) }\n\t\t}\n\t}\n\treturn next\n}\n"],"mappings":";;;AAKA,MAAa,oBAAoB,UAAyD;CACzF,MAAM,OAAO,MAAM;CACnB,OAAO,SAAS,QAAQ,OAAO,SAAS,YAAY,CAAC,MAAM,QAAQ,IAAI,IACnE,OACD,KAAA;AACJ;;;;;;AAOA,MAAa,qBACZ,QACA,YAC6B;CAC7B,IAAI,OAAO;CACX,KAAK,MAAM,SAAS,QAAQ;EAC3B,MAAM,OAAO,iBAAiB,KAAK;EACnC,IAAI,MACH,OAAO;GAAE,GAAG;IAAO,MAAM,OAAO,aAAa,MAAM,IAAI;EAAE;CAE3D;CACA,OAAO;AACR"}
1
+ {"version":3,"file":"computeCalcFields.js","names":[],"sources":["../../src/calc/computeCalcFields.ts"],"sourcesContent":["import { isNamedField } from '../fields/fieldKey'\nimport type { FormFieldInstance } from '../submissions/types'\nimport { type CalcResolved, evaluateCalc } from './evaluate'\nimport type { CalcExpression } from './types'\n\n/** Returns the calc expression if the field carries one (non-null object), otherwise undefined. */\nexport const calcExpressionOf = (field: FormFieldInstance): CalcExpression | undefined => {\n\tconst expr = field.expression\n\treturn expr !== null && typeof expr === 'object' && !Array.isArray(expr)\n\t\t? (expr as CalcExpression)\n\t\t: undefined\n}\n\n/**\n * Returns answers with every calc field's value derived from its expression,\n * folded in declaration order so a calc may reference an earlier calc's result.\n * Identity when no field carries an expression. `resolved` threads server-resolved\n * source values, weight maps, and custom functions into each evaluation.\n */\nexport const computeCalcFields = (\n\tfields: FormFieldInstance[],\n\tanswers: Record<string, unknown>,\n\tresolved?: CalcResolved\n): Record<string, unknown> => {\n\tlet next = answers\n\tfor (const field of fields) {\n\t\tconst expr = calcExpressionOf(field)\n\t\tif (expr && isNamedField(field)) {\n\t\t\tnext = { ...next, [field.name]: evaluateCalc(expr, next, resolved) }\n\t\t}\n\t}\n\treturn next\n}\n"],"mappings":";;;;AAMA,MAAa,oBAAoB,UAAyD;CACzF,MAAM,OAAO,MAAM;CACnB,OAAO,SAAS,QAAQ,OAAO,SAAS,YAAY,CAAC,MAAM,QAAQ,IAAI,IACnE,OACD,KAAA;AACJ;;;;;;;AAQA,MAAa,qBACZ,QACA,SACA,aAC6B;CAC7B,IAAI,OAAO;CACX,KAAK,MAAM,SAAS,QAAQ;EAC3B,MAAM,OAAO,iBAAiB,KAAK;EACnC,IAAI,QAAQ,aAAa,KAAK,GAC7B,OAAO;GAAE,GAAG;IAAO,MAAM,OAAO,aAAa,MAAM,MAAM,QAAQ;EAAE;CAErE;CACA,OAAO;AACR"}
@@ -1,8 +1,31 @@
1
1
  import { CalcExpression } from "./types.js";
2
2
 
3
3
  //#region src/calc/evaluate.d.ts
4
- /** Evaluate a calc expression against form answers. Total + safe: no `eval`, always finite, div/mod by zero -> 0, missing ref -> 0, depth-guarded. Isomorphic (client + server). */
5
- declare const evaluateCalc: (expr: CalcExpression | null | undefined, answers: Record<string, unknown>) => number;
4
+ /**
5
+ * Server-resolved calc extension values, threaded into evaluation as data (never re-resolved
6
+ * here, so the evaluator stays sync and isomorphic). `sources`/`weights` are serializable and
7
+ * ride the form document for the client's live preview; `functions` never serialize and are
8
+ * supplied per environment (registry `apply` fns on the server, the Form prop on the client).
9
+ */
10
+ type CalcResolved = {
11
+ sources?: Record<string, number>; /** Keyed `source + ' ' + field` (see `calcWeightKey` / `resolveCalcContext`). */
12
+ weights?: Record<string, Record<string, number>>;
13
+ functions?: Record<string, (args: number[]) => number>;
14
+ };
15
+ /**
16
+ * The `CalcResolved.weights` key for a sourced weight node: source key + space + field name.
17
+ * Unambiguous because registered source keys can never contain a space
18
+ * (`assertValidCalcSourceKeys` enforces `[\w.-]` at boot), so the first space always ends the
19
+ * source segment.
20
+ */
21
+ declare const calcWeightKey: (source: string, field: string) => string;
22
+ /**
23
+ * Evaluate a calc expression against form answers. Total + safe: no `eval`, always finite, div/mod
24
+ * by zero -> 0, missing ref -> 0, depth-guarded. Isomorphic (client + server). `resolved` supplies
25
+ * server-resolved source values, sourced weight maps, and custom functions; any extension node
26
+ * whose value is absent evaluates to 0.
27
+ */
28
+ declare const evaluateCalc: (expr: CalcExpression | null | undefined, answers: Record<string, unknown>, resolved?: CalcResolved) => number;
6
29
  //#endregion
7
- export { evaluateCalc };
30
+ export { CalcResolved, calcWeightKey, evaluateCalc };
8
31
  //# sourceMappingURL=evaluate.d.ts.map
@@ -1,5 +1,11 @@
1
1
  //#region src/calc/evaluate.ts
2
- const MAX_DEPTH = 64;
2
+ /**
3
+ * The `CalcResolved.weights` key for a sourced weight node: source key + space + field name.
4
+ * Unambiguous because registered source keys can never contain a space
5
+ * (`assertValidCalcSourceKeys` enforces `[\w.-]` at boot), so the first space always ends the
6
+ * source segment.
7
+ */
8
+ const calcWeightKey = (source, field) => `${source} ${field}`;
3
9
  const toNumber = (value) => {
4
10
  if (typeof value === "number") return Number.isFinite(value) ? value : 0;
5
11
  if (typeof value === "string" && value.trim() !== "") {
@@ -17,15 +23,17 @@ const FUNCTIONS = {
17
23
  ceil: (a) => Math.ceil(a[0] ?? 0),
18
24
  floor: (a) => Math.floor(a[0] ?? 0)
19
25
  };
20
- const evalNode = (expr, answers, depth) => {
21
- if (depth > MAX_DEPTH || expr == null || typeof expr !== "object") return 0;
26
+ const evalNode = (expr, ctx, depth) => {
27
+ if (depth > 64 || expr == null || typeof expr !== "object") return 0;
28
+ const { answers, resolved } = ctx;
22
29
  switch (expr.type) {
23
30
  case "lit": return finite(toNumber(expr.value));
24
31
  case "ref": return toNumber(answers[expr.field]);
25
- case "neg": return finite(-evalNode(expr.operand, answers, depth + 1));
32
+ case "source": return toNumber(resolved?.sources?.[expr.source]);
33
+ case "neg": return finite(-evalNode(expr.operand, ctx, depth + 1));
26
34
  case "op": {
27
- const l = evalNode(expr.left, answers, depth + 1);
28
- const r = evalNode(expr.right, answers, depth + 1);
35
+ const l = evalNode(expr.left, ctx, depth + 1);
36
+ const r = evalNode(expr.right, ctx, depth + 1);
29
37
  switch (expr.op) {
30
38
  case "+": return finite(l + r);
31
39
  case "-": return finite(l - r);
@@ -36,24 +44,37 @@ const evalNode = (expr, answers, depth) => {
36
44
  }
37
45
  }
38
46
  case "fn": {
39
- const fn = FUNCTIONS[expr.fn];
47
+ const fn = FUNCTIONS[expr.fn] ?? resolved?.functions?.[expr.fn];
40
48
  if (!fn) return 0;
41
- return finite(fn(Array.isArray(expr.args) ? expr.args.map((a) => evalNode(a, answers, depth + 1)) : []));
49
+ const args = Array.isArray(expr.args) ? expr.args.map((a) => evalNode(a, ctx, depth + 1)) : [];
50
+ try {
51
+ return finite(fn(args));
52
+ } catch {
53
+ return 0;
54
+ }
42
55
  }
43
56
  case "weight": {
44
- const weights = expr.weights ?? {};
57
+ const weights = typeof expr.source === "string" ? resolved?.weights?.[calcWeightKey(expr.source, expr.field)] ?? {} : expr.weights ?? {};
45
58
  const value = answers[expr.field];
46
59
  return finite((Array.isArray(value) ? value : value == null || value === "" ? [] : [value]).reduce((sum, v) => sum + toNumber(weights[String(v)]), 0));
47
60
  }
48
61
  default: return 0;
49
62
  }
50
63
  };
51
- /** Evaluate a calc expression against form answers. Total + safe: no `eval`, always finite, div/mod by zero -> 0, missing ref -> 0, depth-guarded. Isomorphic (client + server). */
52
- const evaluateCalc = (expr, answers) => {
64
+ /**
65
+ * Evaluate a calc expression against form answers. Total + safe: no `eval`, always finite, div/mod
66
+ * by zero -> 0, missing ref -> 0, depth-guarded. Isomorphic (client + server). `resolved` supplies
67
+ * server-resolved source values, sourced weight maps, and custom functions; any extension node
68
+ * whose value is absent evaluates to 0.
69
+ */
70
+ const evaluateCalc = (expr, answers, resolved) => {
53
71
  if (!expr) return 0;
54
- return evalNode(expr, answers, 0);
72
+ return evalNode(expr, {
73
+ answers,
74
+ resolved
75
+ }, 0);
55
76
  };
56
77
  //#endregion
57
- export { evaluateCalc };
78
+ export { calcWeightKey, evaluateCalc };
58
79
 
59
80
  //# sourceMappingURL=evaluate.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"evaluate.js","names":[],"sources":["../../src/calc/evaluate.ts"],"sourcesContent":["import type { CalcExpression } from './types'\n\nconst MAX_DEPTH = 64\n\nconst toNumber = (value: unknown): number => {\n\tif (typeof value === 'number') {\n\t\treturn Number.isFinite(value) ? value : 0\n\t}\n\tif (typeof value === 'string' && value.trim() !== '') {\n\t\tconst n = Number(value)\n\t\treturn Number.isFinite(n) ? n : 0\n\t}\n\treturn 0\n}\n\nconst finite = (n: number): number => (Number.isFinite(n) ? n : 0)\n\nconst FUNCTIONS: Record<string, (args: number[]) => number> = {\n\tmin: (a) => (a.length ? Math.min(...a) : 0),\n\tmax: (a) => (a.length ? Math.max(...a) : 0),\n\tround: (a) => Math.round(a[0] ?? 0),\n\tabs: (a) => Math.abs(a[0] ?? 0),\n\tceil: (a) => Math.ceil(a[0] ?? 0),\n\tfloor: (a) => Math.floor(a[0] ?? 0),\n}\n\nconst evalNode = (\n\texpr: CalcExpression,\n\tanswers: Record<string, unknown>,\n\tdepth: number\n): number => {\n\tif (depth > MAX_DEPTH || expr == null || typeof expr !== 'object') {\n\t\treturn 0\n\t}\n\tswitch (expr.type) {\n\t\tcase 'lit':\n\t\t\treturn finite(toNumber(expr.value))\n\t\tcase 'ref':\n\t\t\treturn toNumber(answers[expr.field])\n\t\tcase 'neg':\n\t\t\treturn finite(-evalNode(expr.operand, answers, depth + 1))\n\t\tcase 'op': {\n\t\t\tconst l = evalNode(expr.left, answers, depth + 1)\n\t\t\tconst r = evalNode(expr.right, answers, depth + 1)\n\t\t\tswitch (expr.op) {\n\t\t\t\tcase '+':\n\t\t\t\t\treturn finite(l + r)\n\t\t\t\tcase '-':\n\t\t\t\t\treturn finite(l - r)\n\t\t\t\tcase '*':\n\t\t\t\t\treturn finite(l * r)\n\t\t\t\tcase '/':\n\t\t\t\t\treturn r === 0 ? 0 : finite(l / r)\n\t\t\t\tcase '%':\n\t\t\t\t\treturn r === 0 ? 0 : finite(l % r)\n\t\t\t\tdefault:\n\t\t\t\t\treturn 0\n\t\t\t}\n\t\t}\n\t\tcase 'fn': {\n\t\t\tconst fn = FUNCTIONS[expr.fn]\n\t\t\tif (!fn) {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tconst args = Array.isArray(expr.args)\n\t\t\t\t? expr.args.map((a) => evalNode(a, answers, depth + 1))\n\t\t\t\t: []\n\t\t\treturn finite(fn(args))\n\t\t}\n\t\tcase 'weight': {\n\t\t\tconst weights = expr.weights ?? {}\n\t\t\tconst value = answers[expr.field]\n\t\t\tconst chosen = Array.isArray(value) ? value : value == null || value === '' ? [] : [value]\n\t\t\treturn finite(\n\t\t\t\tchosen.reduce((sum: number, v: unknown) => sum + toNumber(weights[String(v)]), 0)\n\t\t\t)\n\t\t}\n\t\tdefault:\n\t\t\treturn 0\n\t}\n}\n\n/** Evaluate a calc expression against form answers. Total + safe: no `eval`, always finite, div/mod by zero -> 0, missing ref -> 0, depth-guarded. Isomorphic (client + server). */\nexport const evaluateCalc = (\n\texpr: CalcExpression | null | undefined,\n\tanswers: Record<string, unknown>\n): number => {\n\tif (!expr) {\n\t\treturn 0\n\t}\n\treturn evalNode(expr, answers, 0)\n}\n"],"mappings":";AAEA,MAAM,YAAY;AAElB,MAAM,YAAY,UAA2B;CAC5C,IAAI,OAAO,UAAU,UACpB,OAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;CAEzC,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,MAAM,IAAI;EACrD,MAAM,IAAI,OAAO,KAAK;EACtB,OAAO,OAAO,SAAS,CAAC,IAAI,IAAI;CACjC;CACA,OAAO;AACR;AAEA,MAAM,UAAU,MAAuB,OAAO,SAAS,CAAC,IAAI,IAAI;AAEhE,MAAM,YAAwD;CAC7D,MAAM,MAAO,EAAE,SAAS,KAAK,IAAI,GAAG,CAAC,IAAI;CACzC,MAAM,MAAO,EAAE,SAAS,KAAK,IAAI,GAAG,CAAC,IAAI;CACzC,QAAQ,MAAM,KAAK,MAAM,EAAE,MAAM,CAAC;CAClC,MAAM,MAAM,KAAK,IAAI,EAAE,MAAM,CAAC;CAC9B,OAAO,MAAM,KAAK,KAAK,EAAE,MAAM,CAAC;CAChC,QAAQ,MAAM,KAAK,MAAM,EAAE,MAAM,CAAC;AACnC;AAEA,MAAM,YACL,MACA,SACA,UACY;CACZ,IAAI,QAAQ,aAAa,QAAQ,QAAQ,OAAO,SAAS,UACxD,OAAO;CAER,QAAQ,KAAK,MAAb;EACC,KAAK,OACJ,OAAO,OAAO,SAAS,KAAK,KAAK,CAAC;EACnC,KAAK,OACJ,OAAO,SAAS,QAAQ,KAAK,MAAM;EACpC,KAAK,OACJ,OAAO,OAAO,CAAC,SAAS,KAAK,SAAS,SAAS,QAAQ,CAAC,CAAC;EAC1D,KAAK,MAAM;GACV,MAAM,IAAI,SAAS,KAAK,MAAM,SAAS,QAAQ,CAAC;GAChD,MAAM,IAAI,SAAS,KAAK,OAAO,SAAS,QAAQ,CAAC;GACjD,QAAQ,KAAK,IAAb;IACC,KAAK,KACJ,OAAO,OAAO,IAAI,CAAC;IACpB,KAAK,KACJ,OAAO,OAAO,IAAI,CAAC;IACpB,KAAK,KACJ,OAAO,OAAO,IAAI,CAAC;IACpB,KAAK,KACJ,OAAO,MAAM,IAAI,IAAI,OAAO,IAAI,CAAC;IAClC,KAAK,KACJ,OAAO,MAAM,IAAI,IAAI,OAAO,IAAI,CAAC;IAClC,SACC,OAAO;GACT;EACD;EACA,KAAK,MAAM;GACV,MAAM,KAAK,UAAU,KAAK;GAC1B,IAAI,CAAC,IACJ,OAAO;GAKR,OAAO,OAAO,GAHD,MAAM,QAAQ,KAAK,IAAI,IACjC,KAAK,KAAK,KAAK,MAAM,SAAS,GAAG,SAAS,QAAQ,CAAC,CAAC,IACpD,CAAC,CACiB,CAAC;EACvB;EACA,KAAK,UAAU;GACd,MAAM,UAAU,KAAK,WAAW,CAAC;GACjC,MAAM,QAAQ,QAAQ,KAAK;GAE3B,OAAO,QADQ,MAAM,QAAQ,KAAK,IAAI,QAAQ,SAAS,QAAQ,UAAU,KAAK,CAAC,IAAI,CAAC,KAAK,GAEjF,QAAQ,KAAa,MAAe,MAAM,SAAS,QAAQ,OAAO,CAAC,EAAE,GAAG,CAAC,CACjF;EACD;EACA,SACC,OAAO;CACT;AACD;;AAGA,MAAa,gBACZ,MACA,YACY;CACZ,IAAI,CAAC,MACJ,OAAO;CAER,OAAO,SAAS,MAAM,SAAS,CAAC;AACjC"}
1
+ {"version":3,"file":"evaluate.js","names":[],"sources":["../../src/calc/evaluate.ts"],"sourcesContent":["import { type CalcExpression, MAX_DEPTH } from './types'\n\n/**\n * Server-resolved calc extension values, threaded into evaluation as data (never re-resolved\n * here, so the evaluator stays sync and isomorphic). `sources`/`weights` are serializable and\n * ride the form document for the client's live preview; `functions` never serialize and are\n * supplied per environment (registry `apply` fns on the server, the Form prop on the client).\n */\nexport type CalcResolved = {\n\tsources?: Record<string, number>\n\t/** Keyed `source + ' ' + field` (see `calcWeightKey` / `resolveCalcContext`). */\n\tweights?: Record<string, Record<string, number>>\n\tfunctions?: Record<string, (args: number[]) => number>\n}\n\n/**\n * The `CalcResolved.weights` key for a sourced weight node: source key + space + field name.\n * Unambiguous because registered source keys can never contain a space\n * (`assertValidCalcSourceKeys` enforces `[\\w.-]` at boot), so the first space always ends the\n * source segment.\n */\nexport const calcWeightKey = (source: string, field: string): string => `${source} ${field}`\n\nconst toNumber = (value: unknown): number => {\n\tif (typeof value === 'number') {\n\t\treturn Number.isFinite(value) ? value : 0\n\t}\n\tif (typeof value === 'string' && value.trim() !== '') {\n\t\tconst n = Number(value)\n\t\treturn Number.isFinite(n) ? n : 0\n\t}\n\treturn 0\n}\n\nconst finite = (n: number): number => (Number.isFinite(n) ? n : 0)\n\nconst FUNCTIONS: Record<string, (args: number[]) => number> = {\n\tmin: (a) => (a.length ? Math.min(...a) : 0),\n\tmax: (a) => (a.length ? Math.max(...a) : 0),\n\tround: (a) => Math.round(a[0] ?? 0),\n\tabs: (a) => Math.abs(a[0] ?? 0),\n\tceil: (a) => Math.ceil(a[0] ?? 0),\n\tfloor: (a) => Math.floor(a[0] ?? 0),\n}\n\ntype EvalContext = { answers: Record<string, unknown>; resolved?: CalcResolved }\n\nconst evalNode = (expr: CalcExpression, ctx: EvalContext, depth: number): number => {\n\tif (depth > MAX_DEPTH || expr == null || typeof expr !== 'object') {\n\t\treturn 0\n\t}\n\tconst { answers, resolved } = ctx\n\tswitch (expr.type) {\n\t\tcase 'lit':\n\t\t\treturn finite(toNumber(expr.value))\n\t\tcase 'ref':\n\t\t\treturn toNumber(answers[expr.field])\n\t\tcase 'source':\n\t\t\treturn toNumber(resolved?.sources?.[expr.source])\n\t\tcase 'neg':\n\t\t\treturn finite(-evalNode(expr.operand, ctx, depth + 1))\n\t\tcase 'op': {\n\t\t\tconst l = evalNode(expr.left, ctx, depth + 1)\n\t\t\tconst r = evalNode(expr.right, ctx, depth + 1)\n\t\t\tswitch (expr.op) {\n\t\t\t\tcase '+':\n\t\t\t\t\treturn finite(l + r)\n\t\t\t\tcase '-':\n\t\t\t\t\treturn finite(l - r)\n\t\t\t\tcase '*':\n\t\t\t\t\treturn finite(l * r)\n\t\t\t\tcase '/':\n\t\t\t\t\treturn r === 0 ? 0 : finite(l / r)\n\t\t\t\tcase '%':\n\t\t\t\t\treturn r === 0 ? 0 : finite(l % r)\n\t\t\t\tdefault:\n\t\t\t\t\treturn 0\n\t\t\t}\n\t\t}\n\t\tcase 'fn': {\n\t\t\t// Built-ins always win, so a resolved custom fn can never shadow the canonical grammar.\n\t\t\tconst fn = FUNCTIONS[expr.fn] ?? resolved?.functions?.[expr.fn]\n\t\t\tif (!fn) {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tconst args = Array.isArray(expr.args) ? expr.args.map((a) => evalNode(a, ctx, depth + 1)) : []\n\t\t\t// Custom fns are host code: a throw must degrade to 0, not break the whole evaluation.\n\t\t\ttry {\n\t\t\t\treturn finite(fn(args))\n\t\t\t} catch {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t}\n\t\tcase 'weight': {\n\t\t\t// A sourced weight always reads the resolved map (missing map -> 0 per chosen); inline weights are ignored.\n\t\t\tconst weights =\n\t\t\t\ttypeof expr.source === 'string'\n\t\t\t\t\t? (resolved?.weights?.[calcWeightKey(expr.source, expr.field)] ?? {})\n\t\t\t\t\t: (expr.weights ?? {})\n\t\t\tconst value = answers[expr.field]\n\t\t\tconst chosen = Array.isArray(value) ? value : value == null || value === '' ? [] : [value]\n\t\t\treturn finite(\n\t\t\t\tchosen.reduce((sum: number, v: unknown) => sum + toNumber(weights[String(v)]), 0)\n\t\t\t)\n\t\t}\n\t\tdefault:\n\t\t\treturn 0\n\t}\n}\n\n/**\n * Evaluate a calc expression against form answers. Total + safe: no `eval`, always finite, div/mod\n * by zero -> 0, missing ref -> 0, depth-guarded. Isomorphic (client + server). `resolved` supplies\n * server-resolved source values, sourced weight maps, and custom functions; any extension node\n * whose value is absent evaluates to 0.\n */\nexport const evaluateCalc = (\n\texpr: CalcExpression | null | undefined,\n\tanswers: Record<string, unknown>,\n\tresolved?: CalcResolved\n): number => {\n\tif (!expr) {\n\t\treturn 0\n\t}\n\treturn evalNode(expr, { answers, resolved }, 0)\n}\n"],"mappings":";;;;;;;AAqBA,MAAa,iBAAiB,QAAgB,UAA0B,GAAG,OAAO,GAAG;AAErF,MAAM,YAAY,UAA2B;CAC5C,IAAI,OAAO,UAAU,UACpB,OAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;CAEzC,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,MAAM,IAAI;EACrD,MAAM,IAAI,OAAO,KAAK;EACtB,OAAO,OAAO,SAAS,CAAC,IAAI,IAAI;CACjC;CACA,OAAO;AACR;AAEA,MAAM,UAAU,MAAuB,OAAO,SAAS,CAAC,IAAI,IAAI;AAEhE,MAAM,YAAwD;CAC7D,MAAM,MAAO,EAAE,SAAS,KAAK,IAAI,GAAG,CAAC,IAAI;CACzC,MAAM,MAAO,EAAE,SAAS,KAAK,IAAI,GAAG,CAAC,IAAI;CACzC,QAAQ,MAAM,KAAK,MAAM,EAAE,MAAM,CAAC;CAClC,MAAM,MAAM,KAAK,IAAI,EAAE,MAAM,CAAC;CAC9B,OAAO,MAAM,KAAK,KAAK,EAAE,MAAM,CAAC;CAChC,QAAQ,MAAM,KAAK,MAAM,EAAE,MAAM,CAAC;AACnC;AAIA,MAAM,YAAY,MAAsB,KAAkB,UAA0B;CACnF,IAAI,QAAA,MAAqB,QAAQ,QAAQ,OAAO,SAAS,UACxD,OAAO;CAER,MAAM,EAAE,SAAS,aAAa;CAC9B,QAAQ,KAAK,MAAb;EACC,KAAK,OACJ,OAAO,OAAO,SAAS,KAAK,KAAK,CAAC;EACnC,KAAK,OACJ,OAAO,SAAS,QAAQ,KAAK,MAAM;EACpC,KAAK,UACJ,OAAO,SAAS,UAAU,UAAU,KAAK,OAAO;EACjD,KAAK,OACJ,OAAO,OAAO,CAAC,SAAS,KAAK,SAAS,KAAK,QAAQ,CAAC,CAAC;EACtD,KAAK,MAAM;GACV,MAAM,IAAI,SAAS,KAAK,MAAM,KAAK,QAAQ,CAAC;GAC5C,MAAM,IAAI,SAAS,KAAK,OAAO,KAAK,QAAQ,CAAC;GAC7C,QAAQ,KAAK,IAAb;IACC,KAAK,KACJ,OAAO,OAAO,IAAI,CAAC;IACpB,KAAK,KACJ,OAAO,OAAO,IAAI,CAAC;IACpB,KAAK,KACJ,OAAO,OAAO,IAAI,CAAC;IACpB,KAAK,KACJ,OAAO,MAAM,IAAI,IAAI,OAAO,IAAI,CAAC;IAClC,KAAK,KACJ,OAAO,MAAM,IAAI,IAAI,OAAO,IAAI,CAAC;IAClC,SACC,OAAO;GACT;EACD;EACA,KAAK,MAAM;GAEV,MAAM,KAAK,UAAU,KAAK,OAAO,UAAU,YAAY,KAAK;GAC5D,IAAI,CAAC,IACJ,OAAO;GAER,MAAM,OAAO,MAAM,QAAQ,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,MAAM,SAAS,GAAG,KAAK,QAAQ,CAAC,CAAC,IAAI,CAAC;GAE7F,IAAI;IACH,OAAO,OAAO,GAAG,IAAI,CAAC;GACvB,QAAQ;IACP,OAAO;GACR;EACD;EACA,KAAK,UAAU;GAEd,MAAM,UACL,OAAO,KAAK,WAAW,WACnB,UAAU,UAAU,cAAc,KAAK,QAAQ,KAAK,KAAK,MAAM,CAAC,IAChE,KAAK,WAAW,CAAC;GACtB,MAAM,QAAQ,QAAQ,KAAK;GAE3B,OAAO,QADQ,MAAM,QAAQ,KAAK,IAAI,QAAQ,SAAS,QAAQ,UAAU,KAAK,CAAC,IAAI,CAAC,KAAK,GAEjF,QAAQ,KAAa,MAAe,MAAM,SAAS,QAAQ,OAAO,CAAC,EAAE,GAAG,CAAC,CACjF;EACD;EACA,SACC,OAAO;CACT;AACD;;;;;;;AAQA,MAAa,gBACZ,MACA,SACA,aACY;CACZ,IAAI,CAAC,MACJ,OAAO;CAER,OAAO,SAAS,MAAM;EAAE;EAAS;CAAS,GAAG,CAAC;AAC/C"}
@@ -0,0 +1,11 @@
1
+ import { CalcExpression } from "./types.js";
2
+
3
+ //#region src/calc/formatCalc.d.ts
4
+ /**
5
+ * Renders a CalcExpression as a human-readable one-line string for the visual expression builder's
6
+ * live preview. `sourceLabel` resolves a calc source key to its display label (raw key when omitted).
7
+ */
8
+ declare const formatCalc: (expr: CalcExpression, labelOf: (field: string) => string, sourceLabel?: (key: string) => string) => string;
9
+ //#endregion
10
+ export { formatCalc };
11
+ //# sourceMappingURL=formatCalc.d.ts.map
@@ -0,0 +1,35 @@
1
+ //#region src/calc/formatCalc.ts
2
+ const OP_GLYPHS = {
3
+ "+": "+",
4
+ "-": "−",
5
+ "*": "×",
6
+ "/": "÷",
7
+ "%": "%"
8
+ };
9
+ const wrap = (child, resolvers, wrapTypes = ["op"]) => {
10
+ const formatted = format(child, resolvers);
11
+ return wrapTypes.includes(child.type) ? `(${formatted})` : formatted;
12
+ };
13
+ const format = (expr, resolvers) => {
14
+ switch (expr.type) {
15
+ case "lit": return String(expr.value);
16
+ case "ref": return resolvers.labelOf(expr.field);
17
+ case "neg": return `−${wrap(expr.operand, resolvers, ["op", "neg"])}`;
18
+ case "op": return `${wrap(expr.left, resolvers)} ${OP_GLYPHS[expr.op]} ${wrap(expr.right, resolvers)}`;
19
+ case "fn": return `${expr.fn}(${expr.args.map((arg) => format(arg, resolvers)).join(", ")})`;
20
+ case "source": return resolvers.sourceLabel(expr.source);
21
+ case "weight": return `weights(${resolvers.labelOf(expr.field)})`;
22
+ }
23
+ };
24
+ /**
25
+ * Renders a CalcExpression as a human-readable one-line string for the visual expression builder's
26
+ * live preview. `sourceLabel` resolves a calc source key to its display label (raw key when omitted).
27
+ */
28
+ const formatCalc = (expr, labelOf, sourceLabel = (key) => key) => format(expr, {
29
+ labelOf,
30
+ sourceLabel
31
+ });
32
+ //#endregion
33
+ export { formatCalc };
34
+
35
+ //# sourceMappingURL=formatCalc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatCalc.js","names":[],"sources":["../../src/calc/formatCalc.ts"],"sourcesContent":["import type { CalcExpression, CalcOp } from './types'\n\nconst OP_GLYPHS: Record<CalcOp, string> = {\n\t'+': '+',\n\t'-': '−',\n\t'*': '×',\n\t'/': '÷',\n\t'%': '%',\n}\n\ntype LabelResolvers = {\n\tlabelOf: (field: string) => string\n\tsourceLabel: (key: string) => string\n}\n\nconst wrap = (\n\tchild: CalcExpression,\n\tresolvers: LabelResolvers,\n\twrapTypes: readonly CalcExpression['type'][] = ['op']\n): string => {\n\tconst formatted = format(child, resolvers)\n\treturn wrapTypes.includes(child.type) ? `(${formatted})` : formatted\n}\n\nconst format = (expr: CalcExpression, resolvers: LabelResolvers): string => {\n\tswitch (expr.type) {\n\t\tcase 'lit':\n\t\t\treturn String(expr.value)\n\t\tcase 'ref':\n\t\t\treturn resolvers.labelOf(expr.field)\n\t\tcase 'neg':\n\t\t\t// Parenthesize a nested `neg` operand too, otherwise \"-(-5)\" glues into the ambiguous \"--5\".\n\t\t\treturn `−${wrap(expr.operand, resolvers, ['op', 'neg'])}`\n\t\tcase 'op':\n\t\t\treturn `${wrap(expr.left, resolvers)} ${OP_GLYPHS[expr.op]} ${wrap(expr.right, resolvers)}`\n\t\tcase 'fn':\n\t\t\treturn `${expr.fn}(${expr.args.map((arg) => format(arg, resolvers)).join(', ')})`\n\t\tcase 'source':\n\t\t\treturn resolvers.sourceLabel(expr.source)\n\t\tcase 'weight':\n\t\t\treturn `weights(${resolvers.labelOf(expr.field)})`\n\t}\n}\n\n/**\n * Renders a CalcExpression as a human-readable one-line string for the visual expression builder's\n * live preview. `sourceLabel` resolves a calc source key to its display label (raw key when omitted).\n */\nexport const formatCalc = (\n\texpr: CalcExpression,\n\tlabelOf: (field: string) => string,\n\tsourceLabel: (key: string) => string = (key) => key\n): string => format(expr, { labelOf, sourceLabel })\n"],"mappings":";AAEA,MAAM,YAAoC;CACzC,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;AACN;AAOA,MAAM,QACL,OACA,WACA,YAA+C,CAAC,IAAI,MACxC;CACZ,MAAM,YAAY,OAAO,OAAO,SAAS;CACzC,OAAO,UAAU,SAAS,MAAM,IAAI,IAAI,IAAI,UAAU,KAAK;AAC5D;AAEA,MAAM,UAAU,MAAsB,cAAsC;CAC3E,QAAQ,KAAK,MAAb;EACC,KAAK,OACJ,OAAO,OAAO,KAAK,KAAK;EACzB,KAAK,OACJ,OAAO,UAAU,QAAQ,KAAK,KAAK;EACpC,KAAK,OAEJ,OAAO,IAAI,KAAK,KAAK,SAAS,WAAW,CAAC,MAAM,KAAK,CAAC;EACvD,KAAK,MACJ,OAAO,GAAG,KAAK,KAAK,MAAM,SAAS,EAAE,GAAG,UAAU,KAAK,IAAI,GAAG,KAAK,KAAK,OAAO,SAAS;EACzF,KAAK,MACJ,OAAO,GAAG,KAAK,GAAG,GAAG,KAAK,KAAK,KAAK,QAAQ,OAAO,KAAK,SAAS,CAAC,EAAE,KAAK,IAAI,EAAE;EAChF,KAAK,UACJ,OAAO,UAAU,YAAY,KAAK,MAAM;EACzC,KAAK,UACJ,OAAO,WAAW,UAAU,QAAQ,KAAK,KAAK,EAAE;CAClD;AACD;;;;;AAMA,MAAa,cACZ,MACA,SACA,eAAwC,QAAQ,QACpC,OAAO,MAAM;CAAE;CAAS;AAAY,CAAC"}
@@ -0,0 +1,17 @@
1
+ //#region src/calc/formatCalcValue.d.ts
2
+ /** The calculation field's display settings (`decimals`, `prefix`, `suffix` config fields). */
3
+ type CalcDisplayConfig = {
4
+ decimals?: unknown;
5
+ prefix?: unknown;
6
+ suffix?: unknown;
7
+ };
8
+ /**
9
+ * Formats a computed calculation value for display: fixed `decimals` when configured, wrapped in
10
+ * `prefix`/`suffix` exactly as authored (no injected spaces; authors control spacing in their
11
+ * strings). Shared by the visitor renderer and the definition's `format` so the frontend, admin
12
+ * answers view, emails, and recall all agree.
13
+ */
14
+ declare const formatCalcValue: (value: number, config?: CalcDisplayConfig) => string;
15
+ //#endregion
16
+ export { CalcDisplayConfig, formatCalcValue };
17
+ //# sourceMappingURL=formatCalcValue.d.ts.map
@@ -0,0 +1,17 @@
1
+ //#region src/calc/formatCalcValue.ts
2
+ const asDecimals = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 6 ? value : void 0;
3
+ /**
4
+ * Formats a computed calculation value for display: fixed `decimals` when configured, wrapped in
5
+ * `prefix`/`suffix` exactly as authored (no injected spaces; authors control spacing in their
6
+ * strings). Shared by the visitor renderer and the definition's `format` so the frontend, admin
7
+ * answers view, emails, and recall all agree.
8
+ */
9
+ const formatCalcValue = (value, config) => {
10
+ const decimals = asDecimals(config?.decimals);
11
+ const number = decimals !== void 0 ? value.toFixed(decimals) : String(value);
12
+ return `${typeof config?.prefix === "string" ? config.prefix : ""}${number}${typeof config?.suffix === "string" ? config.suffix : ""}`;
13
+ };
14
+ //#endregion
15
+ export { formatCalcValue };
16
+
17
+ //# sourceMappingURL=formatCalcValue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatCalcValue.js","names":[],"sources":["../../src/calc/formatCalcValue.ts"],"sourcesContent":["/** The calculation field's display settings (`decimals`, `prefix`, `suffix` config fields). */\nexport type CalcDisplayConfig = {\n\tdecimals?: unknown\n\tprefix?: unknown\n\tsuffix?: unknown\n}\n\nconst asDecimals = (value: unknown): number | undefined =>\n\ttypeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 6\n\t\t? value\n\t\t: undefined\n\n/**\n * Formats a computed calculation value for display: fixed `decimals` when configured, wrapped in\n * `prefix`/`suffix` exactly as authored (no injected spaces; authors control spacing in their\n * strings). Shared by the visitor renderer and the definition's `format` so the frontend, admin\n * answers view, emails, and recall all agree.\n */\nexport const formatCalcValue = (value: number, config?: CalcDisplayConfig): string => {\n\tconst decimals = asDecimals(config?.decimals)\n\tconst number = decimals !== undefined ? value.toFixed(decimals) : String(value)\n\tconst prefix = typeof config?.prefix === 'string' ? config.prefix : ''\n\tconst suffix = typeof config?.suffix === 'string' ? config.suffix : ''\n\treturn `${prefix}${number}${suffix}`\n}\n"],"mappings":";AAOA,MAAM,cAAc,UACnB,OAAO,UAAU,YAAY,OAAO,UAAU,KAAK,KAAK,SAAS,KAAK,SAAS,IAC5E,QACA,KAAA;;;;;;;AAQJ,MAAa,mBAAmB,OAAe,WAAuC;CACrF,MAAM,WAAW,WAAW,QAAQ,QAAQ;CAC5C,MAAM,SAAS,aAAa,KAAA,IAAY,MAAM,QAAQ,QAAQ,IAAI,OAAO,KAAK;CAG9E,OAAO,GAFQ,OAAO,QAAQ,WAAW,WAAW,OAAO,SAAS,KAEjD,SADJ,OAAO,QAAQ,WAAW,WAAW,OAAO,SAAS;AAErE"}
@@ -1,8 +1,22 @@
1
1
  import { CalcExpression } from "./types.js";
2
2
 
3
3
  //#region src/calc/normalizeCalc.d.ts
4
- /** Structural guard: returns a valid CalcExpression if `value` is structurally sound (depth-guarded at 64), otherwise undefined. Never throws. */
5
- declare const normalizeCalc: (value: unknown) => CalcExpression | undefined;
4
+ /**
5
+ * The extension names a normalized expression may reference: registered calc source keys and custom
6
+ * function names (plugin option `calc`). Absent (the default for every un-threaded caller), only the
7
+ * built-in grammar is valid: `source` nodes, sourced weights, and custom fns all reject.
8
+ */
9
+ type CalcAllowed = {
10
+ sources?: ReadonlySet<string>;
11
+ functions?: ReadonlySet<string>;
12
+ };
13
+ /**
14
+ * Structural guard: returns a valid CalcExpression if `value` is structurally sound (depth-guarded
15
+ * at 64), otherwise undefined. Never throws. `allowed` admits registered extension names (source
16
+ * keys, custom function names); without it only the built-in grammar passes, so an un-threaded
17
+ * caller can never validate an expression the evaluator has no values for.
18
+ */
19
+ declare const normalizeCalc: (value: unknown, allowed?: CalcAllowed) => CalcExpression | undefined;
6
20
  //#endregion
7
- export { normalizeCalc };
21
+ export { CalcAllowed, normalizeCalc };
8
22
  //# sourceMappingURL=normalizeCalc.d.ts.map