@10x-media/form-builder 0.1.0-beta.22 → 0.1.0-beta.23
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 +17 -0
- package/LICENSE +21 -21
- package/dist/actions/defineAction.d.ts +23 -1
- package/dist/actions/defineAction.js +14 -1
- package/dist/actions/defineAction.js.map +1 -1
- package/dist/actions/dispatch.d.ts +12 -0
- package/dist/actions/dispatch.js +65 -37
- package/dist/actions/dispatch.js.map +1 -1
- package/dist/actions/dispatchContext.js +7 -1
- package/dist/actions/dispatchContext.js.map +1 -1
- package/dist/actions/runActions.d.ts +2 -0
- package/dist/actions/runActions.js +4 -1
- package/dist/actions/runActions.js.map +1 -1
- package/dist/actions/task.js +5 -1
- package/dist/actions/task.js.map +1 -1
- package/dist/collections/formSubmissions.js +53 -24
- package/dist/collections/formSubmissions.js.map +1 -1
- package/dist/collections/forms.js +47 -1
- package/dist/collections/forms.js.map +1 -1
- package/dist/exports/i18n.d.ts +10 -1
- package/dist/exports/i18n.js +10 -1
- package/dist/exports/react.d.ts +10 -1
- package/dist/exports/react.js +10 -1
- package/dist/exports/types.d.ts +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/options.d.ts +10 -0
- package/dist/plugin/registerCollections.js +2 -1
- package/dist/plugin/registerCollections.js.map +1 -1
- package/dist/submissions/voteChangeEndpoint.js +4 -1
- package/dist/submissions/voteChangeEndpoint.js.map +1 -1
- package/dist/translations/ar.d.ts +7 -0
- package/dist/translations/ar.js +379 -0
- package/dist/translations/ar.js.map +1 -0
- package/dist/translations/de.js +2 -0
- package/dist/translations/de.js.map +1 -1
- package/dist/translations/en.js +2 -0
- package/dist/translations/en.js.map +1 -1
- package/dist/translations/es.d.ts +7 -0
- package/dist/translations/es.js +379 -0
- package/dist/translations/es.js.map +1 -0
- package/dist/translations/fr.d.ts +7 -0
- package/dist/translations/fr.js +379 -0
- package/dist/translations/fr.js.map +1 -0
- package/dist/translations/id.d.ts +7 -0
- package/dist/translations/id.js +379 -0
- package/dist/translations/id.js.map +1 -0
- package/dist/translations/index.d.ts +9 -0
- package/dist/translations/index.js +19 -1
- package/dist/translations/index.js.map +1 -1
- package/dist/translations/keys.d.ts +2 -0
- package/dist/translations/keys.js +2 -0
- package/dist/translations/keys.js.map +1 -1
- package/dist/translations/ko.d.ts +7 -0
- package/dist/translations/ko.js +379 -0
- package/dist/translations/ko.js.map +1 -0
- package/dist/translations/pt.d.ts +7 -0
- package/dist/translations/pt.js +379 -0
- package/dist/translations/pt.js.map +1 -0
- package/dist/translations/ru.d.ts +7 -0
- package/dist/translations/ru.js +379 -0
- package/dist/translations/ru.js.map +1 -0
- package/dist/translations/uk.d.ts +7 -0
- package/dist/translations/uk.js +379 -0
- package/dist/translations/uk.js.map +1 -0
- package/dist/translations/zh.d.ts +7 -0
- package/dist/translations/zh.js +379 -0
- package/dist/translations/zh.js.map +1 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @10x-media/form-builder
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.23
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Essential action dispatch, opened up for hosts running real providers behind it.
|
|
8
|
+
|
|
9
|
+
- New `dispatch: { deadlineMs }` plugin option bounds the inline action passes (essential and no-runner fallback); the 5s default is exported as `INLINE_DISPATCH_DEADLINE_MS` so an action can size its own HTTP timeouts under the bound it runs under.
|
|
10
|
+
- A deadline breach is no longer reported as a definite failure: the visitor gets a 504 with an "outcome unknown" message (a definite refusal keeps the 502), and the kept row is stamped `actionUncertain` instead of `actionFailed`. Behavioral change to note: timed-out essential passes previously stamped `actionFailed`; operator filters keyed on that flag should treat `actionUncertain` as "still in flight".
|
|
11
|
+
- Breached essential work is watched to settlement instead of dropped: a late success clears the stamp, runs the skipped rest actions and prune, and emits `submission.created`; a late failure upgrades the stamp to `actionFailed`.
|
|
12
|
+
- `validateConfig` on `ActionDefinition`: a save-time, cross-field check over one stored action instance, with the message attached to the action block (`actions.<index>`) rather than to a single config field.
|
|
13
|
+
- `ActionResult.detail` plus an exported `ActionError(message, detail)` let an action report structured failure context (status code, provider response) without concatenating it into the message; the plugin logs it as a structured field.
|
|
14
|
+
|
|
15
|
+
- More built-in locales for the `formBuilder:` strings.
|
|
16
|
+
|
|
17
|
+
- Added: `es`, `fr`, `id`, `pt`, `ru`, `zh`, `uk`, `ar`, `ko`. Every key is covered in each.
|
|
18
|
+
- Each locale is a complete bundle, so `makeTranslate(locale)` resolves the visitor-facing strings too, not just the admin authoring UI. Every bundle is exported by name from `/i18n` and `/react` beside `bundles`.
|
|
19
|
+
|
|
3
20
|
## 0.1.0-beta.22
|
|
4
21
|
|
|
5
22
|
### 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.
|
|
@@ -21,6 +21,20 @@ type ActionRunArgs<TConfig extends Record<string, unknown> = Record<string, unkn
|
|
|
21
21
|
t: Translate; /** Serialize a rich text (or legacy string) body config into channel-ready HTML. */
|
|
22
22
|
renderBody: (body: unknown) => Promise<string>;
|
|
23
23
|
};
|
|
24
|
+
/** Context passed to an action's `validateConfig` when a form carrying it is saved. */
|
|
25
|
+
type ActionValidateArgs = {
|
|
26
|
+
/** The whole form data under validation, merged over the stored doc on partial updates. */data: Record<string, unknown>;
|
|
27
|
+
req: PayloadRequest;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Throw from an action's `run` to attach structured context (a status code, a provider response)
|
|
31
|
+
* to the failed `ActionResult` without concatenating it into the message. The plugin logs `detail`
|
|
32
|
+
* alongside the failure; any thrown error carrying a `detail` property is treated the same.
|
|
33
|
+
*/
|
|
34
|
+
declare class ActionError extends Error {
|
|
35
|
+
detail?: unknown;
|
|
36
|
+
constructor(message: string, detail?: unknown);
|
|
37
|
+
}
|
|
24
38
|
/**
|
|
25
39
|
* A post-submit action type, authored once: `config` is the admin `Field[]` for authoring;
|
|
26
40
|
* `run` executes when a submission completes. Built-ins use this same primitive.
|
|
@@ -38,11 +52,19 @@ type ActionDefinition<TConfig extends Record<string, unknown> = Record<string, u
|
|
|
38
52
|
* for notifications and other fire-and-forget work.
|
|
39
53
|
*/
|
|
40
54
|
essential?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Cross-field check over one stored instance of this action, run alongside field validation on
|
|
57
|
+
* every form save. Return `true` to accept or an error message to refuse; the message is
|
|
58
|
+
* attached to the action block (`actions.<index>`) rather than to any one config field, which
|
|
59
|
+
* per-field `validate` cannot express (an optional array's `validate` never runs on the empty
|
|
60
|
+
* array, the case a "template token has no mapping" check cares about most).
|
|
61
|
+
*/
|
|
62
|
+
validateConfig?: (config: TConfig, ctx: ActionValidateArgs) => string | true | Promise<string | true>;
|
|
41
63
|
run: (args: ActionRunArgs<TConfig>) => Promise<void> | void;
|
|
42
64
|
};
|
|
43
65
|
/** Erased shape stored in the registry; config re-narrows per matched type at execution. */
|
|
44
66
|
type AnyActionDefinition = ActionDefinition<Record<string, unknown>>;
|
|
45
67
|
declare const defineAction: <TConfig extends Record<string, unknown> = Record<string, unknown>>(definition: ActionDefinition<TConfig>) => ActionDefinition<TConfig>;
|
|
46
68
|
//#endregion
|
|
47
|
-
export { ActionDefinition, ActionRunArgs, AnyActionDefinition, defineAction };
|
|
69
|
+
export { ActionDefinition, ActionError, ActionRunArgs, ActionValidateArgs, AnyActionDefinition, defineAction };
|
|
48
70
|
//# sourceMappingURL=defineAction.d.ts.map
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
//#region src/actions/defineAction.ts
|
|
2
|
+
/**
|
|
3
|
+
* Throw from an action's `run` to attach structured context (a status code, a provider response)
|
|
4
|
+
* to the failed `ActionResult` without concatenating it into the message. The plugin logs `detail`
|
|
5
|
+
* alongside the failure; any thrown error carrying a `detail` property is treated the same.
|
|
6
|
+
*/
|
|
7
|
+
var ActionError = class extends Error {
|
|
8
|
+
detail;
|
|
9
|
+
constructor(message, detail) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "ActionError";
|
|
12
|
+
this.detail = detail;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
2
15
|
const defineAction = (definition) => definition;
|
|
3
16
|
//#endregion
|
|
4
|
-
export { defineAction };
|
|
17
|
+
export { ActionError, defineAction };
|
|
5
18
|
|
|
6
19
|
//# sourceMappingURL=defineAction.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defineAction.js","names":[],"sources":["../../src/actions/defineAction.ts"],"sourcesContent":["import type { Field, Payload, PayloadRequest } from 'payload'\nimport type { FormContextReference } from '../context/formContext'\nimport type { Translate } from '../fields/types'\nimport type { SubmissionDescriptor, SubmissionValue } from '../submissions/types'\n\n/** Context passed to an action's `run` when a submission completes. */\nexport type ActionRunArgs<TConfig extends Record<string, unknown> = Record<string, unknown>> = {\n\tform: { id: number | string; title?: string }\n\tsubmissionId: number | string\n\tvalues: SubmissionValue[]\n\tdescriptors: SubmissionDescriptor[]\n\t/** The verified form-context reference the submission was made through, or null when it had none. */\n\tcontext: FormContextReference | null\n\tconfig: TConfig\n\tpayload: Payload\n\treq?: PayloadRequest\n\tlocale: string\n\tt: Translate\n\t/** Serialize a rich text (or legacy string) body config into channel-ready HTML. */\n\trenderBody: (body: unknown) => Promise<string>\n}\n\n/**\n * A post-submit action type, authored once: `config` is the admin `Field[]` for authoring;\n * `run` executes when a submission completes. Built-ins use this same primitive.\n */\nexport type ActionDefinition<TConfig extends Record<string, unknown> = Record<string, unknown>> = {\n\ttype: string\n\t/** i18n-key or literal (resolved like a field label), or a per-locale record. */\n\tlabel: string | Record<string, string>\n\tconfig?: Field[]\n\t/**\n\t * This action's failure is the submission's failure: it runs inline before the response (never\n\t * queued, bounded by the dispatch deadline), a throw or timeout turns the submit into an error\n\t * the visitor sees, the remaining actions are skipped, and the submission is kept even on a\n\t * `persistSubmissions: false` form so what the visitor sent is never lost. For an action that\n\t * IS the point of the submission (a signup provider that is the system of record); leave unset\n\t * for notifications and other fire-and-forget work.\n\t */\n\tessential?: boolean\n\trun: (args: ActionRunArgs<TConfig>) => Promise<void> | void\n}\n\n/** Erased shape stored in the registry; config re-narrows per matched type at execution. */\nexport type AnyActionDefinition = ActionDefinition<Record<string, unknown>>\n\nexport const defineAction = <TConfig extends Record<string, unknown> = Record<string, unknown>>(\n\tdefinition: ActionDefinition<TConfig>\n): ActionDefinition<TConfig> => definition\n"],"mappings":";
|
|
1
|
+
{"version":3,"file":"defineAction.js","names":[],"sources":["../../src/actions/defineAction.ts"],"sourcesContent":["import type { Field, Payload, PayloadRequest } from 'payload'\nimport type { FormContextReference } from '../context/formContext'\nimport type { Translate } from '../fields/types'\nimport type { SubmissionDescriptor, SubmissionValue } from '../submissions/types'\n\n/** Context passed to an action's `run` when a submission completes. */\nexport type ActionRunArgs<TConfig extends Record<string, unknown> = Record<string, unknown>> = {\n\tform: { id: number | string; title?: string }\n\tsubmissionId: number | string\n\tvalues: SubmissionValue[]\n\tdescriptors: SubmissionDescriptor[]\n\t/** The verified form-context reference the submission was made through, or null when it had none. */\n\tcontext: FormContextReference | null\n\tconfig: TConfig\n\tpayload: Payload\n\treq?: PayloadRequest\n\tlocale: string\n\tt: Translate\n\t/** Serialize a rich text (or legacy string) body config into channel-ready HTML. */\n\trenderBody: (body: unknown) => Promise<string>\n}\n\n/** Context passed to an action's `validateConfig` when a form carrying it is saved. */\nexport type ActionValidateArgs = {\n\t/** The whole form data under validation, merged over the stored doc on partial updates. */\n\tdata: Record<string, unknown>\n\treq: PayloadRequest\n}\n\n/**\n * Throw from an action's `run` to attach structured context (a status code, a provider response)\n * to the failed `ActionResult` without concatenating it into the message. The plugin logs `detail`\n * alongside the failure; any thrown error carrying a `detail` property is treated the same.\n */\nexport class ActionError extends Error {\n\tdetail?: unknown\n\n\tconstructor(message: string, detail?: unknown) {\n\t\tsuper(message)\n\t\tthis.name = 'ActionError'\n\t\tthis.detail = detail\n\t}\n}\n\n/**\n * A post-submit action type, authored once: `config` is the admin `Field[]` for authoring;\n * `run` executes when a submission completes. Built-ins use this same primitive.\n */\nexport type ActionDefinition<TConfig extends Record<string, unknown> = Record<string, unknown>> = {\n\ttype: string\n\t/** i18n-key or literal (resolved like a field label), or a per-locale record. */\n\tlabel: string | Record<string, string>\n\tconfig?: Field[]\n\t/**\n\t * This action's failure is the submission's failure: it runs inline before the response (never\n\t * queued, bounded by the dispatch deadline), a throw or timeout turns the submit into an error\n\t * the visitor sees, the remaining actions are skipped, and the submission is kept even on a\n\t * `persistSubmissions: false` form so what the visitor sent is never lost. For an action that\n\t * IS the point of the submission (a signup provider that is the system of record); leave unset\n\t * for notifications and other fire-and-forget work.\n\t */\n\tessential?: boolean\n\t/**\n\t * Cross-field check over one stored instance of this action, run alongside field validation on\n\t * every form save. Return `true` to accept or an error message to refuse; the message is\n\t * attached to the action block (`actions.<index>`) rather than to any one config field, which\n\t * per-field `validate` cannot express (an optional array's `validate` never runs on the empty\n\t * array, the case a \"template token has no mapping\" check cares about most).\n\t */\n\tvalidateConfig?: (\n\t\tconfig: TConfig,\n\t\tctx: ActionValidateArgs\n\t) => string | true | Promise<string | true>\n\trun: (args: ActionRunArgs<TConfig>) => Promise<void> | void\n}\n\n/** Erased shape stored in the registry; config re-narrows per matched type at execution. */\nexport type AnyActionDefinition = ActionDefinition<Record<string, unknown>>\n\nexport const defineAction = <TConfig extends Record<string, unknown> = Record<string, unknown>>(\n\tdefinition: ActionDefinition<TConfig>\n): ActionDefinition<TConfig> => definition\n"],"mappings":";;;;;;AAkCA,IAAa,cAAb,cAAiC,MAAM;CACtC;CAEA,YAAY,SAAiB,QAAkB;EAC9C,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,SAAS;CACf;AACD;AAqCA,MAAa,gBACZ,eAC+B"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Payload, PayloadRequest } from "payload";
|
|
2
|
+
|
|
3
|
+
//#region src/actions/dispatch.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Default cap on inline action work so a missing worker never hangs the submission response.
|
|
6
|
+
* Overridable per host via the `dispatch.deadlineMs` plugin option; an essential action making
|
|
7
|
+
* outbound calls should size its own timeouts under whichever value is in effect.
|
|
8
|
+
*/
|
|
9
|
+
declare const INLINE_DISPATCH_DEADLINE_MS = 5000;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { INLINE_DISPATCH_DEADLINE_MS };
|
|
12
|
+
//# sourceMappingURL=dispatch.d.ts.map
|
package/dist/actions/dispatch.js
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { isEssentialAction } from "./registry.js";
|
|
2
|
-
import { ESSENTIAL_ACTION_FAILED_CONTEXT_KEY } from "./dispatchContext.js";
|
|
2
|
+
import { ESSENTIAL_ACTION_FAILED_CONTEXT_KEY, ESSENTIAL_ACTION_UNCERTAIN_CONTEXT_KEY } from "./dispatchContext.js";
|
|
3
3
|
import { ACTIONS_TASK_SLUG, runActionsForSubmission } from "./task.js";
|
|
4
|
+
//#region src/actions/dispatch.ts
|
|
5
|
+
/**
|
|
6
|
+
* Default cap on inline action work so a missing worker never hangs the submission response.
|
|
7
|
+
* Overridable per host via the `dispatch.deadlineMs` plugin option; an essential action making
|
|
8
|
+
* outbound calls should size its own timeouts under whichever value is in effect.
|
|
9
|
+
*/
|
|
10
|
+
const INLINE_DISPATCH_DEADLINE_MS = 5e3;
|
|
4
11
|
const canQueue = (payload) => typeof payload.jobs?.queue === "function";
|
|
5
12
|
const deadline = (ms) => new Promise((resolve) => {
|
|
6
13
|
setTimeout(resolve, ms).unref?.();
|
|
7
14
|
});
|
|
15
|
+
/** Guard a caller-supplied callback so its throw can never break the dispatch flow. */
|
|
16
|
+
const guarded = async (payload, label, callback) => {
|
|
17
|
+
try {
|
|
18
|
+
await callback?.();
|
|
19
|
+
} catch (error) {
|
|
20
|
+
payload.logger?.error(`@10x-media/form-builder: ${label} callback threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
8
23
|
/**
|
|
9
24
|
* Dispatch a submission's post-submit actions without throwing and without blocking the response on slow
|
|
10
25
|
* action work. With no actions, returns immediately. When a job runner is present, enqueues the native
|
|
@@ -17,6 +32,41 @@ const dispatchActions = async (args) => {
|
|
|
17
32
|
const { payload, registry, req, formId, submissionId } = args;
|
|
18
33
|
const actions = args.actions ?? [];
|
|
19
34
|
const hasEssential = actions.some((instance) => isEssentialAction(registry, instance));
|
|
35
|
+
const rest = hasEssential ? actions.filter((instance) => !isEssentialAction(registry, instance)) : actions;
|
|
36
|
+
const runClosingPass = async (withReq) => {
|
|
37
|
+
const subset = hasEssential ? { subset: "rest" } : {};
|
|
38
|
+
if (args.hasRunner && canQueue(payload)) {
|
|
39
|
+
try {
|
|
40
|
+
await payload.jobs.queue({
|
|
41
|
+
task: ACTIONS_TASK_SLUG,
|
|
42
|
+
input: {
|
|
43
|
+
formId: String(formId),
|
|
44
|
+
submissionId: String(submissionId),
|
|
45
|
+
...subset
|
|
46
|
+
},
|
|
47
|
+
...withReq ? { req: withReq } : {}
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
payload.logger?.error(`@10x-media/form-builder: failed to enqueue actions for submission ${String(submissionId)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const ms = args.deadlineMs ?? 5e3;
|
|
55
|
+
const work = runActionsForSubmission({
|
|
56
|
+
input: {
|
|
57
|
+
formId,
|
|
58
|
+
submissionId,
|
|
59
|
+
...subset
|
|
60
|
+
},
|
|
61
|
+
registry,
|
|
62
|
+
payload,
|
|
63
|
+
req: withReq,
|
|
64
|
+
richText: args.richText
|
|
65
|
+
}).catch((error) => {
|
|
66
|
+
payload.logger?.error(`@10x-media/form-builder: inline action dispatch for submission ${String(submissionId)} threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
67
|
+
});
|
|
68
|
+
await (withReq ? Promise.race([work, deadline(ms)]) : work);
|
|
69
|
+
};
|
|
20
70
|
if (hasEssential) {
|
|
21
71
|
const ms = args.deadlineMs ?? 5e3;
|
|
22
72
|
const work = runActionsForSubmission({
|
|
@@ -44,50 +94,28 @@ const dispatchActions = async (args) => {
|
|
|
44
94
|
failed: true
|
|
45
95
|
}))]);
|
|
46
96
|
if (outcome.failed) {
|
|
47
|
-
|
|
97
|
+
const contextKey = outcome.timedOut ? ESSENTIAL_ACTION_UNCERTAIN_CONTEXT_KEY : ESSENTIAL_ACTION_FAILED_CONTEXT_KEY;
|
|
48
98
|
if (req) req.context = {
|
|
49
99
|
...req.context ?? {},
|
|
50
|
-
[
|
|
100
|
+
[contextKey]: true
|
|
51
101
|
};
|
|
102
|
+
await guarded(payload, "onEssentialFailed", () => args.onEssentialFailed?.({ timedOut: outcome.timedOut }));
|
|
103
|
+
if (outcome.timedOut) {
|
|
104
|
+
payload.logger?.error(`@10x-media/form-builder: essential action pass for submission ${String(submissionId)} outlived its ${ms}ms deadline; reporting the submission uncertain (the work may still complete)`);
|
|
105
|
+
work.then(async (late) => {
|
|
106
|
+
payload.logger?.[late.failed ? "error" : "info"]?.(`@10x-media/form-builder: essential action pass for submission ${String(submissionId)} settled after its deadline: ${late.failed ? "failed" : "succeeded"}`);
|
|
107
|
+
if (!late.failed && (rest.length > 0 || args.persistSubmissions === false)) await runClosingPass(void 0);
|
|
108
|
+
await guarded(payload, "onEssentialSettled", () => args.onEssentialSettled?.({ ok: !late.failed }));
|
|
109
|
+
});
|
|
110
|
+
}
|
|
52
111
|
return { essentialFailed: true };
|
|
53
112
|
}
|
|
54
113
|
}
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
if (args.hasRunner && canQueue(payload)) {
|
|
58
|
-
try {
|
|
59
|
-
await payload.jobs.queue({
|
|
60
|
-
task: ACTIONS_TASK_SLUG,
|
|
61
|
-
input: {
|
|
62
|
-
formId: String(formId),
|
|
63
|
-
submissionId: String(submissionId),
|
|
64
|
-
...subset
|
|
65
|
-
},
|
|
66
|
-
req
|
|
67
|
-
});
|
|
68
|
-
} catch (error) {
|
|
69
|
-
payload.logger?.error(`@10x-media/form-builder: failed to enqueue actions for submission ${String(submissionId)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
70
|
-
}
|
|
71
|
-
return { essentialFailed: false };
|
|
72
|
-
}
|
|
73
|
-
const ms = args.deadlineMs ?? 5e3;
|
|
74
|
-
const work = runActionsForSubmission({
|
|
75
|
-
input: {
|
|
76
|
-
formId,
|
|
77
|
-
submissionId,
|
|
78
|
-
...subset
|
|
79
|
-
},
|
|
80
|
-
registry,
|
|
81
|
-
payload,
|
|
82
|
-
req,
|
|
83
|
-
richText: args.richText
|
|
84
|
-
}).catch((error) => {
|
|
85
|
-
payload.logger?.error(`@10x-media/form-builder: inline action dispatch for submission ${String(submissionId)} threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
86
|
-
});
|
|
87
|
-
await Promise.race([work, deadline(ms)]);
|
|
114
|
+
if (rest.length === 0 && args.persistSubmissions !== false) return { essentialFailed: false };
|
|
115
|
+
await runClosingPass(req);
|
|
88
116
|
return { essentialFailed: false };
|
|
89
117
|
};
|
|
90
118
|
//#endregion
|
|
91
|
-
export { dispatchActions };
|
|
119
|
+
export { INLINE_DISPATCH_DEADLINE_MS, dispatchActions };
|
|
92
120
|
|
|
93
121
|
//# sourceMappingURL=dispatch.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.js","names":[],"sources":["../../src/actions/dispatch.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport type { RichTextBodyOption } from './body/serializeBody'\nimport { ESSENTIAL_ACTION_FAILED_CONTEXT_KEY } from './dispatchContext'\nimport { type ActionRegistry, isEssentialAction } from './registry'\nimport type { ActionInstance } from './runActions'\nimport { ACTIONS_TASK_SLUG, runActionsForSubmission } from './task'\n\nexport { ESSENTIAL_ACTION_FAILED_CONTEXT_KEY }\n\n/** Default cap on inline action work so a missing worker never hangs the submission response. */\nexport const INLINE_DISPATCH_DEADLINE_MS = 5_000\n\nexport type DispatchActionsArgs = {\n\tactions: ActionInstance[] | null | undefined\n\tformId: number | string\n\tsubmissionId: number | string\n\tregistry: ActionRegistry\n\tpayload: Payload\n\treq?: PayloadRequest\n\t/** Whether a job runner is likely present (queued path); otherwise the bounded-inline fallback runs. */\n\thasRunner: boolean\n\t/** The owning form's `persistSubmissions`; when explicitly `false`, run the completion even with no actions so the row is pruned. */\n\tpersistSubmissions?: boolean\n\tdeadlineMs?: number\n\trichText?: RichTextBodyOption\n}\n\nconst canQueue = (payload: Payload): boolean => typeof payload.jobs?.queue === 'function'\n\nconst deadline = (ms: number): Promise<void> =>\n\tnew Promise((resolve) => {\n\t\tconst timer = setTimeout(resolve, ms)\n\t\ttimer.unref?.()\n\t})\n\n/**\n * Dispatch a submission's post-submit actions without throwing and without blocking the response on slow\n * action work. With no actions, returns immediately. When a job runner is present, enqueues the native\n * `form-builder-actions` task and returns (action work happens out of band). Otherwise runs the actions\n * inline but bounded by a deadline so a missing worker still delivers without hanging the request; any\n * error is swallowed (logged via `payload.logger`). Never rejects; the return reports whether an\n * essential pass failed, so the caller can withhold success signals (the created event, the 201).\n */\nexport const dispatchActions = async (\n\targs: DispatchActionsArgs\n): Promise<{ essentialFailed: boolean }> => {\n\tconst { payload, registry, req, formId, submissionId } = args\n\tconst actions = args.actions ?? []\n\tconst hasEssential = actions.some((instance) => isEssentialAction(registry, instance))\n\n\t// Essential actions run first, inline and awaited, never queued: their failure is the\n\t// submission's failure, which the submit endpoint turns into an error response via the context\n\t// flag. On failure the remaining actions are skipped and so is the pruning completion, so a\n\t// `persistSubmissions: false` form keeps the row: the provider never received it, and the\n\t// stored copy is the only record of what the visitor sent.\n\tif (hasEssential) {\n\t\tconst ms = args.deadlineMs ?? INLINE_DISPATCH_DEADLINE_MS\n\t\tconst work = runActionsForSubmission({\n\t\t\tinput: { formId, submissionId, subset: 'essential' },\n\t\t\tregistry,\n\t\t\tpayload,\n\t\t\treq,\n\t\t\trichText: args.richText,\n\t\t}).then(\n\t\t\t(results) => ({ timedOut: false, failed: results.some((result) => !result.ok) }),\n\t\t\t(error) => {\n\t\t\t\tpayload.logger?.error(\n\t\t\t\t\t`@10x-media/form-builder: essential action pass for submission ${String(submissionId)} threw: ${\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\treturn { timedOut: false, failed: true }\n\t\t\t}\n\t\t)\n\t\tconst outcome = await Promise.race([\n\t\t\twork,\n\t\t\tdeadline(ms).then(() => ({ timedOut: true, failed: true })),\n\t\t])\n\t\tif (outcome.failed) {\n\t\t\tif (outcome.timedOut) {\n\t\t\t\tpayload.logger?.error(\n\t\t\t\t\t`@10x-media/form-builder: essential action pass for submission ${String(submissionId)} outlived its ${ms}ms deadline; treating the submission as failed (the work may still complete)`\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (req) {\n\t\t\t\treq.context = { ...(req.context ?? {}), [ESSENTIAL_ACTION_FAILED_CONTEXT_KEY]: true }\n\t\t\t}\n\t\t\treturn { essentialFailed: true }\n\t\t}\n\t}\n\n\t// Nothing to run and nothing to prune: skip. An action-less form that opted out of persistence still\n\t// runs the completion below, so `runActionsForSubmission` can delete the row out of band.\n\tconst rest = hasEssential\n\t\t? actions.filter((instance) => !isEssentialAction(registry, instance))\n\t\t: actions\n\tif (rest.length === 0 && args.persistSubmissions !== false) {\n\t\treturn { essentialFailed: false }\n\t}\n\n\t// With an essential pass already run, the closing pass covers only the rest (subset threading\n\t// keeps the queued task from re-running the essential actions).\n\tconst subset = hasEssential ? ({ subset: 'rest' } as const) : {}\n\n\tif (args.hasRunner && canQueue(payload)) {\n\t\ttry {\n\t\t\tawait payload.jobs.queue({\n\t\t\t\ttask: ACTIONS_TASK_SLUG,\n\t\t\t\tinput: { formId: String(formId), submissionId: String(submissionId), ...subset },\n\t\t\t\treq,\n\t\t\t})\n\t\t} catch (error) {\n\t\t\tpayload.logger?.error(\n\t\t\t\t`@10x-media/form-builder: failed to enqueue actions for submission ${String(submissionId)}: ${\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 { essentialFailed: false }\n\t}\n\n\tconst ms = args.deadlineMs ?? INLINE_DISPATCH_DEADLINE_MS\n\t// Guard the action arm itself (not just the race) so a rejection AFTER the deadline wins is never an unhandled rejection.\n\tconst work = runActionsForSubmission({\n\t\tinput: { formId, submissionId, ...subset },\n\t\tregistry,\n\t\tpayload,\n\t\treq,\n\t\trichText: args.richText,\n\t}).catch((error) => {\n\t\tpayload.logger?.error(\n\t\t\t`@10x-media/form-builder: inline action dispatch for submission ${String(submissionId)} threw: ${\n\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t}`\n\t\t)\n\t})\n\tawait Promise.race([work, deadline(ms)])\n\treturn { essentialFailed: false }\n}\n"],"mappings":";;;AA2BA,MAAM,YAAY,YAA8B,OAAO,QAAQ,MAAM,UAAU;AAE/E,MAAM,YAAY,OACjB,IAAI,SAAS,YAAY;CAExB,WADyB,SAAS,EAC9B,EAAE,QAAQ;AACf,CAAC;;;;;;;;;AAUF,MAAa,kBAAkB,OAC9B,SAC2C;CAC3C,MAAM,EAAE,SAAS,UAAU,KAAK,QAAQ,iBAAiB;CACzD,MAAM,UAAU,KAAK,WAAW,CAAC;CACjC,MAAM,eAAe,QAAQ,MAAM,aAAa,kBAAkB,UAAU,QAAQ,CAAC;CAOrF,IAAI,cAAc;EACjB,MAAM,KAAK,KAAK,cAAA;EAChB,MAAM,OAAO,wBAAwB;GACpC,OAAO;IAAE;IAAQ;IAAc,QAAQ;GAAY;GACnD;GACA;GACA;GACA,UAAU,KAAK;EAChB,CAAC,EAAE,MACD,aAAa;GAAE,UAAU;GAAO,QAAQ,QAAQ,MAAM,WAAW,CAAC,OAAO,EAAE;EAAE,KAC7E,UAAU;GACV,QAAQ,QAAQ,MACf,iEAAiE,OAAO,YAAY,EAAE,UACrF,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAEvD;GACA,OAAO;IAAE,UAAU;IAAO,QAAQ;GAAK;EACxC,CACD;EACA,MAAM,UAAU,MAAM,QAAQ,KAAK,CAClC,MACA,SAAS,EAAE,EAAE,YAAY;GAAE,UAAU;GAAM,QAAQ;EAAK,EAAE,CAC3D,CAAC;EACD,IAAI,QAAQ,QAAQ;GACnB,IAAI,QAAQ,UACX,QAAQ,QAAQ,MACf,iEAAiE,OAAO,YAAY,EAAE,gBAAgB,GAAG,6EAC1G;GAED,IAAI,KACH,IAAI,UAAU;IAAE,GAAI,IAAI,WAAW,CAAC;KAAK,sCAAsC;GAAK;GAErF,OAAO,EAAE,iBAAiB,KAAK;EAChC;CACD;CAOA,KAHa,eACV,QAAQ,QAAQ,aAAa,CAAC,kBAAkB,UAAU,QAAQ,CAAC,IACnE,SACM,WAAW,KAAK,KAAK,uBAAuB,OACpD,OAAO,EAAE,iBAAiB,MAAM;CAKjC,MAAM,SAAS,eAAgB,EAAE,QAAQ,OAAO,IAAc,CAAC;CAE/D,IAAI,KAAK,aAAa,SAAS,OAAO,GAAG;EACxC,IAAI;GACH,MAAM,QAAQ,KAAK,MAAM;IACxB,MAAM;IACN,OAAO;KAAE,QAAQ,OAAO,MAAM;KAAG,cAAc,OAAO,YAAY;KAAG,GAAG;IAAO;IAC/E;GACD,CAAC;EACF,SAAS,OAAO;GACf,QAAQ,QAAQ,MACf,qEAAqE,OAAO,YAAY,EAAE,IACzF,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAEvD;EACD;EACA,OAAO,EAAE,iBAAiB,MAAM;CACjC;CAEA,MAAM,KAAK,KAAK,cAAA;CAEhB,MAAM,OAAO,wBAAwB;EACpC,OAAO;GAAE;GAAQ;GAAc,GAAG;EAAO;EACzC;EACA;EACA;EACA,UAAU,KAAK;CAChB,CAAC,EAAE,OAAO,UAAU;EACnB,QAAQ,QAAQ,MACf,kEAAkE,OAAO,YAAY,EAAE,UACtF,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAEvD;CACD,CAAC;CACD,MAAM,QAAQ,KAAK,CAAC,MAAM,SAAS,EAAE,CAAC,CAAC;CACvC,OAAO,EAAE,iBAAiB,MAAM;AACjC"}
|
|
1
|
+
{"version":3,"file":"dispatch.js","names":[],"sources":["../../src/actions/dispatch.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport type { RichTextBodyOption } from './body/serializeBody'\nimport {\n\tESSENTIAL_ACTION_FAILED_CONTEXT_KEY,\n\tESSENTIAL_ACTION_UNCERTAIN_CONTEXT_KEY,\n} from './dispatchContext'\nimport { type ActionRegistry, isEssentialAction } from './registry'\nimport type { ActionInstance } from './runActions'\nimport { ACTIONS_TASK_SLUG, runActionsForSubmission } from './task'\n\nexport { ESSENTIAL_ACTION_FAILED_CONTEXT_KEY, ESSENTIAL_ACTION_UNCERTAIN_CONTEXT_KEY }\n\n/**\n * Default cap on inline action work so a missing worker never hangs the submission response.\n * Overridable per host via the `dispatch.deadlineMs` plugin option; an essential action making\n * outbound calls should size its own timeouts under whichever value is in effect.\n */\nexport const INLINE_DISPATCH_DEADLINE_MS = 5_000\n\nexport type DispatchActionsArgs = {\n\tactions: ActionInstance[] | null | undefined\n\tformId: number | string\n\tsubmissionId: number | string\n\tregistry: ActionRegistry\n\tpayload: Payload\n\treq?: PayloadRequest\n\t/** Whether a job runner is likely present (queued path); otherwise the bounded-inline fallback runs. */\n\thasRunner: boolean\n\t/** The owning form's `persistSubmissions`; when explicitly `false`, run the completion even with no actions so the row is pruned. */\n\tpersistSubmissions?: boolean\n\tdeadlineMs?: number\n\trichText?: RichTextBodyOption\n\t/**\n\t * Awaited before an essential failure is returned, while the create transaction is still open:\n\t * the caller stamps the submission (`actionFailed`, or `actionUncertain` on a deadline breach).\n\t */\n\tonEssentialFailed?: (outcome: { timedOut: boolean }) => void | Promise<void>\n\t/**\n\t * Called when a deadline-breached essential pass later settles, with the real outcome. Runs\n\t * after the response went out (no open transaction). On success it is awaited AFTER the late\n\t * closing pass (rest actions + prune), so the uncertain stamp outlives any partially completed\n\t * reconciliation and the caller's success signals follow the finished work; a stamp-clearing\n\t * caller must tolerate the row having been pruned. On failure it runs immediately.\n\t */\n\tonEssentialSettled?: (outcome: { ok: boolean }) => void | Promise<void>\n}\n\nconst canQueue = (payload: Payload): boolean => typeof payload.jobs?.queue === 'function'\n\nconst deadline = (ms: number): Promise<void> =>\n\tnew Promise((resolve) => {\n\t\tconst timer = setTimeout(resolve, ms)\n\t\ttimer.unref?.()\n\t})\n\n/** Guard a caller-supplied callback so its throw can never break the dispatch flow. */\nconst guarded = async (\n\tpayload: Payload,\n\tlabel: string,\n\tcallback: (() => void | Promise<void>) | undefined\n): Promise<void> => {\n\ttry {\n\t\tawait callback?.()\n\t} catch (error) {\n\t\tpayload.logger?.error(\n\t\t\t`@10x-media/form-builder: ${label} callback threw: ${\n\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t}`\n\t\t)\n\t}\n}\n\n/**\n * Dispatch a submission's post-submit actions without throwing and without blocking the response on slow\n * action work. With no actions, returns immediately. When a job runner is present, enqueues the native\n * `form-builder-actions` task and returns (action work happens out of band). Otherwise runs the actions\n * inline but bounded by a deadline so a missing worker still delivers without hanging the request; any\n * error is swallowed (logged via `payload.logger`). Never rejects; the return reports whether an\n * essential pass failed, so the caller can withhold success signals (the created event, the 201).\n */\nexport const dispatchActions = async (\n\targs: DispatchActionsArgs\n): Promise<{ essentialFailed: boolean }> => {\n\tconst { payload, registry, req, formId, submissionId } = args\n\tconst actions = args.actions ?? []\n\tconst hasEssential = actions.some((instance) => isEssentialAction(registry, instance))\n\tconst rest = hasEssential\n\t\t? actions.filter((instance) => !isEssentialAction(registry, instance))\n\t\t: actions\n\n\t// The pass an essential failure skips: rest actions plus, for a `persistSubmissions: false`\n\t// form, the prune completion. Shared by the normal flow below and by late reconciliation, where\n\t// a breached-then-successful essential pass finishes what the timeout interrupted. `withReq`\n\t// only on the request path: by the time a late settlement runs, the request transaction is gone.\n\tconst runClosingPass = async (withReq: PayloadRequest | undefined): Promise<void> => {\n\t\tconst subset = hasEssential ? ({ subset: 'rest' } as const) : {}\n\t\tif (args.hasRunner && canQueue(payload)) {\n\t\t\ttry {\n\t\t\t\tawait payload.jobs.queue({\n\t\t\t\t\ttask: ACTIONS_TASK_SLUG,\n\t\t\t\t\tinput: { formId: String(formId), submissionId: String(submissionId), ...subset },\n\t\t\t\t\t...(withReq ? { req: withReq } : {}),\n\t\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 enqueue actions for submission ${String(submissionId)}: ${\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\t\treturn\n\t\t}\n\t\tconst ms = args.deadlineMs ?? INLINE_DISPATCH_DEADLINE_MS\n\t\t// Guard the action arm itself (not just the race) so a rejection AFTER the deadline wins is never an unhandled rejection.\n\t\tconst work = runActionsForSubmission({\n\t\t\tinput: { formId, submissionId, ...subset },\n\t\t\tregistry,\n\t\t\tpayload,\n\t\t\treq: withReq,\n\t\t\trichText: args.richText,\n\t\t}).catch((error) => {\n\t\t\tpayload.logger?.error(\n\t\t\t\t`@10x-media/form-builder: inline action dispatch for submission ${String(submissionId)} threw: ${\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\t// Only the request path races the deadline; a late pass has no response waiting on it.\n\t\tawait (withReq ? Promise.race([work, deadline(ms)]) : work)\n\t}\n\n\t// Essential actions run first, inline and awaited, never queued: their failure is the\n\t// submission's failure, which the submit endpoint turns into an error response via the context\n\t// flag. On failure the remaining actions are skipped and so is the pruning completion, so a\n\t// `persistSubmissions: false` form keeps the row: the provider never received it, and the\n\t// stored copy is the only record of what the visitor sent.\n\tif (hasEssential) {\n\t\tconst ms = args.deadlineMs ?? INLINE_DISPATCH_DEADLINE_MS\n\t\tconst work = runActionsForSubmission({\n\t\t\tinput: { formId, submissionId, subset: 'essential' },\n\t\t\tregistry,\n\t\t\tpayload,\n\t\t\treq,\n\t\t\trichText: args.richText,\n\t\t}).then(\n\t\t\t(results) => ({ timedOut: false, failed: results.some((result) => !result.ok) }),\n\t\t\t(error) => {\n\t\t\t\tpayload.logger?.error(\n\t\t\t\t\t`@10x-media/form-builder: essential action pass for submission ${String(submissionId)} threw: ${\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\treturn { timedOut: false, failed: true }\n\t\t\t}\n\t\t)\n\t\tconst outcome = await Promise.race([\n\t\t\twork,\n\t\t\tdeadline(ms).then(() => ({ timedOut: true, failed: true })),\n\t\t])\n\t\tif (outcome.failed) {\n\t\t\tconst contextKey = outcome.timedOut\n\t\t\t\t? ESSENTIAL_ACTION_UNCERTAIN_CONTEXT_KEY\n\t\t\t\t: ESSENTIAL_ACTION_FAILED_CONTEXT_KEY\n\t\t\tif (req) {\n\t\t\t\treq.context = { ...(req.context ?? {}), [contextKey]: true }\n\t\t\t}\n\t\t\tawait guarded(payload, 'onEssentialFailed', () =>\n\t\t\t\targs.onEssentialFailed?.({ timedOut: outcome.timedOut })\n\t\t\t)\n\t\t\tif (outcome.timedOut) {\n\t\t\t\tpayload.logger?.error(\n\t\t\t\t\t`@10x-media/form-builder: essential action pass for submission ${String(submissionId)} outlived its ${ms}ms deadline; reporting the submission uncertain (the work may still complete)`\n\t\t\t\t)\n\t\t\t\t// The breached work is still running: when it settles, record the real outcome instead\n\t\t\t\t// of dropping it. On late success the closing pass the timeout skipped runs FIRST,\n\t\t\t\t// then the caller reconciles (clears the stamp, emits its success signals), matching\n\t\t\t\t// the on-time order and keeping the uncertain stamp truthful if the process dies\n\t\t\t\t// mid-reconciliation: a half-finished recovery stays flagged, never cleared.\n\t\t\t\tvoid work.then(async (late) => {\n\t\t\t\t\tpayload.logger?.[late.failed ? 'error' : 'info']?.(\n\t\t\t\t\t\t`@10x-media/form-builder: essential action pass for submission ${String(submissionId)} settled after its deadline: ${\n\t\t\t\t\t\t\tlate.failed ? 'failed' : 'succeeded'\n\t\t\t\t\t\t}`\n\t\t\t\t\t)\n\t\t\t\t\tif (!late.failed && (rest.length > 0 || args.persistSubmissions === false)) {\n\t\t\t\t\t\tawait runClosingPass(undefined)\n\t\t\t\t\t}\n\t\t\t\t\tawait guarded(payload, 'onEssentialSettled', () =>\n\t\t\t\t\t\targs.onEssentialSettled?.({ ok: !late.failed })\n\t\t\t\t\t)\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn { essentialFailed: true }\n\t\t}\n\t}\n\n\t// Nothing to run and nothing to prune: skip. An action-less form that opted out of persistence still\n\t// runs the completion below, so `runActionsForSubmission` can delete the row out of band.\n\tif (rest.length === 0 && args.persistSubmissions !== false) {\n\t\treturn { essentialFailed: false }\n\t}\n\n\t// With an essential pass already run, the closing pass covers only the rest (subset threading\n\t// keeps the queued task from re-running the essential actions).\n\tawait runClosingPass(req)\n\treturn { essentialFailed: false }\n}\n"],"mappings":"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"}
|
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
* because both the dispatcher and the submit endpoint need it across an import cycle.
|
|
8
8
|
*/
|
|
9
9
|
const ESSENTIAL_ACTION_FAILED_CONTEXT_KEY = "formBuilderEssentialActionFailed";
|
|
10
|
+
/**
|
|
11
|
+
* Set instead of the failed key when the essential pass outlived its deadline: the work may still
|
|
12
|
+
* complete, so the visitor is told the outcome is unknown (504), not that it failed (502). The two
|
|
13
|
+
* are different facts and prompt different visitor behavior (wait vs retry).
|
|
14
|
+
*/
|
|
15
|
+
const ESSENTIAL_ACTION_UNCERTAIN_CONTEXT_KEY = "formBuilderEssentialActionUncertain";
|
|
10
16
|
//#endregion
|
|
11
|
-
export { ESSENTIAL_ACTION_FAILED_CONTEXT_KEY };
|
|
17
|
+
export { ESSENTIAL_ACTION_FAILED_CONTEXT_KEY, ESSENTIAL_ACTION_UNCERTAIN_CONTEXT_KEY };
|
|
12
18
|
|
|
13
19
|
//# sourceMappingURL=dispatchContext.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatchContext.js","names":[],"sources":["../../src/actions/dispatchContext.ts"],"sourcesContent":["/**\n * `req.context` key the dispatcher sets to `true` when an essential action failed (or outlived its\n * deadline) for the submission this request created. The submit endpoint reads it to turn the 201\n * into an error response; the submission itself stays stored, prune included, so a failed handoff\n * to a provider never deletes the only copy of what the visitor sent. A leaf module (no imports)\n * because both the dispatcher and the submit endpoint need it across an import cycle.\n */\nexport const ESSENTIAL_ACTION_FAILED_CONTEXT_KEY = 'formBuilderEssentialActionFailed'\n"],"mappings":";;;;;;;;AAOA,MAAa,sCAAsC"}
|
|
1
|
+
{"version":3,"file":"dispatchContext.js","names":[],"sources":["../../src/actions/dispatchContext.ts"],"sourcesContent":["/**\n * `req.context` key the dispatcher sets to `true` when an essential action failed (or outlived its\n * deadline) for the submission this request created. The submit endpoint reads it to turn the 201\n * into an error response; the submission itself stays stored, prune included, so a failed handoff\n * to a provider never deletes the only copy of what the visitor sent. A leaf module (no imports)\n * because both the dispatcher and the submit endpoint need it across an import cycle.\n */\nexport const ESSENTIAL_ACTION_FAILED_CONTEXT_KEY = 'formBuilderEssentialActionFailed'\n\n/**\n * Set instead of the failed key when the essential pass outlived its deadline: the work may still\n * complete, so the visitor is told the outcome is unknown (504), not that it failed (502). The two\n * are different facts and prompt different visitor behavior (wait vs retry).\n */\nexport const ESSENTIAL_ACTION_UNCERTAIN_CONTEXT_KEY = 'formBuilderEssentialActionUncertain'\n"],"mappings":";;;;;;;;AAOA,MAAa,sCAAsC;;;;;;AAOnD,MAAa,yCAAyC"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
//#region src/actions/runActions.d.ts
|
|
2
|
+
/** `detail` carries whatever structured context the action attached to its throw (see `ActionError`). */
|
|
2
3
|
type ActionResult = {
|
|
3
4
|
type: string;
|
|
4
5
|
ok: boolean;
|
|
5
6
|
error?: string;
|
|
7
|
+
detail?: unknown;
|
|
6
8
|
};
|
|
7
9
|
//#endregion
|
|
8
10
|
export { ActionResult };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { makeRenderBody } from "./body/serializeBody.js";
|
|
2
2
|
//#region src/actions/runActions.ts
|
|
3
|
+
const detailOf = (error) => error != null && typeof error === "object" && "detail" in error ? error.detail : void 0;
|
|
3
4
|
/**
|
|
4
5
|
* Run each configured action, isolating failures: a throwing action is captured as a failed result
|
|
5
6
|
* and never breaks the others or the caller.
|
|
@@ -28,10 +29,12 @@ const runActions = async (args) => {
|
|
|
28
29
|
ok: true
|
|
29
30
|
});
|
|
30
31
|
} catch (error) {
|
|
32
|
+
const detail = detailOf(error);
|
|
31
33
|
results.push({
|
|
32
34
|
type: instance.blockType,
|
|
33
35
|
ok: false,
|
|
34
|
-
error: error instanceof Error ? error.message : String(error)
|
|
36
|
+
error: error instanceof Error ? error.message : String(error),
|
|
37
|
+
...detail !== void 0 ? { detail } : {}
|
|
35
38
|
});
|
|
36
39
|
}
|
|
37
40
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runActions.js","names":[],"sources":["../../src/actions/runActions.ts"],"sourcesContent":["import type { RichTextBodyOption } from './body/serializeBody'\nimport { makeRenderBody } from './body/serializeBody'\nimport type { ActionRunArgs } from './defineAction'\nimport type { ActionRegistry } from './registry'\n\n/** A stored action instance from the form's `actions` blocks array. */\nexport type ActionInstance = { blockType: string; [key: string]: unknown }\n\nexport type ActionResult = { type: string; ok: boolean; error?: string }\n\nexport type RunActionsArgs = Omit<ActionRunArgs, 'config' | 'renderBody'> & {\n\tactions: ActionInstance[]\n\tregistry: ActionRegistry\n\trichText?: RichTextBodyOption\n}\n\n/**\n * Run each configured action, isolating failures: a throwing action is captured as a failed result\n * and never breaks the others or the caller.\n */\nexport const runActions = async (args: RunActionsArgs): Promise<ActionResult[]> => {\n\tconst { actions, registry, richText, ...ctx } = args\n\tconst renderBody = makeRenderBody({\n\t\tvalues: ctx.values,\n\t\tdescriptors: ctx.descriptors,\n\t\tform: ctx.form,\n\t\treq: ctx.req,\n\t\trichText,\n\t})\n\tconst results: ActionResult[] = []\n\tfor (const instance of actions) {\n\t\tconst definition = registry.get(instance.blockType)\n\t\tif (!definition) {\n\t\t\tcontinue\n\t\t}\n\t\ttry {\n\t\t\tawait definition.run({ ...ctx, renderBody, config: instance })\n\t\t\tresults.push({ type: instance.blockType, ok: true })\n\t\t} catch (error) {\n\t\t\tresults.push({\n\t\t\t\ttype: instance.blockType,\n\t\t\t\tok: false,\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t})\n\t\t}\n\t}\n\treturn results\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"runActions.js","names":[],"sources":["../../src/actions/runActions.ts"],"sourcesContent":["import type { RichTextBodyOption } from './body/serializeBody'\nimport { makeRenderBody } from './body/serializeBody'\nimport type { ActionRunArgs } from './defineAction'\nimport type { ActionRegistry } from './registry'\n\n/** A stored action instance from the form's `actions` blocks array. */\nexport type ActionInstance = { blockType: string; [key: string]: unknown }\n\n/** `detail` carries whatever structured context the action attached to its throw (see `ActionError`). */\nexport type ActionResult = { type: string; ok: boolean; error?: string; detail?: unknown }\n\nconst detailOf = (error: unknown): unknown =>\n\terror != null && typeof error === 'object' && 'detail' in error\n\t\t? (error as { detail?: unknown }).detail\n\t\t: undefined\n\nexport type RunActionsArgs = Omit<ActionRunArgs, 'config' | 'renderBody'> & {\n\tactions: ActionInstance[]\n\tregistry: ActionRegistry\n\trichText?: RichTextBodyOption\n}\n\n/**\n * Run each configured action, isolating failures: a throwing action is captured as a failed result\n * and never breaks the others or the caller.\n */\nexport const runActions = async (args: RunActionsArgs): Promise<ActionResult[]> => {\n\tconst { actions, registry, richText, ...ctx } = args\n\tconst renderBody = makeRenderBody({\n\t\tvalues: ctx.values,\n\t\tdescriptors: ctx.descriptors,\n\t\tform: ctx.form,\n\t\treq: ctx.req,\n\t\trichText,\n\t})\n\tconst results: ActionResult[] = []\n\tfor (const instance of actions) {\n\t\tconst definition = registry.get(instance.blockType)\n\t\tif (!definition) {\n\t\t\tcontinue\n\t\t}\n\t\ttry {\n\t\t\tawait definition.run({ ...ctx, renderBody, config: instance })\n\t\t\tresults.push({ type: instance.blockType, ok: true })\n\t\t} catch (error) {\n\t\t\tconst detail = detailOf(error)\n\t\t\tresults.push({\n\t\t\t\ttype: instance.blockType,\n\t\t\t\tok: false,\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t\t...(detail !== undefined ? { detail } : {}),\n\t\t\t})\n\t\t}\n\t}\n\treturn results\n}\n"],"mappings":";;AAWA,MAAM,YAAY,UACjB,SAAS,QAAQ,OAAO,UAAU,YAAY,YAAY,QACtD,MAA+B,SAChC,KAAA;;;;;AAYJ,MAAa,aAAa,OAAO,SAAkD;CAClF,MAAM,EAAE,SAAS,UAAU,UAAU,GAAG,QAAQ;CAChD,MAAM,aAAa,eAAe;EACjC,QAAQ,IAAI;EACZ,aAAa,IAAI;EACjB,MAAM,IAAI;EACV,KAAK,IAAI;EACT;CACD,CAAC;CACD,MAAM,UAA0B,CAAC;CACjC,KAAK,MAAM,YAAY,SAAS;EAC/B,MAAM,aAAa,SAAS,IAAI,SAAS,SAAS;EAClD,IAAI,CAAC,YACJ;EAED,IAAI;GACH,MAAM,WAAW,IAAI;IAAE,GAAG;IAAK;IAAY,QAAQ;GAAS,CAAC;GAC7D,QAAQ,KAAK;IAAE,MAAM,SAAS;IAAW,IAAI;GAAK,CAAC;EACpD,SAAS,OAAO;GACf,MAAM,SAAS,SAAS,KAAK;GAC7B,QAAQ,KAAK;IACZ,MAAM,SAAS;IACf,IAAI;IACJ,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IAC5D,GAAI,WAAW,KAAA,IAAY,EAAE,OAAO,IAAI,CAAC;GAC1C,CAAC;EACF;CACD;CACA,OAAO;AACR"}
|
package/dist/actions/task.js
CHANGED
|
@@ -67,7 +67,11 @@ const runActionsForSubmission = async (args) => {
|
|
|
67
67
|
locale,
|
|
68
68
|
t
|
|
69
69
|
});
|
|
70
|
-
for (const result of results) if (!result.ok)
|
|
70
|
+
for (const result of results) if (!result.ok) {
|
|
71
|
+
const message = `@10x-media/form-builder: action "${result.type}" failed for submission ${String(submission.id)}: ${result.error ?? "unknown error"}`;
|
|
72
|
+
if (result.detail !== void 0) payload.logger?.error({ detail: result.detail }, message);
|
|
73
|
+
else payload.logger?.error(message);
|
|
74
|
+
}
|
|
71
75
|
if (subset !== "essential" && form.persistSubmissions === false) await payload.delete({
|
|
72
76
|
collection: FORM_SUBMISSIONS_SLUG,
|
|
73
77
|
id: submission.id,
|
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, TypedLocale } from 'payload'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport { FORMS_SLUG } from '../collections/forms'\nimport type { FormContextReference } from '../context/formContext'\nimport type { Translate } from '../fields/types'\nimport type { SubmissionDescriptor, SubmissionValue } from '../submissions/types'\nimport { asFieldTranslate } from '../translations/server'\nimport type { RichTextBodyOption } from './body/serializeBody'\nimport { type ActionRegistry, isEssentialAction } from './registry'\nimport type { ActionInstance, ActionResult } from './runActions'\nimport { runActions } from './runActions'\n\nexport const ACTIONS_TASK_SLUG = 'form-builder-actions'\n\n/** Input the dispatch path enqueues; the handler re-loads everything else from the DB. */\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: FORM_SUBMISSIONS_SLUG,\n\t\t\tid: input.submissionId,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t})\n\t\t.catch(() => null)\n\tif (!submission) {\n\t\treturn []\n\t}\n\n\t// The submission's own stored locale (set from req.locale at submit) is authoritative, so the form\n\t// is loaded at it. A localized action config, notably the emailTeam `to`, then resolves to the\n\t// submission's locale even on the queued path, where the job runner's req may carry a different\n\t// (or no) locale than the visitor who submitted.\n\tconst locale = typeof submission.locale === 'string' ? submission.locale : (req?.locale ?? 'en')\n\n\tconst form = await payload\n\t\t.findByID({\n\t\t\tcollection: FORMS_SLUG,\n\t\t\tid: input.formId,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\t// Cast: the stored locale is a plain string; a host's concrete locale union is unknowable from\n\t\t\t// the plugin, and an unrecognized code just falls back on read, so this narrows (zero runtime\n\t\t\t// delta) to satisfy a host whose `findByID` locale is a real union.\n\t\t\tlocale: locale as TypedLocale,\n\t\t\treq,\n\t\t})\n\t\t.catch(() => null)\n\tif (!form) {\n\t\treturn []\n\t}\n\n\tconst t: Translate = asFieldTranslate(req?.i18n?.t ?? ((key: string) => key))\n\n\tconst 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\tform: { id: form.id, title: typeof form.title === 'string' ? form.title : undefined },\n\t\tsubmissionId: submission.id,\n\t\tvalues: asValues(submission.values),\n\t\tdescriptors: asDescriptors(submission.descriptors),\n\t\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\
|
|
1
|
+
{"version":3,"file":"task.js","names":[],"sources":["../../src/actions/task.ts"],"sourcesContent":["import type { Config, Payload, PayloadRequest, TaskConfig, TypedLocale } from 'payload'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport { FORMS_SLUG } from '../collections/forms'\nimport type { FormContextReference } from '../context/formContext'\nimport type { Translate } from '../fields/types'\nimport type { SubmissionDescriptor, SubmissionValue } from '../submissions/types'\nimport { asFieldTranslate } from '../translations/server'\nimport type { RichTextBodyOption } from './body/serializeBody'\nimport { type ActionRegistry, isEssentialAction } from './registry'\nimport type { ActionInstance, ActionResult } from './runActions'\nimport { runActions } from './runActions'\n\nexport const ACTIONS_TASK_SLUG = 'form-builder-actions'\n\n/** Input the dispatch path enqueues; the handler re-loads everything else from the DB. */\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: FORM_SUBMISSIONS_SLUG,\n\t\t\tid: input.submissionId,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t})\n\t\t.catch(() => null)\n\tif (!submission) {\n\t\treturn []\n\t}\n\n\t// The submission's own stored locale (set from req.locale at submit) is authoritative, so the form\n\t// is loaded at it. A localized action config, notably the emailTeam `to`, then resolves to the\n\t// submission's locale even on the queued path, where the job runner's req may carry a different\n\t// (or no) locale than the visitor who submitted.\n\tconst locale = typeof submission.locale === 'string' ? submission.locale : (req?.locale ?? 'en')\n\n\tconst form = await payload\n\t\t.findByID({\n\t\t\tcollection: FORMS_SLUG,\n\t\t\tid: input.formId,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\t// Cast: the stored locale is a plain string; a host's concrete locale union is unknowable from\n\t\t\t// the plugin, and an unrecognized code just falls back on read, so this narrows (zero runtime\n\t\t\t// delta) to satisfy a host whose `findByID` locale is a real union.\n\t\t\tlocale: locale as TypedLocale,\n\t\t\treq,\n\t\t})\n\t\t.catch(() => null)\n\tif (!form) {\n\t\treturn []\n\t}\n\n\tconst t: Translate = asFieldTranslate(req?.i18n?.t ?? ((key: string) => key))\n\n\tconst 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\tform: { id: form.id, title: typeof form.title === 'string' ? form.title : undefined },\n\t\tsubmissionId: submission.id,\n\t\tvalues: asValues(submission.values),\n\t\tdescriptors: asDescriptors(submission.descriptors),\n\t\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({ collection: FORM_SUBMISSIONS_SLUG, id: submission.id, overrideAccess: true, req })\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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"}
|
|
@@ -35,6 +35,28 @@ const makeAfterChange = (args) => async ({ doc, operation, req }) => {
|
|
|
35
35
|
overrideAccess: true,
|
|
36
36
|
req
|
|
37
37
|
}).catch(() => null);
|
|
38
|
+
const stamp = async (data, withReq) => {
|
|
39
|
+
await payload.update.bind(payload)({
|
|
40
|
+
collection: FORM_SUBMISSIONS_SLUG,
|
|
41
|
+
id: doc.id,
|
|
42
|
+
data,
|
|
43
|
+
depth: 0,
|
|
44
|
+
overrideAccess: true,
|
|
45
|
+
...withReq ? { req: withReq } : {}
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const emitCreated = async () => {
|
|
49
|
+
try {
|
|
50
|
+
await resolveEventSink(args.events).emit({
|
|
51
|
+
type: "submission.created",
|
|
52
|
+
formId: String(formId),
|
|
53
|
+
submissionId: String(doc.id),
|
|
54
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
payload.logger?.error(`@10x-media/form-builder: submission.created sink threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
38
60
|
const { essentialFailed } = await dispatchActions({
|
|
39
61
|
actions: form?.actions ?? null,
|
|
40
62
|
formId,
|
|
@@ -44,29 +66,21 @@ const makeAfterChange = (args) => async ({ doc, operation, req }) => {
|
|
|
44
66
|
req,
|
|
45
67
|
hasRunner: args.hasRunner,
|
|
46
68
|
persistSubmissions: form?.persistSubmissions,
|
|
47
|
-
richText: args.richText
|
|
69
|
+
richText: args.richText,
|
|
70
|
+
deadlineMs: args.dispatchDeadlineMs,
|
|
71
|
+
onEssentialFailed: ({ timedOut }) => stamp(timedOut ? { actionUncertain: true } : { actionFailed: true }, req),
|
|
72
|
+
onEssentialSettled: async ({ ok }) => {
|
|
73
|
+
if (ok) {
|
|
74
|
+
await stamp({ actionUncertain: false }).catch(() => {});
|
|
75
|
+
await emitCreated();
|
|
76
|
+
} else await stamp({
|
|
77
|
+
actionFailed: true,
|
|
78
|
+
actionUncertain: false
|
|
79
|
+
});
|
|
80
|
+
}
|
|
48
81
|
});
|
|
49
|
-
if (essentialFailed)
|
|
50
|
-
|
|
51
|
-
collection: FORM_SUBMISSIONS_SLUG,
|
|
52
|
-
id: doc.id,
|
|
53
|
-
data: { actionFailed: true },
|
|
54
|
-
depth: 0,
|
|
55
|
-
overrideAccess: true,
|
|
56
|
-
req
|
|
57
|
-
});
|
|
58
|
-
return doc;
|
|
59
|
-
}
|
|
60
|
-
try {
|
|
61
|
-
await resolveEventSink(args.events).emit({
|
|
62
|
-
type: "submission.created",
|
|
63
|
-
formId: String(formId),
|
|
64
|
-
submissionId: String(doc.id),
|
|
65
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
66
|
-
});
|
|
67
|
-
} catch (error) {
|
|
68
|
-
payload.logger?.error(`@10x-media/form-builder: submission.created sink threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
69
|
-
}
|
|
82
|
+
if (essentialFailed) return doc;
|
|
83
|
+
await emitCreated();
|
|
70
84
|
} catch (error) {
|
|
71
85
|
payload.logger?.error(`@10x-media/form-builder: afterChange dispatch failed for submission ${String(doc.id)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
72
86
|
}
|
|
@@ -117,7 +131,7 @@ const makeVotedCookieHook = (args) => {
|
|
|
117
131
|
return doc;
|
|
118
132
|
};
|
|
119
133
|
};
|
|
120
|
-
const buildSubmissionsCollection = ({ registry, ruleRegistry, calcSources, calcFunctions, consentSources, consentSnapshot, actionRegistry = /* @__PURE__ */ new Map(), events, hasRunner = false, 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, overrides }) => {
|
|
121
135
|
const defaultFields = [
|
|
122
136
|
{
|
|
123
137
|
name: "form",
|
|
@@ -156,6 +170,20 @@ const buildSubmissionsCollection = ({ registry, ruleRegistry, calcSources, calcF
|
|
|
156
170
|
condition: (data) => data?.actionFailed === true
|
|
157
171
|
}
|
|
158
172
|
},
|
|
173
|
+
{
|
|
174
|
+
name: "actionUncertain",
|
|
175
|
+
type: "checkbox",
|
|
176
|
+
index: true,
|
|
177
|
+
label: labelForKey(keys.submissionActionUncertainFlag),
|
|
178
|
+
access: {
|
|
179
|
+
create: () => false,
|
|
180
|
+
update: () => false
|
|
181
|
+
},
|
|
182
|
+
admin: {
|
|
183
|
+
readOnly: true,
|
|
184
|
+
condition: (data) => data?.actionUncertain === true
|
|
185
|
+
}
|
|
186
|
+
},
|
|
159
187
|
{
|
|
160
188
|
name: "answers",
|
|
161
189
|
type: "ui",
|
|
@@ -251,7 +279,8 @@ const buildSubmissionsCollection = ({ registry, ruleRegistry, calcSources, calcF
|
|
|
251
279
|
actionRegistry,
|
|
252
280
|
events,
|
|
253
281
|
hasRunner,
|
|
254
|
-
richText
|
|
282
|
+
richText,
|
|
283
|
+
dispatchDeadlineMs
|
|
255
284
|
}),
|
|
256
285
|
makeVotedCookieHook({ votedCookie }),
|
|
257
286
|
...overrides?.hooks?.afterChange ?? []
|