@10x-media/form-builder 0.1.0-beta.25 → 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 (63) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/actions/body/serializeBody.d.ts +10 -14
  3. package/dist/actions/body/serializeBody.js +5 -10
  4. package/dist/actions/body/serializeBody.js.map +1 -1
  5. package/dist/actions/builtin/emailAction.d.ts +7 -0
  6. package/dist/actions/builtin/emailAction.js +14 -7
  7. package/dist/actions/builtin/emailAction.js.map +1 -1
  8. package/dist/actions/defineAction.d.ts +2 -4
  9. package/dist/actions/defineAction.js.map +1 -1
  10. package/dist/actions/emailRender.d.ts +5 -5
  11. package/dist/actions/fromAddresses.d.ts +3 -1
  12. package/dist/actions/fromAddresses.js.map +1 -1
  13. package/dist/actions/recipientSources.d.ts +6 -17
  14. package/dist/actions/recipientSources.js.map +1 -1
  15. package/dist/actions/submissionContext.d.ts +34 -0
  16. package/dist/actions/task.js +10 -13
  17. package/dist/actions/task.js.map +1 -1
  18. package/dist/aggregation/aggregateResponses.js +1 -1
  19. package/dist/aggregation/resolveResultsRequest.js +38 -24
  20. package/dist/aggregation/resolveResultsRequest.js.map +1 -1
  21. package/dist/client/EndpointOptionsSelect.d.ts +1 -0
  22. package/dist/client/EndpointOptionsSelect.js +1 -0
  23. package/dist/client/EndpointOptionsSelect.js.map +1 -1
  24. package/dist/client/FieldNameSelect.d.ts +1 -0
  25. package/dist/client/FieldNameSelect.js +1 -0
  26. package/dist/client/FieldNameSelect.js.map +1 -1
  27. package/dist/client/RecipientsSelect.d.ts +1 -0
  28. package/dist/client/RecipientsSelect.js +1 -0
  29. package/dist/client/RecipientsSelect.js.map +1 -1
  30. package/dist/collections/formSubmissions.js +3 -3
  31. package/dist/collections/forms.js +2 -2
  32. package/dist/exports/rsc.d.ts +1 -1
  33. package/dist/exports/rsc.js +3 -3
  34. package/dist/form/findFormAtLocale.d.ts +30 -0
  35. package/dist/form/findFormAtLocale.js +88 -0
  36. package/dist/form/findFormAtLocale.js.map +1 -0
  37. package/dist/index.d.ts +4 -2
  38. package/dist/index.js +15 -12
  39. package/dist/index.js.map +1 -1
  40. package/dist/options.d.ts +24 -1
  41. package/dist/plugin/registerCollections.js +2 -2
  42. package/dist/poll/resolvePollOutcome.js +1 -1
  43. package/dist/poll/votes/recountPollVotes.js +2 -2
  44. package/dist/poll/votes/voteTallyHook.js +2 -2
  45. package/dist/react/Form.d.ts +9 -5
  46. package/dist/react/Form.js +8 -9
  47. package/dist/react/Form.js.map +1 -1
  48. package/dist/react/Poll.js +4 -2
  49. package/dist/react/Poll.js.map +1 -1
  50. package/dist/react/fetchResults.d.ts +2 -1
  51. package/dist/react/fetchResults.js +5 -2
  52. package/dist/react/fetchResults.js.map +1 -1
  53. package/dist/react/submitForm.d.ts +4 -4
  54. package/dist/react/submitForm.js.map +1 -1
  55. package/dist/submissions/createSubmission.d.ts +6 -0
  56. package/dist/submissions/createSubmission.js +7 -2
  57. package/dist/submissions/createSubmission.js.map +1 -1
  58. package/dist/submissions/submissionLocale.js +27 -6
  59. package/dist/submissions/submissionLocale.js.map +1 -1
  60. package/dist/submissions/validateSubmission.js +108 -101
  61. package/dist/submissions/validateSubmission.js.map +1 -1
  62. package/dist/submissions/voteChangeEndpoint.js +1 -1
  63. package/package.json +3 -3
@@ -1,19 +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
- import { resolveSubmissionLocale } from "./submissionLocale.js";
16
15
  import { FORM_SUBMISSIONS_SLUG } from "../collections/formSubmissions.js";
16
+ import { findFormAtLocale } from "../form/findFormAtLocale.js";
17
17
  import { APIError, ValidationError } from "payload";
18
18
  //#region src/submissions/validateSubmission.ts
19
19
  /**
@@ -35,112 +35,119 @@ const validateSubmission = ({ registry, ruleRegistry, calcSources, calcFunctions
35
35
  const boundFormId = originalFormId != null ? formIdOf(originalFormId) : null;
36
36
  if (boundFormId == null || String(boundFormId) !== String(formId)) throw new APIError("form-builder: a vote change cannot move a submission across forms", 400);
37
37
  }
38
- const locale = resolveSubmissionLocale(req.locale, req.payload.config.localization);
39
- req.locale = locale;
40
- const form = await req.payload.findByID({
41
- collection: FORMS_SLUG,
42
- id: formId,
43
- depth: 0,
44
- locale,
45
- req
46
- });
47
- const poll = pollConfigOf(form.poll);
48
- const pollEnabled = form.pollEnabled === true;
49
- req.context[POLL_CONTEXT_KEY] = {
50
- pollEnabled,
51
- allowChange: poll?.allowChange === true
52
- };
53
- if (pollEnabled && isPollClosed(poll)) throw new APIError(asTranslate(req.i18n.t)(keys.pollClosed), 403);
54
- let fields = form.fields ?? [];
55
- const incoming = data.values ?? [];
56
- const t = asFieldTranslate(req.i18n.t);
57
- const resultsField = pollEnabled && typeof poll?.resultsField === "string" && poll.resultsField.length > 0 ? poll.resultsField : void 0;
58
- const resultsInstance = resultsField ? fields.find((instance) => instance.name === resultsField) : void 0;
59
- const usesResolver = typeof poll?.optionSource === "string" && poll.optionSource.length > 0 || Boolean(resultsInstance && registry.get(resultsInstance.blockType)?.resolveOptions);
60
- if (resultsField && usesResolver) {
61
- let resolved;
62
- try {
63
- 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({
64
91
  payload: req.payload,
65
92
  req,
66
93
  form,
67
- sources: pollSourceRegistry ?? /* @__PURE__ */ new Map(),
68
- fieldTypes: registry
94
+ sources: consentSources
69
95
  });
70
96
  } catch {
71
- throw new APIError(asTranslate(req.i18n.t)(keys.pollOptionsUnavailable), 503);
97
+ throw new APIError(asTranslate(req.i18n.t)(keys.consentSourcesUnavailable), 503);
72
98
  }
73
- fields = applyPollOptions(fields, resultsField, resolved);
74
- const answer = incoming.find((entry) => entry.field === resultsField)?.value;
75
- if (answer != null && answer !== "" && !resolved.some((option) => option.value === answer)) throw new ValidationError({
76
- collection: FORM_SUBMISSIONS_SLUG,
77
- errors: [{
78
- path: resultsField,
79
- message: asTranslate(req.i18n.t)(keys.validationSelect)
80
- }]
81
- }, req.t);
82
- }
83
- const expectedOwner = typeof req.context?.["formBuilderSpamIdentity"] === "string" ? req.context[IDENTITY_CONTEXT_KEY] : void 0;
84
- let consentEntries = [];
85
- if (consentSources && fields.some((field) => field.blockType === "consent")) try {
86
- consentEntries = await resolveConsentEntries({
87
- 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",
88
131
  req,
89
- form,
90
- sources: consentSources
132
+ payload: req.payload,
133
+ formId,
134
+ uploadSlug,
135
+ expectedOwner,
136
+ strictUploadOwnership
91
137
  });
92
- } catch {
93
- throw new APIError(asTranslate(req.i18n.t)(keys.consentSourcesUnavailable), 503);
94
- }
95
- let calcResolved;
96
- if (fields.some((instance) => calcExpressionOf(instance) !== void 0)) {
97
- let resolved;
98
- try {
99
- resolved = await resolveCalcContext({
100
- fields,
101
- sources: calcSources ?? {},
102
- form,
103
- payload: req.payload,
104
- req
105
- });
106
- } catch (error) {
107
- req.payload.logger?.error(`@10x-media/form-builder: calc source resolution failed for form ${String(formId)}: ${error instanceof Error ? error.message : String(error)}`);
108
- throw new APIError(asTranslate(req.i18n.t)(keys.calcSourcesUnavailable), 503);
109
- }
110
- const functions = calcFunctions && Object.keys(calcFunctions).length > 0 ? Object.fromEntries(Object.entries(calcFunctions).map(([name, definition]) => [name, definition.apply])) : void 0;
111
- calcResolved = {
112
- ...resolved,
113
- ...functions ? { functions } : {}
114
- };
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;
115
150
  }
116
- const result = await runSubmission({
117
- fields,
118
- values: incoming,
119
- calcResolved,
120
- registry,
121
- ruleRegistry,
122
- consentEntries,
123
- consentSnapshot,
124
- locale,
125
- t,
126
- operation: "create",
127
- req,
128
- payload: req.payload,
129
- formId,
130
- uploadSlug,
131
- expectedOwner,
132
- strictUploadOwnership
133
- });
134
- if (result.errors.length > 0) throw new ValidationError({
135
- collection: FORM_SUBMISSIONS_SLUG,
136
- errors: result.errors
137
- }, req.t);
138
- data.values = result.values;
139
- data.descriptors = result.descriptors;
140
- data.consent = result.consent.length > 0 ? result.consent : void 0;
141
- data.locale = locale;
142
- if (!req.user) data.status = "complete";
143
- return data;
144
151
  };
145
152
  //#endregion
146
153
  export { validateSubmission };
@@ -1 +1 @@
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 { 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 { resolveSubmissionLocale } 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=`. Clamp it before anything reads it (the form\n\t\t// load below, the host's consent and calc resolvers, the inline action dispatch) so a visitor\n\t\t// cannot run the pipeline under `all` or an unconfigured code. Cast where Payload takes it: the\n\t\t// clamped value is one of the host's own codes, but its concrete `TypedLocale` union is unknowable here.\n\t\tconst locale = resolveSubmissionLocale(req.locale, req.payload.config.localization)\n\t\treq.locale = locale as TypedLocale\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: locale as TypedLocale,\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 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,MAAa,sBACX,EACA,UACA,cACA,aACA,eACA,gBACA,iBACA,YACA,oBACA,4BAED,OAAO,EAAE,MAAM,WAAW,aAAa,UAAU;CAGhD,MAAM,eAAe,cAAc,WAAW,mBAAmB,GAAG,IAAI,KAAA;CACxE,IAAK,cAAc,YAAY,iBAAiB,KAAA,KAAc,CAAC,MAC9D,OAAO;CAER,MAAM,SAAS,KAAK;CACpB,IAAI,UAAU,MACb,OAAO;CAER,IAAI,iBAAiB,KAAA,GAAW;EAE/B,MAAM,iBAAkB,aAAgD;EACxE,MAAM,cAAc,kBAAkB,OAAO,SAAS,cAAc,IAAI;EACxE,IAAI,eAAe,QAAQ,OAAO,WAAW,MAAM,OAAO,MAAM,GAC/D,MAAM,IAAI,SAAS,qEAAqE,GAAG;CAE7F;CAMA,MAAM,SAAS,wBAAwB,IAAI,QAAQ,IAAI,QAAQ,OAAO,YAAY;CAClF,IAAI,SAAS;CAEb,MAAM,OAAO,MAAM,IAAI,QAAQ,SAAS;EACvC,YAAY;EACZ,IAAI;EACJ,OAAO;EACC;EACR;CACD,CAAC;CAID,MAAM,OAAO,aAAa,KAAK,IAAI;CACnC,MAAM,cAAc,KAAK,gBAAgB;CACzC,IAAI,QAAQ,oBAAoB;EAC/B;EACA,aAAa,MAAM,gBAAgB;CACpC;CAIA,IAAI,eAAe,aAAa,IAAI,GACnC,MAAM,IAAI,SAAS,YAAY,IAAI,KAAK,CAAC,EAAE,KAAK,UAAU,GAAG,GAAG;CAGjE,IAAI,SAAW,KAAK,UAA8C,CAAC;CACnE,MAAM,WAAa,KAAK,UAA4C,CAAC;CACrE,MAAM,IAAI,iBAAiB,IAAI,KAAK,CAAC;CASrC,MAAM,eACL,eAAe,OAAO,MAAM,iBAAiB,YAAY,KAAK,aAAa,SAAS,IACjF,KAAK,eACL,KAAA;CACJ,MAAM,kBAAkB,eACrB,OAAO,MAAM,aAAa,SAAS,SAAS,YAAY,IACxD,KAAA;CACH,MAAM,eACJ,OAAO,MAAM,iBAAiB,YAAY,KAAK,aAAa,SAAS,KACtE,QAAQ,mBAAmB,SAAS,IAAI,gBAAgB,SAAS,GAAG,cAAc;CACnF,IAAI,gBAAgB,cAAc;EACjC,IAAI;EACJ,IAAI;GACH,WAAW,MAAM,4BAA4B;IAC5C,SAAS,IAAI;IACb;IACA;IACA,SAAS,sCAAsB,IAAI,IAAI;IACvC,YAAY;GACb,CAAC;EACF,QAAQ;GACP,MAAM,IAAI,SAAS,YAAY,IAAI,KAAK,CAAC,EAAE,KAAK,sBAAsB,GAAG,GAAG;EAC7E;EACA,SAAS,iBAAiB,QAAQ,cAAc,QAAQ;EACxD,MAAM,SAAS,SAAS,MAAM,UAAU,MAAM,UAAU,YAAY,GAAG;EAEvE,IADmB,UAAU,QAAQ,WAAW,MAC9B,CAAC,SAAS,MAAM,WAAW,OAAO,UAAU,MAAM,GACnE,MAAM,IAAI,gBACT;GACC,YAAY;GACZ,QAAQ,CACP;IAAE,MAAM;IAAc,SAAS,YAAY,IAAI,KAAK,CAAC,EAAE,KAAK,gBAAgB;GAAE,CAC/E;EACD,GACA,IAAI,CACL;CAEF;CACA,MAAM,gBACL,OAAO,IAAI,UAAA,+BAAoC,WAC3C,IAAI,QAAQ,wBACb,KAAA;CAKJ,IAAI,iBAAuC,CAAC;CAC5C,IAAI,kBAAkB,OAAO,MAAM,UAAU,MAAM,cAAc,SAAS,GACzE,IAAI;EACH,iBAAiB,MAAM,sBAAsB;GAC5C,SAAS,IAAI;GACb;GACA;GACA,SAAS;EACV,CAAC;CACF,QAAQ;EACP,MAAM,IAAI,SAAS,YAAY,IAAI,KAAK,CAAC,EAAE,KAAK,yBAAyB,GAAG,GAAG;CAChF;CAQD,IAAI;CACJ,IAAI,OAAO,MAAM,aAAa,iBAAiB,QAAQ,MAAM,KAAA,CAAS,GAAG;EACxE,IAAI;EACJ,IAAI;GACH,WAAW,MAAM,mBAAmB;IACnC;IACA,SAAS,eAAe,CAAC;IAGnB;IACN,SAAS,IAAI;IACb;GACD,CAAC;EACF,SAAS,OAAO;GACf,IAAI,QAAQ,QAAQ,MACnB,mEAAmE,OAAO,MAAM,EAAE,IACjF,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAEvD;GACA,MAAM,IAAI,SAAS,YAAY,IAAI,KAAK,CAAC,EAAE,KAAK,sBAAsB,GAAG,GAAG;EAC7E;EACA,MAAM,YACL,iBAAiB,OAAO,KAAK,aAAa,EAAE,SAAS,IAClD,OAAO,YACP,OAAO,QAAQ,aAAa,EAAE,KAAK,CAAC,MAAM,gBAAgB,CAAC,MAAM,WAAW,KAAK,CAAC,CACnF,IACC,KAAA;EACJ,eAAe;GAAE,GAAG;GAAU,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;EAAG;CACnE;CAEA,MAAM,SAAS,MAAM,cAAc;EAClC;EACA,QAAQ;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA,WAAW;EACX;EACA,SAAS,IAAI;EACL;EACR;EACA;EACA;CACD,CAAC;CAED,IAAI,OAAO,OAAO,SAAS,GAC1B,MAAM,IAAI,gBAAgB;EAAE,YAAY;EAAuB,QAAQ,OAAO;CAAO,GAAG,IAAI,CAAC;CAG9F,KAAK,SAAS,OAAO;CACrB,KAAK,cAAc,OAAO;CAC1B,KAAK,UAAU,OAAO,QAAQ,SAAS,IAAI,OAAO,UAAU,KAAA;CAC5D,KAAK,SAAS;CAId,IAAI,CAAC,IAAI,MACR,KAAK,SAAS;CAEf,OAAO;AACR"}
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.25",
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,8 +99,8 @@
99
99
  "tsdown": "0.22.1",
100
100
  "typescript": "5.9.3",
101
101
  "vitest": "4.1.7",
102
- "@10x-media/tsdown-config": "0.0.0",
103
102
  "@10x-media/tsconfig": "0.0.0",
103
+ "@10x-media/tsdown-config": "0.0.0",
104
104
  "@10x-media/vitest-config": "0.0.0",
105
105
  "@10x-media/payload-test-harness": "0.0.0"
106
106
  },
@@ -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",