@pilotiq/pilotiq 0.24.1 → 0.24.3

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 (518) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/boost/guidelines.md +571 -0
  3. package/boost/skills/pilotiq-actions/SKILL.md +49 -0
  4. package/boost/skills/pilotiq-actions/rules/dispatch-modes.md +177 -0
  5. package/boost/skills/pilotiq-actions/rules/factories.md +130 -0
  6. package/boost/skills/pilotiq-actions/rules/visibility-and-authorization.md +125 -0
  7. package/boost/skills/pilotiq-fields/SKILL.md +47 -0
  8. package/boost/skills/pilotiq-fields/rules/field-catalog.md +288 -0
  9. package/boost/skills/pilotiq-fields/rules/reactive-fields.md +199 -0
  10. package/boost/skills/pilotiq-fields/rules/validation.md +198 -0
  11. package/boost/skills/pilotiq-relations/SKILL.md +47 -0
  12. package/boost/skills/pilotiq-relations/rules/relation-managers.md +256 -0
  13. package/boost/skills/pilotiq-relations/rules/repeater-relationship.md +177 -0
  14. package/boost/skills/pilotiq-resource/SKILL.md +61 -0
  15. package/boost/skills/pilotiq-resource/rules/authorization.md +242 -0
  16. package/boost/skills/pilotiq-resource/rules/defining-resources.md +228 -0
  17. package/boost/skills/pilotiq-resource/rules/page-overrides.md +296 -0
  18. package/dist/Pilotiq.d.ts +31 -0
  19. package/dist/Pilotiq.d.ts.map +1 -1
  20. package/dist/Pilotiq.js +3 -1
  21. package/dist/Pilotiq.js.map +1 -1
  22. package/dist/PilotiqRegistry.d.ts +13 -0
  23. package/dist/PilotiqRegistry.d.ts.map +1 -1
  24. package/dist/PilotiqRegistry.js +15 -0
  25. package/dist/PilotiqRegistry.js.map +1 -1
  26. package/dist/pageData/misc.d.ts.map +1 -1
  27. package/dist/pageData/misc.js +6 -0
  28. package/dist/pageData/misc.js.map +1 -1
  29. package/dist/pageData/navigation.d.ts +1 -0
  30. package/dist/pageData/navigation.d.ts.map +1 -1
  31. package/dist/pageData/navigation.js +3 -0
  32. package/dist/pageData/navigation.js.map +1 -1
  33. package/dist/pageData/relationPages.d.ts.map +1 -1
  34. package/dist/pageData/relationPages.js +3 -0
  35. package/dist/pageData/relationPages.js.map +1 -1
  36. package/dist/pageData/resourcePages.d.ts.map +1 -1
  37. package/dist/pageData/resourcePages.js +8 -0
  38. package/dist/pageData/resourcePages.js.map +1 -1
  39. package/dist/react/AppShell.d.ts +8 -0
  40. package/dist/react/AppShell.d.ts.map +1 -1
  41. package/dist/react/AppShell.js.map +1 -1
  42. package/dist/react/layouts/SidebarLayout.d.ts.map +1 -1
  43. package/dist/react/layouts/SidebarLayout.js +10 -2
  44. package/dist/react/layouts/SidebarLayout.js.map +1 -1
  45. package/dist/react/widgets/StatsOverviewRenderer.d.ts.map +1 -1
  46. package/dist/react/widgets/StatsOverviewRenderer.js +32 -18
  47. package/dist/react/widgets/StatsOverviewRenderer.js.map +1 -1
  48. package/dist/routes/relations.d.ts.map +1 -1
  49. package/dist/routes/relations.js +25 -18
  50. package/dist/routes/relations.js.map +1 -1
  51. package/dist/routes/resources.js.map +1 -1
  52. package/package.json +10 -5
  53. package/.turbo/turbo-build.log +0 -8
  54. package/CLAUDE.md +0 -265
  55. package/src/Cluster.test.ts +0 -283
  56. package/src/Cluster.ts +0 -83
  57. package/src/Column.test.ts +0 -199
  58. package/src/Column.ts +0 -710
  59. package/src/Global.test.ts +0 -367
  60. package/src/Global.ts +0 -169
  61. package/src/Page.test.ts +0 -114
  62. package/src/Page.ts +0 -208
  63. package/src/Pilotiq.perf.test.ts +0 -252
  64. package/src/Pilotiq.test.ts +0 -129
  65. package/src/Pilotiq.ts +0 -1158
  66. package/src/PilotiqRegistry.ts +0 -36
  67. package/src/PilotiqServiceProvider.ts +0 -121
  68. package/src/RelationManager.test.ts +0 -400
  69. package/src/RelationManager.ts +0 -527
  70. package/src/RenderHook.test.ts +0 -252
  71. package/src/RenderHook.ts +0 -242
  72. package/src/Resource.test.ts +0 -284
  73. package/src/Resource.ts +0 -526
  74. package/src/RightPanel.test.ts +0 -202
  75. package/src/RightPanel.ts +0 -132
  76. package/src/Tab.test.ts +0 -91
  77. package/src/Tab.ts +0 -156
  78. package/src/UserMenuItem.ts +0 -145
  79. package/src/actions/Action.test.ts +0 -2526
  80. package/src/actions/Action.ts +0 -1515
  81. package/src/actions/ActionGroup.test.ts +0 -112
  82. package/src/actions/ActionGroup.ts +0 -173
  83. package/src/actions/attachFactory.ts +0 -172
  84. package/src/actions/bulkFactories.ts +0 -168
  85. package/src/actions/crudFactories.ts +0 -220
  86. package/src/actions/exportFactory.ts +0 -225
  87. package/src/actions/factoryHelpers.ts +0 -177
  88. package/src/actions/importFactory.ts +0 -243
  89. package/src/actions/index.ts +0 -17
  90. package/src/actions/m2mFactories.ts +0 -193
  91. package/src/actions/relationFactories.ts +0 -372
  92. package/src/applyPageHooks.test.ts +0 -463
  93. package/src/applyPageHooks.ts +0 -330
  94. package/src/authorization.test.ts +0 -483
  95. package/src/breadcrumbs.test.ts +0 -238
  96. package/src/cells/coerce.test.ts +0 -85
  97. package/src/cells/coerce.ts +0 -84
  98. package/src/clusterPaths.ts +0 -35
  99. package/src/columns/BadgeColumn.test.ts +0 -54
  100. package/src/columns/BadgeColumn.ts +0 -32
  101. package/src/columns/BooleanColumn.test.ts +0 -41
  102. package/src/columns/BooleanColumn.ts +0 -18
  103. package/src/columns/ColorColumn.test.ts +0 -37
  104. package/src/columns/ColorColumn.ts +0 -38
  105. package/src/columns/IconColumn.test.ts +0 -54
  106. package/src/columns/IconColumn.ts +0 -37
  107. package/src/columns/ImageColumn.test.ts +0 -41
  108. package/src/columns/ImageColumn.ts +0 -28
  109. package/src/columns/SelectColumn.ts +0 -98
  110. package/src/columns/TextColumn.test.ts +0 -190
  111. package/src/columns/TextColumn.ts +0 -20
  112. package/src/columns/TextInputColumn.ts +0 -68
  113. package/src/columns/ToggleColumn.ts +0 -46
  114. package/src/columns/editableColumns.test.ts +0 -238
  115. package/src/columns/index.ts +0 -9
  116. package/src/defaultGlobalPages.ts +0 -95
  117. package/src/defaultPages.test.ts +0 -634
  118. package/src/defaultPages.ts +0 -617
  119. package/src/defaultViewPage.test.ts +0 -147
  120. package/src/elements/Form.test.ts +0 -223
  121. package/src/elements/Form.ts +0 -416
  122. package/src/elements/ListTabs.ts +0 -28
  123. package/src/elements/Table.test.ts +0 -422
  124. package/src/elements/Table.ts +0 -850
  125. package/src/elements/TableGroup.test.ts +0 -260
  126. package/src/elements/TableGroup.ts +0 -334
  127. package/src/elements/dispatchAction.test.ts +0 -463
  128. package/src/elements/dispatchAction.ts +0 -355
  129. package/src/elements/dispatchForm.test.ts +0 -477
  130. package/src/elements/dispatchForm.ts +0 -1993
  131. package/src/elements/dispatchTable.test.ts +0 -1514
  132. package/src/elements/dispatchTable.ts +0 -745
  133. package/src/elements/index.ts +0 -21
  134. package/src/entries/BadgeEntry.ts +0 -39
  135. package/src/entries/CodeEntry.test.ts +0 -40
  136. package/src/entries/CodeEntry.ts +0 -52
  137. package/src/entries/ColorEntry.ts +0 -63
  138. package/src/entries/ComponentEntry.test.ts +0 -173
  139. package/src/entries/ComponentEntry.ts +0 -95
  140. package/src/entries/Entry.ts +0 -304
  141. package/src/entries/IconEntry.ts +0 -49
  142. package/src/entries/ImageEntry.ts +0 -61
  143. package/src/entries/KeyValueEntry.ts +0 -47
  144. package/src/entries/RepeatableEntry.test.ts +0 -239
  145. package/src/entries/RepeatableEntry.ts +0 -173
  146. package/src/entries/TextEntry.test.ts +0 -394
  147. package/src/entries/TextEntry.ts +0 -60
  148. package/src/entries/index.ts +0 -12
  149. package/src/entries/leaves.test.ts +0 -306
  150. package/src/entries/registry.ts +0 -54
  151. package/src/fields/BuilderField.test.ts +0 -1188
  152. package/src/fields/BuilderField.ts +0 -605
  153. package/src/fields/BuilderRelationship.test.ts +0 -811
  154. package/src/fields/CheckboxField.test.ts +0 -44
  155. package/src/fields/CheckboxField.ts +0 -27
  156. package/src/fields/CheckboxListField.test.ts +0 -99
  157. package/src/fields/CheckboxListField.ts +0 -66
  158. package/src/fields/ColorPickerField.test.ts +0 -33
  159. package/src/fields/ColorPickerField.ts +0 -25
  160. package/src/fields/DateField.ts +0 -54
  161. package/src/fields/DateTimeField.test.ts +0 -55
  162. package/src/fields/EmailField.ts +0 -16
  163. package/src/fields/Field.test.ts +0 -654
  164. package/src/fields/Field.ts +0 -817
  165. package/src/fields/FileUploadField.test.ts +0 -143
  166. package/src/fields/FileUploadField.ts +0 -159
  167. package/src/fields/HiddenField.test.ts +0 -27
  168. package/src/fields/HiddenField.ts +0 -28
  169. package/src/fields/KeyValueField.test.ts +0 -105
  170. package/src/fields/KeyValueField.ts +0 -55
  171. package/src/fields/MarkdownField.test.ts +0 -167
  172. package/src/fields/MarkdownField.ts +0 -162
  173. package/src/fields/NumberField.ts +0 -33
  174. package/src/fields/RadioField.test.ts +0 -94
  175. package/src/fields/RadioField.ts +0 -67
  176. package/src/fields/RepeaterField.test.ts +0 -1806
  177. package/src/fields/RepeaterField.ts +0 -939
  178. package/src/fields/RepeaterRelationship.test.ts +0 -1923
  179. package/src/fields/RepeaterSimple.test.ts +0 -248
  180. package/src/fields/RowButton.test.ts +0 -219
  181. package/src/fields/RowButton.ts +0 -135
  182. package/src/fields/SelectField.test.ts +0 -192
  183. package/src/fields/SelectField.ts +0 -235
  184. package/src/fields/SliderField.test.ts +0 -50
  185. package/src/fields/SliderField.ts +0 -53
  186. package/src/fields/SlugField.ts +0 -24
  187. package/src/fields/TagsInputField.test.ts +0 -154
  188. package/src/fields/TagsInputField.ts +0 -133
  189. package/src/fields/TextField.test.ts +0 -213
  190. package/src/fields/TextField.ts +0 -177
  191. package/src/fields/TextareaField.test.ts +0 -58
  192. package/src/fields/TextareaField.ts +0 -59
  193. package/src/fields/ToggleButtonsField.test.ts +0 -106
  194. package/src/fields/ToggleButtonsField.ts +0 -59
  195. package/src/fields/ToggleField.ts +0 -16
  196. package/src/fields/disableOptionsWhenSelectedInSiblingRepeaterItems.test.ts +0 -319
  197. package/src/fields/optionsResolver.ts +0 -95
  198. package/src/fields/resolveField.ts +0 -28
  199. package/src/filters/BooleanFilter.ts +0 -35
  200. package/src/filters/DateRangeFilter.test.ts +0 -194
  201. package/src/filters/DateRangeFilter.ts +0 -148
  202. package/src/filters/Filter.test.ts +0 -268
  203. package/src/filters/Filter.ts +0 -184
  204. package/src/filters/FormFilter.test.ts +0 -238
  205. package/src/filters/FormFilter.ts +0 -215
  206. package/src/filters/MultiSelectFilter.test.ts +0 -119
  207. package/src/filters/MultiSelectFilter.ts +0 -78
  208. package/src/filters/QueryBuilderFilter.test.ts +0 -662
  209. package/src/filters/QueryBuilderFilter.ts +0 -398
  210. package/src/filters/SelectFilter.ts +0 -46
  211. package/src/filters/TernaryFilter.test.ts +0 -160
  212. package/src/filters/TernaryFilter.ts +0 -72
  213. package/src/filters/TrashedFilter.test.ts +0 -149
  214. package/src/filters/TrashedFilter.ts +0 -55
  215. package/src/filters/queryBuilder/BooleanConstraint.ts +0 -31
  216. package/src/filters/queryBuilder/Constraint.ts +0 -115
  217. package/src/filters/queryBuilder/DateConstraint.ts +0 -69
  218. package/src/filters/queryBuilder/NumberConstraint.ts +0 -66
  219. package/src/filters/queryBuilder/SelectConstraint.ts +0 -72
  220. package/src/filters/queryBuilder/TextConstraint.ts +0 -64
  221. package/src/filters/queryBuilder/index.ts +0 -12
  222. package/src/icons/index.ts +0 -2
  223. package/src/icons/lucide.ts +0 -204
  224. package/src/icons/registry.test.ts +0 -56
  225. package/src/icons/registry.ts +0 -41
  226. package/src/icons/types.ts +0 -47
  227. package/src/index.ts +0 -525
  228. package/src/io/csv.test.ts +0 -142
  229. package/src/io/csv.ts +0 -170
  230. package/src/nestedRelationManagerData.test.ts +0 -547
  231. package/src/notifications/Notification.test.ts +0 -210
  232. package/src/notifications/Notification.ts +0 -354
  233. package/src/notifications/broadcast.test.ts +0 -110
  234. package/src/notifications/broadcast.ts +0 -95
  235. package/src/notifications/database.test.ts +0 -383
  236. package/src/notifications/database.ts +0 -398
  237. package/src/notifications/databaseNotifications.test.ts +0 -187
  238. package/src/notifications/dispatchNotificationAction.test.ts +0 -341
  239. package/src/notifications/dispatchNotificationAction.ts +0 -142
  240. package/src/notifications/flash.test.ts +0 -89
  241. package/src/notifications/flash.ts +0 -71
  242. package/src/notifications/index.ts +0 -45
  243. package/src/notifications/registerBroadcastAuth.test.ts +0 -134
  244. package/src/notifications/registerBroadcastAuth.ts +0 -100
  245. package/src/notifications/resolveSavedNotification.test.ts +0 -82
  246. package/src/notifications/resolveSavedNotification.ts +0 -59
  247. package/src/notifications/types.ts +0 -93
  248. package/src/orm/m2mAccessor.ts +0 -66
  249. package/src/orm/modelDefaults.test.ts +0 -633
  250. package/src/orm/modelDefaults.ts +0 -666
  251. package/src/pageData/breadcrumbs.ts +0 -288
  252. package/src/pageData/forms.ts +0 -578
  253. package/src/pageData/helpers.ts +0 -857
  254. package/src/pageData/misc.ts +0 -347
  255. package/src/pageData/navigation.ts +0 -842
  256. package/src/pageData/relationPages.ts +0 -1248
  257. package/src/pageData/relationTabs.ts +0 -286
  258. package/src/pageData/resourcePages.ts +0 -609
  259. package/src/pageData.test.ts +0 -1545
  260. package/src/pageData.ts +0 -341
  261. package/src/plugins/index.ts +0 -8
  262. package/src/plugins/themeEditor.test.ts +0 -36
  263. package/src/plugins/themeEditor.ts +0 -45
  264. package/src/react/AppShell.tsx +0 -251
  265. package/src/react/CollabExtensionFactoryRegistry.ts +0 -55
  266. package/src/react/CollabRoomContext.ts +0 -98
  267. package/src/react/CollabTextRendererRegistry.ts +0 -102
  268. package/src/react/CommandPalette.tsx +0 -375
  269. package/src/react/CurrentUserContext.tsx +0 -50
  270. package/src/react/CustomPageWrapperGate.tsx +0 -69
  271. package/src/react/CustomPageWrapperRegistry.ts +0 -45
  272. package/src/react/FieldFocusReporterRegistry.ts +0 -37
  273. package/src/react/FieldLabelSlotRegistry.ts +0 -30
  274. package/src/react/FieldPresenceRegistry.ts +0 -46
  275. package/src/react/FormCollabBindingRegistry.ts +0 -242
  276. package/src/react/FormStateContext.tsx +0 -591
  277. package/src/react/HeadHooks.tsx +0 -126
  278. package/src/react/MarkdownEditorRegistry.test.ts +0 -38
  279. package/src/react/MarkdownEditorRegistry.ts +0 -107
  280. package/src/react/NotificationActionStrip.tsx +0 -263
  281. package/src/react/NotificationBell.tsx +0 -426
  282. package/src/react/PendingSuggestionApplierRegistry.test.ts +0 -97
  283. package/src/react/PendingSuggestionApplierRegistry.ts +0 -98
  284. package/src/react/PendingSuggestionOverlayRegistry.ts +0 -54
  285. package/src/react/PendingSuggestionsContext.tsx +0 -172
  286. package/src/react/RecordWrapperGate.tsx +0 -58
  287. package/src/react/RecordWrapperRegistry.ts +0 -39
  288. package/src/react/RenderHookSlot.tsx +0 -32
  289. package/src/react/RightSidebar.tsx +0 -257
  290. package/src/react/RightSidebarContext.tsx +0 -234
  291. package/src/react/RightSidebarTrigger.tsx +0 -53
  292. package/src/react/RowCoordsContext.tsx +0 -23
  293. package/src/react/SchemaRenderer.tsx +0 -549
  294. package/src/react/SearchTrigger.tsx +0 -46
  295. package/src/react/ThemeProvider.tsx +0 -93
  296. package/src/react/ThemeSettingsPage.tsx +0 -579
  297. package/src/react/ThemeToggle.tsx +0 -20
  298. package/src/react/Toaster.tsx +0 -158
  299. package/src/react/UserMenu.tsx +0 -196
  300. package/src/react/WidgetDataContext.tsx +0 -157
  301. package/src/react/cells/EditableCell.tsx +0 -389
  302. package/src/react/component-slots.test.ts +0 -103
  303. package/src/react/component-slots.ts +0 -116
  304. package/src/react/fieldJsHandler.test.ts +0 -166
  305. package/src/react/fieldJsHandler.ts +0 -79
  306. package/src/react/fields/BuilderInput.tsx +0 -1078
  307. package/src/react/fields/CheckboxInput.tsx +0 -39
  308. package/src/react/fields/CheckboxListInput.tsx +0 -102
  309. package/src/react/fields/ColorInput.tsx +0 -71
  310. package/src/react/fields/DateFieldInput.tsx +0 -70
  311. package/src/react/fields/DateTimeInput.tsx +0 -62
  312. package/src/react/fields/FieldShell.tsx +0 -348
  313. package/src/react/fields/FileUploadInput.tsx +0 -639
  314. package/src/react/fields/HiddenInput.tsx +0 -17
  315. package/src/react/fields/KeyValueInput.tsx +0 -230
  316. package/src/react/fields/MarkdownInput.tsx +0 -560
  317. package/src/react/fields/RadioInput.tsx +0 -81
  318. package/src/react/fields/RepeaterInput.test.ts +0 -116
  319. package/src/react/fields/RepeaterInput.tsx +0 -1420
  320. package/src/react/fields/SelectFieldInput.tsx +0 -280
  321. package/src/react/fields/SliderInput.tsx +0 -81
  322. package/src/react/fields/TagsInput.tsx +0 -283
  323. package/src/react/fields/TextLikeInput.tsx +0 -256
  324. package/src/react/fields/ToggleButtonsInput.tsx +0 -60
  325. package/src/react/fields/ToggleFieldInput.tsx +0 -56
  326. package/src/react/fields/relationshipRenameDispatch.test.ts +0 -106
  327. package/src/react/fields/relationshipRenameDispatch.ts +0 -97
  328. package/src/react/fields/repeaterReconcile.test.ts +0 -114
  329. package/src/react/fields/repeaterReconcile.ts +0 -104
  330. package/src/react/fields/rowChromeButton.tsx +0 -336
  331. package/src/react/fields/rowState.ts +0 -106
  332. package/src/react/fields/syncRowGates.test.ts +0 -202
  333. package/src/react/fields/syncRowGates.ts +0 -66
  334. package/src/react/fields/textInputControls.tsx +0 -238
  335. package/src/react/fields/useRowReorderDnd.ts +0 -78
  336. package/src/react/formStateHelpers.test.ts +0 -508
  337. package/src/react/formStateHelpers.ts +0 -381
  338. package/src/react/hooks/use-mobile.ts +0 -19
  339. package/src/react/icon-context.tsx +0 -60
  340. package/src/react/index.ts +0 -194
  341. package/src/react/layouts/SidebarLayout.tsx +0 -250
  342. package/src/react/layouts/TopbarLayout.tsx +0 -258
  343. package/src/react/navigate.tsx +0 -37
  344. package/src/react/onProviderSynced.test.ts +0 -90
  345. package/src/react/parseRecordEditUrl.test.ts +0 -122
  346. package/src/react/parseRecordEditUrl.ts +0 -94
  347. package/src/react/persistedState.ts +0 -40
  348. package/src/react/registry.ts +0 -48
  349. package/src/react/right-panel-registry.tsx +0 -47
  350. package/src/react/schemaRenderer/AlertRenderer.tsx +0 -112
  351. package/src/react/schemaRenderer/EntryRenderer.tsx +0 -501
  352. package/src/react/schemaRenderer/SectionRenderer.tsx +0 -120
  353. package/src/react/schemaRenderer/SimpleElements.tsx +0 -306
  354. package/src/react/schemaRenderer/TabsRenderer.tsx +0 -62
  355. package/src/react/schemaRenderer/WizardRenderer.tsx +0 -338
  356. package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +0 -177
  357. package/src/react/schemaRenderer/action/ActionModalDialog.tsx +0 -273
  358. package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +0 -61
  359. package/src/react/schemaRenderer/action/HandlerActionButton.tsx +0 -43
  360. package/src/react/schemaRenderer/action/MethodActionButton.tsx +0 -64
  361. package/src/react/schemaRenderer/action/buttons.tsx +0 -99
  362. package/src/react/schemaRenderer/action/helpers.ts +0 -140
  363. package/src/react/schemaRenderer/action/renderAction.tsx +0 -245
  364. package/src/react/schemaRenderer/columnFormat.ts +0 -65
  365. package/src/react/schemaRenderer/constants.ts +0 -50
  366. package/src/react/schemaRenderer/form/FormRenderer.tsx +0 -274
  367. package/src/react/schemaRenderer/form/renderField.tsx +0 -511
  368. package/src/react/schemaRenderer/helpers.tsx +0 -81
  369. package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +0 -308
  370. package/src/react/schemaRenderer/table/TableRenderer.tsx +0 -123
  371. package/src/react/schemaRenderer/table/TableRendererBody.tsx +0 -974
  372. package/src/react/schemaRenderer/table/filters.tsx +0 -1233
  373. package/src/react/schemaRenderer/table/formatCell.tsx +0 -264
  374. package/src/react/schemaRenderer/table/links.tsx +0 -112
  375. package/src/react/schemaRenderer/table/renderRowActions.tsx +0 -52
  376. package/src/react/schemaRenderer/table/url.tsx +0 -143
  377. package/src/react/theme-preview/apply.ts +0 -99
  378. package/src/react/theme-preview/build-html.ts +0 -436
  379. package/src/react/ui/button.tsx +0 -51
  380. package/src/react/ui/calendar.tsx +0 -67
  381. package/src/react/ui/checkbox.tsx +0 -29
  382. package/src/react/ui/dialog.tsx +0 -108
  383. package/src/react/ui/dropdown-menu.tsx +0 -97
  384. package/src/react/ui/input.tsx +0 -20
  385. package/src/react/ui/label.tsx +0 -21
  386. package/src/react/ui/popover.tsx +0 -50
  387. package/src/react/ui/select.tsx +0 -169
  388. package/src/react/ui/separator.tsx +0 -25
  389. package/src/react/ui/sheet.tsx +0 -136
  390. package/src/react/ui/sidebar.tsx +0 -723
  391. package/src/react/ui/skeleton.tsx +0 -13
  392. package/src/react/ui/slider.tsx +0 -34
  393. package/src/react/ui/switch.tsx +0 -28
  394. package/src/react/ui/table.tsx +0 -105
  395. package/src/react/ui/tabs.tsx +0 -63
  396. package/src/react/ui/textarea.tsx +0 -18
  397. package/src/react/ui/tooltip.tsx +0 -64
  398. package/src/react/useResizableWidth.ts +0 -139
  399. package/src/react/utils.ts +0 -6
  400. package/src/react/widgetRegistry.test.ts +0 -43
  401. package/src/react/widgetRegistry.ts +0 -50
  402. package/src/react/widgets/StatsOverviewRenderer.tsx +0 -232
  403. package/src/react/widgets/TableWidgetRenderer.tsx +0 -231
  404. package/src/react/widgets/ViewRenderer.tsx +0 -71
  405. package/src/relationManagerData.test.ts +0 -1595
  406. package/src/richtext/index.ts +0 -8
  407. package/src/richtext/registry.ts +0 -89
  408. package/src/routes/globals.ts +0 -148
  409. package/src/routes/guard.test.ts +0 -325
  410. package/src/routes/helpers.ts +0 -704
  411. package/src/routes/pages.ts +0 -175
  412. package/src/routes/panel.ts +0 -204
  413. package/src/routes/relations.ts +0 -1243
  414. package/src/routes/resources.ts +0 -781
  415. package/src/routes/theme.ts +0 -91
  416. package/src/routes-nested-relations.test.ts +0 -676
  417. package/src/routes-relations.test.ts +0 -972
  418. package/src/routes.test.ts +0 -2027
  419. package/src/routes.ts +0 -303
  420. package/src/schema/Alert.test.ts +0 -109
  421. package/src/schema/Alert.ts +0 -131
  422. package/src/schema/Block.ts +0 -169
  423. package/src/schema/Breadcrumbs.ts +0 -40
  424. package/src/schema/Card.ts +0 -35
  425. package/src/schema/Divider.ts +0 -20
  426. package/src/schema/Element.ts +0 -219
  427. package/src/schema/EmptyState.test.ts +0 -37
  428. package/src/schema/EmptyState.ts +0 -63
  429. package/src/schema/Fieldset.ts +0 -43
  430. package/src/schema/Grid.ts +0 -43
  431. package/src/schema/Group.ts +0 -30
  432. package/src/schema/Heading.ts +0 -39
  433. package/src/schema/Html.ts +0 -67
  434. package/src/schema/Icon.ts +0 -54
  435. package/src/schema/Image.ts +0 -57
  436. package/src/schema/LinkTag.ts +0 -41
  437. package/src/schema/Markdown.ts +0 -85
  438. package/src/schema/MetaTag.ts +0 -41
  439. package/src/schema/RelationTabs.ts +0 -71
  440. package/src/schema/ScriptTag.ts +0 -55
  441. package/src/schema/Section.ts +0 -160
  442. package/src/schema/ServerDataElement.test.ts +0 -140
  443. package/src/schema/ServerDataElement.ts +0 -156
  444. package/src/schema/SlotComponent.test.ts +0 -77
  445. package/src/schema/SlotComponent.ts +0 -71
  446. package/src/schema/Split.ts +0 -50
  447. package/src/schema/Stat.test.ts +0 -118
  448. package/src/schema/Stat.ts +0 -154
  449. package/src/schema/StatsOverview.test.ts +0 -141
  450. package/src/schema/StatsOverview.ts +0 -119
  451. package/src/schema/StyleTag.ts +0 -35
  452. package/src/schema/TableWidget.test.ts +0 -297
  453. package/src/schema/TableWidget.ts +0 -289
  454. package/src/schema/Tabs.ts +0 -79
  455. package/src/schema/Text.ts +0 -58
  456. package/src/schema/UnorderedList.ts +0 -49
  457. package/src/schema/View.test.ts +0 -111
  458. package/src/schema/View.ts +0 -127
  459. package/src/schema/Wizard.ts +0 -220
  460. package/src/schema/containers.test.ts +0 -564
  461. package/src/schema/headTags.test.ts +0 -134
  462. package/src/schema/index.ts +0 -40
  463. package/src/schema/primes.test.ts +0 -269
  464. package/src/schema/resolveSchema.test.ts +0 -379
  465. package/src/schema/resolveSchema.ts +0 -917
  466. package/src/schema/sanitize.ts +0 -58
  467. package/src/search.test.ts +0 -446
  468. package/src/search.ts +0 -178
  469. package/src/sessionFilters.test.ts +0 -375
  470. package/src/sessionFilters.ts +0 -143
  471. package/src/slot-components/index.ts +0 -10
  472. package/src/slot-components/registry.ts +0 -56
  473. package/src/styles/file-upload.css +0 -13
  474. package/src/summarizers/Summarizer.test.ts +0 -84
  475. package/src/summarizers/Summarizer.ts +0 -123
  476. package/src/summarizers/index.ts +0 -11
  477. package/src/theme/base-colors.ts +0 -68
  478. package/src/theme/chart-colors.ts +0 -50
  479. package/src/theme/colors.ts +0 -447
  480. package/src/theme/generate-css.test.ts +0 -139
  481. package/src/theme/generate-css.ts +0 -44
  482. package/src/theme/generate-scale.test.ts +0 -106
  483. package/src/theme/generate-scale.ts +0 -97
  484. package/src/theme/icon-map.ts +0 -42
  485. package/src/theme/index.ts +0 -34
  486. package/src/theme/migrate.test.ts +0 -178
  487. package/src/theme/migrate.ts +0 -81
  488. package/src/theme/presets.ts +0 -135
  489. package/src/theme/radius.ts +0 -18
  490. package/src/theme/resolve.test.ts +0 -238
  491. package/src/theme/resolve.ts +0 -96
  492. package/src/theme/spacing.ts +0 -18
  493. package/src/theme/storage.test.ts +0 -126
  494. package/src/theme/storage.ts +0 -106
  495. package/src/theme/theme-colors.ts +0 -88
  496. package/src/theme/types.ts +0 -125
  497. package/src/uploads/UploadAdapter.ts +0 -35
  498. package/src/uploads/index.ts +0 -2
  499. package/src/uploads/localUpload.test.ts +0 -70
  500. package/src/uploads/localUpload.ts +0 -84
  501. package/src/validation/Validator.ts +0 -49
  502. package/src/validation/index.ts +0 -28
  503. package/src/validation/rules.ts +0 -78
  504. package/src/validation/runValidators.ts +0 -435
  505. package/src/validation/uniqueValidator.test.ts +0 -196
  506. package/src/validation/uniqueValidator.ts +0 -133
  507. package/src/validation/validators.test.ts +0 -268
  508. package/src/vite.test.ts +0 -184
  509. package/src/vite.ts +0 -787
  510. package/src/widgets/index.ts +0 -10
  511. package/src/widgets/registry.ts +0 -45
  512. package/src/widgets.test.ts +0 -592
  513. package/tsconfig.build.json +0 -11
  514. package/tsconfig.json +0 -4
  515. package/tsconfig.test.json +0 -10
  516. package/views/react/Dashboard.tsx +0 -27
  517. package/views/react/Resources/Form.tsx +0 -102
  518. package/views/react/Resources/Index.tsx +0 -49
@@ -1,1248 +0,0 @@
1
- import type { Pilotiq, PilotiqConfig } from '../Pilotiq.js'
2
- import type { ResourceClass } from '../Resource.js'
3
- import { resourceBasePath } from '../clusterPaths.js'
4
- import { Element } from '../schema/Element.js'
5
- import { resolveSchema, type SchemaContext } from '../schema/resolveSchema.js'
6
- import { Form } from '../elements/Form.js'
7
- import { Table } from '../elements/Table.js'
8
- import { Column } from '../Column.js'
9
- import { findForms } from '../elements/dispatchForm.js'
10
- import { Filter } from '../filters/Filter.js'
11
- import { TrashedFilter } from '../filters/TrashedFilter.js'
12
- import { loadTableRecords } from '../elements/dispatchTable.js'
13
- import { consumeFlashedNotifications } from '../notifications/flash.js'
14
- import { serializeIcon, type SerializedIcon } from '../icons/types.js'
15
- import {
16
- RelationManager,
17
- safeManagerPolicy as safeManagerPolicyImpl,
18
- type ManagerCanMethod as ManagerCanMethodType,
19
- type RelationManagerContext,
20
- } from '../RelationManager.js'
21
- import { RelationTabs } from '../schema/RelationTabs.js'
22
- import {
23
- findRecord,
24
- getMorphRelationDescriptor,
25
- getPrimaryKey,
26
- getRelationType,
27
- modelLoadRecord,
28
- modelRelationTableRecords,
29
- modelSave,
30
- type ModelLike,
31
- type ModelQuery,
32
- } from '../orm/modelDefaults.js'
33
- import { normalizeRelationMode, type RelationMode } from '../RelationManager.js'
34
- import {
35
- nestedRelationCreateBreadcrumbs,
36
- nestedRelationEditBreadcrumbs,
37
- nestedRelationListBreadcrumbs,
38
- nestedRelationViewBreadcrumbs,
39
- relationCreateBreadcrumbs,
40
- relationEditBreadcrumbs,
41
- relationListBreadcrumbs,
42
- relationViewBreadcrumbs,
43
- type RelationChainStep,
44
- } from './breadcrumbs.js'
45
- import {
46
- applyFillPipeline,
47
- applyRelationshipBuilderFill,
48
- applyRelationshipRepeaterFill,
49
- callPageSchema,
50
- resolveServerDataElements,
51
- tagActionDispatch,
52
- tagCellEditUrls,
53
- tagFormActions,
54
- tagTableDeferred,
55
- tagTableReorderUrls,
56
- tagWidgetUrls,
57
- uploadCtx,
58
- userCtx,
59
- } from './helpers.js'
60
- import {
61
- applyRoleHooks,
62
- panelInfo,
63
- type PanelInfoRoute,
64
- } from './navigation.js'
65
- import {
66
- buildNestedRelationTabs,
67
- buildRelationTabs,
68
- deriveParentTitle,
69
- safeBool,
70
- } from './relationTabs.js'
71
-
72
- // ─── Plan #11 relation-manager data builders ────────────────
73
- //
74
- // Depth-1 + depth-2 relation manager URL roles (list / create / view /
75
- // edit) for a parent record's manager-scoped projection. Each builder
76
- // loads the parent + manager + (optionally) child, runs the two-layer
77
- // policy gate (parent canAccess+canEdit, then manager-scope canX), and
78
- // resolves a tab strip + form/table from the manager's configurators.
79
- // Submit-side handlers live in `routes.ts`.
80
-
81
-
82
-
83
-
84
- // ─── Plan #11 relation-manager data builder ─────────────────
85
-
86
- /**
87
- * Plan #11 — three scopes a single relation-manager URL space resolves to:
88
- *
89
- * list: GET {base}/{slug}/:id/{rel}
90
- * create: GET {base}/{slug}/:id/{rel}/create
91
- * edit: GET {base}/{slug}/:id/{rel}/{childId}/edit
92
- *
93
- * Each carries enough state for `relationManagerData` to load the right
94
- * parent + (for edit) child + form/table context. Submit-side handlers
95
- * live in `routes.ts` and reuse `dispatchFormSubmit`.
96
- */
97
- export type RelationManagerScope =
98
- | { kind: 'relation-list'; slug: string; recordId: string; relationship: string; query?: Record<string, string> }
99
- | { kind: 'relation-create'; slug: string; recordId: string; relationship: string; prefill?: { values?: Record<string, unknown>; errors?: Record<string, string[]> } }
100
- | { kind: 'relation-view'; slug: string; recordId: string; relationship: string; childId: string }
101
- | { kind: 'relation-edit'; slug: string; recordId: string; relationship: string; childId: string; prefill?: { values?: Record<string, unknown>; errors?: Record<string, string[]> } }
102
- // Phase B nested resources — the leaf is one manager deeper than the
103
- // depth-1 variants. The two-step `chain` carries the (recordId,
104
- // relationship) for each layer; the trailing `childId` (when present)
105
- // is the leaf record's id under chain[1].
106
- | { kind: 'nested-relation-list'; slug: string; chain: [RelationChainStep, RelationChainStep]; query?: Record<string, string> }
107
- | { kind: 'nested-relation-create'; slug: string; chain: [RelationChainStep, RelationChainStep]; prefill?: { values?: Record<string, unknown>; errors?: Record<string, string[]> } }
108
- | { kind: 'nested-relation-view'; slug: string; chain: [RelationChainStep, RelationChainStep]; childId: string }
109
- | { kind: 'nested-relation-edit'; slug: string; chain: [RelationChainStep, RelationChainStep]; childId: string; prefill?: { values?: Record<string, unknown>; errors?: Record<string, string[]> } }
110
-
111
- /** Phase B — one parent layer in a nested-resources URL chain. The list
112
- * of these identifies a path through the manager tree:
113
- * `[ { recordId: '123', relationship: 'comments' } ]` picks comment
114
- * "456 under post 123" when paired with `childId: '456'`. */
115
- // `RelationChainStep` now lives in `./pageData/breadcrumbs.ts` since
116
- // both the breadcrumb builders and the depth-2 relation manager
117
- // builders consume it. Re-exported below for back-compat with any
118
- // external callsite that imports through `pageData.js`.
119
-
120
- /**
121
- * Failure outcomes the data builder discriminates back to the route
122
- * handler, which decides between 403 / 404 / HTML / JSON shapes.
123
- *
124
- * `null` — unknown panel / parent / manager / child;
125
- * route returns 404
126
- * `{ ok: false, status: 403 }` — policy denied; route returns 403
127
- *
128
- * Success returns the schemaData payload directly (a record, not
129
- * tagged) for parity with `resourceIndexData / resourceCreateData`.
130
- */
131
- export type RelationManagerResult =
132
- | Record<string, unknown>
133
- | { ok: false; status: 403 }
134
- | null
135
-
136
- /**
137
- * Discover the related Resource for a manager. Order:
138
- * 1. `M.relatedResource` explicit override (skip discovery).
139
- * 2. Rudder ORM convention: walk
140
- * `R.model.relations[manager.relationship].model()` and find
141
- * `cfg.resources[i].model === relatedModel`.
142
- * 3. Otherwise undefined — caller must error or fall back.
143
- *
144
- * A returned Resource is the one whose `model` backs the related
145
- * table. Callers use it for `Related.model.find(childId)`,
146
- * `Related.canEdit(user, child)`, and the auto-wired form save handler.
147
- */
148
- export function findRelatedResource(
149
- M: typeof RelationManager,
150
- R: ResourceClass,
151
- cfg: ReturnType<Pilotiq['getConfig']>,
152
- ): ResourceClass | undefined {
153
- if (M.relatedResource) return M.relatedResource
154
- const ParentModel = R.model as unknown as { relations?: Record<string, { model?: () => unknown }> } | undefined
155
- if (!ParentModel) return undefined
156
- const def = ParentModel.relations?.[M.getRelationship()]
157
- const RelatedModel = typeof def?.model === 'function' ? def.model() : undefined
158
- if (!RelatedModel) return undefined
159
- return cfg.resources.find(r => (r.model as unknown) === RelatedModel)
160
- }
161
-
162
- /** Find a registered manager on a Resource by its relationship key.
163
- * Throws on unknown manager — so the route can 404 cleanly. */
164
- function findManager(
165
- R: ResourceClass,
166
- relationship: string,
167
- ): typeof RelationManager | undefined {
168
- return R.relations().find(M => {
169
- try { return M.getRelationship() === relationship } catch { return false }
170
- })
171
- }
172
-
173
- /**
174
- * Verify a child record actually belongs to the given parent under the
175
- * declared relationship. Anti-IDOR — without this an attacker can swap
176
- * the `:childId` segment to load any related-model row regardless of
177
- * whether it's actually owned by the parent.
178
- *
179
- * Strategy: re-resolve the parent's relation query and check whether
180
- * the child's primary key shows up in `where(pk, '=', childId).paginate(1, 1)`.
181
- * Yes, it's a second round-trip — but it's the single point of trust
182
- * for IDOR safety, and it fits naturally into the same query path
183
- * `modelRelationTableRecords` uses.
184
- */
185
- async function childBelongsToParent(
186
- parentModel: ModelLike,
187
- parent: unknown,
188
- relationship: string,
189
- childPk: string,
190
- childId: string,
191
- ): Promise<boolean> {
192
- try {
193
- const q: ModelQuery = (parentModel.relatedQuery
194
- ? parentModel.relatedQuery(parent, relationship)
195
- : (parent as { related: (n: string) => ModelQuery }).related(relationship))
196
- .where(childPk, '=', childId)
197
- if (q.first) return (await q.first()) !== null
198
- const result = await q.paginate(1, 1)
199
- return result.total > 0
200
- } catch {
201
- return false
202
- }
203
- }
204
-
205
- /**
206
- * Auto-wire the manager's table records loader against the parent's
207
- * relation query when the user didn't set `Table.records()` themselves.
208
- * Mirrors `defaultPages`'s wiring of `Table.records()` from `R.model`
209
- * for the resource list page.
210
- */
211
- function autoWireManagerTable(
212
- table: Table,
213
- parentModel: ModelLike,
214
- parent: unknown,
215
- relationship: string,
216
- ): void {
217
- if (table.getRecords()) return // user wired it explicitly
218
- table.records(modelRelationTableRecords(parentModel, parent, relationship, table))
219
- }
220
-
221
- /**
222
- * Plan #13 polish — auto-inject `TrashedFilter` on a relation manager's
223
- * table when the **related** Resource opts into soft deletes. Mirrors the
224
- * resource-list pattern in `defaultPages.applyTableDefaults`. The check
225
- * is on the related Resource (not the manager), because soft-delete is a
226
- * model-level capability — if the child model supports trashing, the
227
- * manager's table should expose the toggle.
228
- *
229
- * No-op when:
230
- * - the related Resource hasn't set `softDeletes = true`
231
- * - the user already attached a `TrashedFilter` in `M.table()`
232
- */
233
- function injectManagerTrashedFilter(
234
- table: Table,
235
- Related: ResourceClass | undefined,
236
- ): void {
237
- if (!Related?.softDeletes) return
238
- const children = table.getChildren() ?? []
239
- const hasTrashed = children.some(c => c instanceof TrashedFilter)
240
- if (hasTrashed) return
241
- const existing = children.filter(c => c instanceof Filter) as Filter[]
242
- table.filters([...existing, TrashedFilter.make()])
243
- }
244
-
245
- /**
246
- * Auto-wire the manager's form save + loadRecord handlers against the
247
- * **related** Resource's `model` when the user didn't set them. The
248
- * route handler is responsible for stamping the parent context
249
- * (parent, parentRecord, parentId, relationship) onto the
250
- * `FormContext` so user-supplied `mutateDataBeforeCreate` etc. can
251
- * read them.
252
- */
253
- function autoWireManagerForm(form: Form, Related: ResourceClass): void {
254
- const RelatedModel = Related.model
255
- if (!RelatedModel) return
256
- if (!form.getSave()) form.save(modelSave(RelatedModel))
257
- if (!form.getLoadRecord()) form.loadRecord(modelLoadRecord(Related))
258
- }
259
-
260
- /** Plan #11 — authorization predicate names a `RelationManager` carries.
261
- * Re-exported from `RelationManager.ts`. */
262
- export type ManagerCanMethod = ManagerCanMethodType
263
-
264
- /** Plan #11 — authorize a relation-manager action with sensible defaults.
265
- * Re-exported from `RelationManager.ts` so external callers (route
266
- * handlers, third-party plugins) keep their existing import path. */
267
- export const safeManagerPolicy = safeManagerPolicyImpl
268
-
269
- /**
270
- * Plan #11 — render data for the three relation-manager URL scopes.
271
- * Mirrors the resource* builders' shape so routes and Vike +data hooks
272
- * consume identical props. Authorization runs inline (parent
273
- * `canAccess + canEdit(parent)` then manager-scoped predicate); IDOR
274
- * check on `relation-edit` runs against the parent's relation query.
275
- *
276
- * Returns:
277
- * - `null` when panel / parent / manager / child don't exist.
278
- * - `{ ok: false, status: 403 }` when authorization denies.
279
- * - the props record on success (route picks SSR view / SPA prop
280
- * downstream).
281
- */
282
- export async function relationManagerData(
283
- pilotiq: Pilotiq,
284
- scope: RelationManagerScope,
285
- req?: unknown,
286
- ): Promise<RelationManagerResult> {
287
- // Phase B nested-relation-* scopes split out into their own pipeline
288
- // — the chain walking + per-layer auth differs enough from the
289
- // depth-1 path that interleaving them would mostly hurt readability.
290
- if (scope.kind === 'nested-relation-list'
291
- || scope.kind === 'nested-relation-create'
292
- || scope.kind === 'nested-relation-view'
293
- || scope.kind === 'nested-relation-edit') {
294
- return nestedRelationManagerData(pilotiq, scope, req)
295
- }
296
-
297
- const cfg = pilotiq.getConfig()
298
-
299
- const R = pilotiq.findResource(scope.slug)
300
- if (!R) return null
301
-
302
- const M = findManager(R, scope.relationship)
303
- if (!M) return null
304
-
305
- const user = await pilotiq.resolveUser(req)
306
-
307
- // Layer 1: parent access. canAccess gates the resource entirely;
308
- // canEdit gates managing its relations (managers are read-write
309
- // surfaces — read-only inline views opt in by overriding the
310
- // manager's can*). Cluster gate composes with R.canAccess — both
311
- // must pass when the parent resource is inside a cluster.
312
- const [clusterOk, accessOk] = await Promise.all([
313
- R.cluster ? safeBool(() => R.cluster!.canAccess(user)) : Promise.resolve(true),
314
- safeBool(() => R.canAccess(user)),
315
- ])
316
- if (!clusterOk || !accessOk) return { ok: false, status: 403 }
317
-
318
- if (!R.model) {
319
- // Without a model on the parent we can't load the parent record,
320
- // and without that we can't IDOR-check children. Point users at
321
- // the missing wiring rather than silent 500s.
322
- throw new Error(
323
- `[Pilotiq] Resource "${R.name}" has relations(${M.name}) but no static model. ` +
324
- `Set Resource.model = … to enable relation managers, or remove the manager.`,
325
- )
326
- }
327
-
328
- const parentRecord = await findRecord(R, scope.recordId, { user }).catch(() => undefined)
329
- if (!parentRecord) return null
330
-
331
- if (!await safeBool(() => R.canEdit(user, parentRecord))) return { ok: false, status: 403 }
332
-
333
- // Read the relation type off the parent's relations map once,
334
- // normalize to the six-way `RelationMode` the manager-side logic
335
- // uses. `belongsToMany` / `morphToMany` (owning polymorphic) /
336
- // `morphedByMany` (inverse polymorphic) all flip into pivot-mutation
337
- // mode (attach / detach / sync — same accessor surface), `morphMany|
338
- // morphOne` collapses to `'morphMany'` (parent-side polymorphic —
339
- // auto-fills morph columns on create), `morphTo` is the child-side
340
- // polymorphic (no auto-actions; requires explicit `M.relatedResource`).
341
- // Everything else collapses to `'hasMany'`.
342
- const relationType = getRelationType(R.model, scope.relationship)
343
- const mode: RelationMode = normalizeRelationMode(relationType)
344
-
345
- const Related = findRelatedResource(M, R, cfg)
346
- // Related Resource is required for: edit/create form auto-wire,
347
- // child loading on edit, related URL generation. Throw when missing
348
- // *only* if we'd otherwise need it — for `relation-list` it's
349
- // optional (the table can be hand-wired by the user).
350
- const needRelated = scope.kind !== 'relation-list'
351
- if (needRelated && !Related) {
352
- throw new Error(
353
- `[Pilotiq] RelationManager ${M.name} on ${R.name} could not resolve its related Resource. ` +
354
- `Set static relatedResource on the manager, or ensure the parent's model declares relations[${JSON.stringify(M.getRelationship())}].`,
355
- )
356
- }
357
-
358
- switch (scope.kind) {
359
- case 'relation-list':
360
- return buildRelationListData(pilotiq, R, M, Related, parentRecord, scope, req, user, mode)
361
- case 'relation-create':
362
- return buildRelationCreateData(pilotiq, R, M, Related!, parentRecord, scope, req, user, mode)
363
- case 'relation-view':
364
- return buildRelationViewData(pilotiq, R, M, Related!, parentRecord, scope, req, user, mode)
365
- case 'relation-edit':
366
- return buildRelationEditData(pilotiq, R, M, Related!, parentRecord, scope, req, user, mode)
367
- }
368
- }
369
-
370
- async function buildRelationListData(
371
- pilotiq: Pilotiq,
372
- R: ResourceClass,
373
- M: typeof RelationManager,
374
- Related: ResourceClass | undefined,
375
- parentRecord: unknown,
376
- scope: Extract<RelationManagerScope, { kind: 'relation-list' }>,
377
- req: unknown,
378
- user: unknown,
379
- mode: RelationMode,
380
- ): Promise<RelationManagerResult> {
381
- if (!await safeManagerPolicy(M, 'canViewAny', Related, user, parentRecord)) return { ok: false, status: 403 }
382
-
383
- const cfg = pilotiq.getConfig()
384
- const base = cfg.path
385
- const resourceBase = resourceBasePath(base, R)
386
- const listUrl = `${resourceBase}/${scope.recordId}/${scope.relationship}`
387
-
388
- // Build a single Table by piping a fresh Table through M.table(table, ctx).
389
- // Context lets the user wire `Action.relationCreate / relationEdit /
390
- // relationDelete(M, ctx)` factories inside `static table()` to template
391
- // URLs without threading basePath / parentId by hand.
392
- const managerCtx = buildRelationManagerCtx(base, scope, parentRecord, Related, mode)
393
- const table = M.table(Table.make(), managerCtx)
394
- autoWireManagerTable(table, R.model as ModelLike, parentRecord, scope.relationship)
395
- injectManagerTrashedFilter(table, Related)
396
-
397
- const ctx: SchemaContext = uploadCtx(userCtx({
398
- mode: 'table',
399
- basePath: base,
400
- record: parentRecord,
401
- }, user), cfg)
402
-
403
- const elements: Element[] = [table]
404
- tagActionDispatch(elements, listUrl)
405
- // Independent work — records load against the parent's relation
406
- // accessor; tabs probe per-tab `safeManagerPolicy` predicates that
407
- // don't touch the records pipeline.
408
- const [, tabs] = await Promise.all([
409
- loadTableRecords(elements, scope.query ?? {}, listUrl, user),
410
- buildRelationTabs(R, scope.recordId, base, scope.relationship, user, parentRecord),
411
- ])
412
- if (tabs) elements.unshift(tabs)
413
-
414
- const breadcrumbs = relationListBreadcrumbs(
415
- cfg, R, M, scope.recordId, deriveParentTitle(R, parentRecord),
416
- )
417
- if (breadcrumbs) elements.unshift(breadcrumbs)
418
-
419
- const relationListRoute: PanelInfoRoute = { resource: R, recordId: scope.recordId }
420
- const [panel, schemaData] = await Promise.all([
421
- panelInfo(pilotiq, req, relationListRoute),
422
- resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'relation-list', metas, relationListRoute)),
423
- ])
424
-
425
- return {
426
- pageType: 'relation-list',
427
- panel,
428
- resource: { name: R.name, label: R.label, labelSingular: R.labelSingular, slug: scope.slug, icon: serializeIcon(R.icon, R.name) },
429
- relation: {
430
- name: M.name,
431
- label: M.getLabel(),
432
- labelSingular: M.getLabelSingular(),
433
- relationship: scope.relationship,
434
- icon: M.getIcon() ? serializeIcon(M.getIcon()!, M.name) : undefined,
435
- relatedSlug: Related?.getSlug(),
436
- },
437
- parent: {
438
- id: scope.recordId,
439
- title: deriveParentTitle(R, parentRecord),
440
- },
441
- basePath: base,
442
- layout: cfg.layout,
443
- schemaData,
444
- notifications: consumeFlashedNotifications(req),
445
- }
446
- }
447
-
448
- async function buildRelationCreateData(
449
- pilotiq: Pilotiq,
450
- R: ResourceClass,
451
- M: typeof RelationManager,
452
- Related: ResourceClass,
453
- parentRecord: unknown,
454
- scope: Extract<RelationManagerScope, { kind: 'relation-create' }>,
455
- req: unknown,
456
- user: unknown,
457
- mode: RelationMode,
458
- ): Promise<RelationManagerResult> {
459
- if (!await safeManagerPolicy(M, 'canCreate', Related, user, parentRecord)) return { ok: false, status: 403 }
460
-
461
- const cfg = pilotiq.getConfig()
462
- const base = cfg.path
463
- const resourceBase = resourceBasePath(base, R)
464
- const createUrl = `${resourceBase}/${scope.recordId}/${scope.relationship}/create`
465
-
466
- const managerCtx = buildRelationManagerCtx(base, scope, parentRecord, Related, mode)
467
- const form = M.form(Form.make(), managerCtx)
468
- if (Related.model) autoWireManagerForm(form, Related)
469
-
470
- const elements: Element[] = [form]
471
- tagFormActions(elements, createUrl)
472
-
473
- if (scope.prefill) {
474
- if (scope.prefill.values) form.withValues(scope.prefill.values)
475
- if (scope.prefill.errors) form.withErrors(scope.prefill.errors)
476
- }
477
-
478
- const tabs = await buildRelationTabs(R, scope.recordId, base, scope.relationship, user, parentRecord)
479
- if (tabs) elements.unshift(tabs)
480
-
481
- const breadcrumbs = relationCreateBreadcrumbs(
482
- cfg, R, M, scope.recordId, deriveParentTitle(R, parentRecord),
483
- )
484
- if (breadcrumbs) elements.unshift(breadcrumbs)
485
-
486
- const ctx: SchemaContext = uploadCtx(userCtx({
487
- mode: 'create',
488
- basePath: base,
489
- record: parentRecord,
490
- }, user), cfg)
491
-
492
- const relationCreateRoute: PanelInfoRoute = { resource: R, recordId: scope.recordId }
493
- const [panel, schemaData] = await Promise.all([
494
- panelInfo(pilotiq, req, relationCreateRoute),
495
- resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'relation-create', metas, relationCreateRoute)),
496
- ])
497
-
498
- return {
499
- pageType: 'relation-create',
500
- panel,
501
- resource: { name: R.name, label: R.labelSingular, slug: scope.slug, icon: serializeIcon(R.icon, R.name) },
502
- relation: {
503
- name: M.name,
504
- label: M.getLabel(),
505
- labelSingular: M.getLabelSingular(),
506
- relationship: scope.relationship,
507
- icon: M.getIcon() ? serializeIcon(M.getIcon()!, M.name) : undefined,
508
- relatedSlug: Related.getSlug(),
509
- },
510
- parent: {
511
- id: scope.recordId,
512
- title: deriveParentTitle(R, parentRecord),
513
- },
514
- mode: 'create' as const,
515
- basePath: base,
516
- layout: cfg.layout,
517
- schemaData,
518
- notifications: consumeFlashedNotifications(req),
519
- ...(scope.prefill?.errors ? { hasErrors: true } : {}),
520
- }
521
- }
522
-
523
- /**
524
- * Phase A — read-only view page for a related record at depth-2:
525
- * `${base}/${slug}/:id/${rel}/:childId`. Mirrors `buildRelationEditData`'s
526
- * IDOR + auth posture but resolves the manager's `static detail(child,
527
- * parent)` instead of its form. The default `detail()` returns `[]` —
528
- * managers opt in by overriding it; the chrome (RelationTabs strip)
529
- * still renders so users can sideways-nav between sibling managers.
530
- */
531
- async function buildRelationViewData(
532
- pilotiq: Pilotiq,
533
- R: ResourceClass,
534
- M: typeof RelationManager,
535
- Related: ResourceClass,
536
- parentRecord: unknown,
537
- scope: Extract<RelationManagerScope, { kind: 'relation-view' }>,
538
- req: unknown,
539
- user: unknown,
540
- _mode: RelationMode,
541
- ): Promise<RelationManagerResult> {
542
- if (!Related.model) {
543
- throw new Error(
544
- `[Pilotiq] Cannot load child record for ${M.name}: Related Resource ${Related.name} has no static model.`,
545
- )
546
- }
547
- const childPk = getPrimaryKey(Related.model)
548
-
549
- const [belongs, child] = await Promise.all([
550
- childBelongsToParent(R.model as ModelLike, parentRecord, scope.relationship, childPk, scope.childId),
551
- findRecord(Related, scope.childId, { user }).catch(() => undefined),
552
- ])
553
- if (!belongs || !child) return null
554
-
555
- if (!await safeManagerPolicy(M, 'canView', Related, user, parentRecord, child)) return { ok: false, status: 403 }
556
-
557
- const cfg = pilotiq.getConfig()
558
- const base = cfg.path
559
-
560
- const elements: Element[] = M.detail(child, parentRecord)
561
-
562
- // Phase B polish — when M declares nested managers, surface them on
563
- // this page too. The strip lists the leaf parent's view tab plus one
564
- // tab per sibling nested manager so users can jump from the Phase A
565
- // view straight into a grandchild list / create / view / edit page.
566
- // Active key `'__view'` because the user is currently viewing the
567
- // leaf parent record itself, not any nested manager.
568
- const nestedTabs = await buildNestedRelationTabs(
569
- R, M, base,
570
- { recordId: scope.recordId, relationship: scope.relationship },
571
- scope.childId,
572
- '__view',
573
- user, child,
574
- )
575
- if (nestedTabs) elements.unshift(nestedTabs)
576
-
577
- const tabs = await buildRelationTabs(R, scope.recordId, base, scope.relationship, user, parentRecord)
578
- if (tabs) elements.unshift(tabs)
579
-
580
- const breadcrumbs = relationViewBreadcrumbs(
581
- cfg, R, M, scope.recordId,
582
- deriveParentTitle(R, parentRecord),
583
- deriveParentTitle(Related, child, M),
584
- )
585
- if (breadcrumbs) elements.unshift(breadcrumbs)
586
-
587
- const ctx: SchemaContext = uploadCtx(userCtx({
588
- mode: 'view',
589
- basePath: base,
590
- record: child,
591
- recordId: scope.childId,
592
- }, user), cfg)
593
-
594
- const relationViewRoute: PanelInfoRoute = { resource: R, recordId: scope.childId }
595
- const [panel, schemaData] = await Promise.all([
596
- panelInfo(pilotiq, req, relationViewRoute),
597
- resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'relation-view', metas, relationViewRoute)),
598
- ])
599
-
600
- return {
601
- pageType: 'relation-view',
602
- panel,
603
- resource: { name: R.name, label: R.labelSingular, slug: scope.slug, icon: serializeIcon(R.icon, R.name) },
604
- relation: {
605
- name: M.name,
606
- label: M.getLabel(),
607
- labelSingular: M.getLabelSingular(),
608
- relationship: scope.relationship,
609
- icon: M.getIcon() ? serializeIcon(M.getIcon()!, M.name) : undefined,
610
- relatedSlug: Related.getSlug(),
611
- },
612
- parent: {
613
- id: scope.recordId,
614
- title: deriveParentTitle(R, parentRecord),
615
- },
616
- mode: 'view' as const,
617
- childId: scope.childId,
618
- basePath: base,
619
- layout: cfg.layout,
620
- schemaData,
621
- notifications: consumeFlashedNotifications(req),
622
- }
623
- }
624
-
625
- async function buildRelationEditData(
626
- pilotiq: Pilotiq,
627
- R: ResourceClass,
628
- M: typeof RelationManager,
629
- Related: ResourceClass,
630
- parentRecord: unknown,
631
- scope: Extract<RelationManagerScope, { kind: 'relation-edit' }>,
632
- req: unknown,
633
- user: unknown,
634
- mode: RelationMode,
635
- ): Promise<RelationManagerResult> {
636
- if (!Related.model) {
637
- throw new Error(
638
- `[Pilotiq] Cannot load child record for ${M.name}: Related Resource ${Related.name} has no static model.`,
639
- )
640
- }
641
- const childPk = getPrimaryKey(Related.model)
642
-
643
- // IDOR check + child load in parallel. Both fail-paths collapse to
644
- // the same `return null` so behavior matches the serial version; the
645
- // independent queries (parent's relation accessor vs related model's
646
- // find) save one RTT on every relation-edit request.
647
- const [belongs, child] = await Promise.all([
648
- childBelongsToParent(R.model as ModelLike, parentRecord, scope.relationship, childPk, scope.childId),
649
- findRecord(Related, scope.childId, { user }).catch(() => undefined),
650
- ])
651
- if (!belongs || !child) return null
652
-
653
- if (!await safeManagerPolicy(M, 'canEdit', Related, user, parentRecord, child)) return { ok: false, status: 403 }
654
-
655
- const cfg = pilotiq.getConfig()
656
- const base = cfg.path
657
- const resourceBase = resourceBasePath(base, R)
658
- const editUrl = `${resourceBase}/${scope.recordId}/${scope.relationship}/${scope.childId}/edit`
659
-
660
- const managerCtx = buildRelationManagerCtx(base, scope, parentRecord, Related, mode)
661
- const form = M.form(Form.make(), managerCtx)
662
- autoWireManagerForm(form, Related)
663
-
664
- const elements: Element[] = [form]
665
- tagFormActions(elements, editUrl)
666
-
667
- // Prefill values: explicit prefill (re-render after 422) wins,
668
- // otherwise pipe the loaded child through Form's fill pipeline.
669
- if (scope.prefill?.values) {
670
- form.withValues(scope.prefill.values)
671
- if (scope.prefill.errors) form.withErrors(scope.prefill.errors)
672
- } else if (child != null) {
673
- const values = await applyFillPipeline(form, child)
674
- form.withValues(values)
675
- }
676
-
677
- const tabs = await buildRelationTabs(R, scope.recordId, base, scope.relationship, user, parentRecord)
678
- if (tabs) elements.unshift(tabs)
679
-
680
- const breadcrumbs = relationEditBreadcrumbs(
681
- cfg, R, M, scope.recordId,
682
- deriveParentTitle(R, parentRecord),
683
- scope.childId,
684
- deriveParentTitle(Related, child, M),
685
- )
686
- if (breadcrumbs) elements.unshift(breadcrumbs)
687
-
688
- const ctx: SchemaContext = uploadCtx(userCtx({
689
- mode: 'edit',
690
- basePath: base,
691
- record: child,
692
- recordId: scope.childId,
693
- }, user), cfg)
694
-
695
- const relationEditRoute: PanelInfoRoute = { resource: R, recordId: scope.childId }
696
- const [panel, schemaData] = await Promise.all([
697
- panelInfo(pilotiq, req, relationEditRoute),
698
- resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'relation-edit', metas, relationEditRoute)),
699
- ])
700
-
701
- return {
702
- pageType: 'relation-edit',
703
- panel,
704
- resource: { name: R.name, label: R.labelSingular, slug: scope.slug, icon: serializeIcon(R.icon, R.name) },
705
- relation: {
706
- name: M.name,
707
- label: M.getLabel(),
708
- labelSingular: M.getLabelSingular(),
709
- relationship: scope.relationship,
710
- icon: M.getIcon() ? serializeIcon(M.getIcon()!, M.name) : undefined,
711
- relatedSlug: Related.getSlug(),
712
- },
713
- parent: {
714
- id: scope.recordId,
715
- title: deriveParentTitle(R, parentRecord),
716
- },
717
- mode: 'edit' as const,
718
- childId: scope.childId,
719
- basePath: base,
720
- layout: cfg.layout,
721
- schemaData,
722
- notifications: consumeFlashedNotifications(req),
723
- ...(scope.prefill?.errors ? { hasErrors: true } : {}),
724
- }
725
- }
726
-
727
- // ─── Phase B nested-relation pipeline ────────────────────────
728
-
729
- /**
730
- * Phase B — narrow `scope` discriminator for nested-relation-*. Lets
731
- * the helpers below avoid restating the union for every parameter.
732
- */
733
- type NestedRelationScope = Extract<RelationManagerScope, { kind: `nested-relation-${string}` }>
734
-
735
- /**
736
- * Phase B — chain walk result. Resolved layer-by-layer in
737
- * `resolveRelationChain`; nested builders consume it. Failures bubble
738
- * up as the same `{ ok: false, status: 403 }` / `null` shape the
739
- * depth-1 path uses.
740
- */
741
- export interface ResolvedChain {
742
- R: ResourceClass
743
- parentRecord: unknown
744
- M1: typeof RelationManager
745
- Related1: ResourceClass
746
- child1: unknown
747
- child1Mode: RelationMode
748
- M2: typeof RelationManager
749
- Related2: ResourceClass | undefined
750
- child2Mode: RelationMode
751
- }
752
-
753
- /**
754
- * Phase B — resolve a depth-2 chain, running every auth + IDOR layer:
755
- * Layer 0 — top-level Resource: cluster gate, R.canAccess.
756
- * Layer 1 — parent record: R.canEdit(parent) (Phase A gate to manage relations).
757
- * Layer 2 — first manager M1: relationship discovered, related resource discovered.
758
- * IDOR #1 — child1 (the leaf parent) must belong to parentRecord under chain[0].relationship.
759
- * Layer 3 — M1.canView(child1, parent) (Filament-style: must be allowed
760
- * to view the child to drill into its sub-relations).
761
- * Layer 4 — second manager M2 lookup; relation type read off Related1.model.
762
- *
763
- * The leaf manager's per-scope predicate (canViewAny / canCreate /
764
- * canView / canEdit) runs inside the per-scope builders below, since
765
- * each predicate has different arguments.
766
- */
767
- export async function resolveRelationChain(
768
- pilotiq: Pilotiq,
769
- scope: NestedRelationScope,
770
- user: unknown,
771
- ): Promise<ResolvedChain | { ok: false; status: 403 } | null> {
772
- const cfg = pilotiq.getConfig()
773
-
774
- const R = pilotiq.findResource(scope.slug)
775
- if (!R) return null
776
-
777
- // Layer 0 — same gates as the depth-1 pipeline.
778
- const [clusterOk, accessOk] = await Promise.all([
779
- R.cluster ? safeBool(() => R.cluster!.canAccess(user)) : Promise.resolve(true),
780
- safeBool(() => R.canAccess(user)),
781
- ])
782
- if (!clusterOk || !accessOk) return { ok: false, status: 403 }
783
-
784
- if (!R.model) {
785
- throw new Error(
786
- `[Pilotiq] Resource "${R.name}" has nested relations but no static model. ` +
787
- `Set Resource.model = … or remove the manager.`,
788
- )
789
- }
790
-
791
- const [step0, step1] = scope.chain
792
- const parentRecord = await findRecord(R, step0.recordId, { user }).catch(() => undefined)
793
- if (!parentRecord) return null
794
-
795
- // Layer 1 — parent record gate.
796
- if (!await safeBool(() => R.canEdit(user, parentRecord))) return { ok: false, status: 403 }
797
-
798
- // Layer 2 — first manager M1.
799
- const M1 = findManager(R, step0.relationship)
800
- if (!M1) return null
801
- const Related1 = findRelatedResource(M1, R, cfg)
802
- if (!Related1) {
803
- throw new Error(
804
- `[Pilotiq] RelationManager ${M1.name} on ${R.name} could not resolve its related Resource. ` +
805
- `Set static relatedResource on the manager, or ensure the parent's model declares relations[${JSON.stringify(M1.getRelationship())}].`,
806
- )
807
- }
808
- if (!Related1.model) {
809
- throw new Error(
810
- `[Pilotiq] Related Resource ${Related1.name} has no static model — ` +
811
- `cannot resolve nested manager chain through it.`,
812
- )
813
- }
814
- const child1Mode: RelationMode = normalizeRelationMode(getRelationType(R.model, step0.relationship))
815
-
816
- // IDOR #1 + child1 load in parallel — confirm the leaf parent
817
- // (`step1.recordId`) actually belongs to the top parent under the
818
- // first relationship key. Independent queries.
819
- const child1Pk = getPrimaryKey(Related1.model)
820
- const [belongs1, child1] = await Promise.all([
821
- childBelongsToParent(R.model as ModelLike, parentRecord, step0.relationship, child1Pk, step1.recordId),
822
- findRecord(Related1, step1.recordId, { user }).catch(() => undefined),
823
- ])
824
- if (!belongs1 || !child1) return null
825
-
826
- // Layer 3 — M1.canView(child1, parent) gate. Filament-style: viewing
827
- // the child is the prerequisite for entering its nested manager strip.
828
- if (!await safeManagerPolicy(M1, 'canView', Related1, user, parentRecord, child1)) return { ok: false, status: 403 }
829
-
830
- // Layer 4 — second manager M2 declared under M1.relations().
831
- const M2 = M1.relations().find(N => {
832
- try { return N.getRelationship() === step1.relationship } catch { return false }
833
- })
834
- if (!M2) return null
835
- const Related2 = findRelatedResource(M2, Related1, cfg)
836
- const child2Mode: RelationMode = normalizeRelationMode(getRelationType(Related1.model, step1.relationship))
837
-
838
- return { R, parentRecord, M1, Related1, child1, child1Mode, M2, Related2, child2Mode }
839
- }
840
-
841
- /**
842
- * Phase B dispatcher — splits the four nested scopes onto their builders
843
- * after the shared chain walk. Mirrors the depth-1 `relationManagerData`
844
- * function shape.
845
- */
846
- async function nestedRelationManagerData(
847
- pilotiq: Pilotiq,
848
- scope: NestedRelationScope,
849
- req?: unknown,
850
- ): Promise<RelationManagerResult> {
851
- const user = await pilotiq.resolveUser(req)
852
- const resolved = await resolveRelationChain(pilotiq, scope, user)
853
- if (resolved === null) return null
854
- if ('ok' in resolved) return resolved
855
-
856
- // For create / view / edit we strictly need a registered Related2 so
857
- // we can load the leaf record + auto-wire the form save.
858
- const needRelated2 = scope.kind !== 'nested-relation-list'
859
- if (needRelated2 && !resolved.Related2) {
860
- throw new Error(
861
- `[Pilotiq] Nested RelationManager ${resolved.M2.name} under ${resolved.M1.name} ` +
862
- `on ${resolved.R.name} could not resolve its related Resource. ` +
863
- `Set static relatedResource on the manager, or ensure the parent's model declares ` +
864
- `relations[${JSON.stringify(resolved.M2.getRelationship())}].`,
865
- )
866
- }
867
-
868
- switch (scope.kind) {
869
- case 'nested-relation-list':
870
- return buildNestedRelationListData(pilotiq, scope, resolved, req, user)
871
- case 'nested-relation-create':
872
- return buildNestedRelationCreateData(pilotiq, scope, resolved, req, user)
873
- case 'nested-relation-view':
874
- return buildNestedRelationViewData(pilotiq, scope, resolved, req, user)
875
- case 'nested-relation-edit':
876
- return buildNestedRelationEditData(pilotiq, scope, resolved, req, user)
877
- }
878
- }
879
-
880
- /** Phase B — build the manager context for a nested leaf manager. The
881
- * parent here is `child1` (the chain's leaf parent record); the URL
882
- * prefix comes from `scope.chain[0]` via `Action.relation*` factories
883
- * reading `ctx.chain`. */
884
- /** Depth-1 manager context constructor — mirror of `nestedManagerCtx` for
885
- * the four `relation-*` page roles. Three call sites (list / create / edit)
886
- * build identical shapes; the helper keeps them in lock-step. */
887
- function buildRelationManagerCtx(
888
- base: string,
889
- scope: { slug: string; recordId: string; relationship: string },
890
- parentRecord: unknown,
891
- Related: ResourceClass | undefined,
892
- mode: RelationMode,
893
- ): RelationManagerContext {
894
- return {
895
- basePath: base,
896
- parentSlug: scope.slug,
897
- parentId: scope.recordId,
898
- relationship: scope.relationship,
899
- parentRecord,
900
- related: Related,
901
- mode,
902
- }
903
- }
904
-
905
- function nestedManagerCtx(
906
- base: string,
907
- scope: NestedRelationScope,
908
- resolved: ResolvedChain,
909
- ): RelationManagerContext {
910
- const [step0, step1] = scope.chain
911
- return {
912
- basePath: base,
913
- parentSlug: resolved.R.getSlug(),
914
- parentId: step1.recordId, // immediate parent = child1's id
915
- relationship: step1.relationship, // leaf manager's relationship
916
- parentRecord: resolved.child1, // immediate parent record = child1
917
- related: resolved.Related2,
918
- mode: resolved.child2Mode,
919
- chain: [{
920
- slug: resolved.R.getSlug(),
921
- recordId: step0.recordId,
922
- relationship: step0.relationship,
923
- }],
924
- }
925
- }
926
-
927
- /** Phase B — assemble the response shape that mirrors the depth-1
928
- * builders but adds a `chain` array so renderers can build breadcrumbs
929
- * and back-links without re-deriving them. */
930
- function nestedResponseEnvelope(
931
- pageType: 'nested-relation-list' | 'nested-relation-create' | 'nested-relation-view' | 'nested-relation-edit',
932
- pilotiq: Pilotiq,
933
- base: string,
934
- scope: NestedRelationScope,
935
- resolved: ResolvedChain,
936
- req: unknown,
937
- ): {
938
- pageType: typeof pageType
939
- resource: { name: string; label?: string | undefined; slug: string; icon?: SerializedIcon | undefined }
940
- parentRelation: { name: string; relationship: string; label: string; relatedSlug?: string | undefined }
941
- parentChild: { id: string; title: string }
942
- relation: { name: string; relationship: string; label: string; labelSingular: string; icon?: SerializedIcon | undefined; relatedSlug?: string | undefined }
943
- parent: { id: string; title: string }
944
- basePath: string
945
- layout: PilotiqConfig['layout']
946
- notifications: ReturnType<typeof consumeFlashedNotifications>
947
- } {
948
- const { R, M1, Related1, child1, M2, Related2 } = resolved
949
- const [step0, step1] = scope.chain
950
- const parentChildTitle = deriveParentTitle(Related1, child1, M1)
951
-
952
- return {
953
- pageType,
954
- resource: { name: R.name, label: R.labelSingular, slug: R.getSlug(), icon: serializeIcon(R.icon, R.name) },
955
- parentRelation: {
956
- name: M1.name,
957
- relationship: step0.relationship,
958
- label: M1.getLabel(),
959
- relatedSlug: Related1.getSlug(),
960
- },
961
- parentChild: {
962
- id: step1.recordId,
963
- title: parentChildTitle,
964
- },
965
- relation: {
966
- name: M2.name,
967
- relationship: step1.relationship,
968
- label: M2.getLabel(),
969
- labelSingular: M2.getLabelSingular(),
970
- icon: M2.getIcon() ? serializeIcon(M2.getIcon()!, M2.name) : undefined,
971
- relatedSlug: Related2?.getSlug(),
972
- },
973
- parent: {
974
- // Top-of-chain record — same shape the depth-1 builders ship as
975
- // `parent` so renderers can reuse the back-to-resource link.
976
- id: step0.recordId,
977
- title: deriveParentTitle(R, resolved.parentRecord),
978
- },
979
- basePath: base,
980
- layout: pilotiq.getConfig().layout,
981
- notifications: consumeFlashedNotifications(req),
982
- }
983
- }
984
-
985
- async function buildNestedRelationListData(
986
- pilotiq: Pilotiq,
987
- scope: Extract<NestedRelationScope, { kind: 'nested-relation-list' }>,
988
- resolved: ResolvedChain,
989
- req: unknown,
990
- user: unknown,
991
- ): Promise<RelationManagerResult> {
992
- const { Related1, child1, M2, Related2 } = resolved
993
-
994
- if (!await safeManagerPolicy(M2, 'canViewAny', Related2, user, child1)) return { ok: false, status: 403 }
995
-
996
- const cfg = pilotiq.getConfig()
997
- const base = cfg.path
998
- const [step0, step1] = scope.chain
999
- const resourceBase = resourceBasePath(base, resolved.R)
1000
- const listUrl = `${resourceBase}/${step0.recordId}/${step0.relationship}/${step1.recordId}/${step1.relationship}`
1001
-
1002
- const managerCtx = nestedManagerCtx(base, scope, resolved)
1003
- const table = M2.table(Table.make(), managerCtx)
1004
- if (Related1.model) {
1005
- autoWireManagerTable(table, Related1.model as ModelLike, child1, step1.relationship)
1006
- }
1007
- injectManagerTrashedFilter(table, Related2)
1008
-
1009
- const ctx: SchemaContext = uploadCtx(userCtx({
1010
- mode: 'table',
1011
- basePath: base,
1012
- record: child1,
1013
- }, user), cfg)
1014
-
1015
- const elements: Element[] = [table]
1016
- tagActionDispatch(elements, listUrl)
1017
- const [, tabs] = await Promise.all([
1018
- loadTableRecords(elements, scope.query ?? {}, listUrl, user),
1019
- buildNestedRelationTabs(resolved.R, resolved.M1, base, scope.chain[0], scope.chain[1].recordId, scope.chain[1].relationship, user, resolved.child1),
1020
- ])
1021
- if (tabs) elements.unshift(tabs)
1022
-
1023
- const breadcrumbs = nestedRelationListBreadcrumbs(
1024
- cfg, resolved.R, resolved.M1, M2, scope.chain[0],
1025
- deriveParentTitle(resolved.R, resolved.parentRecord),
1026
- scope.chain[1].recordId,
1027
- deriveParentTitle(Related1, child1, resolved.M1),
1028
- )
1029
- if (breadcrumbs) elements.unshift(breadcrumbs)
1030
-
1031
- const nestedListRoute: PanelInfoRoute = { resource: resolved.R, recordId: scope.chain[1].recordId }
1032
- const [panel, schemaData] = await Promise.all([
1033
- panelInfo(pilotiq, req, nestedListRoute),
1034
- resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'relation-list', metas, nestedListRoute)),
1035
- ])
1036
-
1037
- return {
1038
- ...nestedResponseEnvelope('nested-relation-list', pilotiq, base, scope, resolved, req),
1039
- panel,
1040
- schemaData,
1041
- }
1042
- }
1043
-
1044
- async function buildNestedRelationCreateData(
1045
- pilotiq: Pilotiq,
1046
- scope: Extract<NestedRelationScope, { kind: 'nested-relation-create' }>,
1047
- resolved: ResolvedChain,
1048
- req: unknown,
1049
- user: unknown,
1050
- ): Promise<RelationManagerResult> {
1051
- const { child1, M2, Related2 } = resolved
1052
-
1053
- if (!await safeManagerPolicy(M2, 'canCreate', Related2, user, child1)) return { ok: false, status: 403 }
1054
-
1055
- const cfg = pilotiq.getConfig()
1056
- const base = cfg.path
1057
- const [step0, step1] = scope.chain
1058
- const resourceBase = resourceBasePath(base, resolved.R)
1059
- const createUrl = `${resourceBase}/${step0.recordId}/${step0.relationship}/${step1.recordId}/${step1.relationship}/create`
1060
-
1061
- const managerCtx = nestedManagerCtx(base, scope, resolved)
1062
- const form = M2.form(Form.make(), managerCtx)
1063
- if (Related2?.model) autoWireManagerForm(form, Related2)
1064
-
1065
- const elements: Element[] = [form]
1066
- tagFormActions(elements, createUrl)
1067
-
1068
- if (scope.prefill) {
1069
- if (scope.prefill.values) form.withValues(scope.prefill.values)
1070
- if (scope.prefill.errors) form.withErrors(scope.prefill.errors)
1071
- }
1072
-
1073
- const tabs = await buildNestedRelationTabs(resolved.R, resolved.M1, base, scope.chain[0], scope.chain[1].recordId, scope.chain[1].relationship, user, resolved.child1)
1074
- if (tabs) elements.unshift(tabs)
1075
-
1076
- const breadcrumbs = nestedRelationCreateBreadcrumbs(
1077
- cfg, resolved.R, resolved.M1, M2, scope.chain[0],
1078
- deriveParentTitle(resolved.R, resolved.parentRecord),
1079
- scope.chain[1].recordId,
1080
- deriveParentTitle(resolved.Related1, child1, resolved.M1),
1081
- )
1082
- if (breadcrumbs) elements.unshift(breadcrumbs)
1083
-
1084
- const ctx: SchemaContext = uploadCtx(userCtx({
1085
- mode: 'create',
1086
- basePath: base,
1087
- record: child1,
1088
- }, user), cfg)
1089
-
1090
- const nestedCreateRoute: PanelInfoRoute = { resource: resolved.R, recordId: scope.chain[1].recordId }
1091
- const [panel, schemaData] = await Promise.all([
1092
- panelInfo(pilotiq, req, nestedCreateRoute),
1093
- resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'relation-create', metas, nestedCreateRoute)),
1094
- ])
1095
-
1096
- return {
1097
- ...nestedResponseEnvelope('nested-relation-create', pilotiq, base, scope, resolved, req),
1098
- panel,
1099
- mode: 'create' as const,
1100
- schemaData,
1101
- ...(scope.prefill?.errors ? { hasErrors: true } : {}),
1102
- }
1103
- }
1104
-
1105
- async function buildNestedRelationViewData(
1106
- pilotiq: Pilotiq,
1107
- scope: Extract<NestedRelationScope, { kind: 'nested-relation-view' }>,
1108
- resolved: ResolvedChain,
1109
- req: unknown,
1110
- user: unknown,
1111
- ): Promise<RelationManagerResult> {
1112
- const { Related1, child1, M2, Related2 } = resolved
1113
- if (!Related2?.model) {
1114
- throw new Error(
1115
- `[Pilotiq] Cannot load child record for nested manager ${M2.name}: ` +
1116
- `Related Resource ${Related2?.name ?? '(none)'} has no static model.`,
1117
- )
1118
- }
1119
- const [, step1] = scope.chain
1120
- const child2Pk = getPrimaryKey(Related2.model)
1121
-
1122
- const [belongs2, child2] = await Promise.all([
1123
- childBelongsToParent(Related1.model as ModelLike, child1, step1.relationship, child2Pk, scope.childId),
1124
- findRecord(Related2, scope.childId, { user }).catch(() => undefined),
1125
- ])
1126
- if (!belongs2 || !child2) return null
1127
-
1128
- if (!await safeManagerPolicy(M2, 'canView', Related2, user, child1, child2)) return { ok: false, status: 403 }
1129
-
1130
- const cfg = pilotiq.getConfig()
1131
- const base = cfg.path
1132
-
1133
- const elements: Element[] = M2.detail(child2, child1)
1134
-
1135
- const tabs = await buildNestedRelationTabs(resolved.R, resolved.M1, base, scope.chain[0], scope.chain[1].recordId, scope.chain[1].relationship, user, resolved.child1)
1136
- if (tabs) elements.unshift(tabs)
1137
-
1138
- const breadcrumbs = nestedRelationViewBreadcrumbs(
1139
- cfg, resolved.R, resolved.M1, M2, scope.chain[0],
1140
- deriveParentTitle(resolved.R, resolved.parentRecord),
1141
- scope.chain[1].recordId,
1142
- deriveParentTitle(Related1, child1, resolved.M1),
1143
- deriveParentTitle(Related2, child2, M2),
1144
- )
1145
- if (breadcrumbs) elements.unshift(breadcrumbs)
1146
-
1147
- const ctx: SchemaContext = uploadCtx(userCtx({
1148
- mode: 'view',
1149
- basePath: base,
1150
- record: child2,
1151
- recordId: scope.childId,
1152
- }, user), cfg)
1153
-
1154
- const nestedViewRoute: PanelInfoRoute = { resource: resolved.R, recordId: scope.childId }
1155
- const [panel, schemaData] = await Promise.all([
1156
- panelInfo(pilotiq, req, nestedViewRoute),
1157
- resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'relation-view', metas, nestedViewRoute)),
1158
- ])
1159
-
1160
- return {
1161
- ...nestedResponseEnvelope('nested-relation-view', pilotiq, base, scope, resolved, req),
1162
- panel,
1163
- mode: 'view' as const,
1164
- childId: scope.childId,
1165
- schemaData,
1166
- }
1167
- }
1168
-
1169
- async function buildNestedRelationEditData(
1170
- pilotiq: Pilotiq,
1171
- scope: Extract<NestedRelationScope, { kind: 'nested-relation-edit' }>,
1172
- resolved: ResolvedChain,
1173
- req: unknown,
1174
- user: unknown,
1175
- ): Promise<RelationManagerResult> {
1176
- const { Related1, child1, M2, Related2 } = resolved
1177
- if (!Related2?.model) {
1178
- throw new Error(
1179
- `[Pilotiq] Cannot load child record for nested manager ${M2.name}: ` +
1180
- `Related Resource ${Related2?.name ?? '(none)'} has no static model.`,
1181
- )
1182
- }
1183
- const [step0, step1] = scope.chain
1184
- const child2Pk = getPrimaryKey(Related2.model)
1185
-
1186
- const [belongs2, child2] = await Promise.all([
1187
- childBelongsToParent(Related1.model as ModelLike, child1, step1.relationship, child2Pk, scope.childId),
1188
- findRecord(Related2, scope.childId, { user }).catch(() => undefined),
1189
- ])
1190
- if (!belongs2 || !child2) return null
1191
-
1192
- if (!await safeManagerPolicy(M2, 'canEdit', Related2, user, child1, child2)) return { ok: false, status: 403 }
1193
-
1194
- const cfg = pilotiq.getConfig()
1195
- const base = cfg.path
1196
- const resourceBase = resourceBasePath(base, resolved.R)
1197
- const editUrl = `${resourceBase}/${step0.recordId}/${step0.relationship}/${step1.recordId}/${step1.relationship}/${scope.childId}/edit`
1198
-
1199
- const managerCtx = nestedManagerCtx(base, scope, resolved)
1200
- const form = M2.form(Form.make(), managerCtx)
1201
- autoWireManagerForm(form, Related2)
1202
-
1203
- const elements: Element[] = [form]
1204
- tagFormActions(elements, editUrl)
1205
-
1206
- if (scope.prefill?.values) {
1207
- form.withValues(scope.prefill.values)
1208
- if (scope.prefill.errors) form.withErrors(scope.prefill.errors)
1209
- } else if (child2 != null) {
1210
- const values = await applyFillPipeline(form, child2)
1211
- form.withValues(values)
1212
- }
1213
-
1214
- const tabs = await buildNestedRelationTabs(resolved.R, resolved.M1, base, scope.chain[0], scope.chain[1].recordId, scope.chain[1].relationship, user, resolved.child1)
1215
- if (tabs) elements.unshift(tabs)
1216
-
1217
- const breadcrumbs = nestedRelationEditBreadcrumbs(
1218
- cfg, resolved.R, resolved.M1, M2, scope.chain[0],
1219
- deriveParentTitle(resolved.R, resolved.parentRecord),
1220
- scope.chain[1].recordId,
1221
- deriveParentTitle(Related1, child1, resolved.M1),
1222
- scope.childId,
1223
- deriveParentTitle(Related2, child2, M2),
1224
- )
1225
- if (breadcrumbs) elements.unshift(breadcrumbs)
1226
-
1227
- const ctx: SchemaContext = uploadCtx(userCtx({
1228
- mode: 'edit',
1229
- basePath: base,
1230
- record: child2,
1231
- recordId: scope.childId,
1232
- }, user), cfg)
1233
-
1234
- const nestedEditRoute: PanelInfoRoute = { resource: resolved.R, recordId: scope.childId }
1235
- const [panel, schemaData] = await Promise.all([
1236
- panelInfo(pilotiq, req, nestedEditRoute),
1237
- resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'relation-edit', metas, nestedEditRoute)),
1238
- ])
1239
-
1240
- return {
1241
- ...nestedResponseEnvelope('nested-relation-edit', pilotiq, base, scope, resolved, req),
1242
- panel,
1243
- mode: 'edit' as const,
1244
- childId: scope.childId,
1245
- schemaData,
1246
- ...(scope.prefill?.errors ? { hasErrors: true } : {}),
1247
- }
1248
- }