@nextlyhq/plugin-form-builder 0.0.2-alpha.60 → 0.0.2-alpha.62
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/dist/admin/index.d.ts +18 -2
- package/dist/admin/index.js +1796 -1306
- package/dist/admin/index.js.map +1 -1
- package/dist/{chunk-XCICDELK.js → chunk-H5X3FKOZ.js} +131 -1
- package/dist/chunk-H5X3FKOZ.js.map +1 -0
- package/dist/index.d.ts +3 -18
- package/dist/index.js +601 -166
- package/dist/index.js.map +1 -1
- package/dist/{types-DuEFdLNU.d.ts → types-hLfLe81K.d.ts} +122 -75
- package/package.json +11 -11
- package/dist/chunk-XCICDELK.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -2,9 +2,11 @@ import {
|
|
|
2
2
|
BUILT_IN_FORM_FIELD_TYPES,
|
|
3
3
|
DEFAULT_FORM_SETTINGS,
|
|
4
4
|
DEFAULT_NOTIFICATION_SETTINGS,
|
|
5
|
+
ENFORCED_VALIDATION_RULES,
|
|
5
6
|
applyFieldDefaults,
|
|
6
7
|
applyFormDefaults,
|
|
7
8
|
createFormConfig,
|
|
9
|
+
documentReachability,
|
|
8
10
|
downloadFile,
|
|
9
11
|
evaluateConditions,
|
|
10
12
|
evaluateSingleCondition,
|
|
@@ -24,9 +26,12 @@ import {
|
|
|
24
26
|
isTextFormField,
|
|
25
27
|
isValidComparisonOperator,
|
|
26
28
|
normalizeFormSettings,
|
|
29
|
+
parseFieldRef,
|
|
30
|
+
parseRedirectReference,
|
|
31
|
+
pickedDocumentField,
|
|
27
32
|
pluralize,
|
|
28
33
|
toTitleCase
|
|
29
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-H5X3FKOZ.js";
|
|
30
35
|
|
|
31
36
|
// src/plugin.ts
|
|
32
37
|
import { createRequire } from "module";
|
|
@@ -39,18 +44,284 @@ import {
|
|
|
39
44
|
textarea,
|
|
40
45
|
select,
|
|
41
46
|
checkbox,
|
|
47
|
+
date,
|
|
42
48
|
group,
|
|
43
49
|
json,
|
|
44
50
|
relationship
|
|
45
51
|
} from "nextly";
|
|
52
|
+
|
|
53
|
+
// src/utils/redirect-target.ts
|
|
54
|
+
import { previewUrlFromTemplate } from "nextly/config";
|
|
55
|
+
var DEFAULT_REDIRECT_PATTERN = "/{slug}";
|
|
56
|
+
function normalizeRedirectRelationships(option2) {
|
|
57
|
+
if (!option2) return {};
|
|
58
|
+
const entries = Array.isArray(option2) ? option2.map((collection) => [collection, DEFAULT_REDIRECT_PATTERN]) : Object.entries(option2);
|
|
59
|
+
return Object.fromEntries(
|
|
60
|
+
entries.filter(
|
|
61
|
+
([, pattern]) => typeof pattern !== "string" || pattern.trim()
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
function applyRedirectPattern(pattern, document) {
|
|
66
|
+
if (typeof pattern === "function") {
|
|
67
|
+
const produced = pattern(document);
|
|
68
|
+
return produced ? produced : void 0;
|
|
69
|
+
}
|
|
70
|
+
return previewUrlFromTemplate(pattern)(document) ?? void 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/collections/access-defaults.ts
|
|
74
|
+
var authenticated = ({ user }) => !!user;
|
|
75
|
+
var administrative = ({ roles }) => roles.includes("admin") || roles.includes("super-admin");
|
|
76
|
+
function accessWithDefaults(overrides, defaults = {}) {
|
|
77
|
+
return {
|
|
78
|
+
read: overrides?.read ?? defaults.read ?? authenticated,
|
|
79
|
+
create: overrides?.create ?? defaults.create ?? authenticated,
|
|
80
|
+
update: overrides?.update ?? authenticated,
|
|
81
|
+
// Deleting is administrative in both collections; the DB permission layer
|
|
82
|
+
// remains the backstop underneath it.
|
|
83
|
+
delete: overrides?.delete ?? administrative
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/collections/forms.ts
|
|
88
|
+
function withHostHooks(pluginHooks, hostHooks, trailing = {}) {
|
|
89
|
+
const host = { ...hostHooks };
|
|
90
|
+
const asList = (value) => Array.isArray(value) ? value : value === void 0 ? [] : [value];
|
|
91
|
+
const merged = { ...host };
|
|
92
|
+
for (const [phase, handlers] of Object.entries(
|
|
93
|
+
pluginHooks
|
|
94
|
+
)) {
|
|
95
|
+
merged[phase] = [...asList(handlers), ...asList(host[phase])];
|
|
96
|
+
}
|
|
97
|
+
for (const [phase, handlers] of Object.entries(trailing)) {
|
|
98
|
+
merged[phase] = [...asList(merged[phase]), ...asList(handlers)];
|
|
99
|
+
}
|
|
100
|
+
return merged;
|
|
101
|
+
}
|
|
102
|
+
function documentRedirectSettings(settings) {
|
|
103
|
+
if (typeof settings !== "object" || settings === null) return null;
|
|
104
|
+
const record = settings;
|
|
105
|
+
const field = pickedDocumentField(record);
|
|
106
|
+
return field ? { settings: record, field } : null;
|
|
107
|
+
}
|
|
108
|
+
function pageRedirectSettings(data, operation) {
|
|
109
|
+
if (!data) return null;
|
|
110
|
+
if (operation !== "create" && data.settings === void 0) return null;
|
|
111
|
+
return documentRedirectSettings(data.settings);
|
|
112
|
+
}
|
|
113
|
+
function formAcceptsSubmissions(data, originalData) {
|
|
114
|
+
return (data?.status ?? originalData?.status) === "published";
|
|
115
|
+
}
|
|
116
|
+
function storedSettings(value) {
|
|
117
|
+
if (typeof value === "string") {
|
|
118
|
+
try {
|
|
119
|
+
return documentRedirectSettings(JSON.parse(value));
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return documentRedirectSettings(value);
|
|
125
|
+
}
|
|
126
|
+
function storedRedirectReference(originalData, patterns) {
|
|
127
|
+
const picked = storedSettings(originalData?.settings);
|
|
128
|
+
if (!picked) return null;
|
|
129
|
+
const collections = Object.keys(patterns);
|
|
130
|
+
const reference = parseRedirectReference(
|
|
131
|
+
picked.settings[picked.field],
|
|
132
|
+
collections
|
|
133
|
+
);
|
|
134
|
+
if (!reference) return null;
|
|
135
|
+
return {
|
|
136
|
+
reference,
|
|
137
|
+
field: picked.field,
|
|
138
|
+
configured: collections.includes(reference.collection)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function normalizeStoredReference(settings, field, reference) {
|
|
142
|
+
const stored = settings[field];
|
|
143
|
+
if (typeof stored === "string") {
|
|
144
|
+
if (stored !== reference.id) settings[field] = reference.id;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (typeof stored !== "object" || stored === null || Array.isArray(stored)) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const record = stored;
|
|
151
|
+
if (record.relationTo !== reference.collection) {
|
|
152
|
+
settings[field] = { ...record, relationTo: reference.collection };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function namedRedirectReference(data, operation, patterns) {
|
|
156
|
+
const picked = pageRedirectSettings(data, operation);
|
|
157
|
+
if (!picked) return null;
|
|
158
|
+
const { settings, field } = picked;
|
|
159
|
+
const collections = Object.keys(patterns);
|
|
160
|
+
const reference = parseRedirectReference(settings[field], collections);
|
|
161
|
+
if (reference) normalizeStoredReference(settings, field, reference);
|
|
162
|
+
return {
|
|
163
|
+
settings,
|
|
164
|
+
field,
|
|
165
|
+
reference,
|
|
166
|
+
configured: reference !== null && collections.includes(reference.collection)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function wouldStrandVisitors(data, originalData, target, targetCollectionIsLocalized) {
|
|
170
|
+
return formAcceptsSubmissions(data, originalData) && documentReachability(target, targetCollectionIsLocalized) === "unreachable";
|
|
171
|
+
}
|
|
172
|
+
function publishesForm(data, originalData) {
|
|
173
|
+
if (data?.status !== "published") return false;
|
|
174
|
+
return originalData?.status !== "published";
|
|
175
|
+
}
|
|
176
|
+
function sameTarget(named, stored) {
|
|
177
|
+
if (!named.reference || !stored) return false;
|
|
178
|
+
return stored.field === named.field && stored.reference.collection === named.reference.collection && stored.reference.id === named.reference.id;
|
|
179
|
+
}
|
|
180
|
+
function judgeNamedReference(named, stored) {
|
|
181
|
+
if (sameTarget(named, stored) && named.reference) {
|
|
182
|
+
return {
|
|
183
|
+
reference: named.reference,
|
|
184
|
+
field: named.field,
|
|
185
|
+
chosen: false,
|
|
186
|
+
configured: named.configured
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (!named.reference || !named.configured) {
|
|
190
|
+
throw redirectRefusal(
|
|
191
|
+
"Choose a page to redirect to, or pick a different confirmation.",
|
|
192
|
+
named.field
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
reference: named.reference,
|
|
197
|
+
field: named.field,
|
|
198
|
+
chosen: true,
|
|
199
|
+
configured: true
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function redirectReferenceForWrite(context, patterns) {
|
|
203
|
+
const stored = storedRedirectReference(
|
|
204
|
+
context.originalData,
|
|
205
|
+
patterns
|
|
206
|
+
);
|
|
207
|
+
const named = namedRedirectReference(
|
|
208
|
+
context.data,
|
|
209
|
+
context.operation,
|
|
210
|
+
patterns
|
|
211
|
+
);
|
|
212
|
+
if (named) return judgeNamedReference(named, stored);
|
|
213
|
+
const data = context.data;
|
|
214
|
+
if (data?.settings !== void 0) return null;
|
|
215
|
+
return stored ? { ...stored, chosen: false } : null;
|
|
216
|
+
}
|
|
217
|
+
function assertPatternBuildsUrl(patterns, reference, field, target) {
|
|
218
|
+
let url2;
|
|
219
|
+
try {
|
|
220
|
+
url2 = applyRedirectPattern(patterns[reference.collection], target);
|
|
221
|
+
} catch {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (url2) return;
|
|
225
|
+
throw redirectRefusal(
|
|
226
|
+
`No URL could be built for that page from the pattern configured for "${reference.collection}". Choose another page, or ask a developer to check that collection's redirect pattern.`,
|
|
227
|
+
field
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
async function assertRedirectTargetUsable(context, patterns, localization) {
|
|
231
|
+
const write = redirectReferenceForWrite(context, patterns);
|
|
232
|
+
if (!write) return;
|
|
233
|
+
const { reference, field, chosen, configured } = write;
|
|
234
|
+
if (!configured) {
|
|
235
|
+
if (publishesForm(
|
|
236
|
+
context.data,
|
|
237
|
+
context.originalData
|
|
238
|
+
)) {
|
|
239
|
+
throw redirectRefusal(
|
|
240
|
+
`This form redirects to "${reference.collection}", which is no longer configured as a redirect target, so it has nowhere to send anyone. Choose another page, or pick a different confirmation.`,
|
|
241
|
+
field
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const nextly = context.req?.nextly;
|
|
247
|
+
if (!nextly) return;
|
|
248
|
+
if (context.executor) return;
|
|
249
|
+
const found = await readRedirectTarget(nextly, reference);
|
|
250
|
+
if (found.status === "unreadable") return;
|
|
251
|
+
if (found.status === "missing") {
|
|
252
|
+
if (!chosen && !publishesForm(
|
|
253
|
+
context.data,
|
|
254
|
+
context.originalData
|
|
255
|
+
)) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
throw redirectRefusal(
|
|
259
|
+
`That page no longer exists in "${reference.collection}". Pick another.`,
|
|
260
|
+
field
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
const judged = chosen || publishesForm(
|
|
264
|
+
context.data,
|
|
265
|
+
context.originalData
|
|
266
|
+
);
|
|
267
|
+
if (judged && wouldStrandVisitors(
|
|
268
|
+
context.data,
|
|
269
|
+
context.originalData,
|
|
270
|
+
found.document,
|
|
271
|
+
localization[reference.collection]
|
|
272
|
+
)) {
|
|
273
|
+
throw redirectRefusal(
|
|
274
|
+
`That page is not published yet, and this form is. A visitor sent there would reach a "page not found". Publish the page, or save this form as a draft until you do.`,
|
|
275
|
+
field
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
if (chosen)
|
|
279
|
+
assertPatternBuildsUrl(patterns, reference, field, found.document);
|
|
280
|
+
}
|
|
281
|
+
async function readRedirectTarget(nextly, reference) {
|
|
282
|
+
try {
|
|
283
|
+
const row = await nextly.findByID({
|
|
284
|
+
collection: reference.collection,
|
|
285
|
+
id: reference.id
|
|
286
|
+
});
|
|
287
|
+
return row ? { status: "ok", document: row } : { status: "missing" };
|
|
288
|
+
} catch (error) {
|
|
289
|
+
return isMissingTarget(error) ? { status: "missing" } : { status: "unreadable" };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function isMissingTarget(error) {
|
|
293
|
+
return typeof error === "object" && error !== null && error.code === "NOT_FOUND";
|
|
294
|
+
}
|
|
295
|
+
function stampFirstPublish(context) {
|
|
296
|
+
const data = context.data;
|
|
297
|
+
if (!data) return;
|
|
298
|
+
const stored = context.originalData;
|
|
299
|
+
delete data.wentLiveAt;
|
|
300
|
+
if (recordedStamp(stored) !== void 0) return;
|
|
301
|
+
if (data.status === "published" || stored?.status === "published") {
|
|
302
|
+
data.wentLiveAt = /* @__PURE__ */ new Date();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function recordedStamp(stored) {
|
|
306
|
+
const value = stored?.wentLiveAt;
|
|
307
|
+
return value === null || value === "" ? void 0 : value;
|
|
308
|
+
}
|
|
309
|
+
function redirectRefusal(message, field) {
|
|
310
|
+
return NextlyError.validation({
|
|
311
|
+
errors: [{ path: `settings.${field}`, code: "REQUIRED", message }]
|
|
312
|
+
});
|
|
313
|
+
}
|
|
46
314
|
function formsCollection(pluginConfig) {
|
|
47
315
|
const {
|
|
48
316
|
slug,
|
|
49
317
|
labels,
|
|
50
318
|
fields: additionalFields,
|
|
51
319
|
access: accessOverrides,
|
|
320
|
+
hooks: hookOverrides,
|
|
321
|
+
admin: adminOverrides,
|
|
52
322
|
...overrides
|
|
53
323
|
} = pluginConfig.formOverrides;
|
|
324
|
+
const redirectCollections = Object.keys(pluginConfig.redirectRelationships);
|
|
54
325
|
const settingsFields = [
|
|
55
326
|
text({
|
|
56
327
|
name: "submitButtonText",
|
|
@@ -67,7 +338,7 @@ function formsCollection(pluginConfig) {
|
|
|
67
338
|
options: [
|
|
68
339
|
{ label: "Show Message", value: "message" },
|
|
69
340
|
{ label: "Redirect to URL", value: "redirect" },
|
|
70
|
-
...
|
|
341
|
+
...redirectCollections.length > 0 ? [{ label: "Redirect to Page", value: "relationship" }] : []
|
|
71
342
|
],
|
|
72
343
|
admin: {
|
|
73
344
|
description: "What happens after a successful form submission"
|
|
@@ -97,12 +368,12 @@ function formsCollection(pluginConfig) {
|
|
|
97
368
|
}
|
|
98
369
|
})
|
|
99
370
|
];
|
|
100
|
-
if (
|
|
371
|
+
if (redirectCollections.length > 0) {
|
|
101
372
|
settingsFields.push(
|
|
102
373
|
relationship({
|
|
103
374
|
name: "redirectPage",
|
|
104
375
|
label: "Redirect Page",
|
|
105
|
-
relationTo:
|
|
376
|
+
relationTo: redirectCollections,
|
|
106
377
|
admin: {
|
|
107
378
|
description: "Select a page to redirect to after submission",
|
|
108
379
|
condition: {
|
|
@@ -224,6 +495,19 @@ function formsCollection(pluginConfig) {
|
|
|
224
495
|
description: "Only published forms can receive submissions. Closed forms display a message instead."
|
|
225
496
|
}
|
|
226
497
|
}),
|
|
498
|
+
// NOT `firstPublishedAt`, which is the obvious name and the one to reach
|
|
499
|
+
// for next. The framework owns that name on collections it manages the
|
|
500
|
+
// publish lifecycle for, and a write naming it from a plugin collection is
|
|
501
|
+
// stripped on the way to the insert — silently, leaving the column null and
|
|
502
|
+
// every closed form reading as one that was never public.
|
|
503
|
+
date({
|
|
504
|
+
name: "wentLiveAt",
|
|
505
|
+
label: "First Published",
|
|
506
|
+
admin: {
|
|
507
|
+
readOnly: true,
|
|
508
|
+
description: "When this form first went live. Written once and never changed."
|
|
509
|
+
}
|
|
510
|
+
}),
|
|
227
511
|
textarea({
|
|
228
512
|
name: "closedMessage",
|
|
229
513
|
label: "Closed Form Message",
|
|
@@ -265,49 +549,75 @@ function formsCollection(pluginConfig) {
|
|
|
265
549
|
}
|
|
266
550
|
}
|
|
267
551
|
},
|
|
268
|
-
...
|
|
552
|
+
...adminOverrides
|
|
269
553
|
},
|
|
270
554
|
// Access control with sensible defaults
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
555
|
+
// `read: true` — a form is public because a site renders it. The rest are
|
|
556
|
+
// the shared defaults.
|
|
557
|
+
access: accessWithDefaults(accessOverrides, { read: true }),
|
|
558
|
+
hooks: withHostHooks(
|
|
559
|
+
{
|
|
560
|
+
// Auto-generate slug from name if not provided
|
|
561
|
+
beforeValidate: [
|
|
562
|
+
(context) => {
|
|
563
|
+
const { data, operation } = context;
|
|
564
|
+
if (operation === "create" && data && !data.slug && data.name) {
|
|
565
|
+
data.slug = String(data.name).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
566
|
+
}
|
|
567
|
+
const setsFields = operation === "create" || data?.fields !== void 0;
|
|
568
|
+
if (data && setsFields && (data.fields === void 0 || Array.isArray(data.fields) && data.fields.length === 0)) {
|
|
569
|
+
throw NextlyError.validation({
|
|
570
|
+
errors: [
|
|
571
|
+
{
|
|
572
|
+
path: "fields",
|
|
573
|
+
code: "REQUIRED",
|
|
574
|
+
message: "Form must have at least one field."
|
|
575
|
+
}
|
|
576
|
+
]
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
namedRedirectReference(
|
|
580
|
+
data,
|
|
581
|
+
operation,
|
|
582
|
+
pluginConfig.redirectRelationships
|
|
583
|
+
);
|
|
584
|
+
return data;
|
|
287
585
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
586
|
+
]
|
|
587
|
+
},
|
|
588
|
+
hookOverrides,
|
|
589
|
+
{
|
|
590
|
+
// The last COLLECTION-level phase, after every host handler in it. A
|
|
591
|
+
// host `beforeValidate` may rewrite the payload once the earlier call
|
|
592
|
+
// has passed, so the check has to run again here.
|
|
593
|
+
//
|
|
594
|
+
// It is not the last mutating point in the write, and saying so would
|
|
595
|
+
// be false: `collection-mutation-service` runs FIELD-level
|
|
596
|
+
// `beforeChange` hooks after this one, and a host may contribute a
|
|
597
|
+
// field through `formOverrides.fields`. Nothing collection-level can
|
|
598
|
+
// sit after that — the next steps are hashing and the insert. What
|
|
599
|
+
// bounds the consequence is the submit path, which resolves an
|
|
600
|
+
// unusable target to NO redirect and logs it, never to a wrong one.
|
|
601
|
+
beforeChange: [
|
|
602
|
+
async (context) => {
|
|
603
|
+
await assertRedirectTargetUsable(
|
|
604
|
+
context,
|
|
605
|
+
pluginConfig.redirectRelationships,
|
|
606
|
+
pluginConfig.redirectTargetLocalization
|
|
607
|
+
);
|
|
608
|
+
stampFirstPublish(context);
|
|
609
|
+
return context.data;
|
|
299
610
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
},
|
|
611
|
+
]
|
|
612
|
+
}
|
|
613
|
+
),
|
|
304
614
|
// Spread any additional overrides (excluding already used properties)
|
|
305
615
|
...overrides
|
|
306
616
|
};
|
|
307
617
|
}
|
|
308
618
|
|
|
309
619
|
// src/collections/submissions.ts
|
|
310
|
-
import { text as text2, textarea as textarea2, select as select2, json as json2, date, relationship as relationship2 } from "nextly";
|
|
620
|
+
import { text as text2, textarea as textarea2, select as select2, json as json2, date as date2, relationship as relationship2 } from "nextly";
|
|
311
621
|
function submissionsCollection(pluginConfig) {
|
|
312
622
|
const {
|
|
313
623
|
slug,
|
|
@@ -400,7 +710,7 @@ function submissionsCollection(pluginConfig) {
|
|
|
400
710
|
description: "Browser/device information of the submitter"
|
|
401
711
|
}
|
|
402
712
|
}),
|
|
403
|
-
|
|
713
|
+
date2({
|
|
404
714
|
name: "submittedAt",
|
|
405
715
|
label: "Submitted At",
|
|
406
716
|
required: true,
|
|
@@ -411,7 +721,7 @@ function submissionsCollection(pluginConfig) {
|
|
|
411
721
|
}),
|
|
412
722
|
// Honesty stamps for admin edits of submitted data: a corrected
|
|
413
723
|
// submission must never be indistinguishable from what the visitor sent.
|
|
414
|
-
|
|
724
|
+
date2({
|
|
415
725
|
name: "editedAt",
|
|
416
726
|
label: "Edited At",
|
|
417
727
|
admin: {
|
|
@@ -475,16 +785,9 @@ function submissionsCollection(pluginConfig) {
|
|
|
475
785
|
},
|
|
476
786
|
// Access control with sensible defaults
|
|
477
787
|
// Public can create (submit forms), but only authenticated users can read/update/delete
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
// Only authenticated users can view submissions
|
|
482
|
-
read: accessOverrides?.read ?? (({ user }) => !!user),
|
|
483
|
-
// Only authenticated users can update submissions (e.g., change status, add notes)
|
|
484
|
-
update: accessOverrides?.update ?? (({ user }) => !!user),
|
|
485
|
-
// Only admins can delete submissions (falls back to DB permissions)
|
|
486
|
-
delete: accessOverrides?.delete ?? (({ roles }) => roles.includes("admin") || roles.includes("super-admin"))
|
|
487
|
-
},
|
|
788
|
+
// `create: true` — a visitor submits without an account. Reading, updating
|
|
789
|
+
// and deleting are the shared defaults.
|
|
790
|
+
access: accessWithDefaults(accessOverrides, { create: true }),
|
|
488
791
|
hooks: {
|
|
489
792
|
// Auto-set submittedAt timestamp on creation. (The edit stamp for
|
|
490
793
|
// updates is registered in plugin.ts init — direct registry hooks are
|
|
@@ -560,7 +863,13 @@ function resolveConfig(options) {
|
|
|
560
863
|
time: options.fields?.time ?? true,
|
|
561
864
|
hidden: options.fields?.hidden ?? true
|
|
562
865
|
},
|
|
563
|
-
redirectRelationships:
|
|
866
|
+
redirectRelationships: normalizeRedirectRelationships(
|
|
867
|
+
options.redirectRelationships
|
|
868
|
+
),
|
|
869
|
+
// Filled at `init` from each collection's own metadata. Empty until then,
|
|
870
|
+
// and a slug that stays absent reads as unknown rather than as not
|
|
871
|
+
// localized — see `ResolvedFormBuilderConfig`.
|
|
872
|
+
redirectTargetLocalization: {},
|
|
564
873
|
beforeEmail: options.beforeEmail,
|
|
565
874
|
notifications: {
|
|
566
875
|
defaultFrom: options.notifications?.defaultFrom,
|
|
@@ -750,6 +1059,12 @@ function formBuilder(options = {}) {
|
|
|
750
1059
|
// Runtime-resolved collection slugs (through ctx.self, so a
|
|
751
1060
|
// framework .rename() is honored too) — admin components
|
|
752
1061
|
// never hardcode "forms"/"form-submissions".
|
|
1062
|
+
// Names only. A pattern may be a function, which does not
|
|
1063
|
+
// survive JSON and which the browser has no use for — the
|
|
1064
|
+
// admin picks a document, the server builds the URL.
|
|
1065
|
+
redirectCollections: Object.keys(
|
|
1066
|
+
resolvedConfig.redirectRelationships
|
|
1067
|
+
),
|
|
753
1068
|
slugs: {
|
|
754
1069
|
forms: ctx.self.collections[resolvedConfig.formOverrides.slug] ?? resolvedConfig.formOverrides.slug,
|
|
755
1070
|
submissions: ctx.self.collections[resolvedConfig.formSubmissionOverrides.slug] ?? resolvedConfig.formSubmissionOverrides.slug
|
|
@@ -776,7 +1091,7 @@ function formBuilder(options = {}) {
|
|
|
776
1091
|
},
|
|
777
1092
|
// -- Init ----------------------------------------------------------------
|
|
778
1093
|
// Registers an afterCreate hook on submissions to send email notifications.
|
|
779
|
-
init(nextly) {
|
|
1094
|
+
async init(nextly) {
|
|
780
1095
|
const declaredSubmissionsSlug = resolvedConfig.formSubmissionOverrides.slug;
|
|
781
1096
|
const submissionSlug = nextly.self.collections[declaredSubmissionsSlug] ?? declaredSubmissionsSlug;
|
|
782
1097
|
nextly.logger.info("Form Builder plugin initialized", {
|
|
@@ -784,6 +1099,30 @@ function formBuilder(options = {}) {
|
|
|
784
1099
|
submissionsCollection: submissionSlug,
|
|
785
1100
|
enabledFields: Object.entries(resolvedConfig.fields).filter(([, on]) => on).map(([name]) => name)
|
|
786
1101
|
});
|
|
1102
|
+
for (const slug of Object.keys(
|
|
1103
|
+
resolvedConfig.redirectTargetLocalization
|
|
1104
|
+
)) {
|
|
1105
|
+
delete resolvedConfig.redirectTargetLocalization[slug];
|
|
1106
|
+
}
|
|
1107
|
+
await Promise.all(
|
|
1108
|
+
Object.keys(resolvedConfig.redirectRelationships).map(
|
|
1109
|
+
async (declared) => {
|
|
1110
|
+
const slug = nextly.self.collections[declared] ?? declared;
|
|
1111
|
+
try {
|
|
1112
|
+
const collection = await nextly.services.collections.getCollection(slug, {});
|
|
1113
|
+
const localized = collection?.localized;
|
|
1114
|
+
if (typeof localized === "boolean") {
|
|
1115
|
+
resolvedConfig.redirectTargetLocalization[declared] = localized;
|
|
1116
|
+
}
|
|
1117
|
+
} catch {
|
|
1118
|
+
nextly.logger.debug?.(
|
|
1119
|
+
"Redirect target localization could not be read",
|
|
1120
|
+
{ collection: slug }
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
)
|
|
1125
|
+
);
|
|
787
1126
|
nextly.__formBuilderConfig = resolvedConfig;
|
|
788
1127
|
if (resolvedConfig.beforeEmail) {
|
|
789
1128
|
nextly.filters.add(
|
|
@@ -850,9 +1189,9 @@ function getFormBuilderConfig(nextly) {
|
|
|
850
1189
|
return nextly.__formBuilderConfig;
|
|
851
1190
|
}
|
|
852
1191
|
function resolveFieldRef(ref, data) {
|
|
853
|
-
const
|
|
854
|
-
if (
|
|
855
|
-
const value = data[
|
|
1192
|
+
const name = parseFieldRef(ref);
|
|
1193
|
+
if (name === null) return ref;
|
|
1194
|
+
const value = data[name];
|
|
856
1195
|
return typeof value === "string" ? value : "";
|
|
857
1196
|
}
|
|
858
1197
|
function collectAttachmentInputs(fields, submittedData) {
|
|
@@ -1351,6 +1690,17 @@ function validateSettings(settings, errors) {
|
|
|
1351
1690
|
});
|
|
1352
1691
|
}
|
|
1353
1692
|
}
|
|
1693
|
+
if (s.confirmationType === "relationship" && !namesADocument(s.redirectPage)) {
|
|
1694
|
+
errors.push({
|
|
1695
|
+
path: "settings.redirectPage",
|
|
1696
|
+
message: "A page is required when confirmation type is 'relationship'",
|
|
1697
|
+
code: "REDIRECT_PAGE_REQUIRED"
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
function namesADocument(value) {
|
|
1702
|
+
if (typeof value === "string") return value.length > 0;
|
|
1703
|
+
return parseRedirectReference(value, []) !== null;
|
|
1354
1704
|
}
|
|
1355
1705
|
function validateAccess(access, errors) {
|
|
1356
1706
|
if (!access) {
|
|
@@ -1444,7 +1794,7 @@ var file = (config) => ({
|
|
|
1444
1794
|
...config,
|
|
1445
1795
|
type: "file"
|
|
1446
1796
|
});
|
|
1447
|
-
var
|
|
1797
|
+
var date3 = (config) => ({
|
|
1448
1798
|
...config,
|
|
1449
1799
|
type: "date"
|
|
1450
1800
|
});
|
|
@@ -1512,53 +1862,74 @@ function generateFieldSchema(field) {
|
|
|
1512
1862
|
return z.unknown();
|
|
1513
1863
|
}
|
|
1514
1864
|
}
|
|
1515
|
-
function
|
|
1516
|
-
|
|
1517
|
-
if (
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1865
|
+
function customMessage(field) {
|
|
1866
|
+
const enforced = ENFORCED_VALIDATION_RULES[field.type] ?? [];
|
|
1867
|
+
if (!enforced.includes("message")) return void 0;
|
|
1868
|
+
return field.validation?.errorMessage;
|
|
1869
|
+
}
|
|
1870
|
+
function enforcedValidation(field) {
|
|
1871
|
+
const stored = field.validation ?? {};
|
|
1872
|
+
return { ...stored, errorMessage: customMessage(field) };
|
|
1873
|
+
}
|
|
1874
|
+
var STRING_RULE_APPLIERS = {
|
|
1875
|
+
minLength: (schema, validation) => validation.minLength === void 0 ? schema : schema.min(
|
|
1876
|
+
validation.minLength,
|
|
1877
|
+
validation.errorMessage || `Minimum ${validation.minLength} characters required`
|
|
1878
|
+
),
|
|
1879
|
+
maxLength: (schema, validation) => validation.maxLength === void 0 ? schema : schema.max(
|
|
1880
|
+
validation.maxLength,
|
|
1881
|
+
validation.errorMessage || `Maximum ${validation.maxLength} characters allowed`
|
|
1882
|
+
),
|
|
1883
|
+
pattern: (schema, validation, fallback) => validation.pattern ? schema.regex(
|
|
1884
|
+
new RegExp(validation.pattern),
|
|
1885
|
+
validation.errorMessage || fallback
|
|
1886
|
+
) : schema
|
|
1887
|
+
};
|
|
1888
|
+
var NUMBER_RULE_APPLIERS = {
|
|
1889
|
+
min: (schema, validation) => validation.min === void 0 ? schema : schema.min(
|
|
1890
|
+
validation.min,
|
|
1891
|
+
validation.errorMessage || `Minimum value is ${validation.min}`
|
|
1892
|
+
),
|
|
1893
|
+
max: (schema, validation) => validation.max === void 0 ? schema : schema.max(
|
|
1894
|
+
validation.max,
|
|
1895
|
+
validation.errorMessage || `Maximum value is ${validation.max}`
|
|
1896
|
+
)
|
|
1897
|
+
};
|
|
1898
|
+
function overridesPattern(field) {
|
|
1899
|
+
const enforced = ENFORCED_VALIDATION_RULES[field.type] ?? [];
|
|
1900
|
+
if (!enforced.includes("pattern")) return false;
|
|
1901
|
+
const validation = field.validation ?? {};
|
|
1902
|
+
return Boolean(validation.pattern);
|
|
1903
|
+
}
|
|
1904
|
+
function applyStringRules(schema, field, patternMessage) {
|
|
1905
|
+
const validation = enforcedValidation(field);
|
|
1906
|
+
let constrained = schema;
|
|
1907
|
+
for (const rule of ENFORCED_VALIDATION_RULES[field.type] ?? []) {
|
|
1908
|
+
const apply = STRING_RULE_APPLIERS[rule];
|
|
1909
|
+
if (apply) constrained = apply(constrained, validation, patternMessage);
|
|
1534
1910
|
}
|
|
1535
|
-
return
|
|
1911
|
+
return constrained;
|
|
1912
|
+
}
|
|
1913
|
+
function generateTextSchema(field) {
|
|
1914
|
+
const schema = applyStringRules(z.string(), field, "Invalid format");
|
|
1915
|
+
return applyRequired(schema, field.required, customMessage(field));
|
|
1536
1916
|
}
|
|
1537
1917
|
function generateEmailSchema(field) {
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
}
|
|
1545
|
-
return applyRequired(schema, field.required, field.validation?.errorMessage);
|
|
1918
|
+
const schema = applyStringRules(
|
|
1919
|
+
z.string().email(customMessage(field) || "Invalid email address"),
|
|
1920
|
+
field,
|
|
1921
|
+
"Invalid email format"
|
|
1922
|
+
);
|
|
1923
|
+
return applyRequired(schema, field.required, customMessage(field));
|
|
1546
1924
|
}
|
|
1547
1925
|
function generateNumberSchema(field) {
|
|
1548
1926
|
let schema = z.number({
|
|
1549
|
-
error: (issue) => issue.input === void 0 ? field
|
|
1927
|
+
error: (issue) => issue.input === void 0 ? customMessage(field) || "This field is required" : "Must be a number"
|
|
1550
1928
|
});
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
);
|
|
1556
|
-
}
|
|
1557
|
-
if (field.validation?.max !== void 0) {
|
|
1558
|
-
schema = schema.max(
|
|
1559
|
-
field.validation.max,
|
|
1560
|
-
field.validation.errorMessage || `Maximum value is ${field.validation.max}`
|
|
1561
|
-
);
|
|
1929
|
+
const validation = enforcedValidation(field);
|
|
1930
|
+
for (const rule of ENFORCED_VALIDATION_RULES[field.type] ?? []) {
|
|
1931
|
+
const apply = NUMBER_RULE_APPLIERS[rule];
|
|
1932
|
+
if (apply) schema = apply(schema, validation);
|
|
1562
1933
|
}
|
|
1563
1934
|
if (field.required) {
|
|
1564
1935
|
return schema;
|
|
@@ -1566,45 +1937,24 @@ function generateNumberSchema(field) {
|
|
|
1566
1937
|
return schema.optional().nullable();
|
|
1567
1938
|
}
|
|
1568
1939
|
function generatePhoneSchema(field) {
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
} else {
|
|
1576
|
-
schema = schema.regex(
|
|
1577
|
-
/^[\d\s\-+()]+$/,
|
|
1578
|
-
field.validation?.errorMessage || "Invalid phone number"
|
|
1579
|
-
);
|
|
1580
|
-
}
|
|
1581
|
-
return applyRequired(schema, field.required, field.validation?.errorMessage);
|
|
1940
|
+
const base = overridesPattern(field) ? z.string() : z.string().regex(
|
|
1941
|
+
/^[\d\s\-+()]+$/,
|
|
1942
|
+
customMessage(field) || "Invalid phone number"
|
|
1943
|
+
);
|
|
1944
|
+
const schema = applyStringRules(base, field, "Invalid phone number format");
|
|
1945
|
+
return applyRequired(schema, field.required, customMessage(field));
|
|
1582
1946
|
}
|
|
1583
1947
|
function generateUrlSchema(field) {
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
}
|
|
1591
|
-
return applyRequired(schema, field.required, field.validation?.errorMessage);
|
|
1948
|
+
const schema = applyStringRules(
|
|
1949
|
+
z.string().url(customMessage(field) || "Invalid URL"),
|
|
1950
|
+
field,
|
|
1951
|
+
"Invalid URL format"
|
|
1952
|
+
);
|
|
1953
|
+
return applyRequired(schema, field.required, customMessage(field));
|
|
1592
1954
|
}
|
|
1593
1955
|
function generateTextareaSchema(field) {
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
schema = schema.min(
|
|
1597
|
-
field.validation.minLength,
|
|
1598
|
-
field.validation.errorMessage || `Minimum ${field.validation.minLength} characters required`
|
|
1599
|
-
);
|
|
1600
|
-
}
|
|
1601
|
-
if (field.validation?.maxLength !== void 0) {
|
|
1602
|
-
schema = schema.max(
|
|
1603
|
-
field.validation.maxLength,
|
|
1604
|
-
field.validation.errorMessage || `Maximum ${field.validation.maxLength} characters allowed`
|
|
1605
|
-
);
|
|
1606
|
-
}
|
|
1607
|
-
return applyRequired(schema, field.required, field.validation?.errorMessage);
|
|
1956
|
+
const schema = applyStringRules(z.string(), field, "Invalid format");
|
|
1957
|
+
return applyRequired(schema, field.required, customMessage(field));
|
|
1608
1958
|
}
|
|
1609
1959
|
function generateSelectSchema(field) {
|
|
1610
1960
|
const validValues = field.options.map((opt) => opt.value);
|
|
@@ -1613,13 +1963,13 @@ function generateSelectSchema(field) {
|
|
|
1613
1963
|
if (field.required) {
|
|
1614
1964
|
return schema2.min(
|
|
1615
1965
|
1,
|
|
1616
|
-
field
|
|
1966
|
+
customMessage(field) || "Please select at least one option"
|
|
1617
1967
|
);
|
|
1618
1968
|
}
|
|
1619
1969
|
return schema2.optional();
|
|
1620
1970
|
}
|
|
1621
1971
|
const schema = z.enum(validValues, {
|
|
1622
|
-
error: () => field
|
|
1972
|
+
error: () => customMessage(field) || "Please select a valid option"
|
|
1623
1973
|
});
|
|
1624
1974
|
if (field.required) {
|
|
1625
1975
|
return schema;
|
|
@@ -1630,7 +1980,7 @@ function generateCheckboxSchema(field) {
|
|
|
1630
1980
|
const schema = z.boolean();
|
|
1631
1981
|
if (field.required) {
|
|
1632
1982
|
return schema.refine((val) => val === true, {
|
|
1633
|
-
message: field
|
|
1983
|
+
message: customMessage(field) || "This field is required"
|
|
1634
1984
|
});
|
|
1635
1985
|
}
|
|
1636
1986
|
return schema.optional();
|
|
@@ -1638,7 +1988,7 @@ function generateCheckboxSchema(field) {
|
|
|
1638
1988
|
function generateRadioSchema(field) {
|
|
1639
1989
|
const validValues = field.options.map((opt) => opt.value);
|
|
1640
1990
|
const schema = z.enum(validValues, {
|
|
1641
|
-
error: () => field
|
|
1991
|
+
error: () => customMessage(field) || "Please select an option"
|
|
1642
1992
|
});
|
|
1643
1993
|
if (field.required) {
|
|
1644
1994
|
return schema;
|
|
@@ -1651,42 +2001,42 @@ function generateFileSchema(field) {
|
|
|
1651
2001
|
if (field.required) {
|
|
1652
2002
|
return schema2.min(
|
|
1653
2003
|
1,
|
|
1654
|
-
field
|
|
2004
|
+
customMessage(field) || "Please upload at least one file"
|
|
1655
2005
|
);
|
|
1656
2006
|
}
|
|
1657
2007
|
return schema2.optional();
|
|
1658
2008
|
}
|
|
1659
2009
|
const schema = z.string();
|
|
1660
|
-
return applyRequired(schema, field.required, field
|
|
2010
|
+
return applyRequired(schema, field.required, customMessage(field));
|
|
1661
2011
|
}
|
|
1662
2012
|
function generateDateSchema(field) {
|
|
1663
2013
|
let schema = z.string();
|
|
1664
2014
|
schema = schema.refine(
|
|
1665
2015
|
(val) => {
|
|
1666
2016
|
if (!val) return true;
|
|
1667
|
-
const
|
|
1668
|
-
return !isNaN(
|
|
2017
|
+
const date4 = new Date(val);
|
|
2018
|
+
return !isNaN(date4.getTime());
|
|
1669
2019
|
},
|
|
1670
|
-
{ message: field
|
|
2020
|
+
{ message: customMessage(field) || "Invalid date" }
|
|
1671
2021
|
);
|
|
1672
2022
|
if (field.min || field.max) {
|
|
1673
2023
|
schema = schema.refine(
|
|
1674
2024
|
(val) => {
|
|
1675
2025
|
if (!val) return true;
|
|
1676
|
-
const
|
|
1677
|
-
if (field.min &&
|
|
1678
|
-
if (field.max &&
|
|
2026
|
+
const date4 = new Date(val);
|
|
2027
|
+
if (field.min && date4 < new Date(field.min)) return false;
|
|
2028
|
+
if (field.max && date4 > new Date(field.max)) return false;
|
|
1679
2029
|
return true;
|
|
1680
2030
|
},
|
|
1681
2031
|
{
|
|
1682
|
-
message: field
|
|
2032
|
+
message: customMessage(field) || `Date must be between ${field.min || "any"} and ${field.max || "any"}`
|
|
1683
2033
|
}
|
|
1684
2034
|
);
|
|
1685
2035
|
}
|
|
1686
2036
|
if (field.required) {
|
|
1687
2037
|
return schema.refine(
|
|
1688
2038
|
(val) => val !== void 0 && val !== null && val !== "",
|
|
1689
|
-
{ message: field
|
|
2039
|
+
{ message: customMessage(field) || "This field is required" }
|
|
1690
2040
|
);
|
|
1691
2041
|
}
|
|
1692
2042
|
return schema.optional().or(z.literal(""));
|
|
@@ -1694,13 +2044,13 @@ function generateDateSchema(field) {
|
|
|
1694
2044
|
function generateTimeSchema(field) {
|
|
1695
2045
|
const schema = z.string().regex(
|
|
1696
2046
|
/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/,
|
|
1697
|
-
field
|
|
2047
|
+
customMessage(field) || "Invalid time format (HH:mm)"
|
|
1698
2048
|
);
|
|
1699
|
-
return applyRequired(schema, field.required, field
|
|
2049
|
+
return applyRequired(schema, field.required, customMessage(field));
|
|
1700
2050
|
}
|
|
1701
2051
|
function generateHiddenSchema(field) {
|
|
1702
2052
|
const schema = z.string();
|
|
1703
|
-
return applyRequired(schema, field.required, field
|
|
2053
|
+
return applyRequired(schema, field.required, customMessage(field));
|
|
1704
2054
|
}
|
|
1705
2055
|
function applyRequired(schema, required, errorMessage) {
|
|
1706
2056
|
if (required) {
|
|
@@ -1775,6 +2125,9 @@ function getValidationErrors(result) {
|
|
|
1775
2125
|
return errors;
|
|
1776
2126
|
}
|
|
1777
2127
|
|
|
2128
|
+
// src/handlers/submit-form.ts
|
|
2129
|
+
import { formAvailability, NO_SUCH_FORM } from "nextly";
|
|
2130
|
+
|
|
1778
2131
|
// src/handlers/spam-detection.ts
|
|
1779
2132
|
var rateLimitStore = /* @__PURE__ */ new Map();
|
|
1780
2133
|
async function checkSpam(options) {
|
|
@@ -1899,19 +2252,18 @@ async function submitForm(options, context) {
|
|
|
1899
2252
|
logger.warn?.("Form submission attempted for non-existent form", {
|
|
1900
2253
|
formSlug
|
|
1901
2254
|
});
|
|
1902
|
-
return {
|
|
1903
|
-
success: false,
|
|
1904
|
-
error: "Form not found"
|
|
1905
|
-
};
|
|
2255
|
+
return { success: false, error: NO_SUCH_FORM };
|
|
1906
2256
|
}
|
|
1907
|
-
|
|
1908
|
-
|
|
2257
|
+
const availability = formAvailability(form);
|
|
2258
|
+
if (availability.kind !== "open") {
|
|
2259
|
+
logger.info?.("Form submission rejected - form not accepting", {
|
|
1909
2260
|
formSlug,
|
|
1910
|
-
status: form.status
|
|
2261
|
+
status: form.status,
|
|
2262
|
+
availability: availability.kind
|
|
1911
2263
|
});
|
|
1912
2264
|
return {
|
|
1913
2265
|
success: false,
|
|
1914
|
-
error:
|
|
2266
|
+
error: availability.kind === "closed" ? availability.message : NO_SUCH_FORM
|
|
1915
2267
|
};
|
|
1916
2268
|
}
|
|
1917
2269
|
const settings = normalizeFormSettings(form.settings);
|
|
@@ -2026,7 +2378,11 @@ async function submitForm(options, context) {
|
|
|
2026
2378
|
warnings: submission.warnings
|
|
2027
2379
|
});
|
|
2028
2380
|
}
|
|
2029
|
-
const redirect =
|
|
2381
|
+
const redirect = await resolveRedirectUrl(
|
|
2382
|
+
form,
|
|
2383
|
+
pluginConfig,
|
|
2384
|
+
pluginContext
|
|
2385
|
+
);
|
|
2030
2386
|
if (isContentSpam) {
|
|
2031
2387
|
return { success: true, redirect };
|
|
2032
2388
|
}
|
|
@@ -2064,21 +2420,100 @@ async function fetchFormBySlug(slug, pluginConfig, pluginContext) {
|
|
|
2064
2420
|
return null;
|
|
2065
2421
|
}
|
|
2066
2422
|
}
|
|
2067
|
-
function
|
|
2068
|
-
|
|
2423
|
+
async function resolveRedirectUrl(form, pluginConfig, pluginContext) {
|
|
2424
|
+
const settings = form.settings;
|
|
2425
|
+
if (!settings) return void 0;
|
|
2426
|
+
if (settings.confirmationType === "redirect" && settings.redirectUrl) {
|
|
2427
|
+
return settings.redirectUrl;
|
|
2428
|
+
}
|
|
2429
|
+
const field = pickedDocumentField(settings);
|
|
2430
|
+
if (!field) return void 0;
|
|
2431
|
+
return urlForPickedDocument(
|
|
2432
|
+
settings[field],
|
|
2433
|
+
form,
|
|
2434
|
+
pluginConfig,
|
|
2435
|
+
pluginContext
|
|
2436
|
+
);
|
|
2437
|
+
}
|
|
2438
|
+
async function urlForPickedDocument(stored, form, pluginConfig, pluginContext) {
|
|
2439
|
+
const { logger } = pluginContext;
|
|
2440
|
+
const patterns = pluginConfig.redirectRelationships;
|
|
2441
|
+
const reference = parseRedirectReference(stored, Object.keys(patterns));
|
|
2442
|
+
if (!reference) {
|
|
2443
|
+
logger.warn?.("Form redirects to a page, but names no readable document", {
|
|
2444
|
+
form: form.slug
|
|
2445
|
+
});
|
|
2446
|
+
return void 0;
|
|
2447
|
+
}
|
|
2448
|
+
const pattern = patterns[reference.collection];
|
|
2449
|
+
if (!pattern) {
|
|
2450
|
+
logger.warn?.(
|
|
2451
|
+
"Form redirects to a collection that is not in redirectRelationships",
|
|
2452
|
+
{ form: form.slug, collection: reference.collection }
|
|
2453
|
+
);
|
|
2454
|
+
return void 0;
|
|
2455
|
+
}
|
|
2456
|
+
const target = await readTarget(reference, form, pluginContext);
|
|
2457
|
+
if (!target) return void 0;
|
|
2458
|
+
if (documentReachability(
|
|
2459
|
+
target,
|
|
2460
|
+
pluginConfig.redirectTargetLocalization[reference.collection]
|
|
2461
|
+
) === "unreachable") {
|
|
2462
|
+
logger.warn?.("Form redirects to a page that has never been published", {
|
|
2463
|
+
form: form.slug,
|
|
2464
|
+
collection: reference.collection,
|
|
2465
|
+
target: reference.id
|
|
2466
|
+
});
|
|
2069
2467
|
return void 0;
|
|
2070
2468
|
}
|
|
2071
|
-
|
|
2469
|
+
return buildUrl(pattern, target, reference.collection, form, pluginContext);
|
|
2470
|
+
}
|
|
2471
|
+
function buildUrl(pattern, target, collection, form, pluginContext) {
|
|
2472
|
+
const { logger } = pluginContext;
|
|
2473
|
+
try {
|
|
2474
|
+
const url2 = applyRedirectPattern(pattern, target);
|
|
2475
|
+
if (!url2) {
|
|
2476
|
+
logger.warn?.(
|
|
2477
|
+
"Form redirect pattern could not be filled from the target document",
|
|
2478
|
+
{ form: form.slug, collection }
|
|
2479
|
+
);
|
|
2480
|
+
}
|
|
2481
|
+
return url2;
|
|
2482
|
+
} catch (error) {
|
|
2483
|
+
logger.warn?.("Form redirect pattern threw while building a URL", {
|
|
2484
|
+
form: form.slug,
|
|
2485
|
+
collection,
|
|
2486
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2487
|
+
});
|
|
2072
2488
|
return void 0;
|
|
2073
2489
|
}
|
|
2074
|
-
|
|
2075
|
-
|
|
2490
|
+
}
|
|
2491
|
+
async function readTarget(reference, form, pluginContext) {
|
|
2492
|
+
const { logger } = pluginContext;
|
|
2493
|
+
const context = {
|
|
2494
|
+
form: form.slug,
|
|
2495
|
+
collection: reference.collection,
|
|
2496
|
+
id: reference.id
|
|
2497
|
+
};
|
|
2498
|
+
let row;
|
|
2499
|
+
try {
|
|
2500
|
+
row = await pluginContext.services.collections.findEntryById(
|
|
2501
|
+
reference.collection,
|
|
2502
|
+
reference.id,
|
|
2503
|
+
{ as: "system" }
|
|
2504
|
+
);
|
|
2505
|
+
} catch (error) {
|
|
2506
|
+
logger.warn?.("Form redirect target could not be read", {
|
|
2507
|
+
...context,
|
|
2508
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2509
|
+
});
|
|
2510
|
+
return void 0;
|
|
2076
2511
|
}
|
|
2077
|
-
if (
|
|
2078
|
-
|
|
2079
|
-
return
|
|
2512
|
+
if (!row || typeof row !== "object") {
|
|
2513
|
+
logger.warn?.("Form redirect target no longer exists", context);
|
|
2514
|
+
return void 0;
|
|
2080
2515
|
}
|
|
2081
|
-
return
|
|
2516
|
+
return row;
|
|
2082
2517
|
}
|
|
2083
2518
|
async function validateSubmission(formSlug, data, context) {
|
|
2084
2519
|
const { pluginContext, pluginConfig } = context;
|
|
@@ -2352,7 +2787,7 @@ export {
|
|
|
2352
2787
|
cleanupRateLimitStore,
|
|
2353
2788
|
clearRateLimitStore,
|
|
2354
2789
|
createFormConfig,
|
|
2355
|
-
|
|
2790
|
+
date3 as date,
|
|
2356
2791
|
downloadFile,
|
|
2357
2792
|
email,
|
|
2358
2793
|
evaluateConditions,
|