@pilotiq/pilotiq 0.24.1 → 0.24.2
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.
- package/CHANGELOG.md +33 -0
- package/boost/guidelines.md +566 -0
- package/boost/skills/pilotiq-fields/SKILL.md +47 -0
- package/boost/skills/pilotiq-fields/rules/field-catalog.md +288 -0
- package/boost/skills/pilotiq-fields/rules/reactive-fields.md +199 -0
- package/boost/skills/pilotiq-fields/rules/validation.md +198 -0
- package/boost/skills/pilotiq-relations/SKILL.md +47 -0
- package/boost/skills/pilotiq-relations/rules/relation-managers.md +256 -0
- package/boost/skills/pilotiq-relations/rules/repeater-relationship.md +177 -0
- package/boost/skills/pilotiq-resource/SKILL.md +61 -0
- package/boost/skills/pilotiq-resource/rules/authorization.md +242 -0
- package/boost/skills/pilotiq-resource/rules/defining-resources.md +228 -0
- package/boost/skills/pilotiq-resource/rules/page-overrides.md +296 -0
- package/package.json +6 -1
- package/.turbo/turbo-build.log +0 -8
- package/CLAUDE.md +0 -265
- package/src/Cluster.test.ts +0 -283
- package/src/Cluster.ts +0 -83
- package/src/Column.test.ts +0 -199
- package/src/Column.ts +0 -710
- package/src/Global.test.ts +0 -367
- package/src/Global.ts +0 -169
- package/src/Page.test.ts +0 -114
- package/src/Page.ts +0 -208
- package/src/Pilotiq.perf.test.ts +0 -252
- package/src/Pilotiq.test.ts +0 -129
- package/src/Pilotiq.ts +0 -1158
- package/src/PilotiqRegistry.ts +0 -36
- package/src/PilotiqServiceProvider.ts +0 -121
- package/src/RelationManager.test.ts +0 -400
- package/src/RelationManager.ts +0 -527
- package/src/RenderHook.test.ts +0 -252
- package/src/RenderHook.ts +0 -242
- package/src/Resource.test.ts +0 -284
- package/src/Resource.ts +0 -526
- package/src/RightPanel.test.ts +0 -202
- package/src/RightPanel.ts +0 -132
- package/src/Tab.test.ts +0 -91
- package/src/Tab.ts +0 -156
- package/src/UserMenuItem.ts +0 -145
- package/src/actions/Action.test.ts +0 -2526
- package/src/actions/Action.ts +0 -1515
- package/src/actions/ActionGroup.test.ts +0 -112
- package/src/actions/ActionGroup.ts +0 -173
- package/src/actions/attachFactory.ts +0 -172
- package/src/actions/bulkFactories.ts +0 -168
- package/src/actions/crudFactories.ts +0 -220
- package/src/actions/exportFactory.ts +0 -225
- package/src/actions/factoryHelpers.ts +0 -177
- package/src/actions/importFactory.ts +0 -243
- package/src/actions/index.ts +0 -17
- package/src/actions/m2mFactories.ts +0 -193
- package/src/actions/relationFactories.ts +0 -372
- package/src/applyPageHooks.test.ts +0 -463
- package/src/applyPageHooks.ts +0 -330
- package/src/authorization.test.ts +0 -483
- package/src/breadcrumbs.test.ts +0 -238
- package/src/cells/coerce.test.ts +0 -85
- package/src/cells/coerce.ts +0 -84
- package/src/clusterPaths.ts +0 -35
- package/src/columns/BadgeColumn.test.ts +0 -54
- package/src/columns/BadgeColumn.ts +0 -32
- package/src/columns/BooleanColumn.test.ts +0 -41
- package/src/columns/BooleanColumn.ts +0 -18
- package/src/columns/ColorColumn.test.ts +0 -37
- package/src/columns/ColorColumn.ts +0 -38
- package/src/columns/IconColumn.test.ts +0 -54
- package/src/columns/IconColumn.ts +0 -37
- package/src/columns/ImageColumn.test.ts +0 -41
- package/src/columns/ImageColumn.ts +0 -28
- package/src/columns/SelectColumn.ts +0 -98
- package/src/columns/TextColumn.test.ts +0 -190
- package/src/columns/TextColumn.ts +0 -20
- package/src/columns/TextInputColumn.ts +0 -68
- package/src/columns/ToggleColumn.ts +0 -46
- package/src/columns/editableColumns.test.ts +0 -238
- package/src/columns/index.ts +0 -9
- package/src/defaultGlobalPages.ts +0 -95
- package/src/defaultPages.test.ts +0 -634
- package/src/defaultPages.ts +0 -617
- package/src/defaultViewPage.test.ts +0 -147
- package/src/elements/Form.test.ts +0 -223
- package/src/elements/Form.ts +0 -416
- package/src/elements/ListTabs.ts +0 -28
- package/src/elements/Table.test.ts +0 -422
- package/src/elements/Table.ts +0 -850
- package/src/elements/TableGroup.test.ts +0 -260
- package/src/elements/TableGroup.ts +0 -334
- package/src/elements/dispatchAction.test.ts +0 -463
- package/src/elements/dispatchAction.ts +0 -355
- package/src/elements/dispatchForm.test.ts +0 -477
- package/src/elements/dispatchForm.ts +0 -1993
- package/src/elements/dispatchTable.test.ts +0 -1514
- package/src/elements/dispatchTable.ts +0 -745
- package/src/elements/index.ts +0 -21
- package/src/entries/BadgeEntry.ts +0 -39
- package/src/entries/CodeEntry.test.ts +0 -40
- package/src/entries/CodeEntry.ts +0 -52
- package/src/entries/ColorEntry.ts +0 -63
- package/src/entries/ComponentEntry.test.ts +0 -173
- package/src/entries/ComponentEntry.ts +0 -95
- package/src/entries/Entry.ts +0 -304
- package/src/entries/IconEntry.ts +0 -49
- package/src/entries/ImageEntry.ts +0 -61
- package/src/entries/KeyValueEntry.ts +0 -47
- package/src/entries/RepeatableEntry.test.ts +0 -239
- package/src/entries/RepeatableEntry.ts +0 -173
- package/src/entries/TextEntry.test.ts +0 -394
- package/src/entries/TextEntry.ts +0 -60
- package/src/entries/index.ts +0 -12
- package/src/entries/leaves.test.ts +0 -306
- package/src/entries/registry.ts +0 -54
- package/src/fields/BuilderField.test.ts +0 -1188
- package/src/fields/BuilderField.ts +0 -605
- package/src/fields/BuilderRelationship.test.ts +0 -811
- package/src/fields/CheckboxField.test.ts +0 -44
- package/src/fields/CheckboxField.ts +0 -27
- package/src/fields/CheckboxListField.test.ts +0 -99
- package/src/fields/CheckboxListField.ts +0 -66
- package/src/fields/ColorPickerField.test.ts +0 -33
- package/src/fields/ColorPickerField.ts +0 -25
- package/src/fields/DateField.ts +0 -54
- package/src/fields/DateTimeField.test.ts +0 -55
- package/src/fields/EmailField.ts +0 -16
- package/src/fields/Field.test.ts +0 -654
- package/src/fields/Field.ts +0 -817
- package/src/fields/FileUploadField.test.ts +0 -143
- package/src/fields/FileUploadField.ts +0 -159
- package/src/fields/HiddenField.test.ts +0 -27
- package/src/fields/HiddenField.ts +0 -28
- package/src/fields/KeyValueField.test.ts +0 -105
- package/src/fields/KeyValueField.ts +0 -55
- package/src/fields/MarkdownField.test.ts +0 -167
- package/src/fields/MarkdownField.ts +0 -162
- package/src/fields/NumberField.ts +0 -33
- package/src/fields/RadioField.test.ts +0 -94
- package/src/fields/RadioField.ts +0 -67
- package/src/fields/RepeaterField.test.ts +0 -1806
- package/src/fields/RepeaterField.ts +0 -939
- package/src/fields/RepeaterRelationship.test.ts +0 -1923
- package/src/fields/RepeaterSimple.test.ts +0 -248
- package/src/fields/RowButton.test.ts +0 -219
- package/src/fields/RowButton.ts +0 -135
- package/src/fields/SelectField.test.ts +0 -192
- package/src/fields/SelectField.ts +0 -235
- package/src/fields/SliderField.test.ts +0 -50
- package/src/fields/SliderField.ts +0 -53
- package/src/fields/SlugField.ts +0 -24
- package/src/fields/TagsInputField.test.ts +0 -154
- package/src/fields/TagsInputField.ts +0 -133
- package/src/fields/TextField.test.ts +0 -213
- package/src/fields/TextField.ts +0 -177
- package/src/fields/TextareaField.test.ts +0 -58
- package/src/fields/TextareaField.ts +0 -59
- package/src/fields/ToggleButtonsField.test.ts +0 -106
- package/src/fields/ToggleButtonsField.ts +0 -59
- package/src/fields/ToggleField.ts +0 -16
- package/src/fields/disableOptionsWhenSelectedInSiblingRepeaterItems.test.ts +0 -319
- package/src/fields/optionsResolver.ts +0 -95
- package/src/fields/resolveField.ts +0 -28
- package/src/filters/BooleanFilter.ts +0 -35
- package/src/filters/DateRangeFilter.test.ts +0 -194
- package/src/filters/DateRangeFilter.ts +0 -148
- package/src/filters/Filter.test.ts +0 -268
- package/src/filters/Filter.ts +0 -184
- package/src/filters/FormFilter.test.ts +0 -238
- package/src/filters/FormFilter.ts +0 -215
- package/src/filters/MultiSelectFilter.test.ts +0 -119
- package/src/filters/MultiSelectFilter.ts +0 -78
- package/src/filters/QueryBuilderFilter.test.ts +0 -662
- package/src/filters/QueryBuilderFilter.ts +0 -398
- package/src/filters/SelectFilter.ts +0 -46
- package/src/filters/TernaryFilter.test.ts +0 -160
- package/src/filters/TernaryFilter.ts +0 -72
- package/src/filters/TrashedFilter.test.ts +0 -149
- package/src/filters/TrashedFilter.ts +0 -55
- package/src/filters/queryBuilder/BooleanConstraint.ts +0 -31
- package/src/filters/queryBuilder/Constraint.ts +0 -115
- package/src/filters/queryBuilder/DateConstraint.ts +0 -69
- package/src/filters/queryBuilder/NumberConstraint.ts +0 -66
- package/src/filters/queryBuilder/SelectConstraint.ts +0 -72
- package/src/filters/queryBuilder/TextConstraint.ts +0 -64
- package/src/filters/queryBuilder/index.ts +0 -12
- package/src/icons/index.ts +0 -2
- package/src/icons/lucide.ts +0 -204
- package/src/icons/registry.test.ts +0 -56
- package/src/icons/registry.ts +0 -41
- package/src/icons/types.ts +0 -47
- package/src/index.ts +0 -525
- package/src/io/csv.test.ts +0 -142
- package/src/io/csv.ts +0 -170
- package/src/nestedRelationManagerData.test.ts +0 -547
- package/src/notifications/Notification.test.ts +0 -210
- package/src/notifications/Notification.ts +0 -354
- package/src/notifications/broadcast.test.ts +0 -110
- package/src/notifications/broadcast.ts +0 -95
- package/src/notifications/database.test.ts +0 -383
- package/src/notifications/database.ts +0 -398
- package/src/notifications/databaseNotifications.test.ts +0 -187
- package/src/notifications/dispatchNotificationAction.test.ts +0 -341
- package/src/notifications/dispatchNotificationAction.ts +0 -142
- package/src/notifications/flash.test.ts +0 -89
- package/src/notifications/flash.ts +0 -71
- package/src/notifications/index.ts +0 -45
- package/src/notifications/registerBroadcastAuth.test.ts +0 -134
- package/src/notifications/registerBroadcastAuth.ts +0 -100
- package/src/notifications/resolveSavedNotification.test.ts +0 -82
- package/src/notifications/resolveSavedNotification.ts +0 -59
- package/src/notifications/types.ts +0 -93
- package/src/orm/m2mAccessor.ts +0 -66
- package/src/orm/modelDefaults.test.ts +0 -633
- package/src/orm/modelDefaults.ts +0 -666
- package/src/pageData/breadcrumbs.ts +0 -288
- package/src/pageData/forms.ts +0 -578
- package/src/pageData/helpers.ts +0 -857
- package/src/pageData/misc.ts +0 -347
- package/src/pageData/navigation.ts +0 -842
- package/src/pageData/relationPages.ts +0 -1248
- package/src/pageData/relationTabs.ts +0 -286
- package/src/pageData/resourcePages.ts +0 -609
- package/src/pageData.test.ts +0 -1545
- package/src/pageData.ts +0 -341
- package/src/plugins/index.ts +0 -8
- package/src/plugins/themeEditor.test.ts +0 -36
- package/src/plugins/themeEditor.ts +0 -45
- package/src/react/AppShell.tsx +0 -251
- package/src/react/CollabExtensionFactoryRegistry.ts +0 -55
- package/src/react/CollabRoomContext.ts +0 -98
- package/src/react/CollabTextRendererRegistry.ts +0 -102
- package/src/react/CommandPalette.tsx +0 -375
- package/src/react/CurrentUserContext.tsx +0 -50
- package/src/react/CustomPageWrapperGate.tsx +0 -69
- package/src/react/CustomPageWrapperRegistry.ts +0 -45
- package/src/react/FieldFocusReporterRegistry.ts +0 -37
- package/src/react/FieldLabelSlotRegistry.ts +0 -30
- package/src/react/FieldPresenceRegistry.ts +0 -46
- package/src/react/FormCollabBindingRegistry.ts +0 -242
- package/src/react/FormStateContext.tsx +0 -591
- package/src/react/HeadHooks.tsx +0 -126
- package/src/react/MarkdownEditorRegistry.test.ts +0 -38
- package/src/react/MarkdownEditorRegistry.ts +0 -107
- package/src/react/NotificationActionStrip.tsx +0 -263
- package/src/react/NotificationBell.tsx +0 -426
- package/src/react/PendingSuggestionApplierRegistry.test.ts +0 -97
- package/src/react/PendingSuggestionApplierRegistry.ts +0 -98
- package/src/react/PendingSuggestionOverlayRegistry.ts +0 -54
- package/src/react/PendingSuggestionsContext.tsx +0 -172
- package/src/react/RecordWrapperGate.tsx +0 -58
- package/src/react/RecordWrapperRegistry.ts +0 -39
- package/src/react/RenderHookSlot.tsx +0 -32
- package/src/react/RightSidebar.tsx +0 -257
- package/src/react/RightSidebarContext.tsx +0 -234
- package/src/react/RightSidebarTrigger.tsx +0 -53
- package/src/react/RowCoordsContext.tsx +0 -23
- package/src/react/SchemaRenderer.tsx +0 -549
- package/src/react/SearchTrigger.tsx +0 -46
- package/src/react/ThemeProvider.tsx +0 -93
- package/src/react/ThemeSettingsPage.tsx +0 -579
- package/src/react/ThemeToggle.tsx +0 -20
- package/src/react/Toaster.tsx +0 -158
- package/src/react/UserMenu.tsx +0 -196
- package/src/react/WidgetDataContext.tsx +0 -157
- package/src/react/cells/EditableCell.tsx +0 -389
- package/src/react/component-slots.test.ts +0 -103
- package/src/react/component-slots.ts +0 -116
- package/src/react/fieldJsHandler.test.ts +0 -166
- package/src/react/fieldJsHandler.ts +0 -79
- package/src/react/fields/BuilderInput.tsx +0 -1078
- package/src/react/fields/CheckboxInput.tsx +0 -39
- package/src/react/fields/CheckboxListInput.tsx +0 -102
- package/src/react/fields/ColorInput.tsx +0 -71
- package/src/react/fields/DateFieldInput.tsx +0 -70
- package/src/react/fields/DateTimeInput.tsx +0 -62
- package/src/react/fields/FieldShell.tsx +0 -348
- package/src/react/fields/FileUploadInput.tsx +0 -639
- package/src/react/fields/HiddenInput.tsx +0 -17
- package/src/react/fields/KeyValueInput.tsx +0 -230
- package/src/react/fields/MarkdownInput.tsx +0 -560
- package/src/react/fields/RadioInput.tsx +0 -81
- package/src/react/fields/RepeaterInput.test.ts +0 -116
- package/src/react/fields/RepeaterInput.tsx +0 -1420
- package/src/react/fields/SelectFieldInput.tsx +0 -280
- package/src/react/fields/SliderInput.tsx +0 -81
- package/src/react/fields/TagsInput.tsx +0 -283
- package/src/react/fields/TextLikeInput.tsx +0 -256
- package/src/react/fields/ToggleButtonsInput.tsx +0 -60
- package/src/react/fields/ToggleFieldInput.tsx +0 -56
- package/src/react/fields/relationshipRenameDispatch.test.ts +0 -106
- package/src/react/fields/relationshipRenameDispatch.ts +0 -97
- package/src/react/fields/repeaterReconcile.test.ts +0 -114
- package/src/react/fields/repeaterReconcile.ts +0 -104
- package/src/react/fields/rowChromeButton.tsx +0 -336
- package/src/react/fields/rowState.ts +0 -106
- package/src/react/fields/syncRowGates.test.ts +0 -202
- package/src/react/fields/syncRowGates.ts +0 -66
- package/src/react/fields/textInputControls.tsx +0 -238
- package/src/react/fields/useRowReorderDnd.ts +0 -78
- package/src/react/formStateHelpers.test.ts +0 -508
- package/src/react/formStateHelpers.ts +0 -381
- package/src/react/hooks/use-mobile.ts +0 -19
- package/src/react/icon-context.tsx +0 -60
- package/src/react/index.ts +0 -194
- package/src/react/layouts/SidebarLayout.tsx +0 -250
- package/src/react/layouts/TopbarLayout.tsx +0 -258
- package/src/react/navigate.tsx +0 -37
- package/src/react/onProviderSynced.test.ts +0 -90
- package/src/react/parseRecordEditUrl.test.ts +0 -122
- package/src/react/parseRecordEditUrl.ts +0 -94
- package/src/react/persistedState.ts +0 -40
- package/src/react/registry.ts +0 -48
- package/src/react/right-panel-registry.tsx +0 -47
- package/src/react/schemaRenderer/AlertRenderer.tsx +0 -112
- package/src/react/schemaRenderer/EntryRenderer.tsx +0 -501
- package/src/react/schemaRenderer/SectionRenderer.tsx +0 -120
- package/src/react/schemaRenderer/SimpleElements.tsx +0 -306
- package/src/react/schemaRenderer/TabsRenderer.tsx +0 -62
- package/src/react/schemaRenderer/WizardRenderer.tsx +0 -338
- package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +0 -177
- package/src/react/schemaRenderer/action/ActionModalDialog.tsx +0 -273
- package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +0 -61
- package/src/react/schemaRenderer/action/HandlerActionButton.tsx +0 -43
- package/src/react/schemaRenderer/action/MethodActionButton.tsx +0 -64
- package/src/react/schemaRenderer/action/buttons.tsx +0 -99
- package/src/react/schemaRenderer/action/helpers.ts +0 -140
- package/src/react/schemaRenderer/action/renderAction.tsx +0 -245
- package/src/react/schemaRenderer/columnFormat.ts +0 -65
- package/src/react/schemaRenderer/constants.ts +0 -50
- package/src/react/schemaRenderer/form/FormRenderer.tsx +0 -274
- package/src/react/schemaRenderer/form/renderField.tsx +0 -511
- package/src/react/schemaRenderer/helpers.tsx +0 -81
- package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +0 -308
- package/src/react/schemaRenderer/table/TableRenderer.tsx +0 -123
- package/src/react/schemaRenderer/table/TableRendererBody.tsx +0 -974
- package/src/react/schemaRenderer/table/filters.tsx +0 -1233
- package/src/react/schemaRenderer/table/formatCell.tsx +0 -264
- package/src/react/schemaRenderer/table/links.tsx +0 -112
- package/src/react/schemaRenderer/table/renderRowActions.tsx +0 -52
- package/src/react/schemaRenderer/table/url.tsx +0 -143
- package/src/react/theme-preview/apply.ts +0 -99
- package/src/react/theme-preview/build-html.ts +0 -436
- package/src/react/ui/button.tsx +0 -51
- package/src/react/ui/calendar.tsx +0 -67
- package/src/react/ui/checkbox.tsx +0 -29
- package/src/react/ui/dialog.tsx +0 -108
- package/src/react/ui/dropdown-menu.tsx +0 -97
- package/src/react/ui/input.tsx +0 -20
- package/src/react/ui/label.tsx +0 -21
- package/src/react/ui/popover.tsx +0 -50
- package/src/react/ui/select.tsx +0 -169
- package/src/react/ui/separator.tsx +0 -25
- package/src/react/ui/sheet.tsx +0 -136
- package/src/react/ui/sidebar.tsx +0 -723
- package/src/react/ui/skeleton.tsx +0 -13
- package/src/react/ui/slider.tsx +0 -34
- package/src/react/ui/switch.tsx +0 -28
- package/src/react/ui/table.tsx +0 -105
- package/src/react/ui/tabs.tsx +0 -63
- package/src/react/ui/textarea.tsx +0 -18
- package/src/react/ui/tooltip.tsx +0 -64
- package/src/react/useResizableWidth.ts +0 -139
- package/src/react/utils.ts +0 -6
- package/src/react/widgetRegistry.test.ts +0 -43
- package/src/react/widgetRegistry.ts +0 -50
- package/src/react/widgets/StatsOverviewRenderer.tsx +0 -232
- package/src/react/widgets/TableWidgetRenderer.tsx +0 -231
- package/src/react/widgets/ViewRenderer.tsx +0 -71
- package/src/relationManagerData.test.ts +0 -1595
- package/src/richtext/index.ts +0 -8
- package/src/richtext/registry.ts +0 -89
- package/src/routes/globals.ts +0 -148
- package/src/routes/guard.test.ts +0 -325
- package/src/routes/helpers.ts +0 -704
- package/src/routes/pages.ts +0 -175
- package/src/routes/panel.ts +0 -204
- package/src/routes/relations.ts +0 -1243
- package/src/routes/resources.ts +0 -781
- package/src/routes/theme.ts +0 -91
- package/src/routes-nested-relations.test.ts +0 -676
- package/src/routes-relations.test.ts +0 -972
- package/src/routes.test.ts +0 -2027
- package/src/routes.ts +0 -303
- package/src/schema/Alert.test.ts +0 -109
- package/src/schema/Alert.ts +0 -131
- package/src/schema/Block.ts +0 -169
- package/src/schema/Breadcrumbs.ts +0 -40
- package/src/schema/Card.ts +0 -35
- package/src/schema/Divider.ts +0 -20
- package/src/schema/Element.ts +0 -219
- package/src/schema/EmptyState.test.ts +0 -37
- package/src/schema/EmptyState.ts +0 -63
- package/src/schema/Fieldset.ts +0 -43
- package/src/schema/Grid.ts +0 -43
- package/src/schema/Group.ts +0 -30
- package/src/schema/Heading.ts +0 -39
- package/src/schema/Html.ts +0 -67
- package/src/schema/Icon.ts +0 -54
- package/src/schema/Image.ts +0 -57
- package/src/schema/LinkTag.ts +0 -41
- package/src/schema/Markdown.ts +0 -85
- package/src/schema/MetaTag.ts +0 -41
- package/src/schema/RelationTabs.ts +0 -71
- package/src/schema/ScriptTag.ts +0 -55
- package/src/schema/Section.ts +0 -160
- package/src/schema/ServerDataElement.test.ts +0 -140
- package/src/schema/ServerDataElement.ts +0 -156
- package/src/schema/SlotComponent.test.ts +0 -77
- package/src/schema/SlotComponent.ts +0 -71
- package/src/schema/Split.ts +0 -50
- package/src/schema/Stat.test.ts +0 -118
- package/src/schema/Stat.ts +0 -154
- package/src/schema/StatsOverview.test.ts +0 -141
- package/src/schema/StatsOverview.ts +0 -119
- package/src/schema/StyleTag.ts +0 -35
- package/src/schema/TableWidget.test.ts +0 -297
- package/src/schema/TableWidget.ts +0 -289
- package/src/schema/Tabs.ts +0 -79
- package/src/schema/Text.ts +0 -58
- package/src/schema/UnorderedList.ts +0 -49
- package/src/schema/View.test.ts +0 -111
- package/src/schema/View.ts +0 -127
- package/src/schema/Wizard.ts +0 -220
- package/src/schema/containers.test.ts +0 -564
- package/src/schema/headTags.test.ts +0 -134
- package/src/schema/index.ts +0 -40
- package/src/schema/primes.test.ts +0 -269
- package/src/schema/resolveSchema.test.ts +0 -379
- package/src/schema/resolveSchema.ts +0 -917
- package/src/schema/sanitize.ts +0 -58
- package/src/search.test.ts +0 -446
- package/src/search.ts +0 -178
- package/src/sessionFilters.test.ts +0 -375
- package/src/sessionFilters.ts +0 -143
- package/src/slot-components/index.ts +0 -10
- package/src/slot-components/registry.ts +0 -56
- package/src/styles/file-upload.css +0 -13
- package/src/summarizers/Summarizer.test.ts +0 -84
- package/src/summarizers/Summarizer.ts +0 -123
- package/src/summarizers/index.ts +0 -11
- package/src/theme/base-colors.ts +0 -68
- package/src/theme/chart-colors.ts +0 -50
- package/src/theme/colors.ts +0 -447
- package/src/theme/generate-css.test.ts +0 -139
- package/src/theme/generate-css.ts +0 -44
- package/src/theme/generate-scale.test.ts +0 -106
- package/src/theme/generate-scale.ts +0 -97
- package/src/theme/icon-map.ts +0 -42
- package/src/theme/index.ts +0 -34
- package/src/theme/migrate.test.ts +0 -178
- package/src/theme/migrate.ts +0 -81
- package/src/theme/presets.ts +0 -135
- package/src/theme/radius.ts +0 -18
- package/src/theme/resolve.test.ts +0 -238
- package/src/theme/resolve.ts +0 -96
- package/src/theme/spacing.ts +0 -18
- package/src/theme/storage.test.ts +0 -126
- package/src/theme/storage.ts +0 -106
- package/src/theme/theme-colors.ts +0 -88
- package/src/theme/types.ts +0 -125
- package/src/uploads/UploadAdapter.ts +0 -35
- package/src/uploads/index.ts +0 -2
- package/src/uploads/localUpload.test.ts +0 -70
- package/src/uploads/localUpload.ts +0 -84
- package/src/validation/Validator.ts +0 -49
- package/src/validation/index.ts +0 -28
- package/src/validation/rules.ts +0 -78
- package/src/validation/runValidators.ts +0 -435
- package/src/validation/uniqueValidator.test.ts +0 -196
- package/src/validation/uniqueValidator.ts +0 -133
- package/src/validation/validators.test.ts +0 -268
- package/src/vite.test.ts +0 -184
- package/src/vite.ts +0 -787
- package/src/widgets/index.ts +0 -10
- package/src/widgets/registry.ts +0 -45
- package/src/widgets.test.ts +0 -592
- package/tsconfig.build.json +0 -11
- package/tsconfig.json +0 -4
- package/tsconfig.test.json +0 -10
- package/views/react/Dashboard.tsx +0 -27
- package/views/react/Resources/Form.tsx +0 -102
- package/views/react/Resources/Index.tsx +0 -49
package/src/routes/resources.ts
DELETED
|
@@ -1,781 +0,0 @@
|
|
|
1
|
-
import type { Router } from '@rudderjs/router'
|
|
2
|
-
import type { AppRequest, AppResponse } from '@rudderjs/contracts'
|
|
3
|
-
import { view } from '@rudderjs/view'
|
|
4
|
-
import type { Pilotiq } from '../Pilotiq.js'
|
|
5
|
-
import type { ResourceClass } from '../Resource.js'
|
|
6
|
-
import { resolveSchema, type SchemaContext } from '../schema/resolveSchema.js'
|
|
7
|
-
import { dispatchFormSubmit, findForms, selectForm } from '../elements/dispatchForm.js'
|
|
8
|
-
import { dispatchAction, parseActionBody, type ResolveRecord } from '../elements/dispatchAction.js'
|
|
9
|
-
import {
|
|
10
|
-
listFiltersKey,
|
|
11
|
-
readPersistedListQuery,
|
|
12
|
-
writePersistedListQuery,
|
|
13
|
-
readPersistedLastTab,
|
|
14
|
-
writePersistedLastTab,
|
|
15
|
-
encodePersistedQuery,
|
|
16
|
-
} from '../sessionFilters.js'
|
|
17
|
-
import {
|
|
18
|
-
callPageSchema, tagFormActions, tagActionDispatch,
|
|
19
|
-
resourceIndexData, resourceTableData,
|
|
20
|
-
resourceCreateData, resourceEditData,
|
|
21
|
-
resourceViewData, resourceRecordPageData,
|
|
22
|
-
} from '../pageData.js'
|
|
23
|
-
import { findRecord } from '../orm/modelDefaults.js'
|
|
24
|
-
import { Table } from '../elements/Table.js'
|
|
25
|
-
import { Column } from '../Column.js'
|
|
26
|
-
import { coerceCellValue, CellCoerceError } from '../cells/coerce.js'
|
|
27
|
-
import { resourceBasePath } from '../clusterPaths.js'
|
|
28
|
-
import { registerRelationRoutes } from './relations.js'
|
|
29
|
-
import {
|
|
30
|
-
wantsJson,
|
|
31
|
-
readFormBody,
|
|
32
|
-
normalizeRedirect,
|
|
33
|
-
splitMeta,
|
|
34
|
-
forbidden,
|
|
35
|
-
cellHookErrorMessage,
|
|
36
|
-
checkPolicy,
|
|
37
|
-
policyAccess,
|
|
38
|
-
policyGate,
|
|
39
|
-
resolveDispatchTarget,
|
|
40
|
-
handleFormState,
|
|
41
|
-
handleFormWizard,
|
|
42
|
-
handleFormCreateOption,
|
|
43
|
-
handleFormMentions,
|
|
44
|
-
handleWidgetData,
|
|
45
|
-
sendActionResult,
|
|
46
|
-
sendMutationSuccess,
|
|
47
|
-
sendRedirectResponse,
|
|
48
|
-
findInQueryWithTrashed,
|
|
49
|
-
loadAccessGated,
|
|
50
|
-
} from './helpers.js'
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Register every depth-0 route for one Resource — list / view / create /
|
|
54
|
-
* edit / delete / restore / force-delete (when soft-deletes are enabled),
|
|
55
|
-
* resource-action dispatch, `_widget` polling, the deferred-table JSON
|
|
56
|
-
* endpoint, the per-row `_reorder` and `_cell` endpoints (gated by the
|
|
57
|
-
* boot-time probes the caller hands in via `options`), the four
|
|
58
|
-
* form-state companion endpoints for both create and edit, plus the
|
|
59
|
-
* resource-record sub-pages (Filament-style `pages().record`).
|
|
60
|
-
*
|
|
61
|
-
* Also drives `registerRelationRoutes` per registered relation manager
|
|
62
|
-
* — the depth-1 + depth-2 relation strips compose under this Resource.
|
|
63
|
-
*
|
|
64
|
-
* Pulled out of `registerPilotiqRoutes` in 2026-05-12 (Phase 5 of the
|
|
65
|
-
* routes.ts split). Called once per `cfg.resources` entry.
|
|
66
|
-
*
|
|
67
|
-
* `options` carries the boot-time probe results — the host barrel knows
|
|
68
|
-
* whether `Table.reorderable()` was set and whether any editable column
|
|
69
|
-
* is present (both need a model-side method to exist; the barrel throws
|
|
70
|
-
* a config error there).
|
|
71
|
-
*/
|
|
72
|
-
export function registerResourceRoutes(
|
|
73
|
-
router: Router,
|
|
74
|
-
pilotiq: Pilotiq,
|
|
75
|
-
R: ResourceClass,
|
|
76
|
-
base: string,
|
|
77
|
-
options: { reorderable: boolean; editable: boolean },
|
|
78
|
-
): void {
|
|
79
|
-
const cfg = pilotiq.getConfig()
|
|
80
|
-
|
|
81
|
-
const slug = R.getSlug()
|
|
82
|
-
const resourceBase = resourceBasePath(base, R)
|
|
83
|
-
const pages = R.resolvePages()
|
|
84
|
-
|
|
85
|
-
// Index — GET ${resourceBase}
|
|
86
|
-
if (pages.index) {
|
|
87
|
-
const PageClass = pages.index
|
|
88
|
-
const indexUrl = resourceBase
|
|
89
|
-
router.get(indexUrl, async (req, res) => {
|
|
90
|
-
const user = await pilotiq.resolveUser(req)
|
|
91
|
-
if (!await policyGate(R, user, () => R.canViewAny(user))) return forbidden(res, wantsJson(req))
|
|
92
|
-
|
|
93
|
-
if (R.persistFiltersInSession) {
|
|
94
|
-
const query = (req.query as Record<string, unknown> | undefined) ?? {}
|
|
95
|
-
const sessionSlug = resourceBase.slice(base.length + 1)
|
|
96
|
-
if (Object.keys(query).length === 0) {
|
|
97
|
-
const restoreTab = readPersistedLastTab(req, base, sessionSlug) ?? ''
|
|
98
|
-
const stored = readPersistedListQuery(req, listFiltersKey(base, sessionSlug, restoreTab))
|
|
99
|
-
if (stored) {
|
|
100
|
-
const qs = encodePersistedQuery(stored, restoreTab)
|
|
101
|
-
if (qs !== '') return res.redirect(`${indexUrl}?${qs}`, 302)
|
|
102
|
-
}
|
|
103
|
-
} else {
|
|
104
|
-
const tab = typeof query['tab'] === 'string' ? query['tab'] : ''
|
|
105
|
-
writePersistedListQuery(req, listFiltersKey(base, sessionSlug, tab), query)
|
|
106
|
-
writePersistedLastTab(req, base, sessionSlug, tab)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const data = await resourceIndexData(pilotiq, slug, req.query, req)
|
|
111
|
-
return view('pilotiq.slug', data ?? {})
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
router.post(`${indexUrl}/_widget/:id`, async (req, res) => {
|
|
115
|
-
const user = await pilotiq.resolveUser(req)
|
|
116
|
-
if (!await policyGate(R, user, () => R.canViewAny(user))) return forbidden(res, true)
|
|
117
|
-
return handleWidgetData(req, res, pilotiq, { kind: 'resource', slug }, req.params['id']!)
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
if (R.deferLoading) {
|
|
121
|
-
router.get(`${indexUrl}/_table`, async (req, res) => {
|
|
122
|
-
const user = await pilotiq.resolveUser(req)
|
|
123
|
-
if (!await policyGate(R, user, () => R.canViewAny(user))) return forbidden(res, true)
|
|
124
|
-
const data = await resourceTableData(pilotiq, slug, req.query as Record<string, string>, req)
|
|
125
|
-
if (!data) { res.status(404); return res.json({ ok: false, error: 'Resource not found' }) }
|
|
126
|
-
return res.json({ ok: true, ...data })
|
|
127
|
-
})
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Action dispatch — POST ${resourceBase}/_action/:actionName
|
|
131
|
-
router.post(`${indexUrl}/_action/:actionName`, async (req, res) => {
|
|
132
|
-
const user = await pilotiq.resolveUser(req)
|
|
133
|
-
if (!await policyAccess(R, user)) return forbidden(res, wantsJson(req))
|
|
134
|
-
|
|
135
|
-
const actionName = req.params['actionName']!
|
|
136
|
-
const json = wantsJson(req)
|
|
137
|
-
const body = await readFormBody(req)
|
|
138
|
-
const input = parseActionBody(body)
|
|
139
|
-
|
|
140
|
-
const ctx: SchemaContext = { mode: 'table', basePath: base, ...(user !== null ? { user: user as NonNullable<SchemaContext['user']> } : {}) }
|
|
141
|
-
const elements = await callPageSchema(PageClass, ctx)
|
|
142
|
-
tagActionDispatch(elements, indexUrl)
|
|
143
|
-
const target = resolveDispatchTarget(elements, actionName)
|
|
144
|
-
if (!target) {
|
|
145
|
-
if (json) { res.status(404); return res.json({ ok: false, error: `Action "${actionName}" not found` }) }
|
|
146
|
-
res.status(404)
|
|
147
|
-
return res.send(`Action "${actionName}" not found on ${R.label}`)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const resolveRecord: ResolveRecord | undefined = R.model
|
|
151
|
-
? (id: string) => findRecord(R, id, { user })
|
|
152
|
-
: undefined
|
|
153
|
-
|
|
154
|
-
const result = await dispatchAction(target.action, {
|
|
155
|
-
...input,
|
|
156
|
-
request: req,
|
|
157
|
-
user,
|
|
158
|
-
...(target.rowField ? { rowField: target.rowField } : {}),
|
|
159
|
-
...(target.formSchema ? { formSchema: target.formSchema } : {}),
|
|
160
|
-
}, resolveRecord)
|
|
161
|
-
return sendActionResult(req, res, json, result, base, indexUrl)
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
// Reorderable rows — POST ${resourceBase}/_reorder { ids: [] }
|
|
165
|
-
// Only mounted when `Resource.table()` opts in (boot-time probe
|
|
166
|
-
// populates `reorderEnabled`).
|
|
167
|
-
if (options.reorderable) {
|
|
168
|
-
router.post(`${indexUrl}/_reorder`, async (req, res) => {
|
|
169
|
-
const user = await pilotiq.resolveUser(req)
|
|
170
|
-
// List-level edit gate. The drop affects many rows at once;
|
|
171
|
-
// there's no single record to authorize against, so we pass
|
|
172
|
-
// `undefined` and let user-supplied `canEdit` overrides branch
|
|
173
|
-
// on `record === undefined` if they want row-level granularity.
|
|
174
|
-
if (!await policyGate(R, user, () => R.canEdit(user, undefined))) return forbidden(res, true)
|
|
175
|
-
|
|
176
|
-
const body = await readFormBody(req)
|
|
177
|
-
const raw = (body as { ids?: unknown }).ids
|
|
178
|
-
if (!Array.isArray(raw) || raw.length === 0) {
|
|
179
|
-
res.status(400)
|
|
180
|
-
return res.json({ ok: false, error: 'Missing or empty ids array' })
|
|
181
|
-
}
|
|
182
|
-
const ids = raw.filter((id): id is string | number =>
|
|
183
|
-
typeof id === 'string' || typeof id === 'number',
|
|
184
|
-
)
|
|
185
|
-
if (ids.length !== raw.length) {
|
|
186
|
-
res.status(400)
|
|
187
|
-
return res.json({ ok: false, error: 'ids must contain only strings or numbers' })
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
// Boot already verified `R.model?.reorder` exists; the `!`
|
|
192
|
-
// assertions are safe.
|
|
193
|
-
await R.model!.reorder!(ids)
|
|
194
|
-
return res.json({ ok: true })
|
|
195
|
-
} catch (err) {
|
|
196
|
-
res.status(422)
|
|
197
|
-
return res.json({
|
|
198
|
-
ok: false,
|
|
199
|
-
error: err instanceof Error ? err.message : 'Reorder failed',
|
|
200
|
-
})
|
|
201
|
-
}
|
|
202
|
-
})
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Editable cell columns — POST ${resourceBase}/:id/_cell/:column
|
|
206
|
-
// { value: <coerced> }. Only mounted when the resource declares at
|
|
207
|
-
// least one editable column (boot-time probe populates
|
|
208
|
-
// `editableEnabled`).
|
|
209
|
-
if (options.editable) {
|
|
210
|
-
router.post(`${indexUrl}/:id/_cell/:column`, async (req, res) => {
|
|
211
|
-
const user = await pilotiq.resolveUser(req)
|
|
212
|
-
if (!await policyAccess(R, user)) return forbidden(res, true)
|
|
213
|
-
|
|
214
|
-
const id = req.params['id']!
|
|
215
|
-
const colName = req.params['column']!
|
|
216
|
-
|
|
217
|
-
// Locate the column on the table. We re-derive `Table.make()`
|
|
218
|
-
// here (same probe shape used by the boot guard + reorder route)
|
|
219
|
-
// so the column instance carries its validators / discriminator.
|
|
220
|
-
const probe = R.table(Table.make())
|
|
221
|
-
const col = (probe.getChildren() ?? [])
|
|
222
|
-
.find((c): c is Column => c instanceof Column && c.name === colName)
|
|
223
|
-
if (!col) {
|
|
224
|
-
res.status(400)
|
|
225
|
-
return res.json({ ok: false, error: `Unknown column "${colName}"` })
|
|
226
|
-
}
|
|
227
|
-
if (!col.isEditable()) {
|
|
228
|
-
res.status(400)
|
|
229
|
-
return res.json({ ok: false, error: `Column "${colName}" is not editable` })
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Boot already verified `R.model?.update`; the `!` is safe.
|
|
233
|
-
const record = await findRecord(R, id, { user })
|
|
234
|
-
if (record === null || record === undefined) {
|
|
235
|
-
res.status(404)
|
|
236
|
-
return res.json({ ok: false, error: 'Record not found' })
|
|
237
|
-
}
|
|
238
|
-
if (!await checkPolicy(() => R.canEdit(user, record))) return forbidden(res, true)
|
|
239
|
-
|
|
240
|
-
const body = await readFormBody(req)
|
|
241
|
-
const raw = (body as { value?: unknown }).value
|
|
242
|
-
|
|
243
|
-
let value: unknown
|
|
244
|
-
try { value = coerceCellValue(col, raw) }
|
|
245
|
-
catch (err) {
|
|
246
|
-
const message = err instanceof CellCoerceError ? err.message
|
|
247
|
-
: err instanceof Error ? err.message
|
|
248
|
-
: 'Invalid value'
|
|
249
|
-
res.status(422)
|
|
250
|
-
return res.json({ ok: false, errors: { value: [message] } })
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const errors = await col.runValidators(value, { record })
|
|
254
|
-
if (errors.length > 0) {
|
|
255
|
-
res.status(422)
|
|
256
|
-
return res.json({ ok: false, errors: { value: errors } })
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// beforeStateUpdated — runs after validators pass, before the
|
|
260
|
-
// DB write. Throwing halts with 422 under `_cell`.
|
|
261
|
-
const beforeHook = col.getBeforeStateUpdated()
|
|
262
|
-
if (beforeHook) {
|
|
263
|
-
try { await beforeHook(value, { record: record as Record<string, unknown>, user }) }
|
|
264
|
-
catch (err) {
|
|
265
|
-
res.status(422)
|
|
266
|
-
return res.json({ ok: false, errors: { _cell: [cellHookErrorMessage(err)] } })
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
try {
|
|
271
|
-
await R.model!.update(id, { [col.name]: value })
|
|
272
|
-
} catch (err) {
|
|
273
|
-
res.status(422)
|
|
274
|
-
return res.json({
|
|
275
|
-
ok: false,
|
|
276
|
-
error: err instanceof Error ? err.message : 'Update failed',
|
|
277
|
-
})
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// afterStateUpdated — runs only on a confirmed write. Throwing
|
|
281
|
-
// surfaces the error to the user; the DB row is already
|
|
282
|
-
// updated (the hook is for follow-up effects, not rollback).
|
|
283
|
-
const afterHook = col.getAfterStateUpdated()
|
|
284
|
-
if (afterHook) {
|
|
285
|
-
try { await afterHook(value, { record: record as Record<string, unknown>, user }) }
|
|
286
|
-
catch (err) {
|
|
287
|
-
res.status(422)
|
|
288
|
-
return res.json({ ok: false, errors: { _cell: [cellHookErrorMessage(err)] } })
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return res.json({ ok: true, value, notifications: [] })
|
|
293
|
-
})
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Plan #5 — partial-resolve endpoint for create-mode forms.
|
|
298
|
-
// POST ${resourceBase}/_form/:formId/state
|
|
299
|
-
if (pages.create) {
|
|
300
|
-
router.post(`${resourceBase}/_form/:formId/state`, async (req, res) => {
|
|
301
|
-
const user = await pilotiq.resolveUser(req)
|
|
302
|
-
if (!await policyGate(R, user, () => R.canCreate(user))) return forbidden(res, true)
|
|
303
|
-
const formId = req.params['formId']!
|
|
304
|
-
return handleFormState(req, res, pilotiq, { kind: 'resource-create', slug }, formId)
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
// Plan #8 — wizard step-validate endpoint for create-mode forms.
|
|
308
|
-
router.post(`${resourceBase}/_form/:formId/wizard`, async (req, res) => {
|
|
309
|
-
const user = await pilotiq.resolveUser(req)
|
|
310
|
-
if (!await policyGate(R, user, () => R.canCreate(user))) return forbidden(res, true)
|
|
311
|
-
const formId = req.params['formId']!
|
|
312
|
-
return handleFormWizard(req, res, pilotiq, { kind: 'resource-create', slug }, formId)
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
// Async-mention endpoint for create-mode forms.
|
|
316
|
-
router.post(`${resourceBase}/_form/:formId/mentions`, async (req, res) => {
|
|
317
|
-
const user = await pilotiq.resolveUser(req)
|
|
318
|
-
if (!await policyGate(R, user, () => R.canCreate(user))) return forbidden(res, true)
|
|
319
|
-
const formId = req.params['formId']!
|
|
320
|
-
return handleFormMentions(req, res, pilotiq, { kind: 'resource-create', slug }, formId)
|
|
321
|
-
})
|
|
322
|
-
|
|
323
|
-
// SelectField inline-create modal endpoint for create-mode forms.
|
|
324
|
-
router.post(`${resourceBase}/_form/:formId/create-option/:fieldName`, async (req, res) => {
|
|
325
|
-
const user = await pilotiq.resolveUser(req)
|
|
326
|
-
if (!await policyGate(R, user, () => R.canCreate(user))) return forbidden(res, true)
|
|
327
|
-
const formId = req.params['formId']!
|
|
328
|
-
const fieldName = req.params['fieldName']!
|
|
329
|
-
return handleFormCreateOption(req, res, pilotiq, { kind: 'resource-create', slug }, formId, fieldName)
|
|
330
|
-
})
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Plan #5 — partial-resolve endpoint for edit-mode forms.
|
|
334
|
-
// POST ${resourceBase}/:id/_form/:formId/state
|
|
335
|
-
if (pages.edit) {
|
|
336
|
-
router.post(`${resourceBase}/:id/_form/:formId/state`, async (req, res) => {
|
|
337
|
-
const recordId = req.params['id']!
|
|
338
|
-
const formId = req.params['formId']!
|
|
339
|
-
const user = await pilotiq.resolveUser(req)
|
|
340
|
-
const { access, record: policyRecord } = await loadAccessGated(R, recordId, user)
|
|
341
|
-
if (!access) return forbidden(res, true)
|
|
342
|
-
if (!await checkPolicy(() => R.canEdit(user, policyRecord))) return forbidden(res, true)
|
|
343
|
-
return handleFormState(req, res, pilotiq, { kind: 'resource-edit', slug, recordId }, formId)
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
// Plan #8 — wizard step-validate endpoint for edit-mode forms.
|
|
347
|
-
router.post(`${resourceBase}/:id/_form/:formId/wizard`, async (req, res) => {
|
|
348
|
-
const recordId = req.params['id']!
|
|
349
|
-
const formId = req.params['formId']!
|
|
350
|
-
const user = await pilotiq.resolveUser(req)
|
|
351
|
-
const { access, record: policyRecord } = await loadAccessGated(R, recordId, user)
|
|
352
|
-
if (!access) return forbidden(res, true)
|
|
353
|
-
if (!await checkPolicy(() => R.canEdit(user, policyRecord))) return forbidden(res, true)
|
|
354
|
-
return handleFormWizard(req, res, pilotiq, { kind: 'resource-edit', slug, recordId }, formId)
|
|
355
|
-
})
|
|
356
|
-
|
|
357
|
-
// Async-mention endpoint for edit-mode forms.
|
|
358
|
-
router.post(`${resourceBase}/:id/_form/:formId/mentions`, async (req, res) => {
|
|
359
|
-
const recordId = req.params['id']!
|
|
360
|
-
const formId = req.params['formId']!
|
|
361
|
-
const user = await pilotiq.resolveUser(req)
|
|
362
|
-
const { access, record: policyRecord } = await loadAccessGated(R, recordId, user)
|
|
363
|
-
if (!access) return forbidden(res, true)
|
|
364
|
-
if (!await checkPolicy(() => R.canEdit(user, policyRecord))) return forbidden(res, true)
|
|
365
|
-
return handleFormMentions(req, res, pilotiq, { kind: 'resource-edit', slug, recordId }, formId)
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
// SelectField inline-create modal endpoint for edit-mode forms.
|
|
369
|
-
router.post(`${resourceBase}/:id/_form/:formId/create-option/:fieldName`, async (req, res) => {
|
|
370
|
-
const recordId = req.params['id']!
|
|
371
|
-
const formId = req.params['formId']!
|
|
372
|
-
const fieldName = req.params['fieldName']!
|
|
373
|
-
const user = await pilotiq.resolveUser(req)
|
|
374
|
-
const { access, record: policyRecord } = await loadAccessGated(R, recordId, user)
|
|
375
|
-
if (!access) return forbidden(res, true)
|
|
376
|
-
if (!await checkPolicy(() => R.canEdit(user, policyRecord))) return forbidden(res, true)
|
|
377
|
-
return handleFormCreateOption(req, res, pilotiq, { kind: 'resource-edit', slug, recordId }, formId, fieldName)
|
|
378
|
-
})
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Create — GET ${resourceBase}/create
|
|
382
|
-
if (pages.create) {
|
|
383
|
-
const PageClass = pages.create
|
|
384
|
-
const createUrl = `${resourceBase}/create`
|
|
385
|
-
|
|
386
|
-
router.get(createUrl, async (req, res) => {
|
|
387
|
-
const user = await pilotiq.resolveUser(req)
|
|
388
|
-
if (!await policyGate(R, user, () => R.canCreate(user))) return forbidden(res, wantsJson(req))
|
|
389
|
-
const data = await resourceCreateData(pilotiq, slug, undefined, req)
|
|
390
|
-
return view('pilotiq.resource-create', data ?? {})
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
// Create — POST ${resourceBase}/create
|
|
394
|
-
router.post(createUrl, async (req, res) => {
|
|
395
|
-
const user = await pilotiq.resolveUser(req)
|
|
396
|
-
if (!await policyGate(R, user, () => R.canCreate(user))) return forbidden(res, wantsJson(req))
|
|
397
|
-
|
|
398
|
-
const body = await readFormBody(req)
|
|
399
|
-
const { values, formId, continueCreate } = splitMeta(body)
|
|
400
|
-
const json = wantsJson(req)
|
|
401
|
-
|
|
402
|
-
const ctx: SchemaContext = { mode: 'create', basePath: base, ...(user !== null ? { user: user as NonNullable<SchemaContext['user']> } : {}) }
|
|
403
|
-
const elements = await callPageSchema(PageClass, ctx)
|
|
404
|
-
tagFormActions(elements, createUrl)
|
|
405
|
-
const form = selectForm(findForms(elements), formId)
|
|
406
|
-
if (!form) {
|
|
407
|
-
if (json) { res.status(404); return res.json({ ok: false, error: 'No form found on page' }) }
|
|
408
|
-
res.status(404)
|
|
409
|
-
return res.send('No form found on page')
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const result = await dispatchFormSubmit(form, values, {
|
|
413
|
-
values,
|
|
414
|
-
basePath: base,
|
|
415
|
-
...(R.model ? { parentModel: R.model } : {}),
|
|
416
|
-
})
|
|
417
|
-
|
|
418
|
-
if (!result.ok) {
|
|
419
|
-
if (json) {
|
|
420
|
-
res.status(422)
|
|
421
|
-
return res.json({ ok: false, errors: result.errors })
|
|
422
|
-
}
|
|
423
|
-
// Re-render through the same builder so the page is identical to GET,
|
|
424
|
-
// just with values + errors prefilled.
|
|
425
|
-
const data = await resourceCreateData(pilotiq, slug, { values, errors: result.errors })
|
|
426
|
-
res.status(422)
|
|
427
|
-
return view('pilotiq.resource-create', data ?? {})
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const recordId = (result.record as { id?: unknown })?.id
|
|
431
|
-
// "Create & create another" — when the secondary submit fired,
|
|
432
|
-
// route back to the create page with a fresh form. Skips any
|
|
433
|
-
// user-supplied `redirectAfterSave`: the user clicked the
|
|
434
|
-
// button asking explicitly to create another, so the
|
|
435
|
-
// continue-intent wins. `force: true` tells the SPA-mode
|
|
436
|
-
// FormRenderer to navigate even though the redirect URL
|
|
437
|
-
// matches the current page (otherwise the same-URL skip
|
|
438
|
-
// would preserve the just-submitted values on screen).
|
|
439
|
-
const fallback = continueCreate
|
|
440
|
-
? createUrl
|
|
441
|
-
: recordId !== undefined ? `${resourceBase}/${String(recordId)}/edit` : `${resourceBase}`
|
|
442
|
-
const redirect = continueCreate
|
|
443
|
-
? createUrl
|
|
444
|
-
: normalizeRedirect(result.redirect, base) ?? fallback
|
|
445
|
-
return sendRedirectResponse(req, res, json, redirect, result.notifications, {
|
|
446
|
-
...(continueCreate ? { force: true as const } : {}),
|
|
447
|
-
...(result.relationshipRenames.length > 0 ? { relationshipRenames: result.relationshipRenames } : {}),
|
|
448
|
-
})
|
|
449
|
-
})
|
|
450
|
-
|
|
451
|
-
// Action dispatch — POST ${createUrl}/_action/:actionName
|
|
452
|
-
// Handles both page-level handler-style actions AND Repeater /
|
|
453
|
-
// Builder `extraItemActions` rows. The latter pass `_rowPath` in
|
|
454
|
-
// the body so the dispatcher hydrates `ctx.row` from the form's
|
|
455
|
-
// coerced values.
|
|
456
|
-
router.post(`${createUrl}/_action/:actionName`, async (req, res) => {
|
|
457
|
-
const user = await pilotiq.resolveUser(req)
|
|
458
|
-
if (!await policyGate(R, user, () => R.canCreate(user))) return forbidden(res, wantsJson(req))
|
|
459
|
-
|
|
460
|
-
const actionName = req.params['actionName']!
|
|
461
|
-
const json = wantsJson(req)
|
|
462
|
-
const body = await readFormBody(req)
|
|
463
|
-
const input = parseActionBody(body)
|
|
464
|
-
|
|
465
|
-
const ctx: SchemaContext = { mode: 'create', basePath: base, ...(user !== null ? { user: user as NonNullable<SchemaContext['user']> } : {}) }
|
|
466
|
-
const elements = await callPageSchema(PageClass, ctx)
|
|
467
|
-
tagActionDispatch(elements, createUrl)
|
|
468
|
-
const target = resolveDispatchTarget(elements, actionName)
|
|
469
|
-
if (!target) {
|
|
470
|
-
if (json) { res.status(404); return res.json({ ok: false, error: `Action "${actionName}" not found` }) }
|
|
471
|
-
res.status(404)
|
|
472
|
-
return res.send(`Action "${actionName}" not found on ${R.label}`)
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const result = await dispatchAction(target.action, {
|
|
476
|
-
...input,
|
|
477
|
-
request: req,
|
|
478
|
-
user,
|
|
479
|
-
...(target.rowField ? { rowField: target.rowField } : {}),
|
|
480
|
-
...(target.formSchema ? { formSchema: target.formSchema } : {}),
|
|
481
|
-
})
|
|
482
|
-
return sendActionResult(req, res, json, result, base, createUrl)
|
|
483
|
-
})
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// View — GET ${resourceBase}/:id (literal `create` matches first via
|
|
487
|
-
// Hono's literal-over-param routing, so `:id` only catches everything else.)
|
|
488
|
-
if (pages.view) {
|
|
489
|
-
router.get(`${resourceBase}/:id`, async (req, res) => {
|
|
490
|
-
const recordId = req.params['id']!
|
|
491
|
-
// Hono routes both `/create` and `/:id` against this slot; only the
|
|
492
|
-
// literal `create` segment hits the create route. Defensive guard:
|
|
493
|
-
if (recordId === 'create') return // handled by create route
|
|
494
|
-
|
|
495
|
-
const user = await pilotiq.resolveUser(req)
|
|
496
|
-
// Load the record in parallel with the access probe so canView
|
|
497
|
-
// can inspect it. Stub `{ id }` when the resource has no model
|
|
498
|
-
// wired — the user-authored predicate gets to decide what to
|
|
499
|
-
// do with it.
|
|
500
|
-
const { access, record } = await loadAccessGated(R, recordId, user)
|
|
501
|
-
if (!access) return forbidden(res, wantsJson(req))
|
|
502
|
-
if (!await checkPolicy(() => R.canView(user, record))) return forbidden(res, wantsJson(req))
|
|
503
|
-
|
|
504
|
-
const data = await resourceViewData(pilotiq, slug, recordId, req)
|
|
505
|
-
return view('pilotiq.resource-view', data ?? {})
|
|
506
|
-
})
|
|
507
|
-
|
|
508
|
-
// Delete — POST ${resourceBase}/:id/delete
|
|
509
|
-
router.post(`${resourceBase}/:id/delete`, async (req, res) => {
|
|
510
|
-
const recordId = req.params['id']!
|
|
511
|
-
const json = wantsJson(req)
|
|
512
|
-
const indexUrl = `${resourceBase}`
|
|
513
|
-
|
|
514
|
-
const user = await pilotiq.resolveUser(req)
|
|
515
|
-
const { access, record } = await loadAccessGated(R, recordId, user)
|
|
516
|
-
if (!access) return forbidden(res, json)
|
|
517
|
-
if (!await checkPolicy(() => R.canDelete(user, record))) return forbidden(res, json)
|
|
518
|
-
|
|
519
|
-
try {
|
|
520
|
-
await R.deleteRecord(recordId)
|
|
521
|
-
} catch (err) {
|
|
522
|
-
const message = err instanceof Error ? err.message : 'Delete failed'
|
|
523
|
-
if (json) {
|
|
524
|
-
res.status(500)
|
|
525
|
-
return res.json({ ok: false, error: message })
|
|
526
|
-
}
|
|
527
|
-
res.status(500)
|
|
528
|
-
return res.send(message)
|
|
529
|
-
}
|
|
530
|
-
// Plan #13: use "moved to trash" framing on soft-delete resources
|
|
531
|
-
// so users know the row is recoverable.
|
|
532
|
-
const title = R.softDeletes
|
|
533
|
-
? `${R.labelSingular} moved to trash`
|
|
534
|
-
: `${R.labelSingular} deleted`
|
|
535
|
-
return sendMutationSuccess(req, res, json, { id: recordId, kind: 'delete', title, redirect: indexUrl })
|
|
536
|
-
})
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// ─── Plan #13 soft-delete routes (restore / force-delete) ─────
|
|
540
|
-
// Both routes opt-in only when `Resource.softDeletes = true`. They
|
|
541
|
-
// load the target row through `withTrashed()` so the lookup finds
|
|
542
|
-
// currently-trashed records (which the default scope hides). The
|
|
543
|
-
// `restore` route undoes a prior soft-delete; `force-delete`
|
|
544
|
-
// bypasses soft-delete entirely.
|
|
545
|
-
if (R.softDeletes) {
|
|
546
|
-
// Boot-time guard — yell loudly if the rudder ORM model isn't
|
|
547
|
-
// wired up. Keeps "why didn't restore work?" debug sessions
|
|
548
|
-
// short. Pilotiq's flag and rudder's flag are deliberately
|
|
549
|
-
// independent (see plan doc).
|
|
550
|
-
if (!R.model) {
|
|
551
|
-
throw new Error(
|
|
552
|
-
`[Pilotiq] ${R.name}: softDeletes = true requires a Resource.model. Wire one up or unset softDeletes.`,
|
|
553
|
-
)
|
|
554
|
-
}
|
|
555
|
-
if (typeof R.model.restore !== 'function' || typeof R.model.forceDelete !== 'function') {
|
|
556
|
-
throw new Error(
|
|
557
|
-
`[Pilotiq] ${R.name}: softDeletes = true but model.restore / model.forceDelete are missing. ` +
|
|
558
|
-
`Set Model.softDeletes = true on the rudder side, or upgrade @rudderjs/orm.`,
|
|
559
|
-
)
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
const M = R.model
|
|
563
|
-
const pk = (M.primaryKey ?? 'id') as string
|
|
564
|
-
|
|
565
|
-
// Load a row through `withTrashed` so currently-trashed records
|
|
566
|
-
// resolve. Falls back to `M.find` when the query doesn't expose
|
|
567
|
-
// `withTrashed()` (older ORM versions). Returns undefined when
|
|
568
|
-
// the lookup misses (route converts to 404).
|
|
569
|
-
const loadTrashable = async (id: string): Promise<unknown> => {
|
|
570
|
-
const found = await findInQueryWithTrashed(M.query(), pk, id)
|
|
571
|
-
if (found !== undefined) return found
|
|
572
|
-
return M.find(id).catch(() => undefined)
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// Restore — POST ${resourceBase}/:id/restore
|
|
576
|
-
router.post(`${resourceBase}/:id/restore`, async (req, res) => {
|
|
577
|
-
const recordId = req.params['id']!
|
|
578
|
-
const json = wantsJson(req)
|
|
579
|
-
const indexUrl = `${resourceBase}`
|
|
580
|
-
|
|
581
|
-
const user = await pilotiq.resolveUser(req)
|
|
582
|
-
if (!await policyAccess(R, user)) return forbidden(res, json)
|
|
583
|
-
const record = await loadTrashable(recordId)
|
|
584
|
-
if (!record) {
|
|
585
|
-
res.status(404)
|
|
586
|
-
return json ? res.json({ ok: false, error: 'Not found' }) : res.send('Not found')
|
|
587
|
-
}
|
|
588
|
-
if (!await checkPolicy(() => R.canRestore(user, record))) return forbidden(res, json)
|
|
589
|
-
|
|
590
|
-
try {
|
|
591
|
-
await M.restore!(recordId)
|
|
592
|
-
} catch (err) {
|
|
593
|
-
const message = err instanceof Error ? err.message : 'Restore failed'
|
|
594
|
-
res.status(500)
|
|
595
|
-
return json ? res.json({ ok: false, error: message }) : res.send(message)
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
return sendMutationSuccess(req, res, json, {
|
|
599
|
-
id: recordId, kind: 'restore', title: `${R.labelSingular} restored`, redirect: indexUrl,
|
|
600
|
-
})
|
|
601
|
-
})
|
|
602
|
-
|
|
603
|
-
// Force-delete — POST ${resourceBase}/:id/force-delete
|
|
604
|
-
router.post(`${resourceBase}/:id/force-delete`, async (req, res) => {
|
|
605
|
-
const recordId = req.params['id']!
|
|
606
|
-
const json = wantsJson(req)
|
|
607
|
-
const indexUrl = `${resourceBase}`
|
|
608
|
-
|
|
609
|
-
const user = await pilotiq.resolveUser(req)
|
|
610
|
-
if (!await policyAccess(R, user)) return forbidden(res, json)
|
|
611
|
-
const record = await loadTrashable(recordId)
|
|
612
|
-
if (!record) {
|
|
613
|
-
res.status(404)
|
|
614
|
-
return json ? res.json({ ok: false, error: 'Not found' }) : res.send('Not found')
|
|
615
|
-
}
|
|
616
|
-
if (!await checkPolicy(() => R.canForceDelete(user, record))) return forbidden(res, json)
|
|
617
|
-
|
|
618
|
-
try {
|
|
619
|
-
await M.forceDelete!(recordId)
|
|
620
|
-
} catch (err) {
|
|
621
|
-
const message = err instanceof Error ? err.message : 'Force-delete failed'
|
|
622
|
-
res.status(500)
|
|
623
|
-
return json ? res.json({ ok: false, error: message }) : res.send(message)
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
return sendMutationSuccess(req, res, json, {
|
|
627
|
-
id: recordId, kind: 'fdelete', title: `${R.labelSingular} permanently deleted`, redirect: indexUrl,
|
|
628
|
-
})
|
|
629
|
-
})
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// Edit — GET ${resourceBase}/:id/edit
|
|
633
|
-
if (pages.edit) {
|
|
634
|
-
const PageClass = pages.edit
|
|
635
|
-
|
|
636
|
-
router.get(`${resourceBase}/:id/edit`, async (req, res) => {
|
|
637
|
-
const recordId = req.params['id']!
|
|
638
|
-
const user = await pilotiq.resolveUser(req)
|
|
639
|
-
const { access, record } = await loadAccessGated(R, recordId, user)
|
|
640
|
-
if (!access) return forbidden(res, wantsJson(req))
|
|
641
|
-
if (!await checkPolicy(() => R.canEdit(user, record))) return forbidden(res, wantsJson(req))
|
|
642
|
-
|
|
643
|
-
const data = await resourceEditData(pilotiq, slug, recordId, undefined, req)
|
|
644
|
-
return view('pilotiq.resource-edit', data ?? {})
|
|
645
|
-
})
|
|
646
|
-
|
|
647
|
-
// Edit — POST ${resourceBase}/:id/edit
|
|
648
|
-
router.post(`${resourceBase}/:id/edit`, async (req, res) => {
|
|
649
|
-
const recordId = req.params['id']!
|
|
650
|
-
const editUrl = `${resourceBase}/${recordId}/edit`
|
|
651
|
-
const body = await readFormBody(req)
|
|
652
|
-
const { values, formId } = splitMeta(body)
|
|
653
|
-
const json = wantsJson(req)
|
|
654
|
-
|
|
655
|
-
const user = await pilotiq.resolveUser(req)
|
|
656
|
-
const { access, record: policyRecord } = await loadAccessGated(R, recordId, user)
|
|
657
|
-
if (!access) return forbidden(res, json)
|
|
658
|
-
if (!await checkPolicy(() => R.canEdit(user, policyRecord))) return forbidden(res, json)
|
|
659
|
-
|
|
660
|
-
const ctx: SchemaContext = { mode: 'edit', recordId, basePath: base, ...(user !== null ? { user: user as NonNullable<SchemaContext['user']> } : {}) }
|
|
661
|
-
const elements = await callPageSchema(PageClass, ctx)
|
|
662
|
-
tagFormActions(elements, editUrl)
|
|
663
|
-
const form = selectForm(findForms(elements), formId)
|
|
664
|
-
if (!form) {
|
|
665
|
-
if (json) { res.status(404); return res.json({ ok: false, error: 'No form found on page' }) }
|
|
666
|
-
res.status(404)
|
|
667
|
-
return res.send('No form found on page')
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// Try to load the record so validators with cross-field rules see it.
|
|
671
|
-
let record: unknown = undefined
|
|
672
|
-
if (form.getLoadRecord()) {
|
|
673
|
-
try { record = await form.getLoadRecord()!(recordId, { values }) } catch { /* ignore */ }
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
const result = await dispatchFormSubmit(
|
|
677
|
-
form,
|
|
678
|
-
values,
|
|
679
|
-
{
|
|
680
|
-
values,
|
|
681
|
-
basePath: base,
|
|
682
|
-
...(record !== undefined ? { record } : {}),
|
|
683
|
-
...(R.model ? { parentModel: R.model } : {}),
|
|
684
|
-
},
|
|
685
|
-
)
|
|
686
|
-
|
|
687
|
-
if (!result.ok) {
|
|
688
|
-
if (json) {
|
|
689
|
-
res.status(422)
|
|
690
|
-
return res.json({ ok: false, errors: result.errors })
|
|
691
|
-
}
|
|
692
|
-
const data = await resourceEditData(pilotiq, slug, recordId, { values, errors: result.errors })
|
|
693
|
-
res.status(422)
|
|
694
|
-
return view('pilotiq.resource-edit', data ?? {})
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
const redirect = normalizeRedirect(result.redirect, base) ?? editUrl
|
|
698
|
-
return sendRedirectResponse(req, res, json, redirect, result.notifications,
|
|
699
|
-
result.relationshipRenames.length > 0 ? { relationshipRenames: result.relationshipRenames } : undefined,
|
|
700
|
-
)
|
|
701
|
-
})
|
|
702
|
-
|
|
703
|
-
// Action dispatch — POST ${editUrl}/_action/:actionName
|
|
704
|
-
// Same shape as the create-page _action route. The `:id` segment
|
|
705
|
-
// gates record-aware policy (canEdit per record); row-scoped
|
|
706
|
-
// dispatch reuses the form schema we resolve here for `coerceFormValues`.
|
|
707
|
-
router.post(`${resourceBase}/:id/_action/:actionName`, async (req, res) => {
|
|
708
|
-
const recordId = req.params['id']!
|
|
709
|
-
// Hono routes `/edit` and `/delete` against this slot too — bail
|
|
710
|
-
// out so the dedicated handlers downstream pick them up. The
|
|
711
|
-
// `:actionName` capture catches anything; the explicit guard
|
|
712
|
-
// mirrors the view-route `recordId === 'create'` defensive branch.
|
|
713
|
-
const actionName = req.params['actionName']!
|
|
714
|
-
|
|
715
|
-
const user = await pilotiq.resolveUser(req)
|
|
716
|
-
const { access, record: policyRecord } = await loadAccessGated(R, recordId, user)
|
|
717
|
-
if (!access) return forbidden(res, wantsJson(req))
|
|
718
|
-
if (!await checkPolicy(() => R.canEdit(user, policyRecord))) return forbidden(res, wantsJson(req))
|
|
719
|
-
|
|
720
|
-
const json = wantsJson(req)
|
|
721
|
-
const body = await readFormBody(req)
|
|
722
|
-
const input = parseActionBody(body)
|
|
723
|
-
|
|
724
|
-
const editUrl = `${resourceBase}/${recordId}/edit`
|
|
725
|
-
const ctx: SchemaContext = { mode: 'edit', recordId, basePath: base, ...(user !== null ? { user: user as NonNullable<SchemaContext['user']> } : {}) }
|
|
726
|
-
const elements = await callPageSchema(PageClass, ctx)
|
|
727
|
-
tagActionDispatch(elements, editUrl)
|
|
728
|
-
const target = resolveDispatchTarget(elements, actionName)
|
|
729
|
-
if (!target) {
|
|
730
|
-
if (json) { res.status(404); return res.json({ ok: false, error: `Action "${actionName}" not found` }) }
|
|
731
|
-
res.status(404)
|
|
732
|
-
return res.send(`Action "${actionName}" not found on ${R.label}`)
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
const resolveRecord: ResolveRecord | undefined = R.model
|
|
736
|
-
? (id: string) => findRecord(R, id, { user })
|
|
737
|
-
: undefined
|
|
738
|
-
|
|
739
|
-
const result = await dispatchAction(target.action, {
|
|
740
|
-
...input,
|
|
741
|
-
request: req,
|
|
742
|
-
user,
|
|
743
|
-
...(target.rowField ? { rowField: target.rowField } : {}),
|
|
744
|
-
...(target.formSchema ? { formSchema: target.formSchema } : {}),
|
|
745
|
-
}, resolveRecord)
|
|
746
|
-
return sendActionResult(req, res, json, result, base, editUrl)
|
|
747
|
-
})
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// ── Plan #11 relation manager routes ───────────────
|
|
751
|
-
// Per-manager: list, create (GET/POST), edit (GET/POST), delete (POST),
|
|
752
|
-
// restore / force-delete (soft-deletes), `_action`, `_detach` (M2M).
|
|
753
|
-
// Mounted under ${resourceBase}/:id/${rel} — the `:id` segment is the
|
|
754
|
-
// PARENT record id; the `:childId` segment (where present) is the
|
|
755
|
-
// related record's id. Authorization runs in two layers: parent
|
|
756
|
-
// canAccess + canEdit(parent), then manager-scoped can*. Depth-2
|
|
757
|
-
// nested relations register the same shape under a deeper prefix.
|
|
758
|
-
//
|
|
759
|
-
// Pulled out 2026-05-12 (Phase 4 of the routes.ts split).
|
|
760
|
-
registerRelationRoutes(router, pilotiq, R, base)
|
|
761
|
-
|
|
762
|
-
// ── Record sub-pages ───────────────────────────────
|
|
763
|
-
// `${resourceBase}/:id/${subSlug}` — same URL slot as a relation
|
|
764
|
-
// manager's `relationship`, distinguished by registry: the data
|
|
765
|
-
// builder tries the relation lookup first, then falls through to
|
|
766
|
-
// the record sub-page map. Boot-time validation ensures the slugs
|
|
767
|
-
// don't collide.
|
|
768
|
-
for (const [subPageSlug, SubPage] of Object.entries(R.getRecordPages())) {
|
|
769
|
-
void SubPage // referenced inside `resourceRecordPageData` via the registry; the local binding is captured for closure-stable types only.
|
|
770
|
-
const recordPageUrl = `${resourceBase}/:id/${subPageSlug}`
|
|
771
|
-
router.get(recordPageUrl, async (req, res) => {
|
|
772
|
-
const user = await pilotiq.resolveUser(req)
|
|
773
|
-
if (!await policyAccess(R, user)) return forbidden(res, wantsJson(req))
|
|
774
|
-
const recordId = req.params['id']!
|
|
775
|
-
const data = await resourceRecordPageData(pilotiq, slug, recordId, subPageSlug, req)
|
|
776
|
-
if (data === null) { res.status(404); return res.send('Not found') }
|
|
777
|
-
if ('ok' in data && data.ok === false) return forbidden(res, wantsJson(req))
|
|
778
|
-
return view('pilotiq.slug', data)
|
|
779
|
-
})
|
|
780
|
-
}
|
|
781
|
-
}
|