@10x-media/form-builder 0.1.0-beta.26 → 0.1.0-beta.27
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 +16 -0
- package/LICENSE +21 -21
- package/dist/actions/dispatch.js +1 -1
- package/dist/actions/task.js +4 -4
- package/dist/actions/task.js.map +1 -1
- package/dist/aggregation/aggregateResponses.js +3 -4
- package/dist/aggregation/aggregateResponses.js.map +1 -1
- package/dist/aggregation/resolveResultsRequest.js +2 -2
- package/dist/collections/formSubmissions.js +11 -12
- package/dist/collections/formSubmissions.js.map +1 -1
- package/dist/collections/forms.js +12 -12
- package/dist/collections/forms.js.map +1 -1
- package/dist/consent/resolveConsentSourcesRequest.js +2 -2
- package/dist/consent/resolveConsentSourcesRequest.js.map +1 -1
- package/dist/exports/react.d.ts +2 -2
- package/dist/exports/react.js +2 -2
- package/dist/exports/rsc.d.ts +2 -1
- package/dist/exports/rsc.js +3 -2
- package/dist/exports/types.d.ts +2 -1
- package/dist/form/findFormAtLocale.js +2 -2
- package/dist/form/findFormAtLocale.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +14 -9
- package/dist/index.js.map +1 -1
- package/dist/options.d.ts +15 -1
- package/dist/plugin/collectionSlugs.d.ts +28 -0
- package/dist/plugin/collectionSlugs.js +65 -0
- package/dist/plugin/collectionSlugs.js.map +1 -0
- package/dist/plugin/customState.js +1 -1
- package/dist/plugin/customState.js.map +1 -1
- package/dist/plugin/registerCollections.js +7 -2
- package/dist/plugin/registerCollections.js.map +1 -1
- package/dist/poll/closeJob.js +2 -2
- package/dist/poll/closeJob.js.map +1 -1
- package/dist/poll/outcomeBeforeChange.js +2 -2
- package/dist/poll/outcomeBeforeChange.js.map +1 -1
- package/dist/poll/resolvePollCloseRequest.js +3 -3
- package/dist/poll/resolvePollCloseRequest.js.map +1 -1
- package/dist/poll/resolvePollOptionsRequest.js +2 -2
- package/dist/poll/resolvePollOptionsRequest.js.map +1 -1
- package/dist/poll/resolvePollOutcome.js +3 -3
- package/dist/poll/resolvePollOutcome.js.map +1 -1
- package/dist/poll/votes/aggregateFromVotes.js +3 -2
- package/dist/poll/votes/aggregateFromVotes.js.map +1 -1
- package/dist/poll/votes/bumpPollVote.js +9 -6
- package/dist/poll/votes/bumpPollVote.js.map +1 -1
- package/dist/poll/votes/recountPollVotes.js +4 -4
- package/dist/poll/votes/recountPollVotes.js.map +1 -1
- package/dist/poll/votes/voteTallyHook.js +2 -2
- package/dist/poll/votes/voteTallyHook.js.map +1 -1
- package/dist/poll/votes/votesCollection.d.ts +5 -1
- package/dist/poll/votes/votesCollection.js +7 -2
- package/dist/poll/votes/votesCollection.js.map +1 -1
- package/dist/react/Form.d.ts +11 -2
- package/dist/react/Form.js +15 -8
- package/dist/react/Form.js.map +1 -1
- package/dist/react/Poll.d.ts +1 -1
- package/dist/react/Poll.js +3 -0
- package/dist/react/Poll.js.map +1 -1
- package/dist/react/fetchResults.d.ts +5 -3
- package/dist/react/fetchResults.js +6 -4
- package/dist/react/fetchResults.js.map +1 -1
- package/dist/react/submitForm.d.ts +8 -3
- package/dist/react/submitForm.js +5 -3
- package/dist/react/submitForm.js.map +1 -1
- package/dist/submissions/createSubmission.d.ts +1 -1
- package/dist/submissions/createSubmission.js +3 -3
- package/dist/submissions/createSubmission.js.map +1 -1
- package/dist/submissions/resolveVotedSubmission.js +5 -5
- package/dist/submissions/resolveVotedSubmission.js.map +1 -1
- package/dist/submissions/runSubmission.js +1 -1
- package/dist/submissions/validateSubmission.js +7 -7
- package/dist/submissions/validateSubmission.js.map +1 -1
- package/dist/submissions/voteChangeEndpoint.js +11 -10
- package/dist/submissions/voteChangeEndpoint.js.map +1 -1
- package/dist/submissions/votedCookie.d.ts +8 -4
- package/dist/submissions/votedCookie.js +18 -15
- package/dist/submissions/votedCookie.js.map +1 -1
- package/dist/validation/builtin/notAlreadySubmitted.js +2 -2
- package/dist/validation/builtin/notAlreadySubmitted.js.map +1 -1
- package/package.json +7 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @10x-media/form-builder
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.27
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Collection slugs are now overridable. `overrides.forms.slug`, `overrides.formSubmissions.slug`, and `poll.votes.overrides.slug` rename the plugin's collections, and every internal path follows: the submissions relationship, the actions job and pruning, the vote tally's raw Mongo and Postgres writes, results and outcomes, `notAlreadySubmitted`, and the signed voted cookie. Two plugin collections sharing a slug is a boot error.
|
|
8
|
+
|
|
9
|
+
- `poll.cookiePrefix` renames the `fb-voted-{formId}` cookie (validated as a cookie-name token at boot). `hasVotedCookie` and `votedCookieName` take an optional `payload` to read it.
|
|
10
|
+
- `<Form collections={{ forms, formSubmissions }}>` points the browser transports at renamed collections; it reaches `<Poll>` and the registry form, and a custom `onSubmit` receives it as `input.collection`. `submitForm` and `fetchFormResults` accept `collection`.
|
|
11
|
+
- `collectionSlugsOf(payload)` returns the live slugs for host server code; `DEFAULT_COLLECTION_SLUGS` and the `FormBuilderCollectionSlugs` / `FormCollections` types are exported.
|
|
12
|
+
|
|
13
|
+
Behavioral change: a `slug` set on any of those overrides was previously accepted by the type and silently ignored. It is now honored, so a config that already carries one renames the collection on upgrade. Remove it to keep the default slug.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- Fix a multi-step focus race: submitting right after a step change could leave focus on the previous step's region, or nowhere, instead of on the first invalid field the form routed back to. React can run a render's effect late, just before the next render, and the late effect was acting on the newer request against DOM that did not contain its target. Each focus request is now handled only by the render it triggered.
|
|
18
|
+
|
|
3
19
|
## 0.1.0-beta.26
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 10x Media GmbH
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 10x Media GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/actions/dispatch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isEssentialAction } from "./registry.js";
|
|
2
|
-
import { ESSENTIAL_ACTION_FAILED_CONTEXT_KEY, ESSENTIAL_ACTION_UNCERTAIN_CONTEXT_KEY } from "./dispatchContext.js";
|
|
3
2
|
import { ACTIONS_TASK_SLUG, runActionsForSubmission } from "./task.js";
|
|
3
|
+
import { ESSENTIAL_ACTION_FAILED_CONTEXT_KEY, ESSENTIAL_ACTION_UNCERTAIN_CONTEXT_KEY } from "./dispatchContext.js";
|
|
4
4
|
//#region src/actions/dispatch.ts
|
|
5
5
|
/**
|
|
6
6
|
* Default cap on inline action work so a missing worker never hangs the submission response.
|
package/dist/actions/task.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { asFieldTranslate } from "../translations/server.js";
|
|
2
2
|
import { isEssentialAction } from "./registry.js";
|
|
3
|
+
import { pluginSlugsOf } from "../plugin/collectionSlugs.js";
|
|
4
|
+
import { findFormAtLocale, missingFormOnReadError } from "../form/findFormAtLocale.js";
|
|
3
5
|
import { resolveSubmissionLocale } from "../submissions/submissionLocale.js";
|
|
4
6
|
import { runActions } from "./runActions.js";
|
|
5
|
-
import { FORM_SUBMISSIONS_SLUG } from "../collections/formSubmissions.js";
|
|
6
|
-
import { findFormAtLocale, missingFormOnReadError } from "../form/findFormAtLocale.js";
|
|
7
7
|
//#region src/actions/task.ts
|
|
8
8
|
const ACTIONS_TASK_SLUG = "form-builder-actions";
|
|
9
9
|
const asActions = (value) => Array.isArray(value) ? value : [];
|
|
@@ -28,7 +28,7 @@ const asContext = (value) => {
|
|
|
28
28
|
const runActionsForSubmission = async (args) => {
|
|
29
29
|
const { input, registry, payload, req, richText } = args;
|
|
30
30
|
const submission = await payload.findByID({
|
|
31
|
-
collection:
|
|
31
|
+
collection: pluginSlugsOf(payload).formSubmissions,
|
|
32
32
|
id: input.submissionId,
|
|
33
33
|
depth: 0,
|
|
34
34
|
overrideAccess: true,
|
|
@@ -70,7 +70,7 @@ const runActionsForSubmission = async (args) => {
|
|
|
70
70
|
else payload.logger?.error(message);
|
|
71
71
|
}
|
|
72
72
|
if (subset !== "essential" && form.persistSubmissions === false) await payload.delete({
|
|
73
|
-
collection:
|
|
73
|
+
collection: pluginSlugsOf(payload).formSubmissions,
|
|
74
74
|
id: submission.id,
|
|
75
75
|
overrideAccess: true,
|
|
76
76
|
req
|
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
|
|
1
|
+
{"version":3,"file":"task.js","names":[],"sources":["../../src/actions/task.ts"],"sourcesContent":["import type { Config, Payload, PayloadRequest, TaskConfig } from 'payload'\nimport type { FormContextReference } from '../context/formContext'\nimport type { Translate } from '../fields/types'\nimport { findFormAtLocale, missingFormOnReadError } from '../form/findFormAtLocale'\nimport { pluginSlugsOf } from '../plugin/collectionSlugs'\nimport { resolveSubmissionLocale } from '../submissions/submissionLocale'\nimport type { SubmissionDescriptor, SubmissionValue } from '../submissions/types'\nimport { asFieldTranslate } from '../translations/server'\nimport type { RichTextBodyOption } from './body/serializeBody'\nimport { type ActionRegistry, isEssentialAction } from './registry'\nimport type { ActionInstance, ActionResult } from './runActions'\nimport { runActions } from './runActions'\nimport type { SubmissionForm } from './submissionContext'\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. */\n/** `subset` filters by the action definitions' `essential` flag; absent runs everything ('all'). */\nexport type ActionsTaskInput = {\n\tformId: number | string\n\tsubmissionId: number | string\n\tsubset?: 'essential' | 'rest'\n}\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/** The verified `{ relationTo, value }` stored on a submission, or null when it carried no context. */\nconst asContext = (value: unknown): FormContextReference | null => {\n\tif (value && typeof value === 'object') {\n\t\tconst { relationTo, value: reference } = value as Record<string, unknown>\n\t\tif (\n\t\t\ttypeof relationTo === 'string' &&\n\t\t\t(typeof reference === 'string' || typeof reference === 'number')\n\t\t) {\n\t\t\treturn { relationTo, value: reference }\n\t\t}\n\t}\n\treturn null\n}\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: pluginSlugsOf(payload).formSubmissions,\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. Re-clamped, since a host may have dropped that\n\t// locale since the submission was stored.\n\tconst locale = resolveSubmissionLocale(\n\t\ttypeof submission.locale === 'string' ? submission.locale : req?.locale,\n\t\tpayload.config.localization\n\t)\n\n\tconst form = await findFormAtLocale({\n\t\tpayload,\n\t\tid: input.formId,\n\t\tlocale,\n\t\treq,\n\t\toverrideAccess: true,\n\t}).catch(missingFormOnReadError)\n\tif (!form) {\n\t\treturn []\n\t}\n\n\tconst t: Translate = asFieldTranslate(req?.i18n?.t ?? ((key: string) => key))\n\n\tconst subset = input.subset ?? 'all'\n\tconst selected = asActions(form.actions).filter((instance) => {\n\t\tif (subset === 'all') {\n\t\t\treturn true\n\t\t}\n\t\tconst isEssential = isEssentialAction(registry, instance)\n\t\treturn subset === 'essential' ? isEssential : !isEssential\n\t})\n\n\tconst results = await runActions({\n\t\tactions: selected,\n\t\tregistry,\n\t\trichText,\n\t\t// The whole document, not just its identity, so a send-time hook reads a field off it (a\n\t\t// multi-tenant host's `tenant`) instead of loading the same form again. Double cast: a host's\n\t\t// generated Form interface has no index signature.\n\t\tform: form as unknown as SubmissionForm,\n\t\tsubmissionId: submission.id,\n\t\tvalues: asValues(submission.values),\n\t\tdescriptors: asDescriptors(submission.descriptors),\n\t\tcontext: asContext(submission.context),\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\tconst message = `@10x-media/form-builder: action \"${result.type}\" failed for submission ${String(submission.id)}: ${result.error ?? 'unknown error'}`\n\t\t\t// Pino's (mergeObject, message) form: an ActionError's structured detail lands as a\n\t\t\t// queryable log field instead of being concatenated into the message.\n\t\t\tif (result.detail !== undefined) {\n\t\t\t\tpayload.logger?.error({ detail: result.detail }, message)\n\t\t\t} else {\n\t\t\t\tpayload.logger?.error(message)\n\t\t\t}\n\t\t}\n\t}\n\t// A form can opt out of storing submissions (a pure signup that only POSTs to a provider): prune the\n\t// row after the whole action pass, regardless of individual action success (every action already got\n\t// the values). Best-effort: a delete failure is logged, never thrown. Uploads referenced in the values\n\t// are host-owned and not cascaded (documented).\n\t// Never on the essential pass: essential actions run first and their failure keeps the row (the\n\t// dispatcher then skips this completion entirely), so pruning belongs to the closing pass alone.\n\tif (subset !== 'essential' && form.persistSubmissions === false) {\n\t\tawait payload\n\t\t\t.delete({\n\t\t\t\tcollection: pluginSlugsOf(payload).formSubmissions,\n\t\t\t\tid: submission.id,\n\t\t\t\toverrideAccess: true,\n\t\t\t\treq,\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tpayload.logger?.error(\n\t\t\t\t\t`@10x-media/form-builder: failed to prune submission ${String(submission.id)}: ${\n\t\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t\t}`\n\t\t\t\t)\n\t\t\t})\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":"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"}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
import { pluginSlugsOf } from "../plugin/collectionSlugs.js";
|
|
1
2
|
import { isNamedField } from "../fields/fieldKey.js";
|
|
2
3
|
import { instanceOptionsOf } from "../fields/instanceOptions.js";
|
|
3
|
-
import { FORM_SUBMISSIONS_SLUG } from "../collections/formSubmissions.js";
|
|
4
4
|
import { aggregateRowsForFields } from "./aggregateRows.js";
|
|
5
|
-
import { FORMS_SLUG } from "../collections/forms.js";
|
|
6
5
|
//#region src/aggregation/aggregateResponses.ts
|
|
7
6
|
/** True when a field declares non-empty options (a choice field safe to aggregate publicly). */
|
|
8
7
|
const fieldHasOptions = (field) => instanceOptionsOf(field) !== void 0;
|
|
@@ -21,7 +20,7 @@ const metaFor = (field, resolved) => ({
|
|
|
21
20
|
const aggregateFormResponses = async (args) => {
|
|
22
21
|
const { payload, formId, fields, status = "complete", req, maxSubmissions = 1e4, pageSize = 500, resolvedOptions } = args;
|
|
23
22
|
const form = await payload.findByID({
|
|
24
|
-
collection:
|
|
23
|
+
collection: pluginSlugsOf(payload).forms,
|
|
25
24
|
id: formId,
|
|
26
25
|
depth: 0,
|
|
27
26
|
overrideAccess: true,
|
|
@@ -38,7 +37,7 @@ const aggregateFormResponses = async (args) => {
|
|
|
38
37
|
let truncated = false;
|
|
39
38
|
for (;;) {
|
|
40
39
|
const result = await payload.find({
|
|
41
|
-
collection:
|
|
40
|
+
collection: pluginSlugsOf(payload).formSubmissions,
|
|
42
41
|
where,
|
|
43
42
|
limit: pageSize,
|
|
44
43
|
page,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aggregateResponses.js","names":[],"sources":["../../src/aggregation/aggregateResponses.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport {
|
|
1
|
+
{"version":3,"file":"aggregateResponses.js","names":[],"sources":["../../src/aggregation/aggregateResponses.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { isNamedField, type NamedFormFieldInstance } from '../fields/fieldKey'\nimport { instanceOptionsOf } from '../fields/instanceOptions'\nimport { pluginSlugsOf } from '../plugin/collectionSlugs'\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({\n\t\t\tcollection: pluginSlugsOf(payload).forms,\n\t\t\tid: formId,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t})\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: pluginSlugsOf(payload).formSubmissions,\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":";;;;;;AA6BA,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;EACT,YAAY,cAAc,OAAO,EAAE;EACnC,IAAI;EACJ,OAAO;EACP,gBAAgB;EAChB;CACD,CAAC,EACA,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,cAAc,OAAO,EAAE;GACnC;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"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { findFormAtLocale, missingFormOnReadError } from "../form/findFormAtLocale.js";
|
|
2
2
|
import { resolveSubmissionLocale } from "../submissions/submissionLocale.js";
|
|
3
|
+
import { isPollClosed, pollConfigOf } from "../form/pollState.js";
|
|
3
4
|
import { resolveEffectivePollOptions } from "../poll/effectivePollOptions.js";
|
|
4
5
|
import { aggregateFormResponses, fieldHasOptions } from "./aggregateResponses.js";
|
|
5
6
|
import { aggregateFromVotes } from "../poll/votes/aggregateFromVotes.js";
|
|
6
7
|
import { resolvePollOutcome } from "../poll/resolvePollOutcome.js";
|
|
7
8
|
import { shouldAutoResolvePoll } from "../poll/closeJob.js";
|
|
8
|
-
import { findFormAtLocale, missingFormOnReadError } from "../form/findFormAtLocale.js";
|
|
9
9
|
//#region src/aggregation/resolveResultsRequest.ts
|
|
10
10
|
const forbidden = {
|
|
11
11
|
status: 403,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { keys } from "../translations/keys.js";
|
|
2
2
|
import { labelForKey } from "../translations/server.js";
|
|
3
|
-
import {
|
|
4
|
-
import { isLoggedIn } from "../plugin/access.js";
|
|
3
|
+
import { DEFAULT_COLLECTION_SLUGS, pluginSlugsOf } from "../plugin/collectionSlugs.js";
|
|
5
4
|
import { dispatchActions } from "../actions/dispatch.js";
|
|
6
5
|
import { resolveEventSink } from "../events/resolveEventSink.js";
|
|
6
|
+
import { pollConfigOf } from "../form/pollState.js";
|
|
7
|
+
import { isLoggedIn } from "../plugin/access.js";
|
|
7
8
|
import { formIdOf } from "../submissions/formIdOf.js";
|
|
8
9
|
import { makeVoteTallyHook } from "../poll/votes/voteTallyHook.js";
|
|
9
10
|
import { voteChangeTargetOf } from "../submissions/voteChange.js";
|
|
@@ -12,9 +13,7 @@ import { buildSpamGuard } from "../spam/spamGuard.js";
|
|
|
12
13
|
import { validateSubmission } from "../submissions/validateSubmission.js";
|
|
13
14
|
import { verifyContext } from "../submissions/verifyContext.js";
|
|
14
15
|
import { buildVoteSubmitEndpoint } from "../submissions/voteChangeEndpoint.js";
|
|
15
|
-
import { FORMS_SLUG } from "./forms.js";
|
|
16
16
|
//#region src/collections/formSubmissions.ts
|
|
17
|
-
const FORM_SUBMISSIONS_SLUG = "form-submissions";
|
|
18
17
|
/**
|
|
19
18
|
* On a completed submission create, dispatch the form's post-submit actions and emit `submission.created`.
|
|
20
19
|
* Both are side effects on an already-written row, so the whole body is wrapped: nothing it does can throw
|
|
@@ -30,7 +29,7 @@ const makeAfterChange = (args) => async ({ doc, operation, req }) => {
|
|
|
30
29
|
const formId = formIdOf(doc.form);
|
|
31
30
|
if (formId == null) return doc;
|
|
32
31
|
const form = await payload.findByID({
|
|
33
|
-
collection:
|
|
32
|
+
collection: pluginSlugsOf(req.payload).forms,
|
|
34
33
|
id: formId,
|
|
35
34
|
depth: 0,
|
|
36
35
|
overrideAccess: true,
|
|
@@ -38,7 +37,7 @@ const makeAfterChange = (args) => async ({ doc, operation, req }) => {
|
|
|
38
37
|
}).catch(() => null);
|
|
39
38
|
const stamp = async (data, withReq) => {
|
|
40
39
|
await payload.update.bind(payload)({
|
|
41
|
-
collection:
|
|
40
|
+
collection: pluginSlugsOf(req.payload).formSubmissions,
|
|
42
41
|
id: doc.id,
|
|
43
42
|
data,
|
|
44
43
|
depth: 0,
|
|
@@ -110,7 +109,7 @@ const makeVotedCookieHook = (args) => {
|
|
|
110
109
|
let state = isPollContextState(stashed) ? stashed : void 0;
|
|
111
110
|
if (state === void 0) {
|
|
112
111
|
const form = await req.payload.findByID({
|
|
113
|
-
collection:
|
|
112
|
+
collection: pluginSlugsOf(req.payload).forms,
|
|
114
113
|
id: formId,
|
|
115
114
|
depth: 0,
|
|
116
115
|
overrideAccess: true,
|
|
@@ -128,16 +127,16 @@ const makeVotedCookieHook = (args) => {
|
|
|
128
127
|
const authCookies = adminUserSlug ? collections[adminUserSlug]?.config.auth?.cookies : void 0;
|
|
129
128
|
const secure = state.allowChange && authCookies?.secure === true ? "; Secure" : "";
|
|
130
129
|
req.responseHeaders ??= new Headers();
|
|
131
|
-
req.responseHeaders.append("Set-Cookie", `${votedCookieName(formId)}=${value}; Path=/; Max-Age=${VOTED_COOKIE_MAX_AGE_SECONDS}; HttpOnly; SameSite=Lax${secure}`);
|
|
130
|
+
req.responseHeaders.append("Set-Cookie", `${votedCookieName(formId, req.payload)}=${value}; Path=/; Max-Age=${VOTED_COOKIE_MAX_AGE_SECONDS}; HttpOnly; SameSite=Lax${secure}`);
|
|
132
131
|
return doc;
|
|
133
132
|
};
|
|
134
133
|
};
|
|
135
|
-
const buildSubmissionsCollection = ({ registry, ruleRegistry, calcSources, calcFunctions, consentSources, consentSnapshot, actionRegistry = /* @__PURE__ */ new Map(), events, hasRunner = false, dispatchDeadlineMs, richText, uploadSlug, spam, votedCookie = false, pollSourceRegistry, pollVotes = false, showRawFields = false, overrides }) => {
|
|
134
|
+
const buildSubmissionsCollection = ({ registry, ruleRegistry, calcSources, calcFunctions, consentSources, consentSnapshot, actionRegistry = /* @__PURE__ */ new Map(), events, hasRunner = false, dispatchDeadlineMs, richText, uploadSlug, spam, votedCookie = false, pollSourceRegistry, pollVotes = false, showRawFields = false, slugs = DEFAULT_COLLECTION_SLUGS, overrides }) => {
|
|
136
135
|
const defaultFields = [
|
|
137
136
|
{
|
|
138
137
|
name: "form",
|
|
139
138
|
type: "relationship",
|
|
140
|
-
relationTo:
|
|
139
|
+
relationTo: slugs.forms,
|
|
141
140
|
required: true
|
|
142
141
|
},
|
|
143
142
|
{
|
|
@@ -234,7 +233,7 @@ const buildSubmissionsCollection = ({ registry, ruleRegistry, calcSources, calcF
|
|
|
234
233
|
];
|
|
235
234
|
return {
|
|
236
235
|
...overrides ?? {},
|
|
237
|
-
slug:
|
|
236
|
+
slug: slugs.formSubmissions,
|
|
238
237
|
labels: {
|
|
239
238
|
singular: labelForKey(keys.collectionSubmissionSingular),
|
|
240
239
|
plural: labelForKey(keys.collectionSubmissionPlural),
|
|
@@ -292,6 +291,6 @@ const buildSubmissionsCollection = ({ registry, ruleRegistry, calcSources, calcF
|
|
|
292
291
|
};
|
|
293
292
|
};
|
|
294
293
|
//#endregion
|
|
295
|
-
export {
|
|
294
|
+
export { buildSubmissionsCollection };
|
|
296
295
|
|
|
297
296
|
//# sourceMappingURL=formSubmissions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formSubmissions.js","names":[],"sources":["../../src/collections/formSubmissions.ts"],"sourcesContent":["import type { CollectionAfterChangeHook, CollectionConfig, Field, PayloadRequest } from 'payload'\nimport type { RichTextBodyOption } from '../actions/body/serializeBody'\nimport { dispatchActions } from '../actions/dispatch'\nimport type { ActionRegistry } from '../actions/registry'\nimport type { ActionInstance } from '../actions/runActions'\nimport type { CalcFunction, CalcSource } from '../calc/registry'\nimport type { ConsentSnapshotMode } from '../consent/captureConsent'\nimport type { ConsentSourcesResolver } from '../consent/types'\nimport { resolveEventSink } from '../events/resolveEventSink'\nimport type { FormEventSink } from '../events/types'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport { pollConfigOf } from '../form/pollState'\nimport { isLoggedIn } from '../plugin/access'\nimport type { CollectionOverrides } from '../plugin/collectionOverrides'\nimport type { PollOptionSourceRegistry } from '../poll/registry'\nimport { makeVoteTallyHook } from '../poll/votes/voteTallyHook'\nimport { buildSpamGuard } from '../spam/spamGuard'\nimport type { ResolvedSpamConfig } from '../spam/types'\nimport { formIdOf } from '../submissions/formIdOf'\nimport { validateSubmission } from '../submissions/validateSubmission'\nimport { verifyContext } from '../submissions/verifyContext'\nimport { buildVoteSubmitEndpoint } from '../submissions/voteChangeEndpoint'\nimport {\n\tPOLL_CONTEXT_KEY,\n\ttype PollContextState,\n\tsignVotedCookieValue,\n\tVOTED_COOKIE_MAX_AGE_SECONDS,\n\tvoteChangeTargetOf,\n\tvotedCookieName,\n} from '../submissions/votedCookie'\nimport { keys } from '../translations/keys'\nimport { labelForKey } from '../translations/server'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport { FORMS_SLUG } from './forms'\n\nexport const FORM_SUBMISSIONS_SLUG = 'form-submissions'\n\ntype BuildSubmissionsCollectionArgs = {\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\t/** Registered calc sources; submission validation re-resolves them fresh (a failure rejects the submission). */\n\tcalcSources?: Record<string, CalcSource>\n\t/** Registered calc functions; threaded into submit-time calc evaluation. */\n\tcalcFunctions?: Record<string, CalcFunction>\n\t/** The host's consent sources resolver (plugin option `consent.sources`); absent when no sources are configured. */\n\tconsentSources?: ConsentSourcesResolver\n\t/** What each consent proof snapshots of the agreed wording (plugin option `consent.snapshot`). */\n\tconsentSnapshot?: ConsentSnapshotMode\n\tactionRegistry?: ActionRegistry\n\tevents?: FormEventSink\n\t/** Whether a job runner is likely present; gates the queued vs bounded-inline dispatch path. */\n\thasRunner?: boolean\n\t/** Host override for the inline dispatch deadline (plugin option `dispatch.deadlineMs`). */\n\tdispatchDeadlineMs?: number\n\t/** Body serialization customization forwarded to the inline action dispatch path. */\n\trichText?: RichTextBodyOption\n\t/** The plugin-configured uploads collection slug; absent when uploads are disabled. */\n\tuploadSlug?: string\n\t/** Resolved spam config; when active, prepends the spam guard before validation. `false` disables it. */\n\tspam?: ResolvedSpamConfig | false\n\t/**\n\t * Opt-in (`poll.votedCookie`): set an httpOnly `fb-voted-{formId}` cookie on poll submission\n\t * creates. An `allowChange` poll sets its signed-id cookie regardless of this flag.\n\t */\n\tvotedCookie?: boolean\n\t/** Registered poll option sources; submission validation resolves allowed values through them. */\n\tpollSourceRegistry?: PollOptionSourceRegistry\n\t/** Resolved `poll.votes` option; `false` skips registering the append-only vote tally hook. */\n\tpollVotes?: false | { overrides?: CollectionOverrides }\n\t/**\n\t * When `true`, reveals the standalone `locale`, `values`, `descriptors`, `consent`, and `meta`\n\t * fields in the admin UI. Default `false`, because the `SubmissionAnswers` UI component already\n\t * represents them fully (locale and meta are folded into its Submission-details section).\n\t */\n\tshowRawFields?: boolean\n\toverrides?: CollectionOverrides\n}\n\n/**\n * On a completed submission create, dispatch the form's post-submit actions and emit `submission.created`.\n * Both are side effects on an already-written row, so the whole body is wrapped: nothing it does can throw\n * past the hook (a failed action or sink must never fail the submission write). Dispatch is itself bounded\n * and non-throwing; the inline fallback is awaited so the bound caps the added latency, while the queued\n * path returns immediately. The form's `actions` are null-guarded for legacy rows created before the field\n * existed.\n */\nconst makeAfterChange =\n\t(args: {\n\t\tactionRegistry: ActionRegistry\n\t\tevents?: FormEventSink\n\t\thasRunner: boolean\n\t\trichText?: RichTextBodyOption\n\t\tdispatchDeadlineMs?: number\n\t}): CollectionAfterChangeHook =>\n\tasync ({ doc, operation, req }) => {\n\t\tif (operation !== 'create' || (doc.status != null && doc.status !== 'complete')) {\n\t\t\treturn doc\n\t\t}\n\t\tconst { payload } = req\n\t\ttry {\n\t\t\tconst formId = formIdOf(doc.form)\n\t\t\tif (formId == null) {\n\t\t\t\treturn doc\n\t\t\t}\n\t\t\tconst form = await payload\n\t\t\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, overrideAccess: true, req })\n\t\t\t\t.catch(() => null)\n\n\t\t\t// Field access blocks every API writer, so this slug-agnostic override write is the only\n\t\t\t// path that can set the outcome stamps. `withReq` only while the create transaction is\n\t\t\t// still open; a late-settlement write happens after the response and passes none.\n\t\t\tconst stamp = async (\n\t\t\t\tdata: { actionFailed?: boolean; actionUncertain?: boolean },\n\t\t\t\twithReq?: PayloadRequest\n\t\t\t) => {\n\t\t\t\tconst update = payload.update.bind(payload) as unknown as (options: {\n\t\t\t\t\tcollection: string\n\t\t\t\t\tid: number | string\n\t\t\t\t\tdata: { actionFailed?: boolean; actionUncertain?: boolean }\n\t\t\t\t\tdepth?: number\n\t\t\t\t\toverrideAccess?: boolean\n\t\t\t\t\treq?: PayloadRequest\n\t\t\t\t}) => Promise<unknown>\n\t\t\t\tawait update({\n\t\t\t\t\tcollection: FORM_SUBMISSIONS_SLUG,\n\t\t\t\t\tid: doc.id as number | string,\n\t\t\t\t\tdata,\n\t\t\t\t\tdepth: 0,\n\t\t\t\t\toverrideAccess: true,\n\t\t\t\t\t...(withReq ? { req: withReq } : {}),\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tconst emitCreated = async () => {\n\t\t\t\ttry {\n\t\t\t\t\tawait resolveEventSink(args.events).emit({\n\t\t\t\t\t\ttype: 'submission.created',\n\t\t\t\t\t\tformId: String(formId),\n\t\t\t\t\t\tsubmissionId: String(doc.id),\n\t\t\t\t\t\tat: new Date().toISOString(),\n\t\t\t\t\t})\n\t\t\t\t} catch (error) {\n\t\t\t\t\tpayload.logger?.error(\n\t\t\t\t\t\t`@10x-media/form-builder: submission.created sink threw: ${\n\t\t\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t\t\t}`\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst { essentialFailed } = await dispatchActions({\n\t\t\t\tactions: (form?.actions ?? null) as ActionInstance[] | null,\n\t\t\t\tformId,\n\t\t\t\tsubmissionId: doc.id as number | string,\n\t\t\t\tregistry: args.actionRegistry,\n\t\t\t\tpayload,\n\t\t\t\treq,\n\t\t\t\thasRunner: args.hasRunner,\n\t\t\t\tpersistSubmissions: form?.persistSubmissions as boolean | undefined,\n\t\t\t\trichText: args.richText,\n\t\t\t\tdeadlineMs: args.dispatchDeadlineMs,\n\t\t\t\t// A definite refusal stamps `actionFailed` (the operator's replay filter); a deadline\n\t\t\t\t// breach stamps `actionUncertain` instead, because the work may still land and a\n\t\t\t\t// premature failure stamp would invite a duplicate replay.\n\t\t\t\tonEssentialFailed: ({ timedOut }) =>\n\t\t\t\t\tstamp(timedOut ? { actionUncertain: true } : { actionFailed: true }, req),\n\t\t\t\t// The real outcome of a breached pass, recorded durably once the work settles. The\n\t\t\t\t// dispatcher invokes this AFTER the late closing pass on success, so the stamp only\n\t\t\t\t// clears once the skipped work (rest actions, prune) is done and the created event\n\t\t\t\t// follows it, mirroring the on-time order; a `persistSubmissions: false` prune has\n\t\t\t\t// already deleted the row by then, so the clear tolerates a missing target. A late\n\t\t\t\t// failure converges to the definite-failure stamp.\n\t\t\t\tonEssentialSettled: async ({ ok }) => {\n\t\t\t\t\tif (ok) {\n\t\t\t\t\t\tawait stamp({ actionUncertain: false }).catch(() => {})\n\t\t\t\t\t\tawait emitCreated()\n\t\t\t\t\t} else {\n\t\t\t\t\t\tawait stamp({ actionFailed: true, actionUncertain: false })\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t})\n\t\t\t// A failed-essential submission is kept for recovery but is not a completed signup: no\n\t\t\t// created event, so a sink-driven automation never treats it as one.\n\t\t\tif (essentialFailed) {\n\t\t\t\treturn doc\n\t\t\t}\n\n\t\t\tawait emitCreated()\n\t\t} catch (error) {\n\t\t\tpayload.logger?.error(\n\t\t\t\t`@10x-media/form-builder: afterChange dispatch failed for submission ${String(doc.id)}: ${\n\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t}`\n\t\t\t)\n\t\t}\n\t\treturn doc\n\t}\n\nconst isPollContextState = (value: unknown): value is PollContextState =>\n\tvalue != null &&\n\ttypeof value === 'object' &&\n\ttypeof (value as PollContextState).pollEnabled === 'boolean'\n\n/**\n * Voted-cookie hook: after a submission to a poll-enabled form is created (or updated through the\n * vote-change path), appends an httpOnly `fb-voted-{formId}` cookie via `req.responseHeaders`,\n * Payload's supported channel for hook-set response headers (its own auth strategies use it;\n * `handleEndpoints` merges it into every REST response). The value stays the legacy `1` marker\n * under the `poll.votedCookie` plugin option; an `allowChange` poll instead carries the signed\n * submission id (set regardless of the option, because re-vote identification depends on it) and\n * refreshes it on every change. Reads the poll state `validateSubmission` stashed on\n * `req.context`, falling back to a form fetch when absent. httpOnly keeps the marker readable\n * only server-side (`hasVotedCookie`/`resolveVotedSubmission`); the client `<Poll>` keeps its\n * localStorage guard.\n */\nconst makeVotedCookieHook = (args: { votedCookie: boolean }): CollectionAfterChangeHook => {\n\treturn async ({ doc, operation, req }) => {\n\t\tconst changing = operation === 'update' && voteChangeTargetOf(req) != null\n\t\tif (operation !== 'create' && !changing) {\n\t\t\treturn doc\n\t\t}\n\t\tconst formId = formIdOf(doc.form)\n\t\tif (formId == null) {\n\t\t\treturn doc\n\t\t}\n\t\tconst stashed = req.context?.[POLL_CONTEXT_KEY]\n\t\tlet state = isPollContextState(stashed) ? stashed : undefined\n\t\tif (state === undefined) {\n\t\t\tconst form = await req.payload\n\t\t\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, overrideAccess: true, req })\n\t\t\t\t.catch(() => null)\n\t\t\tstate = {\n\t\t\t\tpollEnabled: form?.pollEnabled === true,\n\t\t\t\tallowChange: pollConfigOf(form?.poll)?.allowChange === true,\n\t\t\t}\n\t\t}\n\t\tif (!state.pollEnabled || !(args.votedCookie || state.allowChange)) {\n\t\t\treturn doc\n\t\t}\n\t\tconst value = state.allowChange\n\t\t\t? signVotedCookieValue(req.payload, doc.id as number | string)\n\t\t\t: '1'\n\t\t// A signed id is a bearer capability (it authorizes changing that vote), so it follows the\n\t\t// host's Payload auth-cookie transport policy: `Secure` whenever the admin user collection's\n\t\t// `auth.cookies.secure` is on. The legacy `1` marker stays a plain UX flag either way.\n\t\tconst adminUserSlug = req.payload.config.admin?.user\n\t\t// String-indexed cast: a host's generated types key `collections` to its own slugs, which\n\t\t// this framework-level read cannot know.\n\t\tconst collections = req.payload.collections as Record<\n\t\t\tstring,\n\t\t\t{ config: { auth?: { cookies?: { secure?: boolean } } } } | undefined\n\t\t>\n\t\tconst authCookies = adminUserSlug ? collections[adminUserSlug]?.config.auth?.cookies : undefined\n\t\tconst secure = state.allowChange && authCookies?.secure === true ? '; Secure' : ''\n\t\treq.responseHeaders ??= new Headers()\n\t\treq.responseHeaders.append(\n\t\t\t'Set-Cookie',\n\t\t\t`${votedCookieName(formId)}=${value}; Path=/; Max-Age=${VOTED_COOKIE_MAX_AGE_SECONDS}; HttpOnly; SameSite=Lax${secure}`\n\t\t)\n\t\treturn doc\n\t}\n}\n\nexport const buildSubmissionsCollection = ({\n\tregistry,\n\truleRegistry,\n\tcalcSources,\n\tcalcFunctions,\n\tconsentSources,\n\tconsentSnapshot,\n\tactionRegistry = new Map(),\n\tevents,\n\thasRunner = false,\n\tdispatchDeadlineMs,\n\trichText,\n\tuploadSlug,\n\tspam,\n\tvotedCookie = false,\n\tpollSourceRegistry,\n\tpollVotes = false,\n\tshowRawFields = false,\n\toverrides,\n}: BuildSubmissionsCollectionArgs): CollectionConfig => {\n\tconst defaultFields: Field[] = [\n\t\t{ name: 'form', type: 'relationship', relationTo: FORMS_SLUG, required: true },\n\t\t{\n\t\t\tname: 'status',\n\t\t\ttype: 'select',\n\t\t\tdefaultValue: 'complete',\n\t\t\toptions: [\n\t\t\t\t{ label: labelForKey(keys.statusComplete), value: 'complete' },\n\t\t\t\t{ label: labelForKey(keys.statusPartial), value: 'partial' },\n\t\t\t],\n\t\t\t// Not clearable: aggregation counts `status: complete` submissions, so a cleared status\n\t\t\t// would drop the submission out of every poll result with nothing in the UI saying so.\n\t\t\tadmin: { isClearable: false },\n\t\t\t// Defense-in-depth at the REST layer: anonymous clients cannot set status via the API.\n\t\t\t// The validateSubmission hook also forces 'complete' server-side, so this covers both paths.\n\t\t\taccess: { create: isLoggedIn, update: isLoggedIn },\n\t\t},\n\t\t// Stamped by the dispatcher when an essential action failed and the row was kept: the flag is\n\t\t// what lets an operator filter these ownerless rows and clear them once the provider has been\n\t\t// fixed and the addresses replayed. Hidden while unset; never client-writable.\n\t\t{\n\t\t\tname: 'actionFailed',\n\t\t\ttype: 'checkbox',\n\t\t\tindex: true,\n\t\t\tlabel: labelForKey(keys.submissionActionFailedFlag),\n\t\t\taccess: { create: () => false, update: () => false },\n\t\t\tadmin: { readOnly: true, condition: (data) => data?.actionFailed === true },\n\t\t},\n\t\t// Stamped instead of `actionFailed` when the essential pass outlived its deadline: the work\n\t\t// may still complete. Cleared once the breached work settles successfully (the row leaves\n\t\t// both filters), or upgraded to `actionFailed` when it settles as a definite failure.\n\t\t{\n\t\t\tname: 'actionUncertain',\n\t\t\ttype: 'checkbox',\n\t\t\tindex: true,\n\t\t\tlabel: labelForKey(keys.submissionActionUncertainFlag),\n\t\t\taccess: { create: () => false, update: () => false },\n\t\t\tadmin: { readOnly: true, condition: (data) => data?.actionUncertain === true },\n\t\t},\n\t\t// answers UI appears first so it is the dominant view when opening a submission document.\n\t\t{\n\t\t\tname: 'answers',\n\t\t\ttype: 'ui',\n\t\t\tadmin: {\n\t\t\t\tcomponents: { Field: '@10x-media/form-builder/rsc#SubmissionAnswers' },\n\t\t\t},\n\t\t},\n\t\t{ name: 'locale', type: 'text', admin: { hidden: !showRawFields } },\n\t\t{ name: 'values', type: 'json', admin: { hidden: !showRawFields } },\n\t\t{ name: 'descriptors', type: 'json', admin: { hidden: !showRawFields } },\n\t\t{ name: 'consent', type: 'json', admin: { hidden: !showRawFields } },\n\t\t{ name: 'meta', type: 'json', admin: { hidden: !showRawFields } },\n\t\t// The verified reference to the document this form was rendered for (see verifyContext). Only forms\n\t\t// rendered with a signed context carry one, so the group is shown only when present. Readable, so it\n\t\t// is a legible audit record of which document a submission came through.\n\t\t{\n\t\t\tname: 'context',\n\t\t\ttype: 'group',\n\t\t\tlabel: labelForKey(keys.submissionContext),\n\t\t\t// A Payload group always materializes as an object, so gate on a populated `relationTo`\n\t\t\t// (not the group itself) to show it only for submissions made through a signed context.\n\t\t\tadmin: { readOnly: true, condition: (data) => Boolean(data?.context?.relationTo) },\n\t\t\tfields: [\n\t\t\t\t{ name: 'relationTo', type: 'text' },\n\t\t\t\t{ name: 'value', type: 'text' },\n\t\t\t],\n\t\t},\n\t]\n\n\treturn {\n\t\t...(overrides ?? {}),\n\t\tslug: FORM_SUBMISSIONS_SLUG,\n\t\tlabels: {\n\t\t\tsingular: labelForKey(keys.collectionSubmissionSingular),\n\t\t\tplural: labelForKey(keys.collectionSubmissionPlural),\n\t\t\t...(overrides?.labels ?? {}),\n\t\t},\n\t\tadmin: {\n\t\t\tgroup: 'Forms',\n\t\t\tdefaultColumns: ['form', 'status', 'locale', 'createdAt'],\n\t\t\t...(overrides?.admin ?? {}),\n\t\t},\n\t\taccess: {\n\t\t\tcreate: () => true,\n\t\t\tread: isLoggedIn,\n\t\t\tupdate: () => false,\n\t\t\t...(overrides?.access ?? {}),\n\t\t},\n\t\thooks: {\n\t\t\t...(overrides?.hooks ?? {}),\n\t\t\t// Spam guard + validateSubmission must remain first: they enforce the security invariant\n\t\t\t// that anonymous callers cannot bypass post-submit actions or supply a forged status.\n\t\t\t// Consumer beforeValidate hooks are appended after so they run on already-validated data.\n\t\t\tbeforeValidate: [\n\t\t\t\t...(spam ? [buildSpamGuard(spam)] : []),\n\t\t\t\t// Verify + store the signed form context before validation stores the answers, and independently\n\t\t\t\t// of spam so it runs even with spam off.\n\t\t\t\tverifyContext(),\n\t\t\t\tvalidateSubmission({\n\t\t\t\t\tregistry,\n\t\t\t\t\truleRegistry,\n\t\t\t\t\tcalcSources,\n\t\t\t\t\tcalcFunctions,\n\t\t\t\t\tconsentSources,\n\t\t\t\t\tconsentSnapshot,\n\t\t\t\t\tuploadSlug,\n\t\t\t\t\tpollSourceRegistry,\n\t\t\t\t\tstrictUploadOwnership: spam ? spam.uploadOwnership === 'strict' : false,\n\t\t\t\t}),\n\t\t\t\t...(overrides?.hooks?.beforeValidate ?? []),\n\t\t\t],\n\t\t\tafterChange: [\n\t\t\t\t// The tally hook runs first and does not swallow errors: a bump failure must throw so\n\t\t\t\t// Payload rolls the submission create/update back inside the operation transaction,\n\t\t\t\t// rather than being absorbed by the dispatch hook's swallow-all error boundary below.\n\t\t\t\t...(pollVotes !== false ? [makeVoteTallyHook()] : []),\n\t\t\t\tmakeAfterChange({ actionRegistry, events, hasRunner, richText, dispatchDeadlineMs }),\n\t\t\t\t// Always registered: it self-gates on the option and on the form's allowChange flag.\n\t\t\t\tmakeVotedCookieHook({ votedCookie }),\n\t\t\t\t...(overrides?.hooks?.afterChange ?? []),\n\t\t\t],\n\t\t},\n\t\t// The vote-submit endpoint must stay first so it shadows the stock REST create (custom\n\t\t// endpoints match before built-ins); `endpoints: false` from a host disables ours too.\n\t\tendpoints:\n\t\t\toverrides?.endpoints === false\n\t\t\t\t? false\n\t\t\t\t: [buildVoteSubmitEndpoint(), ...(overrides?.endpoints ?? [])],\n\t\tfields: overrides?.fields ? overrides.fields({ defaultFields }) : defaultFields,\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"formSubmissions.js","names":[],"sources":["../../src/collections/formSubmissions.ts"],"sourcesContent":["import type { CollectionAfterChangeHook, CollectionConfig, Field, PayloadRequest } from 'payload'\nimport type { RichTextBodyOption } from '../actions/body/serializeBody'\nimport { dispatchActions } from '../actions/dispatch'\nimport type { ActionRegistry } from '../actions/registry'\nimport type { ActionInstance } from '../actions/runActions'\nimport type { CalcFunction, CalcSource } from '../calc/registry'\nimport type { ConsentSnapshotMode } from '../consent/captureConsent'\nimport type { ConsentSourcesResolver } from '../consent/types'\nimport { resolveEventSink } from '../events/resolveEventSink'\nimport type { FormEventSink } from '../events/types'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport { pollConfigOf } from '../form/pollState'\nimport { isLoggedIn } from '../plugin/access'\nimport type { CollectionOverrides } from '../plugin/collectionOverrides'\nimport {\n\tDEFAULT_COLLECTION_SLUGS,\n\ttype FormBuilderCollectionSlugs,\n\tpluginSlugsOf,\n} from '../plugin/collectionSlugs'\nimport type { PollOptionSourceRegistry } from '../poll/registry'\nimport { makeVoteTallyHook } from '../poll/votes/voteTallyHook'\nimport { buildSpamGuard } from '../spam/spamGuard'\nimport type { ResolvedSpamConfig } from '../spam/types'\nimport { formIdOf } from '../submissions/formIdOf'\nimport { validateSubmission } from '../submissions/validateSubmission'\nimport { verifyContext } from '../submissions/verifyContext'\nimport { buildVoteSubmitEndpoint } from '../submissions/voteChangeEndpoint'\nimport {\n\tPOLL_CONTEXT_KEY,\n\ttype PollContextState,\n\tsignVotedCookieValue,\n\tVOTED_COOKIE_MAX_AGE_SECONDS,\n\tvoteChangeTargetOf,\n\tvotedCookieName,\n} from '../submissions/votedCookie'\nimport { keys } from '../translations/keys'\nimport { labelForKey } from '../translations/server'\nimport type { ValidationRuleRegistry } from '../validation/registry'\n\ntype BuildSubmissionsCollectionArgs = {\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\t/** Registered calc sources; submission validation re-resolves them fresh (a failure rejects the submission). */\n\tcalcSources?: Record<string, CalcSource>\n\t/** Registered calc functions; threaded into submit-time calc evaluation. */\n\tcalcFunctions?: Record<string, CalcFunction>\n\t/** The host's consent sources resolver (plugin option `consent.sources`); absent when no sources are configured. */\n\tconsentSources?: ConsentSourcesResolver\n\t/** What each consent proof snapshots of the agreed wording (plugin option `consent.snapshot`). */\n\tconsentSnapshot?: ConsentSnapshotMode\n\tactionRegistry?: ActionRegistry\n\tevents?: FormEventSink\n\t/** Whether a job runner is likely present; gates the queued vs bounded-inline dispatch path. */\n\thasRunner?: boolean\n\t/** Host override for the inline dispatch deadline (plugin option `dispatch.deadlineMs`). */\n\tdispatchDeadlineMs?: number\n\t/** Body serialization customization forwarded to the inline action dispatch path. */\n\trichText?: RichTextBodyOption\n\t/** The plugin-configured uploads collection slug; absent when uploads are disabled. */\n\tuploadSlug?: string\n\t/** Resolved spam config; when active, prepends the spam guard before validation. `false` disables it. */\n\tspam?: ResolvedSpamConfig | false\n\t/**\n\t * Opt-in (`poll.votedCookie`): set an httpOnly `fb-voted-{formId}` cookie on poll submission\n\t * creates. An `allowChange` poll sets its signed-id cookie regardless of this flag.\n\t */\n\tvotedCookie?: boolean\n\t/** Registered poll option sources; submission validation resolves allowed values through them. */\n\tpollSourceRegistry?: PollOptionSourceRegistry\n\t/** Resolved `poll.votes` option; `false` skips registering the append-only vote tally hook. */\n\tpollVotes?: false | { overrides?: CollectionOverrides }\n\t/**\n\t * When `true`, reveals the standalone `locale`, `values`, `descriptors`, `consent`, and `meta`\n\t * fields in the admin UI. Default `false`, because the `SubmissionAnswers` UI component already\n\t * represents them fully (locale and meta are folded into its Submission-details section).\n\t */\n\tshowRawFields?: boolean\n\t/** Resolved plugin collection slugs (after host `overrides.*.slug`); defaults when omitted. */\n\tslugs?: FormBuilderCollectionSlugs\n\toverrides?: CollectionOverrides\n}\n\n/**\n * On a completed submission create, dispatch the form's post-submit actions and emit `submission.created`.\n * Both are side effects on an already-written row, so the whole body is wrapped: nothing it does can throw\n * past the hook (a failed action or sink must never fail the submission write). Dispatch is itself bounded\n * and non-throwing; the inline fallback is awaited so the bound caps the added latency, while the queued\n * path returns immediately. The form's `actions` are null-guarded for legacy rows created before the field\n * existed.\n */\nconst makeAfterChange =\n\t(args: {\n\t\tactionRegistry: ActionRegistry\n\t\tevents?: FormEventSink\n\t\thasRunner: boolean\n\t\trichText?: RichTextBodyOption\n\t\tdispatchDeadlineMs?: number\n\t}): CollectionAfterChangeHook =>\n\tasync ({ doc, operation, req }) => {\n\t\tif (operation !== 'create' || (doc.status != null && doc.status !== 'complete')) {\n\t\t\treturn doc\n\t\t}\n\t\tconst { payload } = req\n\t\ttry {\n\t\t\tconst formId = formIdOf(doc.form)\n\t\t\tif (formId == null) {\n\t\t\t\treturn doc\n\t\t\t}\n\t\t\tconst form = await payload\n\t\t\t\t.findByID({\n\t\t\t\t\tcollection: pluginSlugsOf(req.payload).forms,\n\t\t\t\t\tid: formId,\n\t\t\t\t\tdepth: 0,\n\t\t\t\t\toverrideAccess: true,\n\t\t\t\t\treq,\n\t\t\t\t})\n\t\t\t\t.catch(() => null)\n\n\t\t\t// Field access blocks every API writer, so this slug-agnostic override write is the only\n\t\t\t// path that can set the outcome stamps. `withReq` only while the create transaction is\n\t\t\t// still open; a late-settlement write happens after the response and passes none.\n\t\t\tconst stamp = async (\n\t\t\t\tdata: { actionFailed?: boolean; actionUncertain?: boolean },\n\t\t\t\twithReq?: PayloadRequest\n\t\t\t) => {\n\t\t\t\tconst update = payload.update.bind(payload) as unknown as (options: {\n\t\t\t\t\tcollection: string\n\t\t\t\t\tid: number | string\n\t\t\t\t\tdata: { actionFailed?: boolean; actionUncertain?: boolean }\n\t\t\t\t\tdepth?: number\n\t\t\t\t\toverrideAccess?: boolean\n\t\t\t\t\treq?: PayloadRequest\n\t\t\t\t}) => Promise<unknown>\n\t\t\t\tawait update({\n\t\t\t\t\tcollection: pluginSlugsOf(req.payload).formSubmissions,\n\t\t\t\t\tid: doc.id as number | string,\n\t\t\t\t\tdata,\n\t\t\t\t\tdepth: 0,\n\t\t\t\t\toverrideAccess: true,\n\t\t\t\t\t...(withReq ? { req: withReq } : {}),\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tconst emitCreated = async () => {\n\t\t\t\ttry {\n\t\t\t\t\tawait resolveEventSink(args.events).emit({\n\t\t\t\t\t\ttype: 'submission.created',\n\t\t\t\t\t\tformId: String(formId),\n\t\t\t\t\t\tsubmissionId: String(doc.id),\n\t\t\t\t\t\tat: new Date().toISOString(),\n\t\t\t\t\t})\n\t\t\t\t} catch (error) {\n\t\t\t\t\tpayload.logger?.error(\n\t\t\t\t\t\t`@10x-media/form-builder: submission.created sink threw: ${\n\t\t\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t\t\t}`\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst { essentialFailed } = await dispatchActions({\n\t\t\t\tactions: (form?.actions ?? null) as ActionInstance[] | null,\n\t\t\t\tformId,\n\t\t\t\tsubmissionId: doc.id as number | string,\n\t\t\t\tregistry: args.actionRegistry,\n\t\t\t\tpayload,\n\t\t\t\treq,\n\t\t\t\thasRunner: args.hasRunner,\n\t\t\t\tpersistSubmissions: form?.persistSubmissions as boolean | undefined,\n\t\t\t\trichText: args.richText,\n\t\t\t\tdeadlineMs: args.dispatchDeadlineMs,\n\t\t\t\t// A definite refusal stamps `actionFailed` (the operator's replay filter); a deadline\n\t\t\t\t// breach stamps `actionUncertain` instead, because the work may still land and a\n\t\t\t\t// premature failure stamp would invite a duplicate replay.\n\t\t\t\tonEssentialFailed: ({ timedOut }) =>\n\t\t\t\t\tstamp(timedOut ? { actionUncertain: true } : { actionFailed: true }, req),\n\t\t\t\t// The real outcome of a breached pass, recorded durably once the work settles. The\n\t\t\t\t// dispatcher invokes this AFTER the late closing pass on success, so the stamp only\n\t\t\t\t// clears once the skipped work (rest actions, prune) is done and the created event\n\t\t\t\t// follows it, mirroring the on-time order; a `persistSubmissions: false` prune has\n\t\t\t\t// already deleted the row by then, so the clear tolerates a missing target. A late\n\t\t\t\t// failure converges to the definite-failure stamp.\n\t\t\t\tonEssentialSettled: async ({ ok }) => {\n\t\t\t\t\tif (ok) {\n\t\t\t\t\t\tawait stamp({ actionUncertain: false }).catch(() => {})\n\t\t\t\t\t\tawait emitCreated()\n\t\t\t\t\t} else {\n\t\t\t\t\t\tawait stamp({ actionFailed: true, actionUncertain: false })\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t})\n\t\t\t// A failed-essential submission is kept for recovery but is not a completed signup: no\n\t\t\t// created event, so a sink-driven automation never treats it as one.\n\t\t\tif (essentialFailed) {\n\t\t\t\treturn doc\n\t\t\t}\n\n\t\t\tawait emitCreated()\n\t\t} catch (error) {\n\t\t\tpayload.logger?.error(\n\t\t\t\t`@10x-media/form-builder: afterChange dispatch failed for submission ${String(doc.id)}: ${\n\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t}`\n\t\t\t)\n\t\t}\n\t\treturn doc\n\t}\n\nconst isPollContextState = (value: unknown): value is PollContextState =>\n\tvalue != null &&\n\ttypeof value === 'object' &&\n\ttypeof (value as PollContextState).pollEnabled === 'boolean'\n\n/**\n * Voted-cookie hook: after a submission to a poll-enabled form is created (or updated through the\n * vote-change path), appends an httpOnly `fb-voted-{formId}` cookie via `req.responseHeaders`,\n * Payload's supported channel for hook-set response headers (its own auth strategies use it;\n * `handleEndpoints` merges it into every REST response). The value stays the legacy `1` marker\n * under the `poll.votedCookie` plugin option; an `allowChange` poll instead carries the signed\n * submission id (set regardless of the option, because re-vote identification depends on it) and\n * refreshes it on every change. Reads the poll state `validateSubmission` stashed on\n * `req.context`, falling back to a form fetch when absent. httpOnly keeps the marker readable\n * only server-side (`hasVotedCookie`/`resolveVotedSubmission`); the client `<Poll>` keeps its\n * localStorage guard.\n */\nconst makeVotedCookieHook = (args: { votedCookie: boolean }): CollectionAfterChangeHook => {\n\treturn async ({ doc, operation, req }) => {\n\t\tconst changing = operation === 'update' && voteChangeTargetOf(req) != null\n\t\tif (operation !== 'create' && !changing) {\n\t\t\treturn doc\n\t\t}\n\t\tconst formId = formIdOf(doc.form)\n\t\tif (formId == null) {\n\t\t\treturn doc\n\t\t}\n\t\tconst stashed = req.context?.[POLL_CONTEXT_KEY]\n\t\tlet state = isPollContextState(stashed) ? stashed : undefined\n\t\tif (state === undefined) {\n\t\t\tconst form = await req.payload\n\t\t\t\t.findByID({\n\t\t\t\t\tcollection: pluginSlugsOf(req.payload).forms,\n\t\t\t\t\tid: formId,\n\t\t\t\t\tdepth: 0,\n\t\t\t\t\toverrideAccess: true,\n\t\t\t\t\treq,\n\t\t\t\t})\n\t\t\t\t.catch(() => null)\n\t\t\tstate = {\n\t\t\t\tpollEnabled: form?.pollEnabled === true,\n\t\t\t\tallowChange: pollConfigOf(form?.poll)?.allowChange === true,\n\t\t\t}\n\t\t}\n\t\tif (!state.pollEnabled || !(args.votedCookie || state.allowChange)) {\n\t\t\treturn doc\n\t\t}\n\t\tconst value = state.allowChange\n\t\t\t? signVotedCookieValue(req.payload, doc.id as number | string)\n\t\t\t: '1'\n\t\t// A signed id is a bearer capability (it authorizes changing that vote), so it follows the\n\t\t// host's Payload auth-cookie transport policy: `Secure` whenever the admin user collection's\n\t\t// `auth.cookies.secure` is on. The legacy `1` marker stays a plain UX flag either way.\n\t\tconst adminUserSlug = req.payload.config.admin?.user\n\t\t// String-indexed cast: a host's generated types key `collections` to its own slugs, which\n\t\t// this framework-level read cannot know.\n\t\tconst collections = req.payload.collections as Record<\n\t\t\tstring,\n\t\t\t{ config: { auth?: { cookies?: { secure?: boolean } } } } | undefined\n\t\t>\n\t\tconst authCookies = adminUserSlug ? collections[adminUserSlug]?.config.auth?.cookies : undefined\n\t\tconst secure = state.allowChange && authCookies?.secure === true ? '; Secure' : ''\n\t\treq.responseHeaders ??= new Headers()\n\t\treq.responseHeaders.append(\n\t\t\t'Set-Cookie',\n\t\t\t`${votedCookieName(formId, req.payload)}=${value}; Path=/; Max-Age=${VOTED_COOKIE_MAX_AGE_SECONDS}; HttpOnly; SameSite=Lax${secure}`\n\t\t)\n\t\treturn doc\n\t}\n}\n\nexport const buildSubmissionsCollection = ({\n\tregistry,\n\truleRegistry,\n\tcalcSources,\n\tcalcFunctions,\n\tconsentSources,\n\tconsentSnapshot,\n\tactionRegistry = new Map(),\n\tevents,\n\thasRunner = false,\n\tdispatchDeadlineMs,\n\trichText,\n\tuploadSlug,\n\tspam,\n\tvotedCookie = false,\n\tpollSourceRegistry,\n\tpollVotes = false,\n\tshowRawFields = false,\n\tslugs = DEFAULT_COLLECTION_SLUGS,\n\toverrides,\n}: BuildSubmissionsCollectionArgs): CollectionConfig => {\n\tconst defaultFields: Field[] = [\n\t\t{ name: 'form', type: 'relationship', relationTo: slugs.forms, required: true },\n\t\t{\n\t\t\tname: 'status',\n\t\t\ttype: 'select',\n\t\t\tdefaultValue: 'complete',\n\t\t\toptions: [\n\t\t\t\t{ label: labelForKey(keys.statusComplete), value: 'complete' },\n\t\t\t\t{ label: labelForKey(keys.statusPartial), value: 'partial' },\n\t\t\t],\n\t\t\t// Not clearable: aggregation counts `status: complete` submissions, so a cleared status\n\t\t\t// would drop the submission out of every poll result with nothing in the UI saying so.\n\t\t\tadmin: { isClearable: false },\n\t\t\t// Defense-in-depth at the REST layer: anonymous clients cannot set status via the API.\n\t\t\t// The validateSubmission hook also forces 'complete' server-side, so this covers both paths.\n\t\t\taccess: { create: isLoggedIn, update: isLoggedIn },\n\t\t},\n\t\t// Stamped by the dispatcher when an essential action failed and the row was kept: the flag is\n\t\t// what lets an operator filter these ownerless rows and clear them once the provider has been\n\t\t// fixed and the addresses replayed. Hidden while unset; never client-writable.\n\t\t{\n\t\t\tname: 'actionFailed',\n\t\t\ttype: 'checkbox',\n\t\t\tindex: true,\n\t\t\tlabel: labelForKey(keys.submissionActionFailedFlag),\n\t\t\taccess: { create: () => false, update: () => false },\n\t\t\tadmin: { readOnly: true, condition: (data) => data?.actionFailed === true },\n\t\t},\n\t\t// Stamped instead of `actionFailed` when the essential pass outlived its deadline: the work\n\t\t// may still complete. Cleared once the breached work settles successfully (the row leaves\n\t\t// both filters), or upgraded to `actionFailed` when it settles as a definite failure.\n\t\t{\n\t\t\tname: 'actionUncertain',\n\t\t\ttype: 'checkbox',\n\t\t\tindex: true,\n\t\t\tlabel: labelForKey(keys.submissionActionUncertainFlag),\n\t\t\taccess: { create: () => false, update: () => false },\n\t\t\tadmin: { readOnly: true, condition: (data) => data?.actionUncertain === true },\n\t\t},\n\t\t// answers UI appears first so it is the dominant view when opening a submission document.\n\t\t{\n\t\t\tname: 'answers',\n\t\t\ttype: 'ui',\n\t\t\tadmin: {\n\t\t\t\tcomponents: { Field: '@10x-media/form-builder/rsc#SubmissionAnswers' },\n\t\t\t},\n\t\t},\n\t\t{ name: 'locale', type: 'text', admin: { hidden: !showRawFields } },\n\t\t{ name: 'values', type: 'json', admin: { hidden: !showRawFields } },\n\t\t{ name: 'descriptors', type: 'json', admin: { hidden: !showRawFields } },\n\t\t{ name: 'consent', type: 'json', admin: { hidden: !showRawFields } },\n\t\t{ name: 'meta', type: 'json', admin: { hidden: !showRawFields } },\n\t\t// The verified reference to the document this form was rendered for (see verifyContext). Only forms\n\t\t// rendered with a signed context carry one, so the group is shown only when present. Readable, so it\n\t\t// is a legible audit record of which document a submission came through.\n\t\t{\n\t\t\tname: 'context',\n\t\t\ttype: 'group',\n\t\t\tlabel: labelForKey(keys.submissionContext),\n\t\t\t// A Payload group always materializes as an object, so gate on a populated `relationTo`\n\t\t\t// (not the group itself) to show it only for submissions made through a signed context.\n\t\t\tadmin: { readOnly: true, condition: (data) => Boolean(data?.context?.relationTo) },\n\t\t\tfields: [\n\t\t\t\t{ name: 'relationTo', type: 'text' },\n\t\t\t\t{ name: 'value', type: 'text' },\n\t\t\t],\n\t\t},\n\t]\n\n\treturn {\n\t\t...(overrides ?? {}),\n\t\tslug: slugs.formSubmissions,\n\t\tlabels: {\n\t\t\tsingular: labelForKey(keys.collectionSubmissionSingular),\n\t\t\tplural: labelForKey(keys.collectionSubmissionPlural),\n\t\t\t...(overrides?.labels ?? {}),\n\t\t},\n\t\tadmin: {\n\t\t\tgroup: 'Forms',\n\t\t\tdefaultColumns: ['form', 'status', 'locale', 'createdAt'],\n\t\t\t...(overrides?.admin ?? {}),\n\t\t},\n\t\taccess: {\n\t\t\tcreate: () => true,\n\t\t\tread: isLoggedIn,\n\t\t\tupdate: () => false,\n\t\t\t...(overrides?.access ?? {}),\n\t\t},\n\t\thooks: {\n\t\t\t...(overrides?.hooks ?? {}),\n\t\t\t// Spam guard + validateSubmission must remain first: they enforce the security invariant\n\t\t\t// that anonymous callers cannot bypass post-submit actions or supply a forged status.\n\t\t\t// Consumer beforeValidate hooks are appended after so they run on already-validated data.\n\t\t\tbeforeValidate: [\n\t\t\t\t...(spam ? [buildSpamGuard(spam)] : []),\n\t\t\t\t// Verify + store the signed form context before validation stores the answers, and independently\n\t\t\t\t// of spam so it runs even with spam off.\n\t\t\t\tverifyContext(),\n\t\t\t\tvalidateSubmission({\n\t\t\t\t\tregistry,\n\t\t\t\t\truleRegistry,\n\t\t\t\t\tcalcSources,\n\t\t\t\t\tcalcFunctions,\n\t\t\t\t\tconsentSources,\n\t\t\t\t\tconsentSnapshot,\n\t\t\t\t\tuploadSlug,\n\t\t\t\t\tpollSourceRegistry,\n\t\t\t\t\tstrictUploadOwnership: spam ? spam.uploadOwnership === 'strict' : false,\n\t\t\t\t}),\n\t\t\t\t...(overrides?.hooks?.beforeValidate ?? []),\n\t\t\t],\n\t\t\tafterChange: [\n\t\t\t\t// The tally hook runs first and does not swallow errors: a bump failure must throw so\n\t\t\t\t// Payload rolls the submission create/update back inside the operation transaction,\n\t\t\t\t// rather than being absorbed by the dispatch hook's swallow-all error boundary below.\n\t\t\t\t...(pollVotes !== false ? [makeVoteTallyHook()] : []),\n\t\t\t\tmakeAfterChange({ actionRegistry, events, hasRunner, richText, dispatchDeadlineMs }),\n\t\t\t\t// Always registered: it self-gates on the option and on the form's allowChange flag.\n\t\t\t\tmakeVotedCookieHook({ votedCookie }),\n\t\t\t\t...(overrides?.hooks?.afterChange ?? []),\n\t\t\t],\n\t\t},\n\t\t// The vote-submit endpoint must stay first so it shadows the stock REST create (custom\n\t\t// endpoints match before built-ins); `endpoints: false` from a host disables ours too.\n\t\tendpoints:\n\t\t\toverrides?.endpoints === false\n\t\t\t\t? false\n\t\t\t\t: [buildVoteSubmitEndpoint(), ...(overrides?.endpoints ?? [])],\n\t\tfields: overrides?.fields ? overrides.fields({ defaultFields }) : defaultFields,\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -4,16 +4,17 @@ import { asTranslate, labelForKey, resolveDefinitionLabel } from "../translation
|
|
|
4
4
|
import { localizedIf } from "../fields/localizedIf.js";
|
|
5
5
|
import { validateUrl } from "../validation/validateUrl.js";
|
|
6
6
|
import { normalizeCalc } from "../calc/normalizeCalc.js";
|
|
7
|
-
import {
|
|
7
|
+
import { DEFAULT_COLLECTION_SLUGS } from "../plugin/collectionSlugs.js";
|
|
8
|
+
import { pollConfigOf } from "../form/pollState.js";
|
|
9
|
+
import { isLoggedIn } from "../plugin/access.js";
|
|
8
10
|
import { calcUsesSources, resolveCalcContext } from "../calc/resolveCalcContext.js";
|
|
11
|
+
import { buildActionBlocks } from "../actions/buildActionBlocks.js";
|
|
9
12
|
import { buildConditionTypeMap } from "../conditions/conditionType.js";
|
|
10
13
|
import { buildOperandTypes, normalizeFormConditions, normalizeWhere } from "../conditions/normalizeConditions.js";
|
|
11
14
|
import { resolveConsentStatements } from "../consent/resolveConsentStatements.js";
|
|
12
15
|
import { buildFieldBlocks } from "../fields/buildFieldBlocks.js";
|
|
13
16
|
import { normalizeFlow } from "../flow/normalizeFlow.js";
|
|
14
17
|
import { END_OF_FORM } from "../flow/types.js";
|
|
15
|
-
import { pollConfigOf } from "../form/pollState.js";
|
|
16
|
-
import { isLoggedIn } from "../plugin/access.js";
|
|
17
18
|
import { buildPollOptionSourceFields } from "../poll/buildPollOptionSourceFields.js";
|
|
18
19
|
import { resolvePollTypes } from "../poll/pollTypeRegistry.js";
|
|
19
20
|
import { enqueuePollClose } from "../poll/closeJob.js";
|
|
@@ -25,7 +26,6 @@ import { buildFormsEndpoints } from "./formsEndpoints.js";
|
|
|
25
26
|
import { composeSettingsFields } from "./settingsFields.js";
|
|
26
27
|
import { ValidationError } from "payload";
|
|
27
28
|
//#region src/collections/forms.ts
|
|
28
|
-
const FORMS_SLUG = "forms";
|
|
29
29
|
/** `req.context` key under which `consentAfterRead` tracks the form ids it is currently resolving, to break re-entrant reads. */
|
|
30
30
|
const CONSENT_AFTER_READ_GUARD = "formBuilderConsentAfterReadInFlight";
|
|
31
31
|
/** `req.context` key under which `calcAfterRead` tracks the form ids it is currently resolving, to break re-entrant reads. */
|
|
@@ -109,7 +109,7 @@ const mergeSavedShape = (original, delta) => {
|
|
|
109
109
|
}
|
|
110
110
|
return merged;
|
|
111
111
|
};
|
|
112
|
-
const buildFormsCollection = ({ overrides, registry, ruleRegistry, calcAllowed, calcSources, consentSources, consentResolveOnRead, actionRegistry = /* @__PURE__ */ new Map(), localizeContent = true, richText, uploadsCollectionSlug, resultsAccess, pollVotesEnabled = false, pollSourceRegistry, pollTypeRegistry, outcomeFields, buttons, settings, response, fromAddresses, fromSources, departments, redirectRelationships }) => {
|
|
112
|
+
const buildFormsCollection = ({ overrides, slugs = DEFAULT_COLLECTION_SLUGS, registry, ruleRegistry, calcAllowed, calcSources, consentSources, consentResolveOnRead, actionRegistry = /* @__PURE__ */ new Map(), localizeContent = true, richText, uploadsCollectionSlug, resultsAccess, pollVotesEnabled = false, pollSourceRegistry, pollTypeRegistry, outcomeFields, buttons, settings, response, fromAddresses, fromSources, departments, redirectRelationships }) => {
|
|
113
113
|
const conditionTypes = buildConditionTypeMap(registry);
|
|
114
114
|
const pollTypes = pollTypeRegistry ?? resolvePollTypes();
|
|
115
115
|
const pollResultsTypes = pollEligibleTypes(registry);
|
|
@@ -128,7 +128,7 @@ const buildFormsCollection = ({ overrides, registry, ruleRegistry, calcAllowed,
|
|
|
128
128
|
const pollOn = (data?.pollEnabled ?? originalDoc?.pollEnabled) === true;
|
|
129
129
|
const persistOff = (data?.persistSubmissions ?? originalDoc?.persistSubmissions) === false;
|
|
130
130
|
if (pollOn && persistOff) throw new ValidationError({
|
|
131
|
-
collection:
|
|
131
|
+
collection: slugs.forms,
|
|
132
132
|
errors: [{
|
|
133
133
|
path: "persistSubmissions",
|
|
134
134
|
message: asTranslate(req.t)(keys.pollNeedsPersistedSubmissions)
|
|
@@ -140,7 +140,7 @@ const buildFormsCollection = ({ overrides, registry, ruleRegistry, calcAllowed,
|
|
|
140
140
|
const persistOff = (data?.persistSubmissions ?? originalDoc?.persistSubmissions) === false;
|
|
141
141
|
const allowChangeOn = (pollConfigOf(data?.poll)?.allowChange ?? pollConfigOf(originalDoc?.poll)?.allowChange) === true;
|
|
142
142
|
if (pollOn && persistOff && allowChangeOn) throw new ValidationError({
|
|
143
|
-
collection:
|
|
143
|
+
collection: slugs.forms,
|
|
144
144
|
errors: [{
|
|
145
145
|
path: "persistSubmissions",
|
|
146
146
|
message: asTranslate(req.t)(keys.pollAllowChangeNeedsPersistedSubmissions)
|
|
@@ -159,7 +159,7 @@ const buildFormsCollection = ({ overrides, registry, ruleRegistry, calcAllowed,
|
|
|
159
159
|
const operandTypes = buildOperandTypes(normalized, conditionTypes);
|
|
160
160
|
const normalizedFlow = normalizeFlow(data.flow, fieldKeys, (w) => normalizeWhere(w, operandTypes));
|
|
161
161
|
if (providedFlowStepCount(data.flow) > 0 && normalizedFlow === void 0) throw new ValidationError({
|
|
162
|
-
collection:
|
|
162
|
+
collection: slugs.forms,
|
|
163
163
|
errors: [{
|
|
164
164
|
path: "flow",
|
|
165
165
|
message: asTranslate(req.t)(keys.flowNeedsTwoSteps)
|
|
@@ -179,7 +179,7 @@ const buildFormsCollection = ({ overrides, registry, ruleRegistry, calcAllowed,
|
|
|
179
179
|
else {
|
|
180
180
|
const messageKey = eligible.length > 1 ? keys.pollVoteFieldChoose : keys.pollVoteFieldMissing;
|
|
181
181
|
throw new ValidationError({
|
|
182
|
-
collection:
|
|
182
|
+
collection: slugs.forms,
|
|
183
183
|
errors: [{
|
|
184
184
|
path: "poll.resultsField",
|
|
185
185
|
message: asTranslate(req.t)(messageKey)
|
|
@@ -209,7 +209,7 @@ const buildFormsCollection = ({ overrides, registry, ruleRegistry, calcAllowed,
|
|
|
209
209
|
});
|
|
210
210
|
}
|
|
211
211
|
if (errors.length > 0) throw new ValidationError({
|
|
212
|
-
collection:
|
|
212
|
+
collection: slugs.forms,
|
|
213
213
|
errors
|
|
214
214
|
}, req.t);
|
|
215
215
|
return data;
|
|
@@ -534,7 +534,7 @@ const buildFormsCollection = ({ overrides, registry, ruleRegistry, calcAllowed,
|
|
|
534
534
|
});
|
|
535
535
|
return {
|
|
536
536
|
...overrides ?? {},
|
|
537
|
-
slug:
|
|
537
|
+
slug: slugs.forms,
|
|
538
538
|
labels: {
|
|
539
539
|
singular: labelForKey(keys.collectionFormSingular),
|
|
540
540
|
plural: labelForKey(keys.collectionFormPlural),
|
|
@@ -576,6 +576,6 @@ const buildFormsCollection = ({ overrides, registry, ruleRegistry, calcAllowed,
|
|
|
576
576
|
};
|
|
577
577
|
};
|
|
578
578
|
//#endregion
|
|
579
|
-
export {
|
|
579
|
+
export { buildFormsCollection };
|
|
580
580
|
|
|
581
581
|
//# sourceMappingURL=forms.js.map
|