@10x-media/form-builder 0.1.0-beta.1 → 0.1.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +293 -0
- package/README.md +6 -4
- package/dist/actions/body/converters.d.ts +20 -0
- package/dist/actions/body/converters.js +57 -0
- package/dist/actions/body/converters.js.map +1 -0
- package/dist/actions/body/escapeHtml.d.ts +6 -0
- package/dist/actions/body/escapeHtml.js +14 -0
- package/dist/actions/body/escapeHtml.js.map +1 -0
- package/dist/actions/body/serializeBody.d.ts +55 -0
- package/dist/actions/body/serializeBody.js +78 -0
- package/dist/actions/body/serializeBody.js.map +1 -0
- package/dist/actions/body/serializeSlate.js +31 -0
- package/dist/actions/body/serializeSlate.js.map +1 -0
- package/dist/actions/body/textOfBody.d.ts +13 -0
- package/dist/actions/body/textOfBody.js +35 -0
- package/dist/actions/body/textOfBody.js.map +1 -0
- package/dist/actions/body/wildcards.d.ts +10 -0
- package/dist/actions/body/wildcards.js +33 -0
- package/dist/actions/body/wildcards.js.map +1 -0
- package/dist/actions/buildActionBlocks.js +12 -9
- package/dist/actions/buildActionBlocks.js.map +1 -1
- package/dist/actions/builtin/confirmation.js +46 -43
- package/dist/actions/builtin/confirmation.js.map +1 -1
- package/dist/actions/builtin/emailAction.d.ts +17 -0
- package/dist/actions/builtin/emailAction.js +83 -0
- package/dist/actions/builtin/emailAction.js.map +1 -0
- package/dist/actions/builtin/emailTeam.js +14 -38
- package/dist/actions/builtin/emailTeam.js.map +1 -1
- package/dist/actions/builtin/index.d.ts +16 -1
- package/dist/actions/builtin/index.js +21 -7
- package/dist/actions/builtin/index.js.map +1 -1
- package/dist/actions/builtin/signedWebhook.js +6 -2
- package/dist/actions/builtin/signedWebhook.js.map +1 -1
- package/dist/actions/defineAction.d.ts +5 -4
- package/dist/actions/defineAction.js.map +1 -1
- package/dist/actions/dispatch.js +2 -1
- package/dist/actions/dispatch.js.map +1 -1
- package/dist/actions/emailRecipients.d.ts +12 -0
- package/dist/actions/emailRecipients.js +116 -0
- package/dist/actions/emailRecipients.js.map +1 -0
- package/dist/actions/fromAddresses.d.ts +21 -0
- package/dist/actions/fromAddresses.js +74 -0
- package/dist/actions/fromAddresses.js.map +1 -0
- package/dist/actions/registry.d.ts +2 -1
- package/dist/actions/registry.js +2 -6
- package/dist/actions/registry.js.map +1 -1
- package/dist/actions/runActions.js +10 -1
- package/dist/actions/runActions.js.map +1 -1
- package/dist/actions/task.js +20 -15
- package/dist/actions/task.js.map +1 -1
- package/dist/aggregation/aggregateResponses.d.ts +9 -0
- package/dist/aggregation/aggregateResponses.js +8 -14
- package/dist/aggregation/aggregateResponses.js.map +1 -1
- package/dist/aggregation/resolveResultsRequest.d.ts +35 -6
- package/dist/aggregation/resolveResultsRequest.js +68 -11
- package/dist/aggregation/resolveResultsRequest.js.map +1 -1
- package/dist/calc/computeCalcFields.js +2 -1
- package/dist/calc/computeCalcFields.js.map +1 -1
- package/dist/calc/evaluate.js +1 -2
- package/dist/calc/evaluate.js.map +1 -1
- package/dist/calc/normalizeCalc.js +6 -19
- package/dist/calc/normalizeCalc.js.map +1 -1
- package/dist/calc/types.d.ts +8 -2
- package/dist/calc/types.js +22 -0
- package/dist/calc/types.js.map +1 -0
- package/dist/client/ByteSizeField.d.ts +29 -0
- package/dist/client/ByteSizeField.js +58 -0
- package/dist/client/ByteSizeField.js.map +1 -0
- package/dist/client/ClosePollButton.d.ts +15 -0
- package/dist/client/ClosePollButton.js +80 -0
- package/dist/client/ClosePollButton.js.map +1 -0
- package/dist/client/CondensedArray.d.ts +15 -0
- package/dist/client/CondensedArray.js +97 -0
- package/dist/client/CondensedArray.js.map +1 -0
- package/dist/client/ConditionBuilder.js +2 -1
- package/dist/client/ConditionBuilder.js.map +1 -1
- package/dist/client/ConsentBlockLabel.d.ts +15 -0
- package/dist/client/ConsentBlockLabel.js +71 -0
- package/dist/client/ConsentBlockLabel.js.map +1 -0
- package/dist/client/ConsentSourceRowLabel.d.ts +9 -0
- package/dist/client/ConsentSourceRowLabel.js +21 -0
- package/dist/client/ConsentSourceRowLabel.js.map +1 -0
- package/dist/client/EndpointOptionsSelect.d.ts +46 -0
- package/dist/client/EndpointOptionsSelect.js +106 -0
- package/dist/client/EndpointOptionsSelect.js.map +1 -0
- package/dist/client/FieldBlockLabel.d.ts +18 -0
- package/dist/client/FieldBlockLabel.js +34 -0
- package/dist/client/FieldBlockLabel.js.map +1 -0
- package/dist/client/FieldNameSelect.d.ts +37 -0
- package/dist/client/FieldNameSelect.js +72 -0
- package/dist/client/FieldNameSelect.js.map +1 -0
- package/dist/client/FlowBuilder.d.ts +2 -2
- package/dist/client/FlowBuilder.js +137 -88
- package/dist/client/FlowBuilder.js.map +1 -1
- package/dist/client/FormConditionField.js +1 -4
- package/dist/client/FormConditionField.js.map +1 -1
- package/dist/client/RecipientsSelect.d.ts +31 -0
- package/dist/client/RecipientsSelect.js +174 -0
- package/dist/client/RecipientsSelect.js.map +1 -0
- package/dist/client/RuleDescription.d.ts +21 -0
- package/dist/client/RuleDescription.js +19 -0
- package/dist/client/RuleDescription.js.map +1 -0
- package/dist/client/admin.css +275 -0
- package/dist/client/endpointOptions.js +29 -0
- package/dist/client/endpointOptions.js.map +1 -0
- package/dist/client/flowAuthoring.js +142 -0
- package/dist/client/flowAuthoring.js.map +1 -0
- package/dist/client/synthesizeClientField.js +21 -10
- package/dist/client/synthesizeClientField.js.map +1 -1
- package/dist/client/toStaticLabel.js +13 -0
- package/dist/client/toStaticLabel.js.map +1 -0
- package/dist/collections/buttonFields.d.ts +34 -0
- package/dist/collections/buttonFields.js +35 -0
- package/dist/collections/buttonFields.js.map +1 -0
- package/dist/collections/cells/FieldCountCell.d.ts +15 -0
- package/dist/collections/cells/FieldCountCell.js +18 -0
- package/dist/collections/cells/FieldCountCell.js.map +1 -0
- package/dist/collections/cells/FlowStepsCell.d.ts +14 -0
- package/dist/collections/cells/FlowStepsCell.js +17 -0
- package/dist/collections/cells/FlowStepsCell.js.map +1 -0
- package/dist/collections/formSubmissions.js +108 -42
- package/dist/collections/formSubmissions.js.map +1 -1
- package/dist/collections/forms.js +409 -114
- package/dist/collections/forms.js.map +1 -1
- package/dist/collections/formsEndpoints.js +101 -0
- package/dist/collections/formsEndpoints.js.map +1 -0
- package/dist/collections/redirectFields.d.ts +29 -0
- package/dist/conditions/conditionType.js +9 -2
- package/dist/conditions/conditionType.js.map +1 -1
- package/dist/conditions/evaluate.js +1 -1
- package/dist/conditions/evaluate.js.map +1 -1
- package/dist/conditions/fieldTypes.d.ts +3 -1
- package/dist/conditions/fieldTypes.js +4 -1
- package/dist/conditions/fieldTypes.js.map +1 -1
- package/dist/conditions/normalizeConditions.js +20 -6
- package/dist/conditions/normalizeConditions.js.map +1 -1
- package/dist/consent/applyConsentStatements.d.ts +14 -0
- package/dist/consent/applyConsentStatements.js +17 -0
- package/dist/consent/applyConsentStatements.js.map +1 -0
- package/dist/consent/captureConsent.d.ts +38 -11
- package/dist/consent/captureConsent.js +60 -13
- package/dist/consent/captureConsent.js.map +1 -1
- package/dist/consent/consentSourcesField.d.ts +66 -0
- package/dist/consent/consentSourcesField.js +90 -0
- package/dist/consent/consentSourcesField.js.map +1 -0
- package/dist/consent/resolveConsentEntries.d.ts +24 -0
- package/dist/consent/resolveConsentEntries.js +40 -0
- package/dist/consent/resolveConsentEntries.js.map +1 -0
- package/dist/consent/resolveConsentSourcesRequest.d.ts +37 -0
- package/dist/consent/resolveConsentSourcesRequest.js +54 -0
- package/dist/consent/resolveConsentSourcesRequest.js.map +1 -0
- package/dist/consent/resolveConsentStatements.d.ts +45 -0
- package/dist/consent/resolveConsentStatements.js +44 -0
- package/dist/consent/resolveConsentStatements.js.map +1 -0
- package/dist/consent/resolvePublishedVersionRef.d.ts +17 -11
- package/dist/consent/resolvePublishedVersionRef.js +13 -10
- package/dist/consent/resolvePublishedVersionRef.js.map +1 -1
- package/dist/consent/types.d.ts +61 -0
- package/dist/email/departments.d.ts +59 -0
- package/dist/email/departments.js +87 -0
- package/dist/email/departments.js.map +1 -0
- package/dist/email/departmentsField.d.ts +53 -0
- package/dist/email/departmentsField.js +73 -0
- package/dist/email/departmentsField.js.map +1 -0
- package/dist/exports/client.d.ts +13 -1
- package/dist/exports/client.js +13 -1
- package/dist/exports/i18n.d.ts +5 -2
- package/dist/exports/i18n.js +5 -2
- package/dist/exports/react.d.ts +26 -8
- package/dist/exports/react.js +19 -3
- package/dist/exports/rsc.d.ts +8 -1
- package/dist/exports/rsc.js +8 -1
- package/dist/exports/types.d.ts +19 -10
- package/dist/fields/buildFieldBlocks.js +53 -19
- package/dist/fields/buildFieldBlocks.js.map +1 -1
- package/dist/fields/builtin/calculation.js +122 -2
- package/dist/fields/builtin/calculation.js.map +1 -1
- package/dist/fields/builtin/checkbox.js +8 -0
- package/dist/fields/builtin/checkbox.js.map +1 -1
- package/dist/fields/builtin/consent.js +30 -15
- package/dist/fields/builtin/consent.js.map +1 -1
- package/dist/fields/builtin/country.d.ts +13 -0
- package/dist/fields/builtin/country.js +30 -0
- package/dist/fields/builtin/country.js.map +1 -0
- package/dist/fields/builtin/date.js +31 -0
- package/dist/fields/builtin/date.js.map +1 -0
- package/dist/fields/builtin/email.js +1 -1
- package/dist/fields/builtin/email.js.map +1 -1
- package/dist/fields/builtin/file.d.ts +15 -0
- package/dist/fields/builtin/file.js +95 -8
- package/dist/fields/builtin/file.js.map +1 -1
- package/dist/fields/builtin/index.d.ts +26 -0
- package/dist/fields/builtin/index.js +30 -6
- package/dist/fields/builtin/index.js.map +1 -1
- package/dist/fields/builtin/message.js +30 -0
- package/dist/fields/builtin/message.js.map +1 -0
- package/dist/fields/builtin/repeater.js +49 -0
- package/dist/fields/builtin/repeater.js.map +1 -0
- package/dist/fields/builtin/select.js +8 -3
- package/dist/fields/builtin/select.js.map +1 -1
- package/dist/fields/builtin/state.d.ts +13 -0
- package/dist/fields/builtin/state.js +30 -0
- package/dist/fields/builtin/state.js.map +1 -0
- package/dist/fields/data/regions.d.ts +21 -0
- package/dist/fields/data/regions.js +1238 -0
- package/dist/fields/data/regions.js.map +1 -0
- package/dist/fields/fieldKey.d.ts +15 -0
- package/dist/fields/fieldKey.js +16 -0
- package/dist/fields/fieldKey.js.map +1 -0
- package/dist/fields/fieldNamesOfType.js +30 -0
- package/dist/fields/fieldNamesOfType.js.map +1 -0
- package/dist/fields/instanceOptions.js +20 -0
- package/dist/fields/instanceOptions.js.map +1 -0
- package/dist/fields/localizedIf.d.ts +13 -0
- package/dist/fields/localizedIf.js +12 -0
- package/dist/fields/localizedIf.js.map +1 -0
- package/dist/fields/registry.d.ts +3 -1
- package/dist/fields/registry.js +23 -8
- package/dist/fields/registry.js.map +1 -1
- package/dist/fields/sharedConfig.js +120 -58
- package/dist/fields/sharedConfig.js.map +1 -1
- package/dist/fields/types.d.ts +71 -7
- package/dist/flow/engine.d.ts +6 -4
- package/dist/flow/engine.js +11 -6
- package/dist/flow/engine.js.map +1 -1
- package/dist/flow/normalizeFlow.js +28 -9
- package/dist/flow/normalizeFlow.js.map +1 -1
- package/dist/flow/stepLabel.js +15 -0
- package/dist/flow/stepLabel.js.map +1 -0
- package/dist/flow/types.d.ts +12 -3
- package/dist/flow/types.js +11 -0
- package/dist/flow/types.js.map +1 -0
- package/dist/form/pollState.d.ts +22 -0
- package/dist/form/pollState.js +17 -0
- package/dist/form/pollState.js.map +1 -0
- package/dist/form/toFormDocument.d.ts +102 -0
- package/dist/form/toFormDocument.js +87 -0
- package/dist/form/toFormDocument.js.map +1 -0
- package/dist/form/types.d.ts +65 -0
- package/dist/index.d.ts +53 -39
- package/dist/index.js +85 -23
- package/dist/index.js.map +1 -1
- package/dist/options.d.ts +229 -0
- package/dist/plugin/applyRegistryConfig.d.ts +11 -0
- package/dist/plugin/applyRegistryConfig.js +20 -0
- package/dist/plugin/applyRegistryConfig.js.map +1 -0
- package/dist/plugin/collectionOverrides.d.ts +22 -0
- package/dist/plugin/customState.js +26 -0
- package/dist/plugin/customState.js.map +1 -0
- package/dist/plugin/registerCollections.js +40 -20
- package/dist/plugin/registerCollections.js.map +1 -1
- package/dist/plugin/uploadsCollection.d.ts +16 -0
- package/dist/plugin/uploadsCollection.js +53 -0
- package/dist/plugin/uploadsCollection.js.map +1 -0
- package/dist/poll/applyPollOptions.js +18 -0
- package/dist/poll/applyPollOptions.js.map +1 -0
- package/dist/poll/buildPollOptionSourceFields.js +50 -0
- package/dist/poll/buildPollOptionSourceFields.js.map +1 -0
- package/dist/poll/closeJob.d.ts +52 -0
- package/dist/poll/closeJob.js +119 -0
- package/dist/poll/closeJob.js.map +1 -0
- package/dist/poll/definePollOptionSource.d.ts +44 -0
- package/dist/poll/definePollOptionSource.js +6 -0
- package/dist/poll/definePollOptionSource.js.map +1 -0
- package/dist/poll/definePollType.d.ts +34 -0
- package/dist/poll/definePollType.js +6 -0
- package/dist/poll/definePollType.js.map +1 -0
- package/dist/poll/effectivePollOptions.d.ts +38 -0
- package/dist/poll/effectivePollOptions.js +64 -0
- package/dist/poll/effectivePollOptions.js.map +1 -0
- package/dist/poll/mostVoted.d.ts +21 -0
- package/dist/poll/mostVoted.js +32 -0
- package/dist/poll/mostVoted.js.map +1 -0
- package/dist/poll/outcomeBeforeChange.js +91 -0
- package/dist/poll/outcomeBeforeChange.js.map +1 -0
- package/dist/poll/outcomeFields.d.ts +38 -0
- package/dist/poll/outcomeFields.js +52 -0
- package/dist/poll/outcomeFields.js.map +1 -0
- package/dist/poll/pollTypeRegistry.d.ts +34 -0
- package/dist/poll/pollTypeRegistry.js +69 -0
- package/dist/poll/pollTypeRegistry.js.map +1 -0
- package/dist/poll/registry.d.ts +17 -0
- package/dist/poll/registry.js +12 -0
- package/dist/poll/registry.js.map +1 -0
- package/dist/poll/resolvePollCloseRequest.js +105 -0
- package/dist/poll/resolvePollCloseRequest.js.map +1 -0
- package/dist/poll/resolvePollOptions.d.ts +29 -0
- package/dist/poll/resolvePollOptions.js +52 -0
- package/dist/poll/resolvePollOptions.js.map +1 -0
- package/dist/poll/resolvePollOptionsRequest.js +50 -0
- package/dist/poll/resolvePollOptionsRequest.js.map +1 -0
- package/dist/poll/resolvePollOutcome.d.ts +34 -0
- package/dist/poll/resolvePollOutcome.js +76 -0
- package/dist/poll/resolvePollOutcome.js.map +1 -0
- package/dist/poll/resultsField.js +21 -0
- package/dist/poll/resultsField.js.map +1 -0
- package/dist/prefill/valuesFromSearchParams.js +5 -9
- package/dist/prefill/valuesFromSearchParams.js.map +1 -1
- package/dist/presentations/types.d.ts +3 -2
- package/dist/react/Form.d.ts +66 -19
- package/dist/react/Form.js +342 -180
- package/dist/react/Form.js.map +1 -1
- package/dist/react/FormContext.d.ts +44 -1
- package/dist/react/FormContext.js.map +1 -1
- package/dist/react/FormControls.d.ts +55 -0
- package/dist/react/FormControls.js +56 -0
- package/dist/react/FormControls.js.map +1 -0
- package/dist/react/FormFields.d.ts +18 -0
- package/dist/react/FormFields.js +114 -0
- package/dist/react/FormFields.js.map +1 -0
- package/dist/react/FormLayout.d.ts +4 -2
- package/dist/react/FormLayout.js +3 -2
- package/dist/react/FormLayout.js.map +1 -1
- package/dist/react/FormResults.d.ts +6 -3
- package/dist/react/FormResults.js +37 -29
- package/dist/react/FormResults.js.map +1 -1
- package/dist/react/FormSteps.d.ts +17 -0
- package/dist/react/FormSteps.js +35 -0
- package/dist/react/FormSteps.js.map +1 -0
- package/dist/react/Poll.d.ts +17 -6
- package/dist/react/Poll.js +79 -21
- package/dist/react/Poll.js.map +1 -1
- package/dist/react/captcha/HcaptchaCaptcha.d.ts +28 -0
- package/dist/react/captcha/HcaptchaCaptcha.js +53 -0
- package/dist/react/captcha/HcaptchaCaptcha.js.map +1 -0
- package/dist/react/captcha/RecaptchaCaptcha.d.ts +41 -0
- package/dist/react/captcha/RecaptchaCaptcha.js +93 -0
- package/dist/react/captcha/RecaptchaCaptcha.js.map +1 -0
- package/dist/react/captcha/TurnstileCaptcha.d.ts +28 -0
- package/dist/react/captcha/TurnstileCaptcha.js +53 -0
- package/dist/react/captcha/TurnstileCaptcha.js.map +1 -0
- package/dist/react/captcha/types.d.ts +8 -0
- package/dist/react/captcha/useCaptchaScript.js +45 -0
- package/dist/react/captcha/useCaptchaScript.js.map +1 -0
- package/dist/react/captcha/vendors.js +8 -0
- package/dist/react/captcha/vendors.js.map +1 -0
- package/dist/react/contract.d.ts +2 -3
- package/dist/react/contract.js.map +1 -1
- package/dist/react/fetchResults.d.ts +1 -1
- package/dist/react/fetchResults.js +1 -1
- package/dist/react/fetchResults.js.map +1 -1
- package/dist/react/presentation/DialogSurface.js +1 -4
- package/dist/react/presentation/DialogSurface.js.map +1 -1
- package/dist/react/primitives/Checkbox.d.ts +4 -2
- package/dist/react/primitives/Checkbox.js +2 -1
- package/dist/react/primitives/Checkbox.js.map +1 -1
- package/dist/react/primitives/FieldShell.d.ts +2 -4
- package/dist/react/primitives/FieldShell.js +12 -19
- package/dist/react/primitives/FieldShell.js.map +1 -1
- package/dist/react/primitives/Input.d.ts +1 -1
- package/dist/react/primitives/Input.js.map +1 -1
- package/dist/react/renderers/calculation.js +1 -4
- package/dist/react/renderers/calculation.js.map +1 -1
- package/dist/react/renderers/checkbox.js +1 -4
- package/dist/react/renderers/checkbox.js.map +1 -1
- package/dist/react/renderers/consent.js +78 -35
- package/dist/react/renderers/consent.js.map +1 -1
- package/dist/react/renderers/date.js +33 -0
- package/dist/react/renderers/date.js.map +1 -0
- package/dist/react/renderers/email.js +1 -4
- package/dist/react/renderers/email.js.map +1 -1
- package/dist/react/renderers/file.js +29 -14
- package/dist/react/renderers/file.js.map +1 -1
- package/dist/react/renderers/index.js +9 -0
- package/dist/react/renderers/index.js.map +1 -1
- package/dist/react/renderers/message.js +41 -0
- package/dist/react/renderers/message.js.map +1 -0
- package/dist/react/renderers/number.js +1 -4
- package/dist/react/renderers/number.js.map +1 -1
- package/dist/react/renderers/region.js +43 -0
- package/dist/react/renderers/region.js.map +1 -0
- package/dist/react/renderers/repeater.js +131 -0
- package/dist/react/renderers/repeater.js.map +1 -0
- package/dist/react/renderers/select.js +1 -4
- package/dist/react/renderers/select.js.map +1 -1
- package/dist/react/renderers/text.js +1 -4
- package/dist/react/renderers/text.js.map +1 -1
- package/dist/react/renderers/textarea.js +1 -4
- package/dist/react/renderers/textarea.js.map +1 -1
- package/dist/react/state.d.ts +40 -3
- package/dist/react/state.js +64 -9
- package/dist/react/state.js.map +1 -1
- package/dist/react/uploadFile.d.ts +2 -2
- package/dist/react/uploadFile.js +1 -1
- package/dist/react/uploadFile.js.map +1 -1
- package/dist/react/useField.d.ts +0 -1
- package/dist/react/useField.js +6 -6
- package/dist/react/useField.js.map +1 -1
- package/dist/react/useFormState.d.ts +1 -1
- package/dist/react/useFormState.js +1 -1
- package/dist/react/useFormState.js.map +1 -1
- package/dist/react/validateField.js +3 -6
- package/dist/react/validateField.js.map +1 -1
- package/dist/recall/interpolate.d.ts +4 -3
- package/dist/recall/interpolate.js +5 -4
- package/dist/recall/interpolate.js.map +1 -1
- package/dist/recall/resolver.d.ts +1 -1
- package/dist/recall/resolver.js +14 -2
- package/dist/recall/resolver.js.map +1 -1
- package/dist/spam/captcha.d.ts +4 -3
- package/dist/spam/captcha.js +4 -3
- package/dist/spam/captcha.js.map +1 -1
- package/dist/spam/clientIp.js +9 -0
- package/dist/spam/clientIp.js.map +1 -0
- package/dist/spam/constants.js +7 -1
- package/dist/spam/constants.js.map +1 -1
- package/dist/spam/identify.js +3 -6
- package/dist/spam/identify.js.map +1 -1
- package/dist/spam/providers/hcaptcha.d.ts +17 -0
- package/dist/spam/providers/hcaptcha.js +18 -0
- package/dist/spam/providers/hcaptcha.js.map +1 -0
- package/dist/spam/providers/recaptcha.d.ts +23 -0
- package/dist/spam/providers/recaptcha.js +23 -0
- package/dist/spam/providers/recaptcha.js.map +1 -0
- package/dist/spam/providers/siteverify.js +34 -0
- package/dist/spam/providers/siteverify.js.map +1 -0
- package/dist/spam/providers/turnstile.d.ts +17 -0
- package/dist/spam/providers/turnstile.js +18 -0
- package/dist/spam/providers/turnstile.js.map +1 -0
- package/dist/spam/resolveSpam.js +2 -1
- package/dist/spam/resolveSpam.js.map +1 -1
- package/dist/spam/spamGuard.js +23 -10
- package/dist/spam/spamGuard.js.map +1 -1
- package/dist/spam/types.d.ts +19 -4
- package/dist/submissions/SubmissionAnswers.d.ts +2 -4
- package/dist/submissions/SubmissionAnswers.js +114 -34
- package/dist/submissions/SubmissionAnswers.js.map +1 -1
- package/dist/submissions/SubmissionAnswersClient.js +178 -0
- package/dist/submissions/SubmissionAnswersClient.js.map +1 -0
- package/dist/submissions/coerceBoolean.js +14 -0
- package/dist/submissions/coerceBoolean.js.map +1 -0
- package/dist/submissions/createSubmission.d.ts +36 -0
- package/dist/submissions/createSubmission.js +26 -0
- package/dist/submissions/createSubmission.js.map +1 -0
- package/dist/submissions/runSubmission.d.ts +10 -0
- package/dist/submissions/runSubmission.js +128 -27
- package/dist/submissions/runSubmission.js.map +1 -1
- package/dist/submissions/types.d.ts +12 -4
- package/dist/submissions/validateSubmission.js +62 -11
- package/dist/submissions/validateSubmission.js.map +1 -1
- package/dist/submissions/votedCookie.d.ts +13 -0
- package/dist/submissions/votedCookie.js +27 -0
- package/dist/submissions/votedCookie.js.map +1 -0
- package/dist/translations/clientRuntimeKeys.d.ts +17 -0
- package/dist/translations/clientRuntimeKeys.js +46 -0
- package/dist/translations/clientRuntimeKeys.js.map +1 -0
- package/dist/translations/de.d.ts +12 -0
- package/dist/translations/de.js +309 -0
- package/dist/translations/de.js.map +1 -0
- package/dist/translations/en.js +203 -27
- package/dist/translations/en.js.map +1 -1
- package/dist/translations/index.d.ts +14 -5
- package/dist/translations/index.js +14 -3
- package/dist/translations/index.js.map +1 -1
- package/dist/translations/keys.d.ts +199 -23
- package/dist/translations/keys.js +200 -24
- package/dist/translations/keys.js.map +1 -1
- package/dist/translations/makeTranslate.d.ts +7 -3
- package/dist/translations/makeTranslate.js +12 -3
- package/dist/translations/makeTranslate.js.map +1 -1
- package/dist/translations/server.js +8 -1
- package/dist/translations/server.js.map +1 -1
- package/dist/uploads/captureFileRef.d.ts +10 -3
- package/dist/uploads/captureFileRef.js +18 -9
- package/dist/uploads/captureFileRef.js.map +1 -1
- package/dist/uploads/formatBytes.d.ts +9 -0
- package/dist/uploads/formatBytes.js +25 -0
- package/dist/uploads/formatBytes.js.map +1 -0
- package/dist/uploads/resolveFileRef.js +3 -2
- package/dist/uploads/resolveFileRef.js.map +1 -1
- package/dist/uploads/types.d.ts +1 -2
- package/dist/validation/buildRuleBlocks.js +18 -17
- package/dist/validation/buildRuleBlocks.js.map +1 -1
- package/dist/validation/builtin/email.js +2 -1
- package/dist/validation/builtin/email.js.map +1 -1
- package/dist/validation/builtin/index.d.ts +14 -0
- package/dist/validation/builtin/index.js +12 -1
- package/dist/validation/builtin/index.js.map +1 -1
- package/dist/validation/builtin/matchesField.js +3 -7
- package/dist/validation/builtin/matchesField.js.map +1 -1
- package/dist/validation/builtin/max.js +1 -0
- package/dist/validation/builtin/max.js.map +1 -1
- package/dist/validation/builtin/maxDate.js +24 -0
- package/dist/validation/builtin/maxDate.js.map +1 -0
- package/dist/validation/builtin/maxLength.js +1 -0
- package/dist/validation/builtin/maxLength.js.map +1 -1
- package/dist/validation/builtin/min.js +1 -0
- package/dist/validation/builtin/min.js.map +1 -1
- package/dist/validation/builtin/minDate.js +24 -0
- package/dist/validation/builtin/minDate.js.map +1 -0
- package/dist/validation/builtin/minLength.js +1 -0
- package/dist/validation/builtin/minLength.js.map +1 -1
- package/dist/validation/builtin/notAlreadySubmitted.js +2 -0
- package/dist/validation/builtin/notAlreadySubmitted.js.map +1 -1
- package/dist/validation/builtin/oneOf.js +1 -0
- package/dist/validation/builtin/oneOf.js.map +1 -1
- package/dist/validation/builtin/pattern.js +11 -2
- package/dist/validation/builtin/pattern.js.map +1 -1
- package/dist/validation/builtin/url.js +1 -0
- package/dist/validation/builtin/url.js.map +1 -1
- package/dist/validation/builtin/validateDateBound.js +18 -0
- package/dist/validation/builtin/validateDateBound.js.map +1 -0
- package/dist/validation/builtin/validateRegexParams.js +44 -0
- package/dist/validation/builtin/validateRegexParams.js.map +1 -0
- package/dist/validation/emailPattern.js +10 -0
- package/dist/validation/emailPattern.js.map +1 -0
- package/dist/validation/fieldTargetParam.d.ts +22 -0
- package/dist/validation/fieldTargetParam.js +38 -0
- package/dist/validation/fieldTargetParam.js.map +1 -0
- package/dist/validation/registry.d.ts +2 -1
- package/dist/validation/registry.js +2 -9
- package/dist/validation/registry.js.map +1 -1
- package/dist/validation/runValidation.js +9 -30
- package/dist/validation/runValidation.js.map +1 -1
- package/dist/validation/types.d.ts +5 -11
- package/dist/validation/validateUrl.js +22 -0
- package/dist/validation/validateUrl.js.map +1 -0
- package/package.json +7 -5
- package/styles/form-builder.css +10 -190
- package/dist/collections/uploads.d.ts +0 -29
- package/dist/collections/uploads.js +0 -46
- package/dist/collections/uploads.js.map +0 -1
- package/dist/consent/buildConsentSourceConfig.js +0 -49
- package/dist/consent/buildConsentSourceConfig.js.map +0 -1
- package/dist/consent/builtin/index.d.ts +0 -7
- package/dist/consent/builtin/index.js +0 -11
- package/dist/consent/builtin/index.js.map +0 -1
- package/dist/consent/builtin/pageReference.js +0 -75
- package/dist/consent/builtin/pageReference.js.map +0 -1
- package/dist/consent/builtin/static.js +0 -44
- package/dist/consent/builtin/static.js.map +0 -1
- package/dist/consent/defineConsentSource.d.ts +0 -35
- package/dist/consent/defineConsentSource.js +0 -6
- package/dist/consent/defineConsentSource.js.map +0 -1
- package/dist/consent/registry.d.ts +0 -16
- package/dist/consent/registry.js +0 -16
- package/dist/consent/registry.js.map +0 -1
- package/dist/consent/resolveConsentLinks.d.ts +0 -19
- package/dist/consent/resolveConsentLinks.js +0 -26
- package/dist/consent/resolveConsentLinks.js.map +0 -1
- package/dist/presentations/registry.d.ts +0 -15
- package/dist/presentations/registry.js +0 -15
- package/dist/presentations/registry.js.map +0 -1
- package/dist/react/toFormDocument.d.ts +0 -24
- package/dist/react/toFormDocument.js +0 -22
- package/dist/react/toFormDocument.js.map +0 -1
package/dist/actions/task.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task.js","names":[],"sources":["../../src/actions/task.ts"],"sourcesContent":["import type { Config, Payload, PayloadRequest, TaskConfig } from 'payload'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport { FORMS_SLUG } from '../collections/forms'\nimport type { Translate } from '../fields/types'\nimport type { SubmissionDescriptor, SubmissionValue } from '../submissions/types'\nimport { asFieldTranslate } from '../translations/server'\nimport type { ActionRegistry } from './registry'\nimport type { ActionInstance } from './runActions'\nimport { runActions } from './runActions'\n\nexport const ACTIONS_TASK_SLUG = 'form-builder-actions'\n\n/** Input the dispatch path enqueues; the handler re-loads everything else from the DB. */\nexport type ActionsTaskInput = { formId: number | string; submissionId: number | string }\n\nconst asActions = (value: unknown): ActionInstance[] =>\n\tArray.isArray(value) ? (value as ActionInstance[]) : []\n\nconst asValues = (value: unknown): SubmissionValue[] =>\n\tArray.isArray(value) ? (value as SubmissionValue[]) : []\n\nconst asDescriptors = (value: unknown): SubmissionDescriptor[] =>\n\tArray.isArray(value) ? (value as SubmissionDescriptor[]) : []\n\n/**\n * Load the form and submission by id and run the form's actions through the shared, failure-isolating\n * `runActions`. Tolerates a missing form or submission (the row may have been deleted between enqueue and\n * run) by returning early. Used by both the queued task handler and the inline fallback.\n */\nexport const runActionsForSubmission = async (args: {\n\tinput: ActionsTaskInput\n\tregistry: ActionRegistry\n\tpayload: Payload\n\treq?: PayloadRequest\n}): Promise<
|
|
1
|
+
{"version":3,"file":"task.js","names":[],"sources":["../../src/actions/task.ts"],"sourcesContent":["import type { Config, Payload, PayloadRequest, TaskConfig, TypedLocale } from 'payload'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport { FORMS_SLUG } from '../collections/forms'\nimport type { Translate } from '../fields/types'\nimport type { SubmissionDescriptor, SubmissionValue } from '../submissions/types'\nimport { asFieldTranslate } from '../translations/server'\nimport type { RichTextBodyOption } from './body/serializeBody'\nimport type { ActionRegistry } from './registry'\nimport type { ActionInstance, ActionResult } from './runActions'\nimport { runActions } from './runActions'\n\nexport const ACTIONS_TASK_SLUG = 'form-builder-actions'\n\n/** Input the dispatch path enqueues; the handler re-loads everything else from the DB. */\nexport type ActionsTaskInput = { formId: number | string; submissionId: number | string }\n\nconst asActions = (value: unknown): ActionInstance[] =>\n\tArray.isArray(value) ? (value as ActionInstance[]) : []\n\nconst asValues = (value: unknown): SubmissionValue[] =>\n\tArray.isArray(value) ? (value as SubmissionValue[]) : []\n\nconst asDescriptors = (value: unknown): SubmissionDescriptor[] =>\n\tArray.isArray(value) ? (value as SubmissionDescriptor[]) : []\n\n/**\n * Load the form and submission by id and run the form's actions through the shared, failure-isolating\n * `runActions`. Tolerates a missing form or submission (the row may have been deleted between enqueue and\n * run) by returning early. Used by both the queued task handler and the inline fallback.\n */\nexport const runActionsForSubmission = async (args: {\n\tinput: ActionsTaskInput\n\tregistry: ActionRegistry\n\tpayload: Payload\n\treq?: PayloadRequest\n\trichText?: RichTextBodyOption\n}): Promise<ActionResult[]> => {\n\tconst { input, registry, payload, req, richText } = args\n\tconst submission = await payload\n\t\t.findByID({\n\t\t\tcollection: FORM_SUBMISSIONS_SLUG,\n\t\t\tid: input.submissionId,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t})\n\t\t.catch(() => null)\n\tif (!submission) {\n\t\treturn []\n\t}\n\n\t// The submission's own stored locale (set from req.locale at submit) is authoritative, so the form\n\t// is loaded at it. A localized action config, notably the emailTeam `to`, then resolves to the\n\t// submission's locale even on the queued path, where the job runner's req may carry a different\n\t// (or no) locale than the visitor who submitted.\n\tconst locale = typeof submission.locale === 'string' ? submission.locale : (req?.locale ?? 'en')\n\n\tconst form = await payload\n\t\t.findByID({\n\t\t\tcollection: FORMS_SLUG,\n\t\t\tid: input.formId,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\t// Cast: the stored locale is a plain string; a host's concrete locale union is unknowable from\n\t\t\t// the plugin, and an unrecognized code just falls back on read, so this narrows (zero runtime\n\t\t\t// delta) to satisfy a host whose `findByID` locale is a real union.\n\t\t\tlocale: locale as TypedLocale,\n\t\t\treq,\n\t\t})\n\t\t.catch(() => null)\n\tif (!form) {\n\t\treturn []\n\t}\n\n\tconst t: Translate = asFieldTranslate(req?.i18n?.t ?? ((key: string) => key))\n\n\tconst results = await runActions({\n\t\tactions: asActions(form.actions),\n\t\tregistry,\n\t\trichText,\n\t\tform: { id: form.id, title: typeof form.title === 'string' ? form.title : undefined },\n\t\tsubmissionId: submission.id,\n\t\tvalues: asValues(submission.values),\n\t\tdescriptors: asDescriptors(submission.descriptors),\n\t\tpayload,\n\t\treq,\n\t\tlocale,\n\t\tt,\n\t})\n\t// A failed action (SMTP down, webhook non-2xx, missing adapter) is isolated per action; surface it\n\t// so a silently undelivered email/webhook is visible instead of the submission looking successful.\n\tfor (const result of results) {\n\t\tif (!result.ok) {\n\t\t\tpayload.logger?.error(\n\t\t\t\t`@10x-media/form-builder: action \"${result.type}\" failed for submission ${String(submission.id)}: ${result.error ?? 'unknown error'}`\n\t\t\t)\n\t\t}\n\t}\n\treturn results\n}\n\n/** Native Payload jobs task that runs a submission's post-submit actions out of band. */\nexport const buildActionsTask = (\n\tregistry: ActionRegistry,\n\trichText?: RichTextBodyOption\n): TaskConfig =>\n\t({\n\t\tslug: ACTIONS_TASK_SLUG,\n\t\tinputSchema: [\n\t\t\t{ name: 'formId', type: 'text', required: true },\n\t\t\t{ name: 'submissionId', type: 'text', required: true },\n\t\t],\n\t\thandler: async ({ input, req }) => {\n\t\t\tawait runActionsForSubmission({\n\t\t\t\tinput: input as ActionsTaskInput,\n\t\t\t\tregistry,\n\t\t\t\tpayload: req.payload,\n\t\t\t\treq,\n\t\t\t\trichText,\n\t\t\t})\n\t\t\treturn { output: {} }\n\t\t},\n\t}) as TaskConfig\n\n/** Register the actions task on `config.jobs.tasks`, creating the jobs config if absent. */\nexport const registerActionsTask = (\n\tconfig: Config,\n\tregistry: ActionRegistry,\n\trichText?: RichTextBodyOption\n): void => {\n\tconfig.jobs ??= {}\n\tconfig.jobs.tasks ??= []\n\tconfig.jobs.tasks.push(buildActionsTask(registry, richText))\n}\n"],"mappings":";;;;;AAWA,MAAa,oBAAoB;AAKjC,MAAM,aAAa,UAClB,MAAM,QAAQ,KAAK,IAAK,QAA6B,CAAC;AAEvD,MAAM,YAAY,UACjB,MAAM,QAAQ,KAAK,IAAK,QAA8B,CAAC;AAExD,MAAM,iBAAiB,UACtB,MAAM,QAAQ,KAAK,IAAK,QAAmC,CAAC;;;;;;AAO7D,MAAa,0BAA0B,OAAO,SAMf;CAC9B,MAAM,EAAE,OAAO,UAAU,SAAS,KAAK,aAAa;CACpD,MAAM,aAAa,MAAM,QACvB,SAAS;EACT,YAAY;EACZ,IAAI,MAAM;EACV,OAAO;EACP,gBAAgB;EAChB;CACD,CAAC,EACA,YAAY,IAAI;CAClB,IAAI,CAAC,YACJ,OAAO,CAAC;CAOT,MAAM,SAAS,OAAO,WAAW,WAAW,WAAW,WAAW,SAAU,KAAK,UAAU;CAE3F,MAAM,OAAO,MAAM,QACjB,SAAS;EACT,YAAY;EACZ,IAAI,MAAM;EACV,OAAO;EACP,gBAAgB;EAIR;EACR;CACD,CAAC,EACA,YAAY,IAAI;CAClB,IAAI,CAAC,MACJ,OAAO,CAAC;CAGT,MAAM,IAAe,iBAAiB,KAAK,MAAM,OAAO,QAAgB,IAAI;CAE5E,MAAM,UAAU,MAAM,WAAW;EAChC,SAAS,UAAU,KAAK,OAAO;EAC/B;EACA;EACA,MAAM;GAAE,IAAI,KAAK;GAAI,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ,KAAA;EAAU;EACpF,cAAc,WAAW;EACzB,QAAQ,SAAS,WAAW,MAAM;EAClC,aAAa,cAAc,WAAW,WAAW;EACjD;EACA;EACA;EACA;CACD,CAAC;CAGD,KAAK,MAAM,UAAU,SACpB,IAAI,CAAC,OAAO,IACX,QAAQ,QAAQ,MACf,oCAAoC,OAAO,KAAK,0BAA0B,OAAO,WAAW,EAAE,EAAE,IAAI,OAAO,SAAS,iBACrH;CAGF,OAAO;AACR;;AAGA,MAAa,oBACZ,UACA,cAEC;CACA,MAAM;CACN,aAAa,CACZ;EAAE,MAAM;EAAU,MAAM;EAAQ,UAAU;CAAK,GAC/C;EAAE,MAAM;EAAgB,MAAM;EAAQ,UAAU;CAAK,CACtD;CACA,SAAS,OAAO,EAAE,OAAO,UAAU;EAClC,MAAM,wBAAwB;GACtB;GACP;GACA,SAAS,IAAI;GACb;GACA;EACD,CAAC;EACD,OAAO,EAAE,QAAQ,CAAC,EAAE;CACrB;AACD;;AAGD,MAAa,uBACZ,QACA,UACA,aACU;CACV,OAAO,SAAS,CAAC;CACjB,OAAO,KAAK,UAAU,CAAC;CACvB,OAAO,KAAK,MAAM,KAAK,iBAAiB,UAAU,QAAQ,CAAC;AAC5D"}
|
|
@@ -11,6 +11,15 @@ type AggregateFormResponsesArgs = {
|
|
|
11
11
|
req?: PayloadRequest; /** Safety cap on submissions scanned; beyond it the result is flagged `truncated`. Default 10000. */
|
|
12
12
|
maxSubmissions?: number; /** Page size for the submission scan. Default 500. */
|
|
13
13
|
pageSize?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Source-resolved options by field name (an option-source poll's results field). They replace
|
|
16
|
+
* the instance's authored options for bucket seeding, order, and labels, and mark the field
|
|
17
|
+
* enumerable for the default field selection.
|
|
18
|
+
*/
|
|
19
|
+
resolvedOptions?: Record<string, {
|
|
20
|
+
value: string;
|
|
21
|
+
label: string;
|
|
22
|
+
}[]>;
|
|
14
23
|
};
|
|
15
24
|
/** True when a field declares non-empty options (a choice field safe to aggregate publicly). */
|
|
16
25
|
declare const fieldHasOptions: (field: FormFieldInstance) => boolean;
|
|
@@ -1,22 +1,16 @@
|
|
|
1
|
+
import { isNamedField } from "../fields/fieldKey.js";
|
|
2
|
+
import { instanceOptionsOf } from "../fields/instanceOptions.js";
|
|
1
3
|
import { aggregateRowsForFields } from "./aggregateRows.js";
|
|
2
4
|
import { FORMS_SLUG } from "../collections/forms.js";
|
|
3
5
|
import { FORM_SUBMISSIONS_SLUG } from "../collections/formSubmissions.js";
|
|
4
6
|
//#region src/aggregation/aggregateResponses.ts
|
|
5
|
-
const optionsOf = (field) => {
|
|
6
|
-
if (!Array.isArray(field.options)) return;
|
|
7
|
-
const options = field.options.filter((option) => typeof option?.value === "string").map((option) => ({
|
|
8
|
-
value: String(option.value),
|
|
9
|
-
label: option.label ?? String(option.value)
|
|
10
|
-
}));
|
|
11
|
-
return options.length > 0 ? options : void 0;
|
|
12
|
-
};
|
|
13
7
|
/** True when a field declares non-empty options (a choice field safe to aggregate publicly). */
|
|
14
|
-
const fieldHasOptions = (field) =>
|
|
15
|
-
const metaFor = (field) => ({
|
|
8
|
+
const fieldHasOptions = (field) => instanceOptionsOf(field) !== void 0;
|
|
9
|
+
const metaFor = (field, resolved) => ({
|
|
16
10
|
field: field.name,
|
|
17
11
|
label: field.label ?? field.name,
|
|
18
12
|
fieldType: field.blockType,
|
|
19
|
-
options:
|
|
13
|
+
options: resolved && resolved.length > 0 ? resolved : instanceOptionsOf(field)
|
|
20
14
|
});
|
|
21
15
|
/**
|
|
22
16
|
* Aggregate submission responses for a form. Loads the form for current labels + option order, then pages
|
|
@@ -25,7 +19,7 @@ const metaFor = (field) => ({
|
|
|
25
19
|
* `truncated`. Returns one `FieldAggregation` per requested field (or per enumerable field by default).
|
|
26
20
|
*/
|
|
27
21
|
const aggregateFormResponses = async (args) => {
|
|
28
|
-
const { payload, formId, fields, status = "complete", req, maxSubmissions = 1e4, pageSize = 500 } = args;
|
|
22
|
+
const { payload, formId, fields, status = "complete", req, maxSubmissions = 1e4, pageSize = 500, resolvedOptions } = args;
|
|
29
23
|
const form = await payload.findByID({
|
|
30
24
|
collection: FORMS_SLUG,
|
|
31
25
|
id: formId,
|
|
@@ -35,9 +29,9 @@ const aggregateFormResponses = async (args) => {
|
|
|
35
29
|
}).catch(() => null);
|
|
36
30
|
if (!form || !Array.isArray(form.fields)) return [];
|
|
37
31
|
const instances = form.fields;
|
|
38
|
-
const selected = fields ? instances.filter((field) => fields.includes(field.name)) : instances.filter((field) =>
|
|
32
|
+
const selected = fields ? instances.filter((field) => isNamedField(field) && fields.includes(field.name)) : instances.filter((field) => isNamedField(field) && (instanceOptionsOf(field) !== void 0 || (resolvedOptions?.[field.name]?.length ?? 0) > 0));
|
|
39
33
|
if (selected.length === 0) return [];
|
|
40
|
-
const metas = selected.map(metaFor);
|
|
34
|
+
const metas = selected.map((field) => metaFor(field, resolvedOptions?.[field.name]));
|
|
41
35
|
const where = { and: [{ form: { equals: formId } }, ...status === "all" ? [] : [{ status: { equals: status } }]] };
|
|
42
36
|
const rows = [];
|
|
43
37
|
let page = 1;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aggregateResponses.js","names":[],"sources":["../../src/aggregation/aggregateResponses.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport { FORMS_SLUG } from '../collections/forms'\nimport type { FormFieldInstance } from '../submissions/types'\nimport { aggregateRowsForFields } from './aggregateRows'\nimport type { AggregationRow, FieldAggregation, FieldMeta, SubmissionStatusFilter } from './types'\n\nexport type AggregateFormResponsesArgs = {\n\tpayload: Payload\n\tformId: number | string\n\t/** Field machine names to aggregate. Defaults to every field that declares options (enumerable). */\n\tfields?: string[]\n\t/** Submission status to count. Defaults to `complete`. */\n\tstatus?: SubmissionStatusFilter\n\treq?: PayloadRequest\n\t/** Safety cap on submissions scanned; beyond it the result is flagged `truncated`. Default 10000. */\n\tmaxSubmissions?: number\n\t/** Page size for the submission scan. Default 500. */\n\tpageSize?: number\n
|
|
1
|
+
{"version":3,"file":"aggregateResponses.js","names":[],"sources":["../../src/aggregation/aggregateResponses.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport { FORMS_SLUG } from '../collections/forms'\nimport { isNamedField, type NamedFormFieldInstance } from '../fields/fieldKey'\nimport { instanceOptionsOf } from '../fields/instanceOptions'\nimport type { FormFieldInstance } from '../submissions/types'\nimport { aggregateRowsForFields } from './aggregateRows'\nimport type { AggregationRow, FieldAggregation, FieldMeta, SubmissionStatusFilter } from './types'\n\nexport type AggregateFormResponsesArgs = {\n\tpayload: Payload\n\tformId: number | string\n\t/** Field machine names to aggregate. Defaults to every field that declares options (enumerable). */\n\tfields?: string[]\n\t/** Submission status to count. Defaults to `complete`. */\n\tstatus?: SubmissionStatusFilter\n\treq?: PayloadRequest\n\t/** Safety cap on submissions scanned; beyond it the result is flagged `truncated`. Default 10000. */\n\tmaxSubmissions?: number\n\t/** Page size for the submission scan. Default 500. */\n\tpageSize?: number\n\t/**\n\t * Source-resolved options by field name (an option-source poll's results field). They replace\n\t * the instance's authored options for bucket seeding, order, and labels, and mark the field\n\t * enumerable for the default field selection.\n\t */\n\tresolvedOptions?: Record<string, { value: string; label: string }[]>\n}\n\n/** True when a field declares non-empty options (a choice field safe to aggregate publicly). */\nexport const fieldHasOptions = (field: FormFieldInstance): boolean =>\n\tinstanceOptionsOf(field) !== undefined\n\nconst metaFor = (\n\tfield: NamedFormFieldInstance,\n\tresolved?: { value: string; label: string }[]\n): FieldMeta => ({\n\tfield: field.name,\n\tlabel: field.label ?? field.name,\n\tfieldType: field.blockType,\n\toptions: resolved && resolved.length > 0 ? resolved : instanceOptionsOf(field),\n})\n\n/**\n * Aggregate submission responses for a form. Loads the form for current labels + option order, then pages\n * `payload.find` over its submissions (JSON `values` cannot be GROUP BY'd portably across Mongo + Postgres,\n * so we scan + reduce in JS, cross-DB-identically). Bounded by `maxSubmissions`; beyond it the result is\n * `truncated`. Returns one `FieldAggregation` per requested field (or per enumerable field by default).\n */\nexport const aggregateFormResponses = async (\n\targs: AggregateFormResponsesArgs\n): Promise<FieldAggregation[]> => {\n\tconst {\n\t\tpayload,\n\t\tformId,\n\t\tfields,\n\t\tstatus = 'complete',\n\t\treq,\n\t\tmaxSubmissions = 10000,\n\t\tpageSize = 500,\n\t\tresolvedOptions,\n\t} = args\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 || !Array.isArray(form.fields)) {\n\t\treturn []\n\t}\n\tconst instances = form.fields as FormFieldInstance[]\n\tconst selected = fields\n\t\t? instances.filter(\n\t\t\t\t(field): field is NamedFormFieldInstance =>\n\t\t\t\t\tisNamedField(field) && fields.includes(field.name)\n\t\t\t)\n\t\t: instances.filter(\n\t\t\t\t(field): field is NamedFormFieldInstance =>\n\t\t\t\t\tisNamedField(field) &&\n\t\t\t\t\t(instanceOptionsOf(field) !== undefined ||\n\t\t\t\t\t\t(resolvedOptions?.[field.name]?.length ?? 0) > 0)\n\t\t\t)\n\tif (selected.length === 0) {\n\t\treturn []\n\t}\n\tconst metas = selected.map((field) => metaFor(field, resolvedOptions?.[field.name]))\n\n\tconst statusWhere = status === 'all' ? [] : [{ status: { equals: status } }]\n\tconst where = { and: [{ form: { equals: formId } }, ...statusWhere] }\n\n\tconst rows: AggregationRow[] = []\n\tlet page = 1\n\tlet truncated = false\n\tfor (;;) {\n\t\tconst result = await payload.find({\n\t\t\tcollection: FORM_SUBMISSIONS_SLUG,\n\t\t\twhere,\n\t\t\tlimit: pageSize,\n\t\t\tpage,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t\tselect: { values: true, descriptors: true },\n\t\t})\n\t\tfor (const doc of result.docs as AggregationRow[]) {\n\t\t\t// Check before pushing so `truncated` means \"matching rows were left out\", not \"the cap was reached\n\t\t\t// exactly\": a form with precisely `maxSubmissions` responses is complete, not a sample.\n\t\t\tif (rows.length >= maxSubmissions) {\n\t\t\t\ttruncated = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\trows.push({ values: doc.values, descriptors: doc.descriptors })\n\t\t}\n\t\tif (truncated || !result.hasNextPage) {\n\t\t\tbreak\n\t\t}\n\t\tpage += 1\n\t}\n\n\treturn aggregateRowsForFields(rows, metas, truncated)\n}\n\nexport type AggregateFieldResponsesArgs = Omit<AggregateFormResponsesArgs, 'fields'> & {\n\tfield: string\n}\n\n/** Single-field convenience over `aggregateFormResponses`. Returns the field's aggregation, or null if absent. */\nexport const aggregateFieldResponses = async (\n\targs: AggregateFieldResponsesArgs\n): Promise<FieldAggregation | null> => {\n\tconst { field, ...rest } = args\n\tconst [result] = await aggregateFormResponses({ ...rest, fields: [field] })\n\treturn result ?? null\n}\n"],"mappings":";;;;;;;AA8BA,MAAa,mBAAmB,UAC/B,kBAAkB,KAAK,MAAM,KAAA;AAE9B,MAAM,WACL,OACA,cACgB;CAChB,OAAO,MAAM;CACb,OAAO,MAAM,SAAS,MAAM;CAC5B,WAAW,MAAM;CACjB,SAAS,YAAY,SAAS,SAAS,IAAI,WAAW,kBAAkB,KAAK;AAC9E;;;;;;;AAQA,MAAa,yBAAyB,OACrC,SACiC;CACjC,MAAM,EACL,SACA,QACA,QACA,SAAS,YACT,KACA,iBAAiB,KACjB,WAAW,KACX,oBACG;CACJ,MAAM,OAAO,MAAM,QACjB,SAAS;EAAE,YAAY;EAAY,IAAI;EAAQ,OAAO;EAAG,gBAAgB;EAAM;CAAI,CAAC,EACpF,YAAY,IAAI;CAClB,IAAI,CAAC,QAAQ,CAAC,MAAM,QAAQ,KAAK,MAAM,GACtC,OAAO,CAAC;CAET,MAAM,YAAY,KAAK;CACvB,MAAM,WAAW,SACd,UAAU,QACT,UACA,aAAa,KAAK,KAAK,OAAO,SAAS,MAAM,IAAI,CACnD,IACC,UAAU,QACT,UACA,aAAa,KAAK,MACjB,kBAAkB,KAAK,MAAM,KAAA,MAC5B,kBAAkB,MAAM,OAAO,UAAU,KAAK,EAClD;CACF,IAAI,SAAS,WAAW,GACvB,OAAO,CAAC;CAET,MAAM,QAAQ,SAAS,KAAK,UAAU,QAAQ,OAAO,kBAAkB,MAAM,KAAK,CAAC;CAGnF,MAAM,QAAQ,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,OAAO,EAAE,GAAG,GADhC,WAAW,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,QAAQ,OAAO,EAAE,CAAC,CACT,EAAE;CAEpE,MAAM,OAAyB,CAAC;CAChC,IAAI,OAAO;CACX,IAAI,YAAY;CAChB,SAAS;EACR,MAAM,SAAS,MAAM,QAAQ,KAAK;GACjC,YAAY;GACZ;GACA,OAAO;GACP;GACA,OAAO;GACP,gBAAgB;GAChB;GACA,QAAQ;IAAE,QAAQ;IAAM,aAAa;GAAK;EAC3C,CAAC;EACD,KAAK,MAAM,OAAO,OAAO,MAA0B;GAGlD,IAAI,KAAK,UAAU,gBAAgB;IAClC,YAAY;IACZ;GACD;GACA,KAAK,KAAK;IAAE,QAAQ,IAAI;IAAQ,aAAa,IAAI;GAAY,CAAC;EAC/D;EACA,IAAI,aAAa,CAAC,OAAO,aACxB;EAED,QAAQ;CACT;CAEA,OAAO,uBAAuB,MAAM,OAAO,SAAS;AACrD;;AAOA,MAAa,0BAA0B,OACtC,SACsC;CACtC,MAAM,EAAE,OAAO,GAAG,SAAS;CAC3B,MAAM,CAAC,UAAU,MAAM,uBAAuB;EAAE,GAAG;EAAM,QAAQ,CAAC,KAAK;CAAE,CAAC;CAC1E,OAAO,UAAU;AAClB"}
|
|
@@ -2,12 +2,33 @@ 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[];
|
|
11
32
|
};
|
|
12
33
|
type ResolveResultsRequestResult = {
|
|
13
34
|
status: number;
|
|
@@ -21,12 +42,20 @@ type ResolveResultsRequestResult = {
|
|
|
21
42
|
};
|
|
22
43
|
/**
|
|
23
44
|
* Authorize and resolve a poll/survey results request. Authed callers may aggregate any field (or all
|
|
24
|
-
* enumerable fields)
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* only
|
|
45
|
+
* enumerable fields) and bypass the `access` seam; for a poll-enabled form they also get the results
|
|
46
|
+
* field's effective options injected so sourced/resolver-backed polls render labels (best-effort, a
|
|
47
|
+
* resolve failure degrades to raw values rather than blocking the trusted read). Anonymous callers are
|
|
48
|
+
* allowed only when the form's poll is enabled, the poll's `resultsVisibility` permits it (`afterVote`:
|
|
49
|
+
* any time; `afterClose`: only once `closesAt` has passed), and the optional host `access` seam
|
|
50
|
+
* approves; and then only for the configured `poll.resultsField`, and only if that field is enumerable
|
|
51
|
+
* so a misconfigured `resultsField` pointing at a free-text or PII field can never be dumped publicly. A
|
|
52
|
+
* static poll is enumerable through its authored options; a poll whose options come from an
|
|
53
|
+
* `optionSource` or the field type's own `resolveOptions` resolves them here (registry via
|
|
54
|
+
* `config.custom`, per-request cache), gated by `eligibleTypes` and enumerable only when resolution
|
|
55
|
+
* yields any, with the resolved options driving bucket order and labels. Resolution failure fails
|
|
56
|
+
* closed (503) on the anonymous path. Returns only aggregate counts, never raw submissions.
|
|
28
57
|
*/
|
|
29
58
|
declare const resolveFormResultsRequest: (args: ResolveResultsRequestArgs) => Promise<ResolveResultsRequestResult>;
|
|
30
59
|
//#endregion
|
|
31
|
-
export { ResolveResultsRequestArgs, ResolveResultsRequestResult, resolveFormResultsRequest };
|
|
60
|
+
export { FormResultsAccess, FormResultsAccessArgs, ResolveResultsRequestArgs, ResolveResultsRequestResult, resolveFormResultsRequest };
|
|
32
61
|
//# sourceMappingURL=resolveResultsRequest.d.ts.map
|
|
@@ -1,19 +1,35 @@
|
|
|
1
|
+
import { isPollClosed, pollConfigOf } from "../form/pollState.js";
|
|
1
2
|
import { aggregateFormResponses, fieldHasOptions } from "./aggregateResponses.js";
|
|
3
|
+
import { resolvePollOutcome } from "../poll/resolvePollOutcome.js";
|
|
4
|
+
import { shouldAutoResolvePoll } from "../poll/closeJob.js";
|
|
5
|
+
import { resolveEffectivePollOptions } from "../poll/effectivePollOptions.js";
|
|
2
6
|
import { FORMS_SLUG } from "../collections/forms.js";
|
|
3
7
|
//#region src/aggregation/resolveResultsRequest.ts
|
|
4
8
|
const forbidden = {
|
|
5
9
|
status: 403,
|
|
6
10
|
body: { errors: [{ message: "Forbidden" }] }
|
|
7
11
|
};
|
|
12
|
+
const unavailable = {
|
|
13
|
+
status: 503,
|
|
14
|
+
body: { errors: [{ message: "Poll options unavailable" }] }
|
|
15
|
+
};
|
|
8
16
|
/**
|
|
9
17
|
* Authorize and resolve a poll/survey results request. Authed callers may aggregate any field (or all
|
|
10
|
-
* enumerable fields)
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* only
|
|
18
|
+
* enumerable fields) and bypass the `access` seam; for a poll-enabled form they also get the results
|
|
19
|
+
* field's effective options injected so sourced/resolver-backed polls render labels (best-effort, a
|
|
20
|
+
* resolve failure degrades to raw values rather than blocking the trusted read). Anonymous callers are
|
|
21
|
+
* allowed only when the form's poll is enabled, the poll's `resultsVisibility` permits it (`afterVote`:
|
|
22
|
+
* any time; `afterClose`: only once `closesAt` has passed), and the optional host `access` seam
|
|
23
|
+
* approves; and then only for the configured `poll.resultsField`, and only if that field is enumerable
|
|
24
|
+
* so a misconfigured `resultsField` pointing at a free-text or PII field can never be dumped publicly. A
|
|
25
|
+
* static poll is enumerable through its authored options; a poll whose options come from an
|
|
26
|
+
* `optionSource` or the field type's own `resolveOptions` resolves them here (registry via
|
|
27
|
+
* `config.custom`, per-request cache), gated by `eligibleTypes` and enumerable only when resolution
|
|
28
|
+
* yields any, with the resolved options driving bucket order and labels. Resolution failure fails
|
|
29
|
+
* closed (503) on the anonymous path. Returns only aggregate counts, never raw submissions.
|
|
14
30
|
*/
|
|
15
31
|
const resolveFormResultsRequest = async (args) => {
|
|
16
|
-
const { payload, formId, field, isAuthed, req } = args;
|
|
32
|
+
const { payload, formId, field, isAuthed, req, access, eligibleTypes } = args;
|
|
17
33
|
if (formId == null) return {
|
|
18
34
|
status: 400,
|
|
19
35
|
body: { errors: [{ message: "Missing form id" }] }
|
|
@@ -29,15 +45,55 @@ const resolveFormResultsRequest = async (args) => {
|
|
|
29
45
|
status: 404,
|
|
30
46
|
body: { errors: [{ message: "Not found" }] }
|
|
31
47
|
};
|
|
48
|
+
if (shouldAutoResolvePoll(form)) await resolvePollOutcome({
|
|
49
|
+
payload,
|
|
50
|
+
formId,
|
|
51
|
+
req
|
|
52
|
+
}).catch(() => void 0);
|
|
32
53
|
let fields;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
54
|
+
let resolvedOptions;
|
|
55
|
+
if (isAuthed) {
|
|
56
|
+
fields = field ? [field] : void 0;
|
|
57
|
+
const poll = pollConfigOf(form.poll);
|
|
58
|
+
const resultsField = typeof poll?.resultsField === "string" && poll.resultsField.length > 0 ? poll.resultsField : void 0;
|
|
59
|
+
if (form.pollEnabled === true && resultsField && (!field || field === resultsField)) {
|
|
60
|
+
const options = await resolveEffectivePollOptions({
|
|
61
|
+
payload,
|
|
62
|
+
req,
|
|
63
|
+
form
|
|
64
|
+
}).catch(() => []);
|
|
65
|
+
if (options.length > 0) resolvedOptions = { [resultsField]: options };
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
const poll = pollConfigOf(form.poll);
|
|
69
|
+
if (form.pollEnabled !== true || !poll) return forbidden;
|
|
70
|
+
if (poll.resultsVisibility === "afterClose" && !isPollClosed(poll)) return forbidden;
|
|
71
|
+
if (access) {
|
|
72
|
+
if (!(req ? await access({
|
|
73
|
+
req,
|
|
74
|
+
form
|
|
75
|
+
}) : false)) return forbidden;
|
|
76
|
+
}
|
|
77
|
+
const publicField = typeof poll.resultsField === "string" && poll.resultsField.length > 0 ? poll.resultsField : void 0;
|
|
37
78
|
if (!publicField) return forbidden;
|
|
38
79
|
if (field && field !== publicField) return forbidden;
|
|
39
80
|
const instance = (Array.isArray(form.fields) ? form.fields : []).find((entry) => entry.name === publicField);
|
|
40
|
-
if (!instance
|
|
81
|
+
if (!instance) return forbidden;
|
|
82
|
+
const hasSource = typeof poll.optionSource === "string" && poll.optionSource.length > 0;
|
|
83
|
+
const authored = fieldHasOptions(instance);
|
|
84
|
+
if ((hasSource || !authored) && eligibleTypes && !eligibleTypes.includes(instance.blockType)) return forbidden;
|
|
85
|
+
let options;
|
|
86
|
+
try {
|
|
87
|
+
options = await resolveEffectivePollOptions({
|
|
88
|
+
payload,
|
|
89
|
+
req,
|
|
90
|
+
form
|
|
91
|
+
});
|
|
92
|
+
} catch {
|
|
93
|
+
return unavailable;
|
|
94
|
+
}
|
|
95
|
+
if (options.length === 0) return forbidden;
|
|
96
|
+
resolvedOptions = { [publicField]: options };
|
|
41
97
|
fields = [publicField];
|
|
42
98
|
}
|
|
43
99
|
return {
|
|
@@ -46,7 +102,8 @@ const resolveFormResultsRequest = async (args) => {
|
|
|
46
102
|
payload,
|
|
47
103
|
formId,
|
|
48
104
|
fields,
|
|
49
|
-
req
|
|
105
|
+
req,
|
|
106
|
+
resolvedOptions
|
|
50
107
|
}) }
|
|
51
108
|
};
|
|
52
109
|
};
|
|
@@ -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 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}\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/**\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 } = 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 }).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}\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\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":";;;;;;;AAiDA,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;;;;;;;;;;;;;;;;AAiBA,MAAa,4BAA4B,OACxC,SAC0C;CAC1C,MAAM,EAAE,SAAS,QAAQ,OAAO,UAAU,KAAK,QAAQ,kBAAkB;CACzE,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;CAAI,CAAC,EAAE,YAAY,KAAA,CAAS;CAGzE,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;EAE9C;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,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,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. */
|
|
@@ -14,7 +15,7 @@ const computeCalcFields = (fields, answers) => {
|
|
|
14
15
|
let next = answers;
|
|
15
16
|
for (const field of fields) {
|
|
16
17
|
const expr = calcExpressionOf(field);
|
|
17
|
-
if (expr) next = {
|
|
18
|
+
if (expr && isNamedField(field)) next = {
|
|
18
19
|
...next,
|
|
19
20
|
[field.name]: evaluateCalc(expr, next)
|
|
20
21
|
};
|
|
@@ -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":"
|
|
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 { 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 && isNamedField(field)) {\n\t\t\tnext = { ...next, [field.name]: evaluateCalc(expr, next) }\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;;;;;;AAOA,MAAa,qBACZ,QACA,YAC6B;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,IAAI;EAAE;CAE3D;CACA,OAAO;AACR"}
|
package/dist/calc/evaluate.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
//#region src/calc/evaluate.ts
|
|
2
|
-
const MAX_DEPTH = 64;
|
|
3
2
|
const toNumber = (value) => {
|
|
4
3
|
if (typeof value === "number") return Number.isFinite(value) ? value : 0;
|
|
5
4
|
if (typeof value === "string" && value.trim() !== "") {
|
|
@@ -18,7 +17,7 @@ const FUNCTIONS = {
|
|
|
18
17
|
floor: (a) => Math.floor(a[0] ?? 0)
|
|
19
18
|
};
|
|
20
19
|
const evalNode = (expr, answers, depth) => {
|
|
21
|
-
if (depth >
|
|
20
|
+
if (depth > 64 || expr == null || typeof expr !== "object") return 0;
|
|
22
21
|
switch (expr.type) {
|
|
23
22
|
case "lit": return finite(toNumber(expr.value));
|
|
24
23
|
case "ref": return toNumber(answers[expr.field]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"evaluate.js","names":[],"sources":["../../src/calc/evaluate.ts"],"sourcesContent":["import type
|
|
1
|
+
{"version":3,"file":"evaluate.js","names":[],"sources":["../../src/calc/evaluate.ts"],"sourcesContent":["import { type CalcExpression, MAX_DEPTH } from './types'\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,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,QAAA,MAAqB,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,23 +1,10 @@
|
|
|
1
|
+
import { CALC_FNS, CALC_OPS } from "./types.js";
|
|
1
2
|
//#region src/calc/normalizeCalc.ts
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
"+",
|
|
5
|
-
"-",
|
|
6
|
-
"*",
|
|
7
|
-
"/",
|
|
8
|
-
"%"
|
|
9
|
-
]);
|
|
10
|
-
const FN_SET = new Set([
|
|
11
|
-
"min",
|
|
12
|
-
"max",
|
|
13
|
-
"round",
|
|
14
|
-
"abs",
|
|
15
|
-
"ceil",
|
|
16
|
-
"floor"
|
|
17
|
-
]);
|
|
3
|
+
const isOp = (v) => CALC_OPS.includes(v);
|
|
4
|
+
const isFn = (v) => CALC_FNS.includes(v);
|
|
18
5
|
const isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
19
6
|
const normalizeNode = (value, depth) => {
|
|
20
|
-
if (depth >
|
|
7
|
+
if (depth > 64) return void 0;
|
|
21
8
|
if (!isRecord(value)) return void 0;
|
|
22
9
|
switch (value.type) {
|
|
23
10
|
case "lit": {
|
|
@@ -38,7 +25,7 @@ const normalizeNode = (value, depth) => {
|
|
|
38
25
|
}
|
|
39
26
|
case "op": {
|
|
40
27
|
const { op, left, right } = value;
|
|
41
|
-
if (
|
|
28
|
+
if (!isOp(op)) return void 0;
|
|
42
29
|
const l = normalizeNode(left, depth + 1);
|
|
43
30
|
if (!l) return void 0;
|
|
44
31
|
const r = normalizeNode(right, depth + 1);
|
|
@@ -60,7 +47,7 @@ const normalizeNode = (value, depth) => {
|
|
|
60
47
|
}
|
|
61
48
|
case "fn": {
|
|
62
49
|
const { fn, args } = value;
|
|
63
|
-
if (
|
|
50
|
+
if (!isFn(fn)) return void 0;
|
|
64
51
|
if (!Array.isArray(args)) return void 0;
|
|
65
52
|
const normalizedArgs = [];
|
|
66
53
|
for (const arg of args) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalizeCalc.js","names":[],"sources":["../../src/calc/normalizeCalc.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"normalizeCalc.js","names":[],"sources":["../../src/calc/normalizeCalc.ts"],"sourcesContent":["import {\n\tCALC_FNS,\n\tCALC_OPS,\n\ttype CalcExpression,\n\ttype CalcFn,\n\ttype CalcOp,\n\tMAX_DEPTH,\n} from './types'\n\nconst isOp = (v: unknown): v is CalcOp => (CALC_OPS as readonly unknown[]).includes(v)\nconst isFn = (v: unknown): v is CalcFn => (CALC_FNS as readonly unknown[]).includes(v)\n\nconst isRecord = (v: unknown): v is Record<string, unknown> =>\n\tv !== null && typeof v === 'object' && !Array.isArray(v)\n\nconst normalizeNode = (value: unknown, depth: number): CalcExpression | undefined => {\n\tif (depth > MAX_DEPTH) return undefined\n\tif (!isRecord(value)) return undefined\n\n\tswitch (value.type) {\n\t\tcase 'lit': {\n\t\t\tconst { value: v } = value\n\t\t\tif (typeof v !== 'number' || !Number.isFinite(v)) return undefined\n\t\t\treturn { type: 'lit', value: v }\n\t\t}\n\t\tcase 'ref': {\n\t\t\tconst { field } = value\n\t\t\tif (typeof field !== 'string') return undefined\n\t\t\treturn { type: 'ref', field }\n\t\t}\n\t\tcase 'op': {\n\t\t\tconst { op, left, right } = value\n\t\t\tif (!isOp(op)) return undefined\n\t\t\tconst l = normalizeNode(left, depth + 1)\n\t\t\tif (!l) return undefined\n\t\t\tconst r = normalizeNode(right, depth + 1)\n\t\t\tif (!r) return undefined\n\t\t\treturn { type: 'op', op, left: l, right: r }\n\t\t}\n\t\tcase 'neg': {\n\t\t\tconst operand = normalizeNode(value.operand, depth + 1)\n\t\t\tif (!operand) return undefined\n\t\t\treturn { type: 'neg', operand }\n\t\t}\n\t\tcase 'fn': {\n\t\t\tconst { fn, args } = value\n\t\t\tif (!isFn(fn)) return undefined\n\t\t\tif (!Array.isArray(args)) return undefined\n\t\t\tconst normalizedArgs: CalcExpression[] = []\n\t\t\tfor (const arg of args) {\n\t\t\t\tconst a = normalizeNode(arg, depth + 1)\n\t\t\t\tif (!a) return undefined\n\t\t\t\tnormalizedArgs.push(a)\n\t\t\t}\n\t\t\treturn { type: 'fn', fn, args: normalizedArgs }\n\t\t}\n\t\tcase 'weight': {\n\t\t\tconst { field, weights } = value\n\t\t\tif (typeof field !== 'string') return undefined\n\t\t\tif (!isRecord(weights)) return undefined\n\t\t\tconst normalizedWeights: Record<string, number> = {}\n\t\t\tfor (const [k, v] of Object.entries(weights)) {\n\t\t\t\tif (typeof v !== 'number' || !Number.isFinite(v)) return undefined\n\t\t\t\tnormalizedWeights[k] = v\n\t\t\t}\n\t\t\treturn { type: 'weight', field, weights: normalizedWeights }\n\t\t}\n\t\tdefault:\n\t\t\treturn undefined\n\t}\n}\n\n/** Structural guard: returns a valid CalcExpression if `value` is structurally sound (depth-guarded at 64), otherwise undefined. Never throws. */\nexport const normalizeCalc = (value: unknown): CalcExpression | undefined => normalizeNode(value, 0)\n"],"mappings":";;AASA,MAAM,QAAQ,MAA6B,SAAgC,SAAS,CAAC;AACrF,MAAM,QAAQ,MAA6B,SAAgC,SAAS,CAAC;AAErF,MAAM,YAAY,MACjB,MAAM,QAAQ,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC;AAExD,MAAM,iBAAiB,OAAgB,UAA8C;CACpF,IAAI,QAAA,IAAmB,OAAO,KAAA;CAC9B,IAAI,CAAC,SAAS,KAAK,GAAG,OAAO,KAAA;CAE7B,QAAQ,MAAM,MAAd;EACC,KAAK,OAAO;GACX,MAAM,EAAE,OAAO,MAAM;GACrB,IAAI,OAAO,MAAM,YAAY,CAAC,OAAO,SAAS,CAAC,GAAG,OAAO,KAAA;GACzD,OAAO;IAAE,MAAM;IAAO,OAAO;GAAE;EAChC;EACA,KAAK,OAAO;GACX,MAAM,EAAE,UAAU;GAClB,IAAI,OAAO,UAAU,UAAU,OAAO,KAAA;GACtC,OAAO;IAAE,MAAM;IAAO;GAAM;EAC7B;EACA,KAAK,MAAM;GACV,MAAM,EAAE,IAAI,MAAM,UAAU;GAC5B,IAAI,CAAC,KAAK,EAAE,GAAG,OAAO,KAAA;GACtB,MAAM,IAAI,cAAc,MAAM,QAAQ,CAAC;GACvC,IAAI,CAAC,GAAG,OAAO,KAAA;GACf,MAAM,IAAI,cAAc,OAAO,QAAQ,CAAC;GACxC,IAAI,CAAC,GAAG,OAAO,KAAA;GACf,OAAO;IAAE,MAAM;IAAM;IAAI,MAAM;IAAG,OAAO;GAAE;EAC5C;EACA,KAAK,OAAO;GACX,MAAM,UAAU,cAAc,MAAM,SAAS,QAAQ,CAAC;GACtD,IAAI,CAAC,SAAS,OAAO,KAAA;GACrB,OAAO;IAAE,MAAM;IAAO;GAAQ;EAC/B;EACA,KAAK,MAAM;GACV,MAAM,EAAE,IAAI,SAAS;GACrB,IAAI,CAAC,KAAK,EAAE,GAAG,OAAO,KAAA;GACtB,IAAI,CAAC,MAAM,QAAQ,IAAI,GAAG,OAAO,KAAA;GACjC,MAAM,iBAAmC,CAAC;GAC1C,KAAK,MAAM,OAAO,MAAM;IACvB,MAAM,IAAI,cAAc,KAAK,QAAQ,CAAC;IACtC,IAAI,CAAC,GAAG,OAAO,KAAA;IACf,eAAe,KAAK,CAAC;GACtB;GACA,OAAO;IAAE,MAAM;IAAM;IAAI,MAAM;GAAe;EAC/C;EACA,KAAK,UAAU;GACd,MAAM,EAAE,OAAO,YAAY;GAC3B,IAAI,OAAO,UAAU,UAAU,OAAO,KAAA;GACtC,IAAI,CAAC,SAAS,OAAO,GAAG,OAAO,KAAA;GAC/B,MAAM,oBAA4C,CAAC;GACnD,KAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,OAAO,GAAG;IAC7C,IAAI,OAAO,MAAM,YAAY,CAAC,OAAO,SAAS,CAAC,GAAG,OAAO,KAAA;IACzD,kBAAkB,KAAK;GACxB;GACA,OAAO;IAAE,MAAM;IAAU;IAAO,SAAS;GAAkB;EAC5D;EACA,SACC;CACF;AACD;;AAGA,MAAa,iBAAiB,UAA+C,cAAc,OAAO,CAAC"}
|
package/dist/calc/types.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
//#region src/calc/types.d.ts
|
|
2
|
+
/** Canonical operator grammar; `CalcExpression`, `normalizeCalc`, and the admin editor's jsonSchema all derive from this list. */
|
|
3
|
+
declare const CALC_OPS: readonly ["+", "-", "*", "/", "%"];
|
|
4
|
+
type CalcOp = (typeof CALC_OPS)[number];
|
|
5
|
+
/** Canonical function grammar; same single source of truth as `CALC_OPS`. */
|
|
6
|
+
declare const CALC_FNS: readonly ["min", "max", "round", "abs", "ceil", "floor"];
|
|
7
|
+
type CalcFn = (typeof CALC_FNS)[number];
|
|
2
8
|
/** A serializable, safe-by-construction calculation expression. No strings are parsed; the evaluator tree-walks this closed node set (never `eval`). */
|
|
3
9
|
type CalcExpression = {
|
|
4
10
|
type: 'lit';
|
|
@@ -8,7 +14,7 @@ type CalcExpression = {
|
|
|
8
14
|
field: string;
|
|
9
15
|
} | {
|
|
10
16
|
type: 'op';
|
|
11
|
-
op:
|
|
17
|
+
op: CalcOp;
|
|
12
18
|
left: CalcExpression;
|
|
13
19
|
right: CalcExpression;
|
|
14
20
|
} | {
|
|
@@ -16,7 +22,7 @@ type CalcExpression = {
|
|
|
16
22
|
operand: CalcExpression;
|
|
17
23
|
} | {
|
|
18
24
|
type: 'fn';
|
|
19
|
-
fn:
|
|
25
|
+
fn: CalcFn;
|
|
20
26
|
args: CalcExpression[];
|
|
21
27
|
} | {
|
|
22
28
|
type: 'weight';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/calc/types.ts
|
|
2
|
+
/** Canonical operator grammar; `CalcExpression`, `normalizeCalc`, and the admin editor's jsonSchema all derive from this list. */
|
|
3
|
+
const CALC_OPS = [
|
|
4
|
+
"+",
|
|
5
|
+
"-",
|
|
6
|
+
"*",
|
|
7
|
+
"/",
|
|
8
|
+
"%"
|
|
9
|
+
];
|
|
10
|
+
/** Canonical function grammar; same single source of truth as `CALC_OPS`. */
|
|
11
|
+
const CALC_FNS = [
|
|
12
|
+
"min",
|
|
13
|
+
"max",
|
|
14
|
+
"round",
|
|
15
|
+
"abs",
|
|
16
|
+
"ceil",
|
|
17
|
+
"floor"
|
|
18
|
+
];
|
|
19
|
+
//#endregion
|
|
20
|
+
export { CALC_FNS, CALC_OPS };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","names":[],"sources":["../../src/calc/types.ts"],"sourcesContent":["/** Canonical operator grammar; `CalcExpression`, `normalizeCalc`, and the admin editor's jsonSchema all derive from this list. */\nexport const CALC_OPS = ['+', '-', '*', '/', '%'] as const\nexport type CalcOp = (typeof CALC_OPS)[number]\n\n/** Canonical function grammar; same single source of truth as `CALC_OPS`. */\nexport const CALC_FNS = ['min', 'max', 'round', 'abs', 'ceil', 'floor'] as const\nexport type CalcFn = (typeof CALC_FNS)[number]\n\n/** Recursion guard shared by `normalizeCalc` (parse) and `evaluate` (walk) so both bound depth identically. */\nexport const MAX_DEPTH = 64\n\n/** A serializable, safe-by-construction calculation expression. No strings are parsed; the evaluator tree-walks this closed node set (never `eval`). */\nexport type CalcExpression =\n\t| { type: 'lit'; value: number }\n\t| { type: 'ref'; field: string }\n\t| { type: 'op'; op: CalcOp; left: CalcExpression; right: CalcExpression }\n\t| { type: 'neg'; operand: CalcExpression }\n\t| { type: 'fn'; fn: CalcFn; args: CalcExpression[] }\n\t| { type: 'weight'; field: string; weights: Record<string, number> }\n"],"mappings":";;AACA,MAAa,WAAW;CAAC;CAAK;CAAK;CAAK;CAAK;AAAG;;AAIhD,MAAa,WAAW;CAAC;CAAO;CAAO;CAAS;CAAO;CAAQ;AAAO"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { TranslationKey } from "../translations/keys.js";
|
|
2
|
+
|
|
3
|
+
//#region src/client/ByteSizeField.d.ts
|
|
4
|
+
/** Props: standard number field client props; label and description arrive resolved on `field`. */
|
|
5
|
+
type ByteSizeFieldProps = {
|
|
6
|
+
path?: string;
|
|
7
|
+
field?: {
|
|
8
|
+
label?: unknown;
|
|
9
|
+
admin?: {
|
|
10
|
+
description?: unknown;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
label?: unknown;
|
|
14
|
+
/**
|
|
15
|
+
* Field description as a translation key, resolved client-side. Payload drops `admin.description`
|
|
16
|
+
* functions from client fields, so a translated description must travel as a key; a static
|
|
17
|
+
* `admin.description` string still renders when this is unset.
|
|
18
|
+
*/
|
|
19
|
+
descriptionKey?: TranslationKey;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The `Field` mounted on a byte-count number config field (the file field's `maxSize`): a plain
|
|
23
|
+
* number input that live-previews the value as a human-readable size ('= 2.5 MB') so authors are
|
|
24
|
+
* not left decoding raw bytes. Stores the raw byte number unchanged.
|
|
25
|
+
*/
|
|
26
|
+
declare const ByteSizeField: (props: ByteSizeFieldProps) => import("react/jsx-runtime").JSX.Element;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { ByteSizeField };
|
|
29
|
+
//# sourceMappingURL=ByteSizeField.d.ts.map
|