@btst/plugin-form-builder 0.0.0-stage → 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/LICENSE.md +17 -0
  2. package/dist/api/getters.cjs +154 -0
  3. package/dist/api/getters.mjs +145 -0
  4. package/dist/api/index.cjs +25 -0
  5. package/dist/api/index.d.cts +119 -0
  6. package/dist/api/index.d.mts +119 -0
  7. package/dist/api/index.d.ts +119 -0
  8. package/dist/api/index.mjs +6 -0
  9. package/dist/api/lifecycle-migrations.cjs +15 -0
  10. package/dist/api/lifecycle-migrations.mjs +13 -0
  11. package/dist/api/operations.cjs +1093 -0
  12. package/dist/api/operations.mjs +1082 -0
  13. package/dist/api/plugin.cjs +201 -0
  14. package/dist/api/plugin.mjs +192 -0
  15. package/dist/api/query-key-defs.cjs +43 -0
  16. package/dist/api/query-key-defs.mjs +39 -0
  17. package/dist/client/components/forms/form-renderer.cjs +193 -0
  18. package/dist/client/components/forms/form-renderer.mjs +191 -0
  19. package/dist/client/components/index.cjs +29 -0
  20. package/dist/client/components/index.d.cts +97 -0
  21. package/dist/client/components/index.d.mts +97 -0
  22. package/dist/client/components/index.d.ts +97 -0
  23. package/dist/client/components/index.mjs +18 -0
  24. package/dist/client/components/loading/form-builder-skeleton.cjs +32 -0
  25. package/dist/client/components/loading/form-builder-skeleton.mjs +30 -0
  26. package/dist/client/components/loading/form-list-skeleton.cjs +21 -0
  27. package/dist/client/components/loading/form-list-skeleton.mjs +19 -0
  28. package/dist/client/components/loading/submissions-skeleton.cjs +34 -0
  29. package/dist/client/components/loading/submissions-skeleton.mjs +32 -0
  30. package/dist/client/components/pages/404-page.cjs +28 -0
  31. package/dist/client/components/pages/404-page.mjs +26 -0
  32. package/dist/client/components/pages/form-builder-page.cjs +77 -0
  33. package/dist/client/components/pages/form-builder-page.internal.cjs +217 -0
  34. package/dist/client/components/pages/form-builder-page.internal.mjs +215 -0
  35. package/dist/client/components/pages/form-builder-page.mjs +75 -0
  36. package/dist/client/components/pages/form-list-page.cjs +44 -0
  37. package/dist/client/components/pages/form-list-page.internal.cjs +282 -0
  38. package/dist/client/components/pages/form-list-page.internal.mjs +280 -0
  39. package/dist/client/components/pages/form-list-page.mjs +42 -0
  40. package/dist/client/components/pages/submissions-page.cjs +64 -0
  41. package/dist/client/components/pages/submissions-page.internal.cjs +255 -0
  42. package/dist/client/components/pages/submissions-page.internal.mjs +253 -0
  43. package/dist/client/components/pages/submissions-page.mjs +62 -0
  44. package/dist/client/components/shared/default-error.cjs +17 -0
  45. package/dist/client/components/shared/default-error.mjs +15 -0
  46. package/dist/client/components/shared/empty-state.cjs +16 -0
  47. package/dist/client/components/shared/empty-state.mjs +14 -0
  48. package/dist/client/components/shared/page-wrapper.cjs +29 -0
  49. package/dist/client/components/shared/page-wrapper.mjs +27 -0
  50. package/dist/client/components/shared/pagination.cjs +49 -0
  51. package/dist/client/components/shared/pagination.mjs +47 -0
  52. package/dist/client/constants.cjs +5 -0
  53. package/dist/client/constants.mjs +3 -0
  54. package/dist/client/hooks/form-builder-hooks.cjs +203 -0
  55. package/dist/client/hooks/form-builder-hooks.mjs +186 -0
  56. package/dist/client/hooks/form-builder-resource.cjs +13 -0
  57. package/dist/client/hooks/form-builder-resource.mjs +11 -0
  58. package/dist/client/hooks/index.cjs +22 -0
  59. package/dist/client/hooks/index.d.cts +378 -0
  60. package/dist/client/hooks/index.d.mts +378 -0
  61. package/dist/client/hooks/index.d.ts +378 -0
  62. package/dist/client/hooks/index.mjs +1 -0
  63. package/dist/client/index.cjs +13 -0
  64. package/dist/client/index.d.cts +410 -0
  65. package/dist/client/index.d.mts +410 -0
  66. package/dist/client/index.d.ts +410 -0
  67. package/dist/client/index.mjs +2 -0
  68. package/dist/client/plugin.cjs +311 -0
  69. package/dist/client/plugin.mjs +309 -0
  70. package/dist/client.css +3 -0
  71. package/dist/db.cjs +99 -0
  72. package/dist/db.mjs +97 -0
  73. package/dist/permissions.cjs +95 -0
  74. package/dist/permissions.d.cts +94 -0
  75. package/dist/permissions.d.mts +94 -0
  76. package/dist/permissions.d.ts +94 -0
  77. package/dist/permissions.mjs +93 -0
  78. package/dist/query-keys.cjs +140 -0
  79. package/dist/query-keys.d.cts +392 -0
  80. package/dist/query-keys.d.mts +392 -0
  81. package/dist/query-keys.d.ts +392 -0
  82. package/dist/query-keys.mjs +137 -0
  83. package/dist/schemas.cjs +91 -0
  84. package/dist/schemas.mjs +82 -0
  85. package/dist/shared/plugin-form-builder.BZYGyFgT.d.mts +351 -0
  86. package/dist/shared/plugin-form-builder.Bbl9NufB.d.cts +408 -0
  87. package/dist/shared/plugin-form-builder.CSP5HR2O.d.ts +408 -0
  88. package/dist/shared/plugin-form-builder.Cmk083dA.d.cts +351 -0
  89. package/dist/shared/plugin-form-builder.D258Qpb8.d.ts +351 -0
  90. package/dist/shared/plugin-form-builder.DWrQ3lRX.d.cts +118 -0
  91. package/dist/shared/plugin-form-builder.DWrQ3lRX.d.mts +118 -0
  92. package/dist/shared/plugin-form-builder.DWrQ3lRX.d.ts +118 -0
  93. package/dist/shared/plugin-form-builder.lp98rFNl.d.mts +408 -0
  94. package/dist/style.css +6 -0
  95. package/dist/types.cjs +2 -0
  96. package/dist/types.d.cts +6 -0
  97. package/dist/types.d.mts +6 -0
  98. package/dist/types.d.ts +6 -0
  99. package/dist/types.mjs +1 -0
  100. package/dist/ui/src/components/accordion.cjs +81 -0
  101. package/dist/ui/src/components/accordion.mjs +62 -0
  102. package/dist/ui/src/components/alert-dialog.cjs +157 -0
  103. package/dist/ui/src/components/alert-dialog.mjs +132 -0
  104. package/dist/ui/src/components/auto-form/common/label.cjs +18 -0
  105. package/dist/ui/src/components/auto-form/common/label.mjs +16 -0
  106. package/dist/ui/src/components/auto-form/common/tooltip.cjs +9 -0
  107. package/dist/ui/src/components/auto-form/common/tooltip.mjs +7 -0
  108. package/dist/ui/src/components/auto-form/config.cjs +40 -0
  109. package/dist/ui/src/components/auto-form/config.mjs +37 -0
  110. package/dist/ui/src/components/auto-form/dependencies.cjs +48 -0
  111. package/dist/ui/src/components/auto-form/dependencies.mjs +46 -0
  112. package/dist/ui/src/components/auto-form/fields/array.cjs +266 -0
  113. package/dist/ui/src/components/auto-form/fields/array.mjs +264 -0
  114. package/dist/ui/src/components/auto-form/fields/checkbox.cjs +38 -0
  115. package/dist/ui/src/components/auto-form/fields/checkbox.mjs +36 -0
  116. package/dist/ui/src/components/auto-form/fields/date.cjs +67 -0
  117. package/dist/ui/src/components/auto-form/fields/date.mjs +65 -0
  118. package/dist/ui/src/components/auto-form/fields/enum.cjs +73 -0
  119. package/dist/ui/src/components/auto-form/fields/enum.mjs +71 -0
  120. package/dist/ui/src/components/auto-form/fields/input.cjs +32 -0
  121. package/dist/ui/src/components/auto-form/fields/input.mjs +30 -0
  122. package/dist/ui/src/components/auto-form/fields/number.cjs +31 -0
  123. package/dist/ui/src/components/auto-form/fields/number.mjs +29 -0
  124. package/dist/ui/src/components/auto-form/fields/object.cjs +233 -0
  125. package/dist/ui/src/components/auto-form/fields/object.mjs +217 -0
  126. package/dist/ui/src/components/auto-form/fields/radio-group.cjs +67 -0
  127. package/dist/ui/src/components/auto-form/fields/radio-group.mjs +65 -0
  128. package/dist/ui/src/components/auto-form/fields/switch.cjs +38 -0
  129. package/dist/ui/src/components/auto-form/fields/switch.mjs +36 -0
  130. package/dist/ui/src/components/auto-form/fields/textarea.cjs +31 -0
  131. package/dist/ui/src/components/auto-form/fields/textarea.mjs +29 -0
  132. package/dist/ui/src/components/auto-form/helpers.cjs +270 -0
  133. package/dist/ui/src/components/auto-form/helpers.mjs +259 -0
  134. package/dist/ui/src/components/auto-form/index.cjs +92 -0
  135. package/dist/ui/src/components/auto-form/index.mjs +86 -0
  136. package/dist/ui/src/components/auto-form/stepped-auto-form.cjs +374 -0
  137. package/dist/ui/src/components/auto-form/stepped-auto-form.mjs +368 -0
  138. package/dist/ui/src/components/auto-form/types.cjs +11 -0
  139. package/dist/ui/src/components/auto-form/types.mjs +9 -0
  140. package/dist/ui/src/components/button.cjs +56 -0
  141. package/dist/ui/src/components/button.mjs +53 -0
  142. package/dist/ui/src/components/calendar.cjs +207 -0
  143. package/dist/ui/src/components/calendar.mjs +190 -0
  144. package/dist/ui/src/components/checkbox.cjs +48 -0
  145. package/dist/ui/src/components/checkbox.mjs +32 -0
  146. package/dist/ui/src/components/date-picker.cjs +41 -0
  147. package/dist/ui/src/components/date-picker.mjs +39 -0
  148. package/dist/ui/src/components/dialog.cjs +141 -0
  149. package/dist/ui/src/components/dialog.mjs +118 -0
  150. package/dist/ui/src/components/dropdown-menu.cjs +81 -0
  151. package/dist/ui/src/components/dropdown-menu.mjs +62 -0
  152. package/dist/ui/src/components/form-builder/canvas.cjs +111 -0
  153. package/dist/ui/src/components/form-builder/canvas.mjs +109 -0
  154. package/dist/ui/src/components/form-builder/components/index.cjs +571 -0
  155. package/dist/ui/src/components/form-builder/components/index.mjs +554 -0
  156. package/dist/ui/src/components/form-builder/edit-field-dialog.cjs +131 -0
  157. package/dist/ui/src/components/form-builder/edit-field-dialog.mjs +129 -0
  158. package/dist/ui/src/components/form-builder/form-preview.cjs +73 -0
  159. package/dist/ui/src/components/form-builder/form-preview.mjs +71 -0
  160. package/dist/ui/src/components/form-builder/index.cjs +350 -0
  161. package/dist/ui/src/components/form-builder/index.mjs +344 -0
  162. package/dist/ui/src/components/form-builder/nested-field-editor-dialog.cjs +263 -0
  163. package/dist/ui/src/components/form-builder/nested-field-editor-dialog.mjs +261 -0
  164. package/dist/ui/src/components/form-builder/palette.cjs +52 -0
  165. package/dist/ui/src/components/form-builder/palette.mjs +49 -0
  166. package/dist/ui/src/components/form-builder/schema-utils.cjs +128 -0
  167. package/dist/ui/src/components/form-builder/schema-utils.mjs +122 -0
  168. package/dist/ui/src/components/form-builder/sortable-field.cjs +151 -0
  169. package/dist/ui/src/components/form-builder/sortable-field.mjs +148 -0
  170. package/dist/ui/src/components/form-builder/step-tabs.cjs +180 -0
  171. package/dist/ui/src/components/form-builder/step-tabs.mjs +178 -0
  172. package/dist/ui/src/components/form-builder/types.cjs +7 -0
  173. package/dist/ui/src/components/form-builder/types.mjs +5 -0
  174. package/dist/ui/src/components/form-builder/validation-schemas.cjs +67 -0
  175. package/dist/ui/src/components/form-builder/validation-schemas.mjs +56 -0
  176. package/dist/ui/src/components/form.cjs +120 -0
  177. package/dist/ui/src/components/form.mjs +98 -0
  178. package/dist/ui/src/components/input.cjs +23 -0
  179. package/dist/ui/src/components/input.mjs +21 -0
  180. package/dist/ui/src/components/label.cjs +39 -0
  181. package/dist/ui/src/components/label.mjs +23 -0
  182. package/dist/ui/src/components/page-layout.cjs +25 -0
  183. package/dist/ui/src/components/page-layout.mjs +23 -0
  184. package/dist/ui/src/components/page-wrapper.cjs +20 -0
  185. package/dist/ui/src/components/page-wrapper.mjs +18 -0
  186. package/dist/ui/src/components/popover.cjs +60 -0
  187. package/dist/ui/src/components/popover.mjs +42 -0
  188. package/dist/ui/src/components/radio-group.cjs +62 -0
  189. package/dist/ui/src/components/radio-group.mjs +45 -0
  190. package/dist/ui/src/components/select.cjs +161 -0
  191. package/dist/ui/src/components/select.mjs +139 -0
  192. package/dist/ui/src/components/separator.cjs +43 -0
  193. package/dist/ui/src/components/separator.mjs +27 -0
  194. package/dist/ui/src/components/skeleton.cjs +17 -0
  195. package/dist/ui/src/components/skeleton.mjs +15 -0
  196. package/dist/ui/src/components/stack-attribution.cjs +24 -0
  197. package/dist/ui/src/components/stack-attribution.mjs +22 -0
  198. package/dist/ui/src/components/switch.cjs +48 -0
  199. package/dist/ui/src/components/switch.mjs +32 -0
  200. package/dist/ui/src/components/table.cjs +89 -0
  201. package/dist/ui/src/components/table.mjs +82 -0
  202. package/dist/ui/src/components/tabs.cjs +84 -0
  203. package/dist/ui/src/components/tabs.mjs +65 -0
  204. package/dist/ui/src/components/textarea.cjs +20 -0
  205. package/dist/ui/src/components/textarea.mjs +18 -0
  206. package/dist/ui/src/lib/schema-converter.cjs +133 -0
  207. package/dist/ui/src/lib/schema-converter.mjs +127 -0
  208. package/dist/ui/src/lib/utils.cjs +10 -0
  209. package/dist/ui/src/lib/utils.mjs +8 -0
  210. package/dist/utils.cjs +41 -0
  211. package/dist/utils.mjs +32 -0
  212. package/package.json +175 -3
  213. package/README.md +0 -3
@@ -0,0 +1,1082 @@
1
+ import { OperationHttpError, defineOperation } from '@btst/stack/plugins/api';
2
+ import { formSchemaToZod } from '../ui/src/lib/schema-converter.mjs';
3
+ import { z } from 'zod';
4
+ import { formBuilderPermissions } from '../permissions.mjs';
5
+ import { listFormsQuerySchema, createFormSchema, updateFormSchema, listSubmissionsQuerySchema } from '../schemas.mjs';
6
+ import { slugify, extractUserAgent, extractIpAddress } from '../utils.mjs';
7
+ import { getAllForms, serializeFormSubmission, serializeForm, getFormSubmissions, serializeFormSubmissionSummary, serializeFormSubmissionWithData } from './getters.mjs';
8
+
9
+ const SubmissionDataSchema = z.record(z.string(), z.unknown()).transform((value) => value);
10
+ const CreateFormHookResultSchema = createFormSchema.extend({
11
+ createdBy: z.string().min(1).optional()
12
+ });
13
+ const SubmitFormOperationInputSchema = z.object({
14
+ slug: z.string(),
15
+ data: SubmissionDataSchema
16
+ });
17
+ const UpdateFormOperationInputSchema = z.object({
18
+ id: z.string(),
19
+ data: updateFormSchema
20
+ });
21
+ const DeleteFormOperationInputSchema = z.object({ id: z.string() });
22
+ const GetFormBySlugOperationInputSchema = z.object({ slug: z.string() });
23
+ const GetFormByIdOperationInputSchema = z.object({ id: z.string() });
24
+ const GetFormForUpdateOperationInputSchema = z.object({
25
+ id: z.string()
26
+ });
27
+ const ListSubmissionsOperationInputSchema = z.object({
28
+ formId: z.string(),
29
+ query: listSubmissionsQuerySchema
30
+ });
31
+ const GetSubmissionOperationInputSchema = z.object({
32
+ formId: z.string(),
33
+ submissionId: z.string()
34
+ });
35
+ class FormBuilderOperationError extends OperationHttpError {
36
+ constructor(statusCode, message, code, issues) {
37
+ super(statusCode, message, code, issues);
38
+ this.name = "FormBuilderOperationError";
39
+ }
40
+ }
41
+ function snapshotForm(form) {
42
+ return {
43
+ id: form.id,
44
+ slug: form.slug,
45
+ ...form.createdBy ? { ownerId: form.createdBy } : {},
46
+ status: form.status,
47
+ updatedAt: form.updatedAt
48
+ };
49
+ }
50
+ function snapshotSubmission(submission) {
51
+ return {
52
+ id: submission.id,
53
+ formId: submission.formId,
54
+ ...submission.submittedBy ? { submittedBy: submission.submittedBy } : {},
55
+ submittedAt: submission.submittedAt
56
+ };
57
+ }
58
+ function sameFormSnapshot(form, expected) {
59
+ if (!form || !expected) return form === null && expected === null;
60
+ const current = snapshotForm(form);
61
+ return current.id === expected.id && current.slug === expected.slug && current.ownerId === expected.ownerId && current.status === expected.status && current.updatedAt.getTime() === expected.updatedAt.getTime();
62
+ }
63
+ function sameSubmissionSnapshot(submission, expected) {
64
+ if (!submission) return false;
65
+ const current = snapshotSubmission(submission);
66
+ return current.id === expected.id && current.formId === expected.formId && current.submittedBy === expected.submittedBy && current.submittedAt.getTime() === expected.submittedAt.getTime();
67
+ }
68
+ const AFFECTED_ROW_KEYS = [
69
+ "rowCount",
70
+ "affectedRows",
71
+ "rowsAffected",
72
+ "changes",
73
+ "numUpdatedRows"
74
+ ];
75
+ function hasPositiveCount(value) {
76
+ if (typeof value === "number") return Number.isFinite(value) && value > 0;
77
+ if (typeof value === "bigint") return value > 0n;
78
+ return false;
79
+ }
80
+ function didAffectRow(result, expectedId) {
81
+ if (typeof result === "number" || typeof result === "bigint") {
82
+ return hasPositiveCount(result);
83
+ }
84
+ if (!result || typeof result !== "object") return false;
85
+ const record = result;
86
+ if ("count" in record) return hasPositiveCount(record.count);
87
+ if (Array.isArray(result)) {
88
+ return result.length > 0 && didAffectRow(result[0], expectedId);
89
+ }
90
+ for (const key of AFFECTED_ROW_KEYS) {
91
+ if (key in record) return hasPositiveCount(record[key]);
92
+ }
93
+ if ("meta" in record) {
94
+ const meta = record.meta;
95
+ return Boolean(
96
+ meta && typeof meta === "object" && "changes" in meta && hasPositiveCount(meta.changes)
97
+ );
98
+ }
99
+ return record.id === expectedId;
100
+ }
101
+ function formSnapshotWhere(snapshot) {
102
+ return [
103
+ { field: "id", value: snapshot.id, operator: "eq" },
104
+ { field: "slug", value: snapshot.slug, operator: "eq" },
105
+ { field: "status", value: snapshot.status, operator: "eq" },
106
+ {
107
+ field: "createdBy",
108
+ value: snapshot.ownerId ?? null,
109
+ operator: "eq"
110
+ },
111
+ {
112
+ field: "updatedAt",
113
+ value: snapshot.updatedAt,
114
+ operator: "eq"
115
+ }
116
+ ];
117
+ }
118
+ function submissionSnapshotWhere(snapshot) {
119
+ return [
120
+ { field: "id", value: snapshot.id, operator: "eq" },
121
+ { field: "formId", value: snapshot.formId, operator: "eq" },
122
+ {
123
+ field: "submittedBy",
124
+ value: snapshot.submittedBy ?? null,
125
+ operator: "eq"
126
+ },
127
+ {
128
+ field: "submittedAt",
129
+ value: snapshot.submittedAt,
130
+ operator: "eq"
131
+ }
132
+ ];
133
+ }
134
+ function nextSnapshotDate(previous) {
135
+ return new Date(Math.max(Date.now(), previous.getTime() + 1));
136
+ }
137
+ function requireAtomicTransactions(adapter) {
138
+ if (adapter.id === "memory" || typeof adapter.options?.adapterConfig.transaction !== "function") {
139
+ throw new FormBuilderOperationError(
140
+ 500,
141
+ "Form Builder owner-sensitive writes require an adapter with isolated transaction support.",
142
+ "ATOMIC_TRANSACTION_REQUIRED"
143
+ );
144
+ }
145
+ }
146
+ async function claimFormSnapshot(adapter, snapshot, stale) {
147
+ const claimedAt = nextSnapshotDate(snapshot.updatedAt);
148
+ const matched = await adapter.updateMany({
149
+ model: "form",
150
+ where: formSnapshotWhere(snapshot),
151
+ update: { updatedAt: claimedAt }
152
+ });
153
+ if (!didAffectRow(matched, snapshot.id)) throw stale();
154
+ return claimedAt;
155
+ }
156
+ async function restoreFormSnapshot(adapter, snapshot, claimedAt, stale) {
157
+ const restored = await adapter.updateMany({
158
+ model: "form",
159
+ where: formSnapshotWhere({ ...snapshot, updatedAt: claimedAt }),
160
+ update: { updatedAt: snapshot.updatedAt }
161
+ });
162
+ if (!didAffectRow(restored, snapshot.id)) throw stale();
163
+ }
164
+ async function claimSubmissionSnapshot(adapter, snapshot) {
165
+ const claimedAt = nextSnapshotDate(snapshot.submittedAt);
166
+ const matched = await adapter.updateMany({
167
+ model: "formSubmission",
168
+ where: submissionSnapshotWhere(snapshot),
169
+ update: { submittedAt: claimedAt }
170
+ });
171
+ if (!didAffectRow(matched, snapshot.id)) throw staleSubmissionError();
172
+ return claimedAt;
173
+ }
174
+ function staleFormError() {
175
+ return new FormBuilderOperationError(
176
+ 409,
177
+ "Form changed while authorization was being evaluated. Retry the operation.",
178
+ "FORM_STATE_CHANGED"
179
+ );
180
+ }
181
+ function staleSubmissionError() {
182
+ return new FormBuilderOperationError(
183
+ 409,
184
+ "Submission changed while authorization was being evaluated. Retry the operation.",
185
+ "SUBMISSION_STATE_CHANGED"
186
+ );
187
+ }
188
+ function formNotFoundError() {
189
+ return new FormBuilderOperationError(404, "Form not found", "FORM_NOT_FOUND");
190
+ }
191
+ function submissionNotFoundError() {
192
+ return new FormBuilderOperationError(
193
+ 404,
194
+ "Submission not found",
195
+ "SUBMISSION_NOT_FOUND"
196
+ );
197
+ }
198
+ async function findFormById(adapter, id) {
199
+ return await adapter.findOne({
200
+ model: "form",
201
+ where: [{ field: "id", value: id, operator: "eq" }]
202
+ }) ?? null;
203
+ }
204
+ async function findFormBySlug(adapter, slug) {
205
+ return await adapter.findOne({
206
+ model: "form",
207
+ where: [{ field: "slug", value: slug, operator: "eq" }]
208
+ }) ?? null;
209
+ }
210
+ async function findSubmission(adapter, submissionId) {
211
+ return await adapter.findOne({
212
+ model: "formSubmission",
213
+ where: [{ field: "id", value: submissionId, operator: "eq" }]
214
+ }) ?? null;
215
+ }
216
+ function requestFields(request) {
217
+ if (!request) return {};
218
+ return {
219
+ request,
220
+ headers: request.headers,
221
+ ...extractIpAddress(request.headers) ? { ipAddress: extractIpAddress(request.headers) } : {},
222
+ ...extractUserAgent(request.headers) ? { userAgent: extractUserAgent(request.headers) } : {}
223
+ };
224
+ }
225
+ function hookContext(context) {
226
+ return Object.freeze({
227
+ ...context,
228
+ ...context.identity ? { userId: context.identity.id } : {},
229
+ ...requestFields(context.request)
230
+ });
231
+ }
232
+ function submissionHookContext(context, formSlug, formId) {
233
+ return Object.freeze({
234
+ ...hookContext(context),
235
+ formSlug,
236
+ formId
237
+ });
238
+ }
239
+ function normalizeError(error, fallback) {
240
+ if (error instanceof Error) return error;
241
+ return new Error(typeof error === "string" ? error : fallback, {
242
+ cause: error
243
+ });
244
+ }
245
+ async function notifyError(hooks, error, operation, context) {
246
+ try {
247
+ await hooks?.onError?.(
248
+ normalizeError(error, "Form Builder operation failed."),
249
+ operation,
250
+ context
251
+ );
252
+ } catch {
253
+ }
254
+ }
255
+ async function runDomainHook(run, statusCode, code) {
256
+ try {
257
+ return await run();
258
+ } catch (error) {
259
+ throw new FormBuilderOperationError(
260
+ statusCode,
261
+ normalizeError(error, "Operation rejected").message,
262
+ code
263
+ );
264
+ }
265
+ }
266
+ function publicReadFacts(slug, form) {
267
+ return {
268
+ slug,
269
+ exists: form !== null,
270
+ ...form ? {
271
+ formId: form.id,
272
+ ...form.createdBy ? { ownerId: form.createdBy } : {},
273
+ status: form.status
274
+ } : {}
275
+ };
276
+ }
277
+ function recordReadFacts(id, form) {
278
+ return {
279
+ scope: "record",
280
+ formId: id,
281
+ exists: form !== null,
282
+ ...form ? {
283
+ ...form.createdBy ? { ownerId: form.createdBy } : {},
284
+ status: form.status
285
+ } : {}
286
+ };
287
+ }
288
+ function resolvedFormFacts(form) {
289
+ return {
290
+ formId: form.id,
291
+ ...form.createdBy ? { ownerId: form.createdBy } : {},
292
+ status: form.status
293
+ };
294
+ }
295
+ function submissionRecordFacts(formId, submissionId, form, submission) {
296
+ const exists = submission?.formId === formId && form?.id === formId;
297
+ return {
298
+ scope: "record",
299
+ formId,
300
+ submissionId,
301
+ exists,
302
+ ...form?.createdBy ? { ownerId: form.createdBy } : {},
303
+ ...exists && submission?.submittedBy ? { submittedBy: submission.submittedBy } : {}
304
+ };
305
+ }
306
+ function assertValidJsonSchema(schema) {
307
+ try {
308
+ JSON.parse(schema);
309
+ } catch {
310
+ throw new FormBuilderOperationError(
311
+ 400,
312
+ "Invalid JSON Schema",
313
+ "INVALID_FORM_SCHEMA"
314
+ );
315
+ }
316
+ }
317
+ function sanitizeSlug(rawSlug) {
318
+ const slug = slugify(rawSlug);
319
+ if (!slug) {
320
+ throw new FormBuilderOperationError(
321
+ 400,
322
+ "Invalid slug: must contain at least one alphanumeric character",
323
+ "INVALID_SLUG"
324
+ );
325
+ }
326
+ return slug;
327
+ }
328
+ function duplicateSlugError() {
329
+ return new FormBuilderOperationError(
330
+ 409,
331
+ "Form with this slug already exists",
332
+ "DUPLICATE_FORM_SLUG"
333
+ );
334
+ }
335
+ function operationForm(form) {
336
+ const serialized = serializeForm(form);
337
+ return {
338
+ id: serialized.id,
339
+ name: serialized.name,
340
+ slug: serialized.slug,
341
+ description: serialized.description,
342
+ schema: serialized.schema,
343
+ successMessage: serialized.successMessage,
344
+ redirectUrl: serialized.redirectUrl,
345
+ status: serialized.status,
346
+ createdBy: serialized.createdBy,
347
+ createdAt: serialized.createdAt,
348
+ updatedAt: serialized.updatedAt
349
+ };
350
+ }
351
+ function operationFormsResult(result) {
352
+ return {
353
+ items: result.items.map((form) => ({ ...form })),
354
+ total: result.total,
355
+ ...result.limit !== void 0 ? { limit: result.limit } : {},
356
+ ...result.offset !== void 0 ? { offset: result.offset } : {}
357
+ };
358
+ }
359
+ function operationJson(value) {
360
+ if (value === null) return null;
361
+ return JSON.parse(JSON.stringify(value));
362
+ }
363
+ function operationSubmissionWithData(submission) {
364
+ const serialized = serializeFormSubmissionWithData(submission);
365
+ return {
366
+ id: serialized.id,
367
+ formId: serialized.formId,
368
+ data: serialized.data,
369
+ submittedAt: serialized.submittedAt,
370
+ submittedBy: serialized.submittedBy,
371
+ ipAddress: serialized.ipAddress,
372
+ userAgent: serialized.userAgent,
373
+ parsedData: operationJson(serialized.parsedData)
374
+ };
375
+ }
376
+ function operationSubmissionsResult(result) {
377
+ return {
378
+ items: result.items.map(serializeFormSubmissionSummary),
379
+ total: result.total,
380
+ ...result.limit !== void 0 ? { limit: result.limit } : {},
381
+ ...result.offset !== void 0 ? { offset: result.offset } : {}
382
+ };
383
+ }
384
+ function operationSubmissionFormContext(form) {
385
+ return {
386
+ id: form.id,
387
+ name: form.name,
388
+ ...form.createdBy ? { createdBy: form.createdBy } : {}
389
+ };
390
+ }
391
+ function createFormBuilderOperations(adapter, hooks) {
392
+ const formSnapshots = /* @__PURE__ */ new WeakMap();
393
+ const submissionSnapshots = /* @__PURE__ */ new WeakMap();
394
+ const submittedFormSnapshots = /* @__PURE__ */ new WeakMap();
395
+ const listForms = defineOperation({
396
+ input: listFormsQuerySchema,
397
+ permission: formBuilderPermissions.form.read,
398
+ facts: () => ({ scope: "collection" }),
399
+ before: async (context) => {
400
+ await runDomainHook(
401
+ () => hooks?.onBeforeListForms?.(hookContext(context)),
402
+ 403,
403
+ "LIST_FORMS_REJECTED"
404
+ );
405
+ },
406
+ execute: async ({ input }) => operationFormsResult(await getAllForms(adapter, input)),
407
+ onError: ({ error, ...context }) => notifyError(hooks, error, "list", hookContext(context))
408
+ });
409
+ const getFormBySlug = defineOperation({
410
+ input: GetFormBySlugOperationInputSchema,
411
+ permission: formBuilderPermissions.form.render,
412
+ facts: async ({ input }) => {
413
+ const form = await findFormBySlug(adapter, input.slug);
414
+ formSnapshots.set(input, form ? snapshotForm(form) : null);
415
+ return publicReadFacts(input.slug, form);
416
+ },
417
+ before: async (context) => {
418
+ const current = await findFormBySlug(adapter, context.input.slug);
419
+ if (!sameFormSnapshot(
420
+ current,
421
+ formSnapshots.get(context.input) ?? null
422
+ )) {
423
+ throw staleFormError();
424
+ }
425
+ await runDomainHook(
426
+ () => hooks?.onBeforeGetForm?.(context.input.slug, hookContext(context)),
427
+ 403,
428
+ "GET_FORM_REJECTED"
429
+ );
430
+ },
431
+ execute: async (context) => {
432
+ const form = await findFormBySlug(adapter, context.input.slug);
433
+ if (!sameFormSnapshot(
434
+ form,
435
+ formSnapshots.get(context.input) ?? null
436
+ )) {
437
+ throw staleFormError();
438
+ }
439
+ if (!form) throw formNotFoundError();
440
+ return operationForm(form);
441
+ },
442
+ onError: ({ error, ...context }) => notifyError(hooks, error, "get", hookContext(context))
443
+ });
444
+ const getFormById = defineOperation({
445
+ input: GetFormByIdOperationInputSchema,
446
+ permission: formBuilderPermissions.form.read,
447
+ facts: async ({ input }) => {
448
+ const form = await findFormById(adapter, input.id);
449
+ formSnapshots.set(input, form ? snapshotForm(form) : null);
450
+ return recordReadFacts(input.id, form);
451
+ },
452
+ before: async (context) => {
453
+ const current = await findFormById(adapter, context.input.id);
454
+ if (!sameFormSnapshot(
455
+ current,
456
+ formSnapshots.get(context.input) ?? null
457
+ )) {
458
+ throw staleFormError();
459
+ }
460
+ await runDomainHook(
461
+ () => hooks?.onBeforeGetForm?.(context.input.id, hookContext(context)),
462
+ 403,
463
+ "GET_FORM_REJECTED"
464
+ );
465
+ },
466
+ execute: async (context) => {
467
+ const form = await findFormById(adapter, context.input.id);
468
+ if (!sameFormSnapshot(
469
+ form,
470
+ formSnapshots.get(context.input) ?? null
471
+ )) {
472
+ throw staleFormError();
473
+ }
474
+ if (!form) throw formNotFoundError();
475
+ return operationForm(form);
476
+ },
477
+ onError: ({ error, ...context }) => notifyError(hooks, error, "get", hookContext(context))
478
+ });
479
+ const getFormForUpdate = defineOperation({
480
+ input: GetFormForUpdateOperationInputSchema,
481
+ permission: formBuilderPermissions.form.update,
482
+ facts: async ({ input }) => {
483
+ const form = await findFormById(adapter, input.id);
484
+ if (!form) throw formNotFoundError();
485
+ formSnapshots.set(input, snapshotForm(form));
486
+ return resolvedFormFacts(form);
487
+ },
488
+ before: async (context) => {
489
+ const current = await findFormById(adapter, context.input.id);
490
+ if (!sameFormSnapshot(
491
+ current,
492
+ formSnapshots.get(context.input) ?? null
493
+ )) {
494
+ throw staleFormError();
495
+ }
496
+ await runDomainHook(
497
+ () => hooks?.onBeforeGetFormForUpdate?.(
498
+ context.input.id,
499
+ hookContext(context)
500
+ ),
501
+ 403,
502
+ "GET_FORM_REJECTED"
503
+ );
504
+ },
505
+ execute: async (context) => {
506
+ const form = await findFormById(adapter, context.input.id);
507
+ if (!sameFormSnapshot(
508
+ form,
509
+ formSnapshots.get(context.input) ?? null
510
+ )) {
511
+ throw staleFormError();
512
+ }
513
+ if (!form) throw formNotFoundError();
514
+ return operationForm(form);
515
+ },
516
+ onError: ({ error, ...context }) => notifyError(hooks, error, "get", hookContext(context))
517
+ });
518
+ const createForm = defineOperation({
519
+ input: createFormSchema,
520
+ permission: formBuilderPermissions.form.create,
521
+ facts: () => void 0,
522
+ execute: async (context) => {
523
+ const initial = {
524
+ ...context.input,
525
+ slug: sanitizeSlug(context.input.slug),
526
+ redirectUrl: context.input.redirectUrl || void 0,
527
+ ...context.identity ? { createdBy: context.identity.id } : {}
528
+ };
529
+ assertValidJsonSchema(initial.schema);
530
+ const modified = await runDomainHook(
531
+ async () => await hooks?.onBeforeCreateForm?.(initial, hookContext(context)) ?? initial,
532
+ 403,
533
+ "CREATE_FORM_REJECTED"
534
+ );
535
+ const validation = CreateFormHookResultSchema.safeParse(modified);
536
+ if (!validation.success) {
537
+ throw new FormBuilderOperationError(
538
+ 400,
539
+ "Invalid form data",
540
+ "FORM_VALIDATION_FAILED",
541
+ validation.error.issues
542
+ );
543
+ }
544
+ const formInput = {
545
+ ...validation.data,
546
+ slug: sanitizeSlug(validation.data.slug),
547
+ ...context.identity ? { createdBy: context.identity.id } : {}
548
+ };
549
+ assertValidJsonSchema(formInput.schema);
550
+ return adapter.transaction(async (tx) => {
551
+ const duplicate = await findFormBySlug(tx, formInput.slug);
552
+ if (duplicate) throw duplicateSlugError();
553
+ const now = /* @__PURE__ */ new Date();
554
+ return operationForm(
555
+ await tx.create({
556
+ model: "form",
557
+ data: {
558
+ name: formInput.name,
559
+ slug: formInput.slug,
560
+ description: formInput.description,
561
+ schema: formInput.schema,
562
+ successMessage: formInput.successMessage,
563
+ redirectUrl: formInput.redirectUrl,
564
+ status: formInput.status ?? "active",
565
+ createdBy: formInput.createdBy,
566
+ createdAt: now,
567
+ updatedAt: now
568
+ }
569
+ })
570
+ );
571
+ });
572
+ },
573
+ after: async (context) => {
574
+ await hooks?.onAfterCreateForm?.(context.result, hookContext(context));
575
+ },
576
+ onError: ({ error, ...context }) => notifyError(hooks, error, "create", hookContext(context))
577
+ });
578
+ const updateForm = defineOperation({
579
+ input: UpdateFormOperationInputSchema,
580
+ permission: formBuilderPermissions.form.update,
581
+ facts: async ({ input }) => {
582
+ const form = await findFormById(adapter, input.id);
583
+ if (!form) throw formNotFoundError();
584
+ formSnapshots.set(input, snapshotForm(form));
585
+ return resolvedFormFacts(form);
586
+ },
587
+ execute: async (context) => {
588
+ const authorized = formSnapshots.get(context.input);
589
+ if (!authorized) throw staleFormError();
590
+ requireAtomicTransactions(adapter);
591
+ const initial = {
592
+ ...context.input.data,
593
+ ...context.input.data.slug !== void 0 ? { slug: sanitizeSlug(context.input.data.slug) } : {}
594
+ };
595
+ if (initial.schema !== void 0) assertValidJsonSchema(initial.schema);
596
+ return adapter.transaction(async (tx) => {
597
+ const latest = await findFormById(tx, context.input.id);
598
+ if (!sameFormSnapshot(latest, authorized)) throw staleFormError();
599
+ const claimedAt = await claimFormSnapshot(
600
+ tx,
601
+ authorized,
602
+ staleFormError
603
+ );
604
+ const modified = await runDomainHook(
605
+ async () => await hooks?.onBeforeUpdateForm?.(
606
+ context.input.id,
607
+ initial,
608
+ hookContext(context)
609
+ ) ?? initial,
610
+ 403,
611
+ "UPDATE_FORM_REJECTED"
612
+ );
613
+ const validation = updateFormSchema.safeParse(modified);
614
+ if (!validation.success) {
615
+ throw new FormBuilderOperationError(
616
+ 400,
617
+ "Invalid form data",
618
+ "FORM_VALIDATION_FAILED",
619
+ validation.error.issues
620
+ );
621
+ }
622
+ const update = {
623
+ ...validation.data,
624
+ ...validation.data.slug !== void 0 ? { slug: sanitizeSlug(validation.data.slug) } : {}
625
+ };
626
+ if (update.schema !== void 0) assertValidJsonSchema(update.schema);
627
+ const claimed = await findFormById(tx, authorized.id);
628
+ if (!sameFormSnapshot(claimed, { ...authorized, updatedAt: claimedAt })) {
629
+ throw staleFormError();
630
+ }
631
+ if (update.slug && update.slug !== latest?.slug) {
632
+ const duplicate = await findFormBySlug(tx, update.slug);
633
+ if (duplicate) throw duplicateSlugError();
634
+ }
635
+ const updateData = {
636
+ updatedAt: nextSnapshotDate(claimedAt)
637
+ };
638
+ if (update.name !== void 0) updateData.name = update.name;
639
+ if (update.slug !== void 0) updateData.slug = update.slug;
640
+ if (update.description !== void 0)
641
+ updateData.description = update.description;
642
+ if (update.schema !== void 0) updateData.schema = update.schema;
643
+ if (update.successMessage !== void 0)
644
+ updateData.successMessage = update.successMessage;
645
+ if (update.redirectUrl !== void 0)
646
+ updateData.redirectUrl = update.redirectUrl;
647
+ if (update.status !== void 0) updateData.status = update.status;
648
+ const matched = await tx.updateMany({
649
+ model: "form",
650
+ where: formSnapshotWhere({ ...authorized, updatedAt: claimedAt }),
651
+ update: updateData
652
+ });
653
+ if (!didAffectRow(matched, authorized.id)) throw staleFormError();
654
+ const updated = await findFormById(tx, context.input.id);
655
+ if (!updated) throw formNotFoundError();
656
+ return operationForm(updated);
657
+ });
658
+ },
659
+ after: async (context) => {
660
+ await hooks?.onAfterUpdateForm?.(context.result, hookContext(context));
661
+ },
662
+ onError: ({ error, ...context }) => notifyError(hooks, error, "update", hookContext(context))
663
+ });
664
+ const deleteForm = defineOperation({
665
+ input: DeleteFormOperationInputSchema,
666
+ permission: formBuilderPermissions.form.delete,
667
+ facts: async ({ input }) => {
668
+ const form = await findFormById(adapter, input.id);
669
+ if (!form) throw formNotFoundError();
670
+ formSnapshots.set(input, snapshotForm(form));
671
+ return resolvedFormFacts(form);
672
+ },
673
+ execute: async (context) => {
674
+ const authorized = formSnapshots.get(context.input);
675
+ if (!authorized) throw staleFormError();
676
+ requireAtomicTransactions(adapter);
677
+ return adapter.transaction(async (tx) => {
678
+ const current = await findFormById(tx, context.input.id);
679
+ if (!sameFormSnapshot(current, authorized)) throw staleFormError();
680
+ const claimedAt = await claimFormSnapshot(
681
+ tx,
682
+ authorized,
683
+ staleFormError
684
+ );
685
+ await runDomainHook(
686
+ () => hooks?.onBeforeDeleteForm?.(context.input.id, hookContext(context)),
687
+ 403,
688
+ "DELETE_FORM_REJECTED"
689
+ );
690
+ const claimed = await findFormById(tx, authorized.id);
691
+ if (!sameFormSnapshot(claimed, { ...authorized, updatedAt: claimedAt })) {
692
+ throw staleFormError();
693
+ }
694
+ await tx.deleteMany({
695
+ model: "formSubmission",
696
+ where: [
697
+ {
698
+ field: "formId",
699
+ value: context.input.id,
700
+ operator: "eq"
701
+ }
702
+ ]
703
+ });
704
+ const deleted = await tx.deleteMany({
705
+ model: "form",
706
+ where: [
707
+ { field: "id", value: authorized.id, operator: "eq" },
708
+ { field: "updatedAt", value: claimedAt, operator: "eq" }
709
+ ]
710
+ });
711
+ if (deleted !== 1) throw staleFormError();
712
+ return { success: true };
713
+ });
714
+ },
715
+ after: async (context) => {
716
+ await hooks?.onAfterDeleteForm?.(context.input.id, hookContext(context));
717
+ },
718
+ onError: ({ error, ...context }) => notifyError(hooks, error, "delete", hookContext(context))
719
+ });
720
+ const submitForm = defineOperation({
721
+ input: SubmitFormOperationInputSchema,
722
+ permission: formBuilderPermissions.submission.create,
723
+ facts: async ({ input }) => {
724
+ const form = await findFormBySlug(adapter, input.slug);
725
+ formSnapshots.set(input, form ? snapshotForm(form) : null);
726
+ return {
727
+ slug: input.slug,
728
+ exists: form !== null,
729
+ ...form ? {
730
+ formId: form.id,
731
+ ...form.createdBy ? { ownerId: form.createdBy } : {},
732
+ status: form.status
733
+ } : {}
734
+ };
735
+ },
736
+ execute: async (context) => {
737
+ const authorized = formSnapshots.get(context.input) ?? null;
738
+ const form = await findFormBySlug(adapter, context.input.slug);
739
+ if (!sameFormSnapshot(form, authorized)) throw staleFormError();
740
+ if (!form) throw formNotFoundError();
741
+ if (!authorized) throw staleFormError();
742
+ requireAtomicTransactions(adapter);
743
+ if (form.status !== "active") {
744
+ throw new FormBuilderOperationError(
745
+ 400,
746
+ "Form is not accepting submissions",
747
+ "FORM_NOT_ACTIVE"
748
+ );
749
+ }
750
+ try {
751
+ const jsonSchema = JSON.parse(form.schema);
752
+ const validation = formSchemaToZod(jsonSchema).safeParse(
753
+ context.input.data
754
+ );
755
+ if (!validation.success) {
756
+ throw new FormBuilderOperationError(
757
+ 400,
758
+ "Validation failed",
759
+ "SUBMISSION_VALIDATION_FAILED",
760
+ validation.error.issues
761
+ );
762
+ }
763
+ } catch (error) {
764
+ if (error instanceof FormBuilderOperationError) throw error;
765
+ throw new FormBuilderOperationError(
766
+ 400,
767
+ "Invalid form data",
768
+ "INVALID_FORM_DATA"
769
+ );
770
+ }
771
+ const lifecycleContext = submissionHookContext(
772
+ context,
773
+ form.slug,
774
+ form.id
775
+ );
776
+ const submittedData = JSON.parse(
777
+ JSON.stringify(context.input.data)
778
+ );
779
+ return adapter.transaction(async (tx) => {
780
+ const current = await findFormBySlug(tx, context.input.slug);
781
+ if (!sameFormSnapshot(current, authorized)) throw staleFormError();
782
+ if (!current || current.status !== "active") throw staleFormError();
783
+ const claimedAt = await claimFormSnapshot(
784
+ tx,
785
+ authorized,
786
+ staleFormError
787
+ );
788
+ const hookResult = await runDomainHook(
789
+ () => hooks?.onBeforeSubmission?.(
790
+ form.slug,
791
+ submittedData,
792
+ lifecycleContext
793
+ ),
794
+ 400,
795
+ "SUBMISSION_REJECTED"
796
+ );
797
+ const finalData = hookResult ?? submittedData;
798
+ const claimed = await findFormById(tx, authorized.id);
799
+ if (!sameFormSnapshot(claimed, { ...authorized, updatedAt: claimedAt }) || claimed?.status !== "active") {
800
+ throw staleFormError();
801
+ }
802
+ const submission = await tx.create({
803
+ model: "formSubmission",
804
+ data: {
805
+ formId: current.id,
806
+ data: JSON.stringify(finalData),
807
+ submittedAt: /* @__PURE__ */ new Date(),
808
+ ...context.identity ? { submittedBy: context.identity.id } : {},
809
+ ...lifecycleContext.ipAddress ? { ipAddress: lifecycleContext.ipAddress } : {},
810
+ ...lifecycleContext.userAgent ? { userAgent: lifecycleContext.userAgent } : {}
811
+ }
812
+ });
813
+ await restoreFormSnapshot(tx, authorized, claimedAt, staleFormError);
814
+ const serialized = serializeFormSubmission(submission);
815
+ const result = {
816
+ ...serialized,
817
+ form: {
818
+ successMessage: current.successMessage,
819
+ redirectUrl: current.redirectUrl
820
+ }
821
+ };
822
+ submittedFormSnapshots.set(result, serializeForm(current));
823
+ return result;
824
+ });
825
+ },
826
+ after: async (context) => {
827
+ const form = submittedFormSnapshots.get(context.result);
828
+ if (!form) {
829
+ throw new FormBuilderOperationError(
830
+ 500,
831
+ "Submission lifecycle state is unavailable",
832
+ "SUBMISSION_LIFECYCLE_STATE_MISSING"
833
+ );
834
+ }
835
+ await hooks?.onAfterSubmission?.(
836
+ context.result,
837
+ form,
838
+ submissionHookContext(context, form.slug, form.id)
839
+ );
840
+ },
841
+ onError: async ({ error, ...context }) => {
842
+ const facts = context.facts;
843
+ const formId = facts.formId ?? "";
844
+ const lifecycleContext = submissionHookContext(context, context.input.slug, formId);
845
+ try {
846
+ await hooks?.onErrorSubmission?.(
847
+ normalizeError(error, "Submission failed."),
848
+ context.input.slug,
849
+ JSON.parse(JSON.stringify(context.input.data)),
850
+ lifecycleContext
851
+ );
852
+ } catch {
853
+ }
854
+ await notifyError(hooks, error, "submit", lifecycleContext);
855
+ }
856
+ });
857
+ const listSubmissions = defineOperation({
858
+ input: ListSubmissionsOperationInputSchema,
859
+ permission: formBuilderPermissions.submission.read,
860
+ facts: async ({ input }) => {
861
+ const form = await findFormById(adapter, input.formId);
862
+ formSnapshots.set(input, form ? snapshotForm(form) : null);
863
+ return {
864
+ scope: "collection",
865
+ formId: input.formId,
866
+ formExists: form !== null,
867
+ ...form?.createdBy ? { ownerId: form.createdBy } : {}
868
+ };
869
+ },
870
+ before: async (context) => {
871
+ const expected = formSnapshots.get(context.input) ?? null;
872
+ const current = await findFormById(adapter, context.input.formId);
873
+ if (!sameFormSnapshot(current, expected)) throw staleFormError();
874
+ if (!current) throw formNotFoundError();
875
+ await runDomainHook(
876
+ () => hooks?.onBeforeListSubmissions?.(
877
+ context.input.formId,
878
+ hookContext(context)
879
+ ),
880
+ 403,
881
+ "LIST_SUBMISSIONS_REJECTED"
882
+ );
883
+ },
884
+ execute: async (context) => {
885
+ const expected = formSnapshots.get(context.input) ?? null;
886
+ return adapter.transaction(async (tx) => {
887
+ const before = await findFormById(tx, context.input.formId);
888
+ if (!sameFormSnapshot(before, expected)) throw staleFormError();
889
+ if (!before) throw formNotFoundError();
890
+ const result = await getFormSubmissions(
891
+ tx,
892
+ context.input.formId,
893
+ context.input.query
894
+ );
895
+ const after = await findFormById(tx, context.input.formId);
896
+ if (!sameFormSnapshot(after, expected)) throw staleFormError();
897
+ if (!after) throw formNotFoundError();
898
+ return {
899
+ form: operationSubmissionFormContext(after),
900
+ ...operationSubmissionsResult(result)
901
+ };
902
+ });
903
+ },
904
+ onError: ({ error, ...context }) => notifyError(hooks, error, "listSubmissions", hookContext(context))
905
+ });
906
+ const getSubmission = defineOperation({
907
+ input: GetSubmissionOperationInputSchema,
908
+ permission: formBuilderPermissions.submission.read,
909
+ facts: async ({ input }) => {
910
+ const [form, submission] = await Promise.all([
911
+ findFormById(adapter, input.formId),
912
+ findSubmission(adapter, input.submissionId)
913
+ ]);
914
+ const matches = form && submission?.formId === form.id;
915
+ submissionSnapshots.set(
916
+ input,
917
+ matches ? {
918
+ form: snapshotForm(form),
919
+ submission: snapshotSubmission(submission)
920
+ } : null
921
+ );
922
+ return submissionRecordFacts(
923
+ input.formId,
924
+ input.submissionId,
925
+ form,
926
+ submission
927
+ );
928
+ },
929
+ before: async (context) => {
930
+ const snapshot = submissionSnapshots.get(context.input);
931
+ if (!snapshot) throw submissionNotFoundError();
932
+ const [form, submission] = await Promise.all([
933
+ findFormById(adapter, context.input.formId),
934
+ findSubmission(adapter, context.input.submissionId)
935
+ ]);
936
+ if (!sameFormSnapshot(form, snapshot.form) || !sameSubmissionSnapshot(submission, snapshot.submission)) {
937
+ throw staleSubmissionError();
938
+ }
939
+ await runDomainHook(
940
+ () => hooks?.onBeforeGetSubmission?.(
941
+ context.input.submissionId,
942
+ hookContext(context)
943
+ ),
944
+ 403,
945
+ "GET_SUBMISSION_REJECTED"
946
+ );
947
+ },
948
+ execute: async (context) => {
949
+ const snapshot = submissionSnapshots.get(context.input);
950
+ if (!snapshot) throw submissionNotFoundError();
951
+ const submission = await adapter.findOne({
952
+ model: "formSubmission",
953
+ where: [
954
+ {
955
+ field: "id",
956
+ value: context.input.submissionId,
957
+ operator: "eq"
958
+ }
959
+ ],
960
+ join: { form: true }
961
+ });
962
+ if (!submission || submission.formId !== context.input.formId || !sameSubmissionSnapshot(submission, snapshot.submission) || !sameFormSnapshot(submission.form ?? null, snapshot.form)) {
963
+ throw staleSubmissionError();
964
+ }
965
+ return operationSubmissionWithData(submission);
966
+ },
967
+ onError: ({ error, ...context }) => notifyError(hooks, error, "getSubmission", hookContext(context))
968
+ });
969
+ const deleteSubmission = defineOperation({
970
+ input: GetSubmissionOperationInputSchema,
971
+ permission: formBuilderPermissions.submission.delete,
972
+ facts: async ({ input }) => {
973
+ const [form, submission] = await Promise.all([
974
+ findFormById(adapter, input.formId),
975
+ findSubmission(adapter, input.submissionId)
976
+ ]);
977
+ if (!form || !submission || submission.formId !== form.id) {
978
+ throw submissionNotFoundError();
979
+ }
980
+ submissionSnapshots.set(input, {
981
+ form: snapshotForm(form),
982
+ submission: snapshotSubmission(submission)
983
+ });
984
+ return {
985
+ formId: form.id,
986
+ submissionId: submission.id,
987
+ exists: true,
988
+ ...form.createdBy ? { ownerId: form.createdBy } : {},
989
+ ...submission.submittedBy ? { submittedBy: submission.submittedBy } : {}
990
+ };
991
+ },
992
+ execute: async (context) => {
993
+ const snapshot = submissionSnapshots.get(context.input);
994
+ if (!snapshot) throw staleSubmissionError();
995
+ requireAtomicTransactions(adapter);
996
+ return adapter.transaction(async (tx) => {
997
+ const [form, submission] = await Promise.all([
998
+ findFormById(tx, context.input.formId),
999
+ findSubmission(tx, context.input.submissionId)
1000
+ ]);
1001
+ if (!sameFormSnapshot(form, snapshot.form) || !sameSubmissionSnapshot(submission, snapshot.submission)) {
1002
+ throw staleSubmissionError();
1003
+ }
1004
+ const claimedAt = await claimFormSnapshot(
1005
+ tx,
1006
+ snapshot.form,
1007
+ staleSubmissionError
1008
+ );
1009
+ const claimedSubmissionAt = await claimSubmissionSnapshot(
1010
+ tx,
1011
+ snapshot.submission
1012
+ );
1013
+ await runDomainHook(
1014
+ () => hooks?.onBeforeDeleteSubmission?.(
1015
+ context.input.submissionId,
1016
+ hookContext(context)
1017
+ ),
1018
+ 403,
1019
+ "DELETE_SUBMISSION_REJECTED"
1020
+ );
1021
+ const [claimedForm, claimedSubmission] = await Promise.all([
1022
+ findFormById(tx, snapshot.form.id),
1023
+ findSubmission(tx, snapshot.submission.id)
1024
+ ]);
1025
+ if (!sameFormSnapshot(claimedForm, {
1026
+ ...snapshot.form,
1027
+ updatedAt: claimedAt
1028
+ }) || !sameSubmissionSnapshot(claimedSubmission, {
1029
+ ...snapshot.submission,
1030
+ submittedAt: claimedSubmissionAt
1031
+ })) {
1032
+ throw staleSubmissionError();
1033
+ }
1034
+ const deleted = await tx.deleteMany({
1035
+ model: "formSubmission",
1036
+ where: [
1037
+ {
1038
+ field: "id",
1039
+ value: context.input.submissionId,
1040
+ operator: "eq"
1041
+ },
1042
+ {
1043
+ field: "submittedAt",
1044
+ value: claimedSubmissionAt,
1045
+ operator: "eq"
1046
+ }
1047
+ ]
1048
+ });
1049
+ if (deleted !== 1) throw staleSubmissionError();
1050
+ await restoreFormSnapshot(
1051
+ tx,
1052
+ snapshot.form,
1053
+ claimedAt,
1054
+ staleSubmissionError
1055
+ );
1056
+ return { success: true };
1057
+ });
1058
+ },
1059
+ after: async (context) => {
1060
+ await hooks?.onAfterDeleteSubmission?.(
1061
+ context.input.submissionId,
1062
+ hookContext(context)
1063
+ );
1064
+ },
1065
+ onError: ({ error, ...context }) => notifyError(hooks, error, "deleteSubmission", hookContext(context))
1066
+ });
1067
+ return {
1068
+ listForms,
1069
+ getFormBySlug,
1070
+ getFormById,
1071
+ getFormForUpdate,
1072
+ createForm,
1073
+ updateForm,
1074
+ deleteForm,
1075
+ submitForm,
1076
+ listSubmissions,
1077
+ getSubmission,
1078
+ deleteSubmission
1079
+ };
1080
+ }
1081
+
1082
+ export { DeleteFormOperationInputSchema, FormBuilderOperationError, GetFormByIdOperationInputSchema, GetFormBySlugOperationInputSchema, GetFormForUpdateOperationInputSchema, GetSubmissionOperationInputSchema, ListSubmissionsOperationInputSchema, SubmitFormOperationInputSchema, UpdateFormOperationInputSchema, createFormBuilderOperations };