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