@btst/stack 1.5.2 → 1.7.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 (241) hide show
  1. package/dist/api/index.cjs +7 -1
  2. package/dist/api/index.d.cts +2 -2
  3. package/dist/api/index.d.mts +2 -2
  4. package/dist/api/index.d.ts +2 -2
  5. package/dist/api/index.mjs +7 -1
  6. package/dist/client/index.d.cts +1 -1
  7. package/dist/client/index.d.mts +1 -1
  8. package/dist/client/index.d.ts +1 -1
  9. package/dist/index.d.cts +1 -1
  10. package/dist/index.d.mts +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/node_modules/.pnpm/@dnd-kit_accessibility@3.1.1_react@19.2.0/node_modules/@dnd-kit/accessibility/dist/accessibility.esm.cjs +68 -0
  13. package/dist/node_modules/.pnpm/@dnd-kit_accessibility@3.1.1_react@19.2.0/node_modules/@dnd-kit/accessibility/dist/accessibility.esm.mjs +60 -0
  14. package/dist/node_modules/.pnpm/@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@dnd-kit/core/dist/core.esm.cjs +3937 -0
  15. package/dist/node_modules/.pnpm/@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@dnd-kit/core/dist/core.esm.mjs +3907 -0
  16. package/dist/node_modules/.pnpm/@dnd-kit_modifiers@9.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/modifiers/dist/modifiers.esm.cjs +30 -0
  17. package/dist/node_modules/.pnpm/@dnd-kit_modifiers@9.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/modifiers/dist/modifiers.esm.mjs +28 -0
  18. package/dist/node_modules/.pnpm/@dnd-kit_sortable@10.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/sortable/dist/sortable.esm.cjs +675 -0
  19. package/dist/node_modules/.pnpm/@dnd-kit_sortable@10.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/sortable/dist/sortable.esm.mjs +661 -0
  20. package/dist/node_modules/.pnpm/@dnd-kit_utilities@3.2.2_react@19.2.0/node_modules/@dnd-kit/utilities/dist/utilities.esm.cjs +358 -0
  21. package/dist/node_modules/.pnpm/@dnd-kit_utilities@3.2.2_react@19.2.0/node_modules/@dnd-kit/utilities/dist/utilities.esm.mjs +332 -0
  22. package/dist/node_modules/.pnpm/@radix-ui_react-tabs@1.1.13_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react@1_865f042350eb43f3338b0fffb33f6246/node_modules/@radix-ui/react-tabs/dist/index.cjs +211 -0
  23. package/dist/node_modules/.pnpm/@radix-ui_react-tabs@1.1.13_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react@1_865f042350eb43f3338b0fffb33f6246/node_modules/@radix-ui/react-tabs/dist/index.mjs +188 -0
  24. package/dist/packages/better-stack/src/plugins/cms/api/plugin.cjs +3 -2
  25. package/dist/packages/better-stack/src/plugins/cms/api/plugin.mjs +3 -2
  26. package/dist/packages/better-stack/src/plugins/cms/client/components/forms/content-form.cjs +15 -15
  27. package/dist/packages/better-stack/src/plugins/cms/client/components/forms/content-form.mjs +16 -16
  28. package/dist/packages/better-stack/src/plugins/form-builder/api/plugin.cjs +588 -0
  29. package/dist/packages/better-stack/src/plugins/form-builder/api/plugin.mjs +586 -0
  30. package/dist/packages/better-stack/src/plugins/form-builder/client/components/forms/form-renderer.cjs +131 -0
  31. package/dist/packages/better-stack/src/plugins/form-builder/client/components/forms/form-renderer.mjs +129 -0
  32. package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/form-builder-skeleton.cjs +32 -0
  33. package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/form-builder-skeleton.mjs +30 -0
  34. package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/form-list-skeleton.cjs +21 -0
  35. package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/form-list-skeleton.mjs +19 -0
  36. package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/submissions-skeleton.cjs +34 -0
  37. package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/submissions-skeleton.mjs +32 -0
  38. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/404-page.cjs +20 -0
  39. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/404-page.mjs +18 -0
  40. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-builder-page.cjs +19 -0
  41. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-builder-page.internal.cjs +186 -0
  42. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-builder-page.internal.mjs +184 -0
  43. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-builder-page.mjs +17 -0
  44. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-list-page.cjs +19 -0
  45. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-list-page.internal.cjs +165 -0
  46. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-list-page.internal.mjs +163 -0
  47. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-list-page.mjs +17 -0
  48. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/submissions-page.cjs +19 -0
  49. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.cjs +177 -0
  50. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.mjs +175 -0
  51. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/submissions-page.mjs +17 -0
  52. package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/default-error.cjs +17 -0
  53. package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/default-error.mjs +15 -0
  54. package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/empty-state.cjs +16 -0
  55. package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/empty-state.mjs +14 -0
  56. package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/page-wrapper.cjs +27 -0
  57. package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/page-wrapper.mjs +25 -0
  58. package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/pagination.cjs +39 -0
  59. package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/pagination.mjs +37 -0
  60. package/dist/packages/better-stack/src/plugins/form-builder/client/hooks/form-builder-hooks.cjs +551 -0
  61. package/dist/packages/better-stack/src/plugins/form-builder/client/hooks/form-builder-hooks.mjs +537 -0
  62. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-common.cjs +36 -0
  63. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-common.mjs +34 -0
  64. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-editor.cjs +19 -0
  65. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-editor.mjs +17 -0
  66. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-list.cjs +21 -0
  67. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-list.mjs +19 -0
  68. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-submissions.cjs +19 -0
  69. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-submissions.mjs +17 -0
  70. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-toasts.cjs +14 -0
  71. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-toasts.mjs +12 -0
  72. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/index.cjs +17 -0
  73. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/index.mjs +15 -0
  74. package/dist/packages/better-stack/src/plugins/form-builder/client/plugin.cjs +278 -0
  75. package/dist/packages/better-stack/src/plugins/form-builder/client/plugin.mjs +276 -0
  76. package/dist/packages/better-stack/src/plugins/form-builder/db.cjs +99 -0
  77. package/dist/packages/better-stack/src/plugins/form-builder/db.mjs +97 -0
  78. package/dist/packages/better-stack/src/plugins/form-builder/schemas.cjs +82 -0
  79. package/dist/packages/better-stack/src/plugins/form-builder/schemas.mjs +74 -0
  80. package/dist/packages/better-stack/src/plugins/form-builder/utils.cjs +37 -0
  81. package/dist/packages/better-stack/src/plugins/form-builder/utils.mjs +29 -0
  82. package/dist/packages/better-stack/src/plugins/open-api/api/generator.cjs +300 -0
  83. package/dist/packages/better-stack/src/plugins/open-api/api/generator.mjs +284 -0
  84. package/dist/packages/better-stack/src/plugins/open-api/api/plugin.cjs +115 -0
  85. package/dist/packages/better-stack/src/plugins/open-api/api/plugin.mjs +113 -0
  86. package/dist/packages/better-stack/src/plugins/open-api/db.cjs +7 -0
  87. package/dist/packages/better-stack/src/plugins/open-api/db.mjs +5 -0
  88. package/dist/packages/better-stack/src/plugins/open-api/logo.cjs +8 -0
  89. package/dist/packages/better-stack/src/plugins/open-api/logo.mjs +6 -0
  90. package/dist/packages/ui/src/components/auto-form/index.cjs +2 -12
  91. package/dist/packages/ui/src/components/auto-form/index.mjs +2 -9
  92. package/dist/packages/ui/src/components/auto-form/stepped-auto-form.cjs +377 -0
  93. package/dist/packages/ui/src/components/auto-form/stepped-auto-form.mjs +368 -0
  94. package/dist/packages/ui/src/components/auto-form/utils.cjs +1 -56
  95. package/dist/packages/ui/src/components/auto-form/utils.mjs +2 -56
  96. package/dist/packages/ui/src/components/form-builder/canvas.cjs +111 -0
  97. package/dist/packages/ui/src/components/form-builder/canvas.mjs +109 -0
  98. package/dist/packages/ui/src/components/form-builder/components/index.cjs +570 -0
  99. package/dist/packages/ui/src/components/form-builder/components/index.mjs +553 -0
  100. package/dist/packages/ui/src/components/form-builder/edit-field-dialog.cjs +131 -0
  101. package/dist/packages/ui/src/components/form-builder/edit-field-dialog.mjs +129 -0
  102. package/dist/packages/ui/src/components/form-builder/form-preview.cjs +73 -0
  103. package/dist/packages/ui/src/components/form-builder/form-preview.mjs +71 -0
  104. package/dist/packages/ui/src/components/form-builder/index.cjs +353 -0
  105. package/dist/packages/ui/src/components/form-builder/index.mjs +344 -0
  106. package/dist/packages/ui/src/components/form-builder/nested-field-editor-dialog.cjs +263 -0
  107. package/dist/packages/ui/src/components/form-builder/nested-field-editor-dialog.mjs +261 -0
  108. package/dist/packages/ui/src/components/form-builder/palette.cjs +52 -0
  109. package/dist/packages/ui/src/components/form-builder/palette.mjs +49 -0
  110. package/dist/packages/ui/src/components/form-builder/schema-utils.cjs +120 -0
  111. package/dist/packages/ui/src/components/form-builder/schema-utils.mjs +114 -0
  112. package/dist/packages/ui/src/components/form-builder/sortable-field.cjs +151 -0
  113. package/dist/packages/ui/src/components/form-builder/sortable-field.mjs +148 -0
  114. package/dist/packages/ui/src/components/form-builder/step-tabs.cjs +180 -0
  115. package/dist/packages/ui/src/components/form-builder/step-tabs.mjs +178 -0
  116. package/dist/packages/ui/src/components/form-builder/types.cjs +7 -0
  117. package/dist/packages/ui/src/components/form-builder/types.mjs +5 -0
  118. package/dist/packages/ui/src/components/form-builder/validation-schemas.cjs +67 -0
  119. package/dist/packages/ui/src/components/form-builder/validation-schemas.mjs +56 -0
  120. package/dist/packages/ui/src/components/tabs.cjs +70 -0
  121. package/dist/packages/ui/src/components/tabs.mjs +65 -0
  122. package/dist/packages/ui/src/lib/schema-converter.cjs +130 -0
  123. package/dist/packages/ui/src/lib/schema-converter.mjs +124 -0
  124. package/dist/plugins/api/index.d.cts +2 -2
  125. package/dist/plugins/api/index.d.mts +2 -2
  126. package/dist/plugins/api/index.d.ts +2 -2
  127. package/dist/plugins/blog/api/index.d.cts +1 -1
  128. package/dist/plugins/blog/api/index.d.mts +1 -1
  129. package/dist/plugins/blog/api/index.d.ts +1 -1
  130. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  131. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  132. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  133. package/dist/plugins/blog/client/index.d.cts +1 -1
  134. package/dist/plugins/blog/client/index.d.mts +1 -1
  135. package/dist/plugins/blog/client/index.d.ts +1 -1
  136. package/dist/plugins/blog/query-keys.d.cts +2 -2
  137. package/dist/plugins/blog/query-keys.d.mts +2 -2
  138. package/dist/plugins/blog/query-keys.d.ts +2 -2
  139. package/dist/plugins/client/index.d.cts +2 -2
  140. package/dist/plugins/client/index.d.mts +2 -2
  141. package/dist/plugins/client/index.d.ts +2 -2
  142. package/dist/plugins/cms/client/index.cjs +6 -0
  143. package/dist/plugins/cms/client/index.d.cts +6 -113
  144. package/dist/plugins/cms/client/index.d.mts +6 -113
  145. package/dist/plugins/cms/client/index.d.ts +6 -113
  146. package/dist/plugins/cms/client/index.mjs +1 -0
  147. package/dist/plugins/form-builder/api/index.cjs +7 -0
  148. package/dist/plugins/form-builder/api/index.d.cts +141 -0
  149. package/dist/plugins/form-builder/api/index.d.mts +141 -0
  150. package/dist/plugins/form-builder/api/index.d.ts +141 -0
  151. package/dist/plugins/form-builder/api/index.mjs +1 -0
  152. package/dist/plugins/form-builder/client/components/index.cjs +29 -0
  153. package/dist/plugins/form-builder/client/components/index.d.cts +93 -0
  154. package/dist/plugins/form-builder/client/components/index.d.mts +93 -0
  155. package/dist/plugins/form-builder/client/components/index.d.ts +93 -0
  156. package/dist/plugins/form-builder/client/components/index.mjs +18 -0
  157. package/dist/plugins/form-builder/client/hooks/index.cjs +19 -0
  158. package/dist/plugins/form-builder/client/hooks/index.d.cts +154 -0
  159. package/dist/plugins/form-builder/client/hooks/index.d.mts +154 -0
  160. package/dist/plugins/form-builder/client/hooks/index.d.ts +154 -0
  161. package/dist/plugins/form-builder/client/hooks/index.mjs +1 -0
  162. package/dist/plugins/form-builder/client/index.cjs +13 -0
  163. package/dist/plugins/form-builder/client/index.d.cts +381 -0
  164. package/dist/plugins/form-builder/client/index.d.mts +381 -0
  165. package/dist/plugins/form-builder/client/index.d.ts +381 -0
  166. package/dist/plugins/form-builder/client/index.mjs +2 -0
  167. package/dist/plugins/form-builder/client.css +3 -0
  168. package/dist/plugins/form-builder/query-keys.cjs +143 -0
  169. package/dist/plugins/form-builder/query-keys.d.cts +74 -0
  170. package/dist/plugins/form-builder/query-keys.d.mts +74 -0
  171. package/dist/plugins/form-builder/query-keys.d.ts +74 -0
  172. package/dist/plugins/form-builder/query-keys.mjs +141 -0
  173. package/dist/plugins/form-builder/style.css +19 -0
  174. package/dist/plugins/open-api/api/index.cjs +9 -0
  175. package/dist/plugins/open-api/api/index.d.cts +95 -0
  176. package/dist/plugins/open-api/api/index.d.mts +95 -0
  177. package/dist/plugins/open-api/api/index.d.ts +95 -0
  178. package/dist/plugins/open-api/api/index.mjs +2 -0
  179. package/dist/shared/stack.AX5nZ6A3.d.cts +86 -0
  180. package/dist/shared/stack.AX5nZ6A3.d.mts +86 -0
  181. package/dist/shared/stack.AX5nZ6A3.d.ts +86 -0
  182. package/dist/shared/stack.BIh2AXaW.d.cts +123 -0
  183. package/dist/shared/stack.BIh2AXaW.d.mts +123 -0
  184. package/dist/shared/stack.BIh2AXaW.d.ts +123 -0
  185. package/dist/shared/{stack.ByOugz9d.d.cts → stack.CSce37mX.d.cts} +15 -2
  186. package/dist/shared/{stack.ByOugz9d.d.mts → stack.CSce37mX.d.mts} +15 -2
  187. package/dist/shared/{stack.ByOugz9d.d.ts → stack.CSce37mX.d.ts} +15 -2
  188. package/dist/shared/stack.DzH_wcvr.d.cts +195 -0
  189. package/dist/shared/stack.DzH_wcvr.d.mts +195 -0
  190. package/dist/shared/stack.DzH_wcvr.d.ts +195 -0
  191. package/package.json +67 -1
  192. package/src/api/index.ts +14 -2
  193. package/src/plugins/cms/api/plugin.ts +9 -4
  194. package/src/plugins/cms/client/components/forms/content-form.tsx +23 -25
  195. package/src/plugins/cms/client/index.ts +11 -0
  196. package/src/plugins/form-builder/api/index.ts +1 -0
  197. package/src/plugins/form-builder/api/plugin.ts +776 -0
  198. package/src/plugins/form-builder/client/components/forms/form-renderer.tsx +253 -0
  199. package/src/plugins/form-builder/client/components/index.tsx +24 -0
  200. package/src/plugins/form-builder/client/components/loading/form-builder-skeleton.tsx +42 -0
  201. package/src/plugins/form-builder/client/components/loading/form-list-skeleton.tsx +25 -0
  202. package/src/plugins/form-builder/client/components/loading/index.tsx +3 -0
  203. package/src/plugins/form-builder/client/components/loading/submissions-skeleton.tsx +40 -0
  204. package/src/plugins/form-builder/client/components/pages/404-page.tsx +28 -0
  205. package/src/plugins/form-builder/client/components/pages/form-builder-page.internal.tsx +253 -0
  206. package/src/plugins/form-builder/client/components/pages/form-builder-page.tsx +26 -0
  207. package/src/plugins/form-builder/client/components/pages/form-list-page.internal.tsx +231 -0
  208. package/src/plugins/form-builder/client/components/pages/form-list-page.tsx +22 -0
  209. package/src/plugins/form-builder/client/components/pages/submissions-page.internal.tsx +268 -0
  210. package/src/plugins/form-builder/client/components/pages/submissions-page.tsx +26 -0
  211. package/src/plugins/form-builder/client/components/shared/default-error.tsx +30 -0
  212. package/src/plugins/form-builder/client/components/shared/empty-state.tsx +26 -0
  213. package/src/plugins/form-builder/client/components/shared/page-wrapper.tsx +32 -0
  214. package/src/plugins/form-builder/client/components/shared/pagination.tsx +52 -0
  215. package/src/plugins/form-builder/client/hooks/form-builder-hooks.tsx +799 -0
  216. package/src/plugins/form-builder/client/hooks/index.tsx +1 -0
  217. package/src/plugins/form-builder/client/index.ts +22 -0
  218. package/src/plugins/form-builder/client/localization/form-builder-common.ts +36 -0
  219. package/src/plugins/form-builder/client/localization/form-builder-editor.ts +18 -0
  220. package/src/plugins/form-builder/client/localization/form-builder-list.ts +17 -0
  221. package/src/plugins/form-builder/client/localization/form-builder-submissions.ts +17 -0
  222. package/src/plugins/form-builder/client/localization/form-builder-toasts.ts +10 -0
  223. package/src/plugins/form-builder/client/localization/index.ts +15 -0
  224. package/src/plugins/form-builder/client/overrides.ts +146 -0
  225. package/src/plugins/form-builder/client/plugin.tsx +488 -0
  226. package/src/plugins/form-builder/client.css +3 -0
  227. package/src/plugins/form-builder/db.ts +99 -0
  228. package/src/plugins/form-builder/query-keys.ts +198 -0
  229. package/src/plugins/form-builder/schemas.ts +122 -0
  230. package/src/plugins/form-builder/style.css +19 -0
  231. package/src/plugins/form-builder/types.ts +317 -0
  232. package/src/plugins/form-builder/utils.ts +63 -0
  233. package/src/plugins/open-api/api/generator.ts +433 -0
  234. package/src/plugins/open-api/api/index.ts +8 -0
  235. package/src/plugins/open-api/api/plugin.ts +243 -0
  236. package/src/plugins/open-api/db.ts +7 -0
  237. package/src/plugins/open-api/logo.ts +7 -0
  238. package/src/types.ts +15 -1
  239. package/dist/shared/{stack.DLhzx1-D.d.mts → stack.CcI4sYJP.d.cts} +1 -1
  240. package/dist/shared/{stack.DLhzx1-D.d.ts → stack.CcI4sYJP.d.mts} +1 -1
  241. package/dist/shared/{stack.DLhzx1-D.d.cts → stack.CcI4sYJP.d.ts} +1 -1
@@ -0,0 +1,776 @@
1
+ import type { Adapter } from "@btst/db";
2
+ import { defineBackendPlugin } from "@btst/stack/plugins/api";
3
+ import { createEndpoint } from "@btst/stack/plugins/api";
4
+ import { z } from "zod";
5
+ import { formSchemaToZod } from "@workspace/ui/lib/schema-converter";
6
+ import { formBuilderSchema as dbSchema } from "../db";
7
+ import type {
8
+ Form,
9
+ FormSubmission,
10
+ FormSubmissionWithForm,
11
+ FormBuilderBackendConfig,
12
+ FormBuilderHookContext,
13
+ SubmissionHookContext,
14
+ SerializedForm,
15
+ SerializedFormSubmission,
16
+ SerializedFormSubmissionWithData,
17
+ FormInput,
18
+ FormUpdate,
19
+ } from "../types";
20
+ import {
21
+ listFormsQuerySchema,
22
+ createFormSchema,
23
+ updateFormSchema,
24
+ listSubmissionsQuerySchema,
25
+ } from "../schemas";
26
+ import { slugify, extractIpAddress, extractUserAgent } from "../utils";
27
+
28
+ /**
29
+ * Serialize a Form for API response (convert dates to strings)
30
+ */
31
+ function serializeForm(form: Form): SerializedForm {
32
+ return {
33
+ id: form.id,
34
+ name: form.name,
35
+ slug: form.slug,
36
+ description: form.description,
37
+ schema: form.schema,
38
+ successMessage: form.successMessage,
39
+ redirectUrl: form.redirectUrl,
40
+ status: form.status,
41
+ createdBy: form.createdBy,
42
+ createdAt: form.createdAt.toISOString(),
43
+ updatedAt: form.updatedAt.toISOString(),
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Serialize a FormSubmission for API response (convert dates to strings)
49
+ */
50
+ function serializeFormSubmission(
51
+ submission: FormSubmission,
52
+ ): SerializedFormSubmission {
53
+ return {
54
+ ...submission,
55
+ submittedAt: submission.submittedAt.toISOString(),
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Serialize a FormSubmission with parsed data and joined Form
61
+ */
62
+ function serializeFormSubmissionWithData(
63
+ submission: FormSubmissionWithForm,
64
+ ): SerializedFormSubmissionWithData {
65
+ return {
66
+ ...serializeFormSubmission(submission),
67
+ parsedData: JSON.parse(submission.data),
68
+ form: submission.form ? serializeForm(submission.form) : undefined,
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Form Builder backend plugin
74
+ * Provides API endpoints for managing forms and form submissions
75
+ *
76
+ * @param config - Configuration with optional hooks
77
+ */
78
+ export const formBuilderBackendPlugin = (
79
+ config: FormBuilderBackendConfig = {},
80
+ ) =>
81
+ defineBackendPlugin({
82
+ name: "form-builder",
83
+
84
+ dbPlugin: dbSchema,
85
+
86
+ routes: (adapter: Adapter) => {
87
+ // Helper to create hook context from request
88
+ const createContext = (headers?: Headers): FormBuilderHookContext => ({
89
+ headers,
90
+ ipAddress: extractIpAddress(headers),
91
+ userAgent: extractUserAgent(headers),
92
+ });
93
+
94
+ // Helper to create submission hook context
95
+ const createSubmissionContext = (
96
+ formSlug: string,
97
+ formId: string,
98
+ headers?: Headers,
99
+ ): SubmissionHookContext => ({
100
+ ...createContext(headers),
101
+ formSlug,
102
+ formId,
103
+ });
104
+
105
+ // ========== Form CRUD Endpoints (Admin) ==========
106
+
107
+ const listForms = createEndpoint(
108
+ "/forms",
109
+ {
110
+ method: "GET",
111
+ query: listFormsQuerySchema,
112
+ },
113
+ async (ctx) => {
114
+ const { status, limit, offset } = ctx.query;
115
+ const context = createContext(ctx.headers);
116
+
117
+ // Call before hook for auth check
118
+ if (config.hooks?.onBeforeListForms) {
119
+ const canList = await config.hooks.onBeforeListForms(context);
120
+ if (!canList) {
121
+ throw ctx.error(403, { message: "Access denied" });
122
+ }
123
+ }
124
+
125
+ const whereConditions: Array<{
126
+ field: string;
127
+ value: string;
128
+ operator: "eq";
129
+ }> = [];
130
+ if (status) {
131
+ whereConditions.push({
132
+ field: "status",
133
+ value: status,
134
+ operator: "eq" as const,
135
+ });
136
+ }
137
+
138
+ // Get total count
139
+ const allForms = await adapter.findMany<Form>({
140
+ model: "form",
141
+ where: whereConditions.length > 0 ? whereConditions : undefined,
142
+ });
143
+ const total = allForms.length;
144
+
145
+ // Get paginated forms
146
+ const forms = await adapter.findMany<Form>({
147
+ model: "form",
148
+ where: whereConditions.length > 0 ? whereConditions : undefined,
149
+ limit,
150
+ offset,
151
+ sortBy: { field: "createdAt", direction: "desc" },
152
+ });
153
+
154
+ return {
155
+ items: forms.map(serializeForm),
156
+ total,
157
+ limit,
158
+ offset,
159
+ };
160
+ },
161
+ );
162
+
163
+ const getFormBySlug = createEndpoint(
164
+ "/forms/:slug",
165
+ {
166
+ method: "GET",
167
+ params: z.object({ slug: z.string() }),
168
+ },
169
+ async (ctx) => {
170
+ const { slug } = ctx.params;
171
+ const context = createContext(ctx.headers);
172
+
173
+ // Call before hook for access check
174
+ if (config.hooks?.onBeforeGetForm) {
175
+ const canGet = await config.hooks.onBeforeGetForm(slug, context);
176
+ if (!canGet) {
177
+ throw ctx.error(403, { message: "Access denied" });
178
+ }
179
+ }
180
+
181
+ const form = await adapter.findOne<Form>({
182
+ model: "form",
183
+ where: [{ field: "slug", value: slug, operator: "eq" as const }],
184
+ });
185
+
186
+ if (!form) {
187
+ throw ctx.error(404, { message: "Form not found" });
188
+ }
189
+
190
+ return serializeForm(form);
191
+ },
192
+ );
193
+
194
+ const getFormById = createEndpoint(
195
+ "/forms/id/:id",
196
+ {
197
+ method: "GET",
198
+ params: z.object({ id: z.string() }),
199
+ },
200
+ async (ctx) => {
201
+ const { id } = ctx.params;
202
+ const context = createContext(ctx.headers);
203
+
204
+ // Call before hook for access check
205
+ if (config.hooks?.onBeforeGetForm) {
206
+ const canGet = await config.hooks.onBeforeGetForm(id, context);
207
+ if (!canGet) {
208
+ throw ctx.error(403, { message: "Access denied" });
209
+ }
210
+ }
211
+
212
+ const form = await adapter.findOne<Form>({
213
+ model: "form",
214
+ where: [{ field: "id", value: id, operator: "eq" as const }],
215
+ });
216
+
217
+ if (!form) {
218
+ throw ctx.error(404, { message: "Form not found" });
219
+ }
220
+
221
+ return serializeForm(form);
222
+ },
223
+ );
224
+
225
+ const createForm = createEndpoint(
226
+ "/forms",
227
+ {
228
+ method: "POST",
229
+ body: createFormSchema,
230
+ },
231
+ async (ctx) => {
232
+ const body = ctx.body;
233
+ const context = createContext(ctx.headers);
234
+
235
+ // Sanitize slug to ensure it's URL-safe
236
+ const slug = slugify(body.slug);
237
+
238
+ if (!slug) {
239
+ throw ctx.error(400, {
240
+ message:
241
+ "Invalid slug: must contain at least one alphanumeric character",
242
+ });
243
+ }
244
+
245
+ // Check for duplicate slug
246
+ const existing = await adapter.findOne<Form>({
247
+ model: "form",
248
+ where: [{ field: "slug", value: slug, operator: "eq" as const }],
249
+ });
250
+ if (existing) {
251
+ throw ctx.error(409, {
252
+ message: "Form with this slug already exists",
253
+ });
254
+ }
255
+
256
+ // Validate JSON Schema
257
+ try {
258
+ JSON.parse(body.schema);
259
+ } catch {
260
+ throw ctx.error(400, { message: "Invalid JSON Schema" });
261
+ }
262
+
263
+ // Build form input
264
+ let formInput: FormInput = {
265
+ name: body.name,
266
+ slug,
267
+ description: body.description,
268
+ schema: body.schema,
269
+ successMessage: body.successMessage,
270
+ redirectUrl: body.redirectUrl || undefined,
271
+ status: body.status as "active" | "inactive" | "archived",
272
+ };
273
+
274
+ // Call before hook - may modify data or deny operation
275
+ if (config.hooks?.onBeforeFormCreated) {
276
+ const result = await config.hooks.onBeforeFormCreated(
277
+ formInput,
278
+ context,
279
+ );
280
+ if (result === false) {
281
+ throw ctx.error(403, { message: "Create operation denied" });
282
+ }
283
+ if (result && typeof result === "object") {
284
+ formInput = result;
285
+ }
286
+ }
287
+
288
+ const form = await adapter.create<Form>({
289
+ model: "form",
290
+ data: {
291
+ name: formInput.name,
292
+ slug: formInput.slug,
293
+ description: formInput.description,
294
+ schema: formInput.schema,
295
+ successMessage: formInput.successMessage,
296
+ redirectUrl: formInput.redirectUrl,
297
+ status: formInput.status || "active",
298
+ createdBy: formInput.createdBy,
299
+ createdAt: new Date(),
300
+ updatedAt: new Date(),
301
+ },
302
+ });
303
+
304
+ const serialized = serializeForm(form as Form);
305
+
306
+ // Call after hook
307
+ if (config.hooks?.onAfterFormCreated) {
308
+ await config.hooks.onAfterFormCreated(serialized, context);
309
+ }
310
+
311
+ return serialized;
312
+ },
313
+ );
314
+
315
+ const updateForm = createEndpoint(
316
+ "/forms/:id",
317
+ {
318
+ method: "PUT",
319
+ params: z.object({ id: z.string() }),
320
+ body: updateFormSchema,
321
+ },
322
+ async (ctx) => {
323
+ const { id } = ctx.params;
324
+ const body = ctx.body;
325
+ const context = createContext(ctx.headers);
326
+
327
+ const existing = await adapter.findOne<Form>({
328
+ model: "form",
329
+ where: [{ field: "id", value: id, operator: "eq" as const }],
330
+ });
331
+
332
+ if (!existing) {
333
+ throw ctx.error(404, { message: "Form not found" });
334
+ }
335
+
336
+ // Sanitize slug if provided
337
+ let slug: string | undefined;
338
+ if (body.slug) {
339
+ slug = slugify(body.slug);
340
+ if (!slug) {
341
+ throw ctx.error(400, {
342
+ message:
343
+ "Invalid slug: must contain at least one alphanumeric character",
344
+ });
345
+ }
346
+
347
+ // Check for duplicate slug if changing
348
+ if (slug !== existing.slug) {
349
+ const duplicate = await adapter.findOne<Form>({
350
+ model: "form",
351
+ where: [
352
+ { field: "slug", value: slug, operator: "eq" as const },
353
+ ],
354
+ });
355
+ if (duplicate) {
356
+ throw ctx.error(409, {
357
+ message: "Form with this slug already exists",
358
+ });
359
+ }
360
+ }
361
+ }
362
+
363
+ // Validate JSON Schema if provided
364
+ if (body.schema) {
365
+ try {
366
+ JSON.parse(body.schema);
367
+ } catch {
368
+ throw ctx.error(400, { message: "Invalid JSON Schema" });
369
+ }
370
+ }
371
+
372
+ // Build update input
373
+ let updateInput: FormUpdate = {
374
+ name: body.name,
375
+ slug,
376
+ description: body.description,
377
+ schema: body.schema,
378
+ successMessage: body.successMessage,
379
+ redirectUrl: body.redirectUrl,
380
+ status: body.status as
381
+ | "active"
382
+ | "inactive"
383
+ | "archived"
384
+ | undefined,
385
+ };
386
+
387
+ // Call before hook - may modify data or deny operation
388
+ if (config.hooks?.onBeforeFormUpdated) {
389
+ const result = await config.hooks.onBeforeFormUpdated(
390
+ id,
391
+ updateInput,
392
+ context,
393
+ );
394
+ if (result === false) {
395
+ throw ctx.error(403, { message: "Update operation denied" });
396
+ }
397
+ if (result && typeof result === "object") {
398
+ updateInput = result;
399
+ }
400
+ }
401
+
402
+ // Build update data
403
+ const updateData: Partial<Form> = {
404
+ updatedAt: new Date(),
405
+ };
406
+ if (updateInput.name) updateData.name = updateInput.name;
407
+ if (updateInput.slug) updateData.slug = updateInput.slug;
408
+ if (updateInput.description !== undefined)
409
+ updateData.description = updateInput.description;
410
+ if (updateInput.schema) updateData.schema = updateInput.schema;
411
+ if (updateInput.successMessage !== undefined)
412
+ updateData.successMessage = updateInput.successMessage;
413
+ if (updateInput.redirectUrl !== undefined)
414
+ updateData.redirectUrl = updateInput.redirectUrl;
415
+ if (updateInput.status) updateData.status = updateInput.status;
416
+
417
+ await adapter.update({
418
+ model: "form",
419
+ where: [{ field: "id", value: id, operator: "eq" as const }],
420
+ update: updateData,
421
+ });
422
+
423
+ const updated = await adapter.findOne<Form>({
424
+ model: "form",
425
+ where: [{ field: "id", value: id, operator: "eq" as const }],
426
+ });
427
+
428
+ if (!updated) {
429
+ throw ctx.error(500, { message: "Failed to fetch updated form" });
430
+ }
431
+
432
+ const serialized = serializeForm(updated);
433
+
434
+ // Call after hook
435
+ if (config.hooks?.onAfterFormUpdated) {
436
+ await config.hooks.onAfterFormUpdated(serialized, context);
437
+ }
438
+
439
+ return serialized;
440
+ },
441
+ );
442
+
443
+ const deleteForm = createEndpoint(
444
+ "/forms/:id",
445
+ {
446
+ method: "DELETE",
447
+ params: z.object({ id: z.string() }),
448
+ },
449
+ async (ctx) => {
450
+ const { id } = ctx.params;
451
+ const context = createContext(ctx.headers);
452
+
453
+ const existing = await adapter.findOne<Form>({
454
+ model: "form",
455
+ where: [{ field: "id", value: id, operator: "eq" as const }],
456
+ });
457
+
458
+ if (!existing) {
459
+ throw ctx.error(404, { message: "Form not found" });
460
+ }
461
+
462
+ // Call before hook
463
+ if (config.hooks?.onBeforeFormDeleted) {
464
+ const canDelete = await config.hooks.onBeforeFormDeleted(
465
+ id,
466
+ context,
467
+ );
468
+ if (!canDelete) {
469
+ throw ctx.error(403, { message: "Delete operation denied" });
470
+ }
471
+ }
472
+
473
+ // Delete associated submissions first (cascade)
474
+ await adapter.delete({
475
+ model: "formSubmission",
476
+ where: [{ field: "formId", value: id, operator: "eq" as const }],
477
+ });
478
+
479
+ await adapter.delete({
480
+ model: "form",
481
+ where: [{ field: "id", value: id, operator: "eq" as const }],
482
+ });
483
+
484
+ // Call after hook
485
+ if (config.hooks?.onAfterFormDeleted) {
486
+ await config.hooks.onAfterFormDeleted(id, context);
487
+ }
488
+
489
+ return { success: true };
490
+ },
491
+ );
492
+
493
+ // ========== Form Submission Endpoints ==========
494
+
495
+ const submitForm = createEndpoint(
496
+ "/forms/:slug/submit",
497
+ {
498
+ method: "POST",
499
+ params: z.object({ slug: z.string() }),
500
+ body: z.object({
501
+ // Use passthrough object for dynamic form data
502
+ data: z.object({}).passthrough(),
503
+ }),
504
+ },
505
+ async (ctx) => {
506
+ const { slug } = ctx.params;
507
+ const { data } = ctx.body;
508
+ const baseContext = createContext(ctx.headers);
509
+
510
+ // Get form by slug
511
+ const form = await adapter.findOne<Form>({
512
+ model: "form",
513
+ where: [{ field: "slug", value: slug, operator: "eq" as const }],
514
+ });
515
+
516
+ if (!form) {
517
+ throw ctx.error(404, { message: "Form not found" });
518
+ }
519
+
520
+ // Check if form is active
521
+ if (form.status !== "active") {
522
+ throw ctx.error(400, {
523
+ message: "Form is not accepting submissions",
524
+ });
525
+ }
526
+
527
+ const submissionContext = createSubmissionContext(
528
+ slug,
529
+ form.id,
530
+ ctx.headers,
531
+ );
532
+
533
+ // Validate data against form schema
534
+ // Use formSchemaToZod for consistent validation with the client-side,
535
+ // which properly handles date constraints and step metadata
536
+ try {
537
+ const jsonSchema = JSON.parse(form.schema);
538
+ const zodSchema = formSchemaToZod(jsonSchema);
539
+ const validation = zodSchema.safeParse(data);
540
+ if (!validation.success) {
541
+ throw ctx.error(400, {
542
+ message: "Validation failed",
543
+ errors: validation.error.issues,
544
+ });
545
+ }
546
+ } catch (error) {
547
+ if (error && typeof error === "object" && "code" in error) {
548
+ throw error; // Re-throw API errors
549
+ }
550
+ throw ctx.error(400, { message: "Invalid form data" });
551
+ }
552
+
553
+ // Call before submission hook - may modify data or deny
554
+ let finalData = data as Record<string, unknown>;
555
+ if (config.hooks?.onBeforeSubmission) {
556
+ try {
557
+ const result = await config.hooks.onBeforeSubmission(
558
+ slug,
559
+ data as Record<string, unknown>,
560
+ submissionContext,
561
+ );
562
+ if (result === false) {
563
+ throw ctx.error(400, { message: "Submission rejected" });
564
+ }
565
+ if (result && typeof result === "object") {
566
+ finalData = result;
567
+ }
568
+ } catch (error) {
569
+ // Call error hook if submission is rejected
570
+ if (config.hooks?.onSubmissionError) {
571
+ await config.hooks.onSubmissionError(
572
+ error as Error,
573
+ slug,
574
+ data as Record<string, unknown>,
575
+ submissionContext,
576
+ );
577
+ }
578
+ throw error;
579
+ }
580
+ }
581
+
582
+ // Create submission
583
+ const submission = await adapter.create<FormSubmission>({
584
+ model: "formSubmission",
585
+ data: {
586
+ formId: form.id,
587
+ data: JSON.stringify(finalData),
588
+ submittedAt: new Date(),
589
+ ipAddress: baseContext.ipAddress,
590
+ userAgent: baseContext.userAgent,
591
+ },
592
+ });
593
+
594
+ const serialized = serializeFormSubmission(submission);
595
+
596
+ // Call after submission hook
597
+ if (config.hooks?.onAfterSubmission) {
598
+ await config.hooks.onAfterSubmission(
599
+ serialized,
600
+ serializeForm(form),
601
+ submissionContext,
602
+ );
603
+ }
604
+
605
+ return {
606
+ ...serialized,
607
+ form: {
608
+ successMessage: form.successMessage,
609
+ redirectUrl: form.redirectUrl,
610
+ },
611
+ };
612
+ },
613
+ );
614
+
615
+ // ========== Submissions Management Endpoints (Admin) ==========
616
+
617
+ const listSubmissions = createEndpoint(
618
+ "/forms/:formId/submissions",
619
+ {
620
+ method: "GET",
621
+ params: z.object({ formId: z.string() }),
622
+ query: listSubmissionsQuerySchema,
623
+ },
624
+ async (ctx) => {
625
+ const { formId } = ctx.params;
626
+ const { limit, offset } = ctx.query;
627
+ const context = createContext(ctx.headers);
628
+
629
+ // Verify form exists
630
+ const form = await adapter.findOne<Form>({
631
+ model: "form",
632
+ where: [{ field: "id", value: formId, operator: "eq" as const }],
633
+ });
634
+
635
+ if (!form) {
636
+ throw ctx.error(404, { message: "Form not found" });
637
+ }
638
+
639
+ // Call before hook for auth check
640
+ if (config.hooks?.onBeforeListSubmissions) {
641
+ const canList = await config.hooks.onBeforeListSubmissions(
642
+ formId,
643
+ context,
644
+ );
645
+ if (!canList) {
646
+ throw ctx.error(403, { message: "Access denied" });
647
+ }
648
+ }
649
+
650
+ // Get total count
651
+ const allSubmissions = await adapter.findMany<FormSubmission>({
652
+ model: "formSubmission",
653
+ where: [
654
+ { field: "formId", value: formId, operator: "eq" as const },
655
+ ],
656
+ });
657
+ const total = allSubmissions.length;
658
+
659
+ // Get paginated submissions
660
+ const submissions = await adapter.findMany<FormSubmissionWithForm>({
661
+ model: "formSubmission",
662
+ where: [
663
+ { field: "formId", value: formId, operator: "eq" as const },
664
+ ],
665
+ limit,
666
+ offset,
667
+ sortBy: { field: "submittedAt", direction: "desc" },
668
+ join: { form: true },
669
+ });
670
+
671
+ return {
672
+ items: submissions.map(serializeFormSubmissionWithData),
673
+ total,
674
+ limit,
675
+ offset,
676
+ };
677
+ },
678
+ );
679
+
680
+ const getSubmission = createEndpoint(
681
+ "/forms/:formId/submissions/:subId",
682
+ {
683
+ method: "GET",
684
+ params: z.object({ formId: z.string(), subId: z.string() }),
685
+ },
686
+ async (ctx) => {
687
+ const { formId, subId } = ctx.params;
688
+ const context = createContext(ctx.headers);
689
+
690
+ // Call before hook for access check
691
+ if (config.hooks?.onBeforeGetSubmission) {
692
+ const canGet = await config.hooks.onBeforeGetSubmission(
693
+ subId,
694
+ context,
695
+ );
696
+ if (!canGet) {
697
+ throw ctx.error(403, { message: "Access denied" });
698
+ }
699
+ }
700
+
701
+ const submission = await adapter.findOne<FormSubmissionWithForm>({
702
+ model: "formSubmission",
703
+ where: [{ field: "id", value: subId, operator: "eq" as const }],
704
+ join: { form: true },
705
+ });
706
+
707
+ if (!submission || submission.formId !== formId) {
708
+ throw ctx.error(404, { message: "Submission not found" });
709
+ }
710
+
711
+ return serializeFormSubmissionWithData(submission);
712
+ },
713
+ );
714
+
715
+ const deleteSubmission = createEndpoint(
716
+ "/forms/:formId/submissions/:subId",
717
+ {
718
+ method: "DELETE",
719
+ params: z.object({ formId: z.string(), subId: z.string() }),
720
+ },
721
+ async (ctx) => {
722
+ const { formId, subId } = ctx.params;
723
+ const context = createContext(ctx.headers);
724
+
725
+ const existing = await adapter.findOne<FormSubmission>({
726
+ model: "formSubmission",
727
+ where: [{ field: "id", value: subId, operator: "eq" as const }],
728
+ });
729
+
730
+ if (!existing || existing.formId !== formId) {
731
+ throw ctx.error(404, { message: "Submission not found" });
732
+ }
733
+
734
+ // Call before hook
735
+ if (config.hooks?.onBeforeSubmissionDeleted) {
736
+ const canDelete = await config.hooks.onBeforeSubmissionDeleted(
737
+ subId,
738
+ context,
739
+ );
740
+ if (!canDelete) {
741
+ throw ctx.error(403, { message: "Delete operation denied" });
742
+ }
743
+ }
744
+
745
+ await adapter.delete({
746
+ model: "formSubmission",
747
+ where: [{ field: "id", value: subId, operator: "eq" as const }],
748
+ });
749
+
750
+ // Call after hook
751
+ if (config.hooks?.onAfterSubmissionDeleted) {
752
+ await config.hooks.onAfterSubmissionDeleted(subId, context);
753
+ }
754
+
755
+ return { success: true };
756
+ },
757
+ );
758
+
759
+ return {
760
+ listForms,
761
+ getFormBySlug,
762
+ getFormById,
763
+ createForm,
764
+ updateForm,
765
+ deleteForm,
766
+ submitForm,
767
+ listSubmissions,
768
+ getSubmission,
769
+ deleteSubmission,
770
+ };
771
+ },
772
+ });
773
+
774
+ export type FormBuilderApiRouter = ReturnType<
775
+ ReturnType<typeof formBuilderBackendPlugin>["routes"]
776
+ >;