@l4yercak3/cli 1.3.1 → 2.0.0-alpha.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 (357) hide show
  1. package/README.md +10 -220
  2. package/dist/api/client.d.ts +12 -0
  3. package/dist/api/client.d.ts.map +1 -0
  4. package/dist/api/client.js +37 -0
  5. package/dist/api/client.js.map +1 -0
  6. package/dist/api/platform.d.ts +161 -0
  7. package/dist/api/platform.d.ts.map +1 -0
  8. package/dist/api/platform.js +70 -0
  9. package/dist/api/platform.js.map +1 -0
  10. package/dist/bin/sevenlayers.d.ts +3 -0
  11. package/dist/bin/sevenlayers.d.ts.map +1 -0
  12. package/dist/bin/sevenlayers.js +198 -0
  13. package/dist/bin/sevenlayers.js.map +1 -0
  14. package/dist/commands/agent/catalog.d.ts +5 -0
  15. package/dist/commands/agent/catalog.d.ts.map +1 -0
  16. package/dist/commands/agent/catalog.js +142 -0
  17. package/dist/commands/agent/catalog.js.map +1 -0
  18. package/dist/commands/agent/drift.d.ts +5 -0
  19. package/dist/commands/agent/drift.d.ts.map +1 -0
  20. package/dist/commands/agent/drift.js +113 -0
  21. package/dist/commands/agent/drift.js.map +1 -0
  22. package/dist/commands/agent/init.d.ts +5 -0
  23. package/dist/commands/agent/init.d.ts.map +1 -0
  24. package/dist/commands/agent/init.js +75 -0
  25. package/dist/commands/agent/init.js.map +1 -0
  26. package/dist/commands/agent/permissions.d.ts +5 -0
  27. package/dist/commands/agent/permissions.d.ts.map +1 -0
  28. package/dist/commands/agent/permissions.js +88 -0
  29. package/dist/commands/agent/permissions.js.map +1 -0
  30. package/dist/commands/agent/runner.d.ts +14 -0
  31. package/dist/commands/agent/runner.d.ts.map +1 -0
  32. package/dist/commands/agent/runner.js +59 -0
  33. package/dist/commands/agent/runner.js.map +1 -0
  34. package/dist/commands/agent/shared.d.ts +13 -0
  35. package/dist/commands/agent/shared.d.ts.map +1 -0
  36. package/dist/commands/agent/shared.js +31 -0
  37. package/dist/commands/agent/shared.js.map +1 -0
  38. package/dist/commands/agent/template.d.ts +5 -0
  39. package/dist/commands/agent/template.d.ts.map +1 -0
  40. package/dist/commands/agent/template.js +104 -0
  41. package/dist/commands/agent/template.js.map +1 -0
  42. package/dist/commands/app/connect.d.ts +7 -0
  43. package/dist/commands/app/connect.d.ts.map +1 -0
  44. package/dist/commands/app/connect.js +12 -0
  45. package/dist/commands/app/connect.js.map +1 -0
  46. package/dist/commands/app/init.d.ts +7 -0
  47. package/dist/commands/app/init.d.ts.map +1 -0
  48. package/dist/commands/app/init.js +12 -0
  49. package/dist/commands/app/init.js.map +1 -0
  50. package/dist/commands/app/link.d.ts +3 -0
  51. package/dist/commands/app/link.d.ts.map +1 -0
  52. package/dist/commands/app/link.js +92 -0
  53. package/dist/commands/app/link.js.map +1 -0
  54. package/dist/commands/app/pages.d.ts +15 -0
  55. package/dist/commands/app/pages.d.ts.map +1 -0
  56. package/dist/commands/app/pages.js +180 -0
  57. package/dist/commands/app/pages.js.map +1 -0
  58. package/dist/commands/app/register.d.ts +3 -0
  59. package/dist/commands/app/register.d.ts.map +1 -0
  60. package/dist/commands/app/register.js +120 -0
  61. package/dist/commands/app/register.js.map +1 -0
  62. package/dist/commands/app/remote.d.ts +14 -0
  63. package/dist/commands/app/remote.d.ts.map +1 -0
  64. package/dist/commands/app/remote.js +44 -0
  65. package/dist/commands/app/remote.js.map +1 -0
  66. package/dist/commands/app/setup.d.ts +3 -0
  67. package/dist/commands/app/setup.d.ts.map +1 -0
  68. package/dist/commands/app/setup.js +299 -0
  69. package/dist/commands/app/setup.js.map +1 -0
  70. package/dist/commands/app/shared.d.ts +9 -0
  71. package/dist/commands/app/shared.d.ts.map +1 -0
  72. package/dist/commands/app/shared.js +122 -0
  73. package/dist/commands/app/shared.js.map +1 -0
  74. package/dist/commands/app/sync.d.ts +7 -0
  75. package/dist/commands/app/sync.d.ts.map +1 -0
  76. package/dist/commands/app/sync.js +107 -0
  77. package/dist/commands/app/sync.js.map +1 -0
  78. package/dist/commands/booking/check.d.ts +3 -0
  79. package/dist/commands/booking/check.d.ts.map +1 -0
  80. package/dist/commands/booking/check.js +68 -0
  81. package/dist/commands/booking/check.js.map +1 -0
  82. package/dist/commands/booking/setup.d.ts +3 -0
  83. package/dist/commands/booking/setup.d.ts.map +1 -0
  84. package/dist/commands/booking/setup.js +95 -0
  85. package/dist/commands/booking/setup.js.map +1 -0
  86. package/dist/commands/booking/shared.d.ts +31 -0
  87. package/dist/commands/booking/shared.d.ts.map +1 -0
  88. package/dist/commands/booking/shared.js +112 -0
  89. package/dist/commands/booking/shared.js.map +1 -0
  90. package/dist/commands/booking/smoke.d.ts +3 -0
  91. package/dist/commands/booking/smoke.d.ts.map +1 -0
  92. package/dist/commands/booking/smoke.js +101 -0
  93. package/dist/commands/booking/smoke.js.map +1 -0
  94. package/dist/commands/cms/bind.d.ts +3 -0
  95. package/dist/commands/cms/bind.d.ts.map +1 -0
  96. package/dist/commands/cms/bind.js +212 -0
  97. package/dist/commands/cms/bind.js.map +1 -0
  98. package/dist/commands/cms/content.d.ts +40 -0
  99. package/dist/commands/cms/content.d.ts.map +1 -0
  100. package/dist/commands/cms/content.js +169 -0
  101. package/dist/commands/cms/content.js.map +1 -0
  102. package/dist/commands/cms/doctor.d.ts +3 -0
  103. package/dist/commands/cms/doctor.d.ts.map +1 -0
  104. package/dist/commands/cms/doctor.js +69 -0
  105. package/dist/commands/cms/doctor.js.map +1 -0
  106. package/dist/commands/cms/migrate.d.ts +3 -0
  107. package/dist/commands/cms/migrate.d.ts.map +1 -0
  108. package/dist/commands/cms/migrate.js +78 -0
  109. package/dist/commands/cms/migrate.js.map +1 -0
  110. package/dist/commands/cms/registry.d.ts +3 -0
  111. package/dist/commands/cms/registry.d.ts.map +1 -0
  112. package/dist/commands/cms/registry.js +161 -0
  113. package/dist/commands/cms/registry.js.map +1 -0
  114. package/dist/commands/cms/seed.d.ts +3 -0
  115. package/dist/commands/cms/seed.d.ts.map +1 -0
  116. package/dist/commands/cms/seed.js +102 -0
  117. package/dist/commands/cms/seed.js.map +1 -0
  118. package/dist/commands/cms/shared.d.ts +22 -0
  119. package/dist/commands/cms/shared.d.ts.map +1 -0
  120. package/dist/commands/cms/shared.js +82 -0
  121. package/dist/commands/cms/shared.js.map +1 -0
  122. package/dist/commands/doctor/target.d.ts +3 -0
  123. package/dist/commands/doctor/target.d.ts.map +1 -0
  124. package/dist/commands/doctor/target.js +46 -0
  125. package/dist/commands/doctor/target.js.map +1 -0
  126. package/dist/commands/env/list.d.ts +3 -0
  127. package/dist/commands/env/list.d.ts.map +1 -0
  128. package/dist/commands/env/list.js +28 -0
  129. package/dist/commands/env/list.js.map +1 -0
  130. package/dist/commands/env/set.d.ts +3 -0
  131. package/dist/commands/env/set.d.ts.map +1 -0
  132. package/dist/commands/env/set.js +36 -0
  133. package/dist/commands/env/set.js.map +1 -0
  134. package/dist/commands/env/use.d.ts +3 -0
  135. package/dist/commands/env/use.d.ts.map +1 -0
  136. package/dist/commands/env/use.js +15 -0
  137. package/dist/commands/env/use.js.map +1 -0
  138. package/dist/commands/legacy/connect.d.ts +3 -0
  139. package/dist/commands/legacy/connect.d.ts.map +1 -0
  140. package/dist/commands/legacy/connect.js +8 -0
  141. package/dist/commands/legacy/connect.js.map +1 -0
  142. package/dist/commands/legacy/pages.d.ts +3 -0
  143. package/dist/commands/legacy/pages.d.ts.map +1 -0
  144. package/dist/commands/legacy/pages.js +16 -0
  145. package/dist/commands/legacy/pages.js.map +1 -0
  146. package/dist/commands/legacy/spread.d.ts +3 -0
  147. package/dist/commands/legacy/spread.d.ts.map +1 -0
  148. package/dist/commands/legacy/spread.js +8 -0
  149. package/dist/commands/legacy/spread.js.map +1 -0
  150. package/dist/commands/legacy/sync.d.ts +3 -0
  151. package/dist/commands/legacy/sync.d.ts.map +1 -0
  152. package/dist/commands/legacy/sync.js +8 -0
  153. package/dist/commands/legacy/sync.js.map +1 -0
  154. package/dist/config/env-diff.d.ts +10 -0
  155. package/dist/config/env-diff.d.ts.map +1 -0
  156. package/dist/config/env-diff.js +24 -0
  157. package/dist/config/env-diff.js.map +1 -0
  158. package/dist/config/env-parser.d.ts +20 -0
  159. package/dist/config/env-parser.d.ts.map +1 -0
  160. package/dist/config/env-parser.js +70 -0
  161. package/dist/config/env-parser.js.map +1 -0
  162. package/dist/config/env-writer.d.ts +22 -0
  163. package/dist/config/env-writer.d.ts.map +1 -0
  164. package/dist/config/env-writer.js +172 -0
  165. package/dist/config/env-writer.js.map +1 -0
  166. package/dist/config/profile-store.d.ts +29 -0
  167. package/dist/config/profile-store.d.ts.map +1 -0
  168. package/dist/config/profile-store.js +257 -0
  169. package/dist/config/profile-store.js.map +1 -0
  170. package/dist/core/args.d.ts +11 -0
  171. package/dist/core/args.d.ts.map +1 -0
  172. package/dist/core/args.js +106 -0
  173. package/dist/core/args.js.map +1 -0
  174. package/dist/core/colors.d.ts +6 -0
  175. package/dist/core/colors.d.ts.map +1 -0
  176. package/dist/core/colors.js +29 -0
  177. package/dist/core/colors.js.map +1 -0
  178. package/dist/safety/target-guard.d.ts +16 -0
  179. package/dist/safety/target-guard.d.ts.map +1 -0
  180. package/dist/safety/target-guard.js +55 -0
  181. package/dist/safety/target-guard.js.map +1 -0
  182. package/dist/testing/booking-smoke.d.ts +17 -0
  183. package/dist/testing/booking-smoke.d.ts.map +1 -0
  184. package/dist/testing/booking-smoke.js +43 -0
  185. package/dist/testing/booking-smoke.js.map +1 -0
  186. package/dist/tests/agent-commands.test.d.ts +2 -0
  187. package/dist/tests/agent-commands.test.d.ts.map +1 -0
  188. package/dist/tests/agent-commands.test.js +180 -0
  189. package/dist/tests/agent-commands.test.js.map +1 -0
  190. package/dist/tests/agent-governance.test.d.ts +2 -0
  191. package/dist/tests/agent-governance.test.d.ts.map +1 -0
  192. package/dist/tests/agent-governance.test.js +233 -0
  193. package/dist/tests/agent-governance.test.js.map +1 -0
  194. package/dist/tests/app-commands.test.d.ts +2 -0
  195. package/dist/tests/app-commands.test.d.ts.map +1 -0
  196. package/dist/tests/app-commands.test.js +462 -0
  197. package/dist/tests/app-commands.test.js.map +1 -0
  198. package/dist/tests/booking-commands.test.d.ts +2 -0
  199. package/dist/tests/booking-commands.test.d.ts.map +1 -0
  200. package/dist/tests/booking-commands.test.js +204 -0
  201. package/dist/tests/booking-commands.test.js.map +1 -0
  202. package/dist/tests/booking-smoke.test.d.ts +2 -0
  203. package/dist/tests/booking-smoke.test.d.ts.map +1 -0
  204. package/dist/tests/booking-smoke.test.js +183 -0
  205. package/dist/tests/booking-smoke.test.js.map +1 -0
  206. package/dist/tests/cms-commands.test.d.ts +2 -0
  207. package/dist/tests/cms-commands.test.d.ts.map +1 -0
  208. package/dist/tests/cms-commands.test.js +254 -0
  209. package/dist/tests/cms-commands.test.js.map +1 -0
  210. package/dist/tests/cms-ops.test.d.ts +2 -0
  211. package/dist/tests/cms-ops.test.d.ts.map +1 -0
  212. package/dist/tests/cms-ops.test.js +125 -0
  213. package/dist/tests/cms-ops.test.js.map +1 -0
  214. package/dist/tests/env-writer.test.d.ts +2 -0
  215. package/dist/tests/env-writer.test.d.ts.map +1 -0
  216. package/dist/tests/env-writer.test.js +90 -0
  217. package/dist/tests/env-writer.test.js.map +1 -0
  218. package/dist/tests/profile-store.test.d.ts +2 -0
  219. package/dist/tests/profile-store.test.d.ts.map +1 -0
  220. package/dist/tests/profile-store.test.js +88 -0
  221. package/dist/tests/profile-store.test.js.map +1 -0
  222. package/dist/tests/target-guard.test.d.ts +2 -0
  223. package/dist/tests/target-guard.test.d.ts.map +1 -0
  224. package/dist/tests/target-guard.test.js +132 -0
  225. package/dist/tests/target-guard.test.js.map +1 -0
  226. package/dist/ui/logo.d.ts +2 -0
  227. package/dist/ui/logo.d.ts.map +1 -0
  228. package/dist/ui/logo.js +22 -0
  229. package/dist/ui/logo.js.map +1 -0
  230. package/package.json +17 -53
  231. package/.claude/settings.local.json +0 -36
  232. package/.cursor/rules.md +0 -203
  233. package/.eslintrc.js +0 -31
  234. package/CLAUDE.md +0 -100
  235. package/bin/cli.js +0 -116
  236. package/docs/ADDING_FRAMEWORK_DETECTORS.md +0 -391
  237. package/docs/ADDING_NEW_PROJECT_TYPE.md +0 -156
  238. package/docs/ARCHITECTURE_RELATIONSHIPS.md +0 -411
  239. package/docs/CLI_AUTHENTICATION.md +0 -214
  240. package/docs/CLI_PAGE_DETECTION_REQUIREMENTS.md +0 -519
  241. package/docs/CRM-PIPELINES-SEQUENCES-SPEC.md +0 -429
  242. package/docs/DETECTOR_ARCHITECTURE.md +0 -326
  243. package/docs/DEVELOPMENT.md +0 -194
  244. package/docs/IMPLEMENTATION_PHASES.md +0 -468
  245. package/docs/INTEGRATION_PATHS_ARCHITECTURE.md +0 -1543
  246. package/docs/OAUTH_CLARIFICATION.md +0 -258
  247. package/docs/OAUTH_SETUP_GUIDE_TEMPLATE.md +0 -211
  248. package/docs/PHASE_0_PROGRESS.md +0 -120
  249. package/docs/PHASE_1_COMPLETE.md +0 -366
  250. package/docs/PHASE_SUMMARY.md +0 -149
  251. package/docs/PLAN.md +0 -511
  252. package/docs/README.md +0 -56
  253. package/docs/STRIPE_INTEGRATION.md +0 -447
  254. package/docs/SUMMARY.md +0 -230
  255. package/docs/UPDATED_PLAN.md +0 -447
  256. package/docs/mcp_server/MCP_EXTENSION_GUIDE.md +0 -1313
  257. package/docs/mcp_server/MCP_SERVER_ARCHITECTURE.md +0 -1481
  258. package/docs/mcp_server/applicationOntology.ts +0 -817
  259. package/docs/mcp_server/cliApplications.ts +0 -639
  260. package/docs/mcp_server/crmOntology.ts +0 -1063
  261. package/docs/mcp_server/eventOntology.ts +0 -1183
  262. package/docs/mcp_server/formsOntology.ts +0 -1401
  263. package/docs/mcp_server/ontologySchemas.ts +0 -185
  264. package/docs/mcp_server/schema.ts +0 -250
  265. package/docs/microsass_production_machine/CLI_API_REFERENCE.md +0 -1197
  266. package/docs/microsass_production_machine/CLI_PRODUCT_VISION.md +0 -676
  267. package/docs/microsass_production_machine/CLI_REQUIREMENTS.md +0 -606
  268. package/docs/microsass_production_machine/CONNECTED_APPLICATIONS_SPEC.md +0 -390
  269. package/docs/microsass_production_machine/IMPLEMENTATION_ROADMAP.md +0 -725
  270. package/docs/microsass_production_machine/OBJECT_MAPPINGS.md +0 -808
  271. package/docs/microsass_production_machine/REFERENCE_IMPLEMENTATION.md +0 -532
  272. package/src/api/backend-client.js +0 -449
  273. package/src/commands/api-keys.js +0 -119
  274. package/src/commands/connect.js +0 -243
  275. package/src/commands/login.js +0 -332
  276. package/src/commands/logout.js +0 -30
  277. package/src/commands/mcp-server.js +0 -85
  278. package/src/commands/mcp-setup.js +0 -686
  279. package/src/commands/pages.js +0 -317
  280. package/src/commands/scaffold.js +0 -409
  281. package/src/commands/spread.js +0 -861
  282. package/src/commands/status.js +0 -62
  283. package/src/commands/sync.js +0 -169
  284. package/src/commands/upgrade.js +0 -48
  285. package/src/config/config-manager.js +0 -206
  286. package/src/detectors/api-client-detector.js +0 -85
  287. package/src/detectors/base-detector.js +0 -77
  288. package/src/detectors/database-detector.js +0 -245
  289. package/src/detectors/expo-detector.js +0 -166
  290. package/src/detectors/github-detector.js +0 -74
  291. package/src/detectors/index.js +0 -106
  292. package/src/detectors/mapping-suggestor.js +0 -119
  293. package/src/detectors/model-detector.js +0 -318
  294. package/src/detectors/nextjs-detector.js +0 -139
  295. package/src/detectors/oauth-detector.js +0 -122
  296. package/src/detectors/page-detector.js +0 -480
  297. package/src/detectors/registry.js +0 -121
  298. package/src/generators/api-client-generator.js +0 -223
  299. package/src/generators/api-only/client.js +0 -683
  300. package/src/generators/api-only/index.js +0 -96
  301. package/src/generators/api-only/types.js +0 -618
  302. package/src/generators/api-only/webhooks.js +0 -377
  303. package/src/generators/env-generator.js +0 -191
  304. package/src/generators/expo-auth-generator.js +0 -1009
  305. package/src/generators/gitignore-generator.js +0 -92
  306. package/src/generators/index.js +0 -166
  307. package/src/generators/manifest-generator.js +0 -154
  308. package/src/generators/mcp-guide-generator.js +0 -256
  309. package/src/generators/nextauth-generator.js +0 -247
  310. package/src/generators/oauth-guide-generator.js +0 -277
  311. package/src/generators/quickstart/components/index.js +0 -1699
  312. package/src/generators/quickstart/components-mobile/index.js +0 -1440
  313. package/src/generators/quickstart/database/convex.js +0 -1257
  314. package/src/generators/quickstart/database/index.js +0 -34
  315. package/src/generators/quickstart/database/supabase.js +0 -1132
  316. package/src/generators/quickstart/hooks/index.js +0 -1065
  317. package/src/generators/quickstart/index.js +0 -191
  318. package/src/generators/quickstart/pages/index.js +0 -1466
  319. package/src/generators/quickstart/screens/index.js +0 -1498
  320. package/src/logo.js +0 -116
  321. package/src/mcp/auth.js +0 -127
  322. package/src/mcp/registry/domains/applications.js +0 -516
  323. package/src/mcp/registry/domains/benefits.js +0 -798
  324. package/src/mcp/registry/domains/codegen.js +0 -894
  325. package/src/mcp/registry/domains/core.js +0 -324
  326. package/src/mcp/registry/domains/crm.js +0 -591
  327. package/src/mcp/registry/domains/events.js +0 -649
  328. package/src/mcp/registry/domains/forms.js +0 -696
  329. package/src/mcp/registry/index.js +0 -164
  330. package/src/mcp/server.js +0 -116
  331. package/src/utils/file-utils.js +0 -117
  332. package/src/utils/init-helpers.js +0 -243
  333. package/src/utils/prompt-utils.js +0 -195
  334. package/templates/CLAUDE.md +0 -86
  335. package/tests/api-client-detector.test.js +0 -214
  336. package/tests/api-client-generator.test.js +0 -176
  337. package/tests/backend-client.test.js +0 -640
  338. package/tests/base-detector.test.js +0 -101
  339. package/tests/commands/login.test.js +0 -143
  340. package/tests/commands/logout.test.js +0 -84
  341. package/tests/commands/status.test.js +0 -167
  342. package/tests/config-manager.test.js +0 -321
  343. package/tests/database-detector.test.js +0 -221
  344. package/tests/detector-index.test.js +0 -209
  345. package/tests/detector-registry.test.js +0 -93
  346. package/tests/env-generator.test.js +0 -278
  347. package/tests/expo-detector.test.js +0 -263
  348. package/tests/file-utils.test.js +0 -194
  349. package/tests/generators-index.test.js +0 -454
  350. package/tests/github-detector.test.js +0 -145
  351. package/tests/gitignore-generator.test.js +0 -109
  352. package/tests/logo.test.js +0 -96
  353. package/tests/nextauth-generator.test.js +0 -255
  354. package/tests/nextjs-detector.test.js +0 -235
  355. package/tests/oauth-detector.test.js +0 -264
  356. package/tests/oauth-guide-generator.test.js +0 -273
  357. package/tests/page-detector.test.js +0 -371
@@ -1,1401 +0,0 @@
1
- /**
2
- * FORMS ONTOLOGY
3
- *
4
- * Manages forms (templates) and form responses using the universal ontology system.
5
- *
6
- * KEY ARCHITECTURE DECISION:
7
- * Forms are templates that COLLECT data, but the data is EMBEDDED into tickets
8
- * for fast operational queries at events (QR scanning, reporting, check-in).
9
- *
10
- * Data Flow:
11
- * 1. Form (template) → User fills form → FormResponse (audit trail)
12
- * 2. FormResponse data → COPIED to ticket.registrationData (operational use)
13
- * 3. Event managers query tickets directly (no joins needed)
14
- *
15
- * Form Types (subtype):
16
- * - "registration" - Event registration forms (linked to tickets)
17
- * - "survey" - Feedback surveys (standalone or post-event)
18
- * - "application" - Speaker proposals, volunteer applications
19
- *
20
- * Form Status:
21
- * - "draft" - Being built
22
- * - "published" - Active and accepting submissions
23
- * - "archived" - No longer accepting submissions
24
- *
25
- * FormResponse Status:
26
- * - "partial" - Started but not completed
27
- * - "complete" - Successfully submitted
28
- * - "abandoned" - User left before completion
29
- */
30
-
31
- import { query, mutation, internalMutation, internalQuery } from "./_generated/server";
32
- import { v } from "convex/values";
33
- import { requireAuthenticatedUser, checkPermission } from "./rbacHelpers";
34
- import { checkResourceLimit, checkFeatureAccess, getLicenseInternal } from "./licensing/helpers";
35
- import { ConvexError } from "convex/values";
36
-
37
- // Helper function for tier upgrade path
38
- function getNextTier(
39
- currentTier: "free" | "starter" | "professional" | "agency" | "enterprise"
40
- ): string {
41
- const tierUpgradePath: Record<string, string> = {
42
- free: "Starter (€199/month)",
43
- starter: "Professional (€399/month)",
44
- professional: "Agency (€599/month)",
45
- agency: "Enterprise (€1,500+/month)",
46
- enterprise: "Enterprise (contact sales)",
47
- };
48
- return tierUpgradePath[currentTier] || "a higher tier";
49
- }
50
-
51
- /**
52
- * FIELD TYPE DEFINITIONS
53
- * These are the supported form field types
54
- */
55
- export const FIELD_TYPES = {
56
- TEXT: "text",
57
- TEXTAREA: "textarea",
58
- EMAIL: "email",
59
- PHONE: "phone",
60
- NUMBER: "number",
61
- DATE: "date",
62
- TIME: "time",
63
- DATETIME: "datetime",
64
- SELECT: "select",
65
- RADIO: "radio",
66
- CHECKBOX: "checkbox",
67
- MULTI_SELECT: "multi_select",
68
- FILE: "file",
69
- RATING: "rating",
70
- SECTION_HEADER: "section_header",
71
- } as const;
72
-
73
- /**
74
- * CONDITIONAL LOGIC OPERATORS
75
- */
76
- export const OPERATORS = {
77
- EQUALS: "equals",
78
- NOT_EQUALS: "notEquals",
79
- IN: "in",
80
- NOT_IN: "notIn",
81
- GREATER_THAN: "gt",
82
- LESS_THAN: "lt",
83
- CONTAINS: "contains",
84
- IS_EMPTY: "isEmpty",
85
- IS_NOT_EMPTY: "isNotEmpty",
86
- } as const;
87
-
88
- /**
89
- * GET FORMS
90
- * Returns all forms for an organization
91
- */
92
- export const getForms = query({
93
- args: {
94
- sessionId: v.string(),
95
- organizationId: v.id("organizations"),
96
- subtype: v.optional(v.string()), // Filter by form type
97
- status: v.optional(v.string()), // Filter by status
98
- },
99
- handler: async (ctx, args) => {
100
- await requireAuthenticatedUser(ctx, args.sessionId);
101
-
102
- const q = ctx.db
103
- .query("objects")
104
- .withIndex("by_org_type", (q) =>
105
- q.eq("organizationId", args.organizationId).eq("type", "form")
106
- );
107
-
108
- let forms = await q.collect();
109
-
110
- // Apply filters
111
- if (args.subtype) {
112
- forms = forms.filter((f) => f.subtype === args.subtype);
113
- }
114
-
115
- if (args.status) {
116
- forms = forms.filter((f) => f.status === args.status);
117
- }
118
-
119
- return forms;
120
- },
121
- });
122
-
123
- /**
124
- * INTERNAL GET FORMS
125
- * Internal query for AI tools and actions that already have organizationId
126
- * This bypasses session authentication since the action layer is already authenticated
127
- */
128
- export const internalGetForms = internalQuery({
129
- args: {
130
- organizationId: v.id("organizations"),
131
- subtype: v.optional(v.string()), // Filter by form type
132
- status: v.optional(v.string()), // Filter by status
133
- },
134
- handler: async (ctx, args) => {
135
- const q = ctx.db
136
- .query("objects")
137
- .withIndex("by_org_type", (q) =>
138
- q.eq("organizationId", args.organizationId).eq("type", "form")
139
- );
140
-
141
- let forms = await q.collect();
142
-
143
- // Apply filters
144
- if (args.subtype) {
145
- forms = forms.filter((f) => f.subtype === args.subtype);
146
- }
147
-
148
- if (args.status) {
149
- forms = forms.filter((f) => f.status === args.status);
150
- }
151
-
152
- return forms;
153
- },
154
- });
155
-
156
- /**
157
- * GET FORM
158
- * Get a single form by ID
159
- */
160
- export const getForm = query({
161
- args: {
162
- sessionId: v.string(),
163
- formId: v.id("objects"),
164
- },
165
- handler: async (ctx, args) => {
166
- await requireAuthenticatedUser(ctx, args.sessionId);
167
-
168
- const form = await ctx.db.get(args.formId);
169
-
170
- if (!form || !("type" in form) || form.type !== "form") {
171
- throw new Error("Form not found");
172
- }
173
-
174
- return form;
175
- },
176
- });
177
-
178
- /**
179
- * INTERNAL GET FORM
180
- * Internal query for AI tools and actions that already have organizationId
181
- */
182
- export const internalGetForm = internalQuery({
183
- args: {
184
- formId: v.id("objects"),
185
- },
186
- handler: async (ctx, args) => {
187
- const form = await ctx.db.get(args.formId);
188
-
189
- if (!form || !("type" in form) || form.type !== "form") {
190
- throw new Error("Form not found");
191
- }
192
-
193
- return form;
194
- },
195
- });
196
-
197
- /**
198
- * GET PUBLIC FORM
199
- * Get a single published form by ID (no authentication required)
200
- * Used by public checkout pages
201
- */
202
- export const getPublicForm = query({
203
- args: {
204
- formId: v.id("objects"),
205
- },
206
- handler: async (ctx, args) => {
207
- const form = await ctx.db.get(args.formId);
208
-
209
- if (!form || !("type" in form) || form.type !== "form") {
210
- return null;
211
- }
212
-
213
- // Only return published forms to public
214
- if (form.status !== "published") {
215
- return null;
216
- }
217
-
218
- return form;
219
- },
220
- });
221
-
222
- /**
223
- * CREATE FORM
224
- * Create a new form template
225
- */
226
- export const createForm = mutation({
227
- args: {
228
- sessionId: v.string(),
229
- organizationId: v.id("organizations"),
230
- subtype: v.string(), // "registration" | "survey" | "application"
231
- name: v.string(),
232
- description: v.optional(v.string()),
233
- formSchema: v.any(), // Complex nested structure - validated in customProperties
234
- eventId: v.optional(v.id("objects")), // Optional: Link form to a specific event
235
- },
236
- handler: async (ctx, args) => {
237
- const { userId } = await requireAuthenticatedUser(ctx, args.sessionId);
238
-
239
- // CHECK LICENSE LIMIT: Enforce form limit for organization's tier
240
- // Free: 3, Starter: 20, Pro: 100, Agency: Unlimited, Enterprise: Unlimited
241
- await checkResourceLimit(ctx, args.organizationId, "form", "maxForms");
242
-
243
- // Validate event exists if provided
244
- if (args.eventId) {
245
- const event = await ctx.db.get(args.eventId);
246
- if (!event || event.type !== "event") {
247
- throw new Error("Event not found");
248
- }
249
- }
250
-
251
- // Create the form object
252
- const now = Date.now();
253
- const formId = await ctx.db.insert("objects", {
254
- organizationId: args.organizationId,
255
- type: "form",
256
- subtype: args.subtype,
257
- name: args.name,
258
- description: args.description || "",
259
- status: "draft",
260
- customProperties: {
261
- eventId: args.eventId, // Store event link
262
- formSchema: args.formSchema || {
263
- version: "1.0",
264
- fields: [],
265
- settings: {
266
- allowMultipleSubmissions: false,
267
- showProgressBar: true,
268
- submitButtonText: "Submit",
269
- successMessage: "Thank you for your submission!",
270
- redirectUrl: null,
271
- displayMode: "all", // "all" | "single-question" | "section-by-section" | "paginated"
272
- },
273
- sections: [],
274
- },
275
- stats: {
276
- views: 0,
277
- submissions: 0,
278
- completionRate: 0,
279
- },
280
- },
281
- createdBy: userId,
282
- createdAt: now,
283
- updatedAt: now,
284
- });
285
-
286
- // Create objectLink: form --[form_for]--> event (if event provided)
287
- if (args.eventId) {
288
- await ctx.db.insert("objectLinks", {
289
- organizationId: args.organizationId,
290
- fromObjectId: formId,
291
- toObjectId: args.eventId,
292
- linkType: "form_for",
293
- properties: {},
294
- createdAt: now,
295
- });
296
- }
297
-
298
- // Log the action
299
- await ctx.db.insert("objectActions", {
300
- organizationId: args.organizationId,
301
- objectId: formId,
302
- actionType: "created",
303
- performedBy: userId,
304
- performedAt: Date.now(),
305
- actionData: {
306
- status: "draft",
307
- eventId: args.eventId,
308
- },
309
- });
310
-
311
- return formId;
312
- },
313
- });
314
-
315
- /**
316
- * UPDATE FORM
317
- * Update an existing form
318
- */
319
- export const updateForm = mutation({
320
- args: {
321
- sessionId: v.string(),
322
- formId: v.id("objects"),
323
- name: v.optional(v.string()),
324
- description: v.optional(v.string()),
325
- subtype: v.optional(v.string()), // Allow updating form type
326
- formSchema: v.optional(v.any()),
327
- status: v.optional(v.string()),
328
- eventId: v.optional(v.union(v.id("objects"), v.null())), // Allow updating event link
329
- publicUrl: v.optional(v.union(v.string(), v.null())), // Allow updating public URL
330
- },
331
- handler: async (ctx, args) => {
332
- const { userId } = await requireAuthenticatedUser(ctx, args.sessionId);
333
-
334
- const form = await ctx.db.get(args.formId);
335
- if (!form || !("type" in form) || form.type !== "form") {
336
- throw new Error("Form not found");
337
- }
338
-
339
- // Validate event if provided
340
- if (args.eventId !== undefined && args.eventId !== null) {
341
- const event = await ctx.db.get(args.eventId);
342
- if (!event || event.type !== "event") {
343
- throw new Error("Event not found");
344
- }
345
- }
346
-
347
- const updates: Record<string, unknown> = {};
348
- const changes: Record<string, unknown> = {};
349
-
350
- if (args.name !== undefined) {
351
- updates.name = args.name;
352
- changes.name = { from: form.name, to: args.name };
353
- }
354
-
355
- if (args.description !== undefined) {
356
- updates.description = args.description;
357
- changes.description = { from: form.description, to: args.description };
358
- }
359
-
360
- if (args.subtype !== undefined) {
361
- updates.subtype = args.subtype;
362
- changes.subtype = { from: form.subtype, to: args.subtype };
363
- }
364
-
365
- if (args.status !== undefined) {
366
- updates.status = args.status;
367
- changes.status = { from: form.status, to: args.status };
368
- }
369
-
370
- if (args.formSchema !== undefined) {
371
- // CHECK FEATURE ACCESS: Multi-step forms, conditional logic, and file uploads require Starter tier
372
- const schema = args.formSchema;
373
-
374
- // Check multi-step forms (displayMode != "all")
375
- if (schema?.settings?.displayMode && schema.settings.displayMode !== "all") {
376
- await checkFeatureAccess(ctx, form.organizationId, "multiStepFormsEnabled");
377
- }
378
-
379
- // Check conditional logic (any field has conditions)
380
- if (schema?.fields && Array.isArray(schema.fields)) {
381
- const hasConditionalLogic = schema.fields.some((field: { conditionalLogic?: unknown }) =>
382
- field.conditionalLogic !== undefined && field.conditionalLogic !== null
383
- );
384
- if (hasConditionalLogic) {
385
- await checkFeatureAccess(ctx, form.organizationId, "conditionalLogicEnabled");
386
- }
387
-
388
- // Check file uploads (any field is type "file")
389
- const hasFileUploads = schema.fields.some((field: { type?: string }) =>
390
- field.type === "file"
391
- );
392
- if (hasFileUploads) {
393
- await checkFeatureAccess(ctx, form.organizationId, "fileUploadsEnabled");
394
- }
395
- }
396
-
397
- updates.customProperties = {
398
- ...form.customProperties,
399
- formSchema: args.formSchema,
400
- };
401
- changes.formSchema = "updated";
402
- }
403
-
404
- // Update eventId in customProperties
405
- if (args.eventId !== undefined) {
406
- updates.customProperties = {
407
- ...(updates.customProperties || form.customProperties),
408
- eventId: args.eventId,
409
- };
410
- changes.eventId = { from: form.customProperties?.eventId, to: args.eventId };
411
- }
412
-
413
- // Update publicUrl in customProperties
414
- if (args.publicUrl !== undefined) {
415
- updates.customProperties = {
416
- ...(updates.customProperties || form.customProperties),
417
- publicUrl: args.publicUrl,
418
- };
419
- changes.publicUrl = { from: form.customProperties?.publicUrl, to: args.publicUrl };
420
- }
421
-
422
- if (Object.keys(updates).length > 0) {
423
- updates.updatedAt = Date.now();
424
- await ctx.db.patch(args.formId, updates);
425
- }
426
-
427
- // Handle event link updates
428
- if (args.eventId !== undefined) {
429
- // Delete existing event link
430
- const existingLinks = await ctx.db
431
- .query("objectLinks")
432
- .filter((q) =>
433
- q.and(
434
- q.eq(q.field("fromObjectId"), args.formId),
435
- q.eq(q.field("linkType"), "form_for")
436
- )
437
- )
438
- .collect();
439
-
440
- for (const link of existingLinks) {
441
- await ctx.db.delete(link._id);
442
- }
443
-
444
- // Create new event link if eventId is provided (not null)
445
- if (args.eventId !== null) {
446
- await ctx.db.insert("objectLinks", {
447
- organizationId: form.organizationId,
448
- fromObjectId: args.formId,
449
- toObjectId: args.eventId,
450
- linkType: "form_for",
451
- properties: {},
452
- createdAt: Date.now(),
453
- });
454
- }
455
- }
456
-
457
- // Log the action
458
- await ctx.db.insert("objectActions", {
459
- organizationId: form.organizationId,
460
- objectId: args.formId,
461
- actionType: "updated",
462
- performedBy: userId,
463
- performedAt: Date.now(),
464
- actionData: changes,
465
- });
466
- },
467
- });
468
-
469
- /**
470
- * DELETE FORM
471
- * Permanently delete a form
472
- */
473
- export const deleteForm = mutation({
474
- args: {
475
- sessionId: v.string(),
476
- formId: v.id("objects"),
477
- },
478
- handler: async (ctx, args) => {
479
- const { userId } = await requireAuthenticatedUser(ctx, args.sessionId);
480
-
481
- const form = await ctx.db.get(args.formId);
482
- if (!form || !("type" in form) || form.type !== "form") {
483
- throw new Error("Form not found");
484
- }
485
-
486
- // Check permission
487
- const hasPermission = await checkPermission(
488
- ctx,
489
- userId,
490
- "manage_forms",
491
- form.organizationId
492
- );
493
-
494
- if (!hasPermission) {
495
- throw new Error("You do not have permission to delete forms");
496
- }
497
-
498
- // Log the action before deleting
499
- await ctx.db.insert("objectActions", {
500
- organizationId: form.organizationId,
501
- objectId: args.formId,
502
- actionType: "deleted",
503
- performedBy: userId,
504
- performedAt: Date.now(),
505
- actionData: {
506
- formName: form.name,
507
- formType: form.subtype,
508
- previousStatus: form.status,
509
- },
510
- });
511
-
512
- // Hard delete the form
513
- await ctx.db.delete(args.formId);
514
-
515
- return { success: true };
516
- },
517
- });
518
-
519
- /**
520
- * GET FORM RESPONSES
521
- * Get all responses for a form
522
- */
523
- export const getFormResponses = query({
524
- args: {
525
- sessionId: v.string(),
526
- formId: v.id("objects"),
527
- status: v.optional(v.string()), // Filter by completion status
528
- },
529
- handler: async (ctx, args) => {
530
- await requireAuthenticatedUser(ctx, args.sessionId);
531
-
532
- // Get all form responses
533
- let responses = await ctx.db
534
- .query("objects")
535
- .withIndex("by_type", (q) => q.eq("type", "formResponse"))
536
- .collect();
537
-
538
- // Filter by formId
539
- responses = responses.filter(
540
- (r) => r.customProperties?.formId === args.formId
541
- );
542
-
543
- // Filter by status if provided
544
- if (args.status) {
545
- responses = responses.filter((r) => r.status === args.status);
546
- }
547
-
548
- return responses;
549
- },
550
- });
551
-
552
- /**
553
- * INTERNAL GET FORM RESPONSES
554
- * Internal query for AI tools and actions that already have organizationId
555
- */
556
- export const internalGetFormResponses = internalQuery({
557
- args: {
558
- formId: v.id("objects"),
559
- status: v.optional(v.string()), // Filter by completion status
560
- },
561
- handler: async (ctx, args) => {
562
- // Get all form responses
563
- let responses = await ctx.db
564
- .query("objects")
565
- .withIndex("by_type", (q) => q.eq("type", "formResponse"))
566
- .collect();
567
-
568
- // Filter by formId
569
- responses = responses.filter(
570
- (r) => r.customProperties?.formId === args.formId
571
- );
572
-
573
- // Filter by status if provided
574
- if (args.status) {
575
- responses = responses.filter((r) => r.status === args.status);
576
- }
577
-
578
- return responses;
579
- },
580
- });
581
-
582
- /**
583
- * CREATE FORM RESPONSE
584
- * Submit a form response (during checkout or standalone)
585
- */
586
- export const createFormResponse = mutation({
587
- args: {
588
- sessionId: v.string(),
589
- organizationId: v.id("organizations"),
590
- formId: v.id("objects"),
591
- responses: v.any(), // The actual form data (key-value pairs)
592
- calculatedPricing: v.optional(v.any()), // Pricing data if applicable
593
- metadata: v.optional(v.any()), // IP, userAgent, duration, etc.
594
- },
595
- handler: async (ctx, args) => {
596
- const { userId } = await requireAuthenticatedUser(ctx, args.sessionId);
597
-
598
- // Get the form to verify it exists
599
- const form = await ctx.db.get(args.formId);
600
- if (!form || !("type" in form) || form.type !== "form") {
601
- throw new Error("Form not found");
602
- }
603
-
604
- // CHECK LICENSE LIMIT: Enforce form response limit per form
605
- const existingResponses = await ctx.db
606
- .query("objects")
607
- .withIndex("by_org_type", (q) =>
608
- q.eq("organizationId", args.organizationId).eq("type", "formResponse")
609
- )
610
- .filter((q) => {
611
- const customProps = q.field("customProperties") as { formId?: string } | undefined;
612
- return customProps?.formId === args.formId;
613
- })
614
- .collect();
615
-
616
- const responsesCount = existingResponses.length;
617
- const license = await getLicenseInternal(ctx, args.organizationId);
618
- const limit = license.limits.maxResponsesPerForm;
619
-
620
- if (limit !== -1 && responsesCount >= limit) {
621
- throw new ConvexError({
622
- code: "LIMIT_EXCEEDED",
623
- message: `You've reached your maxResponsesPerForm limit (${limit}). ` +
624
- `Upgrade to ${getNextTier(license.planTier)} for more capacity.`,
625
- limitKey: "maxResponsesPerForm",
626
- currentCount: responsesCount,
627
- limit,
628
- planTier: license.planTier,
629
- nextTier: getNextTier(license.planTier),
630
- isNested: true,
631
- parentId: args.formId,
632
- });
633
- }
634
-
635
- // Generate a name from responses (first + last name if available)
636
- const firstName = args.responses.first_name || args.responses.firstName || "";
637
- const lastName = args.responses.last_name || args.responses.lastName || "";
638
- const responseName = firstName && lastName
639
- ? `Response from ${firstName} ${lastName}`
640
- : `Response ${new Date().toLocaleDateString()}`;
641
-
642
- // Create the form response
643
- const now = Date.now();
644
- const responseId = await ctx.db.insert("objects", {
645
- organizationId: args.organizationId,
646
- type: "formResponse",
647
- subtype: form.subtype || "registration",
648
- name: responseName,
649
- description: `Form submission for ${form.name}`,
650
- status: "complete",
651
- customProperties: {
652
- formId: args.formId,
653
- responses: args.responses,
654
- calculatedPricing: args.calculatedPricing,
655
- submittedAt: now,
656
- ...(args.metadata || {}),
657
- },
658
- createdBy: userId,
659
- createdAt: now,
660
- updatedAt: now,
661
- });
662
-
663
- // Update form stats
664
- const currentStats = form.customProperties?.stats || {
665
- views: 0,
666
- submissions: 0,
667
- completionRate: 0,
668
- };
669
-
670
- // ⚡ PROFESSIONAL TIER: Form Analytics
671
- // Professional+ can track detailed form analytics (views, submissions, completion rates)
672
- const hasFormAnalytics = form.customProperties?.enableAnalytics === true;
673
- if (hasFormAnalytics) {
674
- // Check if they have access to analytics
675
- try {
676
- await checkFeatureAccess(ctx, args.organizationId, "formAnalyticsEnabled");
677
- } catch {
678
- // If they don't have access, analytics won't be detailed
679
- // Basic submission counting still works for all tiers
680
- }
681
- }
682
-
683
- await ctx.db.patch(args.formId, {
684
- customProperties: {
685
- ...form.customProperties,
686
- stats: {
687
- ...currentStats,
688
- submissions: currentStats.submissions + 1,
689
- lastSubmittedAt: now,
690
- },
691
- },
692
- updatedAt: Date.now(),
693
- });
694
-
695
- // Log the action
696
- await ctx.db.insert("objectActions", {
697
- organizationId: args.organizationId,
698
- objectId: responseId,
699
- actionType: "created",
700
- performedBy: userId,
701
- performedAt: now,
702
- actionData: {
703
- status: "complete",
704
- },
705
- });
706
-
707
- return responseId;
708
- },
709
- });
710
-
711
- /**
712
- * CREATE PUBLIC FORM RESPONSE (Internal)
713
- * Submit a form response without authentication
714
- * Used by public submission API endpoint
715
- */
716
- export const createPublicFormResponse = internalMutation({
717
- args: {
718
- formId: v.id("objects"),
719
- responses: v.any(), // The actual form data (key-value pairs)
720
- metadata: v.optional(v.any()), // IP, userAgent, submittedAt, etc.
721
- },
722
- handler: async (ctx, args) => {
723
- // Get the form to verify it exists and is published
724
- const form = await ctx.db.get(args.formId);
725
- if (!form || !("type" in form) || form.type !== "form") {
726
- throw new Error("Form not found");
727
- }
728
-
729
- // Only allow submissions to published forms
730
- if (form.status !== "published") {
731
- throw new Error("Form is not published");
732
- }
733
-
734
- // CHECK LICENSE LIMIT: Enforce form response limit per form
735
- const existingResponses = await ctx.db
736
- .query("objects")
737
- .withIndex("by_org_type", (q) =>
738
- q.eq("organizationId", form.organizationId).eq("type", "formResponse")
739
- )
740
- .filter((q) => {
741
- const customProps = q.field("customProperties") as { formId?: string } | undefined;
742
- return customProps?.formId === args.formId;
743
- })
744
- .collect();
745
-
746
- const responsesCount = existingResponses.length;
747
- const license = await getLicenseInternal(ctx, form.organizationId);
748
- const limit = license.limits.maxResponsesPerForm;
749
-
750
- if (limit !== -1 && responsesCount >= limit) {
751
- throw new ConvexError({
752
- code: "LIMIT_EXCEEDED",
753
- message: `You've reached your maxResponsesPerForm limit (${limit}). ` +
754
- `Upgrade to ${getNextTier(license.planTier)} for more capacity.`,
755
- limitKey: "maxResponsesPerForm",
756
- currentCount: responsesCount,
757
- limit,
758
- planTier: license.planTier,
759
- nextTier: getNextTier(license.planTier),
760
- isNested: true,
761
- parentId: args.formId,
762
- });
763
- }
764
-
765
- // Generate a name from responses (first + last name if available)
766
- const firstName = args.responses.first_name || args.responses.firstName || args.responses.contact_name || "";
767
- const lastName = args.responses.last_name || args.responses.lastName || "";
768
- const responseName = firstName && lastName
769
- ? `Response from ${firstName} ${lastName}`
770
- : firstName
771
- ? `Response from ${firstName}`
772
- : `Response ${new Date().toLocaleDateString()}`;
773
-
774
- // Create the form response
775
- const now = Date.now();
776
- const responseId = await ctx.db.insert("objects", {
777
- organizationId: form.organizationId,
778
- type: "formResponse",
779
- subtype: form.subtype || "registration",
780
- name: responseName,
781
- description: `Public form submission for ${form.name}`,
782
- status: "complete",
783
- customProperties: {
784
- formId: args.formId,
785
- responses: args.responses,
786
- submittedAt: args.metadata?.submittedAt || now,
787
- userAgent: args.metadata?.userAgent,
788
- ipAddress: args.metadata?.ipAddress,
789
- isPublicSubmission: true,
790
- },
791
- createdBy: undefined, // No user ID for public submissions
792
- createdAt: now,
793
- updatedAt: now,
794
- });
795
-
796
- // Update form stats
797
- const currentStats = form.customProperties?.stats || {
798
- views: 0,
799
- submissions: 0,
800
- completionRate: 0,
801
- };
802
-
803
- await ctx.db.patch(args.formId, {
804
- customProperties: {
805
- ...form.customProperties,
806
- stats: {
807
- ...currentStats,
808
- submissions: currentStats.submissions + 1,
809
- },
810
- },
811
- updatedAt: Date.now(),
812
- });
813
-
814
- // Log the action (no user, public submission)
815
- await ctx.db.insert("objectActions", {
816
- organizationId: form.organizationId,
817
- objectId: responseId,
818
- actionType: "created",
819
- performedBy: undefined, // No user ID for public submissions
820
- performedAt: now,
821
- actionData: {
822
- status: "complete",
823
- isPublicSubmission: true,
824
- },
825
- });
826
-
827
- return responseId;
828
- },
829
- });
830
-
831
- /**
832
- * GET FORM RESPONSE
833
- * Get a single form response
834
- */
835
- export const getFormResponse = query({
836
- args: {
837
- sessionId: v.string(),
838
- responseId: v.id("objects"),
839
- },
840
- handler: async (ctx, args) => {
841
- await requireAuthenticatedUser(ctx, args.sessionId);
842
-
843
- const response = await ctx.db.get(args.responseId);
844
-
845
- if (!response || !("type" in response) || response.type !== "formResponse") {
846
- throw new Error("Form response not found");
847
- }
848
-
849
- return response;
850
- },
851
- });
852
-
853
- /**
854
- * LINK FORM TO TICKET
855
- * Create an objectLink between a ticket/product and a form
856
- */
857
- export const linkFormToTicket = mutation({
858
- args: {
859
- sessionId: v.string(),
860
- ticketId: v.id("objects"),
861
- formId: v.id("objects"),
862
- timing: v.string(), // "duringCheckout" | "afterPurchase" | "standalone"
863
- required: v.boolean(),
864
- },
865
- handler: async (ctx, args) => {
866
- const { userId } = await requireAuthenticatedUser(ctx, args.sessionId);
867
-
868
- // Verify both objects exist
869
- const ticket = await ctx.db.get(args.ticketId);
870
- const form = await ctx.db.get(args.formId);
871
-
872
- if (!ticket || !("type" in ticket) || ticket.type !== "product") {
873
- throw new Error("Ticket not found");
874
- }
875
-
876
- if (!form || !("type" in form) || form.type !== "form") {
877
- throw new Error("Form not found");
878
- }
879
-
880
- // Create the link
881
- const linkId = await ctx.db.insert("objectLinks", {
882
- organizationId: ticket.organizationId,
883
- fromObjectId: args.ticketId,
884
- toObjectId: args.formId,
885
- linkType: "requiresForm",
886
- properties: {
887
- timing: args.timing,
888
- required: args.required,
889
- },
890
- createdBy: userId,
891
- createdAt: Date.now(),
892
- });
893
-
894
- return linkId;
895
- },
896
- });
897
-
898
- /**
899
- * GET LINKED FORM FOR TICKET
900
- * Get the form associated with a ticket/product
901
- */
902
- export const getLinkedForm = query({
903
- args: {
904
- sessionId: v.string(),
905
- ticketId: v.id("objects"),
906
- },
907
- handler: async (ctx, args) => {
908
- await requireAuthenticatedUser(ctx, args.sessionId);
909
-
910
- // Find the link
911
- const links = await ctx.db
912
- .query("objectLinks")
913
- .withIndex("by_from_object", (q) => q.eq("fromObjectId", args.ticketId))
914
- .collect();
915
-
916
- const formLink = links.find((link) => link.linkType === "requiresForm");
917
-
918
- if (!formLink) {
919
- return null;
920
- }
921
-
922
- // Get the form
923
- const form = await ctx.db.get(formLink.toObjectId);
924
-
925
- if (!form || !("type" in form) || form.type !== "form") {
926
- return null;
927
- }
928
-
929
- return {
930
- form,
931
- linkProperties: formLink.properties,
932
- };
933
- },
934
- });
935
-
936
- /**
937
- * PUBLISH FORM
938
- * Changes form status from draft to published
939
- */
940
- export const publishForm = mutation({
941
- args: {
942
- sessionId: v.string(),
943
- formId: v.id("objects"),
944
- },
945
- handler: async (ctx, args) => {
946
- const { userId } = await requireAuthenticatedUser(ctx, args.sessionId);
947
-
948
- const form = await ctx.db.get(args.formId);
949
- if (!form || !("type" in form) || form.type !== "form") {
950
- throw new Error("Form not found");
951
- }
952
-
953
- // Check permission
954
- const hasPermission = await checkPermission(
955
- ctx,
956
- userId,
957
- "manage_forms",
958
- form.organizationId
959
- );
960
-
961
- if (!hasPermission) {
962
- throw new Error("Permission denied: manage_forms required to publish forms");
963
- }
964
-
965
- // Update status to published
966
- await ctx.db.patch(args.formId, {
967
- status: "published",
968
- updatedAt: Date.now(),
969
- });
970
-
971
- // Log the action
972
- await ctx.db.insert("objectActions", {
973
- organizationId: form.organizationId,
974
- objectId: args.formId,
975
- actionType: "form_published",
976
- actionData: {},
977
- performedBy: userId,
978
- performedAt: Date.now(),
979
- });
980
-
981
- return { success: true };
982
- },
983
- });
984
-
985
- /**
986
- * UNPUBLISH FORM
987
- * Changes form status from published back to draft
988
- */
989
- export const unpublishForm = mutation({
990
- args: {
991
- sessionId: v.string(),
992
- formId: v.id("objects"),
993
- },
994
- handler: async (ctx, args) => {
995
- const { userId } = await requireAuthenticatedUser(ctx, args.sessionId);
996
-
997
- const form = await ctx.db.get(args.formId);
998
- if (!form || !("type" in form) || form.type !== "form") {
999
- throw new Error("Form not found");
1000
- }
1001
-
1002
- // Check permission
1003
- const hasPermission = await checkPermission(
1004
- ctx,
1005
- userId,
1006
- "manage_forms",
1007
- form.organizationId
1008
- );
1009
-
1010
- if (!hasPermission) {
1011
- throw new Error("Permission denied: manage_forms required to unpublish forms");
1012
- }
1013
-
1014
- // Update status to draft
1015
- await ctx.db.patch(args.formId, {
1016
- status: "draft",
1017
- updatedAt: Date.now(),
1018
- });
1019
-
1020
- // Log the action
1021
- await ctx.db.insert("objectActions", {
1022
- organizationId: form.organizationId,
1023
- objectId: args.formId,
1024
- actionType: "form_unpublished",
1025
- actionData: {},
1026
- performedBy: userId,
1027
- performedAt: Date.now(),
1028
- });
1029
-
1030
- return { success: true };
1031
- },
1032
- });
1033
-
1034
- /**
1035
- * DUPLICATE FORM
1036
- * Create a copy of an existing form with "Copy of [name]"
1037
- */
1038
- export const duplicateForm = mutation({
1039
- args: {
1040
- sessionId: v.string(),
1041
- formId: v.id("objects"),
1042
- },
1043
- handler: async (ctx, args) => {
1044
- const { userId } = await requireAuthenticatedUser(ctx, args.sessionId);
1045
-
1046
- const form = await ctx.db.get(args.formId);
1047
- if (!form || !("type" in form) || form.type !== "form") {
1048
- throw new Error("Form not found");
1049
- }
1050
-
1051
- // Check permission
1052
- const hasPermission = await checkPermission(
1053
- ctx,
1054
- userId,
1055
- "manage_forms",
1056
- form.organizationId
1057
- );
1058
-
1059
- if (!hasPermission) {
1060
- throw new Error("Permission denied: manage_forms required to duplicate forms");
1061
- }
1062
-
1063
- // Create the duplicated form
1064
- const now = Date.now();
1065
- const newFormId = await ctx.db.insert("objects", {
1066
- organizationId: form.organizationId,
1067
- type: "form",
1068
- subtype: form.subtype,
1069
- name: `Copy of ${form.name}`,
1070
- description: form.description || "",
1071
- status: "draft", // Always start as draft
1072
- customProperties: {
1073
- ...form.customProperties,
1074
- // Reset stats for the new form
1075
- stats: {
1076
- views: 0,
1077
- submissions: 0,
1078
- completionRate: 0,
1079
- },
1080
- },
1081
- createdBy: userId,
1082
- createdAt: now,
1083
- updatedAt: now,
1084
- });
1085
-
1086
- // If the original form has an event link, copy it
1087
- const eventId = form.customProperties?.eventId;
1088
- if (eventId) {
1089
- await ctx.db.insert("objectLinks", {
1090
- organizationId: form.organizationId,
1091
- fromObjectId: newFormId,
1092
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1093
- toObjectId: eventId as any,
1094
- linkType: "form_for",
1095
- properties: {},
1096
- createdAt: now,
1097
- });
1098
- }
1099
-
1100
- // Log the action
1101
- await ctx.db.insert("objectActions", {
1102
- organizationId: form.organizationId,
1103
- objectId: newFormId,
1104
- actionType: "created",
1105
- performedBy: userId,
1106
- performedAt: now,
1107
- actionData: {
1108
- status: "draft",
1109
- duplicatedFrom: args.formId,
1110
- },
1111
- });
1112
-
1113
- return newFormId;
1114
- },
1115
- });
1116
-
1117
- /**
1118
- * INTERNAL DUPLICATE FORM
1119
- * Internal mutation for AI tools that already have userId and organizationId
1120
- */
1121
- export const internalDuplicateForm = internalMutation({
1122
- args: {
1123
- userId: v.id("users"),
1124
- organizationId: v.id("organizations"),
1125
- formId: v.id("objects"),
1126
- },
1127
- handler: async (ctx, args) => {
1128
- const form = await ctx.db.get(args.formId);
1129
- if (!form || !("type" in form) || form.type !== "form") {
1130
- throw new Error("Form not found");
1131
- }
1132
-
1133
- // Verify form belongs to the organization
1134
- if (form.organizationId !== args.organizationId) {
1135
- throw new Error("Form does not belong to this organization");
1136
- }
1137
-
1138
- // Check permission
1139
- const hasPermission = await checkPermission(
1140
- ctx,
1141
- args.userId,
1142
- "manage_forms",
1143
- form.organizationId
1144
- );
1145
-
1146
- if (!hasPermission) {
1147
- throw new Error("Permission denied: manage_forms required to duplicate forms");
1148
- }
1149
-
1150
- // Create the duplicated form
1151
- const now = Date.now();
1152
- const newFormId = await ctx.db.insert("objects", {
1153
- organizationId: form.organizationId,
1154
- type: "form",
1155
- subtype: form.subtype,
1156
- name: `Copy of ${form.name}`,
1157
- description: form.description || "",
1158
- status: "draft", // Always start as draft
1159
- customProperties: {
1160
- ...form.customProperties,
1161
- // Reset stats for the new form
1162
- stats: {
1163
- views: 0,
1164
- submissions: 0,
1165
- completionRate: 0,
1166
- },
1167
- },
1168
- createdBy: args.userId,
1169
- createdAt: now,
1170
- updatedAt: now,
1171
- });
1172
-
1173
- // If the original form has an event link, copy it
1174
- const eventId = form.customProperties?.eventId;
1175
- if (eventId) {
1176
- await ctx.db.insert("objectLinks", {
1177
- organizationId: form.organizationId,
1178
- fromObjectId: newFormId,
1179
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1180
- toObjectId: eventId as any,
1181
- linkType: "form_for",
1182
- properties: {},
1183
- createdAt: now,
1184
- });
1185
- }
1186
-
1187
- // Log the action
1188
- await ctx.db.insert("objectActions", {
1189
- organizationId: form.organizationId,
1190
- objectId: newFormId,
1191
- actionType: "created",
1192
- performedBy: args.userId,
1193
- performedAt: now,
1194
- actionData: {
1195
- status: "draft",
1196
- duplicatedFrom: args.formId,
1197
- },
1198
- });
1199
-
1200
- return newFormId;
1201
- },
1202
- });
1203
-
1204
- /**
1205
- * INTERNAL UPDATE FORM
1206
- * Internal mutation for AI tools to update form properties
1207
- */
1208
- export const internalUpdateForm = internalMutation({
1209
- args: {
1210
- userId: v.id("users"),
1211
- organizationId: v.id("organizations"),
1212
- formId: v.id("objects"),
1213
- name: v.optional(v.string()),
1214
- description: v.optional(v.string()),
1215
- status: v.optional(v.string()),
1216
- },
1217
- handler: async (ctx, args) => {
1218
- const form = await ctx.db.get(args.formId);
1219
- if (!form || !("type" in form) || form.type !== "form") {
1220
- throw new Error("Form not found");
1221
- }
1222
-
1223
- // Verify form belongs to the organization
1224
- if (form.organizationId !== args.organizationId) {
1225
- throw new Error("Form does not belong to this organization");
1226
- }
1227
-
1228
- // Check permission
1229
- const hasPermission = await checkPermission(
1230
- ctx,
1231
- args.userId,
1232
- "manage_forms",
1233
- form.organizationId
1234
- );
1235
-
1236
- if (!hasPermission) {
1237
- throw new Error("Permission denied: manage_forms required to update forms");
1238
- }
1239
-
1240
- // Build update object
1241
- const updates: {
1242
- updatedAt: number;
1243
- name?: string;
1244
- description?: string;
1245
- status?: string;
1246
- } = {
1247
- updatedAt: Date.now(),
1248
- };
1249
-
1250
- if (args.name !== undefined) {
1251
- updates.name = args.name;
1252
- }
1253
-
1254
- if (args.description !== undefined) {
1255
- updates.description = args.description;
1256
- }
1257
-
1258
- if (args.status !== undefined) {
1259
- updates.status = args.status;
1260
- }
1261
-
1262
- // Update the form
1263
- await ctx.db.patch(args.formId, updates);
1264
-
1265
- // Log the action
1266
- await ctx.db.insert("objectActions", {
1267
- organizationId: form.organizationId,
1268
- objectId: args.formId,
1269
- actionType: "updated",
1270
- performedBy: args.userId,
1271
- performedAt: Date.now(),
1272
- actionData: {
1273
- updates: {
1274
- name: args.name,
1275
- description: args.description,
1276
- status: args.status,
1277
- },
1278
- },
1279
- });
1280
-
1281
- return { success: true };
1282
- },
1283
- });
1284
-
1285
- /**
1286
- * SAVE FORM AS TEMPLATE
1287
- * Save a form's schema as a reusable template in the system organization
1288
- */
1289
- export const saveFormAsTemplate = mutation({
1290
- args: {
1291
- sessionId: v.string(),
1292
- formId: v.id("objects"),
1293
- templateName: v.string(),
1294
- templateDescription: v.optional(v.string()),
1295
- templateCode: v.string(), // Unique code for the template
1296
- },
1297
- handler: async (ctx, args) => {
1298
- const { userId } = await requireAuthenticatedUser(ctx, args.sessionId);
1299
-
1300
- const form = await ctx.db.get(args.formId);
1301
- if (!form || !("type" in form) || form.type !== "form") {
1302
- throw new Error("Form not found");
1303
- }
1304
-
1305
- // Check permission
1306
- const hasPermission = await checkPermission(
1307
- ctx,
1308
- userId,
1309
- "manage_forms",
1310
- form.organizationId
1311
- );
1312
-
1313
- if (!hasPermission) {
1314
- throw new Error("Permission denied: manage_forms required to save templates");
1315
- }
1316
-
1317
- // Get system organization
1318
- const systemOrg = await ctx.db
1319
- .query("organizations")
1320
- .withIndex("by_slug", (q) => q.eq("slug", "system"))
1321
- .first();
1322
-
1323
- if (!systemOrg) {
1324
- throw new Error("System organization not found");
1325
- }
1326
-
1327
- // Check if template code already exists
1328
- const existingTemplate = await ctx.db
1329
- .query("objects")
1330
- .withIndex("by_org_type", (q) =>
1331
- q.eq("organizationId", systemOrg._id).eq("type", "template")
1332
- )
1333
- .filter((q) => q.eq(q.field("subtype"), "form"))
1334
- .filter((q) => q.eq(q.field("customProperties.code"), args.templateCode))
1335
- .first();
1336
-
1337
- if (existingTemplate) {
1338
- throw new Error(`Template with code "${args.templateCode}" already exists`);
1339
- }
1340
-
1341
- // Extract the form schema
1342
- const formSchema = form.customProperties?.formSchema;
1343
- if (!formSchema) {
1344
- throw new Error("Form does not have a schema to save as template");
1345
- }
1346
-
1347
- // Create the template in the system organization
1348
- const now = Date.now();
1349
- const templateId = await ctx.db.insert("objects", {
1350
- organizationId: systemOrg._id,
1351
- type: "template",
1352
- subtype: "form",
1353
- name: args.templateName,
1354
- description: args.templateDescription || "",
1355
- status: "published",
1356
- customProperties: {
1357
- code: args.templateCode,
1358
- description: args.templateDescription || "",
1359
- category: form.subtype || "registration",
1360
- formSchema: formSchema,
1361
- createdFromForm: args.formId,
1362
- originalOrganizationId: form.organizationId,
1363
- },
1364
- createdBy: userId,
1365
- createdAt: now,
1366
- updatedAt: now,
1367
- });
1368
-
1369
- // Auto-enable this template for the current organization
1370
- await ctx.db.insert("objects", {
1371
- organizationId: form.organizationId,
1372
- type: "form_template_availability",
1373
- name: `Form Template Availability: ${args.templateCode}`,
1374
- status: "active",
1375
- customProperties: {
1376
- templateCode: args.templateCode,
1377
- available: true,
1378
- enabledBy: userId,
1379
- enabledAt: now,
1380
- },
1381
- createdBy: userId,
1382
- createdAt: now,
1383
- updatedAt: now,
1384
- });
1385
-
1386
- // Log the action
1387
- await ctx.db.insert("objectActions", {
1388
- organizationId: form.organizationId,
1389
- objectId: templateId,
1390
- actionType: "template_created",
1391
- performedBy: userId,
1392
- performedAt: now,
1393
- actionData: {
1394
- templateCode: args.templateCode,
1395
- sourceFormId: args.formId,
1396
- },
1397
- });
1398
-
1399
- return templateId;
1400
- },
1401
- });