@pilotiq/pilotiq 0.1.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 (1409) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +11 -0
  3. package/CLAUDE.md +207 -0
  4. package/LICENSE +21 -0
  5. package/dist/Cluster.d.ts +56 -0
  6. package/dist/Cluster.d.ts.map +1 -0
  7. package/dist/Cluster.js +62 -0
  8. package/dist/Cluster.js.map +1 -0
  9. package/dist/Column.d.ts +378 -0
  10. package/dist/Column.d.ts.map +1 -0
  11. package/dist/Column.js +434 -0
  12. package/dist/Column.js.map +1 -0
  13. package/dist/Global.d.ts +123 -0
  14. package/dist/Global.d.ts.map +1 -0
  15. package/dist/Global.js +124 -0
  16. package/dist/Global.js.map +1 -0
  17. package/dist/Page.d.ts +90 -0
  18. package/dist/Page.d.ts.map +1 -0
  19. package/dist/Page.js +107 -0
  20. package/dist/Page.js.map +1 -0
  21. package/dist/Pilotiq.d.ts +505 -0
  22. package/dist/Pilotiq.d.ts.map +1 -0
  23. package/dist/Pilotiq.js +463 -0
  24. package/dist/Pilotiq.js.map +1 -0
  25. package/dist/PilotiqRegistry.d.ts +10 -0
  26. package/dist/PilotiqRegistry.d.ts.map +1 -0
  27. package/dist/PilotiqRegistry.js +32 -0
  28. package/dist/PilotiqRegistry.js.map +1 -0
  29. package/dist/PilotiqServiceProvider.d.ts +16 -0
  30. package/dist/PilotiqServiceProvider.d.ts.map +1 -0
  31. package/dist/PilotiqServiceProvider.js +57 -0
  32. package/dist/PilotiqServiceProvider.js.map +1 -0
  33. package/dist/RelationManager.d.ts +372 -0
  34. package/dist/RelationManager.d.ts.map +1 -0
  35. package/dist/RelationManager.js +342 -0
  36. package/dist/RelationManager.js.map +1 -0
  37. package/dist/RenderHook.d.ts +86 -0
  38. package/dist/RenderHook.d.ts.map +1 -0
  39. package/dist/RenderHook.js +116 -0
  40. package/dist/RenderHook.js.map +1 -0
  41. package/dist/Resource.d.ts +290 -0
  42. package/dist/Resource.d.ts.map +1 -0
  43. package/dist/Resource.js +362 -0
  44. package/dist/Resource.js.map +1 -0
  45. package/dist/RightPanel.d.ts +92 -0
  46. package/dist/RightPanel.d.ts.map +1 -0
  47. package/dist/RightPanel.js +61 -0
  48. package/dist/RightPanel.js.map +1 -0
  49. package/dist/Tab.d.ts +92 -0
  50. package/dist/Tab.d.ts.map +1 -0
  51. package/dist/Tab.js +93 -0
  52. package/dist/Tab.js.map +1 -0
  53. package/dist/UserMenuItem.d.ts +76 -0
  54. package/dist/UserMenuItem.d.ts.map +1 -0
  55. package/dist/UserMenuItem.js +87 -0
  56. package/dist/UserMenuItem.js.map +1 -0
  57. package/dist/actions/Action.d.ts +888 -0
  58. package/dist/actions/Action.d.ts.map +1 -0
  59. package/dist/actions/Action.js +1652 -0
  60. package/dist/actions/Action.js.map +1 -0
  61. package/dist/actions/ActionGroup.d.ts +85 -0
  62. package/dist/actions/ActionGroup.d.ts.map +1 -0
  63. package/dist/actions/ActionGroup.js +132 -0
  64. package/dist/actions/ActionGroup.js.map +1 -0
  65. package/dist/actions/attachFactory.d.ts +67 -0
  66. package/dist/actions/attachFactory.d.ts.map +1 -0
  67. package/dist/actions/attachFactory.js +115 -0
  68. package/dist/actions/attachFactory.js.map +1 -0
  69. package/dist/actions/exportFactory.d.ts +88 -0
  70. package/dist/actions/exportFactory.d.ts.map +1 -0
  71. package/dist/actions/exportFactory.js +144 -0
  72. package/dist/actions/exportFactory.js.map +1 -0
  73. package/dist/actions/importFactory.d.ts +97 -0
  74. package/dist/actions/importFactory.d.ts.map +1 -0
  75. package/dist/actions/importFactory.js +143 -0
  76. package/dist/actions/importFactory.js.map +1 -0
  77. package/dist/actions/index.d.ts +3 -0
  78. package/dist/actions/index.d.ts.map +1 -0
  79. package/dist/actions/index.js +3 -0
  80. package/dist/actions/index.js.map +1 -0
  81. package/dist/applyPageHooks.d.ts +54 -0
  82. package/dist/applyPageHooks.d.ts.map +1 -0
  83. package/dist/applyPageHooks.js +149 -0
  84. package/dist/applyPageHooks.js.map +1 -0
  85. package/dist/cells/coerce.d.ts +25 -0
  86. package/dist/cells/coerce.d.ts.map +1 -0
  87. package/dist/cells/coerce.js +87 -0
  88. package/dist/cells/coerce.js.map +1 -0
  89. package/dist/clusterPaths.d.ts +9 -0
  90. package/dist/clusterPaths.d.ts.map +1 -0
  91. package/dist/clusterPaths.js +19 -0
  92. package/dist/clusterPaths.js.map +1 -0
  93. package/dist/columns/BadgeColumn.d.ts +16 -0
  94. package/dist/columns/BadgeColumn.d.ts.map +1 -0
  95. package/dist/columns/BadgeColumn.js +25 -0
  96. package/dist/columns/BadgeColumn.js.map +1 -0
  97. package/dist/columns/BooleanColumn.d.ts +10 -0
  98. package/dist/columns/BooleanColumn.d.ts.map +1 -0
  99. package/dist/columns/BooleanColumn.js +18 -0
  100. package/dist/columns/BooleanColumn.js.map +1 -0
  101. package/dist/columns/ColorColumn.d.ts +27 -0
  102. package/dist/columns/ColorColumn.d.ts.map +1 -0
  103. package/dist/columns/ColorColumn.js +35 -0
  104. package/dist/columns/ColorColumn.js.map +1 -0
  105. package/dist/columns/IconColumn.d.ts +22 -0
  106. package/dist/columns/IconColumn.d.ts.map +1 -0
  107. package/dist/columns/IconColumn.js +28 -0
  108. package/dist/columns/IconColumn.js.map +1 -0
  109. package/dist/columns/ImageColumn.d.ts +17 -0
  110. package/dist/columns/ImageColumn.d.ts.map +1 -0
  111. package/dist/columns/ImageColumn.js +24 -0
  112. package/dist/columns/ImageColumn.js.map +1 -0
  113. package/dist/columns/SelectColumn.d.ts +36 -0
  114. package/dist/columns/SelectColumn.d.ts.map +1 -0
  115. package/dist/columns/SelectColumn.js +52 -0
  116. package/dist/columns/SelectColumn.js.map +1 -0
  117. package/dist/columns/TextColumn.d.ts +18 -0
  118. package/dist/columns/TextColumn.d.ts.map +1 -0
  119. package/dist/columns/TextColumn.js +20 -0
  120. package/dist/columns/TextColumn.js.map +1 -0
  121. package/dist/columns/TextInputColumn.d.ts +47 -0
  122. package/dist/columns/TextInputColumn.d.ts.map +1 -0
  123. package/dist/columns/TextInputColumn.js +60 -0
  124. package/dist/columns/TextInputColumn.js.map +1 -0
  125. package/dist/columns/ToggleColumn.d.ts +32 -0
  126. package/dist/columns/ToggleColumn.d.ts.map +1 -0
  127. package/dist/columns/ToggleColumn.js +45 -0
  128. package/dist/columns/ToggleColumn.js.map +1 -0
  129. package/dist/columns/index.d.ts +10 -0
  130. package/dist/columns/index.d.ts.map +1 -0
  131. package/dist/columns/index.js +10 -0
  132. package/dist/columns/index.js.map +1 -0
  133. package/dist/defaultGlobalPages.d.ts +11 -0
  134. package/dist/defaultGlobalPages.d.ts.map +1 -0
  135. package/dist/defaultGlobalPages.js +87 -0
  136. package/dist/defaultGlobalPages.js.map +1 -0
  137. package/dist/defaultPages.d.ts +247 -0
  138. package/dist/defaultPages.d.ts.map +1 -0
  139. package/dist/defaultPages.js +558 -0
  140. package/dist/defaultPages.js.map +1 -0
  141. package/dist/elements/Form.d.ts +219 -0
  142. package/dist/elements/Form.d.ts.map +1 -0
  143. package/dist/elements/Form.js +259 -0
  144. package/dist/elements/Form.js.map +1 -0
  145. package/dist/elements/ListTabs.d.ts +17 -0
  146. package/dist/elements/ListTabs.d.ts.map +1 -0
  147. package/dist/elements/ListTabs.js +23 -0
  148. package/dist/elements/ListTabs.js.map +1 -0
  149. package/dist/elements/Table.d.ts +535 -0
  150. package/dist/elements/Table.d.ts.map +1 -0
  151. package/dist/elements/Table.js +481 -0
  152. package/dist/elements/Table.js.map +1 -0
  153. package/dist/elements/TableGroup.d.ts +121 -0
  154. package/dist/elements/TableGroup.d.ts.map +1 -0
  155. package/dist/elements/TableGroup.js +162 -0
  156. package/dist/elements/TableGroup.js.map +1 -0
  157. package/dist/elements/dispatchAction.d.ts +127 -0
  158. package/dist/elements/dispatchAction.d.ts.map +1 -0
  159. package/dist/elements/dispatchAction.js +254 -0
  160. package/dist/elements/dispatchAction.js.map +1 -0
  161. package/dist/elements/dispatchForm.d.ts +220 -0
  162. package/dist/elements/dispatchForm.d.ts.map +1 -0
  163. package/dist/elements/dispatchForm.js +1645 -0
  164. package/dist/elements/dispatchForm.js.map +1 -0
  165. package/dist/elements/dispatchTable.d.ts +69 -0
  166. package/dist/elements/dispatchTable.d.ts.map +1 -0
  167. package/dist/elements/dispatchTable.js +606 -0
  168. package/dist/elements/dispatchTable.js.map +1 -0
  169. package/dist/elements/index.d.ts +3 -0
  170. package/dist/elements/index.d.ts.map +1 -0
  171. package/dist/elements/index.js +3 -0
  172. package/dist/elements/index.js.map +1 -0
  173. package/dist/entries/BadgeEntry.d.ts +21 -0
  174. package/dist/entries/BadgeEntry.d.ts.map +1 -0
  175. package/dist/entries/BadgeEntry.js +32 -0
  176. package/dist/entries/BadgeEntry.js.map +1 -0
  177. package/dist/entries/CodeEntry.d.ts +38 -0
  178. package/dist/entries/CodeEntry.d.ts.map +1 -0
  179. package/dist/entries/CodeEntry.js +44 -0
  180. package/dist/entries/CodeEntry.js.map +1 -0
  181. package/dist/entries/ColorEntry.d.ts +32 -0
  182. package/dist/entries/ColorEntry.d.ts.map +1 -0
  183. package/dist/entries/ColorEntry.js +48 -0
  184. package/dist/entries/ColorEntry.js.map +1 -0
  185. package/dist/entries/ComponentEntry.d.ts +66 -0
  186. package/dist/entries/ComponentEntry.d.ts.map +1 -0
  187. package/dist/entries/ComponentEntry.js +86 -0
  188. package/dist/entries/ComponentEntry.js.map +1 -0
  189. package/dist/entries/Entry.d.ts +175 -0
  190. package/dist/entries/Entry.d.ts.map +1 -0
  191. package/dist/entries/Entry.js +233 -0
  192. package/dist/entries/Entry.js.map +1 -0
  193. package/dist/entries/IconEntry.d.ts +30 -0
  194. package/dist/entries/IconEntry.d.ts.map +1 -0
  195. package/dist/entries/IconEntry.js +34 -0
  196. package/dist/entries/IconEntry.js.map +1 -0
  197. package/dist/entries/ImageEntry.d.ts +33 -0
  198. package/dist/entries/ImageEntry.d.ts.map +1 -0
  199. package/dist/entries/ImageEntry.js +47 -0
  200. package/dist/entries/ImageEntry.js.map +1 -0
  201. package/dist/entries/KeyValueEntry.d.ts +30 -0
  202. package/dist/entries/KeyValueEntry.d.ts.map +1 -0
  203. package/dist/entries/KeyValueEntry.js +38 -0
  204. package/dist/entries/KeyValueEntry.js.map +1 -0
  205. package/dist/entries/RepeatableEntry.d.ts +122 -0
  206. package/dist/entries/RepeatableEntry.d.ts.map +1 -0
  207. package/dist/entries/RepeatableEntry.js +121 -0
  208. package/dist/entries/RepeatableEntry.js.map +1 -0
  209. package/dist/entries/TextEntry.d.ts +38 -0
  210. package/dist/entries/TextEntry.d.ts.map +1 -0
  211. package/dist/entries/TextEntry.js +49 -0
  212. package/dist/entries/TextEntry.js.map +1 -0
  213. package/dist/entries/index.d.ts +2 -0
  214. package/dist/entries/index.d.ts.map +1 -0
  215. package/dist/entries/index.js +8 -0
  216. package/dist/entries/index.js.map +1 -0
  217. package/dist/entries/registry.d.ts +41 -0
  218. package/dist/entries/registry.d.ts.map +1 -0
  219. package/dist/entries/registry.js +17 -0
  220. package/dist/entries/registry.js.map +1 -0
  221. package/dist/fields/BuilderField.d.ts +420 -0
  222. package/dist/fields/BuilderField.d.ts.map +1 -0
  223. package/dist/fields/BuilderField.js +359 -0
  224. package/dist/fields/BuilderField.js.map +1 -0
  225. package/dist/fields/CheckboxField.d.ts +18 -0
  226. package/dist/fields/CheckboxField.d.ts.map +1 -0
  227. package/dist/fields/CheckboxField.js +23 -0
  228. package/dist/fields/CheckboxField.js.map +1 -0
  229. package/dist/fields/CheckboxListField.d.ts +25 -0
  230. package/dist/fields/CheckboxListField.d.ts.map +1 -0
  231. package/dist/fields/CheckboxListField.js +46 -0
  232. package/dist/fields/CheckboxListField.js.map +1 -0
  233. package/dist/fields/ColorPickerField.d.ts +16 -0
  234. package/dist/fields/ColorPickerField.d.ts.map +1 -0
  235. package/dist/fields/ColorPickerField.js +21 -0
  236. package/dist/fields/ColorPickerField.js.map +1 -0
  237. package/dist/fields/DateField.d.ts +29 -0
  238. package/dist/fields/DateField.d.ts.map +1 -0
  239. package/dist/fields/DateField.js +45 -0
  240. package/dist/fields/DateField.js.map +1 -0
  241. package/dist/fields/EmailField.d.ts +8 -0
  242. package/dist/fields/EmailField.d.ts.map +1 -0
  243. package/dist/fields/EmailField.js +13 -0
  244. package/dist/fields/EmailField.js.map +1 -0
  245. package/dist/fields/Field.d.ts +485 -0
  246. package/dist/fields/Field.d.ts.map +1 -0
  247. package/dist/fields/Field.js +539 -0
  248. package/dist/fields/Field.js.map +1 -0
  249. package/dist/fields/FileUploadField.d.ts +43 -0
  250. package/dist/fields/FileUploadField.d.ts.map +1 -0
  251. package/dist/fields/FileUploadField.js +60 -0
  252. package/dist/fields/FileUploadField.js.map +1 -0
  253. package/dist/fields/HiddenField.d.ts +19 -0
  254. package/dist/fields/HiddenField.d.ts.map +1 -0
  255. package/dist/fields/HiddenField.js +24 -0
  256. package/dist/fields/HiddenField.js.map +1 -0
  257. package/dist/fields/KeyValueField.d.ts +36 -0
  258. package/dist/fields/KeyValueField.d.ts.map +1 -0
  259. package/dist/fields/KeyValueField.js +47 -0
  260. package/dist/fields/KeyValueField.js.map +1 -0
  261. package/dist/fields/MarkdownField.d.ts +79 -0
  262. package/dist/fields/MarkdownField.d.ts.map +1 -0
  263. package/dist/fields/MarkdownField.js +117 -0
  264. package/dist/fields/MarkdownField.js.map +1 -0
  265. package/dist/fields/NumberField.d.ts +17 -0
  266. package/dist/fields/NumberField.d.ts.map +1 -0
  267. package/dist/fields/NumberField.js +27 -0
  268. package/dist/fields/NumberField.js.map +1 -0
  269. package/dist/fields/RadioField.d.ts +26 -0
  270. package/dist/fields/RadioField.d.ts.map +1 -0
  271. package/dist/fields/RadioField.js +47 -0
  272. package/dist/fields/RadioField.js.map +1 -0
  273. package/dist/fields/RepeaterField.d.ts +594 -0
  274. package/dist/fields/RepeaterField.d.ts.map +1 -0
  275. package/dist/fields/RepeaterField.js +504 -0
  276. package/dist/fields/RepeaterField.js.map +1 -0
  277. package/dist/fields/RowButton.d.ts +86 -0
  278. package/dist/fields/RowButton.d.ts.map +1 -0
  279. package/dist/fields/RowButton.js +85 -0
  280. package/dist/fields/RowButton.js.map +1 -0
  281. package/dist/fields/SelectField.d.ts +127 -0
  282. package/dist/fields/SelectField.d.ts.map +1 -0
  283. package/dist/fields/SelectField.js +160 -0
  284. package/dist/fields/SelectField.js.map +1 -0
  285. package/dist/fields/SliderField.d.ts +31 -0
  286. package/dist/fields/SliderField.d.ts.map +1 -0
  287. package/dist/fields/SliderField.js +45 -0
  288. package/dist/fields/SliderField.js.map +1 -0
  289. package/dist/fields/SlugField.d.ts +11 -0
  290. package/dist/fields/SlugField.d.ts.map +1 -0
  291. package/dist/fields/SlugField.js +19 -0
  292. package/dist/fields/SlugField.js.map +1 -0
  293. package/dist/fields/TagsInputField.d.ts +65 -0
  294. package/dist/fields/TagsInputField.d.ts.map +1 -0
  295. package/dist/fields/TagsInputField.js +104 -0
  296. package/dist/fields/TagsInputField.js.map +1 -0
  297. package/dist/fields/TextField.d.ts +11 -0
  298. package/dist/fields/TextField.d.ts.map +1 -0
  299. package/dist/fields/TextField.js +19 -0
  300. package/dist/fields/TextField.js.map +1 -0
  301. package/dist/fields/TextareaField.d.ts +40 -0
  302. package/dist/fields/TextareaField.d.ts.map +1 -0
  303. package/dist/fields/TextareaField.js +51 -0
  304. package/dist/fields/TextareaField.js.map +1 -0
  305. package/dist/fields/ToggleButtonsField.d.ts +24 -0
  306. package/dist/fields/ToggleButtonsField.d.ts.map +1 -0
  307. package/dist/fields/ToggleButtonsField.js +41 -0
  308. package/dist/fields/ToggleButtonsField.js.map +1 -0
  309. package/dist/fields/ToggleField.d.ts +8 -0
  310. package/dist/fields/ToggleField.d.ts.map +1 -0
  311. package/dist/fields/ToggleField.js +13 -0
  312. package/dist/fields/ToggleField.js.map +1 -0
  313. package/dist/fields/optionsResolver.d.ts +54 -0
  314. package/dist/fields/optionsResolver.d.ts.map +1 -0
  315. package/dist/fields/optionsResolver.js +62 -0
  316. package/dist/fields/optionsResolver.js.map +1 -0
  317. package/dist/fields/resolveField.d.ts +21 -0
  318. package/dist/fields/resolveField.d.ts.map +1 -0
  319. package/dist/fields/resolveField.js +26 -0
  320. package/dist/fields/resolveField.js.map +1 -0
  321. package/dist/filters/BooleanFilter.d.ts +20 -0
  322. package/dist/filters/BooleanFilter.d.ts.map +1 -0
  323. package/dist/filters/BooleanFilter.js +31 -0
  324. package/dist/filters/BooleanFilter.js.map +1 -0
  325. package/dist/filters/DateRangeFilter.d.ts +68 -0
  326. package/dist/filters/DateRangeFilter.d.ts.map +1 -0
  327. package/dist/filters/DateRangeFilter.js +137 -0
  328. package/dist/filters/DateRangeFilter.js.map +1 -0
  329. package/dist/filters/Filter.d.ts +140 -0
  330. package/dist/filters/Filter.d.ts.map +1 -0
  331. package/dist/filters/Filter.js +99 -0
  332. package/dist/filters/Filter.js.map +1 -0
  333. package/dist/filters/FormFilter.d.ts +103 -0
  334. package/dist/filters/FormFilter.d.ts.map +1 -0
  335. package/dist/filters/FormFilter.js +180 -0
  336. package/dist/filters/FormFilter.js.map +1 -0
  337. package/dist/filters/MultiSelectFilter.d.ts +41 -0
  338. package/dist/filters/MultiSelectFilter.d.ts.map +1 -0
  339. package/dist/filters/MultiSelectFilter.js +67 -0
  340. package/dist/filters/MultiSelectFilter.js.map +1 -0
  341. package/dist/filters/QueryBuilderFilter.d.ts +145 -0
  342. package/dist/filters/QueryBuilderFilter.d.ts.map +1 -0
  343. package/dist/filters/QueryBuilderFilter.js +323 -0
  344. package/dist/filters/QueryBuilderFilter.js.map +1 -0
  345. package/dist/filters/SelectFilter.d.ts +26 -0
  346. package/dist/filters/SelectFilter.d.ts.map +1 -0
  347. package/dist/filters/SelectFilter.js +35 -0
  348. package/dist/filters/SelectFilter.js.map +1 -0
  349. package/dist/filters/TernaryFilter.d.ts +35 -0
  350. package/dist/filters/TernaryFilter.d.ts.map +1 -0
  351. package/dist/filters/TernaryFilter.js +71 -0
  352. package/dist/filters/TernaryFilter.js.map +1 -0
  353. package/dist/filters/TrashedFilter.d.ts +28 -0
  354. package/dist/filters/TrashedFilter.d.ts.map +1 -0
  355. package/dist/filters/TrashedFilter.js +52 -0
  356. package/dist/filters/TrashedFilter.js.map +1 -0
  357. package/dist/filters/queryBuilder/BooleanConstraint.d.ts +13 -0
  358. package/dist/filters/queryBuilder/BooleanConstraint.d.ts.map +1 -0
  359. package/dist/filters/queryBuilder/BooleanConstraint.js +27 -0
  360. package/dist/filters/queryBuilder/BooleanConstraint.js.map +1 -0
  361. package/dist/filters/queryBuilder/Constraint.d.ts +74 -0
  362. package/dist/filters/queryBuilder/Constraint.d.ts.map +1 -0
  363. package/dist/filters/queryBuilder/Constraint.js +45 -0
  364. package/dist/filters/queryBuilder/Constraint.js.map +1 -0
  365. package/dist/filters/queryBuilder/DateConstraint.d.ts +18 -0
  366. package/dist/filters/queryBuilder/DateConstraint.d.ts.map +1 -0
  367. package/dist/filters/queryBuilder/DateConstraint.js +63 -0
  368. package/dist/filters/queryBuilder/DateConstraint.js.map +1 -0
  369. package/dist/filters/queryBuilder/NumberConstraint.d.ts +12 -0
  370. package/dist/filters/queryBuilder/NumberConstraint.d.ts.map +1 -0
  371. package/dist/filters/queryBuilder/NumberConstraint.js +61 -0
  372. package/dist/filters/queryBuilder/NumberConstraint.js.map +1 -0
  373. package/dist/filters/queryBuilder/SelectConstraint.d.ts +22 -0
  374. package/dist/filters/queryBuilder/SelectConstraint.d.ts.map +1 -0
  375. package/dist/filters/queryBuilder/SelectConstraint.js +66 -0
  376. package/dist/filters/queryBuilder/SelectConstraint.js.map +1 -0
  377. package/dist/filters/queryBuilder/TextConstraint.d.ts +18 -0
  378. package/dist/filters/queryBuilder/TextConstraint.d.ts.map +1 -0
  379. package/dist/filters/queryBuilder/TextConstraint.js +58 -0
  380. package/dist/filters/queryBuilder/TextConstraint.js.map +1 -0
  381. package/dist/filters/queryBuilder/index.d.ts +7 -0
  382. package/dist/filters/queryBuilder/index.d.ts.map +1 -0
  383. package/dist/filters/queryBuilder/index.js +7 -0
  384. package/dist/filters/queryBuilder/index.js.map +1 -0
  385. package/dist/icons/index.d.ts +3 -0
  386. package/dist/icons/index.d.ts.map +1 -0
  387. package/dist/icons/index.js +3 -0
  388. package/dist/icons/index.js.map +1 -0
  389. package/dist/icons/lucide.d.ts +16 -0
  390. package/dist/icons/lucide.d.ts.map +1 -0
  391. package/dist/icons/lucide.js +173 -0
  392. package/dist/icons/lucide.js.map +1 -0
  393. package/dist/icons/registry.d.ts +27 -0
  394. package/dist/icons/registry.d.ts.map +1 -0
  395. package/dist/icons/registry.js +35 -0
  396. package/dist/icons/registry.js.map +1 -0
  397. package/dist/icons/types.d.ts +38 -0
  398. package/dist/icons/types.d.ts.map +1 -0
  399. package/dist/icons/types.js +23 -0
  400. package/dist/icons/types.js.map +1 -0
  401. package/dist/index.d.ts +118 -0
  402. package/dist/index.d.ts.map +1 -0
  403. package/dist/index.js +135 -0
  404. package/dist/index.js.map +1 -0
  405. package/dist/io/csv.d.ts +51 -0
  406. package/dist/io/csv.d.ts.map +1 -0
  407. package/dist/io/csv.js +168 -0
  408. package/dist/io/csv.js.map +1 -0
  409. package/dist/notifications/Notification.d.ts +181 -0
  410. package/dist/notifications/Notification.d.ts.map +1 -0
  411. package/dist/notifications/Notification.js +290 -0
  412. package/dist/notifications/Notification.js.map +1 -0
  413. package/dist/notifications/broadcast.d.ts +58 -0
  414. package/dist/notifications/broadcast.d.ts.map +1 -0
  415. package/dist/notifications/broadcast.js +72 -0
  416. package/dist/notifications/broadcast.js.map +1 -0
  417. package/dist/notifications/database.d.ts +164 -0
  418. package/dist/notifications/database.d.ts.map +1 -0
  419. package/dist/notifications/database.js +321 -0
  420. package/dist/notifications/database.js.map +1 -0
  421. package/dist/notifications/dispatchNotificationAction.d.ts +48 -0
  422. package/dist/notifications/dispatchNotificationAction.d.ts.map +1 -0
  423. package/dist/notifications/dispatchNotificationAction.js +100 -0
  424. package/dist/notifications/dispatchNotificationAction.js.map +1 -0
  425. package/dist/notifications/flash.d.ts +34 -0
  426. package/dist/notifications/flash.d.ts.map +1 -0
  427. package/dist/notifications/flash.js +51 -0
  428. package/dist/notifications/flash.js.map +1 -0
  429. package/dist/notifications/index.d.ts +8 -0
  430. package/dist/notifications/index.d.ts.map +1 -0
  431. package/dist/notifications/index.js +6 -0
  432. package/dist/notifications/index.js.map +1 -0
  433. package/dist/notifications/registerBroadcastAuth.d.ts +45 -0
  434. package/dist/notifications/registerBroadcastAuth.d.ts.map +1 -0
  435. package/dist/notifications/registerBroadcastAuth.js +86 -0
  436. package/dist/notifications/registerBroadcastAuth.js.map +1 -0
  437. package/dist/notifications/resolveSavedNotification.d.ts +21 -0
  438. package/dist/notifications/resolveSavedNotification.d.ts.map +1 -0
  439. package/dist/notifications/resolveSavedNotification.js +43 -0
  440. package/dist/notifications/resolveSavedNotification.js.map +1 -0
  441. package/dist/notifications/types.d.ts +87 -0
  442. package/dist/notifications/types.d.ts.map +1 -0
  443. package/dist/notifications/types.js +2 -0
  444. package/dist/notifications/types.js.map +1 -0
  445. package/dist/orm/m2mAccessor.d.ts +49 -0
  446. package/dist/orm/m2mAccessor.d.ts.map +1 -0
  447. package/dist/orm/m2mAccessor.js +45 -0
  448. package/dist/orm/m2mAccessor.js.map +1 -0
  449. package/dist/orm/modelDefaults.d.ts +347 -0
  450. package/dist/orm/modelDefaults.d.ts.map +1 -0
  451. package/dist/orm/modelDefaults.js +375 -0
  452. package/dist/orm/modelDefaults.js.map +1 -0
  453. package/dist/pageData.d.ts +778 -0
  454. package/dist/pageData.d.ts.map +1 -0
  455. package/dist/pageData.js +3725 -0
  456. package/dist/pageData.js.map +1 -0
  457. package/dist/plugins/index.d.ts +2 -0
  458. package/dist/plugins/index.d.ts.map +1 -0
  459. package/dist/plugins/index.js +2 -0
  460. package/dist/plugins/index.js.map +1 -0
  461. package/dist/plugins/themeEditor.d.ts +17 -0
  462. package/dist/plugins/themeEditor.d.ts.map +1 -0
  463. package/dist/plugins/themeEditor.js +23 -0
  464. package/dist/plugins/themeEditor.js.map +1 -0
  465. package/dist/react/AppShell.d.ts +58 -0
  466. package/dist/react/AppShell.d.ts.map +1 -0
  467. package/dist/react/AppShell.js +58 -0
  468. package/dist/react/AppShell.js.map +1 -0
  469. package/dist/react/CommandPalette.d.ts +21 -0
  470. package/dist/react/CommandPalette.d.ts.map +1 -0
  471. package/dist/react/CommandPalette.js +236 -0
  472. package/dist/react/CommandPalette.js.map +1 -0
  473. package/dist/react/FormStateContext.d.ts +83 -0
  474. package/dist/react/FormStateContext.d.ts.map +1 -0
  475. package/dist/react/FormStateContext.js +284 -0
  476. package/dist/react/FormStateContext.js.map +1 -0
  477. package/dist/react/HeadHooks.d.ts +26 -0
  478. package/dist/react/HeadHooks.d.ts.map +1 -0
  479. package/dist/react/HeadHooks.js +141 -0
  480. package/dist/react/HeadHooks.js.map +1 -0
  481. package/dist/react/NotificationActionStrip.d.ts +39 -0
  482. package/dist/react/NotificationActionStrip.d.ts.map +1 -0
  483. package/dist/react/NotificationActionStrip.js +129 -0
  484. package/dist/react/NotificationActionStrip.js.map +1 -0
  485. package/dist/react/NotificationBell.d.ts +20 -0
  486. package/dist/react/NotificationBell.d.ts.map +1 -0
  487. package/dist/react/NotificationBell.js +273 -0
  488. package/dist/react/NotificationBell.js.map +1 -0
  489. package/dist/react/RenderHookSlot.d.ts +20 -0
  490. package/dist/react/RenderHookSlot.d.ts.map +1 -0
  491. package/dist/react/RenderHookSlot.js +24 -0
  492. package/dist/react/RenderHookSlot.js.map +1 -0
  493. package/dist/react/RightSidebar.d.ts +33 -0
  494. package/dist/react/RightSidebar.d.ts.map +1 -0
  495. package/dist/react/RightSidebar.js +82 -0
  496. package/dist/react/RightSidebar.js.map +1 -0
  497. package/dist/react/RightSidebarContext.d.ts +62 -0
  498. package/dist/react/RightSidebarContext.d.ts.map +1 -0
  499. package/dist/react/RightSidebarContext.js +178 -0
  500. package/dist/react/RightSidebarContext.js.map +1 -0
  501. package/dist/react/RightSidebarTrigger.d.ts +16 -0
  502. package/dist/react/RightSidebarTrigger.d.ts.map +1 -0
  503. package/dist/react/RightSidebarTrigger.js +24 -0
  504. package/dist/react/RightSidebarTrigger.js.map +1 -0
  505. package/dist/react/SchemaRenderer.d.ts +63 -0
  506. package/dist/react/SchemaRenderer.d.ts.map +1 -0
  507. package/dist/react/SchemaRenderer.js +3458 -0
  508. package/dist/react/SchemaRenderer.js.map +1 -0
  509. package/dist/react/SearchTrigger.d.ts +13 -0
  510. package/dist/react/SearchTrigger.d.ts.map +1 -0
  511. package/dist/react/SearchTrigger.js +30 -0
  512. package/dist/react/SearchTrigger.js.map +1 -0
  513. package/dist/react/ThemeProvider.d.ts +18 -0
  514. package/dist/react/ThemeProvider.d.ts.map +1 -0
  515. package/dist/react/ThemeProvider.js +66 -0
  516. package/dist/react/ThemeProvider.js.map +1 -0
  517. package/dist/react/ThemeSettingsPage.d.ts +10 -0
  518. package/dist/react/ThemeSettingsPage.d.ts.map +1 -0
  519. package/dist/react/ThemeSettingsPage.js +293 -0
  520. package/dist/react/ThemeSettingsPage.js.map +1 -0
  521. package/dist/react/ThemeToggle.d.ts +2 -0
  522. package/dist/react/ThemeToggle.d.ts.map +1 -0
  523. package/dist/react/ThemeToggle.js +8 -0
  524. package/dist/react/ThemeToggle.js.map +1 -0
  525. package/dist/react/Toaster.d.ts +25 -0
  526. package/dist/react/Toaster.d.ts.map +1 -0
  527. package/dist/react/Toaster.js +89 -0
  528. package/dist/react/Toaster.js.map +1 -0
  529. package/dist/react/UserMenu.d.ts +23 -0
  530. package/dist/react/UserMenu.d.ts.map +1 -0
  531. package/dist/react/UserMenu.js +78 -0
  532. package/dist/react/UserMenu.js.map +1 -0
  533. package/dist/react/WidgetDataContext.d.ts +64 -0
  534. package/dist/react/WidgetDataContext.d.ts.map +1 -0
  535. package/dist/react/WidgetDataContext.js +89 -0
  536. package/dist/react/WidgetDataContext.js.map +1 -0
  537. package/dist/react/cells/EditableCell.d.ts +20 -0
  538. package/dist/react/cells/EditableCell.d.ts.map +1 -0
  539. package/dist/react/cells/EditableCell.js +251 -0
  540. package/dist/react/cells/EditableCell.js.map +1 -0
  541. package/dist/react/fieldJsHandler.d.ts +33 -0
  542. package/dist/react/fieldJsHandler.d.ts.map +1 -0
  543. package/dist/react/fieldJsHandler.js +61 -0
  544. package/dist/react/fieldJsHandler.js.map +1 -0
  545. package/dist/react/fields/BuilderInput.d.ts +21 -0
  546. package/dist/react/fields/BuilderInput.d.ts.map +1 -0
  547. package/dist/react/fields/BuilderInput.js +553 -0
  548. package/dist/react/fields/BuilderInput.js.map +1 -0
  549. package/dist/react/fields/CheckboxInput.d.ts +9 -0
  550. package/dist/react/fields/CheckboxInput.d.ts.map +1 -0
  551. package/dist/react/fields/CheckboxInput.js +23 -0
  552. package/dist/react/fields/CheckboxInput.js.map +1 -0
  553. package/dist/react/fields/CheckboxListInput.d.ts +19 -0
  554. package/dist/react/fields/CheckboxListInput.d.ts.map +1 -0
  555. package/dist/react/fields/CheckboxListInput.js +53 -0
  556. package/dist/react/fields/CheckboxListInput.js.map +1 -0
  557. package/dist/react/fields/ColorInput.d.ts +12 -0
  558. package/dist/react/fields/ColorInput.d.ts.map +1 -0
  559. package/dist/react/fields/ColorInput.js +29 -0
  560. package/dist/react/fields/ColorInput.js.map +1 -0
  561. package/dist/react/fields/DateFieldInput.d.ts +8 -0
  562. package/dist/react/fields/DateFieldInput.d.ts.map +1 -0
  563. package/dist/react/fields/DateFieldInput.js +39 -0
  564. package/dist/react/fields/DateFieldInput.js.map +1 -0
  565. package/dist/react/fields/DateTimeInput.d.ts +13 -0
  566. package/dist/react/fields/DateTimeInput.d.ts.map +1 -0
  567. package/dist/react/fields/DateTimeInput.js +29 -0
  568. package/dist/react/fields/DateTimeInput.js.map +1 -0
  569. package/dist/react/fields/FieldShell.d.ts +23 -0
  570. package/dist/react/fields/FieldShell.d.ts.map +1 -0
  571. package/dist/react/fields/FieldShell.js +46 -0
  572. package/dist/react/fields/FieldShell.js.map +1 -0
  573. package/dist/react/fields/FileUploadInput.d.ts +21 -0
  574. package/dist/react/fields/FileUploadInput.d.ts.map +1 -0
  575. package/dist/react/fields/FileUploadInput.js +120 -0
  576. package/dist/react/fields/FileUploadInput.js.map +1 -0
  577. package/dist/react/fields/HiddenInput.d.ts +11 -0
  578. package/dist/react/fields/HiddenInput.d.ts.map +1 -0
  579. package/dist/react/fields/HiddenInput.js +14 -0
  580. package/dist/react/fields/HiddenInput.js.map +1 -0
  581. package/dist/react/fields/KeyValueInput.d.ts +18 -0
  582. package/dist/react/fields/KeyValueInput.d.ts.map +1 -0
  583. package/dist/react/fields/KeyValueInput.js +122 -0
  584. package/dist/react/fields/KeyValueInput.js.map +1 -0
  585. package/dist/react/fields/MarkdownInput.d.ts +29 -0
  586. package/dist/react/fields/MarkdownInput.d.ts.map +1 -0
  587. package/dist/react/fields/MarkdownInput.js +250 -0
  588. package/dist/react/fields/MarkdownInput.js.map +1 -0
  589. package/dist/react/fields/RadioInput.d.ts +18 -0
  590. package/dist/react/fields/RadioInput.d.ts.map +1 -0
  591. package/dist/react/fields/RadioInput.js +34 -0
  592. package/dist/react/fields/RadioInput.js.map +1 -0
  593. package/dist/react/fields/RepeaterInput.d.ts +92 -0
  594. package/dist/react/fields/RepeaterInput.d.ts.map +1 -0
  595. package/dist/react/fields/RepeaterInput.js +705 -0
  596. package/dist/react/fields/RepeaterInput.js.map +1 -0
  597. package/dist/react/fields/SelectFieldInput.d.ts +23 -0
  598. package/dist/react/fields/SelectFieldInput.d.ts.map +1 -0
  599. package/dist/react/fields/SelectFieldInput.js +146 -0
  600. package/dist/react/fields/SelectFieldInput.js.map +1 -0
  601. package/dist/react/fields/SliderInput.d.ts +16 -0
  602. package/dist/react/fields/SliderInput.d.ts.map +1 -0
  603. package/dist/react/fields/SliderInput.js +37 -0
  604. package/dist/react/fields/SliderInput.js.map +1 -0
  605. package/dist/react/fields/TagsInput.d.ts +27 -0
  606. package/dist/react/fields/TagsInput.d.ts.map +1 -0
  607. package/dist/react/fields/TagsInput.js +189 -0
  608. package/dist/react/fields/TagsInput.js.map +1 -0
  609. package/dist/react/fields/TextLikeInput.d.ts +18 -0
  610. package/dist/react/fields/TextLikeInput.d.ts.map +1 -0
  611. package/dist/react/fields/TextLikeInput.js +46 -0
  612. package/dist/react/fields/TextLikeInput.js.map +1 -0
  613. package/dist/react/fields/ToggleButtonsInput.d.ts +20 -0
  614. package/dist/react/fields/ToggleButtonsInput.d.ts.map +1 -0
  615. package/dist/react/fields/ToggleButtonsInput.js +42 -0
  616. package/dist/react/fields/ToggleButtonsInput.js.map +1 -0
  617. package/dist/react/fields/ToggleFieldInput.d.ts +7 -0
  618. package/dist/react/fields/ToggleFieldInput.d.ts.map +1 -0
  619. package/dist/react/fields/ToggleFieldInput.js +30 -0
  620. package/dist/react/fields/ToggleFieldInput.js.map +1 -0
  621. package/dist/react/fields/rowChromeButton.d.ts +84 -0
  622. package/dist/react/fields/rowChromeButton.d.ts.map +1 -0
  623. package/dist/react/fields/rowChromeButton.js +111 -0
  624. package/dist/react/fields/rowChromeButton.js.map +1 -0
  625. package/dist/react/fields/syncRowGates.d.ts +11 -0
  626. package/dist/react/fields/syncRowGates.d.ts.map +1 -0
  627. package/dist/react/fields/syncRowGates.js +55 -0
  628. package/dist/react/fields/syncRowGates.js.map +1 -0
  629. package/dist/react/formStateHelpers.d.ts +44 -0
  630. package/dist/react/formStateHelpers.d.ts.map +1 -0
  631. package/dist/react/formStateHelpers.js +230 -0
  632. package/dist/react/formStateHelpers.js.map +1 -0
  633. package/dist/react/hooks/use-mobile.d.ts +2 -0
  634. package/dist/react/hooks/use-mobile.d.ts.map +1 -0
  635. package/dist/react/hooks/use-mobile.js +16 -0
  636. package/dist/react/hooks/use-mobile.js.map +1 -0
  637. package/dist/react/icon-context.d.ts +35 -0
  638. package/dist/react/icon-context.d.ts.map +1 -0
  639. package/dist/react/icon-context.js +45 -0
  640. package/dist/react/icon-context.js.map +1 -0
  641. package/dist/react/index.d.ts +26 -0
  642. package/dist/react/index.d.ts.map +1 -0
  643. package/dist/react/index.js +28 -0
  644. package/dist/react/index.js.map +1 -0
  645. package/dist/react/layouts/SidebarLayout.d.ts +3 -0
  646. package/dist/react/layouts/SidebarLayout.d.ts.map +1 -0
  647. package/dist/react/layouts/SidebarLayout.js +85 -0
  648. package/dist/react/layouts/SidebarLayout.js.map +1 -0
  649. package/dist/react/layouts/TopbarLayout.d.ts +3 -0
  650. package/dist/react/layouts/TopbarLayout.d.ts.map +1 -0
  651. package/dist/react/layouts/TopbarLayout.js +103 -0
  652. package/dist/react/layouts/TopbarLayout.js.map +1 -0
  653. package/dist/react/navigate.d.ts +22 -0
  654. package/dist/react/navigate.d.ts.map +1 -0
  655. package/dist/react/navigate.js +30 -0
  656. package/dist/react/navigate.js.map +1 -0
  657. package/dist/react/registry.d.ts +35 -0
  658. package/dist/react/registry.d.ts.map +1 -0
  659. package/dist/react/registry.js +22 -0
  660. package/dist/react/registry.js.map +1 -0
  661. package/dist/react/right-panel-registry.d.ts +32 -0
  662. package/dist/react/right-panel-registry.d.ts.map +1 -0
  663. package/dist/react/right-panel-registry.js +20 -0
  664. package/dist/react/right-panel-registry.js.map +1 -0
  665. package/dist/react/theme-preview/apply.d.ts +11 -0
  666. package/dist/react/theme-preview/apply.d.ts.map +1 -0
  667. package/dist/react/theme-preview/apply.js +93 -0
  668. package/dist/react/theme-preview/apply.js.map +1 -0
  669. package/dist/react/theme-preview/build-html.d.ts +3 -0
  670. package/dist/react/theme-preview/build-html.d.ts.map +1 -0
  671. package/dist/react/theme-preview/build-html.js +437 -0
  672. package/dist/react/theme-preview/build-html.js.map +1 -0
  673. package/dist/react/ui/button.d.ts +9 -0
  674. package/dist/react/ui/button.d.ts.map +1 -0
  675. package/dist/react/ui/button.js +35 -0
  676. package/dist/react/ui/button.js.map +1 -0
  677. package/dist/react/ui/calendar.d.ts +5 -0
  678. package/dist/react/ui/calendar.d.ts.map +1 -0
  679. package/dist/react/ui/calendar.js +34 -0
  680. package/dist/react/ui/calendar.js.map +1 -0
  681. package/dist/react/ui/checkbox.d.ts +4 -0
  682. package/dist/react/ui/checkbox.d.ts.map +1 -0
  683. package/dist/react/ui/checkbox.js +9 -0
  684. package/dist/react/ui/checkbox.js.map +1 -0
  685. package/dist/react/ui/dialog.d.ts +12 -0
  686. package/dist/react/ui/dialog.d.ts.map +1 -0
  687. package/dist/react/ui/dialog.js +34 -0
  688. package/dist/react/ui/dialog.js.map +1 -0
  689. package/dist/react/ui/dropdown-menu.d.ts +12 -0
  690. package/dist/react/ui/dropdown-menu.d.ts.map +1 -0
  691. package/dist/react/ui/dropdown-menu.js +23 -0
  692. package/dist/react/ui/dropdown-menu.js.map +1 -0
  693. package/dist/react/ui/input.d.ts +4 -0
  694. package/dist/react/ui/input.d.ts.map +1 -0
  695. package/dist/react/ui/input.js +8 -0
  696. package/dist/react/ui/input.js.map +1 -0
  697. package/dist/react/ui/label.d.ts +4 -0
  698. package/dist/react/ui/label.d.ts.map +1 -0
  699. package/dist/react/ui/label.js +7 -0
  700. package/dist/react/ui/label.js.map +1 -0
  701. package/dist/react/ui/popover.d.ts +6 -0
  702. package/dist/react/ui/popover.d.ts.map +1 -0
  703. package/dist/react/ui/popover.js +14 -0
  704. package/dist/react/ui/popover.js.map +1 -0
  705. package/dist/react/ui/select.d.ts +17 -0
  706. package/dist/react/ui/select.d.ts.map +1 -0
  707. package/dist/react/ui/select.js +39 -0
  708. package/dist/react/ui/select.js.map +1 -0
  709. package/dist/react/ui/separator.d.ts +4 -0
  710. package/dist/react/ui/separator.d.ts.map +1 -0
  711. package/dist/react/ui/separator.js +9 -0
  712. package/dist/react/ui/separator.js.map +1 -0
  713. package/dist/react/ui/sheet.d.ts +15 -0
  714. package/dist/react/ui/sheet.d.ts.map +1 -0
  715. package/dist/react/ui/sheet.js +37 -0
  716. package/dist/react/ui/sheet.js.map +1 -0
  717. package/dist/react/ui/sidebar.d.ts +64 -0
  718. package/dist/react/ui/sidebar.d.ts.map +1 -0
  719. package/dist/react/ui/sidebar.js +257 -0
  720. package/dist/react/ui/sidebar.js.map +1 -0
  721. package/dist/react/ui/skeleton.d.ts +3 -0
  722. package/dist/react/ui/skeleton.d.ts.map +1 -0
  723. package/dist/react/ui/skeleton.js +7 -0
  724. package/dist/react/ui/skeleton.js.map +1 -0
  725. package/dist/react/ui/slider.d.ts +4 -0
  726. package/dist/react/ui/slider.d.ts.map +1 -0
  727. package/dist/react/ui/slider.js +8 -0
  728. package/dist/react/ui/slider.js.map +1 -0
  729. package/dist/react/ui/switch.d.ts +4 -0
  730. package/dist/react/ui/switch.d.ts.map +1 -0
  731. package/dist/react/ui/switch.js +8 -0
  732. package/dist/react/ui/switch.js.map +1 -0
  733. package/dist/react/ui/table.d.ts +11 -0
  734. package/dist/react/ui/table.d.ts.map +1 -0
  735. package/dist/react/ui/table.js +28 -0
  736. package/dist/react/ui/table.js.map +1 -0
  737. package/dist/react/ui/tabs.d.ts +7 -0
  738. package/dist/react/ui/tabs.d.ts.map +1 -0
  739. package/dist/react/ui/tabs.js +17 -0
  740. package/dist/react/ui/tabs.js.map +1 -0
  741. package/dist/react/ui/textarea.d.ts +4 -0
  742. package/dist/react/ui/textarea.d.ts.map +1 -0
  743. package/dist/react/ui/textarea.js +7 -0
  744. package/dist/react/ui/textarea.js.map +1 -0
  745. package/dist/react/ui/tooltip.d.ts +7 -0
  746. package/dist/react/ui/tooltip.d.ts.map +1 -0
  747. package/dist/react/ui/tooltip.js +17 -0
  748. package/dist/react/ui/tooltip.js.map +1 -0
  749. package/dist/react/useResizableWidth.d.ts +47 -0
  750. package/dist/react/useResizableWidth.d.ts.map +1 -0
  751. package/dist/react/useResizableWidth.js +99 -0
  752. package/dist/react/useResizableWidth.js.map +1 -0
  753. package/dist/react/utils.d.ts +3 -0
  754. package/dist/react/utils.d.ts.map +1 -0
  755. package/dist/react/utils.js +6 -0
  756. package/dist/react/utils.js.map +1 -0
  757. package/dist/react/widgetRegistry.d.ts +33 -0
  758. package/dist/react/widgetRegistry.d.ts.map +1 -0
  759. package/dist/react/widgetRegistry.js +15 -0
  760. package/dist/react/widgetRegistry.js.map +1 -0
  761. package/dist/react/widgets/StatsOverviewRenderer.d.ts +6 -0
  762. package/dist/react/widgets/StatsOverviewRenderer.d.ts.map +1 -0
  763. package/dist/react/widgets/StatsOverviewRenderer.js +124 -0
  764. package/dist/react/widgets/StatsOverviewRenderer.js.map +1 -0
  765. package/dist/react/widgets/TableWidgetRenderer.d.ts +6 -0
  766. package/dist/react/widgets/TableWidgetRenderer.d.ts.map +1 -0
  767. package/dist/react/widgets/TableWidgetRenderer.js +123 -0
  768. package/dist/react/widgets/TableWidgetRenderer.js.map +1 -0
  769. package/dist/react/widgets/ViewRenderer.d.ts +16 -0
  770. package/dist/react/widgets/ViewRenderer.d.ts.map +1 -0
  771. package/dist/react/widgets/ViewRenderer.js +26 -0
  772. package/dist/react/widgets/ViewRenderer.js.map +1 -0
  773. package/dist/richtext/index.d.ts +2 -0
  774. package/dist/richtext/index.d.ts.map +1 -0
  775. package/dist/richtext/index.js +2 -0
  776. package/dist/richtext/index.js.map +1 -0
  777. package/dist/richtext/registry.d.ts +55 -0
  778. package/dist/richtext/registry.d.ts.map +1 -0
  779. package/dist/richtext/registry.js +66 -0
  780. package/dist/richtext/registry.js.map +1 -0
  781. package/dist/routes.d.ts +9 -0
  782. package/dist/routes.d.ts.map +1 -0
  783. package/dist/routes.js +3116 -0
  784. package/dist/routes.js.map +1 -0
  785. package/dist/schema/Alert.d.ts +33 -0
  786. package/dist/schema/Alert.d.ts.map +1 -0
  787. package/dist/schema/Alert.js +41 -0
  788. package/dist/schema/Alert.js.map +1 -0
  789. package/dist/schema/Block.d.ts +112 -0
  790. package/dist/schema/Block.d.ts.map +1 -0
  791. package/dist/schema/Block.js +136 -0
  792. package/dist/schema/Block.js.map +1 -0
  793. package/dist/schema/Breadcrumbs.d.ts +31 -0
  794. package/dist/schema/Breadcrumbs.d.ts.map +1 -0
  795. package/dist/schema/Breadcrumbs.js +30 -0
  796. package/dist/schema/Breadcrumbs.js.map +1 -0
  797. package/dist/schema/Card.d.ts +17 -0
  798. package/dist/schema/Card.d.ts.map +1 -0
  799. package/dist/schema/Card.js +31 -0
  800. package/dist/schema/Card.js.map +1 -0
  801. package/dist/schema/Divider.d.ts +12 -0
  802. package/dist/schema/Divider.d.ts.map +1 -0
  803. package/dist/schema/Divider.js +19 -0
  804. package/dist/schema/Divider.js.map +1 -0
  805. package/dist/schema/Element.d.ts +150 -0
  806. package/dist/schema/Element.d.ts.map +1 -0
  807. package/dist/schema/Element.js +124 -0
  808. package/dist/schema/Element.js.map +1 -0
  809. package/dist/schema/EmptyState.d.ts +48 -0
  810. package/dist/schema/EmptyState.d.ts.map +1 -0
  811. package/dist/schema/EmptyState.js +57 -0
  812. package/dist/schema/EmptyState.js.map +1 -0
  813. package/dist/schema/Fieldset.d.ts +25 -0
  814. package/dist/schema/Fieldset.d.ts.map +1 -0
  815. package/dist/schema/Fieldset.js +39 -0
  816. package/dist/schema/Fieldset.js.map +1 -0
  817. package/dist/schema/Grid.d.ts +23 -0
  818. package/dist/schema/Grid.d.ts.map +1 -0
  819. package/dist/schema/Grid.js +36 -0
  820. package/dist/schema/Grid.js.map +1 -0
  821. package/dist/schema/Group.d.ts +19 -0
  822. package/dist/schema/Group.d.ts.map +1 -0
  823. package/dist/schema/Group.js +26 -0
  824. package/dist/schema/Group.js.map +1 -0
  825. package/dist/schema/Heading.d.ts +25 -0
  826. package/dist/schema/Heading.d.ts.map +1 -0
  827. package/dist/schema/Heading.js +34 -0
  828. package/dist/schema/Heading.js.map +1 -0
  829. package/dist/schema/Html.d.ts +48 -0
  830. package/dist/schema/Html.d.ts.map +1 -0
  831. package/dist/schema/Html.js +60 -0
  832. package/dist/schema/Html.js.map +1 -0
  833. package/dist/schema/Icon.d.ts +34 -0
  834. package/dist/schema/Icon.d.ts.map +1 -0
  835. package/dist/schema/Icon.js +40 -0
  836. package/dist/schema/Icon.js.map +1 -0
  837. package/dist/schema/Image.d.ts +38 -0
  838. package/dist/schema/Image.d.ts.map +1 -0
  839. package/dist/schema/Image.js +48 -0
  840. package/dist/schema/Image.js.map +1 -0
  841. package/dist/schema/LinkTag.d.ts +48 -0
  842. package/dist/schema/LinkTag.d.ts.map +1 -0
  843. package/dist/schema/LinkTag.js +16 -0
  844. package/dist/schema/LinkTag.js.map +1 -0
  845. package/dist/schema/Markdown.d.ts +57 -0
  846. package/dist/schema/Markdown.d.ts.map +1 -0
  847. package/dist/schema/Markdown.js +75 -0
  848. package/dist/schema/Markdown.js.map +1 -0
  849. package/dist/schema/MetaTag.d.ts +41 -0
  850. package/dist/schema/MetaTag.d.ts.map +1 -0
  851. package/dist/schema/MetaTag.js +16 -0
  852. package/dist/schema/MetaTag.js.map +1 -0
  853. package/dist/schema/RelationTabs.d.ts +50 -0
  854. package/dist/schema/RelationTabs.d.ts.map +1 -0
  855. package/dist/schema/RelationTabs.js +48 -0
  856. package/dist/schema/RelationTabs.js.map +1 -0
  857. package/dist/schema/ScriptTag.d.ts +63 -0
  858. package/dist/schema/ScriptTag.d.ts.map +1 -0
  859. package/dist/schema/ScriptTag.js +16 -0
  860. package/dist/schema/ScriptTag.js.map +1 -0
  861. package/dist/schema/Section.d.ts +93 -0
  862. package/dist/schema/Section.d.ts.map +1 -0
  863. package/dist/schema/Section.js +127 -0
  864. package/dist/schema/Section.js.map +1 -0
  865. package/dist/schema/ServerDataElement.d.ts +101 -0
  866. package/dist/schema/ServerDataElement.d.ts.map +1 -0
  867. package/dist/schema/ServerDataElement.js +135 -0
  868. package/dist/schema/ServerDataElement.js.map +1 -0
  869. package/dist/schema/Split.d.ts +31 -0
  870. package/dist/schema/Split.d.ts.map +1 -0
  871. package/dist/schema/Split.js +41 -0
  872. package/dist/schema/Split.js.map +1 -0
  873. package/dist/schema/Stat.d.ts +92 -0
  874. package/dist/schema/Stat.d.ts.map +1 -0
  875. package/dist/schema/Stat.js +116 -0
  876. package/dist/schema/Stat.js.map +1 -0
  877. package/dist/schema/StatsOverview.d.ts +76 -0
  878. package/dist/schema/StatsOverview.d.ts.map +1 -0
  879. package/dist/schema/StatsOverview.js +71 -0
  880. package/dist/schema/StatsOverview.js.map +1 -0
  881. package/dist/schema/StyleTag.d.ts +32 -0
  882. package/dist/schema/StyleTag.d.ts.map +1 -0
  883. package/dist/schema/StyleTag.js +38 -0
  884. package/dist/schema/StyleTag.js.map +1 -0
  885. package/dist/schema/TableWidget.d.ts +148 -0
  886. package/dist/schema/TableWidget.d.ts.map +1 -0
  887. package/dist/schema/TableWidget.js +190 -0
  888. package/dist/schema/TableWidget.js.map +1 -0
  889. package/dist/schema/Tabs.d.ts +40 -0
  890. package/dist/schema/Tabs.d.ts.map +1 -0
  891. package/dist/schema/Tabs.js +66 -0
  892. package/dist/schema/Tabs.js.map +1 -0
  893. package/dist/schema/Text.d.ts +33 -0
  894. package/dist/schema/Text.d.ts.map +1 -0
  895. package/dist/schema/Text.js +40 -0
  896. package/dist/schema/Text.js.map +1 -0
  897. package/dist/schema/UnorderedList.d.ts +36 -0
  898. package/dist/schema/UnorderedList.d.ts.map +1 -0
  899. package/dist/schema/UnorderedList.js +42 -0
  900. package/dist/schema/UnorderedList.js.map +1 -0
  901. package/dist/schema/View.d.ts +81 -0
  902. package/dist/schema/View.d.ts.map +1 -0
  903. package/dist/schema/View.js +81 -0
  904. package/dist/schema/View.js.map +1 -0
  905. package/dist/schema/Wizard.d.ts +67 -0
  906. package/dist/schema/Wizard.d.ts.map +1 -0
  907. package/dist/schema/Wizard.js +94 -0
  908. package/dist/schema/Wizard.js.map +1 -0
  909. package/dist/schema/index.d.ts +26 -0
  910. package/dist/schema/index.d.ts.map +1 -0
  911. package/dist/schema/index.js +26 -0
  912. package/dist/schema/index.js.map +1 -0
  913. package/dist/schema/resolveSchema.d.ts +122 -0
  914. package/dist/schema/resolveSchema.d.ts.map +1 -0
  915. package/dist/schema/resolveSchema.js +648 -0
  916. package/dist/schema/resolveSchema.js.map +1 -0
  917. package/dist/schema/sanitize.d.ts +21 -0
  918. package/dist/schema/sanitize.d.ts.map +1 -0
  919. package/dist/schema/sanitize.js +46 -0
  920. package/dist/schema/sanitize.js.map +1 -0
  921. package/dist/search.d.ts +53 -0
  922. package/dist/search.d.ts.map +1 -0
  923. package/dist/search.js +114 -0
  924. package/dist/search.js.map +1 -0
  925. package/dist/sessionFilters.d.ts +8 -0
  926. package/dist/sessionFilters.d.ts.map +1 -0
  927. package/dist/sessionFilters.js +115 -0
  928. package/dist/sessionFilters.js.map +1 -0
  929. package/dist/summarizers/Summarizer.d.ts +65 -0
  930. package/dist/summarizers/Summarizer.d.ts.map +1 -0
  931. package/dist/summarizers/Summarizer.js +98 -0
  932. package/dist/summarizers/Summarizer.js.map +1 -0
  933. package/dist/summarizers/index.d.ts +2 -0
  934. package/dist/summarizers/index.d.ts.map +1 -0
  935. package/dist/summarizers/index.js +2 -0
  936. package/dist/summarizers/index.js.map +1 -0
  937. package/dist/theme/base-colors.d.ts +3 -0
  938. package/dist/theme/base-colors.d.ts.map +1 -0
  939. package/dist/theme/base-colors.js +64 -0
  940. package/dist/theme/base-colors.js.map +1 -0
  941. package/dist/theme/chart-colors.d.ts +3 -0
  942. package/dist/theme/chart-colors.d.ts.map +1 -0
  943. package/dist/theme/chart-colors.js +46 -0
  944. package/dist/theme/chart-colors.js.map +1 -0
  945. package/dist/theme/colors.d.ts +56 -0
  946. package/dist/theme/colors.d.ts.map +1 -0
  947. package/dist/theme/colors.js +410 -0
  948. package/dist/theme/colors.js.map +1 -0
  949. package/dist/theme/generate-css.d.ts +9 -0
  950. package/dist/theme/generate-css.d.ts.map +1 -0
  951. package/dist/theme/generate-css.js +36 -0
  952. package/dist/theme/generate-css.js.map +1 -0
  953. package/dist/theme/generate-scale.d.ts +3 -0
  954. package/dist/theme/generate-scale.d.ts.map +1 -0
  955. package/dist/theme/generate-scale.js +89 -0
  956. package/dist/theme/generate-scale.js.map +1 -0
  957. package/dist/theme/icon-map.d.ts +9 -0
  958. package/dist/theme/icon-map.d.ts.map +1 -0
  959. package/dist/theme/icon-map.js +40 -0
  960. package/dist/theme/icon-map.js.map +1 -0
  961. package/dist/theme/index.d.ts +15 -0
  962. package/dist/theme/index.d.ts.map +1 -0
  963. package/dist/theme/index.js +13 -0
  964. package/dist/theme/index.js.map +1 -0
  965. package/dist/theme/migrate.d.ts +14 -0
  966. package/dist/theme/migrate.d.ts.map +1 -0
  967. package/dist/theme/migrate.js +79 -0
  968. package/dist/theme/migrate.js.map +1 -0
  969. package/dist/theme/presets.d.ts +30 -0
  970. package/dist/theme/presets.d.ts.map +1 -0
  971. package/dist/theme/presets.js +128 -0
  972. package/dist/theme/presets.js.map +1 -0
  973. package/dist/theme/radius.d.ts +11 -0
  974. package/dist/theme/radius.d.ts.map +1 -0
  975. package/dist/theme/radius.js +17 -0
  976. package/dist/theme/radius.js.map +1 -0
  977. package/dist/theme/resolve.d.ts +13 -0
  978. package/dist/theme/resolve.d.ts.map +1 -0
  979. package/dist/theme/resolve.js +91 -0
  980. package/dist/theme/resolve.js.map +1 -0
  981. package/dist/theme/spacing.d.ts +14 -0
  982. package/dist/theme/spacing.d.ts.map +1 -0
  983. package/dist/theme/spacing.js +17 -0
  984. package/dist/theme/spacing.js.map +1 -0
  985. package/dist/theme/theme-colors.d.ts +9 -0
  986. package/dist/theme/theme-colors.d.ts.map +1 -0
  987. package/dist/theme/theme-colors.js +84 -0
  988. package/dist/theme/theme-colors.js.map +1 -0
  989. package/dist/theme/types.d.ts +94 -0
  990. package/dist/theme/types.d.ts.map +1 -0
  991. package/dist/theme/types.js +2 -0
  992. package/dist/theme/types.js.map +1 -0
  993. package/dist/uploads/UploadAdapter.d.ts +34 -0
  994. package/dist/uploads/UploadAdapter.d.ts.map +1 -0
  995. package/dist/uploads/UploadAdapter.js +2 -0
  996. package/dist/uploads/UploadAdapter.js.map +1 -0
  997. package/dist/uploads/index.d.ts +3 -0
  998. package/dist/uploads/index.d.ts.map +1 -0
  999. package/dist/uploads/index.js +2 -0
  1000. package/dist/uploads/index.js.map +1 -0
  1001. package/dist/uploads/localUpload.d.ts +25 -0
  1002. package/dist/uploads/localUpload.d.ts.map +1 -0
  1003. package/dist/uploads/localUpload.js +65 -0
  1004. package/dist/uploads/localUpload.js.map +1 -0
  1005. package/dist/validation/Validator.d.ts +40 -0
  1006. package/dist/validation/Validator.d.ts.map +1 -0
  1007. package/dist/validation/Validator.js +25 -0
  1008. package/dist/validation/Validator.js.map +1 -0
  1009. package/dist/validation/index.d.ts +5 -0
  1010. package/dist/validation/index.d.ts.map +1 -0
  1011. package/dist/validation/index.js +5 -0
  1012. package/dist/validation/index.js.map +1 -0
  1013. package/dist/validation/rules.d.ts +9 -0
  1014. package/dist/validation/rules.d.ts.map +1 -0
  1015. package/dist/validation/rules.js +61 -0
  1016. package/dist/validation/rules.js.map +1 -0
  1017. package/dist/validation/runValidators.d.ts +30 -0
  1018. package/dist/validation/runValidators.d.ts.map +1 -0
  1019. package/dist/validation/runValidators.js +438 -0
  1020. package/dist/validation/runValidators.js.map +1 -0
  1021. package/dist/validation/uniqueValidator.d.ts +61 -0
  1022. package/dist/validation/uniqueValidator.d.ts.map +1 -0
  1023. package/dist/validation/uniqueValidator.js +80 -0
  1024. package/dist/validation/uniqueValidator.js.map +1 -0
  1025. package/dist/vite.d.ts +19 -0
  1026. package/dist/vite.d.ts.map +1 -0
  1027. package/dist/vite.js +696 -0
  1028. package/dist/vite.js.map +1 -0
  1029. package/dist/widgets/index.d.ts +2 -0
  1030. package/dist/widgets/index.d.ts.map +1 -0
  1031. package/dist/widgets/index.js +7 -0
  1032. package/dist/widgets/index.js.map +1 -0
  1033. package/dist/widgets/registry.d.ts +32 -0
  1034. package/dist/widgets/registry.d.ts.map +1 -0
  1035. package/dist/widgets/registry.js +17 -0
  1036. package/dist/widgets/registry.js.map +1 -0
  1037. package/package.json +101 -0
  1038. package/src/Cluster.test.ts +283 -0
  1039. package/src/Cluster.ts +83 -0
  1040. package/src/Column.test.ts +140 -0
  1041. package/src/Column.ts +612 -0
  1042. package/src/Global.test.ts +367 -0
  1043. package/src/Global.ts +169 -0
  1044. package/src/Page.test.ts +50 -0
  1045. package/src/Page.ts +139 -0
  1046. package/src/Pilotiq.test.ts +47 -0
  1047. package/src/Pilotiq.ts +705 -0
  1048. package/src/PilotiqRegistry.ts +36 -0
  1049. package/src/PilotiqServiceProvider.ts +69 -0
  1050. package/src/RelationManager.test.ts +400 -0
  1051. package/src/RelationManager.ts +527 -0
  1052. package/src/RenderHook.test.ts +252 -0
  1053. package/src/RenderHook.ts +226 -0
  1054. package/src/Resource.test.ts +240 -0
  1055. package/src/Resource.ts +439 -0
  1056. package/src/RightPanel.test.ts +202 -0
  1057. package/src/RightPanel.ts +132 -0
  1058. package/src/Tab.test.ts +91 -0
  1059. package/src/Tab.ts +156 -0
  1060. package/src/UserMenuItem.ts +145 -0
  1061. package/src/actions/Action.test.ts +2479 -0
  1062. package/src/actions/Action.ts +2124 -0
  1063. package/src/actions/ActionGroup.test.ts +112 -0
  1064. package/src/actions/ActionGroup.ts +173 -0
  1065. package/src/actions/attachFactory.ts +172 -0
  1066. package/src/actions/exportFactory.ts +215 -0
  1067. package/src/actions/importFactory.ts +222 -0
  1068. package/src/actions/index.ts +17 -0
  1069. package/src/applyPageHooks.test.ts +298 -0
  1070. package/src/applyPageHooks.ts +242 -0
  1071. package/src/authorization.test.ts +483 -0
  1072. package/src/breadcrumbs.test.ts +238 -0
  1073. package/src/cells/coerce.test.ts +85 -0
  1074. package/src/cells/coerce.ts +84 -0
  1075. package/src/clusterPaths.ts +35 -0
  1076. package/src/columns/BadgeColumn.test.ts +54 -0
  1077. package/src/columns/BadgeColumn.ts +32 -0
  1078. package/src/columns/BooleanColumn.test.ts +41 -0
  1079. package/src/columns/BooleanColumn.ts +18 -0
  1080. package/src/columns/ColorColumn.test.ts +37 -0
  1081. package/src/columns/ColorColumn.ts +38 -0
  1082. package/src/columns/IconColumn.test.ts +54 -0
  1083. package/src/columns/IconColumn.ts +37 -0
  1084. package/src/columns/ImageColumn.test.ts +41 -0
  1085. package/src/columns/ImageColumn.ts +28 -0
  1086. package/src/columns/SelectColumn.ts +60 -0
  1087. package/src/columns/TextColumn.test.ts +190 -0
  1088. package/src/columns/TextColumn.ts +20 -0
  1089. package/src/columns/TextInputColumn.ts +68 -0
  1090. package/src/columns/ToggleColumn.ts +46 -0
  1091. package/src/columns/editableColumns.test.ts +193 -0
  1092. package/src/columns/index.ts +9 -0
  1093. package/src/defaultGlobalPages.ts +95 -0
  1094. package/src/defaultPages.test.ts +634 -0
  1095. package/src/defaultPages.ts +614 -0
  1096. package/src/defaultViewPage.test.ts +147 -0
  1097. package/src/elements/Form.test.ts +223 -0
  1098. package/src/elements/Form.ts +397 -0
  1099. package/src/elements/ListTabs.ts +28 -0
  1100. package/src/elements/Table.test.ts +422 -0
  1101. package/src/elements/Table.ts +816 -0
  1102. package/src/elements/TableGroup.test.ts +149 -0
  1103. package/src/elements/TableGroup.ts +199 -0
  1104. package/src/elements/dispatchAction.test.ts +463 -0
  1105. package/src/elements/dispatchAction.ts +355 -0
  1106. package/src/elements/dispatchForm.test.ts +455 -0
  1107. package/src/elements/dispatchForm.ts +1855 -0
  1108. package/src/elements/dispatchTable.test.ts +1247 -0
  1109. package/src/elements/dispatchTable.ts +666 -0
  1110. package/src/elements/index.ts +21 -0
  1111. package/src/entries/BadgeEntry.ts +39 -0
  1112. package/src/entries/CodeEntry.test.ts +40 -0
  1113. package/src/entries/CodeEntry.ts +52 -0
  1114. package/src/entries/ColorEntry.ts +63 -0
  1115. package/src/entries/ComponentEntry.test.ts +173 -0
  1116. package/src/entries/ComponentEntry.ts +95 -0
  1117. package/src/entries/Entry.ts +304 -0
  1118. package/src/entries/IconEntry.ts +49 -0
  1119. package/src/entries/ImageEntry.ts +61 -0
  1120. package/src/entries/KeyValueEntry.ts +47 -0
  1121. package/src/entries/RepeatableEntry.test.ts +239 -0
  1122. package/src/entries/RepeatableEntry.ts +173 -0
  1123. package/src/entries/TextEntry.test.ts +394 -0
  1124. package/src/entries/TextEntry.ts +60 -0
  1125. package/src/entries/index.ts +12 -0
  1126. package/src/entries/leaves.test.ts +306 -0
  1127. package/src/entries/registry.ts +54 -0
  1128. package/src/fields/BuilderField.test.ts +1188 -0
  1129. package/src/fields/BuilderField.ts +568 -0
  1130. package/src/fields/BuilderRelationship.test.ts +811 -0
  1131. package/src/fields/CheckboxField.test.ts +44 -0
  1132. package/src/fields/CheckboxField.ts +27 -0
  1133. package/src/fields/CheckboxListField.test.ts +99 -0
  1134. package/src/fields/CheckboxListField.ts +66 -0
  1135. package/src/fields/ColorPickerField.test.ts +33 -0
  1136. package/src/fields/ColorPickerField.ts +25 -0
  1137. package/src/fields/DateField.ts +54 -0
  1138. package/src/fields/DateTimeField.test.ts +55 -0
  1139. package/src/fields/EmailField.ts +16 -0
  1140. package/src/fields/Field.test.ts +639 -0
  1141. package/src/fields/Field.ts +773 -0
  1142. package/src/fields/FileUploadField.test.ts +97 -0
  1143. package/src/fields/FileUploadField.ts +71 -0
  1144. package/src/fields/HiddenField.test.ts +27 -0
  1145. package/src/fields/HiddenField.ts +28 -0
  1146. package/src/fields/KeyValueField.test.ts +105 -0
  1147. package/src/fields/KeyValueField.ts +55 -0
  1148. package/src/fields/MarkdownField.test.ts +167 -0
  1149. package/src/fields/MarkdownField.ts +151 -0
  1150. package/src/fields/NumberField.ts +33 -0
  1151. package/src/fields/RadioField.test.ts +94 -0
  1152. package/src/fields/RadioField.ts +67 -0
  1153. package/src/fields/RepeaterField.test.ts +1806 -0
  1154. package/src/fields/RepeaterField.ts +791 -0
  1155. package/src/fields/RepeaterRelationship.test.ts +1630 -0
  1156. package/src/fields/RepeaterSimple.test.ts +248 -0
  1157. package/src/fields/RowButton.test.ts +149 -0
  1158. package/src/fields/RowButton.ts +125 -0
  1159. package/src/fields/SelectField.test.ts +192 -0
  1160. package/src/fields/SelectField.ts +235 -0
  1161. package/src/fields/SliderField.test.ts +50 -0
  1162. package/src/fields/SliderField.ts +53 -0
  1163. package/src/fields/SlugField.ts +24 -0
  1164. package/src/fields/TagsInputField.test.ts +154 -0
  1165. package/src/fields/TagsInputField.ts +133 -0
  1166. package/src/fields/TextField.ts +24 -0
  1167. package/src/fields/TextareaField.test.ts +58 -0
  1168. package/src/fields/TextareaField.ts +59 -0
  1169. package/src/fields/ToggleButtonsField.test.ts +106 -0
  1170. package/src/fields/ToggleButtonsField.ts +59 -0
  1171. package/src/fields/ToggleField.ts +16 -0
  1172. package/src/fields/disableOptionsWhenSelectedInSiblingRepeaterItems.test.ts +319 -0
  1173. package/src/fields/optionsResolver.ts +95 -0
  1174. package/src/fields/resolveField.ts +28 -0
  1175. package/src/filters/BooleanFilter.ts +35 -0
  1176. package/src/filters/DateRangeFilter.test.ts +194 -0
  1177. package/src/filters/DateRangeFilter.ts +148 -0
  1178. package/src/filters/Filter.test.ts +268 -0
  1179. package/src/filters/Filter.ts +184 -0
  1180. package/src/filters/FormFilter.test.ts +238 -0
  1181. package/src/filters/FormFilter.ts +215 -0
  1182. package/src/filters/MultiSelectFilter.test.ts +119 -0
  1183. package/src/filters/MultiSelectFilter.ts +78 -0
  1184. package/src/filters/QueryBuilderFilter.test.ts +644 -0
  1185. package/src/filters/QueryBuilderFilter.ts +398 -0
  1186. package/src/filters/SelectFilter.ts +46 -0
  1187. package/src/filters/TernaryFilter.test.ts +160 -0
  1188. package/src/filters/TernaryFilter.ts +72 -0
  1189. package/src/filters/TrashedFilter.test.ts +149 -0
  1190. package/src/filters/TrashedFilter.ts +55 -0
  1191. package/src/filters/queryBuilder/BooleanConstraint.ts +31 -0
  1192. package/src/filters/queryBuilder/Constraint.ts +115 -0
  1193. package/src/filters/queryBuilder/DateConstraint.ts +69 -0
  1194. package/src/filters/queryBuilder/NumberConstraint.ts +66 -0
  1195. package/src/filters/queryBuilder/SelectConstraint.ts +72 -0
  1196. package/src/filters/queryBuilder/TextConstraint.ts +65 -0
  1197. package/src/filters/queryBuilder/index.ts +12 -0
  1198. package/src/icons/index.ts +2 -0
  1199. package/src/icons/lucide.ts +204 -0
  1200. package/src/icons/registry.test.ts +56 -0
  1201. package/src/icons/registry.ts +41 -0
  1202. package/src/icons/types.ts +47 -0
  1203. package/src/index.ts +521 -0
  1204. package/src/io/csv.test.ts +142 -0
  1205. package/src/io/csv.ts +170 -0
  1206. package/src/nestedRelationManagerData.test.ts +526 -0
  1207. package/src/notifications/Notification.test.ts +210 -0
  1208. package/src/notifications/Notification.ts +354 -0
  1209. package/src/notifications/broadcast.test.ts +110 -0
  1210. package/src/notifications/broadcast.ts +95 -0
  1211. package/src/notifications/database.test.ts +383 -0
  1212. package/src/notifications/database.ts +398 -0
  1213. package/src/notifications/databaseNotifications.test.ts +187 -0
  1214. package/src/notifications/dispatchNotificationAction.test.ts +341 -0
  1215. package/src/notifications/dispatchNotificationAction.ts +142 -0
  1216. package/src/notifications/flash.test.ts +89 -0
  1217. package/src/notifications/flash.ts +71 -0
  1218. package/src/notifications/index.ts +45 -0
  1219. package/src/notifications/registerBroadcastAuth.test.ts +134 -0
  1220. package/src/notifications/registerBroadcastAuth.ts +100 -0
  1221. package/src/notifications/resolveSavedNotification.test.ts +82 -0
  1222. package/src/notifications/resolveSavedNotification.ts +59 -0
  1223. package/src/notifications/types.ts +93 -0
  1224. package/src/orm/m2mAccessor.ts +66 -0
  1225. package/src/orm/modelDefaults.test.ts +633 -0
  1226. package/src/orm/modelDefaults.ts +632 -0
  1227. package/src/pageData.test.ts +1121 -0
  1228. package/src/pageData.ts +4662 -0
  1229. package/src/plugins/index.ts +1 -0
  1230. package/src/plugins/themeEditor.ts +24 -0
  1231. package/src/react/AppShell.tsx +148 -0
  1232. package/src/react/CommandPalette.tsx +375 -0
  1233. package/src/react/FormStateContext.tsx +398 -0
  1234. package/src/react/HeadHooks.tsx +126 -0
  1235. package/src/react/NotificationActionStrip.tsx +263 -0
  1236. package/src/react/NotificationBell.tsx +426 -0
  1237. package/src/react/RenderHookSlot.tsx +32 -0
  1238. package/src/react/RightSidebar.tsx +257 -0
  1239. package/src/react/RightSidebarContext.tsx +211 -0
  1240. package/src/react/RightSidebarTrigger.tsx +53 -0
  1241. package/src/react/SchemaRenderer.tsx +6128 -0
  1242. package/src/react/SearchTrigger.tsx +46 -0
  1243. package/src/react/ThemeProvider.tsx +93 -0
  1244. package/src/react/ThemeSettingsPage.tsx +579 -0
  1245. package/src/react/ThemeToggle.tsx +20 -0
  1246. package/src/react/Toaster.tsx +158 -0
  1247. package/src/react/UserMenu.tsx +196 -0
  1248. package/src/react/WidgetDataContext.tsx +157 -0
  1249. package/src/react/cells/EditableCell.tsx +376 -0
  1250. package/src/react/fieldJsHandler.test.ts +166 -0
  1251. package/src/react/fieldJsHandler.ts +79 -0
  1252. package/src/react/fields/BuilderInput.tsx +995 -0
  1253. package/src/react/fields/CheckboxInput.tsx +39 -0
  1254. package/src/react/fields/CheckboxListInput.tsx +81 -0
  1255. package/src/react/fields/ColorInput.tsx +51 -0
  1256. package/src/react/fields/DateFieldInput.tsx +70 -0
  1257. package/src/react/fields/DateTimeInput.tsx +42 -0
  1258. package/src/react/fields/FieldShell.tsx +107 -0
  1259. package/src/react/fields/FileUploadInput.tsx +189 -0
  1260. package/src/react/fields/HiddenInput.tsx +17 -0
  1261. package/src/react/fields/KeyValueInput.tsx +200 -0
  1262. package/src/react/fields/MarkdownInput.tsx +333 -0
  1263. package/src/react/fields/RadioInput.tsx +60 -0
  1264. package/src/react/fields/RepeaterInput.test.ts +116 -0
  1265. package/src/react/fields/RepeaterInput.tsx +1313 -0
  1266. package/src/react/fields/SelectFieldInput.tsx +257 -0
  1267. package/src/react/fields/SliderInput.tsx +63 -0
  1268. package/src/react/fields/TagsInput.tsx +265 -0
  1269. package/src/react/fields/TextLikeInput.tsx +54 -0
  1270. package/src/react/fields/ToggleButtonsInput.tsx +60 -0
  1271. package/src/react/fields/ToggleFieldInput.tsx +35 -0
  1272. package/src/react/fields/rowChromeButton.tsx +225 -0
  1273. package/src/react/fields/syncRowGates.test.ts +202 -0
  1274. package/src/react/fields/syncRowGates.ts +66 -0
  1275. package/src/react/formStateHelpers.test.ts +295 -0
  1276. package/src/react/formStateHelpers.ts +218 -0
  1277. package/src/react/hooks/use-mobile.ts +19 -0
  1278. package/src/react/icon-context.tsx +60 -0
  1279. package/src/react/index.ts +85 -0
  1280. package/src/react/layouts/SidebarLayout.tsx +239 -0
  1281. package/src/react/layouts/TopbarLayout.tsx +245 -0
  1282. package/src/react/navigate.tsx +37 -0
  1283. package/src/react/registry.ts +48 -0
  1284. package/src/react/right-panel-registry.tsx +47 -0
  1285. package/src/react/theme-preview/apply.ts +99 -0
  1286. package/src/react/theme-preview/build-html.ts +436 -0
  1287. package/src/react/ui/button.tsx +51 -0
  1288. package/src/react/ui/calendar.tsx +67 -0
  1289. package/src/react/ui/checkbox.tsx +29 -0
  1290. package/src/react/ui/dialog.tsx +108 -0
  1291. package/src/react/ui/dropdown-menu.tsx +97 -0
  1292. package/src/react/ui/input.tsx +20 -0
  1293. package/src/react/ui/label.tsx +21 -0
  1294. package/src/react/ui/popover.tsx +50 -0
  1295. package/src/react/ui/select.tsx +169 -0
  1296. package/src/react/ui/separator.tsx +25 -0
  1297. package/src/react/ui/sheet.tsx +136 -0
  1298. package/src/react/ui/sidebar.tsx +723 -0
  1299. package/src/react/ui/skeleton.tsx +13 -0
  1300. package/src/react/ui/slider.tsx +34 -0
  1301. package/src/react/ui/switch.tsx +28 -0
  1302. package/src/react/ui/table.tsx +105 -0
  1303. package/src/react/ui/tabs.tsx +63 -0
  1304. package/src/react/ui/textarea.tsx +18 -0
  1305. package/src/react/ui/tooltip.tsx +64 -0
  1306. package/src/react/useResizableWidth.ts +139 -0
  1307. package/src/react/utils.ts +6 -0
  1308. package/src/react/widgetRegistry.test.ts +43 -0
  1309. package/src/react/widgetRegistry.ts +50 -0
  1310. package/src/react/widgets/StatsOverviewRenderer.tsx +232 -0
  1311. package/src/react/widgets/TableWidgetRenderer.tsx +231 -0
  1312. package/src/react/widgets/ViewRenderer.tsx +71 -0
  1313. package/src/relationManagerData.test.ts +1146 -0
  1314. package/src/richtext/index.ts +8 -0
  1315. package/src/richtext/registry.ts +89 -0
  1316. package/src/routes-nested-relations.test.ts +676 -0
  1317. package/src/routes-relations.test.ts +972 -0
  1318. package/src/routes.test.ts +1886 -0
  1319. package/src/routes.ts +3262 -0
  1320. package/src/schema/Alert.test.ts +63 -0
  1321. package/src/schema/Alert.ts +49 -0
  1322. package/src/schema/Block.ts +169 -0
  1323. package/src/schema/Breadcrumbs.ts +40 -0
  1324. package/src/schema/Card.ts +35 -0
  1325. package/src/schema/Divider.ts +20 -0
  1326. package/src/schema/Element.ts +219 -0
  1327. package/src/schema/EmptyState.test.ts +37 -0
  1328. package/src/schema/EmptyState.ts +63 -0
  1329. package/src/schema/Fieldset.ts +43 -0
  1330. package/src/schema/Grid.ts +43 -0
  1331. package/src/schema/Group.ts +30 -0
  1332. package/src/schema/Heading.ts +39 -0
  1333. package/src/schema/Html.ts +67 -0
  1334. package/src/schema/Icon.ts +54 -0
  1335. package/src/schema/Image.ts +57 -0
  1336. package/src/schema/LinkTag.ts +41 -0
  1337. package/src/schema/Markdown.ts +85 -0
  1338. package/src/schema/MetaTag.ts +41 -0
  1339. package/src/schema/RelationTabs.ts +71 -0
  1340. package/src/schema/ScriptTag.ts +55 -0
  1341. package/src/schema/Section.ts +143 -0
  1342. package/src/schema/ServerDataElement.test.ts +140 -0
  1343. package/src/schema/ServerDataElement.ts +156 -0
  1344. package/src/schema/Split.ts +50 -0
  1345. package/src/schema/Stat.test.ts +118 -0
  1346. package/src/schema/Stat.ts +154 -0
  1347. package/src/schema/StatsOverview.test.ts +141 -0
  1348. package/src/schema/StatsOverview.ts +119 -0
  1349. package/src/schema/StyleTag.ts +35 -0
  1350. package/src/schema/TableWidget.test.ts +297 -0
  1351. package/src/schema/TableWidget.ts +289 -0
  1352. package/src/schema/Tabs.ts +79 -0
  1353. package/src/schema/Text.ts +58 -0
  1354. package/src/schema/UnorderedList.ts +49 -0
  1355. package/src/schema/View.test.ts +111 -0
  1356. package/src/schema/View.ts +127 -0
  1357. package/src/schema/Wizard.ts +108 -0
  1358. package/src/schema/containers.test.ts +446 -0
  1359. package/src/schema/headTags.test.ts +134 -0
  1360. package/src/schema/index.ts +39 -0
  1361. package/src/schema/primes.test.ts +269 -0
  1362. package/src/schema/resolveSchema.test.ts +329 -0
  1363. package/src/schema/resolveSchema.ts +807 -0
  1364. package/src/schema/sanitize.ts +49 -0
  1365. package/src/search.test.ts +446 -0
  1366. package/src/search.ts +178 -0
  1367. package/src/sessionFilters.test.ts +352 -0
  1368. package/src/sessionFilters.ts +133 -0
  1369. package/src/summarizers/Summarizer.test.ts +84 -0
  1370. package/src/summarizers/Summarizer.ts +123 -0
  1371. package/src/summarizers/index.ts +11 -0
  1372. package/src/theme/base-colors.ts +68 -0
  1373. package/src/theme/chart-colors.ts +50 -0
  1374. package/src/theme/colors.ts +447 -0
  1375. package/src/theme/generate-css.test.ts +139 -0
  1376. package/src/theme/generate-css.ts +44 -0
  1377. package/src/theme/generate-scale.test.ts +106 -0
  1378. package/src/theme/generate-scale.ts +97 -0
  1379. package/src/theme/icon-map.ts +42 -0
  1380. package/src/theme/index.ts +28 -0
  1381. package/src/theme/migrate.ts +81 -0
  1382. package/src/theme/presets.ts +135 -0
  1383. package/src/theme/radius.ts +18 -0
  1384. package/src/theme/resolve.test.ts +238 -0
  1385. package/src/theme/resolve.ts +96 -0
  1386. package/src/theme/spacing.ts +18 -0
  1387. package/src/theme/theme-colors.ts +88 -0
  1388. package/src/theme/types.ts +125 -0
  1389. package/src/uploads/UploadAdapter.ts +35 -0
  1390. package/src/uploads/index.ts +2 -0
  1391. package/src/uploads/localUpload.test.ts +70 -0
  1392. package/src/uploads/localUpload.ts +84 -0
  1393. package/src/validation/Validator.ts +49 -0
  1394. package/src/validation/index.ts +28 -0
  1395. package/src/validation/rules.ts +78 -0
  1396. package/src/validation/runValidators.ts +435 -0
  1397. package/src/validation/uniqueValidator.test.ts +196 -0
  1398. package/src/validation/uniqueValidator.ts +133 -0
  1399. package/src/validation/validators.test.ts +268 -0
  1400. package/src/vite.ts +758 -0
  1401. package/src/widgets/index.ts +10 -0
  1402. package/src/widgets/registry.ts +45 -0
  1403. package/src/widgets.test.ts +592 -0
  1404. package/tsconfig.build.json +11 -0
  1405. package/tsconfig.json +4 -0
  1406. package/tsconfig.test.json +10 -0
  1407. package/views/react/Dashboard.tsx +27 -0
  1408. package/views/react/Resources/Form.tsx +102 -0
  1409. package/views/react/Resources/Index.tsx +49 -0
package/dist/routes.js ADDED
@@ -0,0 +1,3116 @@
1
+ import { view } from '@rudderjs/view';
2
+ import { Form } from './elements/Form.js';
3
+ import { resolveSchema } from './schema/resolveSchema.js';
4
+ import { dispatchFormSubmit, findForms, selectForm } from './elements/dispatchForm.js';
5
+ import { dispatchAction, findActions, findRowExtraActions, parseActionBody } from './elements/dispatchAction.js';
6
+ import { flashNotifications } from './notifications/flash.js';
7
+ import { listFiltersKey, readPersistedListQuery, writePersistedListQuery, readPersistedLastTab, writePersistedLastTab, encodePersistedQuery, } from './sessionFilters.js';
8
+ import { panelInfo, callPageSchema, tagFormActions, tagActionDispatch, dashboardData, resourceIndexData, resourceTableData, resourceCreateData, resourceEditData, resourceViewData, globalEditData, globalViewData, customPageData, formStateData, formWizardData, formCreateOptionData, mentionResolveData, searchData, relationManagerData, findRelatedResource, safeManagerPolicy, resolveRelationChain, widgetData, } from './pageData.js';
9
+ import { listForUser as listDatabaseNotifications, markAsRead as markDatabaseNotificationAsRead, markAsUnread as markDatabaseNotificationAsUnread, markAllAsRead as markAllDatabaseNotificationsAsRead, } from './notifications/database.js';
10
+ import { dispatchNotificationAction } from './notifications/dispatchNotificationAction.js';
11
+ import { registerBroadcastAuth } from './notifications/registerBroadcastAuth.js';
12
+ import { RESERVED_RELATIONSHIP_TOKENS, normalizeRelationMode, } from './RelationManager.js';
13
+ import { modelSave, modelLoadRecord, findRecord, getPrimaryKey, getRelationType, getMorphRelationDescriptor, computeMorphPayload, } from './orm/modelDefaults.js';
14
+ import { Table } from './elements/Table.js';
15
+ import { Column } from './Column.js';
16
+ import { coerceCellValue, CellCoerceError } from './cells/coerce.js';
17
+ import { presets } from './theme/presets.js';
18
+ import { baseColors } from './theme/base-colors.js';
19
+ import { HUE_NAMES } from './theme/colors.js';
20
+ import { migrateThemeOverrides } from './theme/migrate.js';
21
+ import { radiusMap } from './theme/radius.js';
22
+ import { resourceBasePath, globalBasePath, pageBasePath } from './clusterPaths.js';
23
+ /** True when the client wants a JSON response (modal-form action submitting
24
+ * via fetch), false for a browser-style form post that wants a 303 redirect.
25
+ * Both action endpoints honor this so confirm/handler buttons (form-post)
26
+ * keep working unchanged while modal dialogs use fetch. */
27
+ function wantsJson(req) {
28
+ const headers = req.headers ?? {};
29
+ const accept = headers['accept'] ?? headers['Accept'] ?? '';
30
+ return accept.includes('application/json');
31
+ }
32
+ /**
33
+ * Read the request body as a `Record<string, unknown>`. The hono adapter
34
+ * auto-parses JSON, but `application/x-www-form-urlencoded` and
35
+ * `multipart/form-data` need a manual fall-through to Hono's own parser.
36
+ */
37
+ async function readFormBody(req) {
38
+ if (req.body && typeof req.body === 'object' && !Array.isArray(req.body)) {
39
+ return { ...req.body };
40
+ }
41
+ const raw = req.raw;
42
+ if (raw?.req?.parseBody) {
43
+ try {
44
+ const parsed = await raw.req.parseBody();
45
+ return parsed && typeof parsed === 'object' ? { ...parsed } : {};
46
+ }
47
+ catch {
48
+ return {};
49
+ }
50
+ }
51
+ return {};
52
+ }
53
+ /**
54
+ * Normalize a user-supplied redirect URL. Returns absolute URLs and
55
+ * scheme-prefixed URLs unchanged. Bare relative paths (no leading `/`)
56
+ * are joined under the panel's `basePath` — without this, the browser
57
+ * resolves the redirect against the current request URL and produces
58
+ * paths like `/admin/articles/{id}/articles/{id}/edit`.
59
+ *
60
+ * `getRedirectUrl` page hooks and `Form.redirectAfterSave` callbacks
61
+ * are user-authored; this protects the framework against the common
62
+ * authoring slip while keeping absolute URLs (the documented form)
63
+ * working as-is.
64
+ */
65
+ function normalizeRedirect(url, basePath) {
66
+ if (!url)
67
+ return undefined;
68
+ if (url.startsWith('/'))
69
+ return url;
70
+ if (/^[a-z][a-z0-9+.-]*:/i.test(url))
71
+ return url; // http(s):, mailto:, etc.
72
+ const trimmedBase = basePath.replace(/\/$/, '');
73
+ return `${trimmedBase}/${url}`;
74
+ }
75
+ /** Strip framework meta keys (`_formId`, `_method`, `_continueCreate`)
76
+ * from a parsed body. `continueCreate` mirrors the secondary
77
+ * "Create & create another" submit on `CreatePage`: when `'1'`, the
78
+ * create POST handler routes the redirect back to the create URL
79
+ * instead of the new record's edit page. */
80
+ function splitMeta(body) {
81
+ const { _formId, _method: _omitMethod, _continueCreate, ...rest } = body;
82
+ return {
83
+ values: rest,
84
+ formId: typeof _formId === 'string' ? _formId : undefined,
85
+ continueCreate: _continueCreate === '1' || _continueCreate === 1 || _continueCreate === true,
86
+ };
87
+ }
88
+ /** Strip control characters (`"\\\r\n`) from a download filename so
89
+ * the `Content-Disposition: attachment; filename="…"` header stays
90
+ * unbreakable. Defends against a handler that returns a hostile
91
+ * filename string. Empty fallback `'export'`. */
92
+ function sanitizeFilename(name) {
93
+ const cleaned = (name ?? '').replace(/[\r\n"\\]/g, '').trim();
94
+ return cleaned.length > 0 ? cleaned : 'export';
95
+ }
96
+ /** Write an `Action`-handler download envelope as the response. Sets
97
+ * `Content-Type` + `Content-Disposition: attachment` and ends with
98
+ * the body. Mutually exclusive with redirect — call sites consult
99
+ * `result.download` first. */
100
+ function sendDownload(res, env) {
101
+ res.header('Content-Type', env.contentType);
102
+ res.header('Content-Disposition', `attachment; filename="${sanitizeFilename(env.filename)}"`);
103
+ res.send(env.body);
104
+ }
105
+ /** Plan #10 — send a 403 response. Branches on `Accept: application/json`
106
+ * the same way the action / form dispatch paths do. Used by every route
107
+ * after a `Resource.canX(...)` check fails. We deliberately do NOT
108
+ * redirect to login: 403 means "authenticated but not allowed"; the
109
+ * 401-unauthenticated case is `Pilotiq.guard()`'s job. */
110
+ function forbidden(res, json) {
111
+ res.status(403);
112
+ if (json)
113
+ return res.json({ ok: false, error: 'Forbidden' });
114
+ return res.send('Forbidden');
115
+ }
116
+ /** Run a `canX(...)` predicate, treating throws as `false`. The predicate
117
+ * is user-authored and we want a flaky check to fail closed (deny) rather
118
+ * than 500 the page. */
119
+ async function checkPolicy(fn) {
120
+ try {
121
+ return Boolean(await fn());
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ }
127
+ async function policyAccess(owner, user) {
128
+ const [ownerOk, clusterOk] = await Promise.all([
129
+ checkPolicy(() => owner.canAccess(user)),
130
+ owner.cluster
131
+ ? checkPolicy(() => owner.cluster.canAccess(user))
132
+ : Promise.resolve(true),
133
+ ]);
134
+ return ownerOk && clusterOk;
135
+ }
136
+ /**
137
+ * Locate an action by name in a resolved page schema. Looks at both
138
+ * page-level actions (`findActions`) AND row-scoped extraItemActions on
139
+ * Repeater/Builder fields (`findRowExtraActions`). When the match is
140
+ * row-scoped, also returns the parent field reference and the form
141
+ * schema array — the dispatcher uses both to coerce the form body and
142
+ * navigate to the right row when stamping `ctx.row`.
143
+ *
144
+ * Page-level matches win when a page-level + row-scoped action share the
145
+ * same name (page-level is strictly more privileged: it has access to
146
+ * the full form, not just one row). The collision is undocumented
147
+ * behavior — authors should use distinct names.
148
+ */
149
+ function resolveDispatchTarget(elements, actionName) {
150
+ const pageLevel = findActions(elements).find(a => a.name === actionName);
151
+ if (pageLevel)
152
+ return { action: pageLevel };
153
+ const rowMatches = findRowExtraActions(elements).filter(r => r.action.name === actionName);
154
+ if (rowMatches.length === 0)
155
+ return null;
156
+ if (rowMatches.length > 1) {
157
+ console.warn(`[pilotiq] Action "${actionName}" registered as extraItemActions on multiple ` +
158
+ `fields. Using the first match — disambiguate by renaming.`);
159
+ }
160
+ const first = rowMatches[0];
161
+ // `formSchema` is the entire page tree for v1 — `coerceFormValues`
162
+ // needs the field schema rooted at the form, not just the one row's
163
+ // children. Passing the page tree is over-broad but safe (the function
164
+ // walks until it finds the field). A future polish can narrow to the
165
+ // owning Form once we walk back from the matched field.
166
+ return { action: first.action, rowField: first.field, formSchema: elements };
167
+ }
168
+ async function handleFormState(req, res, pilotiq, scope, formId) {
169
+ const body = (await readFormBody(req));
170
+ const changed = typeof body.changed === 'string' ? body.changed : '';
171
+ const values = (body.values && typeof body.values === 'object' && !Array.isArray(body.values))
172
+ ? body.values
173
+ : {};
174
+ if (!formId || !changed) {
175
+ res.status(400);
176
+ return res.json({ ok: false, error: 'Missing formId or changed field' });
177
+ }
178
+ try {
179
+ const result = await formStateData(pilotiq, scope, { formId, changed, values }, req);
180
+ if (result === null) {
181
+ res.status(404);
182
+ return res.json({ ok: false, error: 'Page not found' });
183
+ }
184
+ if (!result.ok) {
185
+ res.status(result.status);
186
+ return res.json({ ok: false, error: result.error });
187
+ }
188
+ return res.json({ ok: true, form: result.form, dirty: result.dirty });
189
+ }
190
+ catch (err) {
191
+ const message = err instanceof Error ? err.message : 'Form update failed';
192
+ res.status(500);
193
+ return res.json({ ok: false, error: message });
194
+ }
195
+ }
196
+ async function handleFormWizard(req, res, pilotiq, scope, formId) {
197
+ const body = (await readFormBody(req));
198
+ const stepN = typeof body.step === 'number' ? body.step
199
+ : typeof body.step === 'string' ? Number(body.step)
200
+ : NaN;
201
+ const values = (body.values && typeof body.values === 'object' && !Array.isArray(body.values))
202
+ ? body.values
203
+ : {};
204
+ if (!formId || !Number.isFinite(stepN) || stepN < 0) {
205
+ res.status(400);
206
+ return res.json({ ok: false, error: 'Missing formId or invalid step' });
207
+ }
208
+ try {
209
+ const result = await formWizardData(pilotiq, scope, { formId, step: stepN, values }, req);
210
+ if (result === null) {
211
+ res.status(404);
212
+ return res.json({ ok: false, error: 'Page not found' });
213
+ }
214
+ if (!result.ok) {
215
+ res.status(result.status);
216
+ const payload = { ok: false };
217
+ if (result.error)
218
+ payload['error'] = result.error;
219
+ if (result.errors)
220
+ payload['errors'] = result.errors;
221
+ return res.json(payload);
222
+ }
223
+ return res.json({ ok: true });
224
+ }
225
+ catch (err) {
226
+ const message = err instanceof Error ? err.message : 'Wizard step validation failed';
227
+ res.status(500);
228
+ return res.json({ ok: false, error: message });
229
+ }
230
+ }
231
+ async function handleFormCreateOption(req, res, pilotiq, scope, formId, fieldName) {
232
+ const body = (await readFormBody(req));
233
+ const values = (body.values && typeof body.values === 'object' && !Array.isArray(body.values))
234
+ ? body.values
235
+ : {};
236
+ if (!formId || !fieldName) {
237
+ res.status(400);
238
+ return res.json({ ok: false, error: 'Missing formId or fieldName' });
239
+ }
240
+ try {
241
+ const result = await formCreateOptionData(pilotiq, scope, { formId, fieldName, values }, req);
242
+ if (result === null) {
243
+ res.status(404);
244
+ return res.json({ ok: false, error: 'Page not found' });
245
+ }
246
+ if (!result.ok) {
247
+ res.status(result.status);
248
+ const payload = { ok: false };
249
+ if (result.error)
250
+ payload['error'] = result.error;
251
+ if (result.errors)
252
+ payload['errors'] = result.errors;
253
+ return res.json(payload);
254
+ }
255
+ return res.json({ ok: true, option: result.option });
256
+ }
257
+ catch (err) {
258
+ const message = err instanceof Error ? err.message : 'createOption failed';
259
+ res.status(500);
260
+ return res.json({ ok: false, error: message });
261
+ }
262
+ }
263
+ async function handleFormMentions(req, res, pilotiq, scope, formId) {
264
+ const body = (await readFormBody(req));
265
+ const field = typeof body.field === 'string' ? body.field : '';
266
+ const trigger = typeof body.trigger === 'string' ? body.trigger : '';
267
+ const query = typeof body.query === 'string' ? body.query : '';
268
+ if (!formId || !field || trigger.length !== 1) {
269
+ res.status(400);
270
+ return res.json({ ok: false, error: 'Missing formId / field / trigger' });
271
+ }
272
+ // Cap query length — the resolver runs the user's code; the trigger
273
+ // never sends more than a word's worth of characters in practice.
274
+ const cappedQuery = query.length > 200 ? query.slice(0, 200) : query;
275
+ try {
276
+ const result = await mentionResolveData(pilotiq, scope, { formId, field, trigger, query: cappedQuery }, req);
277
+ if (result === null) {
278
+ res.status(404);
279
+ return res.json({ ok: false, error: 'Page not found' });
280
+ }
281
+ if (!result.ok) {
282
+ res.status(result.status);
283
+ return res.json({ ok: false, error: result.error });
284
+ }
285
+ return res.json({ ok: true, items: result.items });
286
+ }
287
+ catch (err) {
288
+ const message = err instanceof Error ? err.message : 'Mention resolve failed';
289
+ res.status(500);
290
+ return res.json({ ok: false, error: message });
291
+ }
292
+ }
293
+ async function handleWidgetData(req, res, pilotiq, scope, id) {
294
+ if (!id) {
295
+ res.status(400);
296
+ return res.json({ ok: false, error: 'Missing widget id' });
297
+ }
298
+ const body = (await readFormBody(req));
299
+ const filter = typeof body.filter === 'string' ? body.filter : undefined;
300
+ try {
301
+ const result = await widgetData(pilotiq, scope, filter !== undefined ? { id, filter } : { id }, req);
302
+ if (!result.ok) {
303
+ res.status(result.status);
304
+ return res.json({ ok: false, error: result.error });
305
+ }
306
+ return res.json({ ok: true, data: result.data, timestamp: result.timestamp });
307
+ }
308
+ catch (err) {
309
+ res.status(500);
310
+ return res.json({ ok: false, error: err instanceof Error ? err.message : 'Widget request failed' });
311
+ }
312
+ }
313
+ /**
314
+ * Handle a single file upload from a `FileUpload` field. Validates
315
+ * accept / maxSize against the (optional) per-request hints, hands
316
+ * the file off to the configured adapter, returns `{ ok, url }`.
317
+ *
318
+ * Body shape (multipart/form-data):
319
+ * - `file`: the file blob
320
+ * - `directory`: optional sub-directory hint
321
+ * - `accept`: optional comma-separated MIME list to enforce
322
+ * - `maxSize`: optional byte cap
323
+ * - `fieldName`: optional tag forwarded to the adapter for routing
324
+ */
325
+ async function handleUploadRequest(req, res, pilotiq) {
326
+ const cfg = pilotiq.getConfig();
327
+ if (!cfg.uploads) {
328
+ res.status(500);
329
+ return res.json({ ok: false, error: 'No upload adapter configured' });
330
+ }
331
+ // Auth: panel-wide `guard` and per-request `user`. We don't enforce
332
+ // per-resource canEdit here because the field doesn't know which
333
+ // resource it belongs to — apps that need it should hook into
334
+ // their adapter's `put()` and consult their own auth there.
335
+ if (cfg.guard && !await cfg.guard(req)) {
336
+ res.status(401);
337
+ return res.json({ ok: false, error: 'Unauthorized' });
338
+ }
339
+ // Parse multipart body. Hono's parseBody returns `Record<string, File | string>`.
340
+ const raw = req.raw;
341
+ if (!raw?.req?.parseBody) {
342
+ res.status(500);
343
+ return res.json({ ok: false, error: 'Multipart parsing unavailable' });
344
+ }
345
+ let body;
346
+ try {
347
+ body = await raw.req.parseBody();
348
+ }
349
+ catch (err) {
350
+ res.status(400);
351
+ return res.json({ ok: false, error: err instanceof Error ? err.message : 'Bad request' });
352
+ }
353
+ const file = body['file'];
354
+ if (!file || !(file instanceof File)) {
355
+ res.status(422);
356
+ return res.json({ ok: false, error: 'No file provided' });
357
+ }
358
+ const directory = typeof body['directory'] === 'string' ? body['directory'] : undefined;
359
+ const fieldName = typeof body['fieldName'] === 'string' ? body['fieldName'] : '';
360
+ // Server-side validation. Both accept and maxSize are advisory hints
361
+ // shipped by the field meta, so we re-check here so a tampered client
362
+ // can't bypass the limits.
363
+ const acceptStr = typeof body['accept'] === 'string' ? body['accept'] : '';
364
+ if (acceptStr) {
365
+ const accept = acceptStr.split(',').map(s => s.trim()).filter(Boolean);
366
+ if (accept.length > 0 && !accept.includes(file.type)) {
367
+ res.status(422);
368
+ return res.json({ ok: false, error: `File type "${file.type}" not allowed` });
369
+ }
370
+ }
371
+ const maxSizeStr = typeof body['maxSize'] === 'string' ? body['maxSize'] : '';
372
+ if (maxSizeStr) {
373
+ const maxSize = Number(maxSizeStr);
374
+ if (Number.isFinite(maxSize) && file.size > maxSize) {
375
+ res.status(422);
376
+ return res.json({ ok: false, error: `File exceeds ${maxSize} bytes` });
377
+ }
378
+ }
379
+ try {
380
+ const result = await cfg.uploads.adapter.put({
381
+ file,
382
+ ...(directory ? { directory } : {}),
383
+ fieldName,
384
+ });
385
+ return res.json({ ok: true, url: result.url, ...(result.meta ? { meta: result.meta } : {}) });
386
+ }
387
+ catch (err) {
388
+ res.status(500);
389
+ return res.json({ ok: false, error: err instanceof Error ? err.message : 'Upload failed' });
390
+ }
391
+ }
392
+ export function registerPilotiqRoutes(router, pilotiq) {
393
+ const cfg = pilotiq.getConfig();
394
+ const base = cfg.path;
395
+ // Fail fast at boot — a silent 404 at request time is much harder to
396
+ // debug than a clear error here. Dangling-reference checks run even
397
+ // when `cfg.clusters` is empty.
398
+ const clusterSet = new Set(cfg.clusters);
399
+ const assertClusterRegistered = (kind, items) => {
400
+ for (const item of items) {
401
+ if (item.cluster === undefined)
402
+ continue;
403
+ if (!clusterSet.has(item.cluster)) {
404
+ throw new Error(`[Pilotiq] ${kind} ${item.name} references cluster ${item.cluster.name} which is not registered. ` +
405
+ `Add it to Pilotiq.clusters([…]).`);
406
+ }
407
+ }
408
+ };
409
+ assertClusterRegistered('Resource', cfg.resources);
410
+ assertClusterRegistered('Global', cfg.globals);
411
+ assertClusterRegistered('Page', cfg.pages);
412
+ if (cfg.clusters.length > 0) {
413
+ const seenClusterSlug = new Set();
414
+ for (const C of cfg.clusters) {
415
+ const s = C.getSlug();
416
+ if (s === '' || /^_/.test(s) || s === 'theme' || s === 'api') {
417
+ throw new Error(`[Pilotiq] Cluster ${C.name} uses reserved slug "${s}". ` +
418
+ `Cluster slugs cannot be empty, start with "_", or equal "theme" / "api".`);
419
+ }
420
+ if (seenClusterSlug.has(s)) {
421
+ throw new Error(`[Pilotiq] Two clusters share slug "${s}". Cluster slugs must be unique.`);
422
+ }
423
+ seenClusterSlug.add(s);
424
+ }
425
+ // Top-level (no cluster) child slugs must not collide with cluster
426
+ // slugs — `<panel-base>/<slug>` would resolve to the cluster first.
427
+ const assertNoSlugCollision = (kind, items, skip) => {
428
+ for (const item of items) {
429
+ if (item.cluster || (skip?.(item) ?? false))
430
+ continue;
431
+ if (seenClusterSlug.has(item.getSlug())) {
432
+ const hint = kind === 'Resource'
433
+ ? ` Either rename the resource or move it inside the cluster.`
434
+ : '';
435
+ throw new Error(`[Pilotiq] ${kind} ${item.name} slug "${item.getSlug()}" collides with a registered cluster slug.${hint}`);
436
+ }
437
+ }
438
+ };
439
+ assertNoSlugCollision('Resource', cfg.resources);
440
+ assertNoSlugCollision('Global', cfg.globals);
441
+ assertNoSlugCollision('Page', cfg.pages, P => P === cfg.dashboardPage);
442
+ // landingPage sanity — a cluster's landing page must be inside the
443
+ // cluster (or the redirect would jump out of the cluster URL space).
444
+ for (const C of cfg.clusters) {
445
+ const lp = C.landingPage;
446
+ if (lp === undefined)
447
+ continue;
448
+ if (!cfg.pages.includes(lp)) {
449
+ throw new Error(`[Pilotiq] Cluster ${C.name}.landingPage references ${lp.name} which is not registered in Pilotiq.pages([…]).`);
450
+ }
451
+ if (lp.cluster !== C) {
452
+ throw new Error(`[Pilotiq] Cluster ${C.name}.landingPage = ${lp.name}, but ${lp.name}.cluster does not point back at ${C.name}.`);
453
+ }
454
+ }
455
+ }
456
+ // Plan #11 — fail fast at boot when any relation manager's
457
+ // `relationship` collides with a reserved URL token. A silent 404 at
458
+ // request time is much harder to debug.
459
+ //
460
+ // Phase B nested resources — same validation walks managers declared
461
+ // via `M.relations()` (one level deep). Depth-3+ is rejected here too:
462
+ // declaring `relations()` on a nested manager isn't supported in
463
+ // Phase B (Filament also caps at depth 2).
464
+ for (const R of cfg.resources) {
465
+ for (const M of R.relations()) {
466
+ const rel = M.getRelationship();
467
+ if (RESERVED_RELATIONSHIP_TOKENS.has(rel)) {
468
+ throw new Error(`[Pilotiq] RelationManager ${M.name} on ${R.name} uses reserved relationship "${rel}". ` +
469
+ `Reserved tokens: ${[...RESERVED_RELATIONSHIP_TOKENS].join(', ')}. Rename it.`);
470
+ }
471
+ for (const N of M.relations()) {
472
+ const nestedRel = N.getRelationship();
473
+ if (RESERVED_RELATIONSHIP_TOKENS.has(nestedRel)) {
474
+ throw new Error(`[Pilotiq] Nested RelationManager ${N.name} under ${M.name} on ${R.name} uses reserved relationship "${nestedRel}". ` +
475
+ `Reserved tokens: ${[...RESERVED_RELATIONSHIP_TOKENS].join(', ')}. Rename it.`);
476
+ }
477
+ if (N.relations().length > 0) {
478
+ throw new Error(`[Pilotiq] Nested RelationManager ${N.name} under ${M.name} on ${R.name} declares its own relations(). ` +
479
+ `Phase B caps nesting at depth 2 (Filament does too). Drop the nested relations() override.`);
480
+ }
481
+ }
482
+ }
483
+ }
484
+ // Reorderable rows — fail fast at boot when a Resource declares
485
+ // `Table.reorderable()` but the bound model can't actually persist a
486
+ // new order. We invoke `R.table(Table.make())` once per resource (the
487
+ // same call shape `defaultPages` uses at request time) and inspect
488
+ // `_reorderableColumn`. The model.reorder check is symmetric with
489
+ // Plan #13's restore/forceDelete guards. Result is cached per-resource
490
+ // so the route loop below can decide whether to mount `_reorder`.
491
+ const reorderEnabled = new Map(); // slug → column
492
+ for (const R of cfg.resources) {
493
+ let probeColumn;
494
+ try {
495
+ probeColumn = R.table(Table.make()).getReorderableColumn();
496
+ }
497
+ catch {
498
+ continue;
499
+ } // user-side throw — not a reorder concern
500
+ if (probeColumn === undefined)
501
+ continue;
502
+ if (!R.model || typeof R.model.reorder !== 'function') {
503
+ throw new Error(`[Pilotiq] ${R.name}.table() calls reorderable("${probeColumn}") but the bound model has no reorder(ids) method. ` +
504
+ `Implement \`async reorder(ids)\` on the rudder Model (or remove the .reorderable() call).`);
505
+ }
506
+ reorderEnabled.set(R.getSlug(), probeColumn);
507
+ }
508
+ // Editable cell columns — fail fast at boot when a Resource declares
509
+ // at least one TextInput/Toggle/SelectColumn but the bound model
510
+ // can't persist a single-column update. Mirrors the reorder guard
511
+ // above. Result is cached per-resource so the route loop below can
512
+ // decide whether to mount `_cell`.
513
+ const editableEnabled = new Set();
514
+ for (const R of cfg.resources) {
515
+ let hasEditable = false;
516
+ try {
517
+ hasEditable = (R.table(Table.make()).getChildren() ?? [])
518
+ .some(c => c instanceof Column && c.isEditable());
519
+ }
520
+ catch {
521
+ continue;
522
+ }
523
+ if (!hasEditable)
524
+ continue;
525
+ if (!R.model || typeof R.model.update !== 'function') {
526
+ throw new Error(`[Pilotiq] ${R.name}.table() declares an editable cell column ` +
527
+ `(TextInputColumn / ToggleColumn / SelectColumn) but the bound ` +
528
+ `model has no update(id, data) method. Set Resource.model = M ` +
529
+ `(rudder ORM convention) or drop the editable column.`);
530
+ }
531
+ editableEnabled.add(R.getSlug());
532
+ }
533
+ // ── Dashboard (1-segment) ─────────────────────────────
534
+ router.get(base, async (req, res) => {
535
+ // Plan #15 — when `panel.dashboard(P)` is set, gate the dashboard
536
+ // route through the page's `canAccess` predicate. Same posture as
537
+ // custom pages — fail-closed on throw.
538
+ if (cfg.dashboardPage) {
539
+ const user = await pilotiq.resolveUser(req);
540
+ if (!await policyAccess(cfg.dashboardPage, user)) {
541
+ return forbidden(res, wantsJson(req));
542
+ }
543
+ }
544
+ return view('pilotiq.dashboard', await dashboardData(pilotiq, req));
545
+ });
546
+ // ── File uploads (FileUpload field POST target) ───────
547
+ router.post(`${base}/_uploads`, async (req, res) => {
548
+ return handleUploadRequest(req, res, pilotiq);
549
+ });
550
+ // ── Plan #15 dashboard widget polling ─────────────────
551
+ // POST ${base}/_widget/:id — re-resolves the dashboard page schema,
552
+ // finds widget by id, runs `getServerData(ctx)`. Body: `{ filter? }`.
553
+ // Mounted unconditionally — widgetData() returns 404 when no
554
+ // dashboard page is registered, so this stays cheap when unused.
555
+ router.post(`${base}/_widget/:id`, async (req, res) => {
556
+ if (cfg.dashboardPage) {
557
+ const user = await pilotiq.resolveUser(req);
558
+ if (!await policyAccess(cfg.dashboardPage, user))
559
+ return forbidden(res, true);
560
+ }
561
+ return handleWidgetData(req, res, pilotiq, { kind: 'panel' }, req.params['id']);
562
+ });
563
+ // ── Plan #12 global search ────────────────────────────
564
+ // GET ${base}/_search?q=…&limit=… → { ok, results }
565
+ // No 403 on unrecognised users — `searchAllResources` filters per
566
+ // resource. The Pilotiq.guard() layer above is the panel-level gate.
567
+ router.get(`${base}/_search`, async (req, res) => {
568
+ const query = req.query;
569
+ const rawQ = query?.['q'];
570
+ const q = typeof rawQ === 'string' ? rawQ.slice(0, 200) : '';
571
+ const data = await searchData(pilotiq, q, req);
572
+ return res.json(data);
573
+ });
574
+ // ── Database notifications (bell-icon dropdown) ───────
575
+ // Only mounted when `Pilotiq.databaseNotifications()` was called.
576
+ // Every route 401s when no user resolves so a non-authenticated
577
+ // request never sees another user's inbox. The `notifiable_type`
578
+ // value is configurable but defaults to `'users'` to match
579
+ // `@rudderjs/notification`'s `DatabaseChannel` writes.
580
+ if (cfg.databaseNotifications?.enabled) {
581
+ const dn = cfg.databaseNotifications;
582
+ const notifiableType = dn.notifiableType ?? 'users';
583
+ const pageSize = dn.pageSize ?? 25;
584
+ /** Resolve `{ id }` from the panel's user resolver. Returns null
585
+ * when no user / unknown id — every route then 401s. The user
586
+ * object is opaque to pilotiq; we duck-type `.id`. */
587
+ const resolveUserId = async (req) => {
588
+ const user = await pilotiq.resolveUser(req);
589
+ if (!user || typeof user !== 'object')
590
+ return null;
591
+ const id = user.id;
592
+ if (id === undefined || id === null)
593
+ return null;
594
+ return String(id);
595
+ };
596
+ // GET ${base}/_notifications → { notifications, unreadCount }
597
+ router.get(`${base}/_notifications`, async (req, res) => {
598
+ const id = await resolveUserId(req);
599
+ if (id === null) {
600
+ res.status(401);
601
+ return res.json({ ok: false, error: 'Not authenticated' });
602
+ }
603
+ const url = new URL(req.url ?? '/', 'http://localhost');
604
+ const unreadOnly = url.searchParams.get('unread') === 'true';
605
+ const limitRaw = Number(url.searchParams.get('limit') ?? pageSize);
606
+ const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? Math.min(limitRaw, 100) : pageSize;
607
+ const data = await listDatabaseNotifications({
608
+ notifiableType,
609
+ notifiableId: id,
610
+ limit,
611
+ unreadOnly,
612
+ });
613
+ return res.json({ ok: true, ...data });
614
+ });
615
+ // POST ${base}/_notifications/:id/read
616
+ router.post(`${base}/_notifications/:id/read`, async (req, res) => {
617
+ const userId = await resolveUserId(req);
618
+ if (userId === null) {
619
+ res.status(401);
620
+ return res.json({ ok: false, error: 'Not authenticated' });
621
+ }
622
+ const rowId = req.params['id'] ?? '';
623
+ const updated = await markDatabaseNotificationAsRead(rowId, {
624
+ notifiableType,
625
+ notifiableId: userId,
626
+ });
627
+ return res.json({ ok: updated });
628
+ });
629
+ // POST ${base}/_notifications/:id/unread
630
+ router.post(`${base}/_notifications/:id/unread`, async (req, res) => {
631
+ const userId = await resolveUserId(req);
632
+ if (userId === null) {
633
+ res.status(401);
634
+ return res.json({ ok: false, error: 'Not authenticated' });
635
+ }
636
+ const rowId = req.params['id'] ?? '';
637
+ const updated = await markDatabaseNotificationAsUnread(rowId, {
638
+ notifiableType,
639
+ notifiableId: userId,
640
+ });
641
+ return res.json({ ok: updated });
642
+ });
643
+ // POST ${base}/_notifications/read-all
644
+ router.post(`${base}/_notifications/read-all`, async (req, res) => {
645
+ const userId = await resolveUserId(req);
646
+ if (userId === null) {
647
+ res.status(401);
648
+ return res.json({ ok: false, error: 'Not authenticated' });
649
+ }
650
+ const count = await markAllDatabaseNotificationsAsRead({
651
+ notifiableType,
652
+ notifiableId: userId,
653
+ });
654
+ return res.json({ ok: true, count });
655
+ });
656
+ // POST ${base}/_notifications/:id/_action/:actionName
657
+ //
658
+ // Notification action dispatch — looks up the stored action on the
659
+ // row, resolves the named handler against the panel's
660
+ // `notificationHandlers` registry, and runs it with the row's
661
+ // stored payload. Optionally flips `read_at` server-side when the
662
+ // action carried `markAsRead: true`.
663
+ //
664
+ // Defends in depth: 404s on missing row / wrong owner / action
665
+ // missing / non-string handler / unknown registry name. Body is
666
+ // ignored — payload reads exclusively from the stored row, so a
667
+ // tampered client can't inject extra payload keys.
668
+ router.post(`${base}/_notifications/:id/_action/:actionName`, async (req, res) => {
669
+ const user = await pilotiq.resolveUser(req);
670
+ const userId = user && typeof user === 'object'
671
+ ? (user.id !== undefined && user.id !== null
672
+ ? String(user.id) : null)
673
+ : null;
674
+ if (userId === null) {
675
+ res.status(401);
676
+ return res.json({ ok: false, error: 'Not authenticated' });
677
+ }
678
+ const params = req.params;
679
+ const result = await dispatchNotificationAction(pilotiq, {
680
+ notificationId: params['id'] ?? '',
681
+ actionName: params['actionName'] ?? '',
682
+ notifiableType,
683
+ notifiableId: userId,
684
+ user,
685
+ request: req,
686
+ });
687
+ if (!result.ok) {
688
+ res.status(result.status);
689
+ return res.json({ ok: false, error: result.error });
690
+ }
691
+ return res.json(result);
692
+ });
693
+ // Phase 2 — register the broadcast auth callback for private
694
+ // `pilotiq-notifications.<userId>` channels. Soft-fails when
695
+ // `@rudderjs/broadcast` isn't installed; apps that haven't enabled
696
+ // broadcast on the toggle stay quiet either way.
697
+ void registerBroadcastAuth(pilotiq);
698
+ }
699
+ // ── Resource routes ───────────────────────────────────
700
+ for (const R of cfg.resources) {
701
+ const slug = R.getSlug();
702
+ const resourceBase = resourceBasePath(base, R);
703
+ const pages = R.resolvePages();
704
+ // Index — GET ${resourceBase}
705
+ if (pages.index) {
706
+ const PageClass = pages.index;
707
+ const indexUrl = resourceBase;
708
+ router.get(indexUrl, async (req, res) => {
709
+ const user = await pilotiq.resolveUser(req);
710
+ if (!await policyAccess(R, user))
711
+ return forbidden(res, wantsJson(req));
712
+ if (!await checkPolicy(() => R.canViewAny(user)))
713
+ return forbidden(res, wantsJson(req));
714
+ if (R.persistFiltersInSession) {
715
+ const query = req.query ?? {};
716
+ const sessionSlug = resourceBase.slice(base.length + 1);
717
+ if (Object.keys(query).length === 0) {
718
+ const restoreTab = readPersistedLastTab(req, base, sessionSlug) ?? '';
719
+ const stored = readPersistedListQuery(req, listFiltersKey(base, sessionSlug, restoreTab));
720
+ if (stored) {
721
+ const qs = encodePersistedQuery(stored, restoreTab);
722
+ if (qs !== '')
723
+ return res.redirect(`${indexUrl}?${qs}`, 302);
724
+ }
725
+ }
726
+ else {
727
+ const tab = typeof query['tab'] === 'string' ? query['tab'] : '';
728
+ writePersistedListQuery(req, listFiltersKey(base, sessionSlug, tab), query);
729
+ writePersistedLastTab(req, base, sessionSlug, tab);
730
+ }
731
+ }
732
+ const data = await resourceIndexData(pilotiq, slug, req.query, req);
733
+ return view('pilotiq.slug', data ?? {});
734
+ });
735
+ router.post(`${indexUrl}/_widget/:id`, async (req, res) => {
736
+ const user = await pilotiq.resolveUser(req);
737
+ if (!await policyAccess(R, user))
738
+ return forbidden(res, true);
739
+ if (!await checkPolicy(() => R.canViewAny(user)))
740
+ return forbidden(res, true);
741
+ return handleWidgetData(req, res, pilotiq, { kind: 'resource', slug }, req.params['id']);
742
+ });
743
+ if (R.deferLoading) {
744
+ router.get(`${indexUrl}/_table`, async (req, res) => {
745
+ const user = await pilotiq.resolveUser(req);
746
+ if (!await policyAccess(R, user))
747
+ return forbidden(res, true);
748
+ if (!await checkPolicy(() => R.canViewAny(user)))
749
+ return forbidden(res, true);
750
+ const data = await resourceTableData(pilotiq, slug, req.query, req);
751
+ if (!data) {
752
+ res.status(404);
753
+ return res.json({ ok: false, error: 'Resource not found' });
754
+ }
755
+ return res.json({ ok: true, ...data });
756
+ });
757
+ }
758
+ // Action dispatch — POST ${resourceBase}/_action/:actionName
759
+ router.post(`${indexUrl}/_action/:actionName`, async (req, res) => {
760
+ const user = await pilotiq.resolveUser(req);
761
+ if (!await policyAccess(R, user))
762
+ return forbidden(res, wantsJson(req));
763
+ const actionName = req.params['actionName'];
764
+ const json = wantsJson(req);
765
+ const body = await readFormBody(req);
766
+ const input = parseActionBody(body);
767
+ const ctx = { mode: 'table', basePath: base, ...(user !== null ? { user: user } : {}) };
768
+ const elements = await callPageSchema(PageClass, ctx);
769
+ tagActionDispatch(elements, indexUrl);
770
+ const target = resolveDispatchTarget(elements, actionName);
771
+ if (!target) {
772
+ if (json) {
773
+ res.status(404);
774
+ return res.json({ ok: false, error: `Action "${actionName}" not found` });
775
+ }
776
+ res.status(404);
777
+ return res.send(`Action "${actionName}" not found on ${R.label}`);
778
+ }
779
+ const resolveRecord = R.model
780
+ ? (id) => findRecord(R, id, { user })
781
+ : undefined;
782
+ const result = await dispatchAction(target.action, {
783
+ ...input,
784
+ request: req,
785
+ user,
786
+ ...(target.rowField ? { rowField: target.rowField } : {}),
787
+ ...(target.formSchema ? { formSchema: target.formSchema } : {}),
788
+ }, resolveRecord);
789
+ if (!result.ok) {
790
+ if (json) {
791
+ res.status(result.errors ? 422 : 500);
792
+ return res.json({ ok: false, error: result.error, ...(result.errors ? { errors: result.errors } : {}) });
793
+ }
794
+ res.status(500);
795
+ return res.send(result.error);
796
+ }
797
+ // Download envelope wins over redirect — `Action.export` and friends
798
+ // return the file body inline. Notifications dropped on this branch
799
+ // because the binary response has no JSON envelope to carry them;
800
+ // the file itself is the success signal.
801
+ if (result.download)
802
+ return sendDownload(res, result.download);
803
+ const redirect = normalizeRedirect(result.redirect, base) ?? indexUrl;
804
+ if (json) {
805
+ return res.json({
806
+ ok: true,
807
+ redirect,
808
+ ...(result.notifications ? { notifications: result.notifications } : {}),
809
+ });
810
+ }
811
+ flashNotifications(req, result.notifications);
812
+ return res.redirect(redirect, 303);
813
+ });
814
+ // Reorderable rows — POST ${resourceBase}/_reorder { ids: [] }
815
+ // Only mounted when `Resource.table()` opts in (boot-time probe
816
+ // populates `reorderEnabled`).
817
+ if (reorderEnabled.has(slug)) {
818
+ router.post(`${indexUrl}/_reorder`, async (req, res) => {
819
+ const user = await pilotiq.resolveUser(req);
820
+ if (!await policyAccess(R, user))
821
+ return forbidden(res, true);
822
+ // List-level edit gate. The drop affects many rows at once;
823
+ // there's no single record to authorize against, so we pass
824
+ // `undefined` and let user-supplied `canEdit` overrides branch
825
+ // on `record === undefined` if they want row-level granularity.
826
+ if (!await checkPolicy(() => R.canEdit(user, undefined)))
827
+ return forbidden(res, true);
828
+ const body = await readFormBody(req);
829
+ const raw = body.ids;
830
+ if (!Array.isArray(raw) || raw.length === 0) {
831
+ res.status(400);
832
+ return res.json({ ok: false, error: 'Missing or empty ids array' });
833
+ }
834
+ const ids = raw.filter((id) => typeof id === 'string' || typeof id === 'number');
835
+ if (ids.length !== raw.length) {
836
+ res.status(400);
837
+ return res.json({ ok: false, error: 'ids must contain only strings or numbers' });
838
+ }
839
+ try {
840
+ // Boot already verified `R.model?.reorder` exists; the `!`
841
+ // assertions are safe.
842
+ await R.model.reorder(ids);
843
+ return res.json({ ok: true });
844
+ }
845
+ catch (err) {
846
+ res.status(422);
847
+ return res.json({
848
+ ok: false,
849
+ error: err instanceof Error ? err.message : 'Reorder failed',
850
+ });
851
+ }
852
+ });
853
+ }
854
+ // Editable cell columns — POST ${resourceBase}/:id/_cell/:column
855
+ // { value: <coerced> }. Only mounted when the resource declares at
856
+ // least one editable column (boot-time probe populates
857
+ // `editableEnabled`).
858
+ if (editableEnabled.has(slug)) {
859
+ router.post(`${indexUrl}/:id/_cell/:column`, async (req, res) => {
860
+ const user = await pilotiq.resolveUser(req);
861
+ if (!await policyAccess(R, user))
862
+ return forbidden(res, true);
863
+ const id = req.params['id'];
864
+ const colName = req.params['column'];
865
+ // Locate the column on the table. We re-derive `Table.make()`
866
+ // here (same probe shape used by the boot guard + reorder route)
867
+ // so the column instance carries its validators / discriminator.
868
+ const probe = R.table(Table.make());
869
+ const col = (probe.getChildren() ?? [])
870
+ .find((c) => c instanceof Column && c.name === colName);
871
+ if (!col) {
872
+ res.status(400);
873
+ return res.json({ ok: false, error: `Unknown column "${colName}"` });
874
+ }
875
+ if (!col.isEditable()) {
876
+ res.status(400);
877
+ return res.json({ ok: false, error: `Column "${colName}" is not editable` });
878
+ }
879
+ // Boot already verified `R.model?.update`; the `!` is safe.
880
+ const record = await findRecord(R, id, { user });
881
+ if (record === null || record === undefined) {
882
+ res.status(404);
883
+ return res.json({ ok: false, error: 'Record not found' });
884
+ }
885
+ if (!await checkPolicy(() => R.canEdit(user, record)))
886
+ return forbidden(res, true);
887
+ const body = await readFormBody(req);
888
+ const raw = body.value;
889
+ let value;
890
+ try {
891
+ value = coerceCellValue(col, raw);
892
+ }
893
+ catch (err) {
894
+ const message = err instanceof CellCoerceError ? err.message
895
+ : err instanceof Error ? err.message
896
+ : 'Invalid value';
897
+ res.status(422);
898
+ return res.json({ ok: false, errors: { value: [message] } });
899
+ }
900
+ const errors = await col.runValidators(value, { record });
901
+ if (errors.length > 0) {
902
+ res.status(422);
903
+ return res.json({ ok: false, errors: { value: errors } });
904
+ }
905
+ try {
906
+ await R.model.update(id, { [col.name]: value });
907
+ }
908
+ catch (err) {
909
+ res.status(422);
910
+ return res.json({
911
+ ok: false,
912
+ error: err instanceof Error ? err.message : 'Update failed',
913
+ });
914
+ }
915
+ return res.json({ ok: true, value, notifications: [] });
916
+ });
917
+ }
918
+ }
919
+ // Plan #5 — partial-resolve endpoint for create-mode forms.
920
+ // POST ${resourceBase}/_form/:formId/state
921
+ if (pages.create) {
922
+ router.post(`${resourceBase}/_form/:formId/state`, async (req, res) => {
923
+ const user = await pilotiq.resolveUser(req);
924
+ if (!await policyAccess(R, user))
925
+ return forbidden(res, true);
926
+ if (!await checkPolicy(() => R.canCreate(user)))
927
+ return forbidden(res, true);
928
+ const formId = req.params['formId'];
929
+ return handleFormState(req, res, pilotiq, { kind: 'resource-create', slug }, formId);
930
+ });
931
+ // Plan #8 — wizard step-validate endpoint for create-mode forms.
932
+ router.post(`${resourceBase}/_form/:formId/wizard`, async (req, res) => {
933
+ const user = await pilotiq.resolveUser(req);
934
+ if (!await policyAccess(R, user))
935
+ return forbidden(res, true);
936
+ if (!await checkPolicy(() => R.canCreate(user)))
937
+ return forbidden(res, true);
938
+ const formId = req.params['formId'];
939
+ return handleFormWizard(req, res, pilotiq, { kind: 'resource-create', slug }, formId);
940
+ });
941
+ // Async-mention endpoint for create-mode forms.
942
+ router.post(`${resourceBase}/_form/:formId/mentions`, async (req, res) => {
943
+ const user = await pilotiq.resolveUser(req);
944
+ if (!await policyAccess(R, user))
945
+ return forbidden(res, true);
946
+ if (!await checkPolicy(() => R.canCreate(user)))
947
+ return forbidden(res, true);
948
+ const formId = req.params['formId'];
949
+ return handleFormMentions(req, res, pilotiq, { kind: 'resource-create', slug }, formId);
950
+ });
951
+ // SelectField inline-create modal endpoint for create-mode forms.
952
+ router.post(`${resourceBase}/_form/:formId/create-option/:fieldName`, async (req, res) => {
953
+ const user = await pilotiq.resolveUser(req);
954
+ if (!await policyAccess(R, user))
955
+ return forbidden(res, true);
956
+ if (!await checkPolicy(() => R.canCreate(user)))
957
+ return forbidden(res, true);
958
+ const formId = req.params['formId'];
959
+ const fieldName = req.params['fieldName'];
960
+ return handleFormCreateOption(req, res, pilotiq, { kind: 'resource-create', slug }, formId, fieldName);
961
+ });
962
+ }
963
+ // Plan #5 — partial-resolve endpoint for edit-mode forms.
964
+ // POST ${resourceBase}/:id/_form/:formId/state
965
+ if (pages.edit) {
966
+ router.post(`${resourceBase}/:id/_form/:formId/state`, async (req, res) => {
967
+ const recordId = req.params['id'];
968
+ const formId = req.params['formId'];
969
+ const user = await pilotiq.resolveUser(req);
970
+ if (!await policyAccess(R, user))
971
+ return forbidden(res, true);
972
+ const policyRecord = R.model ? await findRecord(R, recordId, { user }).catch(() => undefined) : { id: recordId };
973
+ if (!await checkPolicy(() => R.canEdit(user, policyRecord)))
974
+ return forbidden(res, true);
975
+ return handleFormState(req, res, pilotiq, { kind: 'resource-edit', slug, recordId }, formId);
976
+ });
977
+ // Plan #8 — wizard step-validate endpoint for edit-mode forms.
978
+ router.post(`${resourceBase}/:id/_form/:formId/wizard`, async (req, res) => {
979
+ const recordId = req.params['id'];
980
+ const formId = req.params['formId'];
981
+ const user = await pilotiq.resolveUser(req);
982
+ if (!await policyAccess(R, user))
983
+ return forbidden(res, true);
984
+ const policyRecord = R.model ? await findRecord(R, recordId, { user }).catch(() => undefined) : { id: recordId };
985
+ if (!await checkPolicy(() => R.canEdit(user, policyRecord)))
986
+ return forbidden(res, true);
987
+ return handleFormWizard(req, res, pilotiq, { kind: 'resource-edit', slug, recordId }, formId);
988
+ });
989
+ // Async-mention endpoint for edit-mode forms.
990
+ router.post(`${resourceBase}/:id/_form/:formId/mentions`, async (req, res) => {
991
+ const recordId = req.params['id'];
992
+ const formId = req.params['formId'];
993
+ const user = await pilotiq.resolveUser(req);
994
+ if (!await policyAccess(R, user))
995
+ return forbidden(res, true);
996
+ const policyRecord = R.model ? await findRecord(R, recordId, { user }).catch(() => undefined) : { id: recordId };
997
+ if (!await checkPolicy(() => R.canEdit(user, policyRecord)))
998
+ return forbidden(res, true);
999
+ return handleFormMentions(req, res, pilotiq, { kind: 'resource-edit', slug, recordId }, formId);
1000
+ });
1001
+ // SelectField inline-create modal endpoint for edit-mode forms.
1002
+ router.post(`${resourceBase}/:id/_form/:formId/create-option/:fieldName`, async (req, res) => {
1003
+ const recordId = req.params['id'];
1004
+ const formId = req.params['formId'];
1005
+ const fieldName = req.params['fieldName'];
1006
+ const user = await pilotiq.resolveUser(req);
1007
+ if (!await policyAccess(R, user))
1008
+ return forbidden(res, true);
1009
+ const policyRecord = R.model ? await findRecord(R, recordId, { user }).catch(() => undefined) : { id: recordId };
1010
+ if (!await checkPolicy(() => R.canEdit(user, policyRecord)))
1011
+ return forbidden(res, true);
1012
+ return handleFormCreateOption(req, res, pilotiq, { kind: 'resource-edit', slug, recordId }, formId, fieldName);
1013
+ });
1014
+ }
1015
+ // Create — GET ${resourceBase}/create
1016
+ if (pages.create) {
1017
+ const PageClass = pages.create;
1018
+ const createUrl = `${resourceBase}/create`;
1019
+ router.get(createUrl, async (req, res) => {
1020
+ const user = await pilotiq.resolveUser(req);
1021
+ if (!await policyAccess(R, user))
1022
+ return forbidden(res, wantsJson(req));
1023
+ if (!await checkPolicy(() => R.canCreate(user)))
1024
+ return forbidden(res, wantsJson(req));
1025
+ const data = await resourceCreateData(pilotiq, slug, undefined, req);
1026
+ return view('pilotiq.resource-create', data ?? {});
1027
+ });
1028
+ // Create — POST ${resourceBase}/create
1029
+ router.post(createUrl, async (req, res) => {
1030
+ const user = await pilotiq.resolveUser(req);
1031
+ if (!await policyAccess(R, user))
1032
+ return forbidden(res, wantsJson(req));
1033
+ if (!await checkPolicy(() => R.canCreate(user)))
1034
+ return forbidden(res, wantsJson(req));
1035
+ const body = await readFormBody(req);
1036
+ const { values, formId, continueCreate } = splitMeta(body);
1037
+ const json = wantsJson(req);
1038
+ const ctx = { mode: 'create', basePath: base, ...(user !== null ? { user: user } : {}) };
1039
+ const elements = await callPageSchema(PageClass, ctx);
1040
+ tagFormActions(elements, createUrl);
1041
+ const form = selectForm(findForms(elements), formId);
1042
+ if (!form) {
1043
+ if (json) {
1044
+ res.status(404);
1045
+ return res.json({ ok: false, error: 'No form found on page' });
1046
+ }
1047
+ res.status(404);
1048
+ return res.send('No form found on page');
1049
+ }
1050
+ const result = await dispatchFormSubmit(form, values, {
1051
+ values,
1052
+ basePath: base,
1053
+ ...(R.model ? { parentModel: R.model } : {}),
1054
+ });
1055
+ if (!result.ok) {
1056
+ if (json) {
1057
+ res.status(422);
1058
+ return res.json({ ok: false, errors: result.errors });
1059
+ }
1060
+ // Re-render through the same builder so the page is identical to GET,
1061
+ // just with values + errors prefilled.
1062
+ const data = await resourceCreateData(pilotiq, slug, { values, errors: result.errors });
1063
+ res.status(422);
1064
+ return view('pilotiq.resource-create', data ?? {});
1065
+ }
1066
+ const recordId = result.record?.id;
1067
+ // "Create & create another" — when the secondary submit fired,
1068
+ // route back to the create page with a fresh form. Skips any
1069
+ // user-supplied `redirectAfterSave`: the user clicked the
1070
+ // button asking explicitly to create another, so the
1071
+ // continue-intent wins. `force: true` tells the SPA-mode
1072
+ // FormRenderer to navigate even though the redirect URL
1073
+ // matches the current page (otherwise the same-URL skip
1074
+ // would preserve the just-submitted values on screen).
1075
+ const fallback = continueCreate
1076
+ ? createUrl
1077
+ : recordId !== undefined ? `${resourceBase}/${String(recordId)}/edit` : `${resourceBase}`;
1078
+ const redirect = continueCreate
1079
+ ? createUrl
1080
+ : normalizeRedirect(result.redirect, base) ?? fallback;
1081
+ if (json) {
1082
+ return res.json({
1083
+ ok: true,
1084
+ redirect,
1085
+ ...(continueCreate ? { force: true } : {}),
1086
+ ...(result.notifications && result.notifications.length > 0 ? { notifications: result.notifications } : {}),
1087
+ });
1088
+ }
1089
+ flashNotifications(req, result.notifications);
1090
+ return res.redirect(redirect, 303);
1091
+ });
1092
+ // Action dispatch — POST ${createUrl}/_action/:actionName
1093
+ // Handles both page-level handler-style actions AND Repeater /
1094
+ // Builder `extraItemActions` rows. The latter pass `_rowPath` in
1095
+ // the body so the dispatcher hydrates `ctx.row` from the form's
1096
+ // coerced values.
1097
+ router.post(`${createUrl}/_action/:actionName`, async (req, res) => {
1098
+ const user = await pilotiq.resolveUser(req);
1099
+ if (!await policyAccess(R, user))
1100
+ return forbidden(res, wantsJson(req));
1101
+ if (!await checkPolicy(() => R.canCreate(user)))
1102
+ return forbidden(res, wantsJson(req));
1103
+ const actionName = req.params['actionName'];
1104
+ const json = wantsJson(req);
1105
+ const body = await readFormBody(req);
1106
+ const input = parseActionBody(body);
1107
+ const ctx = { mode: 'create', basePath: base, ...(user !== null ? { user: user } : {}) };
1108
+ const elements = await callPageSchema(PageClass, ctx);
1109
+ tagActionDispatch(elements, createUrl);
1110
+ const target = resolveDispatchTarget(elements, actionName);
1111
+ if (!target) {
1112
+ if (json) {
1113
+ res.status(404);
1114
+ return res.json({ ok: false, error: `Action "${actionName}" not found` });
1115
+ }
1116
+ res.status(404);
1117
+ return res.send(`Action "${actionName}" not found on ${R.label}`);
1118
+ }
1119
+ const result = await dispatchAction(target.action, {
1120
+ ...input,
1121
+ request: req,
1122
+ user,
1123
+ ...(target.rowField ? { rowField: target.rowField } : {}),
1124
+ ...(target.formSchema ? { formSchema: target.formSchema } : {}),
1125
+ });
1126
+ if (!result.ok) {
1127
+ if (json) {
1128
+ res.status(result.errors ? 422 : 500);
1129
+ return res.json({ ok: false, error: result.error, ...(result.errors ? { errors: result.errors } : {}) });
1130
+ }
1131
+ res.status(500);
1132
+ return res.send(result.error);
1133
+ }
1134
+ if (result.download)
1135
+ return sendDownload(res, result.download);
1136
+ const redirect = normalizeRedirect(result.redirect, base) ?? createUrl;
1137
+ if (json) {
1138
+ return res.json({
1139
+ ok: true,
1140
+ redirect,
1141
+ ...(result.notifications ? { notifications: result.notifications } : {}),
1142
+ });
1143
+ }
1144
+ flashNotifications(req, result.notifications);
1145
+ return res.redirect(redirect, 303);
1146
+ });
1147
+ }
1148
+ // View — GET ${resourceBase}/:id (literal `create` matches first via
1149
+ // Hono's literal-over-param routing, so `:id` only catches everything else.)
1150
+ if (pages.view) {
1151
+ router.get(`${resourceBase}/:id`, async (req, res) => {
1152
+ const recordId = req.params['id'];
1153
+ // Hono routes both `/create` and `/:id` against this slot; only the
1154
+ // literal `create` segment hits the create route. Defensive guard:
1155
+ if (recordId === 'create')
1156
+ return; // handled by create route
1157
+ const user = await pilotiq.resolveUser(req);
1158
+ if (!await policyAccess(R, user))
1159
+ return forbidden(res, wantsJson(req));
1160
+ // Load the record once so canView can inspect it. Stub `{ id }`
1161
+ // when the resource has no model wired — the user-authored
1162
+ // predicate gets to decide what to do with it.
1163
+ const record = R.model ? await findRecord(R, recordId, { user }).catch(() => undefined) : { id: recordId };
1164
+ if (!await checkPolicy(() => R.canView(user, record)))
1165
+ return forbidden(res, wantsJson(req));
1166
+ const data = await resourceViewData(pilotiq, slug, recordId, req);
1167
+ return view('pilotiq.resource-view', data ?? {});
1168
+ });
1169
+ // Delete — POST ${resourceBase}/:id/delete
1170
+ router.post(`${resourceBase}/:id/delete`, async (req, res) => {
1171
+ const recordId = req.params['id'];
1172
+ const json = wantsJson(req);
1173
+ const indexUrl = `${resourceBase}`;
1174
+ const user = await pilotiq.resolveUser(req);
1175
+ if (!await policyAccess(R, user))
1176
+ return forbidden(res, json);
1177
+ const record = R.model ? await findRecord(R, recordId, { user }).catch(() => undefined) : { id: recordId };
1178
+ if (!await checkPolicy(() => R.canDelete(user, record)))
1179
+ return forbidden(res, json);
1180
+ try {
1181
+ await R.deleteRecord(recordId);
1182
+ }
1183
+ catch (err) {
1184
+ const message = err instanceof Error ? err.message : 'Delete failed';
1185
+ if (json) {
1186
+ res.status(500);
1187
+ return res.json({ ok: false, error: message });
1188
+ }
1189
+ res.status(500);
1190
+ return res.send(message);
1191
+ }
1192
+ if (json) {
1193
+ // Build a synthetic deletion notification so the SPA path gets
1194
+ // the same toast UX as a JSON-dispatched action handler. The
1195
+ // form-method 303 path doesn't have the form-lifecycle toast
1196
+ // pipeline, so we surface confirmation here. Plan #13: use
1197
+ // "moved to trash" framing on soft-delete resources so users
1198
+ // know the row is recoverable.
1199
+ const title = R.softDeletes
1200
+ ? `${R.labelSingular} moved to trash`
1201
+ : `${R.labelSingular} deleted`;
1202
+ const notifications = [
1203
+ { id: `n-delete-${recordId}-${Date.now()}`, type: 'success', title },
1204
+ ];
1205
+ return res.json({ ok: true, redirect: indexUrl, notifications });
1206
+ }
1207
+ return res.redirect(indexUrl, 303);
1208
+ });
1209
+ }
1210
+ // ─── Plan #13 soft-delete routes (restore / force-delete) ─────
1211
+ // Both routes opt-in only when `Resource.softDeletes = true`. They
1212
+ // load the target row through `withTrashed()` so the lookup finds
1213
+ // currently-trashed records (which the default scope hides). The
1214
+ // `restore` route undoes a prior soft-delete; `force-delete`
1215
+ // bypasses soft-delete entirely.
1216
+ if (R.softDeletes) {
1217
+ // Boot-time guard — yell loudly if the rudder ORM model isn't
1218
+ // wired up. Keeps "why didn't restore work?" debug sessions
1219
+ // short. Pilotiq's flag and rudder's flag are deliberately
1220
+ // independent (see plan doc).
1221
+ if (!R.model) {
1222
+ throw new Error(`[Pilotiq] ${R.name}: softDeletes = true requires a Resource.model. Wire one up or unset softDeletes.`);
1223
+ }
1224
+ if (typeof R.model.restore !== 'function' || typeof R.model.forceDelete !== 'function') {
1225
+ throw new Error(`[Pilotiq] ${R.name}: softDeletes = true but model.restore / model.forceDelete are missing. ` +
1226
+ `Set Model.softDeletes = true on the rudder side, or upgrade @rudderjs/orm.`);
1227
+ }
1228
+ const M = R.model;
1229
+ const pk = (M.primaryKey ?? 'id');
1230
+ // Helper — load a row through `withTrashed` so currently-trashed
1231
+ // records resolve. Returns undefined when the lookup misses (route
1232
+ // converts to 404).
1233
+ const loadTrashable = async (id) => {
1234
+ const q = M.query();
1235
+ if (typeof q.withTrashed !== 'function')
1236
+ return M.find(id).catch(() => undefined);
1237
+ const result = await q.withTrashed()
1238
+ .where(pk, '=', id)
1239
+ .paginate(1, 1)
1240
+ .catch(() => ({ data: [] }));
1241
+ return Array.isArray(result.data) ? result.data[0] : undefined;
1242
+ };
1243
+ // Restore — POST ${resourceBase}/:id/restore
1244
+ router.post(`${resourceBase}/:id/restore`, async (req, res) => {
1245
+ const recordId = req.params['id'];
1246
+ const json = wantsJson(req);
1247
+ const indexUrl = `${resourceBase}`;
1248
+ const user = await pilotiq.resolveUser(req);
1249
+ if (!await policyAccess(R, user))
1250
+ return forbidden(res, json);
1251
+ const record = await loadTrashable(recordId);
1252
+ if (!record) {
1253
+ res.status(404);
1254
+ return json ? res.json({ ok: false, error: 'Not found' }) : res.send('Not found');
1255
+ }
1256
+ if (!await checkPolicy(() => R.canRestore(user, record)))
1257
+ return forbidden(res, json);
1258
+ try {
1259
+ await M.restore(recordId);
1260
+ }
1261
+ catch (err) {
1262
+ const message = err instanceof Error ? err.message : 'Restore failed';
1263
+ res.status(500);
1264
+ return json ? res.json({ ok: false, error: message }) : res.send(message);
1265
+ }
1266
+ if (json) {
1267
+ const notifications = [
1268
+ { id: `n-restore-${recordId}-${Date.now()}`, type: 'success', title: `${R.labelSingular} restored` },
1269
+ ];
1270
+ return res.json({ ok: true, redirect: indexUrl, notifications });
1271
+ }
1272
+ return res.redirect(indexUrl, 303);
1273
+ });
1274
+ // Force-delete — POST ${resourceBase}/:id/force-delete
1275
+ router.post(`${resourceBase}/:id/force-delete`, async (req, res) => {
1276
+ const recordId = req.params['id'];
1277
+ const json = wantsJson(req);
1278
+ const indexUrl = `${resourceBase}`;
1279
+ const user = await pilotiq.resolveUser(req);
1280
+ if (!await policyAccess(R, user))
1281
+ return forbidden(res, json);
1282
+ const record = await loadTrashable(recordId);
1283
+ if (!record) {
1284
+ res.status(404);
1285
+ return json ? res.json({ ok: false, error: 'Not found' }) : res.send('Not found');
1286
+ }
1287
+ if (!await checkPolicy(() => R.canForceDelete(user, record)))
1288
+ return forbidden(res, json);
1289
+ try {
1290
+ await M.forceDelete(recordId);
1291
+ }
1292
+ catch (err) {
1293
+ const message = err instanceof Error ? err.message : 'Force-delete failed';
1294
+ res.status(500);
1295
+ return json ? res.json({ ok: false, error: message }) : res.send(message);
1296
+ }
1297
+ if (json) {
1298
+ const notifications = [
1299
+ { id: `n-fdelete-${recordId}-${Date.now()}`, type: 'success', title: `${R.labelSingular} permanently deleted` },
1300
+ ];
1301
+ return res.json({ ok: true, redirect: indexUrl, notifications });
1302
+ }
1303
+ return res.redirect(indexUrl, 303);
1304
+ });
1305
+ }
1306
+ // Edit — GET ${resourceBase}/:id/edit
1307
+ if (pages.edit) {
1308
+ const PageClass = pages.edit;
1309
+ router.get(`${resourceBase}/:id/edit`, async (req, res) => {
1310
+ const recordId = req.params['id'];
1311
+ const user = await pilotiq.resolveUser(req);
1312
+ if (!await policyAccess(R, user))
1313
+ return forbidden(res, wantsJson(req));
1314
+ const record = R.model ? await findRecord(R, recordId, { user }).catch(() => undefined) : { id: recordId };
1315
+ if (!await checkPolicy(() => R.canEdit(user, record)))
1316
+ return forbidden(res, wantsJson(req));
1317
+ const data = await resourceEditData(pilotiq, slug, recordId, undefined, req);
1318
+ return view('pilotiq.resource-edit', data ?? {});
1319
+ });
1320
+ // Edit — POST ${resourceBase}/:id/edit
1321
+ router.post(`${resourceBase}/:id/edit`, async (req, res) => {
1322
+ const recordId = req.params['id'];
1323
+ const editUrl = `${resourceBase}/${recordId}/edit`;
1324
+ const body = await readFormBody(req);
1325
+ const { values, formId } = splitMeta(body);
1326
+ const json = wantsJson(req);
1327
+ const user = await pilotiq.resolveUser(req);
1328
+ if (!await policyAccess(R, user))
1329
+ return forbidden(res, json);
1330
+ const policyRecord = R.model ? await findRecord(R, recordId, { user }).catch(() => undefined) : { id: recordId };
1331
+ if (!await checkPolicy(() => R.canEdit(user, policyRecord)))
1332
+ return forbidden(res, json);
1333
+ const ctx = { mode: 'edit', recordId, basePath: base, ...(user !== null ? { user: user } : {}) };
1334
+ const elements = await callPageSchema(PageClass, ctx);
1335
+ tagFormActions(elements, editUrl);
1336
+ const form = selectForm(findForms(elements), formId);
1337
+ if (!form) {
1338
+ if (json) {
1339
+ res.status(404);
1340
+ return res.json({ ok: false, error: 'No form found on page' });
1341
+ }
1342
+ res.status(404);
1343
+ return res.send('No form found on page');
1344
+ }
1345
+ // Try to load the record so validators with cross-field rules see it.
1346
+ let record = undefined;
1347
+ if (form.getLoadRecord()) {
1348
+ try {
1349
+ record = await form.getLoadRecord()(recordId, { values });
1350
+ }
1351
+ catch { /* ignore */ }
1352
+ }
1353
+ const result = await dispatchFormSubmit(form, values, {
1354
+ values,
1355
+ basePath: base,
1356
+ ...(record !== undefined ? { record } : {}),
1357
+ ...(R.model ? { parentModel: R.model } : {}),
1358
+ });
1359
+ if (!result.ok) {
1360
+ if (json) {
1361
+ res.status(422);
1362
+ return res.json({ ok: false, errors: result.errors });
1363
+ }
1364
+ const data = await resourceEditData(pilotiq, slug, recordId, { values, errors: result.errors });
1365
+ res.status(422);
1366
+ return view('pilotiq.resource-edit', data ?? {});
1367
+ }
1368
+ const redirect = normalizeRedirect(result.redirect, base) ?? editUrl;
1369
+ if (json) {
1370
+ return res.json({
1371
+ ok: true,
1372
+ redirect,
1373
+ ...(result.notifications && result.notifications.length > 0 ? { notifications: result.notifications } : {}),
1374
+ });
1375
+ }
1376
+ flashNotifications(req, result.notifications);
1377
+ return res.redirect(redirect, 303);
1378
+ });
1379
+ // Action dispatch — POST ${editUrl}/_action/:actionName
1380
+ // Same shape as the create-page _action route. The `:id` segment
1381
+ // gates record-aware policy (canEdit per record); row-scoped
1382
+ // dispatch reuses the form schema we resolve here for `coerceFormValues`.
1383
+ router.post(`${resourceBase}/:id/_action/:actionName`, async (req, res) => {
1384
+ const recordId = req.params['id'];
1385
+ // Hono routes `/edit` and `/delete` against this slot too — bail
1386
+ // out so the dedicated handlers downstream pick them up. The
1387
+ // `:actionName` capture catches anything; the explicit guard
1388
+ // mirrors the view-route `recordId === 'create'` defensive branch.
1389
+ const actionName = req.params['actionName'];
1390
+ const user = await pilotiq.resolveUser(req);
1391
+ if (!await policyAccess(R, user))
1392
+ return forbidden(res, wantsJson(req));
1393
+ const policyRecord = R.model ? await findRecord(R, recordId, { user }).catch(() => undefined) : { id: recordId };
1394
+ if (!await checkPolicy(() => R.canEdit(user, policyRecord)))
1395
+ return forbidden(res, wantsJson(req));
1396
+ const json = wantsJson(req);
1397
+ const body = await readFormBody(req);
1398
+ const input = parseActionBody(body);
1399
+ const editUrl = `${resourceBase}/${recordId}/edit`;
1400
+ const ctx = { mode: 'edit', recordId, basePath: base, ...(user !== null ? { user: user } : {}) };
1401
+ const elements = await callPageSchema(PageClass, ctx);
1402
+ tagActionDispatch(elements, editUrl);
1403
+ const target = resolveDispatchTarget(elements, actionName);
1404
+ if (!target) {
1405
+ if (json) {
1406
+ res.status(404);
1407
+ return res.json({ ok: false, error: `Action "${actionName}" not found` });
1408
+ }
1409
+ res.status(404);
1410
+ return res.send(`Action "${actionName}" not found on ${R.label}`);
1411
+ }
1412
+ const resolveRecord = R.model
1413
+ ? (id) => findRecord(R, id, { user })
1414
+ : undefined;
1415
+ const result = await dispatchAction(target.action, {
1416
+ ...input,
1417
+ request: req,
1418
+ user,
1419
+ ...(target.rowField ? { rowField: target.rowField } : {}),
1420
+ ...(target.formSchema ? { formSchema: target.formSchema } : {}),
1421
+ }, resolveRecord);
1422
+ if (!result.ok) {
1423
+ if (json) {
1424
+ res.status(result.errors ? 422 : 500);
1425
+ return res.json({ ok: false, error: result.error, ...(result.errors ? { errors: result.errors } : {}) });
1426
+ }
1427
+ res.status(500);
1428
+ return res.send(result.error);
1429
+ }
1430
+ if (result.download)
1431
+ return sendDownload(res, result.download);
1432
+ const redirect = normalizeRedirect(result.redirect, base) ?? editUrl;
1433
+ if (json) {
1434
+ return res.json({
1435
+ ok: true,
1436
+ redirect,
1437
+ ...(result.notifications ? { notifications: result.notifications } : {}),
1438
+ });
1439
+ }
1440
+ flashNotifications(req, result.notifications);
1441
+ return res.redirect(redirect, 303);
1442
+ });
1443
+ }
1444
+ // ── Plan #11 relation manager routes ───────────────
1445
+ // Per-manager: list, create (GET/POST), edit (GET/POST), delete (POST).
1446
+ // Mounted under ${resourceBase}/:id/${rel} — the `:id` segment is the
1447
+ // PARENT record id; the `:childId` segment (where present) is the
1448
+ // related record's id. Authorization runs in two layers: parent
1449
+ // canAccess + canEdit(parent), then manager-scoped can*.
1450
+ for (const M of R.relations()) {
1451
+ const rel = M.getRelationship();
1452
+ const parentBase = `${resourceBase}/:id/${rel}`;
1453
+ // Read the relation type once at registration so the (R, M)-
1454
+ // scoped closures all see the same mode without re-reading the
1455
+ // relations map per request. `R.model` is asserted by
1456
+ // `requireParent` at request time; here it may legitimately be
1457
+ // missing during late binding, in which case we fall back to
1458
+ // 'hasMany' (the safe default — no special action injection / no
1459
+ // factory short-circuiting). See `normalizeRelationMode` for the
1460
+ // M2M / polymorphic mappings.
1461
+ const relationType = R.model ? getRelationType(R.model, rel) : 'hasMany';
1462
+ const mode = normalizeRelationMode(relationType);
1463
+ // Common policy prelude: load parent, gate access. Returns the
1464
+ // parent record on success or a thrown 403/404 response. Returns
1465
+ // `undefined` when the route should bail out (response already sent).
1466
+ const requireParent = async (req, res, json) => {
1467
+ const recordId = req.params['id'];
1468
+ const user = await pilotiq.resolveUser(req);
1469
+ if (!await policyAccess(R, user)) {
1470
+ forbidden(res, json);
1471
+ return undefined;
1472
+ }
1473
+ if (!R.model) {
1474
+ res.status(500);
1475
+ if (json)
1476
+ res.json({ ok: false, error: `Resource "${R.name}" has relations but no static model` });
1477
+ else
1478
+ res.send(`Resource "${R.name}" has relations but no static model`);
1479
+ return undefined;
1480
+ }
1481
+ const parent = await findRecord(R, recordId, { user }).catch(() => undefined);
1482
+ if (!parent) {
1483
+ res.status(404);
1484
+ if (json)
1485
+ res.json({ ok: false, error: 'Parent not found' });
1486
+ else
1487
+ res.send('Parent not found');
1488
+ return undefined;
1489
+ }
1490
+ if (!await checkPolicy(() => R.canEdit(user, parent))) {
1491
+ forbidden(res, json);
1492
+ return undefined;
1493
+ }
1494
+ return { user, parent, recordId };
1495
+ };
1496
+ // List — GET ${resourceBase}/:id/${rel}
1497
+ // Manager-level canViewAny is enforced inside relationManagerData via
1498
+ // safeManagerPolicy (with related-resource fall-through). We just
1499
+ // surface the {ok:false,status:403} from the data builder as 403.
1500
+ router.get(parentBase, async (req, res) => {
1501
+ const json = wantsJson(req);
1502
+ const ctx = await requireParent(req, res, json);
1503
+ if (!ctx)
1504
+ return;
1505
+ const data = await relationManagerData(pilotiq, {
1506
+ kind: 'relation-list', slug, recordId: ctx.recordId, relationship: rel, query: req.query,
1507
+ }, req);
1508
+ if (data === null) {
1509
+ res.status(404);
1510
+ return res.send('Not found');
1511
+ }
1512
+ if ('ok' in data && data.ok === false)
1513
+ return forbidden(res, json);
1514
+ return view('pilotiq.relation-list', data);
1515
+ });
1516
+ // Create — GET ${resourceBase}/:id/${rel}/create
1517
+ router.get(`${parentBase}/create`, async (req, res) => {
1518
+ const json = wantsJson(req);
1519
+ const ctx = await requireParent(req, res, json);
1520
+ if (!ctx)
1521
+ return;
1522
+ const data = await relationManagerData(pilotiq, {
1523
+ kind: 'relation-create', slug, recordId: ctx.recordId, relationship: rel,
1524
+ }, req);
1525
+ if (data === null) {
1526
+ res.status(404);
1527
+ return res.send('Not found');
1528
+ }
1529
+ if ('ok' in data && data.ok === false)
1530
+ return forbidden(res, json);
1531
+ return view('pilotiq.relation-create', data);
1532
+ });
1533
+ // Create submit — POST ${resourceBase}/:id/${rel}/create
1534
+ router.post(`${parentBase}/create`, async (req, res) => {
1535
+ const json = wantsJson(req);
1536
+ const pre = await requireParent(req, res, json);
1537
+ if (!pre)
1538
+ return;
1539
+ const Related = findRelatedResource(M, R, cfg);
1540
+ if (!Related) {
1541
+ res.status(500);
1542
+ const msg = `RelationManager ${M.name}: cannot resolve related Resource for create`;
1543
+ return json ? res.json({ ok: false, error: msg }) : res.send(msg);
1544
+ }
1545
+ if (!await safeManagerPolicy(M, 'canCreate', Related, pre.user, pre.parent))
1546
+ return forbidden(res, json);
1547
+ const body = await readFormBody(req);
1548
+ const { values } = splitMeta(body);
1549
+ const createUrl = `${parentBase}/create`.replace(':id', pre.recordId);
1550
+ const listUrl = parentBase.replace(':id', pre.recordId);
1551
+ const form = M.form(Form.make(), {
1552
+ basePath: base,
1553
+ parentSlug: slug,
1554
+ parentId: pre.recordId,
1555
+ relationship: rel,
1556
+ parentRecord: pre.parent,
1557
+ related: Related,
1558
+ mode,
1559
+ });
1560
+ if (Related.model) {
1561
+ if (!form.getSave())
1562
+ form.save(modelSave(Related.model));
1563
+ if (!form.getLoadRecord())
1564
+ form.loadRecord(modelLoadRecord(Related));
1565
+ }
1566
+ // Polymorphic auto-injection — when the parent's relation entry
1567
+ // is `morphMany` / `morphOne`, fill the `{morphName}Id` and
1568
+ // `{morphName}Type` columns on the child before persistence.
1569
+ // Compose with any user-supplied `mutateDataBeforeCreate` and
1570
+ // run AFTER it so morph values overwrite anything the form
1571
+ // body or user hook might have set — the parent record is the
1572
+ // single source of truth for who owns the new child, and a
1573
+ // submitted form field cannot be allowed to tamper with that.
1574
+ if (mode === 'morphMany' && R.model) {
1575
+ const morphDesc = getMorphRelationDescriptor(R.model, rel);
1576
+ if (!morphDesc) {
1577
+ res.status(500);
1578
+ const msg = `RelationManager ${M.name}: relations[${JSON.stringify(rel)}] reports a polymorphic type but is missing morphName.`;
1579
+ return json ? res.json({ ok: false, error: msg }) : res.send(msg);
1580
+ }
1581
+ const morphPayload = computeMorphPayload(pre.parent, morphDesc);
1582
+ const existing = form.getMutateDataBeforeCreate();
1583
+ form.mutateDataBeforeCreate(async (data, ctx) => {
1584
+ const next = existing ? await existing(data, ctx) : data;
1585
+ return { ...next, ...morphPayload };
1586
+ });
1587
+ }
1588
+ // Stamp parent context onto FormContext so user hooks
1589
+ // (mutateDataBeforeCreate, redirectAfterSave, etc.) can default
1590
+ // foreign-key columns or build URLs from the parent.
1591
+ const formCtx = {
1592
+ values,
1593
+ basePath: base,
1594
+ parent: pre.parent,
1595
+ parentId: pre.recordId,
1596
+ relationship: rel,
1597
+ };
1598
+ const result = await dispatchFormSubmit(form, values, formCtx);
1599
+ if (!result.ok) {
1600
+ if (json) {
1601
+ res.status(422);
1602
+ return res.json({ ok: false, errors: result.errors });
1603
+ }
1604
+ const data = await relationManagerData(pilotiq, {
1605
+ kind: 'relation-create', slug, recordId: pre.recordId, relationship: rel,
1606
+ prefill: { values, errors: result.errors ?? {} },
1607
+ }, req);
1608
+ res.status(422);
1609
+ return view('pilotiq.relation-create', data ?? {});
1610
+ }
1611
+ const redirect = normalizeRedirect(result.redirect, base) ?? listUrl;
1612
+ if (json) {
1613
+ return res.json({
1614
+ ok: true, redirect,
1615
+ ...(result.notifications && result.notifications.length > 0 ? { notifications: result.notifications } : {}),
1616
+ });
1617
+ }
1618
+ flashNotifications(req, result.notifications);
1619
+ return res.redirect(redirect, 303);
1620
+ });
1621
+ // View — GET ${resourceBase}/:id/${rel}/:childId (Phase A nested
1622
+ // resources). 5-segment URL. The literal `${parentBase}/create`
1623
+ // route is registered above and Hono prefers static segments over
1624
+ // wildcards, but the `childId === 'create'` guard belt-and-suspenders
1625
+ // against any router that doesn't.
1626
+ router.get(`${parentBase}/:childId`, async (req, res) => {
1627
+ const json = wantsJson(req);
1628
+ const pre = await requireParent(req, res, json);
1629
+ if (!pre)
1630
+ return;
1631
+ const childId = req.params['childId'];
1632
+ if (childId === 'create') {
1633
+ res.status(404);
1634
+ return res.send('Not found');
1635
+ }
1636
+ const data = await relationManagerData(pilotiq, {
1637
+ kind: 'relation-view', slug, recordId: pre.recordId, relationship: rel, childId,
1638
+ }, req);
1639
+ if (data === null) {
1640
+ res.status(404);
1641
+ return res.send('Not found');
1642
+ }
1643
+ if ('ok' in data && data.ok === false)
1644
+ return forbidden(res, json);
1645
+ return view('pilotiq.relation-view', data);
1646
+ });
1647
+ // Edit — GET ${resourceBase}/:id/${rel}/:childId/edit
1648
+ router.get(`${parentBase}/:childId/edit`, async (req, res) => {
1649
+ const json = wantsJson(req);
1650
+ const pre = await requireParent(req, res, json);
1651
+ if (!pre)
1652
+ return;
1653
+ const childId = req.params['childId'];
1654
+ const data = await relationManagerData(pilotiq, {
1655
+ kind: 'relation-edit', slug, recordId: pre.recordId, relationship: rel, childId,
1656
+ }, req);
1657
+ if (data === null) {
1658
+ res.status(404);
1659
+ return res.send('Not found');
1660
+ }
1661
+ if ('ok' in data && data.ok === false)
1662
+ return forbidden(res, json);
1663
+ return view('pilotiq.relation-edit', data);
1664
+ });
1665
+ // Edit submit — POST ${resourceBase}/:id/${rel}/:childId/edit
1666
+ router.post(`${parentBase}/:childId/edit`, async (req, res) => {
1667
+ const json = wantsJson(req);
1668
+ const pre = await requireParent(req, res, json);
1669
+ if (!pre)
1670
+ return;
1671
+ const childId = req.params['childId'];
1672
+ const Related = findRelatedResource(M, R, cfg);
1673
+ if (!Related?.model) {
1674
+ res.status(500);
1675
+ const msg = `RelationManager ${M.name}: cannot resolve related Resource for edit`;
1676
+ return json ? res.json({ ok: false, error: msg }) : res.send(msg);
1677
+ }
1678
+ // IDOR + load via the data builder's gating: re-use it to verify
1679
+ // the child belongs to this parent, then do the form submit.
1680
+ const childCheck = await relationManagerData(pilotiq, {
1681
+ kind: 'relation-edit', slug, recordId: pre.recordId, relationship: rel, childId,
1682
+ }, req);
1683
+ if (childCheck === null) {
1684
+ res.status(404);
1685
+ return res.send('Not found');
1686
+ }
1687
+ if ('ok' in childCheck && childCheck.ok === false)
1688
+ return forbidden(res, json);
1689
+ const body = await readFormBody(req);
1690
+ const { values } = splitMeta(body);
1691
+ const editUrl = `${parentBase}/${childId}/edit`.replace(':id', pre.recordId);
1692
+ const form = M.form(Form.make(), {
1693
+ basePath: base,
1694
+ parentSlug: slug,
1695
+ parentId: pre.recordId,
1696
+ relationship: rel,
1697
+ parentRecord: pre.parent,
1698
+ related: Related,
1699
+ mode,
1700
+ });
1701
+ if (!form.getSave())
1702
+ form.save(modelSave(Related.model));
1703
+ if (!form.getLoadRecord())
1704
+ form.loadRecord(modelLoadRecord(Related));
1705
+ // Re-load child for FormContext so cross-field validators see it.
1706
+ let child = undefined;
1707
+ try {
1708
+ child = await findRecord(Related, childId, { user: pre.user });
1709
+ }
1710
+ catch { /* ignore */ }
1711
+ if (!child) {
1712
+ res.status(404);
1713
+ return res.send('Not found');
1714
+ }
1715
+ // Polymorphic re-stamp on update — same posture as the create
1716
+ // path. Re-injecting the morph columns from the live parent
1717
+ // record ensures a tampered body (`commentableId=…` /
1718
+ // `commentableType=…` posted by an attacker) can't reassign
1719
+ // the child to another polymorphic parent. Composed AFTER any
1720
+ // user `mutateDataBeforeUpdate` so the framework wins.
1721
+ if (mode === 'morphMany' && R.model) {
1722
+ const morphDesc = getMorphRelationDescriptor(R.model, rel);
1723
+ if (morphDesc) {
1724
+ const morphPayload = computeMorphPayload(pre.parent, morphDesc);
1725
+ const existing = form.getMutateDataBeforeUpdate();
1726
+ form.mutateDataBeforeUpdate(async (data, ctx) => {
1727
+ const next = existing ? await existing(data, ctx) : data;
1728
+ return { ...next, ...morphPayload };
1729
+ });
1730
+ }
1731
+ }
1732
+ const formCtx = {
1733
+ values,
1734
+ basePath: base,
1735
+ record: child,
1736
+ parent: pre.parent,
1737
+ parentId: pre.recordId,
1738
+ relationship: rel,
1739
+ };
1740
+ const result = await dispatchFormSubmit(form, values, formCtx);
1741
+ if (!result.ok) {
1742
+ if (json) {
1743
+ res.status(422);
1744
+ return res.json({ ok: false, errors: result.errors });
1745
+ }
1746
+ const data = await relationManagerData(pilotiq, {
1747
+ kind: 'relation-edit', slug, recordId: pre.recordId, relationship: rel, childId,
1748
+ prefill: { values, errors: result.errors ?? {} },
1749
+ }, req);
1750
+ res.status(422);
1751
+ return view('pilotiq.relation-edit', data ?? {});
1752
+ }
1753
+ const redirect = normalizeRedirect(result.redirect, base) ?? editUrl;
1754
+ if (json) {
1755
+ return res.json({
1756
+ ok: true, redirect,
1757
+ ...(result.notifications && result.notifications.length > 0 ? { notifications: result.notifications } : {}),
1758
+ });
1759
+ }
1760
+ flashNotifications(req, result.notifications);
1761
+ return res.redirect(redirect, 303);
1762
+ });
1763
+ // Delete — POST ${resourceBase}/:id/${rel}/:childId/delete
1764
+ router.post(`${parentBase}/:childId/delete`, async (req, res) => {
1765
+ const json = wantsJson(req);
1766
+ const pre = await requireParent(req, res, json);
1767
+ if (!pre)
1768
+ return;
1769
+ const childId = req.params['childId'];
1770
+ const Related = findRelatedResource(M, R, cfg);
1771
+ if (!Related?.model) {
1772
+ res.status(500);
1773
+ const msg = `RelationManager ${M.name}: cannot resolve related Resource for delete`;
1774
+ return json ? res.json({ ok: false, error: msg }) : res.send(msg);
1775
+ }
1776
+ // Anti-IDOR: re-use the data builder's child-belongs check.
1777
+ const childCheck = await relationManagerData(pilotiq, {
1778
+ kind: 'relation-edit', slug, recordId: pre.recordId, relationship: rel, childId,
1779
+ }, req);
1780
+ if (childCheck === null) {
1781
+ res.status(404);
1782
+ return res.send('Not found');
1783
+ }
1784
+ if ('ok' in childCheck && childCheck.ok === false)
1785
+ return forbidden(res, json);
1786
+ const child = await findRecord(Related, childId, { user: pre.user }).catch(() => undefined);
1787
+ if (!child) {
1788
+ res.status(404);
1789
+ return res.send('Not found');
1790
+ }
1791
+ if (!await safeManagerPolicy(M, 'canDelete', Related, pre.user, pre.parent, child))
1792
+ return forbidden(res, json);
1793
+ const listUrl = parentBase.replace(':id', pre.recordId);
1794
+ try {
1795
+ await Related.model.delete(childId);
1796
+ }
1797
+ catch (err) {
1798
+ const message = err instanceof Error ? err.message : 'Delete failed';
1799
+ res.status(500);
1800
+ return json ? res.json({ ok: false, error: message }) : res.send(message);
1801
+ }
1802
+ if (json) {
1803
+ const notifications = [
1804
+ { id: `n-rdelete-${childId}-${Date.now()}`, type: 'success', title: `${M.getLabelSingular()} deleted` },
1805
+ ];
1806
+ return res.json({ ok: true, redirect: listUrl, notifications });
1807
+ }
1808
+ return res.redirect(listUrl, 303);
1809
+ });
1810
+ // ── Plan #13 polish — relation restore / force-delete ─────
1811
+ // Mirror the resource-side soft-delete routes, scoped under the
1812
+ // parent record. Both routes opt in only when the related Resource
1813
+ // has `softDeletes = true` AND its model carries `restore` /
1814
+ // `forceDelete`. Two-layer auth: parent canAccess + canEdit, then
1815
+ // manager `canRestore / canForceDelete` (with related-Resource
1816
+ // fall-through). IDOR check re-runs the parent's relation query
1817
+ // through `withTrashed()` so trashed children still resolve.
1818
+ const RelatedForSoft = findRelatedResource(M, R, cfg);
1819
+ if (RelatedForSoft?.softDeletes) {
1820
+ const RM = RelatedForSoft.model;
1821
+ if (!RM) {
1822
+ throw new Error(`[Pilotiq] RelationManager ${M.name} on ${R.name}: related Resource ${RelatedForSoft.name} has softDeletes = true but no model. ` +
1823
+ `Wire one up or unset softDeletes.`);
1824
+ }
1825
+ if (typeof RM.restore !== 'function' || typeof RM.forceDelete !== 'function') {
1826
+ throw new Error(`[Pilotiq] RelationManager ${M.name} on ${R.name}: related Resource ${RelatedForSoft.name} has softDeletes = true but model.restore / model.forceDelete are missing. ` +
1827
+ `Set Model.softDeletes = true on the rudder side, or upgrade @rudderjs/orm.`);
1828
+ }
1829
+ // IDOR-safe load through the parent's relation query, broadened
1830
+ // with `withTrashed()` so currently-trashed children resolve.
1831
+ // Returns undefined when the child doesn't belong to this parent
1832
+ // (under the broadened scope) or the lookup misses.
1833
+ const loadTrashableChild = async (parent, childId) => {
1834
+ if (!R.model)
1835
+ return undefined;
1836
+ const pk = (RM.primaryKey ?? 'id');
1837
+ try {
1838
+ const q = R.model.relatedQuery
1839
+ ? R.model.relatedQuery(parent, rel)
1840
+ : parent.related(rel);
1841
+ const broadened = typeof q.withTrashed === 'function' ? q.withTrashed() : q;
1842
+ const result = await broadened.where(pk, '=', childId).paginate(1, 1);
1843
+ return Array.isArray(result.data) ? result.data[0] : undefined;
1844
+ }
1845
+ catch {
1846
+ return undefined;
1847
+ }
1848
+ };
1849
+ // Restore — POST ${resourceBase}/:id/${rel}/:childId/restore
1850
+ router.post(`${parentBase}/:childId/restore`, async (req, res) => {
1851
+ const json = wantsJson(req);
1852
+ const pre = await requireParent(req, res, json);
1853
+ if (!pre)
1854
+ return;
1855
+ const childId = req.params['childId'];
1856
+ const child = await loadTrashableChild(pre.parent, childId);
1857
+ if (!child) {
1858
+ res.status(404);
1859
+ return res.send('Not found');
1860
+ }
1861
+ if (!await safeManagerPolicy(M, 'canRestore', RelatedForSoft, pre.user, pre.parent, child))
1862
+ return forbidden(res, json);
1863
+ const listUrl = parentBase.replace(':id', pre.recordId);
1864
+ try {
1865
+ await RM.restore(childId);
1866
+ }
1867
+ catch (err) {
1868
+ const message = err instanceof Error ? err.message : 'Restore failed';
1869
+ res.status(500);
1870
+ return json ? res.json({ ok: false, error: message }) : res.send(message);
1871
+ }
1872
+ if (json) {
1873
+ const notifications = [
1874
+ { id: `n-rrestore-${childId}-${Date.now()}`, type: 'success', title: `${M.getLabelSingular()} restored` },
1875
+ ];
1876
+ return res.json({ ok: true, redirect: listUrl, notifications });
1877
+ }
1878
+ return res.redirect(listUrl, 303);
1879
+ });
1880
+ // Force-delete — POST ${resourceBase}/:id/${rel}/:childId/force-delete
1881
+ router.post(`${parentBase}/:childId/force-delete`, async (req, res) => {
1882
+ const json = wantsJson(req);
1883
+ const pre = await requireParent(req, res, json);
1884
+ if (!pre)
1885
+ return;
1886
+ const childId = req.params['childId'];
1887
+ const child = await loadTrashableChild(pre.parent, childId);
1888
+ if (!child) {
1889
+ res.status(404);
1890
+ return res.send('Not found');
1891
+ }
1892
+ if (!await safeManagerPolicy(M, 'canForceDelete', RelatedForSoft, pre.user, pre.parent, child))
1893
+ return forbidden(res, json);
1894
+ const listUrl = parentBase.replace(':id', pre.recordId);
1895
+ try {
1896
+ await RM.forceDelete(childId);
1897
+ }
1898
+ catch (err) {
1899
+ const message = err instanceof Error ? err.message : 'Force-delete failed';
1900
+ res.status(500);
1901
+ return json ? res.json({ ok: false, error: message }) : res.send(message);
1902
+ }
1903
+ if (json) {
1904
+ const notifications = [
1905
+ { id: `n-rforce-${childId}-${Date.now()}`, type: 'success', title: `${M.getLabelSingular()} permanently deleted` },
1906
+ ];
1907
+ return res.json({ ok: true, redirect: listUrl, notifications });
1908
+ }
1909
+ return res.redirect(listUrl, 303);
1910
+ });
1911
+ }
1912
+ // ── M2M follow-up — manager-scoped action dispatch + detach ─────
1913
+ // Two new routes per relation manager. Mounted unconditionally
1914
+ // (even on hasMany managers) because handler-style actions are
1915
+ // useful beyond M2M — any user-defined `Action.handler(...)` on a
1916
+ // manager table needs a place to dispatch. The detach route is
1917
+ // M2M-specific but cheap enough to register either way; non-M2M
1918
+ // managers' `Action.relationDetach` factories return `visible=false`
1919
+ // anyway, so the URL is unreachable in practice.
1920
+ // Action dispatch — POST ${parentBase}/_action/:actionName
1921
+ // Resolves the manager's table elements, finds the named action,
1922
+ // and dispatches it with `ctx.relation = { parent, parentId, rel }`
1923
+ // so M2M handlers can call `parent.related(rel).attach / detach`.
1924
+ // Records hydrate against the related model (the rows visible in
1925
+ // the manager's table are related-model records).
1926
+ router.post(`${parentBase}/_action/:actionName`, async (req, res) => {
1927
+ const json = wantsJson(req);
1928
+ const pre = await requireParent(req, res, json);
1929
+ if (!pre)
1930
+ return;
1931
+ const Related = findRelatedResource(M, R, cfg);
1932
+ const actionName = req.params['actionName'];
1933
+ const body = await readFormBody(req);
1934
+ const input = parseActionBody(body);
1935
+ // Rebuild the manager's table so the dispatcher can find the
1936
+ // action by name. Pure recreation — same context the page-data
1937
+ // builder uses — so factories that close over `ctx` (URL,
1938
+ // mode, parent record) see the same shape as at page render.
1939
+ const managerCtx = {
1940
+ basePath: base,
1941
+ parentSlug: slug,
1942
+ parentId: pre.recordId,
1943
+ relationship: rel,
1944
+ parentRecord: pre.parent,
1945
+ related: Related,
1946
+ mode,
1947
+ };
1948
+ const table = M.table(Table.make(), managerCtx);
1949
+ const elements = [table];
1950
+ // Stamp dispatch URLs so any nested action factories that read
1951
+ // `dispatchUrl` (rare — most read it from the meta at render
1952
+ // time) still see something sensible.
1953
+ const listUrl = parentBase.replace(':id', pre.recordId);
1954
+ tagActionDispatch(elements, listUrl);
1955
+ const target = resolveDispatchTarget(elements, actionName);
1956
+ if (!target) {
1957
+ if (json) {
1958
+ res.status(404);
1959
+ return res.json({ ok: false, error: `Action "${actionName}" not found` });
1960
+ }
1961
+ res.status(404);
1962
+ return res.send(`Action "${actionName}" not found on ${M.name}`);
1963
+ }
1964
+ const resolveRecord = Related?.model
1965
+ ? (id) => Related.model.find(id)
1966
+ : undefined;
1967
+ const result = await dispatchAction(target.action, {
1968
+ ...input,
1969
+ request: req,
1970
+ user: pre.user,
1971
+ relation: { parent: pre.parent, parentId: pre.recordId, relationship: rel },
1972
+ ...(target.rowField ? { rowField: target.rowField } : {}),
1973
+ ...(target.formSchema ? { formSchema: target.formSchema } : {}),
1974
+ }, resolveRecord);
1975
+ if (!result.ok) {
1976
+ if (json) {
1977
+ res.status(result.errors ? 422 : 500);
1978
+ return res.json({ ok: false, error: result.error, ...(result.errors ? { errors: result.errors } : {}) });
1979
+ }
1980
+ res.status(500);
1981
+ return res.send(result.error);
1982
+ }
1983
+ const redirect = normalizeRedirect(result.redirect, base) ?? listUrl;
1984
+ if (json) {
1985
+ return res.json({
1986
+ ok: true,
1987
+ redirect,
1988
+ ...(result.notifications ? { notifications: result.notifications } : {}),
1989
+ });
1990
+ }
1991
+ flashNotifications(req, result.notifications);
1992
+ return res.redirect(redirect, 303);
1993
+ });
1994
+ // Detach — POST ${parentBase}/:childId/_detach
1995
+ // Direct row-action target for `Action.relationDetach`. Removes the
1996
+ // pivot row only; the related record stays in place. IDOR check:
1997
+ // verify the child is currently attached before calling detach so
1998
+ // a tampered URL can't probe random ids.
1999
+ router.post(`${parentBase}/:childId/_detach`, async (req, res) => {
2000
+ const json = wantsJson(req);
2001
+ const pre = await requireParent(req, res, json);
2002
+ if (!pre)
2003
+ return;
2004
+ const childId = req.params['childId'];
2005
+ if (mode !== 'belongsToMany' && mode !== 'morphToMany' && mode !== 'morphedByMany') {
2006
+ // Detach is meaningless for hasMany — the user wants `delete`.
2007
+ // Surface a clear 404 instead of silently no-op'ing.
2008
+ res.status(404);
2009
+ const msg = 'Detach is only supported on M2M relations (belongsToMany, morphToMany, morphedByMany)';
2010
+ return json ? res.json({ ok: false, error: msg }) : res.send(msg);
2011
+ }
2012
+ // Manager-only canDetach: pivot ops don't fall through to the
2013
+ // related Resource. We don't have the related child loaded yet —
2014
+ // pass `undefined` for the per-record arg; canDetach gates on
2015
+ // (user, parent) by default and only sees `record` when a
2016
+ // manager has explicitly overridden with a per-row predicate.
2017
+ // Authors who need per-row gating can detect undefined and either
2018
+ // load the child themselves or short-circuit.
2019
+ // Two distinct accessors are needed under the real
2020
+ // `@rudderjs/orm`:
2021
+ // - `parent.related(rel)` returns a deferred QueryBuilder
2022
+ // with `where / paginate` (IDOR read-side check).
2023
+ // - `parent[rel]()` returns the pivot-mutation accessor with
2024
+ // `attach / detach / sync` (write-side).
2025
+ // Test stubs may collapse both onto the same `parent.related(rel)`
2026
+ // shape — handle that fallback so existing tests keep passing.
2027
+ let child = undefined;
2028
+ const readSide = pre.parent
2029
+ ?.related?.(rel);
2030
+ if (!readSide) {
2031
+ res.status(500);
2032
+ const msg = `Parent.related("${rel}") missing — wrong relation type or ORM version?`;
2033
+ return json ? res.json({ ok: false, error: msg }) : res.send(msg);
2034
+ }
2035
+ try {
2036
+ // IDOR: confirm the child is currently attached.
2037
+ if (typeof readSide.paginate === 'function') {
2038
+ const Related = findRelatedResource(M, R, cfg);
2039
+ const pk = Related?.model ? getPrimaryKey(Related.model) : 'id';
2040
+ const out = await readSide.where(pk, '=', childId).paginate(1, 1);
2041
+ child = Array.isArray(out.data) ? out.data[0] : undefined;
2042
+ }
2043
+ }
2044
+ catch {
2045
+ // fall through; null child means we couldn't verify — safer to 404
2046
+ }
2047
+ if (child === undefined) {
2048
+ res.status(404);
2049
+ return res.send('Not found');
2050
+ }
2051
+ if (!await safeManagerPolicy(M, 'canDetach', undefined, pre.user, pre.parent, child))
2052
+ return forbidden(res, json);
2053
+ // Real ORM: `parent[rel]()` returns the pivot accessor. Test
2054
+ // stubs: `parent.related(rel)` may carry `detach` directly.
2055
+ // Try the prototype-installed instance method first, then fall
2056
+ // back to the read-side shape.
2057
+ let writeAccessor;
2058
+ const inst = pre.parent[rel];
2059
+ if (typeof inst === 'function') {
2060
+ try {
2061
+ const out = inst.call(pre.parent);
2062
+ if (out && typeof out.detach === 'function')
2063
+ writeAccessor = out;
2064
+ }
2065
+ catch { /* fall through to legacy shape */ }
2066
+ }
2067
+ if (!writeAccessor && typeof readSide.detach === 'function') {
2068
+ writeAccessor = readSide;
2069
+ }
2070
+ if (!writeAccessor) {
2071
+ res.status(500);
2072
+ const msg = `Pivot accessor missing on ${rel} — wrong relation type or ORM version?`;
2073
+ return json ? res.json({ ok: false, error: msg }) : res.send(msg);
2074
+ }
2075
+ try {
2076
+ await writeAccessor.detach([childId]);
2077
+ }
2078
+ catch (err) {
2079
+ const message = err instanceof Error ? err.message : 'Detach failed';
2080
+ res.status(500);
2081
+ return json ? res.json({ ok: false, error: message }) : res.send(message);
2082
+ }
2083
+ const listUrl = parentBase.replace(':id', pre.recordId);
2084
+ if (json) {
2085
+ const notifications = [
2086
+ { id: `n-rdetach-${childId}-${Date.now()}`, type: 'success', title: `${M.getLabelSingular()} detached` },
2087
+ ];
2088
+ return res.json({ ok: true, redirect: listUrl, notifications });
2089
+ }
2090
+ return res.redirect(listUrl, 303);
2091
+ });
2092
+ // ── Phase B nested relation routes ──────────────────
2093
+ // For each manager N declared under M.relations(), mount the
2094
+ // depth-2 list/create/view/edit/delete handlers. Auth + chain
2095
+ // IDOR are centralized in `nestedRelationManagerData` — route
2096
+ // bodies dispatch the data builder and unwrap the tagged
2097
+ // {ok:false,status:403} / null shapes. Surface area mirrors
2098
+ // Phase A: no M2M attach/detach, no soft-delete restore on
2099
+ // nested managers in v1 (open follow-ups if a consumer asks).
2100
+ for (const N of M.relations()) {
2101
+ const nestedRel = N.getRelationship();
2102
+ const nestedBase = `${parentBase}/:childId/${nestedRel}`;
2103
+ // Build a `chain` tuple from the URL params for relayed calls
2104
+ // into `relationManagerData`. The childId of the *outer* manager
2105
+ // is the recordId of the leaf step.
2106
+ const buildChain = (id, childId1) => [
2107
+ { recordId: id, relationship: rel },
2108
+ { recordId: childId1, relationship: nestedRel },
2109
+ ];
2110
+ // ── List ──
2111
+ router.get(nestedBase, async (req, res) => {
2112
+ const json = wantsJson(req);
2113
+ const id = req.params['id'];
2114
+ const childId1 = req.params['childId'];
2115
+ const data = await relationManagerData(pilotiq, {
2116
+ kind: 'nested-relation-list', slug,
2117
+ chain: buildChain(id, childId1),
2118
+ query: req.query,
2119
+ }, req);
2120
+ if (data === null) {
2121
+ res.status(404);
2122
+ return res.send('Not found');
2123
+ }
2124
+ if ('ok' in data && data.ok === false)
2125
+ return forbidden(res, json);
2126
+ return view('pilotiq.nested-relation-list', data);
2127
+ });
2128
+ // ── Create (GET) ──
2129
+ router.get(`${nestedBase}/create`, async (req, res) => {
2130
+ const json = wantsJson(req);
2131
+ const id = req.params['id'];
2132
+ const childId1 = req.params['childId'];
2133
+ const data = await relationManagerData(pilotiq, {
2134
+ kind: 'nested-relation-create', slug,
2135
+ chain: buildChain(id, childId1),
2136
+ }, req);
2137
+ if (data === null) {
2138
+ res.status(404);
2139
+ return res.send('Not found');
2140
+ }
2141
+ if ('ok' in data && data.ok === false)
2142
+ return forbidden(res, json);
2143
+ return view('pilotiq.nested-relation-create', data);
2144
+ });
2145
+ // ── Create (POST) ──
2146
+ router.post(`${nestedBase}/create`, async (req, res) => {
2147
+ const json = wantsJson(req);
2148
+ const id = req.params['id'];
2149
+ const childId1 = req.params['childId'];
2150
+ // Run the chain walk once to verify auth + IDOR + load child1.
2151
+ // Any failure returns the same tagged shape we serve on GET.
2152
+ const pre = await relationManagerData(pilotiq, {
2153
+ kind: 'nested-relation-create', slug,
2154
+ chain: buildChain(id, childId1),
2155
+ }, req);
2156
+ if (pre === null) {
2157
+ res.status(404);
2158
+ return res.send('Not found');
2159
+ }
2160
+ if ('ok' in pre && pre.ok === false)
2161
+ return forbidden(res, json);
2162
+ // Re-resolve the leaf manager's bits for form submit. We need
2163
+ // the leaf parent record (`child1`) and the related class for
2164
+ // save/loadRecord wiring. Reuse `findRelatedResource` against
2165
+ // the chain walk's intermediate Resource (Related1).
2166
+ const Related1 = findRelatedResource(M, R, cfg);
2167
+ if (!Related1) {
2168
+ res.status(500);
2169
+ const msg = `Nested manager ${N.name}: cannot resolve middle Resource for create`;
2170
+ return json ? res.json({ ok: false, error: msg }) : res.send(msg);
2171
+ }
2172
+ const Related2 = findRelatedResource(N, Related1, cfg);
2173
+ if (!Related2?.model) {
2174
+ res.status(500);
2175
+ const msg = `Nested manager ${N.name}: cannot resolve related Resource for create`;
2176
+ return json ? res.json({ ok: false, error: msg }) : res.send(msg);
2177
+ }
2178
+ const user = await pilotiq.resolveUser(req);
2179
+ const child1 = await findRecord(Related1, childId1, { user }).catch(() => undefined);
2180
+ if (!child1) {
2181
+ res.status(404);
2182
+ return res.send('Not found');
2183
+ }
2184
+ const body = await readFormBody(req);
2185
+ const { values } = splitMeta(body);
2186
+ const createUrl = `${nestedBase}/create`.replace(':id', id).replace(':childId', childId1);
2187
+ const listUrl = nestedBase.replace(':id', id).replace(':childId', childId1);
2188
+ const nestedMode = Related1.model
2189
+ ? normalizeRelationMode(getRelationType(Related1.model, nestedRel))
2190
+ : 'hasMany';
2191
+ const form = N.form(Form.make(), {
2192
+ basePath: base,
2193
+ parentSlug: slug,
2194
+ parentId: childId1,
2195
+ relationship: nestedRel,
2196
+ parentRecord: child1,
2197
+ related: Related2,
2198
+ mode: nestedMode,
2199
+ chain: [{ slug, recordId: id, relationship: rel }],
2200
+ });
2201
+ if (Related2.model) {
2202
+ if (!form.getSave())
2203
+ form.save(modelSave(Related2.model));
2204
+ if (!form.getLoadRecord())
2205
+ form.loadRecord(modelLoadRecord(Related2));
2206
+ }
2207
+ // Polymorphic morph-column auto-injection mirrors the depth-1
2208
+ // create handler — uses Related1 (the leaf parent's owner) as
2209
+ // the morph source on the leaf relation.
2210
+ if (nestedMode === 'morphMany' && Related1.model) {
2211
+ const morphDesc = getMorphRelationDescriptor(Related1.model, nestedRel);
2212
+ if (!morphDesc) {
2213
+ res.status(500);
2214
+ const msg = `Nested manager ${N.name}: relations[${JSON.stringify(nestedRel)}] reports a polymorphic type but is missing morphName.`;
2215
+ return json ? res.json({ ok: false, error: msg }) : res.send(msg);
2216
+ }
2217
+ const morphPayload = computeMorphPayload(child1, morphDesc);
2218
+ const existing = form.getMutateDataBeforeCreate();
2219
+ form.mutateDataBeforeCreate(async (data, ctx) => {
2220
+ const next = existing ? await existing(data, ctx) : data;
2221
+ return { ...next, ...morphPayload };
2222
+ });
2223
+ }
2224
+ const formCtx = {
2225
+ values,
2226
+ basePath: base,
2227
+ parent: child1,
2228
+ parentId: childId1,
2229
+ relationship: nestedRel,
2230
+ };
2231
+ const result = await dispatchFormSubmit(form, values, formCtx);
2232
+ if (!result.ok) {
2233
+ if (json) {
2234
+ res.status(422);
2235
+ return res.json({ ok: false, errors: result.errors });
2236
+ }
2237
+ const data = await relationManagerData(pilotiq, {
2238
+ kind: 'nested-relation-create', slug,
2239
+ chain: buildChain(id, childId1),
2240
+ prefill: { values, errors: result.errors ?? {} },
2241
+ }, req);
2242
+ res.status(422);
2243
+ return view('pilotiq.nested-relation-create', data ?? {});
2244
+ }
2245
+ const redirect = normalizeRedirect(result.redirect, base) ?? listUrl;
2246
+ if (json) {
2247
+ return res.json({
2248
+ ok: true, redirect,
2249
+ ...(result.notifications && result.notifications.length > 0 ? { notifications: result.notifications } : {}),
2250
+ });
2251
+ }
2252
+ flashNotifications(req, result.notifications);
2253
+ return res.redirect(redirect, 303);
2254
+ // `createUrl` referenced above is intentionally unused on
2255
+ // success — kept for parity with the depth-1 path's prefill
2256
+ // re-render shape if a future caller wants to redirect to it.
2257
+ void createUrl;
2258
+ });
2259
+ // ── View ──
2260
+ router.get(`${nestedBase}/:childId2`, async (req, res) => {
2261
+ const json = wantsJson(req);
2262
+ const id = req.params['id'];
2263
+ const childId1 = req.params['childId'];
2264
+ const childId2 = req.params['childId2'];
2265
+ if (childId2 === 'create') {
2266
+ res.status(404);
2267
+ return res.send('Not found');
2268
+ }
2269
+ const data = await relationManagerData(pilotiq, {
2270
+ kind: 'nested-relation-view', slug,
2271
+ chain: buildChain(id, childId1),
2272
+ childId: childId2,
2273
+ }, req);
2274
+ if (data === null) {
2275
+ res.status(404);
2276
+ return res.send('Not found');
2277
+ }
2278
+ if ('ok' in data && data.ok === false)
2279
+ return forbidden(res, json);
2280
+ return view('pilotiq.nested-relation-view', data);
2281
+ });
2282
+ // ── Edit (GET) ──
2283
+ router.get(`${nestedBase}/:childId2/edit`, async (req, res) => {
2284
+ const json = wantsJson(req);
2285
+ const id = req.params['id'];
2286
+ const childId1 = req.params['childId'];
2287
+ const childId2 = req.params['childId2'];
2288
+ const data = await relationManagerData(pilotiq, {
2289
+ kind: 'nested-relation-edit', slug,
2290
+ chain: buildChain(id, childId1),
2291
+ childId: childId2,
2292
+ }, req);
2293
+ if (data === null) {
2294
+ res.status(404);
2295
+ return res.send('Not found');
2296
+ }
2297
+ if ('ok' in data && data.ok === false)
2298
+ return forbidden(res, json);
2299
+ return view('pilotiq.nested-relation-edit', data);
2300
+ });
2301
+ // ── Edit (POST) ──
2302
+ router.post(`${nestedBase}/:childId2/edit`, async (req, res) => {
2303
+ const json = wantsJson(req);
2304
+ const id = req.params['id'];
2305
+ const childId1 = req.params['childId'];
2306
+ const childId2 = req.params['childId2'];
2307
+ // Replay the chain to verify auth, IDOR, load child1+child2.
2308
+ const pre = await relationManagerData(pilotiq, {
2309
+ kind: 'nested-relation-edit', slug,
2310
+ chain: buildChain(id, childId1),
2311
+ childId: childId2,
2312
+ }, req);
2313
+ if (pre === null) {
2314
+ res.status(404);
2315
+ return res.send('Not found');
2316
+ }
2317
+ if ('ok' in pre && pre.ok === false)
2318
+ return forbidden(res, json);
2319
+ const Related1 = findRelatedResource(M, R, cfg);
2320
+ if (!Related1) {
2321
+ res.status(500);
2322
+ const msg = `Nested manager ${N.name}: cannot resolve middle Resource for edit`;
2323
+ return json ? res.json({ ok: false, error: msg }) : res.send(msg);
2324
+ }
2325
+ const Related2 = findRelatedResource(N, Related1, cfg);
2326
+ if (!Related2?.model) {
2327
+ res.status(500);
2328
+ const msg = `Nested manager ${N.name}: cannot resolve related Resource for edit`;
2329
+ return json ? res.json({ ok: false, error: msg }) : res.send(msg);
2330
+ }
2331
+ const user = await pilotiq.resolveUser(req);
2332
+ const child1 = await findRecord(Related1, childId1, { user }).catch(() => undefined);
2333
+ if (!child1) {
2334
+ res.status(404);
2335
+ return res.send('Not found');
2336
+ }
2337
+ const child2 = await findRecord(Related2, childId2, { user }).catch(() => undefined);
2338
+ if (!child2) {
2339
+ res.status(404);
2340
+ return res.send('Not found');
2341
+ }
2342
+ const body = await readFormBody(req);
2343
+ const { values } = splitMeta(body);
2344
+ const editUrl = `${nestedBase}/${childId2}/edit`.replace(':id', id).replace(':childId', childId1);
2345
+ const nestedMode = Related1.model
2346
+ ? normalizeRelationMode(getRelationType(Related1.model, nestedRel))
2347
+ : 'hasMany';
2348
+ const form = N.form(Form.make(), {
2349
+ basePath: base,
2350
+ parentSlug: slug,
2351
+ parentId: childId1,
2352
+ relationship: nestedRel,
2353
+ parentRecord: child1,
2354
+ related: Related2,
2355
+ mode: nestedMode,
2356
+ chain: [{ slug, recordId: id, relationship: rel }],
2357
+ });
2358
+ if (!form.getSave())
2359
+ form.save(modelSave(Related2.model));
2360
+ if (!form.getLoadRecord())
2361
+ form.loadRecord(modelLoadRecord(Related2));
2362
+ if (nestedMode === 'morphMany' && Related1.model) {
2363
+ const morphDesc = getMorphRelationDescriptor(Related1.model, nestedRel);
2364
+ if (morphDesc) {
2365
+ const morphPayload = computeMorphPayload(child1, morphDesc);
2366
+ const existing = form.getMutateDataBeforeUpdate();
2367
+ form.mutateDataBeforeUpdate(async (data, ctx) => {
2368
+ const next = existing ? await existing(data, ctx) : data;
2369
+ return { ...next, ...morphPayload };
2370
+ });
2371
+ }
2372
+ }
2373
+ const formCtx = {
2374
+ values,
2375
+ basePath: base,
2376
+ record: child2,
2377
+ parent: child1,
2378
+ parentId: childId1,
2379
+ relationship: nestedRel,
2380
+ };
2381
+ const result = await dispatchFormSubmit(form, values, formCtx);
2382
+ if (!result.ok) {
2383
+ if (json) {
2384
+ res.status(422);
2385
+ return res.json({ ok: false, errors: result.errors });
2386
+ }
2387
+ const data = await relationManagerData(pilotiq, {
2388
+ kind: 'nested-relation-edit', slug,
2389
+ chain: buildChain(id, childId1),
2390
+ childId: childId2,
2391
+ prefill: { values, errors: result.errors ?? {} },
2392
+ }, req);
2393
+ res.status(422);
2394
+ return view('pilotiq.nested-relation-edit', data ?? {});
2395
+ }
2396
+ const redirect = normalizeRedirect(result.redirect, base) ?? editUrl;
2397
+ if (json) {
2398
+ return res.json({
2399
+ ok: true, redirect,
2400
+ ...(result.notifications && result.notifications.length > 0 ? { notifications: result.notifications } : {}),
2401
+ });
2402
+ }
2403
+ flashNotifications(req, result.notifications);
2404
+ return res.redirect(redirect, 303);
2405
+ });
2406
+ // ── Delete ──
2407
+ router.post(`${nestedBase}/:childId2/delete`, async (req, res) => {
2408
+ const json = wantsJson(req);
2409
+ const id = req.params['id'];
2410
+ const childId1 = req.params['childId'];
2411
+ const childId2 = req.params['childId2'];
2412
+ // Replay the chain to verify auth + IDOR + load child2.
2413
+ // We piggy-back on the edit scope's checks (canEdit on the
2414
+ // leaf manager — same gate the depth-1 delete uses today via
2415
+ // the relation-edit scope).
2416
+ const pre = await relationManagerData(pilotiq, {
2417
+ kind: 'nested-relation-edit', slug,
2418
+ chain: buildChain(id, childId1),
2419
+ childId: childId2,
2420
+ }, req);
2421
+ if (pre === null) {
2422
+ res.status(404);
2423
+ return res.send('Not found');
2424
+ }
2425
+ if ('ok' in pre && pre.ok === false)
2426
+ return forbidden(res, json);
2427
+ const Related1 = findRelatedResource(M, R, cfg);
2428
+ if (!Related1) {
2429
+ res.status(500);
2430
+ const msg = `Nested manager ${N.name}: cannot resolve middle Resource for delete`;
2431
+ return json ? res.json({ ok: false, error: msg }) : res.send(msg);
2432
+ }
2433
+ const Related2 = findRelatedResource(N, Related1, cfg);
2434
+ if (!Related2?.model) {
2435
+ res.status(500);
2436
+ const msg = `Nested manager ${N.name}: cannot resolve related Resource for delete`;
2437
+ return json ? res.json({ ok: false, error: msg }) : res.send(msg);
2438
+ }
2439
+ const user = await pilotiq.resolveUser(req);
2440
+ const child1 = await findRecord(Related1, childId1, { user }).catch(() => undefined);
2441
+ if (!child1) {
2442
+ res.status(404);
2443
+ return res.send('Not found');
2444
+ }
2445
+ const child2 = await findRecord(Related2, childId2, { user }).catch(() => undefined);
2446
+ if (!child2) {
2447
+ res.status(404);
2448
+ return res.send('Not found');
2449
+ }
2450
+ if (!await safeManagerPolicy(N, 'canDelete', Related2, user, child1, child2))
2451
+ return forbidden(res, json);
2452
+ const listUrl = nestedBase.replace(':id', id).replace(':childId', childId1);
2453
+ try {
2454
+ await Related2.model.delete(childId2);
2455
+ }
2456
+ catch (err) {
2457
+ const message = err instanceof Error ? err.message : 'Delete failed';
2458
+ res.status(500);
2459
+ return json ? res.json({ ok: false, error: message }) : res.send(message);
2460
+ }
2461
+ if (json) {
2462
+ const notifications = [
2463
+ { id: `n-nrdelete-${childId2}-${Date.now()}`, type: 'success', title: `${N.getLabelSingular()} deleted` },
2464
+ ];
2465
+ return res.json({ ok: true, redirect: listUrl, notifications });
2466
+ }
2467
+ return res.redirect(listUrl, 303);
2468
+ });
2469
+ // ── Phase B follow-up — nested action / detach / soft-delete ──
2470
+ // Mirror the depth-1 manager surface (`_action`, `_detach`,
2471
+ // `restore`, `force-delete`) under the nested manager. Auth +
2472
+ // chain IDOR centralized in `resolveRelationChain`; each route
2473
+ // layers its own scope-specific gate (canDetach / canRestore /
2474
+ // canForceDelete; the action route mirrors depth-1 by not adding
2475
+ // an extra manager-level gate beyond the chain walk).
2476
+ const nestedChainSlug = slug;
2477
+ const requireNestedChain = async (req, res, json) => {
2478
+ const id = req.params['id'];
2479
+ const child1Id = req.params['childId'];
2480
+ const user = await pilotiq.resolveUser(req);
2481
+ const resolved = await resolveRelationChain(pilotiq, {
2482
+ kind: 'nested-relation-list',
2483
+ slug: nestedChainSlug,
2484
+ chain: [
2485
+ { recordId: id, relationship: rel },
2486
+ { recordId: child1Id, relationship: nestedRel },
2487
+ ],
2488
+ }, user);
2489
+ if (resolved === null) {
2490
+ res.status(404);
2491
+ res.send('Not found');
2492
+ return undefined;
2493
+ }
2494
+ if ('ok' in resolved) {
2495
+ forbidden(res, json);
2496
+ return undefined;
2497
+ }
2498
+ return { user, resolved, parentId: id, child1Id };
2499
+ };
2500
+ // Listing URL (filled per request — `:id` / `:childId` get baked
2501
+ // in once the params are known). All four routes redirect here
2502
+ // on success so users land back on the nested-relation list.
2503
+ const nestedListUrlFor = (id, child1Id) => nestedBase.replace(':id', id).replace(':childId', child1Id);
2504
+ // ── Action dispatch — POST ${nestedBase}/_action/:actionName ──
2505
+ // Resolves N's table elements, finds the named action, dispatches
2506
+ // it with `ctx.relation = { parent: child1, parentId, rel }` so
2507
+ // M2M handlers on the nested manager can call accessor methods.
2508
+ // Handler-style actions are useful on hasMany too — mounted
2509
+ // unconditionally.
2510
+ router.post(`${nestedBase}/_action/:actionName`, async (req, res) => {
2511
+ const json = wantsJson(req);
2512
+ const pre = await requireNestedChain(req, res, json);
2513
+ if (!pre)
2514
+ return;
2515
+ const { resolved } = pre;
2516
+ const { Related1, child1, M2, Related2, child2Mode } = resolved;
2517
+ const actionName = req.params['actionName'];
2518
+ const body = await readFormBody(req);
2519
+ const input = parseActionBody(body);
2520
+ // Manager ctx for N — same shape `nestedManagerCtx` builds for
2521
+ // the data-builder side, so factories that close over `ctx`
2522
+ // (URL templates, mode-aware visibility) see the same view as
2523
+ // at page render.
2524
+ const nestedManagerCtxObj = {
2525
+ basePath: base,
2526
+ parentSlug: resolved.R.getSlug(),
2527
+ parentId: pre.child1Id, // immediate parent of N = child1
2528
+ relationship: nestedRel,
2529
+ parentRecord: child1,
2530
+ related: Related2,
2531
+ mode: child2Mode,
2532
+ chain: [{
2533
+ slug: resolved.R.getSlug(),
2534
+ recordId: pre.parentId,
2535
+ relationship: rel,
2536
+ }],
2537
+ };
2538
+ const table = M2.table(Table.make(), nestedManagerCtxObj);
2539
+ const elements = [table];
2540
+ const listUrl = nestedListUrlFor(pre.parentId, pre.child1Id);
2541
+ tagActionDispatch(elements, listUrl);
2542
+ const target = resolveDispatchTarget(elements, actionName);
2543
+ if (!target) {
2544
+ if (json) {
2545
+ res.status(404);
2546
+ return res.json({ ok: false, error: `Action "${actionName}" not found` });
2547
+ }
2548
+ res.status(404);
2549
+ return res.send(`Action "${actionName}" not found on ${M2.name}`);
2550
+ }
2551
+ const resolveRecord = Related2?.model
2552
+ ? (id) => Related2.model.find(id)
2553
+ : undefined;
2554
+ const result = await dispatchAction(target.action, {
2555
+ ...input,
2556
+ request: req,
2557
+ user: pre.user,
2558
+ relation: { parent: child1, parentId: pre.child1Id, relationship: nestedRel },
2559
+ ...(target.rowField ? { rowField: target.rowField } : {}),
2560
+ ...(target.formSchema ? { formSchema: target.formSchema } : {}),
2561
+ }, resolveRecord);
2562
+ if (!result.ok) {
2563
+ if (json) {
2564
+ res.status(result.errors ? 422 : 500);
2565
+ return res.json({ ok: false, error: result.error, ...(result.errors ? { errors: result.errors } : {}) });
2566
+ }
2567
+ res.status(500);
2568
+ return res.send(result.error);
2569
+ }
2570
+ const redirect = normalizeRedirect(result.redirect, base) ?? listUrl;
2571
+ if (json) {
2572
+ return res.json({
2573
+ ok: true,
2574
+ redirect,
2575
+ ...(result.notifications ? { notifications: result.notifications } : {}),
2576
+ });
2577
+ }
2578
+ flashNotifications(req, result.notifications);
2579
+ return res.redirect(redirect, 303);
2580
+ });
2581
+ // ── Detach — POST ${nestedBase}/:childId2/_detach ──
2582
+ // M2M-only direct row-detach. IDOR-checks the grandchild against
2583
+ // child1.related(nestedRel), then calls accessor.detach. Mirrors
2584
+ // the depth-1 detach route at line 1955.
2585
+ router.post(`${nestedBase}/:childId2/_detach`, async (req, res) => {
2586
+ const json = wantsJson(req);
2587
+ const pre = await requireNestedChain(req, res, json);
2588
+ if (!pre)
2589
+ return;
2590
+ const childId2 = req.params['childId2'];
2591
+ const { resolved } = pre;
2592
+ const { Related1, child1, M2, Related2, child2Mode } = resolved;
2593
+ if (child2Mode !== 'belongsToMany' && child2Mode !== 'morphToMany' && child2Mode !== 'morphedByMany') {
2594
+ res.status(404);
2595
+ const msg = 'Detach is only supported on M2M relations (belongsToMany, morphToMany, morphedByMany)';
2596
+ return json ? res.json({ ok: false, error: msg }) : res.send(msg);
2597
+ }
2598
+ // IDOR: confirm child2 is currently attached to child1 under
2599
+ // nestedRel. Read-side accessor (`child1.related(nestedRel)`)
2600
+ // returns a deferred QueryBuilder; we never bypass it.
2601
+ const readSide = child1
2602
+ ?.related?.(nestedRel);
2603
+ if (!readSide) {
2604
+ res.status(500);
2605
+ const msg = `child1.related("${nestedRel}") missing — wrong relation type or ORM version?`;
2606
+ return json ? res.json({ ok: false, error: msg }) : res.send(msg);
2607
+ }
2608
+ let child2 = undefined;
2609
+ try {
2610
+ if (typeof readSide.paginate === 'function') {
2611
+ const pk = Related2?.model ? getPrimaryKey(Related2.model) : 'id';
2612
+ const out = await readSide.where(pk, '=', childId2).paginate(1, 1);
2613
+ child2 = Array.isArray(out.data) ? out.data[0] : undefined;
2614
+ }
2615
+ }
2616
+ catch { /* fall through */ }
2617
+ if (child2 === undefined) {
2618
+ res.status(404);
2619
+ return res.send('Not found');
2620
+ }
2621
+ if (!await safeManagerPolicy(M2, 'canDetach', Related2, pre.user, child1, child2))
2622
+ return forbidden(res, json);
2623
+ // Real ORM: child1[nestedRel]() returns the pivot accessor
2624
+ // with attach/detach/sync. Test stubs may collapse onto
2625
+ // `child1.related(nestedRel)` — try both.
2626
+ let writeAccessor;
2627
+ const inst = child1[nestedRel];
2628
+ if (typeof inst === 'function') {
2629
+ try {
2630
+ const out = inst.call(child1);
2631
+ if (out && typeof out.detach === 'function')
2632
+ writeAccessor = out;
2633
+ }
2634
+ catch { /* fall through */ }
2635
+ }
2636
+ if (!writeAccessor && typeof readSide.detach === 'function') {
2637
+ writeAccessor = readSide;
2638
+ }
2639
+ if (!writeAccessor) {
2640
+ res.status(500);
2641
+ const msg = `Pivot accessor missing on ${nestedRel} — wrong relation type or ORM version?`;
2642
+ return json ? res.json({ ok: false, error: msg }) : res.send(msg);
2643
+ }
2644
+ try {
2645
+ await writeAccessor.detach([childId2]);
2646
+ }
2647
+ catch (err) {
2648
+ const message = err instanceof Error ? err.message : 'Detach failed';
2649
+ res.status(500);
2650
+ return json ? res.json({ ok: false, error: message }) : res.send(message);
2651
+ }
2652
+ const listUrl = nestedListUrlFor(pre.parentId, pre.child1Id);
2653
+ if (json) {
2654
+ const notifications = [
2655
+ { id: `n-nrdetach-${childId2}-${Date.now()}`, type: 'success', title: `${M2.getLabelSingular()} detached` },
2656
+ ];
2657
+ return res.json({ ok: true, redirect: listUrl, notifications });
2658
+ }
2659
+ return res.redirect(listUrl, 303);
2660
+ });
2661
+ // ── Soft-delete: restore + force-delete ───────────────────────
2662
+ // Opt in only when Related2 has `softDeletes = true` AND its
2663
+ // model carries `restore` / `forceDelete`. Mirrors the depth-1
2664
+ // routes at line 1804+. IDOR runs against child1.related(nestedRel)
2665
+ // broadened with `withTrashed()` so trashed grandchildren resolve.
2666
+ const Related1ForSoft = findRelatedResource(M, R, cfg);
2667
+ const Related2ForSoft = Related1ForSoft ? findRelatedResource(N, Related1ForSoft, cfg) : undefined;
2668
+ if (Related2ForSoft?.softDeletes) {
2669
+ const RM2 = Related2ForSoft.model;
2670
+ if (!RM2) {
2671
+ throw new Error(`[Pilotiq] Nested RelationManager ${N.name} on ${M.name} (${R.name}): related Resource ${Related2ForSoft.name} has softDeletes = true but no model. ` +
2672
+ `Wire one up or unset softDeletes.`);
2673
+ }
2674
+ if (typeof RM2.restore !== 'function' || typeof RM2.forceDelete !== 'function') {
2675
+ throw new Error(`[Pilotiq] Nested RelationManager ${N.name} on ${M.name} (${R.name}): related Resource ${Related2ForSoft.name} has softDeletes = true but model.restore / model.forceDelete are missing. ` +
2676
+ `Set Model.softDeletes = true on the rudder side, or upgrade @rudderjs/orm.`);
2677
+ }
2678
+ // Like the depth-1 helper: load the grandchild via the parent's
2679
+ // relation query, broadened with `withTrashed()`. Returns
2680
+ // undefined when the lookup misses or the grandchild doesn't
2681
+ // belong to child1 under nestedRel.
2682
+ const loadTrashableGrandchild = async (parentChild, child2Id) => {
2683
+ const pk = (RM2.primaryKey ?? 'id');
2684
+ try {
2685
+ const q = parentChild.related(nestedRel);
2686
+ const broadened = typeof q.withTrashed === 'function' ? q.withTrashed() : q;
2687
+ const result = await broadened.where(pk, '=', child2Id).paginate(1, 1);
2688
+ return Array.isArray(result.data) ? result.data[0] : undefined;
2689
+ }
2690
+ catch {
2691
+ return undefined;
2692
+ }
2693
+ };
2694
+ // Restore — POST ${nestedBase}/:childId2/restore
2695
+ router.post(`${nestedBase}/:childId2/restore`, async (req, res) => {
2696
+ const json = wantsJson(req);
2697
+ const pre = await requireNestedChain(req, res, json);
2698
+ if (!pre)
2699
+ return;
2700
+ const childId2 = req.params['childId2'];
2701
+ const child2 = await loadTrashableGrandchild(pre.resolved.child1, childId2);
2702
+ if (!child2) {
2703
+ res.status(404);
2704
+ return res.send('Not found');
2705
+ }
2706
+ if (!await safeManagerPolicy(N, 'canRestore', Related2ForSoft, pre.user, pre.resolved.child1, child2))
2707
+ return forbidden(res, json);
2708
+ const listUrl = nestedListUrlFor(pre.parentId, pre.child1Id);
2709
+ try {
2710
+ await RM2.restore(childId2);
2711
+ }
2712
+ catch (err) {
2713
+ const message = err instanceof Error ? err.message : 'Restore failed';
2714
+ res.status(500);
2715
+ return json ? res.json({ ok: false, error: message }) : res.send(message);
2716
+ }
2717
+ if (json) {
2718
+ const notifications = [
2719
+ { id: `n-nrrestore-${childId2}-${Date.now()}`, type: 'success', title: `${N.getLabelSingular()} restored` },
2720
+ ];
2721
+ return res.json({ ok: true, redirect: listUrl, notifications });
2722
+ }
2723
+ return res.redirect(listUrl, 303);
2724
+ });
2725
+ // Force-delete — POST ${nestedBase}/:childId2/force-delete
2726
+ router.post(`${nestedBase}/:childId2/force-delete`, async (req, res) => {
2727
+ const json = wantsJson(req);
2728
+ const pre = await requireNestedChain(req, res, json);
2729
+ if (!pre)
2730
+ return;
2731
+ const childId2 = req.params['childId2'];
2732
+ const child2 = await loadTrashableGrandchild(pre.resolved.child1, childId2);
2733
+ if (!child2) {
2734
+ res.status(404);
2735
+ return res.send('Not found');
2736
+ }
2737
+ if (!await safeManagerPolicy(N, 'canForceDelete', Related2ForSoft, pre.user, pre.resolved.child1, child2))
2738
+ return forbidden(res, json);
2739
+ const listUrl = nestedListUrlFor(pre.parentId, pre.child1Id);
2740
+ try {
2741
+ await RM2.forceDelete(childId2);
2742
+ }
2743
+ catch (err) {
2744
+ const message = err instanceof Error ? err.message : 'Force-delete failed';
2745
+ res.status(500);
2746
+ return json ? res.json({ ok: false, error: message }) : res.send(message);
2747
+ }
2748
+ if (json) {
2749
+ const notifications = [
2750
+ { id: `n-nrforce-${childId2}-${Date.now()}`, type: 'success', title: `${N.getLabelSingular()} permanently deleted` },
2751
+ ];
2752
+ return res.json({ ok: true, redirect: listUrl, notifications });
2753
+ }
2754
+ return res.redirect(listUrl, 303);
2755
+ });
2756
+ }
2757
+ }
2758
+ }
2759
+ }
2760
+ // ── Globals (singletons — 2-segment, no /:id) ────────
2761
+ for (const G of cfg.globals) {
2762
+ const slug = G.getSlug();
2763
+ const editUrl = globalBasePath(base, G);
2764
+ const pages = G.resolvePages();
2765
+ if (pages.edit) {
2766
+ const PageClass = pages.edit;
2767
+ // Plan #5 partial-resolve endpoint for the global's edit form.
2768
+ // POST ${editUrl}/_form/:formId/state
2769
+ router.post(`${editUrl}/_form/:formId/state`, async (req, res) => {
2770
+ const user = await pilotiq.resolveUser(req);
2771
+ if (!await policyAccess(G, user))
2772
+ return forbidden(res, true);
2773
+ if (!await checkPolicy(() => G.canEdit(user, undefined)))
2774
+ return forbidden(res, true);
2775
+ const formId = req.params['formId'];
2776
+ return handleFormState(req, res, pilotiq, { kind: 'global-edit', slug }, formId);
2777
+ });
2778
+ // Plan #8 wizard step-validate endpoint for the global's edit form.
2779
+ router.post(`${editUrl}/_form/:formId/wizard`, async (req, res) => {
2780
+ const user = await pilotiq.resolveUser(req);
2781
+ if (!await policyAccess(G, user))
2782
+ return forbidden(res, true);
2783
+ if (!await checkPolicy(() => G.canEdit(user, undefined)))
2784
+ return forbidden(res, true);
2785
+ const formId = req.params['formId'];
2786
+ return handleFormWizard(req, res, pilotiq, { kind: 'global-edit', slug }, formId);
2787
+ });
2788
+ // Async-mention endpoint for the global's edit form.
2789
+ router.post(`${editUrl}/_form/:formId/mentions`, async (req, res) => {
2790
+ const user = await pilotiq.resolveUser(req);
2791
+ if (!await policyAccess(G, user))
2792
+ return forbidden(res, true);
2793
+ if (!await checkPolicy(() => G.canEdit(user, undefined)))
2794
+ return forbidden(res, true);
2795
+ const formId = req.params['formId'];
2796
+ return handleFormMentions(req, res, pilotiq, { kind: 'global-edit', slug }, formId);
2797
+ });
2798
+ // SelectField inline-create modal endpoint for the global's edit form.
2799
+ router.post(`${editUrl}/_form/:formId/create-option/:fieldName`, async (req, res) => {
2800
+ const user = await pilotiq.resolveUser(req);
2801
+ if (!await policyAccess(G, user))
2802
+ return forbidden(res, true);
2803
+ if (!await checkPolicy(() => G.canEdit(user, undefined)))
2804
+ return forbidden(res, true);
2805
+ const formId = req.params['formId'];
2806
+ const fieldName = req.params['fieldName'];
2807
+ return handleFormCreateOption(req, res, pilotiq, { kind: 'global-edit', slug }, formId, fieldName);
2808
+ });
2809
+ router.get(editUrl, async (req, res) => {
2810
+ const user = await pilotiq.resolveUser(req);
2811
+ if (!await policyAccess(G, user))
2812
+ return forbidden(res, wantsJson(req));
2813
+ // Globals carry their record on the singleton form's `loadRecord`;
2814
+ // we don't pre-load here — pass a stub so canEdit's signature is
2815
+ // honored, and let user code decide whether to consult it.
2816
+ if (!await checkPolicy(() => G.canEdit(user, undefined)))
2817
+ return forbidden(res, wantsJson(req));
2818
+ const data = await globalEditData(pilotiq, slug, undefined, req);
2819
+ return view('pilotiq.slug', data ?? {});
2820
+ });
2821
+ router.post(editUrl, async (req, res) => {
2822
+ const body = await readFormBody(req);
2823
+ const { values, formId } = splitMeta(body);
2824
+ const json = wantsJson(req);
2825
+ const user = await pilotiq.resolveUser(req);
2826
+ if (!await policyAccess(G, user))
2827
+ return forbidden(res, json);
2828
+ if (!await checkPolicy(() => G.canEdit(user, undefined)))
2829
+ return forbidden(res, json);
2830
+ const ctx = { mode: 'edit', basePath: base, ...(user !== null ? { user: user } : {}) };
2831
+ const elements = await callPageSchema(PageClass, ctx);
2832
+ tagFormActions(elements, editUrl);
2833
+ const form = selectForm(findForms(elements), formId);
2834
+ if (!form) {
2835
+ if (json) {
2836
+ res.status(404);
2837
+ return res.json({ ok: false, error: 'No form found on page' });
2838
+ }
2839
+ res.status(404);
2840
+ return res.send('No form found on page');
2841
+ }
2842
+ // Provide the existing singleton record to the lifecycle context
2843
+ // so cross-field validators / mutateData see prior state.
2844
+ let record = undefined;
2845
+ if (form.getLoadRecord()) {
2846
+ try {
2847
+ record = await form.getLoadRecord()('', { values });
2848
+ }
2849
+ catch { /* ignore */ }
2850
+ }
2851
+ const result = await dispatchFormSubmit(form, values, record !== undefined ? { values, record, basePath: base } : { values, basePath: base });
2852
+ if (!result.ok) {
2853
+ if (json) {
2854
+ res.status(422);
2855
+ return res.json({ ok: false, errors: result.errors });
2856
+ }
2857
+ const data = await globalEditData(pilotiq, slug, { values, errors: result.errors });
2858
+ res.status(422);
2859
+ return view('pilotiq.slug', data ?? {});
2860
+ }
2861
+ const redirect = normalizeRedirect(result.redirect, base) ?? editUrl;
2862
+ if (json) {
2863
+ return res.json({
2864
+ ok: true,
2865
+ redirect,
2866
+ ...(result.notifications && result.notifications.length > 0 ? { notifications: result.notifications } : {}),
2867
+ });
2868
+ }
2869
+ flashNotifications(req, result.notifications);
2870
+ return res.redirect(redirect, 303);
2871
+ });
2872
+ }
2873
+ // Optional view page when the user opts in via pages().view
2874
+ if (pages.view) {
2875
+ router.get(`${editUrl}/view`, async (req, res) => {
2876
+ const user = await pilotiq.resolveUser(req);
2877
+ if (!await policyAccess(G, user))
2878
+ return forbidden(res, wantsJson(req));
2879
+ if (!await checkPolicy(() => G.canView(user, undefined)))
2880
+ return forbidden(res, wantsJson(req));
2881
+ const data = await globalViewData(pilotiq, slug, req);
2882
+ return view('pilotiq.resource-view', data ?? {});
2883
+ });
2884
+ }
2885
+ }
2886
+ // ── Custom pages (2-segment, slug route) ──────────────
2887
+ for (const PageClass of cfg.pages) {
2888
+ // Plan #15 — the dashboard page lives at `${base}` (handled by the
2889
+ // dashboard route above), so skip it here to avoid registering a
2890
+ // duplicate `${pageUrl}` route or a broken `${base}/` (when
2891
+ // `slug = ''`).
2892
+ if (cfg.dashboardPage === PageClass)
2893
+ continue;
2894
+ const pageSlug = PageClass.getSlug();
2895
+ const pageUrl = pageBasePath(base, PageClass);
2896
+ // Plan #15 — per-page widget polling endpoint. Mirrors the
2897
+ // panel-scope `${base}/_widget/:id` but resolves the custom page's
2898
+ // schema instead of the dashboard's.
2899
+ router.post(`${pageUrl}/_widget/:id`, async (req, res) => {
2900
+ const user = await pilotiq.resolveUser(req);
2901
+ if (!await policyAccess(PageClass, user))
2902
+ return forbidden(res, true);
2903
+ return handleWidgetData(req, res, pilotiq, { kind: 'page', pageSlug }, req.params['id']);
2904
+ });
2905
+ // Plan #5 partial-resolve endpoint for custom pages with reactive forms.
2906
+ // POST ${base}/${pageSlug}/_form/:formId/state
2907
+ router.post(`${pageUrl}/_form/:formId/state`, async (req, res) => {
2908
+ const user = await pilotiq.resolveUser(req);
2909
+ if (!await policyAccess(PageClass, user))
2910
+ return forbidden(res, true);
2911
+ const formId = req.params['formId'];
2912
+ return handleFormState(req, res, pilotiq, { kind: 'page', pageSlug }, formId);
2913
+ });
2914
+ // Plan #8 wizard step-validate endpoint for custom pages.
2915
+ router.post(`${pageUrl}/_form/:formId/wizard`, async (req, res) => {
2916
+ const user = await pilotiq.resolveUser(req);
2917
+ if (!await policyAccess(PageClass, user))
2918
+ return forbidden(res, true);
2919
+ const formId = req.params['formId'];
2920
+ return handleFormWizard(req, res, pilotiq, { kind: 'page', pageSlug }, formId);
2921
+ });
2922
+ // Async-mention endpoint for custom pages.
2923
+ router.post(`${pageUrl}/_form/:formId/mentions`, async (req, res) => {
2924
+ const user = await pilotiq.resolveUser(req);
2925
+ if (!await policyAccess(PageClass, user))
2926
+ return forbidden(res, true);
2927
+ const formId = req.params['formId'];
2928
+ return handleFormMentions(req, res, pilotiq, { kind: 'page', pageSlug }, formId);
2929
+ });
2930
+ // SelectField inline-create modal endpoint for custom pages.
2931
+ router.post(`${pageUrl}/_form/:formId/create-option/:fieldName`, async (req, res) => {
2932
+ const user = await pilotiq.resolveUser(req);
2933
+ if (!await policyAccess(PageClass, user))
2934
+ return forbidden(res, true);
2935
+ const formId = req.params['formId'];
2936
+ const fieldName = req.params['fieldName'];
2937
+ return handleFormCreateOption(req, res, pilotiq, { kind: 'page', pageSlug }, formId, fieldName);
2938
+ });
2939
+ router.get(pageUrl, async (req, res) => {
2940
+ const user = await pilotiq.resolveUser(req);
2941
+ if (!await policyAccess(PageClass, user))
2942
+ return forbidden(res, wantsJson(req));
2943
+ const data = await customPageData(pilotiq, pageSlug, req);
2944
+ return view('pilotiq.slug', data ?? {});
2945
+ });
2946
+ // Action dispatch — POST ${base}/${pageSlug}/_action/:actionName
2947
+ router.post(`${pageUrl}/_action/:actionName`, async (req, res) => {
2948
+ const user = await pilotiq.resolveUser(req);
2949
+ if (!await policyAccess(PageClass, user))
2950
+ return forbidden(res, wantsJson(req));
2951
+ const actionName = req.params['actionName'];
2952
+ const json = wantsJson(req);
2953
+ const body = await readFormBody(req);
2954
+ const input = parseActionBody(body);
2955
+ const ctx = user !== null ? { user: user } : {};
2956
+ const elements = await callPageSchema(PageClass, ctx);
2957
+ tagActionDispatch(elements, pageUrl);
2958
+ const target = resolveDispatchTarget(elements, actionName);
2959
+ if (!target) {
2960
+ if (json) {
2961
+ res.status(404);
2962
+ return res.json({ ok: false, error: `Action "${actionName}" not found` });
2963
+ }
2964
+ res.status(404);
2965
+ return res.send(`Action "${actionName}" not found on page`);
2966
+ }
2967
+ const result = await dispatchAction(target.action, {
2968
+ ...input,
2969
+ request: req,
2970
+ user,
2971
+ ...(target.rowField ? { rowField: target.rowField } : {}),
2972
+ ...(target.formSchema ? { formSchema: target.formSchema } : {}),
2973
+ });
2974
+ if (!result.ok) {
2975
+ if (json) {
2976
+ res.status(result.errors ? 422 : 500);
2977
+ return res.json({ ok: false, error: result.error, ...(result.errors ? { errors: result.errors } : {}) });
2978
+ }
2979
+ res.status(500);
2980
+ return res.send(result.error);
2981
+ }
2982
+ if (result.download)
2983
+ return sendDownload(res, result.download);
2984
+ const redirect = normalizeRedirect(result.redirect, base) ?? pageUrl;
2985
+ if (json) {
2986
+ return res.json({
2987
+ ok: true,
2988
+ redirect,
2989
+ ...(result.notifications ? { notifications: result.notifications } : {}),
2990
+ });
2991
+ }
2992
+ flashNotifications(req, result.notifications);
2993
+ return res.redirect(redirect, 303);
2994
+ });
2995
+ // Custom pages can also accept submits when their schema includes a Form.
2996
+ router.post(pageUrl, async (req, res) => {
2997
+ const body = await readFormBody(req);
2998
+ const { values, formId } = splitMeta(body);
2999
+ const json = wantsJson(req);
3000
+ const user = await pilotiq.resolveUser(req);
3001
+ if (!await policyAccess(PageClass, user))
3002
+ return forbidden(res, json);
3003
+ const ctx = user !== null ? { user: user } : {};
3004
+ const elements = await callPageSchema(PageClass, ctx);
3005
+ tagFormActions(elements, pageUrl);
3006
+ const form = selectForm(findForms(elements), formId);
3007
+ if (!form) {
3008
+ if (json) {
3009
+ res.status(404);
3010
+ return res.json({ ok: false, error: 'No form found on page' });
3011
+ }
3012
+ res.status(404);
3013
+ return res.send('No form found on page');
3014
+ }
3015
+ const result = await dispatchFormSubmit(form, values, { values, basePath: base });
3016
+ if (!result.ok) {
3017
+ if (json) {
3018
+ res.status(422);
3019
+ return res.json({ ok: false, errors: result.errors });
3020
+ }
3021
+ form.withValues(values).withErrors(result.errors);
3022
+ const schemaData = await resolveSchema(elements, ctx);
3023
+ res.status(422);
3024
+ return view('pilotiq.slug', {
3025
+ pageType: 'page',
3026
+ panel: await panelInfo(pilotiq, req),
3027
+ page: PageClass.toMeta(),
3028
+ schemaData,
3029
+ basePath: base,
3030
+ layout: cfg.layout,
3031
+ hasErrors: true,
3032
+ });
3033
+ }
3034
+ const redirect = normalizeRedirect(result.redirect, base) ?? pageUrl;
3035
+ if (json) {
3036
+ return res.json({
3037
+ ok: true,
3038
+ redirect,
3039
+ ...(result.notifications && result.notifications.length > 0 ? { notifications: result.notifications } : {}),
3040
+ });
3041
+ }
3042
+ flashNotifications(req, result.notifications);
3043
+ return res.redirect(redirect, 303);
3044
+ });
3045
+ }
3046
+ // ── Theme editor ──────────────────────────────────────
3047
+ if (cfg.themeEditor) {
3048
+ router.get(`${base}/theme`, async (req) => {
3049
+ return view('pilotiq.theme', {
3050
+ panel: await panelInfo(pilotiq, req),
3051
+ basePath: base,
3052
+ layout: cfg.layout,
3053
+ themeConfig: pilotiq.getMergedTheme() ?? {},
3054
+ });
3055
+ });
3056
+ router.get(`${base}/api/_theme`, async (_req, res) => {
3057
+ let overrides = null;
3058
+ try {
3059
+ const { app } = await import(/* @vite-ignore */ '@rudderjs/core');
3060
+ const prisma = app().make('prisma');
3061
+ const slug = `${cfg.name}__theme`;
3062
+ const row = await prisma.panelGlobal.findUnique({ where: { slug } });
3063
+ if (row?.data) {
3064
+ const raw = typeof row.data === 'string' ? JSON.parse(row.data) : row.data;
3065
+ overrides = migrateThemeOverrides(raw);
3066
+ }
3067
+ }
3068
+ catch { /* no DB or no table — that's fine */ }
3069
+ return res.json({
3070
+ config: cfg.theme ?? {},
3071
+ overrides: overrides ?? {},
3072
+ options: {
3073
+ presets: Object.keys(presets),
3074
+ baseColors: Object.keys(baseColors),
3075
+ themeColors: ['base', ...HUE_NAMES],
3076
+ chartColors: ['base', ...HUE_NAMES],
3077
+ radii: Object.keys(radiusMap),
3078
+ iconLibraries: ['lucide', 'tabler', 'phosphor', 'remix'],
3079
+ },
3080
+ });
3081
+ });
3082
+ router.put(`${base}/api/_theme`, async (req, res) => {
3083
+ try {
3084
+ const overrides = req.body;
3085
+ const { app } = await import(/* @vite-ignore */ '@rudderjs/core');
3086
+ const prisma = app().make('prisma');
3087
+ const slug = `${cfg.name}__theme`;
3088
+ await prisma.panelGlobal.upsert({
3089
+ where: { slug },
3090
+ update: { data: JSON.stringify(overrides) },
3091
+ create: { slug, data: JSON.stringify(overrides) },
3092
+ });
3093
+ pilotiq.setThemeOverrides(overrides);
3094
+ return res.json({ ok: true });
3095
+ }
3096
+ catch (e) {
3097
+ return res.status(500).json({ message: e instanceof Error ? e.message : 'Failed to save theme' });
3098
+ }
3099
+ });
3100
+ router.delete(`${base}/api/_theme`, async (_req, res) => {
3101
+ try {
3102
+ const { app } = await import(/* @vite-ignore */ '@rudderjs/core');
3103
+ const prisma = app().make('prisma');
3104
+ const slug = `${cfg.name}__theme`;
3105
+ await prisma.panelGlobal.delete({ where: { slug } }).catch(() => { });
3106
+ pilotiq.setThemeOverrides(undefined);
3107
+ }
3108
+ catch { /* ignore */ }
3109
+ return res.json({ ok: true });
3110
+ });
3111
+ }
3112
+ }
3113
+ // ─── Lifecycle helpers exported for tests ────────────────
3114
+ export { dispatchFormSubmit, findForms, selectForm };
3115
+ export { loadTableRecords, parseTableQuery, findTables } from './elements/dispatchTable.js';
3116
+ //# sourceMappingURL=routes.js.map