@pilotiq/pilotiq 0.7.2 → 0.8.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 (367) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/CHANGELOG.md +142 -0
  3. package/CLAUDE.md +59 -3
  4. package/dist/Pilotiq.d.ts +83 -0
  5. package/dist/Pilotiq.d.ts.map +1 -1
  6. package/dist/Pilotiq.js +39 -0
  7. package/dist/Pilotiq.js.map +1 -1
  8. package/dist/actions/Action.d.ts +27 -99
  9. package/dist/actions/Action.d.ts.map +1 -1
  10. package/dist/actions/Action.js +52 -754
  11. package/dist/actions/Action.js.map +1 -1
  12. package/dist/actions/bulkFactories.d.ts +46 -0
  13. package/dist/actions/bulkFactories.d.ts.map +1 -0
  14. package/dist/actions/bulkFactories.js +144 -0
  15. package/dist/actions/bulkFactories.js.map +1 -0
  16. package/dist/actions/crudFactories.d.ts +94 -0
  17. package/dist/actions/crudFactories.d.ts.map +1 -0
  18. package/dist/actions/crudFactories.js +209 -0
  19. package/dist/actions/crudFactories.js.map +1 -0
  20. package/dist/actions/factoryHelpers.d.ts +108 -0
  21. package/dist/actions/factoryHelpers.d.ts.map +1 -0
  22. package/dist/actions/factoryHelpers.js +138 -0
  23. package/dist/actions/factoryHelpers.js.map +1 -0
  24. package/dist/actions/m2mFactories.d.ts +47 -0
  25. package/dist/actions/m2mFactories.d.ts.map +1 -0
  26. package/dist/actions/m2mFactories.js +173 -0
  27. package/dist/actions/m2mFactories.js.map +1 -0
  28. package/dist/actions/relationFactories.d.ts +93 -0
  29. package/dist/actions/relationFactories.d.ts.map +1 -0
  30. package/dist/actions/relationFactories.js +321 -0
  31. package/dist/actions/relationFactories.js.map +1 -0
  32. package/dist/elements/dispatchForm.js +1 -1
  33. package/dist/elements/dispatchForm.js.map +1 -1
  34. package/dist/elements/dispatchTable.js +1 -1
  35. package/dist/elements/dispatchTable.js.map +1 -1
  36. package/dist/fields/Field.d.ts +31 -0
  37. package/dist/fields/Field.d.ts.map +1 -1
  38. package/dist/fields/Field.js +25 -0
  39. package/dist/fields/Field.js.map +1 -1
  40. package/dist/pageData/breadcrumbs.d.ts +42 -0
  41. package/dist/pageData/breadcrumbs.d.ts.map +1 -0
  42. package/dist/pageData/breadcrumbs.js +172 -0
  43. package/dist/pageData/breadcrumbs.js.map +1 -0
  44. package/dist/pageData/forms.d.ts +137 -0
  45. package/dist/pageData/forms.d.ts.map +1 -0
  46. package/dist/pageData/forms.js +427 -0
  47. package/dist/pageData/forms.js.map +1 -0
  48. package/dist/pageData/helpers.d.ts +239 -0
  49. package/dist/pageData/helpers.d.ts.map +1 -0
  50. package/dist/pageData/helpers.js +703 -0
  51. package/dist/pageData/helpers.js.map +1 -0
  52. package/dist/pageData/misc.d.ts +76 -0
  53. package/dist/pageData/misc.d.ts.map +1 -0
  54. package/dist/pageData/misc.js +263 -0
  55. package/dist/pageData/misc.js.map +1 -0
  56. package/dist/pageData/navigation.d.ts +292 -0
  57. package/dist/pageData/navigation.d.ts.map +1 -0
  58. package/dist/pageData/navigation.js +591 -0
  59. package/dist/pageData/navigation.js.map +1 -0
  60. package/dist/pageData/relationPages.d.ts +172 -0
  61. package/dist/pageData/relationPages.d.ts.map +1 -0
  62. package/dist/pageData/relationPages.js +867 -0
  63. package/dist/pageData/relationPages.js.map +1 -0
  64. package/dist/pageData/relationTabs.d.ts +65 -0
  65. package/dist/pageData/relationTabs.d.ts.map +1 -0
  66. package/dist/pageData/relationTabs.js +258 -0
  67. package/dist/pageData/relationTabs.js.map +1 -0
  68. package/dist/pageData/resourcePages.d.ts +48 -0
  69. package/dist/pageData/resourcePages.d.ts.map +1 -0
  70. package/dist/pageData/resourcePages.js +504 -0
  71. package/dist/pageData/resourcePages.js.map +1 -0
  72. package/dist/pageData.d.ts +12 -792
  73. package/dist/pageData.d.ts.map +1 -1
  74. package/dist/pageData.js +24 -3797
  75. package/dist/pageData.js.map +1 -1
  76. package/dist/react/AppShell.d.ts +8 -0
  77. package/dist/react/AppShell.d.ts.map +1 -1
  78. package/dist/react/AppShell.js +11 -1
  79. package/dist/react/AppShell.js.map +1 -1
  80. package/dist/react/CollabExtensionFactoryRegistry.d.ts +47 -0
  81. package/dist/react/CollabExtensionFactoryRegistry.d.ts.map +1 -0
  82. package/dist/react/CollabExtensionFactoryRegistry.js +14 -0
  83. package/dist/react/CollabExtensionFactoryRegistry.js.map +1 -0
  84. package/dist/react/CollabRoomContext.d.ts +37 -0
  85. package/dist/react/CollabRoomContext.d.ts.map +1 -0
  86. package/dist/react/CollabRoomContext.js +12 -0
  87. package/dist/react/CollabRoomContext.js.map +1 -0
  88. package/dist/react/FormCollabBindingRegistry.d.ts +62 -0
  89. package/dist/react/FormCollabBindingRegistry.d.ts.map +1 -0
  90. package/dist/react/FormCollabBindingRegistry.js +14 -0
  91. package/dist/react/FormCollabBindingRegistry.js.map +1 -0
  92. package/dist/react/RecordWrapperGate.d.ts +25 -0
  93. package/dist/react/RecordWrapperGate.d.ts.map +1 -0
  94. package/dist/react/RecordWrapperGate.js +30 -0
  95. package/dist/react/RecordWrapperGate.js.map +1 -0
  96. package/dist/react/RecordWrapperRegistry.d.ts +31 -0
  97. package/dist/react/RecordWrapperRegistry.d.ts.map +1 -0
  98. package/dist/react/RecordWrapperRegistry.js +15 -0
  99. package/dist/react/RecordWrapperRegistry.js.map +1 -0
  100. package/dist/react/SchemaRenderer.d.ts +17 -23
  101. package/dist/react/SchemaRenderer.d.ts.map +1 -1
  102. package/dist/react/SchemaRenderer.js +71 -3647
  103. package/dist/react/SchemaRenderer.js.map +1 -1
  104. package/dist/react/component-slots.d.ts +103 -0
  105. package/dist/react/component-slots.d.ts.map +1 -0
  106. package/dist/react/component-slots.js +18 -0
  107. package/dist/react/component-slots.js.map +1 -0
  108. package/dist/react/fields/BuilderInput.d.ts.map +1 -1
  109. package/dist/react/fields/BuilderInput.js +21 -117
  110. package/dist/react/fields/BuilderInput.js.map +1 -1
  111. package/dist/react/fields/MarkdownInput.d.ts.map +1 -1
  112. package/dist/react/fields/MarkdownInput.js +1 -3
  113. package/dist/react/fields/MarkdownInput.js.map +1 -1
  114. package/dist/react/fields/RepeaterInput.d.ts.map +1 -1
  115. package/dist/react/fields/RepeaterInput.js +22 -127
  116. package/dist/react/fields/RepeaterInput.js.map +1 -1
  117. package/dist/react/fields/rowState.d.ts +40 -0
  118. package/dist/react/fields/rowState.d.ts.map +1 -0
  119. package/dist/react/fields/rowState.js +60 -0
  120. package/dist/react/fields/rowState.js.map +1 -0
  121. package/dist/react/fields/useRowReorderDnd.d.ts +28 -0
  122. package/dist/react/fields/useRowReorderDnd.d.ts.map +1 -0
  123. package/dist/react/fields/useRowReorderDnd.js +51 -0
  124. package/dist/react/fields/useRowReorderDnd.js.map +1 -0
  125. package/dist/react/index.d.ts +9 -0
  126. package/dist/react/index.d.ts.map +1 -1
  127. package/dist/react/index.js +8 -0
  128. package/dist/react/index.js.map +1 -1
  129. package/dist/react/layouts/SidebarLayout.d.ts +1 -1
  130. package/dist/react/layouts/SidebarLayout.d.ts.map +1 -1
  131. package/dist/react/layouts/SidebarLayout.js +10 -2
  132. package/dist/react/layouts/SidebarLayout.js.map +1 -1
  133. package/dist/react/layouts/TopbarLayout.d.ts +1 -1
  134. package/dist/react/layouts/TopbarLayout.d.ts.map +1 -1
  135. package/dist/react/layouts/TopbarLayout.js +19 -11
  136. package/dist/react/layouts/TopbarLayout.js.map +1 -1
  137. package/dist/react/parseRecordEditUrl.d.ts +29 -0
  138. package/dist/react/parseRecordEditUrl.d.ts.map +1 -0
  139. package/dist/react/parseRecordEditUrl.js +25 -0
  140. package/dist/react/parseRecordEditUrl.js.map +1 -0
  141. package/dist/react/persistedState.d.ts +19 -0
  142. package/dist/react/persistedState.d.ts.map +1 -0
  143. package/dist/react/persistedState.js +51 -0
  144. package/dist/react/persistedState.js.map +1 -0
  145. package/dist/react/schemaRenderer/AlertRenderer.d.ts +12 -0
  146. package/dist/react/schemaRenderer/AlertRenderer.d.ts.map +1 -0
  147. package/dist/react/schemaRenderer/AlertRenderer.js +61 -0
  148. package/dist/react/schemaRenderer/AlertRenderer.js.map +1 -0
  149. package/dist/react/schemaRenderer/EntryRenderer.d.ts +13 -0
  150. package/dist/react/schemaRenderer/EntryRenderer.d.ts.map +1 -0
  151. package/dist/react/schemaRenderer/EntryRenderer.js +277 -0
  152. package/dist/react/schemaRenderer/EntryRenderer.js.map +1 -0
  153. package/dist/react/schemaRenderer/SectionRenderer.d.ts +16 -0
  154. package/dist/react/schemaRenderer/SectionRenderer.d.ts.map +1 -0
  155. package/dist/react/schemaRenderer/SectionRenderer.js +62 -0
  156. package/dist/react/schemaRenderer/SectionRenderer.js.map +1 -0
  157. package/dist/react/schemaRenderer/SimpleElements.d.ts +25 -0
  158. package/dist/react/schemaRenderer/SimpleElements.d.ts.map +1 -0
  159. package/dist/react/schemaRenderer/SimpleElements.js +147 -0
  160. package/dist/react/schemaRenderer/SimpleElements.js.map +1 -0
  161. package/dist/react/schemaRenderer/TabsRenderer.d.ts +17 -0
  162. package/dist/react/schemaRenderer/TabsRenderer.d.ts.map +1 -0
  163. package/dist/react/schemaRenderer/TabsRenderer.js +31 -0
  164. package/dist/react/schemaRenderer/TabsRenderer.js.map +1 -0
  165. package/dist/react/schemaRenderer/WizardRenderer.d.ts +34 -0
  166. package/dist/react/schemaRenderer/WizardRenderer.d.ts.map +1 -0
  167. package/dist/react/schemaRenderer/WizardRenderer.js +208 -0
  168. package/dist/react/schemaRenderer/WizardRenderer.js.map +1 -0
  169. package/dist/react/schemaRenderer/action/ActionGroupTrigger.d.ts +21 -0
  170. package/dist/react/schemaRenderer/action/ActionGroupTrigger.d.ts.map +1 -0
  171. package/dist/react/schemaRenderer/action/ActionGroupTrigger.js +82 -0
  172. package/dist/react/schemaRenderer/action/ActionGroupTrigger.js.map +1 -0
  173. package/dist/react/schemaRenderer/action/ActionModalDialog.d.ts +30 -0
  174. package/dist/react/schemaRenderer/action/ActionModalDialog.d.ts.map +1 -0
  175. package/dist/react/schemaRenderer/action/ActionModalDialog.js +182 -0
  176. package/dist/react/schemaRenderer/action/ActionModalDialog.js.map +1 -0
  177. package/dist/react/schemaRenderer/action/ConfirmActionDialog.d.ts +17 -0
  178. package/dist/react/schemaRenderer/action/ConfirmActionDialog.d.ts.map +1 -0
  179. package/dist/react/schemaRenderer/action/ConfirmActionDialog.js +19 -0
  180. package/dist/react/schemaRenderer/action/ConfirmActionDialog.js.map +1 -0
  181. package/dist/react/schemaRenderer/action/HandlerActionButton.d.ts +16 -0
  182. package/dist/react/schemaRenderer/action/HandlerActionButton.d.ts.map +1 -0
  183. package/dist/react/schemaRenderer/action/HandlerActionButton.js +16 -0
  184. package/dist/react/schemaRenderer/action/HandlerActionButton.js.map +1 -0
  185. package/dist/react/schemaRenderer/action/MethodActionButton.d.ts +22 -0
  186. package/dist/react/schemaRenderer/action/MethodActionButton.d.ts.map +1 -0
  187. package/dist/react/schemaRenderer/action/MethodActionButton.js +26 -0
  188. package/dist/react/schemaRenderer/action/MethodActionButton.js.map +1 -0
  189. package/dist/react/schemaRenderer/action/buttons.d.ts +18 -0
  190. package/dist/react/schemaRenderer/action/buttons.d.ts.map +1 -0
  191. package/dist/react/schemaRenderer/action/buttons.js +74 -0
  192. package/dist/react/schemaRenderer/action/buttons.js.map +1 -0
  193. package/dist/react/schemaRenderer/action/helpers.d.ts +26 -0
  194. package/dist/react/schemaRenderer/action/helpers.d.ts.map +1 -0
  195. package/dist/react/schemaRenderer/action/helpers.js +126 -0
  196. package/dist/react/schemaRenderer/action/helpers.js.map +1 -0
  197. package/dist/react/schemaRenderer/action/renderAction.d.ts +21 -0
  198. package/dist/react/schemaRenderer/action/renderAction.d.ts.map +1 -0
  199. package/dist/react/schemaRenderer/action/renderAction.js +102 -0
  200. package/dist/react/schemaRenderer/action/renderAction.js.map +1 -0
  201. package/dist/react/schemaRenderer/columnFormat.d.ts +10 -0
  202. package/dist/react/schemaRenderer/columnFormat.d.ts.map +1 -0
  203. package/dist/react/schemaRenderer/columnFormat.js +76 -0
  204. package/dist/react/schemaRenderer/columnFormat.js.map +1 -0
  205. package/dist/react/schemaRenderer/constants.d.ts +8 -0
  206. package/dist/react/schemaRenderer/constants.d.ts.map +1 -0
  207. package/dist/react/schemaRenderer/constants.js +45 -0
  208. package/dist/react/schemaRenderer/constants.js.map +1 -0
  209. package/dist/react/schemaRenderer/form/FormRenderer.d.ts +29 -0
  210. package/dist/react/schemaRenderer/form/FormRenderer.d.ts.map +1 -0
  211. package/dist/react/schemaRenderer/form/FormRenderer.js +152 -0
  212. package/dist/react/schemaRenderer/form/FormRenderer.js.map +1 -0
  213. package/dist/react/schemaRenderer/form/renderField.d.ts +6 -0
  214. package/dist/react/schemaRenderer/form/renderField.d.ts.map +1 -0
  215. package/dist/react/schemaRenderer/form/renderField.js +239 -0
  216. package/dist/react/schemaRenderer/form/renderField.js.map +1 -0
  217. package/dist/react/schemaRenderer/helpers.d.ts +32 -0
  218. package/dist/react/schemaRenderer/helpers.d.ts.map +1 -0
  219. package/dist/react/schemaRenderer/helpers.js +52 -0
  220. package/dist/react/schemaRenderer/helpers.js.map +1 -0
  221. package/dist/react/schemaRenderer/table/CardsLayoutBody.d.ts +60 -0
  222. package/dist/react/schemaRenderer/table/CardsLayoutBody.d.ts.map +1 -0
  223. package/dist/react/schemaRenderer/table/CardsLayoutBody.js +189 -0
  224. package/dist/react/schemaRenderer/table/CardsLayoutBody.js.map +1 -0
  225. package/dist/react/schemaRenderer/table/TableRenderer.d.ts +29 -0
  226. package/dist/react/schemaRenderer/table/TableRenderer.d.ts.map +1 -0
  227. package/dist/react/schemaRenderer/table/TableRenderer.js +85 -0
  228. package/dist/react/schemaRenderer/table/TableRenderer.js.map +1 -0
  229. package/dist/react/schemaRenderer/table/TableRendererBody.d.ts +18 -0
  230. package/dist/react/schemaRenderer/table/TableRendererBody.d.ts.map +1 -0
  231. package/dist/react/schemaRenderer/table/TableRendererBody.js +555 -0
  232. package/dist/react/schemaRenderer/table/TableRendererBody.js.map +1 -0
  233. package/dist/react/schemaRenderer/table/filters.d.ts +263 -0
  234. package/dist/react/schemaRenderer/table/filters.d.ts.map +1 -0
  235. package/dist/react/schemaRenderer/table/filters.js +497 -0
  236. package/dist/react/schemaRenderer/table/filters.js.map +1 -0
  237. package/dist/react/schemaRenderer/table/formatCell.d.ts +11 -0
  238. package/dist/react/schemaRenderer/table/formatCell.d.ts.map +1 -0
  239. package/dist/react/schemaRenderer/table/formatCell.js +172 -0
  240. package/dist/react/schemaRenderer/table/formatCell.js.map +1 -0
  241. package/dist/react/schemaRenderer/table/links.d.ts +42 -0
  242. package/dist/react/schemaRenderer/table/links.d.ts.map +1 -0
  243. package/dist/react/schemaRenderer/table/links.js +55 -0
  244. package/dist/react/schemaRenderer/table/links.js.map +1 -0
  245. package/dist/react/schemaRenderer/table/renderRowActions.d.ts +13 -0
  246. package/dist/react/schemaRenderer/table/renderRowActions.d.ts.map +1 -0
  247. package/dist/react/schemaRenderer/table/renderRowActions.js +25 -0
  248. package/dist/react/schemaRenderer/table/renderRowActions.js.map +1 -0
  249. package/dist/react/schemaRenderer/table/url.d.ts +41 -0
  250. package/dist/react/schemaRenderer/table/url.d.ts.map +1 -0
  251. package/dist/react/schemaRenderer/table/url.js +114 -0
  252. package/dist/react/schemaRenderer/table/url.js.map +1 -0
  253. package/dist/routes/globals.d.ts +13 -0
  254. package/dist/routes/globals.d.ts.map +1 -0
  255. package/dist/routes/globals.js +131 -0
  256. package/dist/routes/globals.js.map +1 -0
  257. package/dist/routes/helpers.d.ts +217 -0
  258. package/dist/routes/helpers.d.ts.map +1 -0
  259. package/dist/routes/helpers.js +498 -0
  260. package/dist/routes/helpers.js.map +1 -0
  261. package/dist/routes/pages.d.ts +15 -0
  262. package/dist/routes/pages.d.ts.map +1 -0
  263. package/dist/routes/pages.js +145 -0
  264. package/dist/routes/pages.js.map +1 -0
  265. package/dist/routes/panel.d.ts +19 -0
  266. package/dist/routes/panel.d.ts.map +1 -0
  267. package/dist/routes/panel.js +191 -0
  268. package/dist/routes/panel.js.map +1 -0
  269. package/dist/routes/relations.d.ts +21 -0
  270. package/dist/routes/relations.d.ts.map +1 -0
  271. package/dist/routes/relations.js +1239 -0
  272. package/dist/routes/relations.js.map +1 -0
  273. package/dist/routes/resources.d.ts +28 -0
  274. package/dist/routes/resources.d.ts.map +1 -0
  275. package/dist/routes/resources.js +741 -0
  276. package/dist/routes/resources.js.map +1 -0
  277. package/dist/routes/theme.d.ts +12 -0
  278. package/dist/routes/theme.d.ts.map +1 -0
  279. package/dist/routes/theme.js +82 -0
  280. package/dist/routes/theme.js.map +1 -0
  281. package/dist/routes.d.ts.map +1 -1
  282. package/dist/routes.js +64 -3078
  283. package/dist/routes.js.map +1 -1
  284. package/dist/vite.d.ts +1 -0
  285. package/dist/vite.d.ts.map +1 -1
  286. package/dist/vite.js +26 -5
  287. package/dist/vite.js.map +1 -1
  288. package/package.json +2 -1
  289. package/src/Pilotiq.ts +95 -0
  290. package/src/actions/Action.ts +79 -723
  291. package/src/actions/bulkFactories.ts +168 -0
  292. package/src/actions/crudFactories.ts +220 -0
  293. package/src/actions/factoryHelpers.ts +177 -0
  294. package/src/actions/m2mFactories.ts +193 -0
  295. package/src/actions/relationFactories.ts +372 -0
  296. package/src/elements/dispatchForm.ts +1 -1
  297. package/src/elements/dispatchTable.ts +1 -1
  298. package/src/fields/Field.ts +39 -0
  299. package/src/pageData/breadcrumbs.ts +288 -0
  300. package/src/pageData/forms.ts +578 -0
  301. package/src/pageData/helpers.ts +764 -0
  302. package/src/pageData/misc.ts +347 -0
  303. package/src/pageData/navigation.ts +779 -0
  304. package/src/pageData/relationPages.ts +1246 -0
  305. package/src/pageData/relationTabs.ts +286 -0
  306. package/src/pageData/resourcePages.ts +593 -0
  307. package/src/pageData.ts +122 -4731
  308. package/src/react/AppShell.tsx +27 -1
  309. package/src/react/CollabExtensionFactoryRegistry.ts +55 -0
  310. package/src/react/CollabRoomContext.ts +42 -0
  311. package/src/react/FormCollabBindingRegistry.ts +72 -0
  312. package/src/react/RecordWrapperGate.tsx +40 -0
  313. package/src/react/RecordWrapperRegistry.ts +39 -0
  314. package/src/react/SchemaRenderer.tsx +230 -6479
  315. package/src/react/component-slots.test.ts +103 -0
  316. package/src/react/component-slots.ts +116 -0
  317. package/src/react/fields/BuilderInput.tsx +29 -117
  318. package/src/react/fields/MarkdownInput.tsx +0 -1
  319. package/src/react/fields/RepeaterInput.tsx +29 -130
  320. package/src/react/fields/rowState.ts +106 -0
  321. package/src/react/fields/useRowReorderDnd.ts +78 -0
  322. package/src/react/index.ts +38 -0
  323. package/src/react/layouts/SidebarLayout.tsx +39 -28
  324. package/src/react/layouts/TopbarLayout.tsx +70 -57
  325. package/src/react/parseRecordEditUrl.test.ts +75 -0
  326. package/src/react/parseRecordEditUrl.ts +55 -0
  327. package/src/react/persistedState.ts +40 -0
  328. package/src/react/schemaRenderer/AlertRenderer.tsx +112 -0
  329. package/src/react/schemaRenderer/EntryRenderer.tsx +501 -0
  330. package/src/react/schemaRenderer/SectionRenderer.tsx +120 -0
  331. package/src/react/schemaRenderer/SimpleElements.tsx +306 -0
  332. package/src/react/schemaRenderer/TabsRenderer.tsx +62 -0
  333. package/src/react/schemaRenderer/WizardRenderer.tsx +338 -0
  334. package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +177 -0
  335. package/src/react/schemaRenderer/action/ActionModalDialog.tsx +273 -0
  336. package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +61 -0
  337. package/src/react/schemaRenderer/action/HandlerActionButton.tsx +43 -0
  338. package/src/react/schemaRenderer/action/MethodActionButton.tsx +64 -0
  339. package/src/react/schemaRenderer/action/buttons.tsx +99 -0
  340. package/src/react/schemaRenderer/action/helpers.ts +140 -0
  341. package/src/react/schemaRenderer/action/renderAction.tsx +245 -0
  342. package/src/react/schemaRenderer/columnFormat.ts +65 -0
  343. package/src/react/schemaRenderer/constants.ts +50 -0
  344. package/src/react/schemaRenderer/form/FormRenderer.tsx +233 -0
  345. package/src/react/schemaRenderer/form/renderField.tsx +511 -0
  346. package/src/react/schemaRenderer/helpers.tsx +81 -0
  347. package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +308 -0
  348. package/src/react/schemaRenderer/table/TableRenderer.tsx +123 -0
  349. package/src/react/schemaRenderer/table/TableRendererBody.tsx +974 -0
  350. package/src/react/schemaRenderer/table/filters.tsx +1233 -0
  351. package/src/react/schemaRenderer/table/formatCell.tsx +264 -0
  352. package/src/react/schemaRenderer/table/links.tsx +112 -0
  353. package/src/react/schemaRenderer/table/renderRowActions.tsx +52 -0
  354. package/src/react/schemaRenderer/table/url.tsx +143 -0
  355. package/src/routes/globals.ts +154 -0
  356. package/src/routes/helpers.ts +668 -0
  357. package/src/routes/pages.ts +173 -0
  358. package/src/routes/panel.ts +204 -0
  359. package/src/routes/relations.ts +1219 -0
  360. package/src/routes/resources.ts +786 -0
  361. package/src/routes/theme.ts +109 -0
  362. package/src/routes.test.ts +1 -1
  363. package/src/routes.ts +64 -3176
  364. package/src/schema/TableWidget.test.ts +2 -2
  365. package/src/theme/migrate.test.ts +178 -0
  366. package/src/vite.test.ts +184 -0
  367. package/src/vite.ts +26 -4
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Bulk-placement Action factories — `bulkDelete / bulkRestore /
3
+ * bulkForceDelete / bulkReplicate`. Handler-style: iterate
4
+ * `ctx.records`, run policy per-row (parallelized via
5
+ * `forEachAllowed`), call the matching Resource / Model method. No new
6
+ * routes — the existing `/_action/:actionName` dispatcher already
7
+ * handles bulk via `ctx.records`.
8
+ *
9
+ * Drop into `bulkActions([...])` from inside `Resource.table()`. Each
10
+ * returns a notification with the count succeeded; rows whose policy
11
+ * denied (or whose call threw) are silently skipped — surface them
12
+ * via your own logging if needed. When no rows succeed (empty
13
+ * selection, all denied, all threw) the handler emits a `'warning'`
14
+ * toast instead of misleading "0 X deleted" success.
15
+ *
16
+ * See `docs/plans/action-split.md` for the split plan.
17
+ */
18
+
19
+ import { Action, type ReplicateOptions, type ResourceLike } from './Action.js'
20
+ import { buildReplica, callPredicate, forEachAllowed } from './factoryHelpers.js'
21
+
22
+ /** Pick the right label form for a count — `labelSingular` for 1,
23
+ * `label` (plural, lowercased) for any other count. Fall back to a
24
+ * naive `${labelSingular}s` when no plural label is set. Used by bulk
25
+ * notification copy so we don't ship "1 posts moved to trash". */
26
+ function labelForCount(R: ResourceLike, n: number): string {
27
+ if (n === 1) return R.labelSingular.toLowerCase()
28
+ const plural = R.label?.toLowerCase()
29
+ return plural ?? `${R.labelSingular.toLowerCase()}s`
30
+ }
31
+
32
+ /** Bulk delete — calls `R.deleteRecord(id)` per row. On a
33
+ * soft-delete resource that hits `Model.delete()` which writes
34
+ * `deletedAt`. Notification: "N posts moved to trash" / "N posts
35
+ * deleted" depending on `R.softDeletes`. */
36
+ export function bulkDeleteAction(R: ResourceLike, _basePath: string): Action {
37
+ return Action.make('bulkDelete')
38
+ .label('Delete selected')
39
+ .destructive()
40
+ .bulk()
41
+ .confirm(`Delete the selected ${labelForCount(R, 0)}?`)
42
+ .handler(async (ctx) => {
43
+ const records = ctx.records ?? []
44
+ const Rfull = R as ResourceLike & { deleteRecord(id: string): Promise<void> }
45
+ const verb = R.softDeletes ? 'moved to trash' : 'deleted'
46
+ const n = await forEachAllowed(
47
+ records,
48
+ (record) => callPredicate(R.canDelete, ctx.user, record),
49
+ async (id) => { await Rfull.deleteRecord(id) },
50
+ )
51
+ if (n === 0) {
52
+ return { notify: { title: `Nothing to delete (no permitted rows)`, type: 'warning' } as never }
53
+ }
54
+ return { notify: { title: `${n} ${labelForCount(R, n)} ${verb}`, type: 'success' } as never }
55
+ })
56
+ }
57
+
58
+ /** Bulk restore — calls `R.model.restore(id)` per row. Visible only
59
+ * on soft-delete resources (the entire bulk-restore concept is
60
+ * specific to them). */
61
+ export function bulkRestoreAction(R: ResourceLike, _basePath: string): Action {
62
+ return Action.make('bulkRestore')
63
+ .label('Restore selected')
64
+ .color('success')
65
+ .bulk()
66
+ .confirm(`Restore the selected ${labelForCount(R, 0)}?`)
67
+ .handler(async (ctx) => {
68
+ const Rfull = R as ResourceLike & { model?: { restore?(id: string | number): Promise<unknown> } }
69
+ const restore = Rfull.model?.restore
70
+ if (!restore) {
71
+ return { notify: { title: 'Restore not configured', type: 'error' } as never }
72
+ }
73
+ const records = ctx.records ?? []
74
+ const n = await forEachAllowed(
75
+ records,
76
+ (record) => callPredicate(R.canRestore, ctx.user, record),
77
+ async (id) => { await restore(id) },
78
+ )
79
+ if (n === 0) {
80
+ return { notify: { title: `Nothing to restore (no permitted rows)`, type: 'warning' } as never }
81
+ }
82
+ return { notify: { title: `${n} ${labelForCount(R, n)} restored`, type: 'success' } as never }
83
+ })
84
+ }
85
+
86
+ /** Bulk force-delete — calls `R.model.forceDelete(id)` per row. Same
87
+ * destructive confirm as the per-row variant. Visible only on
88
+ * soft-delete resources. */
89
+ export function bulkForceDeleteAction(R: ResourceLike, _basePath: string): Action {
90
+ return Action.make('bulkForceDelete')
91
+ .label('Delete forever')
92
+ .destructive()
93
+ .bulk()
94
+ .confirm(`Permanently delete the selected ${labelForCount(R, 0)}? This cannot be undone.`)
95
+ .handler(async (ctx) => {
96
+ const Rfull = R as ResourceLike & { model?: { forceDelete?(id: string | number): Promise<void> } }
97
+ const forceDelete = Rfull.model?.forceDelete
98
+ if (!forceDelete) {
99
+ return { notify: { title: 'Force-delete not configured', type: 'error' } as never }
100
+ }
101
+ const records = ctx.records ?? []
102
+ const n = await forEachAllowed(
103
+ records,
104
+ (record) => callPredicate(R.canForceDelete, ctx.user, record),
105
+ async (id) => { await forceDelete(id) },
106
+ )
107
+ if (n === 0) {
108
+ return { notify: { title: `Nothing to delete (no permitted rows)`, type: 'warning' } as never }
109
+ }
110
+ return { notify: { title: `${n} ${labelForCount(R, n)} permanently deleted`, type: 'success' } as never }
111
+ })
112
+ }
113
+
114
+ /**
115
+ * Bulk replicate — calls `R.model.create(...)` once per selected row
116
+ * with the source row's attributes minus PK / soft-delete column /
117
+ * `opts.excludeAttributes`. Optional `opts.beforeReplicaSaved(replica,
118
+ * source)` runs per-row. Rows that throw during create are skipped
119
+ * silently so a single bad row doesn't abort the batch (the user sees
120
+ * the success count on the toast). Visibility delegates to
121
+ * `R.canCreate(user)`.
122
+ *
123
+ * Sibling of `replicateAction` — same options bag, same strip set,
124
+ * same authorization gate. Stays on the list page (no per-row
125
+ * redirect possible for N rows).
126
+ */
127
+ export function bulkReplicateAction(
128
+ R: ResourceLike,
129
+ _basePath: string,
130
+ opts: ReplicateOptions = {},
131
+ ): Action {
132
+ return Action.make('bulkReplicate')
133
+ .label('Replicate selected')
134
+ .bulk()
135
+ .confirm(`Replicate the selected ${labelForCount(R, 0)}?`)
136
+ .handler(async (ctx) => {
137
+ const M = R.model
138
+ if (!M || typeof M.create !== 'function') {
139
+ return { notify: { title: 'Replicate not configured (resource has no model.create)', type: 'error' } as never }
140
+ }
141
+ const records = ctx.records ?? []
142
+ // Per-row predicate eval preserved — `canCreate` ignores the record
143
+ // in the common case, but users may write stateful predicates that
144
+ // gate per-attempt.
145
+ const n = await forEachAllowed(
146
+ records,
147
+ () => callPredicate(R.canCreate, ctx.user),
148
+ async (_id, source) => {
149
+ const { replica } = await buildReplica(source, M, {
150
+ excludeAttributes: opts.excludeAttributes,
151
+ deletedAtColumn: R.deletedAtColumn,
152
+ beforeReplicaSaved: opts.beforeReplicaSaved,
153
+ })
154
+ await M.create(replica)
155
+ },
156
+ )
157
+ if (n === 0) {
158
+ return { notify: { title: `Nothing to replicate (no permitted rows)`, type: 'warning' } as never }
159
+ }
160
+ const defaultTitle = `${n} ${labelForCount(R, n)} replicated`
161
+ const overrideTitle = opts.getCreatedNotificationTitle
162
+ ? await opts.getCreatedNotificationTitle({ count: n, records })
163
+ : undefined
164
+ const title = overrideTitle !== undefined ? overrideTitle : defaultTitle
165
+ return { notify: { title, type: 'success' } as never }
166
+ })
167
+ .visible(({ user }) => callPredicate(R.canCreate, user))
168
+ }
@@ -0,0 +1,220 @@
1
+ /**
2
+ * CRUD single-row Action factories — `create / edit / view / delete /
3
+ * replicate / restore / forceDelete`, plus the notification `markAsRead`
4
+ * factory.
5
+ *
6
+ * Each factory builds and returns a configured `Action`. The matching
7
+ * `static` methods on the `Action` class are thin delegators that call
8
+ * into these functions, preserving the public `Action.create(R, base)`
9
+ * call shape.
10
+ *
11
+ * See `docs/plans/action-split.md` for the split plan.
12
+ */
13
+
14
+ import { Action, type ReplicateOptions, type ResourceLike } from './Action.js'
15
+ import { buildReplica, callPredicate, isTrashed, resourceBase } from './factoryHelpers.js'
16
+
17
+ /** Create-action factory — link to `${basePath}/${R.slug}/create`.
18
+ * Auto-hides when `R.canCreate(user)` returns false. */
19
+ export function createAction(R: ResourceLike, basePath: string): Action {
20
+ return Action.make('create')
21
+ .label(`New ${R.labelSingular}`)
22
+ .href(`${resourceBase(basePath, R)}/create`)
23
+ .visible(({ user }) => callPredicate(R.canCreate, user))
24
+ }
25
+
26
+ /**
27
+ * Edit-action factory — link to the resource's edit page.
28
+ *
29
+ * Pass `recordId` when building actions for a single-record context
30
+ * (e.g. `ViewPage.getActions()`); the URL is baked at config time.
31
+ * Omit `recordId` for row context (`Table.recordActions(...)`); the
32
+ * URL keeps the `:id` template and the renderer substitutes per-row.
33
+ *
34
+ * Auto-hides when `R.canEdit(user, record)` returns false. For row
35
+ * context the per-row record threads in via `loadTableRecords`'s
36
+ * per-row eval; for view-page context, `resolveSchema` provides the
37
+ * resolved record on the eval context.
38
+ */
39
+ export function editAction(R: ResourceLike, basePath: string, recordId?: string): Action {
40
+ const id = recordId ?? ':id'
41
+ return Action.make('edit')
42
+ .label('Edit')
43
+ .href(`${resourceBase(basePath, R)}/${id}/edit`)
44
+ .visible(({ user, record }) => callPredicate(R.canEdit, user, record))
45
+ }
46
+
47
+ /** View-action factory — link to the resource's view page. See `editAction` for the `recordId` semantics.
48
+ * Auto-hides when `R.canView(user, record)` returns false. */
49
+ export function viewAction(R: ResourceLike, basePath: string, recordId?: string): Action {
50
+ const id = recordId ?? ':id'
51
+ return Action.make('view')
52
+ .label('View')
53
+ .href(`${resourceBase(basePath, R)}/${id}`)
54
+ .visible(({ user, record }) => callPredicate(R.canView, user, record))
55
+ }
56
+
57
+ /**
58
+ * Delete-action factory — POSTs to the resource's delete route,
59
+ * destructive style, with a confirmation prompt referencing the
60
+ * resource label. Same `recordId` semantics as `editAction`.
61
+ * Auto-hides when `R.canDelete(user, record)` returns false.
62
+ *
63
+ * Plan #13 — when `R.softDeletes = true`, additionally hides on
64
+ * rows whose `deletedAtColumn` is set (already-trashed rows get the
65
+ * Restore + ForceDelete pair instead, surfaced via the matching
66
+ * factories below).
67
+ */
68
+ export function deleteAction(R: ResourceLike, basePath: string, recordId?: string): Action {
69
+ const id = recordId ?? ':id'
70
+ return Action.make('delete')
71
+ .label('Delete')
72
+ .destructive()
73
+ .method('post')
74
+ .action(`${resourceBase(basePath, R)}/${id}/delete`)
75
+ .confirm(`Delete this ${R.labelSingular.toLowerCase()}?`)
76
+ .visible(async ({ user, record }) => {
77
+ if (R.softDeletes && isTrashed(record, R)) return false
78
+ return callPredicate(R.canDelete, user, record)
79
+ })
80
+ }
81
+
82
+ /**
83
+ * Replicate-action factory — handler-style. Loads the source record
84
+ * from `ctx.record` (the `_action/:actionName` route already resolves
85
+ * it through `R.query(ctx)` for row + single-target placements),
86
+ * strips PK + soft-delete column + any `opts.excludeAttributes`,
87
+ * optionally runs `opts.beforeReplicaSaved`, and creates a new row
88
+ * via `R.model.create(...)`. Redirects to the new record's edit page
89
+ * on success so the user can review + tweak before saving again.
90
+ *
91
+ * `recordId` kept in the signature for parity with `delete / edit /
92
+ * view` so users can swap factories without rewriting call sites; the
93
+ * dispatcher resolves the source record from the URL and hands it to
94
+ * the handler as `ctx.record`, so we don't reference `recordId` here.
95
+ *
96
+ * Auto-hides when `R.canCreate(user)` returns false — replicating
97
+ * writes a new row, so the gate is `canCreate`, not `canView`.
98
+ */
99
+ export function replicateAction(
100
+ R: ResourceLike,
101
+ basePath: string,
102
+ recordId?: string,
103
+ opts: ReplicateOptions = {},
104
+ ): Action {
105
+ void recordId
106
+ return Action.make('replicate')
107
+ .label('Replicate')
108
+ .handler(async (ctx) => {
109
+ const source = ctx.record
110
+ if (!source || typeof source !== 'object') {
111
+ return { notify: { title: 'Replicate failed: source record missing', type: 'error' } as never }
112
+ }
113
+ const M = R.model
114
+ if (!M || typeof M.create !== 'function') {
115
+ return { notify: { title: 'Replicate not configured (resource has no model.create)', type: 'error' } as never }
116
+ }
117
+
118
+ let replica: Record<string, unknown>
119
+ let pkCol: string
120
+ try {
121
+ ({ replica, pkCol } = await buildReplica(source, M, {
122
+ excludeAttributes: opts.excludeAttributes,
123
+ deletedAtColumn: R.deletedAtColumn,
124
+ beforeReplicaSaved: opts.beforeReplicaSaved,
125
+ }))
126
+ } catch (err) {
127
+ return { notify: { title: `Replicate failed: ${err instanceof Error ? err.message : String(err)}`, type: 'error' } as never }
128
+ }
129
+
130
+ let created: unknown
131
+ try {
132
+ created = await M.create(replica)
133
+ } catch (err) {
134
+ return { notify: { title: `Replicate failed: ${err instanceof Error ? err.message : String(err)}`, type: 'error' } as never }
135
+ }
136
+
137
+ const newId = (created as Record<string, unknown> | null | undefined)?.[pkCol]
138
+ const defaultRedirect = newId !== undefined && newId !== null
139
+ ? `${resourceBase(basePath, R)}/${String(newId)}/edit`
140
+ : `${resourceBase(basePath, R)}`
141
+ // `!== undefined` rather than `??` so an override returning
142
+ // `null`/empty-string isn't silently swallowed (see
143
+ // feedback_nullish_swallows_explicit_null).
144
+ const overrideRedirect = opts.getRedirectUrl
145
+ ? await opts.getRedirectUrl({ replica: created, source })
146
+ : undefined
147
+ const redirect = overrideRedirect !== undefined ? overrideRedirect : defaultRedirect
148
+ const overrideTitle = opts.getCreatedNotificationTitle
149
+ ? await opts.getCreatedNotificationTitle({ replica: created, source })
150
+ : undefined
151
+ const title = overrideTitle !== undefined ? overrideTitle : `${R.labelSingular} replicated`
152
+ return {
153
+ redirect,
154
+ notify: { title, type: 'success' } as never,
155
+ }
156
+ })
157
+ .visible(({ user }) => callPredicate(R.canCreate, user))
158
+ }
159
+
160
+ /**
161
+ * Plan #13 — Restore factory. POSTs to the resource's restore route,
162
+ * success-styled, no confirm prompt (restoration is reversible).
163
+ * Auto-hides on live (non-trashed) rows AND when `R.canRestore(user,
164
+ * record)` returns false. Same `recordId` semantics as `editAction`.
165
+ */
166
+ export function restoreAction(R: ResourceLike, basePath: string, recordId?: string): Action {
167
+ const id = recordId ?? ':id'
168
+ return Action.make('restore')
169
+ .label('Restore')
170
+ .color('success')
171
+ .method('post')
172
+ .action(`${resourceBase(basePath, R)}/${id}/restore`)
173
+ .visible(async ({ user, record }) => {
174
+ if (!isTrashed(record, R)) return false
175
+ return callPredicate(R.canRestore, user, record)
176
+ })
177
+ }
178
+
179
+ /**
180
+ * Plan #13 — Force-delete factory. POSTs to the resource's
181
+ * force-delete route, destructive-styled, with a stricter confirm
182
+ * prompt referencing permanence. Auto-hides on live (non-trashed)
183
+ * rows AND when `R.canForceDelete(user, record)` returns false.
184
+ */
185
+ export function forceDeleteAction(R: ResourceLike, basePath: string, recordId?: string): Action {
186
+ const id = recordId ?? ':id'
187
+ return Action.make('forceDelete')
188
+ .label('Delete forever')
189
+ .destructive()
190
+ .method('post')
191
+ .action(`${resourceBase(basePath, R)}/${id}/force-delete`)
192
+ .confirm(`Permanently delete this ${R.labelSingular.toLowerCase()}? This cannot be undone.`)
193
+ .visible(async ({ user, record }) => {
194
+ if (!isTrashed(record, R)) return false
195
+ return callPredicate(R.canForceDelete, user, record)
196
+ })
197
+ }
198
+
199
+ /**
200
+ * Mark-as-read factory — POSTs to the panel's notification read
201
+ * endpoint for the given notification id. The endpoint
202
+ * (`${base}/_notifications/:id/read`) is mounted by
203
+ * `Pilotiq.databaseNotifications()`, so calling this without
204
+ * opting into the bell surface produces an Action whose POST 404s.
205
+ *
206
+ * `notificationId` is baked at config time. For row context where
207
+ * the id varies per row, omit it and the URL keeps the `:id`
208
+ * template; the renderer substitutes per-row at render time
209
+ * (parallel to `editAction`'s row form).
210
+ *
211
+ * No auto-visibility. Wrap in `.visible(({ record }) => !record.readAt)`
212
+ * to hide on already-read rows.
213
+ */
214
+ export function markAsReadAction(basePath: string, notificationId?: string): Action {
215
+ const id = notificationId ?? ':id'
216
+ return Action.make('markAsRead')
217
+ .label('Mark as read')
218
+ .method('post')
219
+ .action(`${basePath}/_notifications/${id}/read`)
220
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Shared helpers for the Action factory modules — `crudFactories.ts`,
3
+ * `bulkFactories.ts`, `relationFactories.ts`, `m2mFactories.ts`.
4
+ *
5
+ * Lives in its own file so the per-phase factory modules stay focused
6
+ * on their own factory bodies. Anything consumed by 2+ phase files
7
+ * lands here; phase-local helpers stay alongside their phase's
8
+ * factories.
9
+ *
10
+ * See `docs/plans/action-split.md` for the split plan.
11
+ */
12
+
13
+ import type { ResourceLike } from './Action.js'
14
+ import type { RelationManagerContext } from '../RelationManager.js'
15
+
16
+ /** Cluster-aware resource base path. Mirrors `clusterPaths.resourceBasePath`
17
+ * but uses the structural `ResourceLike` shape so `Action.ts` stays
18
+ * cycle-free against `Resource.ts`. */
19
+ export function resourceBase(basePath: string, R: ResourceLike): string {
20
+ if (R.cluster) return `${basePath}/${R.cluster.getSlug()}/${R.getSlug()}`
21
+ return `${basePath}/${R.getSlug()}`
22
+ }
23
+
24
+ /** Call a (possibly undefined) Resource predicate. When unset, the
25
+ * predicate is treated as "allowed" (returns true) so the factory
26
+ * doesn't hide actions on Resources that haven't opted into Plan #10. */
27
+ export function callPredicate(
28
+ fn: ((user: unknown, record?: unknown) => boolean | Promise<boolean>) | undefined,
29
+ user: unknown,
30
+ record?: unknown,
31
+ ): boolean | Promise<boolean> {
32
+ if (!fn) return true
33
+ return fn(user, record)
34
+ }
35
+
36
+ /** Read `record[R.deletedAtColumn ?? 'deletedAt']` and return true when
37
+ * the row is currently trashed (soft-deleted). Permissive on shape —
38
+ * bare `null` / `undefined` count as live; any other truthy value is
39
+ * trashed. */
40
+ export function isTrashed(record: unknown, R: ResourceLike): boolean {
41
+ if (!record || typeof record !== 'object') return false
42
+ const col = R.deletedAtColumn ?? 'deletedAt'
43
+ const v = (record as Record<string, unknown>)[col]
44
+ return v !== null && v !== undefined
45
+ }
46
+
47
+ /** True when a `RelationManagerContext.mode` denotes a pivot-mutation
48
+ * shape — i.e. a many-to-many relation. All three modes share the
49
+ * `attach` / `detach` / `sync` accessor surface (the rudder ORM stamps
50
+ * + filters the polymorphic discriminator transparently for the morph
51
+ * variants). The `relationCreate / Edit / Delete` factories auto-hide
52
+ * under any of these modes because per-pivot-row create / edit / delete
53
+ * is meaningless — users create the related record via its own Resource,
54
+ * then attach via `relationAttach`. */
55
+ export function isM2MMode(mode: RelationManagerContext['mode']): boolean {
56
+ return mode === 'belongsToMany' || mode === 'morphToMany' || mode === 'morphedByMany'
57
+ }
58
+
59
+ /**
60
+ * Build the URL prefix for a relation factory action. Without
61
+ * a `chain` (depth-1 manager), this is the familiar
62
+ * `${base}/${parentSlug}/${parentId}/${relationship}`. With a chain
63
+ * (depth-2 nested manager), it threads the outer record + relationship
64
+ * between the parent slug and the leaf parent id:
65
+ *
66
+ * `${base}/${parentSlug}/${chain[0].recordId}/${chain[0].relationship}/${parentId}/${relationship}`
67
+ *
68
+ * Pure; takes a `RelationManagerContext` and emits a string. The leaf
69
+ * record id (and trailing `/edit`, `/delete`, etc.) gets appended by
70
+ * the caller.
71
+ */
72
+ export function relationUrlPrefix(ctx: RelationManagerContext): string {
73
+ const head = `${ctx.basePath}/${ctx.parentSlug}`
74
+ const chain = ctx.chain ?? []
75
+ let mid = ''
76
+ for (const step of chain) {
77
+ mid += `/${step.recordId}/${step.relationship}`
78
+ }
79
+ return `${head}${mid}/${ctx.parentId}/${ctx.relationship}`
80
+ }
81
+
82
+ /** Options bag for `buildReplica`. Optional fields explicitly accept
83
+ * `undefined` so call sites can pass `opts.excludeAttributes` through
84
+ * unconditionally under `exactOptionalPropertyTypes: true`. */
85
+ export interface BuildReplicaOptions {
86
+ /** Attribute keys to drop from the replicated payload IN ADDITION TO
87
+ * the model's primary key and soft-delete column. */
88
+ excludeAttributes?: readonly string[] | undefined
89
+ /** Soft-delete column name on the source Resource. Defaults to
90
+ * `'deletedAt'`. Read separately from the model because the column
91
+ * lives on the Resource shape (`R.deletedAtColumn`) rather than on
92
+ * the model itself. */
93
+ deletedAtColumn?: string | undefined
94
+ /** Force-pinned columns applied AFTER the strip and BEFORE the user
95
+ * `beforeReplicaSaved` mutator. Used by the relation replicate
96
+ * factories to re-stamp the parent attachment FK / morph columns
97
+ * so a tampered source row can't slip a different parent in. */
98
+ pin?: Record<string, unknown> | undefined
99
+ /** Optional user mutator. Runs after the strip + pin. */
100
+ beforeReplicaSaved?: ((
101
+ replica: Record<string, unknown>,
102
+ source: unknown,
103
+ ) => Record<string, unknown> | Promise<Record<string, unknown>>) | undefined
104
+ }
105
+
106
+ /**
107
+ * Build a replica payload from a source record. Used by every replicate
108
+ * factory (`replicateAction / bulkReplicateAction / relationReplicateAction
109
+ * / relationBulkReplicateAction`).
110
+ *
111
+ * Strips the model's primary key (`model.primaryKey`, defaulting to
112
+ * `'id'`), the soft-delete column (defaulting to `'deletedAt'`), and any
113
+ * `excludeAttributes` keys. Applies `pin` columns (parent attachment
114
+ * for relation factories), then runs the optional `beforeReplicaSaved`
115
+ * user mutator. Returns the replica AND the resolved primary-key column
116
+ * name (callers need it to read `created[pkCol]` for redirect URLs).
117
+ *
118
+ * Does NOT call `model.create` — callers wrap their own create + error
119
+ * handling around the returned replica.
120
+ */
121
+ export async function buildReplica(
122
+ source: unknown,
123
+ model: { primaryKey?: string },
124
+ opts: BuildReplicaOptions = {},
125
+ ): Promise<{ replica: Record<string, unknown>; pkCol: string }> {
126
+ const pkCol = model.primaryKey ?? 'id'
127
+ const trashedCol = opts.deletedAtColumn ?? 'deletedAt'
128
+ const skip = new Set<string>([pkCol, trashedCol, ...(opts.excludeAttributes ?? [])])
129
+ let replica: Record<string, unknown> = {}
130
+ for (const [k, v] of Object.entries(source as Record<string, unknown>)) {
131
+ if (skip.has(k)) continue
132
+ replica[k] = v
133
+ }
134
+ if (opts.pin) Object.assign(replica, opts.pin)
135
+ if (opts.beforeReplicaSaved) {
136
+ replica = await opts.beforeReplicaSaved(replica, source)
137
+ }
138
+ return { replica, pkCol }
139
+ }
140
+
141
+ /**
142
+ * Iterate `records`, run the policy probe in parallel up-front (only
143
+ * allowed rows enter the serial work loop), call `op(id, record)` per
144
+ * allowed row, swallow per-row throws (the aggregate notification shows
145
+ * the count succeeded). Returns the success count.
146
+ *
147
+ * Used by every bulk handler (`bulkDeleteAction / bulkRestoreAction /
148
+ * bulkForceDeleteAction / bulkReplicateAction / relationBulkReplicateAction
149
+ * / relationBulkDetachAction`-style pattern).
150
+ *
151
+ * Rows whose `record.id` coerces to an empty string are skipped without
152
+ * counting them as an attempt. The policy probe runs via `Promise.all`
153
+ * so backend round-trips parallelize, but the write loop stays serial
154
+ * (no transaction in v1 — concurrent writes would muddy failure
155
+ * semantics).
156
+ */
157
+ export async function forEachAllowed(
158
+ records: readonly unknown[],
159
+ isAllowed: (record: unknown, index: number) => boolean | Promise<boolean>,
160
+ op: (id: string, record: unknown, index: number) => Promise<void>,
161
+ ): Promise<number> {
162
+ const allowedFlags = await Promise.all(
163
+ records.map((r, i) => isAllowed(r, i)),
164
+ )
165
+ let n = 0
166
+ for (let i = 0; i < records.length; i++) {
167
+ if (!allowedFlags[i]) continue
168
+ const record = records[i]
169
+ const id = String((record as { id?: unknown } | null | undefined)?.id ?? '')
170
+ if (!id) continue
171
+ try {
172
+ await op(id, record, i)
173
+ n++
174
+ } catch { /* skip — agg notify shows total */ }
175
+ }
176
+ return n
177
+ }