@10x-media/form-builder 0.1.0-beta.24 → 0.1.0-beta.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +21 -21
  3. package/dist/actions/body/serializeBody.d.ts +16 -14
  4. package/dist/actions/body/serializeBody.js +5 -8
  5. package/dist/actions/body/serializeBody.js.map +1 -1
  6. package/dist/actions/builtin/emailAction.d.ts +10 -1
  7. package/dist/actions/builtin/emailAction.js +22 -7
  8. package/dist/actions/builtin/emailAction.js.map +1 -1
  9. package/dist/actions/defineAction.d.ts +2 -4
  10. package/dist/actions/defineAction.js.map +1 -1
  11. package/dist/actions/emailRender.d.ts +27 -0
  12. package/dist/actions/fromAddresses.d.ts +3 -1
  13. package/dist/actions/fromAddresses.js.map +1 -1
  14. package/dist/actions/recipientSources.d.ts +6 -17
  15. package/dist/actions/recipientSources.js.map +1 -1
  16. package/dist/actions/runActions.js +9 -7
  17. package/dist/actions/runActions.js.map +1 -1
  18. package/dist/actions/submissionContext.d.ts +34 -0
  19. package/dist/actions/task.js +10 -13
  20. package/dist/actions/task.js.map +1 -1
  21. package/dist/aggregation/aggregateResponses.js +1 -1
  22. package/dist/aggregation/resolveResultsRequest.js +38 -24
  23. package/dist/aggregation/resolveResultsRequest.js.map +1 -1
  24. package/dist/client/EndpointOptionsSelect.d.ts +1 -0
  25. package/dist/client/EndpointOptionsSelect.js +1 -0
  26. package/dist/client/EndpointOptionsSelect.js.map +1 -1
  27. package/dist/client/FieldNameSelect.d.ts +1 -0
  28. package/dist/client/FieldNameSelect.js +1 -0
  29. package/dist/client/FieldNameSelect.js.map +1 -1
  30. package/dist/client/RecipientsSelect.d.ts +1 -0
  31. package/dist/client/RecipientsSelect.js +1 -0
  32. package/dist/client/RecipientsSelect.js.map +1 -1
  33. package/dist/collections/formSubmissions.js +3 -3
  34. package/dist/collections/forms.js +2 -2
  35. package/dist/exports/rsc.d.ts +1 -1
  36. package/dist/exports/rsc.js +3 -3
  37. package/dist/form/findFormAtLocale.d.ts +30 -0
  38. package/dist/form/findFormAtLocale.js +88 -0
  39. package/dist/form/findFormAtLocale.js.map +1 -0
  40. package/dist/index.d.ts +5 -2
  41. package/dist/index.js +17 -13
  42. package/dist/index.js.map +1 -1
  43. package/dist/options.d.ts +36 -3
  44. package/dist/plugin/registerCollections.js +2 -2
  45. package/dist/poll/resolvePollOutcome.js +1 -1
  46. package/dist/poll/votes/recountPollVotes.js +2 -2
  47. package/dist/poll/votes/voteTallyHook.js +2 -2
  48. package/dist/react/Form.d.ts +11 -2
  49. package/dist/react/Form.js +8 -6
  50. package/dist/react/Form.js.map +1 -1
  51. package/dist/react/Poll.js +4 -2
  52. package/dist/react/Poll.js.map +1 -1
  53. package/dist/react/fetchResults.d.ts +2 -1
  54. package/dist/react/fetchResults.js +5 -2
  55. package/dist/react/fetchResults.js.map +1 -1
  56. package/dist/react/submitForm.d.ts +15 -3
  57. package/dist/react/submitForm.js +5 -3
  58. package/dist/react/submitForm.js.map +1 -1
  59. package/dist/submissions/createSubmission.d.ts +6 -0
  60. package/dist/submissions/createSubmission.js +7 -2
  61. package/dist/submissions/createSubmission.js.map +1 -1
  62. package/dist/submissions/submissionLocale.js +39 -0
  63. package/dist/submissions/submissionLocale.js.map +1 -0
  64. package/dist/submissions/validateSubmission.js +108 -99
  65. package/dist/submissions/validateSubmission.js.map +1 -1
  66. package/dist/submissions/voteChangeEndpoint.js +1 -1
  67. package/package.json +5 -5
@@ -1,18 +1,19 @@
1
1
  import { keys } from "../translations/keys.js";
2
2
  import { asFieldTranslate, asTranslate } from "../translations/server.js";
3
3
  import { resolveConsentEntries } from "../consent/resolveConsentEntries.js";
4
- import { isPollClosed, pollConfigOf } from "../form/pollState.js";
5
4
  import { calcExpressionOf } from "../calc/computeCalcFields.js";
6
5
  import { resolveCalcContext } from "../calc/resolveCalcContext.js";
7
- import { resolveEffectivePollOptions } from "../poll/effectivePollOptions.js";
8
- import { FORMS_SLUG } from "../collections/forms.js";
6
+ import { isPollClosed, pollConfigOf } from "../form/pollState.js";
7
+ import { SUBMISSION_LOCALE_CONTEXT_KEY, resolveSubmissionLocale } from "./submissionLocale.js";
9
8
  import { formIdOf } from "./formIdOf.js";
10
9
  import { voteChangeTargetOf } from "./voteChange.js";
11
10
  import { POLL_CONTEXT_KEY } from "./votedCookie.js";
12
11
  import { IDENTITY_CONTEXT_KEY } from "../spam/constants.js";
13
12
  import { applyPollOptions } from "../poll/applyPollOptions.js";
13
+ import { resolveEffectivePollOptions } from "../poll/effectivePollOptions.js";
14
14
  import { runSubmission } from "./runSubmission.js";
15
15
  import { FORM_SUBMISSIONS_SLUG } from "../collections/formSubmissions.js";
16
+ import { findFormAtLocale } from "../form/findFormAtLocale.js";
16
17
  import { APIError, ValidationError } from "payload";
17
18
  //#region src/submissions/validateSubmission.ts
18
19
  /**
@@ -34,111 +35,119 @@ const validateSubmission = ({ registry, ruleRegistry, calcSources, calcFunctions
34
35
  const boundFormId = originalFormId != null ? formIdOf(originalFormId) : null;
35
36
  if (boundFormId == null || String(boundFormId) !== String(formId)) throw new APIError("form-builder: a vote change cannot move a submission across forms", 400);
36
37
  }
37
- const form = await req.payload.findByID({
38
- collection: FORMS_SLUG,
39
- id: formId,
40
- depth: 0,
41
- locale: req.locale,
42
- req
43
- });
44
- const poll = pollConfigOf(form.poll);
45
- const pollEnabled = form.pollEnabled === true;
46
- req.context[POLL_CONTEXT_KEY] = {
47
- pollEnabled,
48
- allowChange: poll?.allowChange === true
49
- };
50
- if (pollEnabled && isPollClosed(poll)) throw new APIError(asTranslate(req.i18n.t)(keys.pollClosed), 403);
51
- let fields = form.fields ?? [];
52
- const incoming = data.values ?? [];
53
- const locale = req.locale ?? "en";
54
- const t = asFieldTranslate(req.i18n.t);
55
- const resultsField = pollEnabled && typeof poll?.resultsField === "string" && poll.resultsField.length > 0 ? poll.resultsField : void 0;
56
- const resultsInstance = resultsField ? fields.find((instance) => instance.name === resultsField) : void 0;
57
- const usesResolver = typeof poll?.optionSource === "string" && poll.optionSource.length > 0 || Boolean(resultsInstance && registry.get(resultsInstance.blockType)?.resolveOptions);
58
- if (resultsField && usesResolver) {
59
- let resolved;
60
- try {
61
- resolved = await resolveEffectivePollOptions({
38
+ const { localization } = req.payload.config;
39
+ const requested = req.context["formBuilderSubmissionLocale"] ?? req.locale;
40
+ delete req.context[SUBMISSION_LOCALE_CONTEXT_KEY];
41
+ const locale = resolveSubmissionLocale(requested, localization);
42
+ const previousLocale = req.locale;
43
+ if (localization) req.locale = locale;
44
+ try {
45
+ const form = await findFormAtLocale({
46
+ payload: req.payload,
47
+ id: formId,
48
+ locale,
49
+ req
50
+ });
51
+ const poll = pollConfigOf(form.poll);
52
+ const pollEnabled = form.pollEnabled === true;
53
+ req.context[POLL_CONTEXT_KEY] = {
54
+ pollEnabled,
55
+ allowChange: poll?.allowChange === true
56
+ };
57
+ if (pollEnabled && isPollClosed(poll)) throw new APIError(asTranslate(req.i18n.t)(keys.pollClosed), 403);
58
+ let fields = form.fields ?? [];
59
+ const incoming = data.values ?? [];
60
+ const t = asFieldTranslate(req.i18n.t);
61
+ const resultsField = pollEnabled && typeof poll?.resultsField === "string" && poll.resultsField.length > 0 ? poll.resultsField : void 0;
62
+ const resultsInstance = resultsField ? fields.find((instance) => instance.name === resultsField) : void 0;
63
+ const usesResolver = typeof poll?.optionSource === "string" && poll.optionSource.length > 0 || Boolean(resultsInstance && registry.get(resultsInstance.blockType)?.resolveOptions);
64
+ if (resultsField && usesResolver) {
65
+ let resolved;
66
+ try {
67
+ resolved = await resolveEffectivePollOptions({
68
+ payload: req.payload,
69
+ req,
70
+ form,
71
+ sources: pollSourceRegistry ?? /* @__PURE__ */ new Map(),
72
+ fieldTypes: registry
73
+ });
74
+ } catch {
75
+ throw new APIError(asTranslate(req.i18n.t)(keys.pollOptionsUnavailable), 503);
76
+ }
77
+ fields = applyPollOptions(fields, resultsField, resolved);
78
+ const answer = incoming.find((entry) => entry.field === resultsField)?.value;
79
+ if (answer != null && answer !== "" && !resolved.some((option) => option.value === answer)) throw new ValidationError({
80
+ collection: FORM_SUBMISSIONS_SLUG,
81
+ errors: [{
82
+ path: resultsField,
83
+ message: asTranslate(req.i18n.t)(keys.validationSelect)
84
+ }]
85
+ }, req.t);
86
+ }
87
+ const expectedOwner = typeof req.context?.["formBuilderSpamIdentity"] === "string" ? req.context[IDENTITY_CONTEXT_KEY] : void 0;
88
+ let consentEntries = [];
89
+ if (consentSources && fields.some((field) => field.blockType === "consent")) try {
90
+ consentEntries = await resolveConsentEntries({
62
91
  payload: req.payload,
63
92
  req,
64
93
  form,
65
- sources: pollSourceRegistry ?? /* @__PURE__ */ new Map(),
66
- fieldTypes: registry
94
+ sources: consentSources
67
95
  });
68
96
  } catch {
69
- throw new APIError(asTranslate(req.i18n.t)(keys.pollOptionsUnavailable), 503);
97
+ throw new APIError(asTranslate(req.i18n.t)(keys.consentSourcesUnavailable), 503);
70
98
  }
71
- fields = applyPollOptions(fields, resultsField, resolved);
72
- const answer = incoming.find((entry) => entry.field === resultsField)?.value;
73
- if (answer != null && answer !== "" && !resolved.some((option) => option.value === answer)) throw new ValidationError({
74
- collection: FORM_SUBMISSIONS_SLUG,
75
- errors: [{
76
- path: resultsField,
77
- message: asTranslate(req.i18n.t)(keys.validationSelect)
78
- }]
79
- }, req.t);
80
- }
81
- const expectedOwner = typeof req.context?.["formBuilderSpamIdentity"] === "string" ? req.context[IDENTITY_CONTEXT_KEY] : void 0;
82
- let consentEntries = [];
83
- if (consentSources && fields.some((field) => field.blockType === "consent")) try {
84
- consentEntries = await resolveConsentEntries({
85
- payload: req.payload,
99
+ let calcResolved;
100
+ if (fields.some((instance) => calcExpressionOf(instance) !== void 0)) {
101
+ let resolved;
102
+ try {
103
+ resolved = await resolveCalcContext({
104
+ fields,
105
+ sources: calcSources ?? {},
106
+ form,
107
+ payload: req.payload,
108
+ req
109
+ });
110
+ } catch (error) {
111
+ req.payload.logger?.error(`@10x-media/form-builder: calc source resolution failed for form ${String(formId)}: ${error instanceof Error ? error.message : String(error)}`);
112
+ throw new APIError(asTranslate(req.i18n.t)(keys.calcSourcesUnavailable), 503);
113
+ }
114
+ const functions = calcFunctions && Object.keys(calcFunctions).length > 0 ? Object.fromEntries(Object.entries(calcFunctions).map(([name, definition]) => [name, definition.apply])) : void 0;
115
+ calcResolved = {
116
+ ...resolved,
117
+ ...functions ? { functions } : {}
118
+ };
119
+ }
120
+ const result = await runSubmission({
121
+ fields,
122
+ values: incoming,
123
+ calcResolved,
124
+ registry,
125
+ ruleRegistry,
126
+ consentEntries,
127
+ consentSnapshot,
128
+ locale,
129
+ t,
130
+ operation: "create",
86
131
  req,
87
- form,
88
- sources: consentSources
132
+ payload: req.payload,
133
+ formId,
134
+ uploadSlug,
135
+ expectedOwner,
136
+ strictUploadOwnership
89
137
  });
90
- } catch {
91
- throw new APIError(asTranslate(req.i18n.t)(keys.consentSourcesUnavailable), 503);
92
- }
93
- let calcResolved;
94
- if (fields.some((instance) => calcExpressionOf(instance) !== void 0)) {
95
- let resolved;
96
- try {
97
- resolved = await resolveCalcContext({
98
- fields,
99
- sources: calcSources ?? {},
100
- form,
101
- payload: req.payload,
102
- req
103
- });
104
- } catch (error) {
105
- req.payload.logger?.error(`@10x-media/form-builder: calc source resolution failed for form ${String(formId)}: ${error instanceof Error ? error.message : String(error)}`);
106
- throw new APIError(asTranslate(req.i18n.t)(keys.calcSourcesUnavailable), 503);
107
- }
108
- const functions = calcFunctions && Object.keys(calcFunctions).length > 0 ? Object.fromEntries(Object.entries(calcFunctions).map(([name, definition]) => [name, definition.apply])) : void 0;
109
- calcResolved = {
110
- ...resolved,
111
- ...functions ? { functions } : {}
112
- };
138
+ if (result.errors.length > 0) throw new ValidationError({
139
+ collection: FORM_SUBMISSIONS_SLUG,
140
+ errors: result.errors
141
+ }, req.t);
142
+ data.values = result.values;
143
+ data.descriptors = result.descriptors;
144
+ data.consent = result.consent.length > 0 ? result.consent : void 0;
145
+ data.locale = locale;
146
+ if (!req.user) data.status = "complete";
147
+ return data;
148
+ } finally {
149
+ if (localization) req.locale = previousLocale;
113
150
  }
114
- const result = await runSubmission({
115
- fields,
116
- values: incoming,
117
- calcResolved,
118
- registry,
119
- ruleRegistry,
120
- consentEntries,
121
- consentSnapshot,
122
- locale,
123
- t,
124
- operation: "create",
125
- req,
126
- payload: req.payload,
127
- formId,
128
- uploadSlug,
129
- expectedOwner,
130
- strictUploadOwnership
131
- });
132
- if (result.errors.length > 0) throw new ValidationError({
133
- collection: FORM_SUBMISSIONS_SLUG,
134
- errors: result.errors
135
- }, req.t);
136
- data.values = result.values;
137
- data.descriptors = result.descriptors;
138
- data.consent = result.consent.length > 0 ? result.consent : void 0;
139
- data.locale = locale;
140
- if (!req.user) data.status = "complete";
141
- return data;
142
151
  };
143
152
  //#endregion
144
153
  export { validateSubmission };
@@ -1 +1 @@
1
- {"version":3,"file":"validateSubmission.js","names":[],"sources":["../../src/submissions/validateSubmission.ts"],"sourcesContent":["import { APIError, type CollectionBeforeValidateHook, ValidationError } from 'payload'\nimport { calcExpressionOf } from '../calc/computeCalcFields'\nimport type { CalcResolved } from '../calc/evaluate'\nimport type { CalcFunction, CalcSource } from '../calc/registry'\nimport { resolveCalcContext } from '../calc/resolveCalcContext'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport { FORMS_SLUG } from '../collections/forms'\nimport type { ConsentSnapshotMode } from '../consent/captureConsent'\nimport { resolveConsentEntries } from '../consent/resolveConsentEntries'\nimport type { ConsentSourceEntry, ConsentSourcesResolver } from '../consent/types'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport { isPollClosed, pollConfigOf } from '../form/pollState'\nimport { applyPollOptions } from '../poll/applyPollOptions'\nimport type { PollOption } from '../poll/definePollOptionSource'\nimport { resolveEffectivePollOptions } from '../poll/effectivePollOptions'\nimport type { PollOptionSourceRegistry } from '../poll/registry'\nimport { IDENTITY_CONTEXT_KEY } from '../spam/constants'\nimport { keys } from '../translations/keys'\nimport { asFieldTranslate, asTranslate } from '../translations/server'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport { formIdOf } from './formIdOf'\nimport { runSubmission } from './runSubmission'\nimport type { FormFieldInstance, SubmissionValue } from './types'\nimport { POLL_CONTEXT_KEY, type PollContextState, voteChangeTargetOf } from './votedCookie'\n\nexport type ValidateSubmissionArgs = {\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\t/** Registered calc sources (plugin option `calc.sources`); re-resolved fresh per submission. */\n\tcalcSources?: Record<string, CalcSource>\n\t/** Registered calc functions (plugin option `calc.functions`); threaded into calc evaluation. */\n\tcalcFunctions?: Record<string, CalcFunction>\n\t/** The host's consent sources resolver (plugin option `consent.sources`); absent when no sources are configured. */\n\tconsentSources?: ConsentSourcesResolver\n\t/** What each consent proof snapshots of the agreed wording (plugin option `consent.snapshot`). */\n\tconsentSnapshot?: ConsentSnapshotMode\n\t/** The plugin-configured uploads collection slug; absent when uploads are disabled. */\n\tuploadSlug?: string\n\t/** Registered poll option sources; a form's configured `optionSource` resolves through this at validation time. */\n\tpollSourceRegistry?: PollOptionSourceRegistry\n\t/** Plugin `spam.uploadOwnership: 'strict'`: reject an owned upload when the submitter is unidentifiable. */\n\tstrictUploadOwnership?: boolean\n}\n\n/**\n * Server-authoritative submission validation. On create (and on a vote-change update flagged by the\n * vote-submit endpoint) it loads the referenced form, re-runs every\n * field's required check, intrinsic validator, and declarative rules through `runSubmission`, threading\n * `req`/`payload` so server-only async rules can hit the DB, and throws a Payload `ValidationError` with\n * per-field paths on any failure. The client is never trusted.\n * Consent fields are captured into `result.consent` (array of proofs, one per visible consent field),\n * built from the host's sources re-resolved here rather than from anything the form or client carries.\n */\nexport const validateSubmission =\n\t({\n\t\tregistry,\n\t\truleRegistry,\n\t\tcalcSources,\n\t\tcalcFunctions,\n\t\tconsentSources,\n\t\tconsentSnapshot,\n\t\tuploadSlug,\n\t\tpollSourceRegistry,\n\t\tstrictUploadOwnership,\n\t}: ValidateSubmissionArgs): CollectionBeforeValidateHook =>\n\tasync ({ data, operation, originalDoc, req }) => {\n\t\t// Updates run the same create-grade pipeline only when the vote-change endpoint flagged the\n\t\t// request; every other update (host server code with overrideAccess) keeps today's behavior.\n\t\tconst changeTarget = operation === 'update' ? voteChangeTargetOf(req) : undefined\n\t\tif ((operation !== 'create' && changeTarget === undefined) || !data) {\n\t\t\treturn data\n\t\t}\n\t\tconst formId = data.form\n\t\tif (formId == null) {\n\t\t\treturn data\n\t\t}\n\t\tif (changeTarget !== undefined) {\n\t\t\t// Fail closed: an unresolvable stored form binding is as disqualifying as a mismatched one.\n\t\t\tconst originalFormId = (originalDoc as { form?: unknown } | undefined)?.form\n\t\t\tconst boundFormId = originalFormId != null ? formIdOf(originalFormId) : null\n\t\t\tif (boundFormId == null || String(boundFormId) !== String(formId)) {\n\t\t\t\tthrow new APIError('form-builder: a vote change cannot move a submission across forms', 400)\n\t\t\t}\n\t\t}\n\n\t\tconst form = await req.payload.findByID({\n\t\t\tcollection: FORMS_SLUG,\n\t\t\tid: formId as string | number,\n\t\t\tdepth: 0,\n\t\t\tlocale: req.locale,\n\t\t\treq,\n\t\t})\n\n\t\t// Stash the form's poll state so the voted-cookie afterChange hook can skip a second form\n\t\t// fetch on the same request.\n\t\tconst poll = pollConfigOf(form.poll)\n\t\tconst pollEnabled = form.pollEnabled === true\n\t\treq.context[POLL_CONTEXT_KEY] = {\n\t\t\tpollEnabled,\n\t\t\tallowChange: poll?.allowChange === true,\n\t\t} satisfies PollContextState\n\n\t\t// Form-level lifecycle guard, before any field work: a closed poll accepts no submissions,\n\t\t// regardless of what the client rendered.\n\t\tif (pollEnabled && isPollClosed(poll)) {\n\t\t\tthrow new APIError(asTranslate(req.i18n.t)(keys.pollClosed), 403)\n\t\t}\n\n\t\tlet fields = ((form.fields as FormFieldInstance[] | undefined) ?? []) as FormFieldInstance[]\n\t\tconst incoming = ((data.values as SubmissionValue[] | undefined) ?? []) as SubmissionValue[]\n\t\tconst locale = req.locale ?? 'en'\n\t\tconst t = asFieldTranslate(req.i18n.t)\n\n\t\t// With a resolved choice set for the results field (a poll `optionSource`, or the field type's\n\t\t// own `resolveOptions`) the resolved values are the only accepted answers: options are injected\n\t\t// into the field instance (so the select's membership check and the stored option labels use\n\t\t// them) and membership is also enforced directly, so an empty resolution or a non-select results\n\t\t// field still fails closed. A resolve failure rejects the whole submission rather than skipping\n\t\t// the check. Static authored polls are unaffected: their field validates against its own options\n\t\t// through the normal field pipeline.\n\t\tconst resultsField =\n\t\t\tpollEnabled && typeof poll?.resultsField === 'string' && poll.resultsField.length > 0\n\t\t\t\t? poll.resultsField\n\t\t\t\t: undefined\n\t\tconst resultsInstance = resultsField\n\t\t\t? fields.find((instance) => instance.name === resultsField)\n\t\t\t: undefined\n\t\tconst usesResolver =\n\t\t\t(typeof poll?.optionSource === 'string' && poll.optionSource.length > 0) ||\n\t\t\tBoolean(resultsInstance && registry.get(resultsInstance.blockType)?.resolveOptions)\n\t\tif (resultsField && usesResolver) {\n\t\t\tlet resolved: PollOption[]\n\t\t\ttry {\n\t\t\t\tresolved = await resolveEffectivePollOptions({\n\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\treq,\n\t\t\t\t\tform,\n\t\t\t\t\tsources: pollSourceRegistry ?? new Map(),\n\t\t\t\t\tfieldTypes: registry,\n\t\t\t\t})\n\t\t\t} catch {\n\t\t\t\tthrow new APIError(asTranslate(req.i18n.t)(keys.pollOptionsUnavailable), 503)\n\t\t\t}\n\t\t\tfields = applyPollOptions(fields, resultsField, resolved)\n\t\t\tconst answer = incoming.find((entry) => entry.field === resultsField)?.value\n\t\t\tconst isAnswered = answer != null && answer !== ''\n\t\t\tif (isAnswered && !resolved.some((option) => option.value === answer)) {\n\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t{\n\t\t\t\t\t\tcollection: FORM_SUBMISSIONS_SLUG,\n\t\t\t\t\t\terrors: [\n\t\t\t\t\t\t\t{ path: resultsField, message: asTranslate(req.i18n.t)(keys.validationSelect) },\n\t\t\t\t\t\t],\n\t\t\t\t\t},\n\t\t\t\t\treq.t\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t\tconst expectedOwner =\n\t\t\ttypeof req.context?.[IDENTITY_CONTEXT_KEY] === 'string'\n\t\t\t\t? (req.context[IDENTITY_CONTEXT_KEY] as string)\n\t\t\t\t: undefined\n\n\t\t// Every consent proof is built from the source as it reads right now, not from anything the\n\t\t// form or the client carries, so a resolver failure rejects the submission rather than\n\t\t// recording an agreement to a statement the server cannot vouch for.\n\t\tlet consentEntries: ConsentSourceEntry[] = []\n\t\tif (consentSources && fields.some((field) => field.blockType === 'consent')) {\n\t\t\ttry {\n\t\t\t\tconsentEntries = await resolveConsentEntries({\n\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\treq,\n\t\t\t\t\tform,\n\t\t\t\t\tsources: consentSources,\n\t\t\t\t})\n\t\t\t} catch {\n\t\t\t\tthrow new APIError(asTranslate(req.i18n.t)(keys.consentSourcesUnavailable), 503)\n\t\t\t}\n\t\t}\n\n\t\t// Calc extension values re-resolve fresh here, never trusting the render-time embedding, and a\n\t\t// resolver failure rejects the submission: a calc total is money math, so an outage must not\n\t\t// silently evaluate to 0 (the render path fails open instead; see `calcAfterRead`). Surfaced as\n\t\t// a translated 503 (the consent-sources precedent above), with the cause logged server-side\n\t\t// rather than leaked to the anonymous caller.\n\t\tlet calcResolved: CalcResolved | undefined\n\t\tif (fields.some((instance) => calcExpressionOf(instance) !== undefined)) {\n\t\t\tlet resolved: CalcResolved\n\t\t\ttry {\n\t\t\t\tresolved = await resolveCalcContext({\n\t\t\t\t\tfields,\n\t\t\t\t\tsources: calcSources ?? {},\n\t\t\t\t\t// Double cast: a host's generated Form interface has no index signature, so the direct\n\t\t\t\t\t// cast fails under a consumer tsconfig even though the shape is a plain document.\n\t\t\t\t\tform: form as unknown as { id: number | string } & Record<string, unknown>,\n\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\treq,\n\t\t\t\t})\n\t\t\t} catch (error) {\n\t\t\t\treq.payload.logger?.error(\n\t\t\t\t\t`@10x-media/form-builder: calc source resolution failed for form ${String(formId)}: ${\n\t\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t\t}`\n\t\t\t\t)\n\t\t\t\tthrow new APIError(asTranslate(req.i18n.t)(keys.calcSourcesUnavailable), 503)\n\t\t\t}\n\t\t\tconst functions =\n\t\t\t\tcalcFunctions && Object.keys(calcFunctions).length > 0\n\t\t\t\t\t? Object.fromEntries(\n\t\t\t\t\t\t\tObject.entries(calcFunctions).map(([name, definition]) => [name, definition.apply])\n\t\t\t\t\t\t)\n\t\t\t\t\t: undefined\n\t\t\tcalcResolved = { ...resolved, ...(functions ? { functions } : {}) }\n\t\t}\n\n\t\tconst result = await runSubmission({\n\t\t\tfields,\n\t\t\tvalues: incoming,\n\t\t\tcalcResolved,\n\t\t\tregistry,\n\t\t\truleRegistry,\n\t\t\tconsentEntries,\n\t\t\tconsentSnapshot,\n\t\t\tlocale,\n\t\t\tt,\n\t\t\toperation: 'create',\n\t\t\treq,\n\t\t\tpayload: req.payload,\n\t\t\tformId: formId as number | string,\n\t\t\tuploadSlug,\n\t\t\texpectedOwner,\n\t\t\tstrictUploadOwnership,\n\t\t})\n\n\t\tif (result.errors.length > 0) {\n\t\t\tthrow new ValidationError({ collection: FORM_SUBMISSIONS_SLUG, errors: result.errors }, req.t)\n\t\t}\n\n\t\tdata.values = result.values\n\t\tdata.descriptors = result.descriptors\n\t\tdata.consent = result.consent.length > 0 ? result.consent : undefined\n\t\tdata.locale = locale\n\t\t// Unauthenticated submits are always 'complete'. A client-supplied 'partial' would\n\t\t// silently skip all post-submit actions and events. Authenticated callers (e.g. an\n\t\t// admin draft-save flow) may set status themselves.\n\t\tif (!req.user) {\n\t\t\tdata.status = 'complete'\n\t\t}\n\t\treturn data\n\t}\n"],"mappings":"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"}
1
+ {"version":3,"file":"validateSubmission.js","names":[],"sources":["../../src/submissions/validateSubmission.ts"],"sourcesContent":["import {\n\tAPIError,\n\ttype CollectionBeforeValidateHook,\n\ttype TypedLocale,\n\tValidationError,\n} from 'payload'\nimport { calcExpressionOf } from '../calc/computeCalcFields'\nimport type { CalcResolved } from '../calc/evaluate'\nimport type { CalcFunction, CalcSource } from '../calc/registry'\nimport { resolveCalcContext } from '../calc/resolveCalcContext'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport type { ConsentSnapshotMode } from '../consent/captureConsent'\nimport { resolveConsentEntries } from '../consent/resolveConsentEntries'\nimport type { ConsentSourceEntry, ConsentSourcesResolver } from '../consent/types'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport { findFormAtLocale } from '../form/findFormAtLocale'\nimport { isPollClosed, pollConfigOf } from '../form/pollState'\nimport { applyPollOptions } from '../poll/applyPollOptions'\nimport type { PollOption } from '../poll/definePollOptionSource'\nimport { resolveEffectivePollOptions } from '../poll/effectivePollOptions'\nimport type { PollOptionSourceRegistry } from '../poll/registry'\nimport { IDENTITY_CONTEXT_KEY } from '../spam/constants'\nimport { keys } from '../translations/keys'\nimport { asFieldTranslate, asTranslate } from '../translations/server'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport { formIdOf } from './formIdOf'\nimport { runSubmission } from './runSubmission'\nimport { resolveSubmissionLocale, SUBMISSION_LOCALE_CONTEXT_KEY } from './submissionLocale'\nimport type { FormFieldInstance, SubmissionValue } from './types'\nimport { POLL_CONTEXT_KEY, type PollContextState, voteChangeTargetOf } from './votedCookie'\n\nexport type ValidateSubmissionArgs = {\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\t/** Registered calc sources (plugin option `calc.sources`); re-resolved fresh per submission. */\n\tcalcSources?: Record<string, CalcSource>\n\t/** Registered calc functions (plugin option `calc.functions`); threaded into calc evaluation. */\n\tcalcFunctions?: Record<string, CalcFunction>\n\t/** The host's consent sources resolver (plugin option `consent.sources`); absent when no sources are configured. */\n\tconsentSources?: ConsentSourcesResolver\n\t/** What each consent proof snapshots of the agreed wording (plugin option `consent.snapshot`). */\n\tconsentSnapshot?: ConsentSnapshotMode\n\t/** The plugin-configured uploads collection slug; absent when uploads are disabled. */\n\tuploadSlug?: string\n\t/** Registered poll option sources; a form's configured `optionSource` resolves through this at validation time. */\n\tpollSourceRegistry?: PollOptionSourceRegistry\n\t/** Plugin `spam.uploadOwnership: 'strict'`: reject an owned upload when the submitter is unidentifiable. */\n\tstrictUploadOwnership?: boolean\n}\n\n/**\n * Server-authoritative submission validation. On create (and on a vote-change update flagged by the\n * vote-submit endpoint) it loads the referenced form, re-runs every\n * field's required check, intrinsic validator, and declarative rules through `runSubmission`, threading\n * `req`/`payload` so server-only async rules can hit the DB, and throws a Payload `ValidationError` with\n * per-field paths on any failure. The client is never trusted.\n * Consent fields are captured into `result.consent` (array of proofs, one per visible consent field),\n * built from the host's sources re-resolved here rather than from anything the form or client carries.\n */\nexport const validateSubmission =\n\t({\n\t\tregistry,\n\t\truleRegistry,\n\t\tcalcSources,\n\t\tcalcFunctions,\n\t\tconsentSources,\n\t\tconsentSnapshot,\n\t\tuploadSlug,\n\t\tpollSourceRegistry,\n\t\tstrictUploadOwnership,\n\t}: ValidateSubmissionArgs): CollectionBeforeValidateHook =>\n\tasync ({ data, operation, originalDoc, req }) => {\n\t\t// Updates run the same create-grade pipeline only when the vote-change endpoint flagged the\n\t\t// request; every other update (host server code with overrideAccess) keeps today's behavior.\n\t\tconst changeTarget = operation === 'update' ? voteChangeTargetOf(req) : undefined\n\t\tif ((operation !== 'create' && changeTarget === undefined) || !data) {\n\t\t\treturn data\n\t\t}\n\t\tconst formId = data.form\n\t\tif (formId == null) {\n\t\t\treturn data\n\t\t}\n\t\tif (changeTarget !== undefined) {\n\t\t\t// Fail closed: an unresolvable stored form binding is as disqualifying as a mismatched one.\n\t\t\tconst originalFormId = (originalDoc as { form?: unknown } | undefined)?.form\n\t\t\tconst boundFormId = originalFormId != null ? formIdOf(originalFormId) : null\n\t\t\tif (boundFormId == null || String(boundFormId) !== String(formId)) {\n\t\t\t\tthrow new APIError('form-builder: a vote change cannot move a submission across forms', 400)\n\t\t\t}\n\t\t}\n\n\t\t// `req.locale` comes from the visitor's `?locale=` (or `createSubmission`'s `locale`). Clamp it\n\t\t// before anything reads it (the form load below, the host's consent and calc resolvers) so a\n\t\t// visitor cannot run the pipeline under `all` or an unconfigured code. On a localized host the\n\t\t// clamped value is set on `req` only while this hook runs and restored after, because `req` may\n\t\t// be the host's own request; without localization Payload leaves its `locale` unset, and so do we.\n\t\tconst { localization } = req.payload.config\n\t\tconst requested = req.context[SUBMISSION_LOCALE_CONTEXT_KEY] ?? req.locale\n\t\tdelete req.context[SUBMISSION_LOCALE_CONTEXT_KEY]\n\t\tconst locale = resolveSubmissionLocale(requested, localization)\n\t\tconst previousLocale = req.locale\n\t\tif (localization) {\n\t\t\treq.locale = locale as TypedLocale\n\t\t}\n\t\ttry {\n\t\t\tconst form = await findFormAtLocale({\n\t\t\t\tpayload: req.payload,\n\t\t\t\tid: formId as string | number,\n\t\t\t\tlocale,\n\t\t\t\treq,\n\t\t\t})\n\n\t\t\t// Stash the form's poll state so the voted-cookie afterChange hook can skip a second form\n\t\t\t// fetch on the same request.\n\t\t\tconst poll = pollConfigOf(form.poll)\n\t\t\tconst pollEnabled = form.pollEnabled === true\n\t\t\treq.context[POLL_CONTEXT_KEY] = {\n\t\t\t\tpollEnabled,\n\t\t\t\tallowChange: poll?.allowChange === true,\n\t\t\t} satisfies PollContextState\n\n\t\t\t// Form-level lifecycle guard, before any field work: a closed poll accepts no submissions,\n\t\t\t// regardless of what the client rendered.\n\t\t\tif (pollEnabled && isPollClosed(poll)) {\n\t\t\t\tthrow new APIError(asTranslate(req.i18n.t)(keys.pollClosed), 403)\n\t\t\t}\n\n\t\t\tlet fields = ((form.fields as FormFieldInstance[] | undefined) ?? []) as FormFieldInstance[]\n\t\t\tconst incoming = ((data.values as SubmissionValue[] | undefined) ?? []) as SubmissionValue[]\n\t\t\tconst t = asFieldTranslate(req.i18n.t)\n\n\t\t\t// With a resolved choice set for the results field (a poll `optionSource`, or the field type's\n\t\t\t// own `resolveOptions`) the resolved values are the only accepted answers: options are injected\n\t\t\t// into the field instance (so the select's membership check and the stored option labels use\n\t\t\t// them) and membership is also enforced directly, so an empty resolution or a non-select results\n\t\t\t// field still fails closed. A resolve failure rejects the whole submission rather than skipping\n\t\t\t// the check. Static authored polls are unaffected: their field validates against its own options\n\t\t\t// through the normal field pipeline.\n\t\t\tconst resultsField =\n\t\t\t\tpollEnabled && typeof poll?.resultsField === 'string' && poll.resultsField.length > 0\n\t\t\t\t\t? poll.resultsField\n\t\t\t\t\t: undefined\n\t\t\tconst resultsInstance = resultsField\n\t\t\t\t? fields.find((instance) => instance.name === resultsField)\n\t\t\t\t: undefined\n\t\t\tconst usesResolver =\n\t\t\t\t(typeof poll?.optionSource === 'string' && poll.optionSource.length > 0) ||\n\t\t\t\tBoolean(resultsInstance && registry.get(resultsInstance.blockType)?.resolveOptions)\n\t\t\tif (resultsField && usesResolver) {\n\t\t\t\tlet resolved: PollOption[]\n\t\t\t\ttry {\n\t\t\t\t\tresolved = await resolveEffectivePollOptions({\n\t\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\t\treq,\n\t\t\t\t\t\tform,\n\t\t\t\t\t\tsources: pollSourceRegistry ?? new Map(),\n\t\t\t\t\t\tfieldTypes: registry,\n\t\t\t\t\t})\n\t\t\t\t} catch {\n\t\t\t\t\tthrow new APIError(asTranslate(req.i18n.t)(keys.pollOptionsUnavailable), 503)\n\t\t\t\t}\n\t\t\t\tfields = applyPollOptions(fields, resultsField, resolved)\n\t\t\t\tconst answer = incoming.find((entry) => entry.field === resultsField)?.value\n\t\t\t\tconst isAnswered = answer != null && answer !== ''\n\t\t\t\tif (isAnswered && !resolved.some((option) => option.value === answer)) {\n\t\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcollection: FORM_SUBMISSIONS_SLUG,\n\t\t\t\t\t\t\terrors: [\n\t\t\t\t\t\t\t\t{ path: resultsField, message: asTranslate(req.i18n.t)(keys.validationSelect) },\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t},\n\t\t\t\t\t\treq.t\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst expectedOwner =\n\t\t\t\ttypeof req.context?.[IDENTITY_CONTEXT_KEY] === 'string'\n\t\t\t\t\t? (req.context[IDENTITY_CONTEXT_KEY] as string)\n\t\t\t\t\t: undefined\n\n\t\t\t// Every consent proof is built from the source as it reads right now, not from anything the\n\t\t\t// form or the client carries, so a resolver failure rejects the submission rather than\n\t\t\t// recording an agreement to a statement the server cannot vouch for.\n\t\t\tlet consentEntries: ConsentSourceEntry[] = []\n\t\t\tif (consentSources && fields.some((field) => field.blockType === 'consent')) {\n\t\t\t\ttry {\n\t\t\t\t\tconsentEntries = await resolveConsentEntries({\n\t\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\t\treq,\n\t\t\t\t\t\tform,\n\t\t\t\t\t\tsources: consentSources,\n\t\t\t\t\t})\n\t\t\t\t} catch {\n\t\t\t\t\tthrow new APIError(asTranslate(req.i18n.t)(keys.consentSourcesUnavailable), 503)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Calc extension values re-resolve fresh here, never trusting the render-time embedding, and a\n\t\t\t// resolver failure rejects the submission: a calc total is money math, so an outage must not\n\t\t\t// silently evaluate to 0 (the render path fails open instead; see `calcAfterRead`). Surfaced as\n\t\t\t// a translated 503 (the consent-sources precedent above), with the cause logged server-side\n\t\t\t// rather than leaked to the anonymous caller.\n\t\t\tlet calcResolved: CalcResolved | undefined\n\t\t\tif (fields.some((instance) => calcExpressionOf(instance) !== undefined)) {\n\t\t\t\tlet resolved: CalcResolved\n\t\t\t\ttry {\n\t\t\t\t\tresolved = await resolveCalcContext({\n\t\t\t\t\t\tfields,\n\t\t\t\t\t\tsources: calcSources ?? {},\n\t\t\t\t\t\t// Double cast: a host's generated Form interface has no index signature, so the direct\n\t\t\t\t\t\t// cast fails under a consumer tsconfig even though the shape is a plain document.\n\t\t\t\t\t\tform: form as unknown as { id: number | string } & Record<string, unknown>,\n\t\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\t\treq,\n\t\t\t\t\t})\n\t\t\t\t} catch (error) {\n\t\t\t\t\treq.payload.logger?.error(\n\t\t\t\t\t\t`@10x-media/form-builder: calc source resolution failed for form ${String(formId)}: ${\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\tthrow new APIError(asTranslate(req.i18n.t)(keys.calcSourcesUnavailable), 503)\n\t\t\t\t}\n\t\t\t\tconst functions =\n\t\t\t\t\tcalcFunctions && Object.keys(calcFunctions).length > 0\n\t\t\t\t\t\t? Object.fromEntries(\n\t\t\t\t\t\t\t\tObject.entries(calcFunctions).map(([name, definition]) => [name, definition.apply])\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t: undefined\n\t\t\t\tcalcResolved = { ...resolved, ...(functions ? { functions } : {}) }\n\t\t\t}\n\n\t\t\tconst result = await runSubmission({\n\t\t\t\tfields,\n\t\t\t\tvalues: incoming,\n\t\t\t\tcalcResolved,\n\t\t\t\tregistry,\n\t\t\t\truleRegistry,\n\t\t\t\tconsentEntries,\n\t\t\t\tconsentSnapshot,\n\t\t\t\tlocale,\n\t\t\t\tt,\n\t\t\t\toperation: 'create',\n\t\t\t\treq,\n\t\t\t\tpayload: req.payload,\n\t\t\t\tformId: formId as number | string,\n\t\t\t\tuploadSlug,\n\t\t\t\texpectedOwner,\n\t\t\t\tstrictUploadOwnership,\n\t\t\t})\n\n\t\t\tif (result.errors.length > 0) {\n\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t{ collection: FORM_SUBMISSIONS_SLUG, errors: result.errors },\n\t\t\t\t\treq.t\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tdata.values = result.values\n\t\t\tdata.descriptors = result.descriptors\n\t\t\tdata.consent = result.consent.length > 0 ? result.consent : undefined\n\t\t\tdata.locale = locale\n\t\t\t// Unauthenticated submits are always 'complete'. A client-supplied 'partial' would\n\t\t\t// silently skip all post-submit actions and events. Authenticated callers (e.g. an\n\t\t\t// admin draft-save flow) may set status themselves.\n\t\t\tif (!req.user) {\n\t\t\t\tdata.status = 'complete'\n\t\t\t}\n\t\t\treturn data\n\t\t} finally {\n\t\t\tif (localization) {\n\t\t\t\treq.locale = previousLocale\n\t\t\t}\n\t\t}\n\t}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { keys } from "../translations/keys.js";
2
2
  import { asTranslate } from "../translations/server.js";
3
- import "../actions/dispatchContext.js";
4
3
  import { pollConfigOf } from "../form/pollState.js";
4
+ import "../actions/dispatchContext.js";
5
5
  import { formIdOf } from "./formIdOf.js";
6
6
  import { VOTE_CHANGE_CONTEXT_KEY } from "./voteChange.js";
7
7
  import { votedSubmissionIdFromCookie } from "./votedCookie.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@10x-media/form-builder",
3
- "version": "0.1.0-beta.24",
3
+ "version": "0.1.0-beta.26",
4
4
  "description": "End-to-end forms platform for Payload: author, validate, render, collect, aggregate, and act.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -99,10 +99,10 @@
99
99
  "tsdown": "0.22.1",
100
100
  "typescript": "5.9.3",
101
101
  "vitest": "4.1.7",
102
- "@10x-media/payload-test-harness": "0.0.0",
103
- "@10x-media/tsdown-config": "0.0.0",
104
102
  "@10x-media/tsconfig": "0.0.0",
105
- "@10x-media/vitest-config": "0.0.0"
103
+ "@10x-media/tsdown-config": "0.0.0",
104
+ "@10x-media/vitest-config": "0.0.0",
105
+ "@10x-media/payload-test-harness": "0.0.0"
106
106
  },
107
107
  "publishConfig": {
108
108
  "access": "public"
@@ -116,7 +116,7 @@
116
116
  "test": "vitest run",
117
117
  "test:unit": "vitest run src",
118
118
  "test:int": "vitest run tests/int",
119
- "test:matrix": "DB_MATRIX=mongo vitest run tests/int/matrix.int.spec.ts tests/int/messageResponse.int.spec.ts tests/int/pollAllowChange.int.spec.ts tests/int/pollAutoClose.int.spec.ts tests/int/pollVotes.int.spec.ts && DB_MATRIX=postgres vitest run tests/int/matrix.int.spec.ts tests/int/messageResponse.int.spec.ts tests/int/pollAllowChange.int.spec.ts tests/int/pollAutoClose.int.spec.ts tests/int/pollVotes.int.spec.ts",
119
+ "test:matrix": "DB_MATRIX=mongo vitest run tests/int/matrix.int.spec.ts tests/int/messageResponse.int.spec.ts tests/int/pollAllowChange.int.spec.ts tests/int/pollAutoClose.int.spec.ts tests/int/pollVotes.int.spec.ts tests/int/emailLocale.int.spec.ts && DB_MATRIX=postgres vitest run tests/int/matrix.int.spec.ts tests/int/messageResponse.int.spec.ts tests/int/pollAllowChange.int.spec.ts tests/int/pollAutoClose.int.spec.ts tests/int/pollVotes.int.spec.ts tests/int/emailLocale.int.spec.ts",
120
120
  "test:container": "TEST_DB=container DB_MATRIX=mongo vitest run tests/int/matrix.int.spec.ts && TEST_DB=container DB_MATRIX=postgres vitest run tests/int/matrix.int.spec.ts",
121
121
  "test:e2e": "bash scripts/e2e.sh",
122
122
  "dev": "pnpm --filter @10x-media/form-builder-dev dev",