@pilotiq/pilotiq 0.23.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 +91 -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/dist/actions/exportFactory.d.ts +10 -0
- package/dist/actions/exportFactory.d.ts.map +1 -1
- package/dist/actions/exportFactory.js +10 -0
- package/dist/actions/exportFactory.js.map +1 -1
- package/dist/react/CollabRoomContext.d.ts +5 -5
- package/dist/react/index.d.ts +0 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +0 -1
- package/dist/react/index.js.map +1 -1
- package/dist/routes/helpers.d.ts.map +1 -1
- package/dist/routes/helpers.js +6 -2
- package/dist/routes/helpers.js.map +1 -1
- package/dist/routes/relations.d.ts.map +1 -1
- package/dist/routes/relations.js +12 -0
- package/dist/routes/relations.js.map +1 -1
- package/package.json +6 -1
- package/.turbo/turbo-build.log +0 -8
- package/CLAUDE.md +0 -265
- package/dist/react/useCollabSeed.d.ts +0 -23
- package/dist/react/useCollabSeed.d.ts.map +0 -1
- package/dist/react/useCollabSeed.js +0 -82
- package/dist/react/useCollabSeed.js.map +0 -1
- 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 -215
- 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 -195
- 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/useCollabSeed.ts +0 -86
- 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 -700
- package/src/routes/pages.ts +0 -175
- package/src/routes/panel.ts +0 -204
- package/src/routes/relations.ts +0 -1227
- 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
|
@@ -1,676 +0,0 @@
|
|
|
1
|
-
import { describe, it, beforeEach } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
|
|
4
|
-
import { Router } from '@rudderjs/router'
|
|
5
|
-
|
|
6
|
-
import { Pilotiq } from './Pilotiq.js'
|
|
7
|
-
import { Resource } from './Resource.js'
|
|
8
|
-
import { RelationManager } from './RelationManager.js'
|
|
9
|
-
import { Form } from './elements/Form.js'
|
|
10
|
-
import { Table } from './elements/Table.js'
|
|
11
|
-
import { Column } from './Column.js'
|
|
12
|
-
import { TextField } from './fields/TextField.js'
|
|
13
|
-
import { Heading } from './schema/Heading.js'
|
|
14
|
-
import { registerPilotiqRoutes } from './routes.js'
|
|
15
|
-
import type { ModelLike, ModelQuery } from './orm/modelDefaults.js'
|
|
16
|
-
|
|
17
|
-
// ── Test doubles (parallel to routes-relations.test.ts) ──────────
|
|
18
|
-
|
|
19
|
-
interface Row extends Record<string, unknown> { id: string | number }
|
|
20
|
-
|
|
21
|
-
class StubQuery implements ModelQuery {
|
|
22
|
-
private filters: Array<{ col: string; val: unknown }> = []
|
|
23
|
-
constructor(private rows: Row[]) {}
|
|
24
|
-
where(col: string, ...rest: unknown[]): ModelQuery {
|
|
25
|
-
const val = rest.length === 1 ? rest[0] : rest[1]
|
|
26
|
-
this.filters.push({ col, val })
|
|
27
|
-
return this
|
|
28
|
-
}
|
|
29
|
-
orWhere(...args: unknown[]): ModelQuery { return this.where(args[0] as string, ...args.slice(1)) }
|
|
30
|
-
orderBy(_c: string, _d?: 'ASC' | 'DESC'): ModelQuery { return this }
|
|
31
|
-
async paginate() {
|
|
32
|
-
let data = this.rows
|
|
33
|
-
for (const f of this.filters) data = data.filter(r => r[f.col] === f.val)
|
|
34
|
-
return { data, total: data.length }
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function fakeReq(overrides: Partial<{
|
|
39
|
-
params: Record<string, string>
|
|
40
|
-
body: unknown
|
|
41
|
-
query: Record<string, string>
|
|
42
|
-
headers: Record<string, string>
|
|
43
|
-
}> = {}): any {
|
|
44
|
-
return {
|
|
45
|
-
params: overrides.params ?? {},
|
|
46
|
-
body: overrides.body ?? null,
|
|
47
|
-
query: overrides.query ?? {},
|
|
48
|
-
headers: overrides.headers ?? {},
|
|
49
|
-
raw: {},
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface FakeRes {
|
|
54
|
-
statusCode: number
|
|
55
|
-
redirectedTo?: { url: string; code: number }
|
|
56
|
-
sentBody?: unknown
|
|
57
|
-
status(code: number): FakeRes
|
|
58
|
-
redirect(url: string, code?: number): FakeRes
|
|
59
|
-
send(body: unknown): FakeRes
|
|
60
|
-
json(body: unknown): FakeRes
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function fakeRes(): FakeRes {
|
|
64
|
-
const r: FakeRes = {
|
|
65
|
-
statusCode: 200,
|
|
66
|
-
status(code) { this.statusCode = code; return this },
|
|
67
|
-
redirect(url, code = 302) { this.redirectedTo = { url, code }; return this },
|
|
68
|
-
send(body) { this.sentBody = body; return this },
|
|
69
|
-
json(body) { this.sentBody = body; return this },
|
|
70
|
-
}
|
|
71
|
-
return r
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function callHandler(handler: (...args: any[]) => unknown, req: any = fakeReq(), res: any = fakeRes()) {
|
|
75
|
-
const result = await handler(req, res)
|
|
76
|
-
return { result, res: res as FakeRes }
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function findAdapter(find: (id: string) => Promise<unknown>): ModelQuery {
|
|
80
|
-
let captured: unknown
|
|
81
|
-
const q: ModelQuery = {
|
|
82
|
-
where(...args: unknown[]): ModelQuery {
|
|
83
|
-
captured = args.length === 2 ? args[1] : args[2]
|
|
84
|
-
return q
|
|
85
|
-
},
|
|
86
|
-
orWhere(...args: unknown[]): ModelQuery {
|
|
87
|
-
captured = args.length === 2 ? args[1] : args[2]
|
|
88
|
-
return q
|
|
89
|
-
},
|
|
90
|
-
orderBy(): ModelQuery { return q },
|
|
91
|
-
async paginate() {
|
|
92
|
-
const r = await find(String(captured))
|
|
93
|
-
return { data: r ? [r] : [], total: r ? 1 : 0 }
|
|
94
|
-
},
|
|
95
|
-
}
|
|
96
|
-
return q
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ── Post → Comment → Reply world ─────────────────────────────────
|
|
100
|
-
|
|
101
|
-
function buildNestedWorld() {
|
|
102
|
-
const replyRows: Row[] = [
|
|
103
|
-
{ id: 'r1', commentId: 'c1', body: 'reply A' },
|
|
104
|
-
{ id: 'r2', commentId: 'c1', body: 'reply B' },
|
|
105
|
-
{ id: 'r3', commentId: 'c2', body: 'reply on other comment' },
|
|
106
|
-
]
|
|
107
|
-
const ReplyModel: ModelLike = {
|
|
108
|
-
async find(id) { return replyRows.find(r => String(r['id']) === String(id)) ?? null },
|
|
109
|
-
async create(data) { const n: Row = { id: `r${replyRows.length + 1}`, ...data }; replyRows.push(n); return n },
|
|
110
|
-
async update(id, data) { const r = replyRows.find(r => String(r['id']) === String(id)); if (r) Object.assign(r, data); return r ?? null },
|
|
111
|
-
async delete(id) { const i = replyRows.findIndex(r => String(r['id']) === String(id)); if (i >= 0) replyRows.splice(i, 1) },
|
|
112
|
-
query() { return new StubQuery(replyRows) },
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const commentRows: Row[] = [
|
|
116
|
-
{ id: 'c1', postId: 'po1', body: 'comment one' },
|
|
117
|
-
{ id: 'c2', postId: 'po1', body: 'comment two' },
|
|
118
|
-
{ id: 'c3', postId: 'po2', body: 'other-post comment' },
|
|
119
|
-
]
|
|
120
|
-
function commentRecord(id: string) {
|
|
121
|
-
const row = commentRows.find(r => String(r['id']) === id)
|
|
122
|
-
if (!row) return undefined
|
|
123
|
-
return {
|
|
124
|
-
...row,
|
|
125
|
-
related(name: string): ModelQuery {
|
|
126
|
-
if (name !== 'replies') return new StubQuery([])
|
|
127
|
-
return new StubQuery(replyRows.filter(r => r['commentId'] === id))
|
|
128
|
-
},
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
const CommentModel: ModelLike = {
|
|
132
|
-
async find(id) { return commentRecord(String(id)) ?? null },
|
|
133
|
-
async create(data) { const n: Row = { id: `c${commentRows.length + 1}`, ...data }; commentRows.push(n); return n },
|
|
134
|
-
async update(id, data) { const r = commentRows.find(r => String(r['id']) === String(id)); if (r) Object.assign(r, data); return r ?? null },
|
|
135
|
-
async delete(id) { const i = commentRows.findIndex(r => String(r['id']) === String(id)); if (i >= 0) commentRows.splice(i, 1) },
|
|
136
|
-
query(): ModelQuery { return findAdapter((this as ModelLike).find as (id: string) => Promise<unknown>) },
|
|
137
|
-
}
|
|
138
|
-
Object.assign(CommentModel as object, {
|
|
139
|
-
relations: { replies: { model: () => ReplyModel, foreignKey: 'commentId' } },
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
const postRows: Row[] = [
|
|
143
|
-
{ id: 'po1', title: 'Post one' },
|
|
144
|
-
{ id: 'po2', title: 'Post two' },
|
|
145
|
-
]
|
|
146
|
-
function postRecord(id: string) {
|
|
147
|
-
const row = postRows.find(r => String(r['id']) === id)
|
|
148
|
-
if (!row) return undefined
|
|
149
|
-
return {
|
|
150
|
-
...row,
|
|
151
|
-
related(name: string): ModelQuery {
|
|
152
|
-
if (name !== 'comments') return new StubQuery([])
|
|
153
|
-
const filtered = commentRows.filter(r => r['postId'] === id)
|
|
154
|
-
return new StubQuery(filtered.map(r => commentRecord(String(r['id'])) as Row))
|
|
155
|
-
},
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
const PostModel: ModelLike = {
|
|
159
|
-
async find(id) { return postRecord(String(id)) ?? null },
|
|
160
|
-
async create() { throw new Error('not used') },
|
|
161
|
-
async update() { throw new Error('not used') },
|
|
162
|
-
async delete() { /* no-op */ },
|
|
163
|
-
query(): ModelQuery { return findAdapter((this as ModelLike).find as (id: string) => Promise<unknown>) },
|
|
164
|
-
}
|
|
165
|
-
Object.assign(PostModel as object, {
|
|
166
|
-
relations: { comments: { model: () => CommentModel, foreignKey: 'postId' } },
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
class ReplyResource extends Resource {
|
|
170
|
-
static override label = 'Replies'
|
|
171
|
-
static override labelSingular = 'Reply'
|
|
172
|
-
static override slug = 'replies'
|
|
173
|
-
static override get model() { return ReplyModel }
|
|
174
|
-
static override form(form: Form): Form { return form.schema([TextField.make('body').required()]) }
|
|
175
|
-
static override detail() { return [Heading.make('Reply view')] }
|
|
176
|
-
}
|
|
177
|
-
class CommentResource extends Resource {
|
|
178
|
-
static override slug = 'comments'
|
|
179
|
-
static override get model() { return CommentModel }
|
|
180
|
-
static override detail() { return [Heading.make('Comment view')] }
|
|
181
|
-
}
|
|
182
|
-
class CommentRepliesManager extends RelationManager {
|
|
183
|
-
static override relationship = 'replies'
|
|
184
|
-
static override label = 'Replies'
|
|
185
|
-
static override table(t: Table): Table { return t.columns([Column.make('body')]) }
|
|
186
|
-
static override form(f: Form): Form { return f.schema([TextField.make('body').required()]) }
|
|
187
|
-
static override detail() { return [Heading.make('Reply detail under comment')] }
|
|
188
|
-
}
|
|
189
|
-
class PostsCommentsManager extends RelationManager {
|
|
190
|
-
static override relationship = 'comments'
|
|
191
|
-
static override label = 'Comments'
|
|
192
|
-
static override table(t: Table): Table { return t.columns([Column.make('body')]) }
|
|
193
|
-
static override form(f: Form): Form { return f.schema([TextField.make('body').required()]) }
|
|
194
|
-
static override relations() { return [CommentRepliesManager] }
|
|
195
|
-
}
|
|
196
|
-
class PostResource extends Resource {
|
|
197
|
-
static override label = 'Posts'
|
|
198
|
-
static override slug = 'posts'
|
|
199
|
-
static override get model() { return PostModel }
|
|
200
|
-
static override relations() { return [PostsCommentsManager] }
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const panel = Pilotiq.make('NRT-' + Math.random().toString(36).slice(2)).path('/admin')
|
|
204
|
-
.resources([PostResource, CommentResource, ReplyResource])
|
|
205
|
-
|
|
206
|
-
return { panel, PostsCommentsManager, CommentRepliesManager, postRows, commentRows, replyRows }
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// ── Registration ─────────────────────────────────────────────────
|
|
210
|
-
|
|
211
|
-
describe('nested relation routes — registration (Phase B)', () => {
|
|
212
|
-
let router: Router
|
|
213
|
-
beforeEach(() => { router = new Router() })
|
|
214
|
-
|
|
215
|
-
it('mounts list / create / view / edit / delete per (Resource, M, N) tuple', () => {
|
|
216
|
-
const { panel } = buildNestedWorld()
|
|
217
|
-
registerPilotiqRoutes(router, panel)
|
|
218
|
-
const paths = router.list().map(r => `${r.method} ${r.path}`)
|
|
219
|
-
|
|
220
|
-
assert.ok(paths.includes('GET /admin/posts/:id/comments/:childId/replies'), 'list')
|
|
221
|
-
assert.ok(paths.includes('GET /admin/posts/:id/comments/:childId/replies/create'), 'create-get')
|
|
222
|
-
assert.ok(paths.includes('POST /admin/posts/:id/comments/:childId/replies/create'), 'create-post')
|
|
223
|
-
assert.ok(paths.includes('GET /admin/posts/:id/comments/:childId/replies/:childId2'), 'view')
|
|
224
|
-
assert.ok(paths.includes('GET /admin/posts/:id/comments/:childId/replies/:childId2/edit'), 'edit-get')
|
|
225
|
-
assert.ok(paths.includes('POST /admin/posts/:id/comments/:childId/replies/:childId2/edit'), 'edit-post')
|
|
226
|
-
assert.ok(paths.includes('POST /admin/posts/:id/comments/:childId/replies/:childId2/delete'), 'delete')
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
it('throws at boot when a nested manager declares its own relations() (depth-3 cap)', () => {
|
|
230
|
-
class DeepM extends RelationManager {
|
|
231
|
-
static override relationship = 'deep'
|
|
232
|
-
}
|
|
233
|
-
class NestedM extends RelationManager {
|
|
234
|
-
static override relationship = 'replies'
|
|
235
|
-
static override relations() { return [DeepM] }
|
|
236
|
-
}
|
|
237
|
-
class ParentM extends RelationManager {
|
|
238
|
-
static override relationship = 'comments'
|
|
239
|
-
static override relations() { return [NestedM] }
|
|
240
|
-
}
|
|
241
|
-
class WithDepth3 extends Resource {
|
|
242
|
-
static override slug = 'posts'
|
|
243
|
-
static override relations() { return [ParentM] }
|
|
244
|
-
}
|
|
245
|
-
const panel = Pilotiq.make('D3-' + Math.random()).path('/admin').resources([WithDepth3])
|
|
246
|
-
assert.throws(
|
|
247
|
-
() => registerPilotiqRoutes(new Router(), panel),
|
|
248
|
-
/Phase B caps nesting at depth 2/,
|
|
249
|
-
)
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
it('throws at boot when a nested manager uses a reserved relationship token', () => {
|
|
253
|
-
class BadNested extends RelationManager {
|
|
254
|
-
static override relationship = '_attach'
|
|
255
|
-
}
|
|
256
|
-
class ParentM extends RelationManager {
|
|
257
|
-
static override relationship = 'comments'
|
|
258
|
-
static override relations() { return [BadNested] }
|
|
259
|
-
}
|
|
260
|
-
class WithBad extends Resource {
|
|
261
|
-
static override slug = 'posts'
|
|
262
|
-
static override relations() { return [ParentM] }
|
|
263
|
-
}
|
|
264
|
-
const panel = Pilotiq.make('NRB-' + Math.random()).path('/admin').resources([WithBad])
|
|
265
|
-
assert.throws(
|
|
266
|
-
() => registerPilotiqRoutes(new Router(), panel),
|
|
267
|
-
/Nested RelationManager .* uses reserved relationship "_attach"/,
|
|
268
|
-
)
|
|
269
|
-
})
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
// ── List handler ─────────────────────────────────────────────────
|
|
273
|
-
|
|
274
|
-
describe('nested relation routes — list (Phase B)', () => {
|
|
275
|
-
let router: Router
|
|
276
|
-
beforeEach(() => { router = new Router() })
|
|
277
|
-
|
|
278
|
-
it('returns nested-relation-list with the right schema + chain rows', async () => {
|
|
279
|
-
const { panel } = buildNestedWorld()
|
|
280
|
-
registerPilotiqRoutes(router, panel)
|
|
281
|
-
const route = router.list().find(r => r.path === '/admin/posts/:id/comments/:childId/replies' && r.method === 'GET')!
|
|
282
|
-
const { result, res } = await callHandler(
|
|
283
|
-
route.handler,
|
|
284
|
-
fakeReq({ params: { id: 'po1', childId: 'c1' } }),
|
|
285
|
-
)
|
|
286
|
-
assert.equal(res.statusCode, 200)
|
|
287
|
-
const view = result as { id: string; props: Record<string, unknown> }
|
|
288
|
-
assert.equal(view.id, 'pilotiq.nested-relation-list')
|
|
289
|
-
const schema = view.props['schemaData'] as Array<Record<string, unknown>>
|
|
290
|
-
const tableMeta = schema.find(s => s['type'] === 'table') as Record<string, unknown>
|
|
291
|
-
const rows = tableMeta['rows'] as Array<Record<string, unknown>>
|
|
292
|
-
assert.deepEqual(rows.map(r => r['id']).sort(), ['r1', 'r2'])
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
it('404s when chain[1] (child1) does NOT belong to chain[0] (post) — IDOR layer 1', async () => {
|
|
296
|
-
const { panel } = buildNestedWorld()
|
|
297
|
-
registerPilotiqRoutes(router, panel)
|
|
298
|
-
const route = router.list().find(r => r.path === '/admin/posts/:id/comments/:childId/replies' && r.method === 'GET')!
|
|
299
|
-
const { res } = await callHandler(
|
|
300
|
-
route.handler,
|
|
301
|
-
fakeReq({ params: { id: 'po1', childId: 'c3' } }), // c3 belongs to po2
|
|
302
|
-
)
|
|
303
|
-
assert.equal(res.statusCode, 404)
|
|
304
|
-
})
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
// ── View + IDOR layer 2 ──────────────────────────────────────────
|
|
308
|
-
|
|
309
|
-
describe('nested relation routes — view (Phase B)', () => {
|
|
310
|
-
let router: Router
|
|
311
|
-
beforeEach(() => { router = new Router() })
|
|
312
|
-
|
|
313
|
-
it('returns nested-relation-view for a leaf record under the correct chain', async () => {
|
|
314
|
-
const { panel } = buildNestedWorld()
|
|
315
|
-
registerPilotiqRoutes(router, panel)
|
|
316
|
-
const route = router.list().find(r => r.path === '/admin/posts/:id/comments/:childId/replies/:childId2' && r.method === 'GET')!
|
|
317
|
-
const { result, res } = await callHandler(
|
|
318
|
-
route.handler,
|
|
319
|
-
fakeReq({ params: { id: 'po1', childId: 'c1', childId2: 'r1' } }),
|
|
320
|
-
)
|
|
321
|
-
assert.equal(res.statusCode, 200)
|
|
322
|
-
const view = result as { id: string; props: Record<string, unknown> }
|
|
323
|
-
assert.equal(view.id, 'pilotiq.nested-relation-view')
|
|
324
|
-
assert.equal(view.props['childId'], 'r1')
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
it('404s when the leaf record is on a different middle-layer parent (IDOR layer 2)', async () => {
|
|
328
|
-
const { panel } = buildNestedWorld()
|
|
329
|
-
registerPilotiqRoutes(router, panel)
|
|
330
|
-
const route = router.list().find(r => r.path === '/admin/posts/:id/comments/:childId/replies/:childId2' && r.method === 'GET')!
|
|
331
|
-
const { res } = await callHandler(
|
|
332
|
-
route.handler,
|
|
333
|
-
// r3 belongs to c2, not c1 — chain integrity must reject.
|
|
334
|
-
fakeReq({ params: { id: 'po1', childId: 'c1', childId2: 'r3' } }),
|
|
335
|
-
)
|
|
336
|
-
assert.equal(res.statusCode, 404)
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
it('404s when leaf childId is the literal "create" reserved token', async () => {
|
|
340
|
-
const { panel } = buildNestedWorld()
|
|
341
|
-
registerPilotiqRoutes(router, panel)
|
|
342
|
-
const route = router.list().find(r => r.path === '/admin/posts/:id/comments/:childId/replies/:childId2' && r.method === 'GET')!
|
|
343
|
-
const { res } = await callHandler(
|
|
344
|
-
route.handler,
|
|
345
|
-
fakeReq({ params: { id: 'po1', childId: 'c1', childId2: 'create' } }),
|
|
346
|
-
)
|
|
347
|
-
assert.equal(res.statusCode, 404)
|
|
348
|
-
})
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
// ── Mutations ────────────────────────────────────────────────────
|
|
352
|
-
|
|
353
|
-
describe('nested relation routes — mutations (Phase B)', () => {
|
|
354
|
-
let router: Router
|
|
355
|
-
beforeEach(() => { router = new Router() })
|
|
356
|
-
|
|
357
|
-
it('create POST persists a new leaf row pinned under the correct chain[1] parent', async () => {
|
|
358
|
-
const { panel, replyRows } = buildNestedWorld()
|
|
359
|
-
registerPilotiqRoutes(router, panel)
|
|
360
|
-
const route = router.list().find(r => r.path === '/admin/posts/:id/comments/:childId/replies/create' && r.method === 'POST')!
|
|
361
|
-
const { res } = await callHandler(
|
|
362
|
-
route.handler,
|
|
363
|
-
fakeReq({ params: { id: 'po1', childId: 'c1' }, body: { body: 'Fresh reply' } }),
|
|
364
|
-
)
|
|
365
|
-
assert.equal(res.redirectedTo?.url, '/admin/posts/po1/comments/c1/replies')
|
|
366
|
-
assert.equal(res.redirectedTo?.code, 303)
|
|
367
|
-
assert.ok(replyRows.some(r => r['body'] === 'Fresh reply'))
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
it('edit POST updates a leaf row when the chain checks out', async () => {
|
|
371
|
-
const { panel, replyRows } = buildNestedWorld()
|
|
372
|
-
registerPilotiqRoutes(router, panel)
|
|
373
|
-
const route = router.list().find(r => r.path === '/admin/posts/:id/comments/:childId/replies/:childId2/edit' && r.method === 'POST')!
|
|
374
|
-
const { res } = await callHandler(
|
|
375
|
-
route.handler,
|
|
376
|
-
fakeReq({ params: { id: 'po1', childId: 'c1', childId2: 'r1' }, body: { body: 'Renamed' } }),
|
|
377
|
-
)
|
|
378
|
-
assert.equal(res.redirectedTo?.code, 303)
|
|
379
|
-
assert.equal(replyRows.find(r => r['id'] === 'r1')!['body'], 'Renamed')
|
|
380
|
-
})
|
|
381
|
-
|
|
382
|
-
it('edit POST 404s when the leaf belongs to a different middle parent', async () => {
|
|
383
|
-
const { panel } = buildNestedWorld()
|
|
384
|
-
registerPilotiqRoutes(router, panel)
|
|
385
|
-
const route = router.list().find(r => r.path === '/admin/posts/:id/comments/:childId/replies/:childId2/edit' && r.method === 'POST')!
|
|
386
|
-
const { res } = await callHandler(
|
|
387
|
-
route.handler,
|
|
388
|
-
fakeReq({ params: { id: 'po1', childId: 'c1', childId2: 'r3' }, body: { body: 'Hacked' } }),
|
|
389
|
-
)
|
|
390
|
-
assert.equal(res.statusCode, 404)
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
it('delete POST removes the leaf record', async () => {
|
|
394
|
-
const { panel, replyRows } = buildNestedWorld()
|
|
395
|
-
registerPilotiqRoutes(router, panel)
|
|
396
|
-
const route = router.list().find(r => r.path === '/admin/posts/:id/comments/:childId/replies/:childId2/delete' && r.method === 'POST')!
|
|
397
|
-
const { res } = await callHandler(
|
|
398
|
-
route.handler,
|
|
399
|
-
fakeReq({ params: { id: 'po1', childId: 'c1', childId2: 'r1' } }),
|
|
400
|
-
)
|
|
401
|
-
assert.equal(res.redirectedTo?.code, 303)
|
|
402
|
-
assert.ok(!replyRows.some(r => r['id'] === 'r1'))
|
|
403
|
-
})
|
|
404
|
-
})
|
|
405
|
-
|
|
406
|
-
// ── Phase B follow-up: nested action / detach / soft-delete ─────────
|
|
407
|
-
|
|
408
|
-
import { Action } from './actions/Action.js'
|
|
409
|
-
|
|
410
|
-
describe('nested relation routes — _action + _detach registration', () => {
|
|
411
|
-
let router: Router
|
|
412
|
-
beforeEach(() => { router = new Router() })
|
|
413
|
-
|
|
414
|
-
it('mounts action + detach routes per (R, M, N) tuple unconditionally', () => {
|
|
415
|
-
const { panel } = buildNestedWorld()
|
|
416
|
-
registerPilotiqRoutes(router, panel)
|
|
417
|
-
const paths = router.list().map(r => `${r.method} ${r.path}`)
|
|
418
|
-
assert.ok(
|
|
419
|
-
paths.includes('POST /admin/posts/:id/comments/:childId/replies/_action/:actionName'),
|
|
420
|
-
'nested _action route should mount even on hasMany managers',
|
|
421
|
-
)
|
|
422
|
-
assert.ok(
|
|
423
|
-
paths.includes('POST /admin/posts/:id/comments/:childId/replies/:childId2/_detach'),
|
|
424
|
-
'nested _detach route should mount unconditionally',
|
|
425
|
-
)
|
|
426
|
-
})
|
|
427
|
-
|
|
428
|
-
it('does NOT mount restore / force-delete on a non-soft-delete world', () => {
|
|
429
|
-
const { panel } = buildNestedWorld()
|
|
430
|
-
registerPilotiqRoutes(router, panel)
|
|
431
|
-
const paths = router.list().map(r => `${r.method} ${r.path}`)
|
|
432
|
-
assert.ok(!paths.includes('POST /admin/posts/:id/comments/:childId/replies/:childId2/restore'))
|
|
433
|
-
assert.ok(!paths.includes('POST /admin/posts/:id/comments/:childId/replies/:childId2/force-delete'))
|
|
434
|
-
})
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
describe('nested relation routes — _action behavior', () => {
|
|
438
|
-
let router: Router
|
|
439
|
-
beforeEach(() => { router = new Router() })
|
|
440
|
-
|
|
441
|
-
it('dispatches a nested handler-style action with ctx.relation stamped', async () => {
|
|
442
|
-
const { panel } = buildNestedWorld()
|
|
443
|
-
// Splice a handler action into N's table so we have something to fire.
|
|
444
|
-
const M = panel.getConfig().resources[0]!.relations()[0]!
|
|
445
|
-
const N = M.relations()[0]!
|
|
446
|
-
let dispatched: Partial<{ parentId: string; relationship: string }> = {}
|
|
447
|
-
const originalTable = N.table.bind(N)
|
|
448
|
-
;(N as unknown as { table: typeof N.table }).table = (t, ctx) => {
|
|
449
|
-
const out = originalTable(t, ctx) as Table
|
|
450
|
-
out.actions([
|
|
451
|
-
Action.make('ping')
|
|
452
|
-
.label('Ping')
|
|
453
|
-
.handler(async (input) => {
|
|
454
|
-
const rel = (input as { relation?: { parentId: string; relationship: string } }).relation
|
|
455
|
-
if (rel) dispatched = { parentId: rel.parentId, relationship: rel.relationship }
|
|
456
|
-
}),
|
|
457
|
-
])
|
|
458
|
-
return out
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
registerPilotiqRoutes(router, panel)
|
|
462
|
-
const route = router.list().find(r =>
|
|
463
|
-
r.path === '/admin/posts/:id/comments/:childId/replies/_action/:actionName'
|
|
464
|
-
&& r.method === 'POST'
|
|
465
|
-
)!
|
|
466
|
-
const { res } = await callHandler(
|
|
467
|
-
route.handler,
|
|
468
|
-
fakeReq({ params: { id: 'po1', childId: 'c1', actionName: 'ping' } }),
|
|
469
|
-
)
|
|
470
|
-
assert.equal(res.redirectedTo?.code, 303)
|
|
471
|
-
assert.equal(dispatched.parentId, 'c1') // immediate parent of N
|
|
472
|
-
assert.equal(dispatched.relationship, 'replies') // nested rel key
|
|
473
|
-
})
|
|
474
|
-
|
|
475
|
-
it('404s when the action name is unknown', async () => {
|
|
476
|
-
const { panel } = buildNestedWorld()
|
|
477
|
-
registerPilotiqRoutes(router, panel)
|
|
478
|
-
const route = router.list().find(r =>
|
|
479
|
-
r.path === '/admin/posts/:id/comments/:childId/replies/_action/:actionName'
|
|
480
|
-
&& r.method === 'POST'
|
|
481
|
-
)!
|
|
482
|
-
const { res } = await callHandler(
|
|
483
|
-
route.handler,
|
|
484
|
-
fakeReq({ params: { id: 'po1', childId: 'c1', actionName: 'nope' } }),
|
|
485
|
-
)
|
|
486
|
-
assert.equal(res.statusCode, 404)
|
|
487
|
-
})
|
|
488
|
-
})
|
|
489
|
-
|
|
490
|
-
describe('nested relation routes — _detach behavior', () => {
|
|
491
|
-
let router: Router
|
|
492
|
-
beforeEach(() => { router = new Router() })
|
|
493
|
-
|
|
494
|
-
it('404s on non-M2M nested managers (hasMany)', async () => {
|
|
495
|
-
const { panel } = buildNestedWorld()
|
|
496
|
-
registerPilotiqRoutes(router, panel)
|
|
497
|
-
const route = router.list().find(r =>
|
|
498
|
-
r.path === '/admin/posts/:id/comments/:childId/replies/:childId2/_detach'
|
|
499
|
-
&& r.method === 'POST'
|
|
500
|
-
)!
|
|
501
|
-
const { res } = await callHandler(
|
|
502
|
-
route.handler,
|
|
503
|
-
fakeReq({ params: { id: 'po1', childId: 'c1', childId2: 'r1' } }),
|
|
504
|
-
)
|
|
505
|
-
assert.equal(res.statusCode, 404)
|
|
506
|
-
assert.match(String(res.sentBody), /belongsToMany/)
|
|
507
|
-
})
|
|
508
|
-
})
|
|
509
|
-
|
|
510
|
-
// ── Soft-delete world: Comment → Replies, Reply has softDeletes ─────
|
|
511
|
-
|
|
512
|
-
function buildNestedSoftDeleteWorld() {
|
|
513
|
-
const replyRows: Array<Row & { deletedAt?: string | null }> = [
|
|
514
|
-
{ id: 'r1', commentId: 'c1', body: 'reply A', deletedAt: null },
|
|
515
|
-
{ id: 'r2', commentId: 'c1', body: 'reply B (trashed)', deletedAt: '2026-01-01' },
|
|
516
|
-
]
|
|
517
|
-
let restored: string | null = null
|
|
518
|
-
let forced: string | null = null
|
|
519
|
-
const ReplyModel: ModelLike & { softDeletes?: boolean } = {
|
|
520
|
-
softDeletes: true,
|
|
521
|
-
async find(id) { return replyRows.find(r => String(r['id']) === String(id)) ?? null },
|
|
522
|
-
async create() { throw new Error('not used') },
|
|
523
|
-
async update() { throw new Error('not used') },
|
|
524
|
-
async delete() { /* no-op */ },
|
|
525
|
-
async restore(id: string) { restored = id; const r = replyRows.find(r => String(r['id']) === id); if (r) r.deletedAt = null },
|
|
526
|
-
async forceDelete(id: string) { forced = id; const i = replyRows.findIndex(r => String(r['id']) === id); if (i >= 0) replyRows.splice(i, 1) },
|
|
527
|
-
query() { return new StubQuery(replyRows) },
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
const commentRows: Row[] = [{ id: 'c1', postId: 'po1', body: 'comment one' }]
|
|
531
|
-
function commentRecord(id: string) {
|
|
532
|
-
const row = commentRows.find(r => String(r['id']) === id)
|
|
533
|
-
if (!row) return undefined
|
|
534
|
-
return {
|
|
535
|
-
...row,
|
|
536
|
-
related(name: string): ModelQuery & { withTrashed?: () => ModelQuery } {
|
|
537
|
-
if (name !== 'replies') return new StubQuery([]) as ModelQuery
|
|
538
|
-
// Default scope hides trashed; withTrashed includes them.
|
|
539
|
-
const visible = replyRows.filter(r => r.deletedAt == null)
|
|
540
|
-
const all = replyRows.slice()
|
|
541
|
-
const q = new StubQuery(visible) as ModelQuery & { withTrashed?: () => ModelQuery }
|
|
542
|
-
q.withTrashed = () => new StubQuery(all) as ModelQuery
|
|
543
|
-
return q
|
|
544
|
-
},
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
const CommentModel: ModelLike = {
|
|
548
|
-
async find(id) { return commentRecord(String(id)) ?? null },
|
|
549
|
-
async create() { throw new Error('not used') },
|
|
550
|
-
async update() { throw new Error('not used') },
|
|
551
|
-
async delete() { /* no-op */ },
|
|
552
|
-
query(): ModelQuery { return findAdapter((this as ModelLike).find as (id: string) => Promise<unknown>) },
|
|
553
|
-
}
|
|
554
|
-
Object.assign(CommentModel as object, {
|
|
555
|
-
relations: { replies: { model: () => ReplyModel, foreignKey: 'commentId' } },
|
|
556
|
-
})
|
|
557
|
-
|
|
558
|
-
const postRows: Row[] = [{ id: 'po1', title: 'Post one' }]
|
|
559
|
-
function postRecord(id: string) {
|
|
560
|
-
const row = postRows.find(r => String(r['id']) === id)
|
|
561
|
-
if (!row) return undefined
|
|
562
|
-
return {
|
|
563
|
-
...row,
|
|
564
|
-
related(name: string): ModelQuery {
|
|
565
|
-
if (name !== 'comments') return new StubQuery([])
|
|
566
|
-
return new StubQuery(commentRows.filter(r => r['postId'] === id).map(r => commentRecord(String(r['id'])) as Row))
|
|
567
|
-
},
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
const PostModel: ModelLike = {
|
|
571
|
-
async find(id) { return postRecord(String(id)) ?? null },
|
|
572
|
-
async create() { throw new Error('not used') },
|
|
573
|
-
async update() { throw new Error('not used') },
|
|
574
|
-
async delete() { /* no-op */ },
|
|
575
|
-
query(): ModelQuery { return findAdapter((this as ModelLike).find as (id: string) => Promise<unknown>) },
|
|
576
|
-
}
|
|
577
|
-
Object.assign(PostModel as object, {
|
|
578
|
-
relations: { comments: { model: () => CommentModel, foreignKey: 'postId' } },
|
|
579
|
-
})
|
|
580
|
-
|
|
581
|
-
class ReplyResource extends Resource {
|
|
582
|
-
static override label = 'Replies'
|
|
583
|
-
static override labelSingular = 'Reply'
|
|
584
|
-
static override slug = 'replies'
|
|
585
|
-
static override softDeletes = true
|
|
586
|
-
static override get model() { return ReplyModel }
|
|
587
|
-
static override form(form: Form): Form { return form.schema([TextField.make('body').required()]) }
|
|
588
|
-
}
|
|
589
|
-
class CommentResource extends Resource {
|
|
590
|
-
static override slug = 'comments'
|
|
591
|
-
static override get model() { return CommentModel }
|
|
592
|
-
}
|
|
593
|
-
class CommentRepliesManager extends RelationManager {
|
|
594
|
-
static override relationship = 'replies'
|
|
595
|
-
static override label = 'Replies'
|
|
596
|
-
static override table(t: Table): Table { return t.columns([Column.make('body')]) }
|
|
597
|
-
static override form(f: Form): Form { return f.schema([TextField.make('body').required()]) }
|
|
598
|
-
}
|
|
599
|
-
class PostsCommentsManager extends RelationManager {
|
|
600
|
-
static override relationship = 'comments'
|
|
601
|
-
static override label = 'Comments'
|
|
602
|
-
static override table(t: Table): Table { return t.columns([Column.make('body')]) }
|
|
603
|
-
static override form(f: Form): Form { return f.schema([TextField.make('body').required()]) }
|
|
604
|
-
static override relations() { return [CommentRepliesManager] }
|
|
605
|
-
}
|
|
606
|
-
class PostResource extends Resource {
|
|
607
|
-
static override label = 'Posts'
|
|
608
|
-
static override slug = 'posts'
|
|
609
|
-
static override get model() { return PostModel }
|
|
610
|
-
static override relations() { return [PostsCommentsManager] }
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const panel = Pilotiq.make('NRSD-' + Math.random().toString(36).slice(2)).path('/admin')
|
|
614
|
-
.resources([PostResource, CommentResource, ReplyResource])
|
|
615
|
-
|
|
616
|
-
return { panel, restore: () => restored, force: () => forced, replyRows }
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
describe('nested relation routes — soft-delete (restore + force-delete)', () => {
|
|
620
|
-
let router: Router
|
|
621
|
-
beforeEach(() => { router = new Router() })
|
|
622
|
-
|
|
623
|
-
it('mounts restore + force-delete routes when Related2 has softDeletes', () => {
|
|
624
|
-
const { panel } = buildNestedSoftDeleteWorld()
|
|
625
|
-
registerPilotiqRoutes(router, panel)
|
|
626
|
-
const paths = router.list().map(r => `${r.method} ${r.path}`)
|
|
627
|
-
assert.ok(paths.includes('POST /admin/posts/:id/comments/:childId/replies/:childId2/restore'))
|
|
628
|
-
assert.ok(paths.includes('POST /admin/posts/:id/comments/:childId/replies/:childId2/force-delete'))
|
|
629
|
-
})
|
|
630
|
-
|
|
631
|
-
it('restore POST calls model.restore on a trashed grandchild', async () => {
|
|
632
|
-
const world = buildNestedSoftDeleteWorld()
|
|
633
|
-
registerPilotiqRoutes(router, world.panel)
|
|
634
|
-
const route = router.list().find(r =>
|
|
635
|
-
r.path === '/admin/posts/:id/comments/:childId/replies/:childId2/restore'
|
|
636
|
-
&& r.method === 'POST'
|
|
637
|
-
)!
|
|
638
|
-
const { res } = await callHandler(
|
|
639
|
-
route.handler,
|
|
640
|
-
fakeReq({ params: { id: 'po1', childId: 'c1', childId2: 'r2' } }),
|
|
641
|
-
)
|
|
642
|
-
assert.equal(res.redirectedTo?.code, 303)
|
|
643
|
-
assert.equal(world.restore(), 'r2')
|
|
644
|
-
})
|
|
645
|
-
|
|
646
|
-
it('force-delete POST removes the grandchild permanently', async () => {
|
|
647
|
-
const world = buildNestedSoftDeleteWorld()
|
|
648
|
-
registerPilotiqRoutes(router, world.panel)
|
|
649
|
-
const route = router.list().find(r =>
|
|
650
|
-
r.path === '/admin/posts/:id/comments/:childId/replies/:childId2/force-delete'
|
|
651
|
-
&& r.method === 'POST'
|
|
652
|
-
)!
|
|
653
|
-
const { res } = await callHandler(
|
|
654
|
-
route.handler,
|
|
655
|
-
fakeReq({ params: { id: 'po1', childId: 'c1', childId2: 'r2' } }),
|
|
656
|
-
)
|
|
657
|
-
assert.equal(res.redirectedTo?.code, 303)
|
|
658
|
-
assert.equal(world.force(), 'r2')
|
|
659
|
-
assert.ok(!world.replyRows.some(r => r['id'] === 'r2'))
|
|
660
|
-
})
|
|
661
|
-
|
|
662
|
-
it('restore 404s when the grandchild does not belong to the chain[1] parent', async () => {
|
|
663
|
-
const world = buildNestedSoftDeleteWorld()
|
|
664
|
-
registerPilotiqRoutes(router, world.panel)
|
|
665
|
-
const route = router.list().find(r =>
|
|
666
|
-
r.path === '/admin/posts/:id/comments/:childId/replies/:childId2/restore'
|
|
667
|
-
&& r.method === 'POST'
|
|
668
|
-
)!
|
|
669
|
-
const { res } = await callHandler(
|
|
670
|
-
route.handler,
|
|
671
|
-
fakeReq({ params: { id: 'po1', childId: 'c1', childId2: 'nope' } }),
|
|
672
|
-
)
|
|
673
|
-
assert.equal(res.statusCode, 404)
|
|
674
|
-
assert.equal(world.restore(), null)
|
|
675
|
-
})
|
|
676
|
-
})
|