@pilotiq/pilotiq 0.24.1 → 0.24.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/boost/guidelines.md +571 -0
- package/boost/skills/pilotiq-actions/SKILL.md +49 -0
- package/boost/skills/pilotiq-actions/rules/dispatch-modes.md +177 -0
- package/boost/skills/pilotiq-actions/rules/factories.md +130 -0
- package/boost/skills/pilotiq-actions/rules/visibility-and-authorization.md +125 -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/Pilotiq.d.ts +31 -0
- package/dist/Pilotiq.d.ts.map +1 -1
- package/dist/Pilotiq.js +3 -1
- package/dist/Pilotiq.js.map +1 -1
- package/dist/PilotiqRegistry.d.ts +13 -0
- package/dist/PilotiqRegistry.d.ts.map +1 -1
- package/dist/PilotiqRegistry.js +15 -0
- package/dist/PilotiqRegistry.js.map +1 -1
- package/dist/pageData/misc.d.ts.map +1 -1
- package/dist/pageData/misc.js +6 -0
- package/dist/pageData/misc.js.map +1 -1
- package/dist/pageData/navigation.d.ts +1 -0
- package/dist/pageData/navigation.d.ts.map +1 -1
- package/dist/pageData/navigation.js +3 -0
- package/dist/pageData/navigation.js.map +1 -1
- package/dist/pageData/relationPages.d.ts.map +1 -1
- package/dist/pageData/relationPages.js +3 -0
- package/dist/pageData/relationPages.js.map +1 -1
- package/dist/pageData/resourcePages.d.ts.map +1 -1
- package/dist/pageData/resourcePages.js +8 -0
- package/dist/pageData/resourcePages.js.map +1 -1
- package/dist/react/AppShell.d.ts +8 -0
- package/dist/react/AppShell.d.ts.map +1 -1
- package/dist/react/AppShell.js.map +1 -1
- package/dist/react/layouts/SidebarLayout.d.ts.map +1 -1
- package/dist/react/layouts/SidebarLayout.js +10 -2
- package/dist/react/layouts/SidebarLayout.js.map +1 -1
- package/dist/react/widgets/StatsOverviewRenderer.d.ts.map +1 -1
- package/dist/react/widgets/StatsOverviewRenderer.js +32 -18
- package/dist/react/widgets/StatsOverviewRenderer.js.map +1 -1
- package/dist/routes/relations.d.ts.map +1 -1
- package/dist/routes/relations.js +25 -18
- package/dist/routes/relations.js.map +1 -1
- package/dist/routes/resources.js.map +1 -1
- package/package.json +10 -5
- 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
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the notification-action dispatcher. Exercises the auth /
|
|
3
|
-
* lookup chain end-to-end against a fake orm adapter — the route
|
|
4
|
-
* layer that mounts this is exercised separately via integration
|
|
5
|
-
* tests against the rudder router.
|
|
6
|
-
*/
|
|
7
|
-
import { describe, it, beforeEach, afterEach } from 'node:test'
|
|
8
|
-
import assert from 'node:assert/strict'
|
|
9
|
-
|
|
10
|
-
import { Pilotiq } from '../Pilotiq.js'
|
|
11
|
-
import { Notification } from './Notification.js'
|
|
12
|
-
import { Action } from '../actions/Action.js'
|
|
13
|
-
import { _setTestAdapter, persist } from './database.js'
|
|
14
|
-
import { dispatchNotificationAction } from './dispatchNotificationAction.js'
|
|
15
|
-
|
|
16
|
-
// ─── Fake ORM adapter (mirror of database.test.ts) ─────────
|
|
17
|
-
|
|
18
|
-
interface Row { [k: string]: unknown }
|
|
19
|
-
interface FakeStore { rows: Row[] }
|
|
20
|
-
|
|
21
|
-
function makeFakeAdapter() {
|
|
22
|
-
const store: FakeStore = { rows: [] }
|
|
23
|
-
const buildQB = (_table: string) => {
|
|
24
|
-
const filters: Array<(r: Row) => boolean> = []
|
|
25
|
-
let order: { column: string; dir: 'ASC' | 'DESC' } | null = null
|
|
26
|
-
const apply = () => {
|
|
27
|
-
let out = store.rows.slice()
|
|
28
|
-
for (const f of filters) out = out.filter(f)
|
|
29
|
-
if (order) {
|
|
30
|
-
const { column, dir } = order
|
|
31
|
-
out.sort((a, b) => {
|
|
32
|
-
const av = (a[column] ?? '') as string
|
|
33
|
-
const bv = (b[column] ?? '') as string
|
|
34
|
-
const cmp = av < bv ? -1 : av > bv ? 1 : 0
|
|
35
|
-
return dir === 'DESC' ? -cmp : cmp
|
|
36
|
-
})
|
|
37
|
-
}
|
|
38
|
-
return out
|
|
39
|
-
}
|
|
40
|
-
const qb: any = {
|
|
41
|
-
where: (col: string, value: unknown) => {
|
|
42
|
-
filters.push(r => r[col] === value)
|
|
43
|
-
return qb
|
|
44
|
-
},
|
|
45
|
-
orderBy: (col: string, dir: 'ASC' | 'DESC' = 'ASC') => {
|
|
46
|
-
order = { column: col, dir }
|
|
47
|
-
return qb
|
|
48
|
-
},
|
|
49
|
-
paginate: async (_p: number, perPage = 25) => {
|
|
50
|
-
const out = apply()
|
|
51
|
-
return { data: out.slice(0, perPage), total: out.length }
|
|
52
|
-
},
|
|
53
|
-
count: async () => apply().length,
|
|
54
|
-
updateAll: async (data: Row) => {
|
|
55
|
-
const matching = apply()
|
|
56
|
-
for (const r of matching) for (const k of Object.keys(data)) r[k] = data[k]
|
|
57
|
-
return matching.length
|
|
58
|
-
},
|
|
59
|
-
create: async (data: Row) => {
|
|
60
|
-
store.rows.push({ ...data })
|
|
61
|
-
return { ...data }
|
|
62
|
-
},
|
|
63
|
-
}
|
|
64
|
-
return qb
|
|
65
|
-
}
|
|
66
|
-
return { adapter: { query: <T>(t: string) => buildQB(t) }, store }
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
let fake = makeFakeAdapter()
|
|
70
|
-
|
|
71
|
-
beforeEach(() => {
|
|
72
|
-
fake = makeFakeAdapter()
|
|
73
|
-
_setTestAdapter(fake.adapter)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
afterEach(() => {
|
|
77
|
-
_setTestAdapter(undefined)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
async function seedRowWithActions(opts: {
|
|
81
|
-
notifiableId: string
|
|
82
|
-
actions: Action[]
|
|
83
|
-
}): Promise<string> {
|
|
84
|
-
const n = Notification.make('Test').actions(opts.actions)
|
|
85
|
-
const data = n.toDatabase() as Parameters<typeof persist>[0]['data']
|
|
86
|
-
const { id } = await persist({
|
|
87
|
-
notifiableType: 'users',
|
|
88
|
-
notifiableId: opts.notifiableId,
|
|
89
|
-
data,
|
|
90
|
-
})
|
|
91
|
-
return id
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
describe('dispatchNotificationAction — auth / lookup chain', () => {
|
|
95
|
-
it('401 when no user resolves', async () => {
|
|
96
|
-
const result = await dispatchNotificationAction(Pilotiq.make('admin'), {
|
|
97
|
-
notificationId: 'whatever',
|
|
98
|
-
actionName: 'view',
|
|
99
|
-
notifiableType: 'users',
|
|
100
|
-
notifiableId: '1',
|
|
101
|
-
user: null,
|
|
102
|
-
})
|
|
103
|
-
assert.equal(result.ok, false)
|
|
104
|
-
assert.equal(result.ok === false && result.status, 401)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('404 when notification id does not exist', async () => {
|
|
108
|
-
const result = await dispatchNotificationAction(Pilotiq.make('admin'), {
|
|
109
|
-
notificationId: 'no-such-row',
|
|
110
|
-
actionName: 'view',
|
|
111
|
-
notifiableType: 'users',
|
|
112
|
-
notifiableId: '1',
|
|
113
|
-
user: { id: 1 },
|
|
114
|
-
})
|
|
115
|
-
assert.equal(result.ok, false)
|
|
116
|
-
assert.equal(result.ok === false && result.status, 404)
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('404 when the action name is not on the row', async () => {
|
|
120
|
-
const id = await seedRowWithActions({
|
|
121
|
-
notifiableId: '1',
|
|
122
|
-
actions: [Action.make('view').url('/p/123')],
|
|
123
|
-
})
|
|
124
|
-
const result = await dispatchNotificationAction(Pilotiq.make('admin'), {
|
|
125
|
-
notificationId: id,
|
|
126
|
-
actionName: 'nope',
|
|
127
|
-
notifiableType: 'users',
|
|
128
|
-
notifiableId: '1',
|
|
129
|
-
user: { id: 1 },
|
|
130
|
-
})
|
|
131
|
-
assert.equal(result.ok, false)
|
|
132
|
-
assert.equal(result.ok === false && result.status, 404)
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('404 when the matched action is not handler-mode', async () => {
|
|
136
|
-
// url-mode actions don't dispatch through this route — clients
|
|
137
|
-
// navigate the href directly. A request hitting the dispatch
|
|
138
|
-
// endpoint for a url-mode action is malformed; 404 closes the door.
|
|
139
|
-
const id = await seedRowWithActions({
|
|
140
|
-
notifiableId: '1',
|
|
141
|
-
actions: [Action.make('view').url('/p/123')],
|
|
142
|
-
})
|
|
143
|
-
const result = await dispatchNotificationAction(Pilotiq.make('admin'), {
|
|
144
|
-
notificationId: id,
|
|
145
|
-
actionName: 'view',
|
|
146
|
-
notifiableType: 'users',
|
|
147
|
-
notifiableId: '1',
|
|
148
|
-
user: { id: 1 },
|
|
149
|
-
})
|
|
150
|
-
assert.equal(result.ok, false)
|
|
151
|
-
assert.equal(result.ok === false && result.status, 404)
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
it('404 when the registry has no handler for the stored name', async () => {
|
|
155
|
-
const id = await seedRowWithActions({
|
|
156
|
-
notifiableId: '1',
|
|
157
|
-
actions: [Action.make('archive').handler('archive-project').payload({ projectId: 1 })],
|
|
158
|
-
})
|
|
159
|
-
const panel = Pilotiq.make('admin') // no notificationHandlers registered
|
|
160
|
-
const result = await dispatchNotificationAction(panel, {
|
|
161
|
-
notificationId: id,
|
|
162
|
-
actionName: 'archive',
|
|
163
|
-
notifiableType: 'users',
|
|
164
|
-
notifiableId: '1',
|
|
165
|
-
user: { id: 1 },
|
|
166
|
-
})
|
|
167
|
-
assert.equal(result.ok, false)
|
|
168
|
-
assert.equal(result.ok === false && result.status, 404)
|
|
169
|
-
})
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
describe('dispatchNotificationAction — handler dispatch', () => {
|
|
173
|
-
it('runs the registered handler with the stored payload', async () => {
|
|
174
|
-
const id = await seedRowWithActions({
|
|
175
|
-
notifiableId: '1',
|
|
176
|
-
actions: [Action.make('archive').handler('archive-project').payload({ projectId: 42 })],
|
|
177
|
-
})
|
|
178
|
-
let seen: unknown = null
|
|
179
|
-
const panel = Pilotiq.make('admin').notificationHandlers({
|
|
180
|
-
'archive-project': async (ctx) => {
|
|
181
|
-
seen = { user: ctx.user, payload: ctx.payload, notificationId: ctx.notificationId }
|
|
182
|
-
return { notify: { id: 'x', type: 'success', title: 'Archived' } }
|
|
183
|
-
},
|
|
184
|
-
})
|
|
185
|
-
const result = await dispatchNotificationAction(panel, {
|
|
186
|
-
notificationId: id,
|
|
187
|
-
actionName: 'archive',
|
|
188
|
-
notifiableType: 'users',
|
|
189
|
-
notifiableId: '1',
|
|
190
|
-
user: { id: 1 },
|
|
191
|
-
})
|
|
192
|
-
assert.equal(result.ok, true)
|
|
193
|
-
assert.deepEqual(seen, {
|
|
194
|
-
user: { id: 1 },
|
|
195
|
-
payload: { projectId: 42 },
|
|
196
|
-
notificationId: id,
|
|
197
|
-
})
|
|
198
|
-
assert.equal(result.ok === true && result.notifications?.[0]?.title, 'Archived')
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
it('flips read_at when the stored action carries markAsRead', async () => {
|
|
202
|
-
const id = await seedRowWithActions({
|
|
203
|
-
notifiableId: '1',
|
|
204
|
-
actions: [Action.make('archive').handler('archive-project').markAsRead()],
|
|
205
|
-
})
|
|
206
|
-
const panel = Pilotiq.make('admin').notificationHandlers({
|
|
207
|
-
'archive-project': async () => undefined,
|
|
208
|
-
})
|
|
209
|
-
// Pre-condition: row is unread.
|
|
210
|
-
assert.equal(fake.store.rows[0]?.['read_at'], null)
|
|
211
|
-
|
|
212
|
-
const result = await dispatchNotificationAction(panel, {
|
|
213
|
-
notificationId: id,
|
|
214
|
-
actionName: 'archive',
|
|
215
|
-
notifiableType: 'users',
|
|
216
|
-
notifiableId: '1',
|
|
217
|
-
user: { id: 1 },
|
|
218
|
-
})
|
|
219
|
-
assert.equal(result.ok, true)
|
|
220
|
-
assert.equal(result.ok === true && result.markedAsRead, true)
|
|
221
|
-
// Row was actually flipped — defensive in depth.
|
|
222
|
-
assert.notEqual(fake.store.rows[0]?.['read_at'], null)
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
it('does not flip read_at when the stored action lacks markAsRead', async () => {
|
|
226
|
-
const id = await seedRowWithActions({
|
|
227
|
-
notifiableId: '1',
|
|
228
|
-
actions: [Action.make('archive').handler('archive-project')],
|
|
229
|
-
})
|
|
230
|
-
const panel = Pilotiq.make('admin').notificationHandlers({
|
|
231
|
-
'archive-project': async () => undefined,
|
|
232
|
-
})
|
|
233
|
-
const result = await dispatchNotificationAction(panel, {
|
|
234
|
-
notificationId: id,
|
|
235
|
-
actionName: 'archive',
|
|
236
|
-
notifiableType: 'users',
|
|
237
|
-
notifiableId: '1',
|
|
238
|
-
user: { id: 1 },
|
|
239
|
-
})
|
|
240
|
-
assert.equal(result.ok, true)
|
|
241
|
-
assert.equal(result.ok === true && result.markedAsRead, undefined)
|
|
242
|
-
assert.equal(fake.store.rows[0]?.['read_at'], null)
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
it('forwards a redirect from the handler', async () => {
|
|
246
|
-
const id = await seedRowWithActions({
|
|
247
|
-
notifiableId: '1',
|
|
248
|
-
actions: [Action.make('go').handler('go')],
|
|
249
|
-
})
|
|
250
|
-
const panel = Pilotiq.make('admin').notificationHandlers({
|
|
251
|
-
go: async () => ({ redirect: '/somewhere' }),
|
|
252
|
-
})
|
|
253
|
-
const result = await dispatchNotificationAction(panel, {
|
|
254
|
-
notificationId: id,
|
|
255
|
-
actionName: 'go',
|
|
256
|
-
notifiableType: 'users',
|
|
257
|
-
notifiableId: '1',
|
|
258
|
-
user: { id: 1 },
|
|
259
|
-
})
|
|
260
|
-
assert.equal(result.ok === true && result.redirect, '/somewhere')
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
it('500 when the handler throws', async () => {
|
|
264
|
-
const id = await seedRowWithActions({
|
|
265
|
-
notifiableId: '1',
|
|
266
|
-
actions: [Action.make('boom').handler('boom')],
|
|
267
|
-
})
|
|
268
|
-
const panel = Pilotiq.make('admin').notificationHandlers({
|
|
269
|
-
boom: async () => { throw new Error('bang') },
|
|
270
|
-
})
|
|
271
|
-
const result = await dispatchNotificationAction(panel, {
|
|
272
|
-
notificationId: id,
|
|
273
|
-
actionName: 'boom',
|
|
274
|
-
notifiableType: 'users',
|
|
275
|
-
notifiableId: '1',
|
|
276
|
-
user: { id: 1 },
|
|
277
|
-
})
|
|
278
|
-
assert.equal(result.ok, false)
|
|
279
|
-
assert.equal(result.ok === false && result.status, 500)
|
|
280
|
-
assert.match(result.ok === false ? result.error : '', /bang/)
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
it('rejects payload-injection — payload reads from the stored row only', async () => {
|
|
284
|
-
// The route doesn't accept a request body in v1. The dispatcher
|
|
285
|
-
// takes payload exclusively from `action.payload` on the stored row,
|
|
286
|
-
// so even a tampered Pilotiq config can't sneak extra keys in.
|
|
287
|
-
const id = await seedRowWithActions({
|
|
288
|
-
notifiableId: '1',
|
|
289
|
-
actions: [Action.make('check').handler('check').payload({ a: 1 })],
|
|
290
|
-
})
|
|
291
|
-
let received: Record<string, unknown> = {}
|
|
292
|
-
const panel = Pilotiq.make('admin').notificationHandlers({
|
|
293
|
-
check: async (ctx) => { received = ctx.payload; return undefined },
|
|
294
|
-
})
|
|
295
|
-
await dispatchNotificationAction(panel, {
|
|
296
|
-
notificationId: id,
|
|
297
|
-
actionName: 'check',
|
|
298
|
-
notifiableType: 'users',
|
|
299
|
-
notifiableId: '1',
|
|
300
|
-
user: { id: 1 },
|
|
301
|
-
})
|
|
302
|
-
assert.deepEqual(received, { a: 1 })
|
|
303
|
-
})
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
describe('Pilotiq.notificationHandlers registry', () => {
|
|
307
|
-
it('rejects URL-unsafe handler names at registration', () => {
|
|
308
|
-
assert.throws(
|
|
309
|
-
() => Pilotiq.make('admin').notificationHandlers({ 'bad name with spaces': async () => undefined }),
|
|
310
|
-
/URL-safe key/,
|
|
311
|
-
)
|
|
312
|
-
assert.throws(
|
|
313
|
-
() => Pilotiq.make('admin').notificationHandlers({ '': async () => undefined }),
|
|
314
|
-
/URL-safe key/,
|
|
315
|
-
)
|
|
316
|
-
assert.throws(
|
|
317
|
-
() => Pilotiq.make('admin').notificationHandlers({ 'with/slash': async () => undefined }),
|
|
318
|
-
/URL-safe key/,
|
|
319
|
-
)
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
it('accepts alphanumeric + dash + underscore', () => {
|
|
323
|
-
Pilotiq.make('admin').notificationHandlers({
|
|
324
|
-
'archive-project': async () => undefined,
|
|
325
|
-
'mark_done': async () => undefined,
|
|
326
|
-
'X1': async () => undefined,
|
|
327
|
-
})
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
it('subsequent calls merge — later keys override earlier', () => {
|
|
331
|
-
let which: string = ''
|
|
332
|
-
const panel = Pilotiq.make('admin')
|
|
333
|
-
.notificationHandlers({ 'a': async () => { which = 'first'; return undefined } })
|
|
334
|
-
.notificationHandlers({ 'a': async () => { which = 'second'; return undefined } })
|
|
335
|
-
const fn = panel.getNotificationHandler('a')
|
|
336
|
-
assert.ok(fn)
|
|
337
|
-
void fn!({ user: null, payload: {}, notificationId: 'x' })
|
|
338
|
-
// synchronous resolution since the handler closures don't await anything
|
|
339
|
-
assert.equal(which, 'second')
|
|
340
|
-
})
|
|
341
|
-
})
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Server-side dispatcher for `Notification.actions([Action…])` slots
|
|
3
|
-
* that target a registered handler by name. Mounted by `routes.ts` at
|
|
4
|
-
* `POST {base}/_notifications/:id/_action/:actionName`.
|
|
5
|
-
*
|
|
6
|
-
* Auth/lookup chain (each step returns the next-status code on miss):
|
|
7
|
-
* 401 → no resolved user
|
|
8
|
-
* 404 → row doesn't exist OR `notifiable_id !== user.id`
|
|
9
|
-
* 404 → stored row's `data.actions` doesn't contain `actionName`
|
|
10
|
-
* 404 → stored action's `handler` field isn't a string (closures
|
|
11
|
-
* should already be filtered at `sendToDatabase` — defend in
|
|
12
|
-
* depth)
|
|
13
|
-
* 404 → no registry entry under that handler name
|
|
14
|
-
* 200 → handler ran cleanly; result echoes back as
|
|
15
|
-
* `{ ok, redirect?, notifications?, download? }`
|
|
16
|
-
* 500 → handler threw — caught and wrapped
|
|
17
|
-
*
|
|
18
|
-
* The handler reads `payload` exclusively from the stored row, never
|
|
19
|
-
* from the request body, so a tampered client can't inject extra
|
|
20
|
-
* payload keys.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import type { Pilotiq } from '../Pilotiq.js'
|
|
24
|
-
import type {
|
|
25
|
-
NotificationActionContext,
|
|
26
|
-
NotificationActionResult,
|
|
27
|
-
} from './types.js'
|
|
28
|
-
import type { NotificationMeta } from './Notification.js'
|
|
29
|
-
import { findOneForUser, markAsRead } from './database.js'
|
|
30
|
-
|
|
31
|
-
export interface DispatchNotificationActionInput {
|
|
32
|
-
notificationId: string
|
|
33
|
-
actionName: string
|
|
34
|
-
notifiableType: string
|
|
35
|
-
notifiableId: string
|
|
36
|
-
user: unknown
|
|
37
|
-
request?: unknown
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface DispatchNotificationActionSuccess {
|
|
41
|
-
ok: true
|
|
42
|
-
redirect?: string
|
|
43
|
-
notifications?: NotificationMeta[]
|
|
44
|
-
/** When the matched action carried `markAsRead: true`, the route
|
|
45
|
-
* flipped `read_at` server-side. Mirrored back so the bell client
|
|
46
|
-
* can update its optimistic state without an extra round-trip. */
|
|
47
|
-
markedAsRead?: boolean
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface DispatchNotificationActionFailure {
|
|
51
|
-
ok: false
|
|
52
|
-
status: 401 | 404 | 500
|
|
53
|
-
error: string
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export type DispatchNotificationActionResult =
|
|
57
|
-
| DispatchNotificationActionSuccess
|
|
58
|
-
| DispatchNotificationActionFailure
|
|
59
|
-
|
|
60
|
-
export async function dispatchNotificationAction(
|
|
61
|
-
pilotiq: Pilotiq,
|
|
62
|
-
input: DispatchNotificationActionInput,
|
|
63
|
-
): Promise<DispatchNotificationActionResult> {
|
|
64
|
-
if (input.user === null || input.user === undefined) {
|
|
65
|
-
return { ok: false, status: 401, error: 'Not authenticated' }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const row = await findOneForUser(input.notificationId, {
|
|
69
|
-
notifiableType: input.notifiableType,
|
|
70
|
-
notifiableId: input.notifiableId,
|
|
71
|
-
})
|
|
72
|
-
if (!row) return { ok: false, status: 404, error: 'Notification not found' }
|
|
73
|
-
|
|
74
|
-
const action = row.actions?.find(a => a.name === input.actionName)
|
|
75
|
-
if (!action) return { ok: false, status: 404, error: 'Action not found on notification' }
|
|
76
|
-
|
|
77
|
-
// Closures don't survive serialization — if a stored action carries
|
|
78
|
-
// anything other than a string handler, treat it as a tampered row
|
|
79
|
-
// (or one written by an older code path) and refuse to dispatch.
|
|
80
|
-
if (typeof action.handler !== 'string') {
|
|
81
|
-
return {
|
|
82
|
-
ok: false,
|
|
83
|
-
status: 404,
|
|
84
|
-
error: 'Action does not target a registered handler (use Pilotiq.notificationHandlers).',
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const handler = pilotiq.getNotificationHandler(action.handler)
|
|
89
|
-
if (!handler) {
|
|
90
|
-
return {
|
|
91
|
-
ok: false,
|
|
92
|
-
status: 404,
|
|
93
|
-
error: `No notification handler registered for "${action.handler}".`,
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const ctx: NotificationActionContext = {
|
|
98
|
-
user: input.user,
|
|
99
|
-
payload: action.payload ?? {},
|
|
100
|
-
notificationId: input.notificationId,
|
|
101
|
-
}
|
|
102
|
-
if (input.request !== undefined) ctx.request = input.request
|
|
103
|
-
|
|
104
|
-
let result: NotificationActionResult | undefined
|
|
105
|
-
try {
|
|
106
|
-
result = await handler(ctx)
|
|
107
|
-
} catch (e) {
|
|
108
|
-
return {
|
|
109
|
-
ok: false,
|
|
110
|
-
status: 500,
|
|
111
|
-
error: e instanceof Error ? e.message : 'Notification handler threw',
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Side-effect: flip `read_at` when the stored action opted in.
|
|
116
|
-
// Server-side authoritative — handler dispatch is one round-trip,
|
|
117
|
-
// not two, and tampered clients can't suppress the mark.
|
|
118
|
-
let markedAsRead = false
|
|
119
|
-
if (action.markAsRead) {
|
|
120
|
-
markedAsRead = await markAsRead(input.notificationId, {
|
|
121
|
-
notifiableType: input.notifiableType,
|
|
122
|
-
notifiableId: input.notifiableId,
|
|
123
|
-
})
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const success: DispatchNotificationActionSuccess = { ok: true }
|
|
127
|
-
if (result && typeof result === 'object') {
|
|
128
|
-
if (typeof result.redirect === 'string') success.redirect = result.redirect
|
|
129
|
-
if (result.notify !== undefined) {
|
|
130
|
-
const notifs = Array.isArray(result.notify) ? result.notify : [result.notify]
|
|
131
|
-
const valid = notifs.filter(
|
|
132
|
-
(n): n is NotificationMeta => Boolean(n) && typeof n === 'object' && typeof (n as NotificationMeta).title === 'string',
|
|
133
|
-
)
|
|
134
|
-
if (valid.length > 0) success.notifications = valid
|
|
135
|
-
}
|
|
136
|
-
// download envelope (parallel to dispatchAction) — wired through
|
|
137
|
-
// the route layer when present; v1 doesn't surface it here to keep
|
|
138
|
-
// the wire shape narrow. Add when a consumer asks.
|
|
139
|
-
}
|
|
140
|
-
if (markedAsRead) success.markedAsRead = true
|
|
141
|
-
return success
|
|
142
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
|
|
4
|
-
import { flashNotifications, consumeFlashedNotifications } from './flash.js'
|
|
5
|
-
import type { NotificationMeta } from './Notification.js'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Minimal SessionInstance stand-in: just a key/value flash store with the
|
|
9
|
-
* "current request reads previous request's stash" semantics. Mirrors the
|
|
10
|
-
* `flash / getFlash` shape from `@rudderjs/session` without importing it
|
|
11
|
-
* (pilotiq doesn't peer-depend on session — it's optional in the host).
|
|
12
|
-
*/
|
|
13
|
-
function makeSession() {
|
|
14
|
-
let prev: Record<string, unknown> = {}
|
|
15
|
-
let next: Record<string, unknown> = {}
|
|
16
|
-
return {
|
|
17
|
-
flash(key: string, value: unknown) { next[key] = value },
|
|
18
|
-
getFlash<T>(key: string, fallback?: T): T | undefined {
|
|
19
|
-
return (key in prev ? prev[key] : fallback) as T | undefined
|
|
20
|
-
},
|
|
21
|
-
/** Test helper: simulate the redirect happening — current next becomes
|
|
22
|
-
* next request's prev. */
|
|
23
|
-
advance() { prev = next; next = {} },
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const meta = (title: string): NotificationMeta => ({
|
|
28
|
-
id: `n-${title}`,
|
|
29
|
-
type: 'success',
|
|
30
|
-
title,
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
describe('flashNotifications / consumeFlashedNotifications', () => {
|
|
34
|
-
it('roundtrip: flash on one request, consume on the next', () => {
|
|
35
|
-
const session = makeSession()
|
|
36
|
-
const req = { session }
|
|
37
|
-
flashNotifications(req, [meta('Saved')])
|
|
38
|
-
session.advance()
|
|
39
|
-
const got = consumeFlashedNotifications(req)
|
|
40
|
-
assert.equal(got.length, 1)
|
|
41
|
-
assert.equal(got[0]!.title, 'Saved')
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('empty array no-ops (does not stash an empty key that overwrites a prior flash)', () => {
|
|
45
|
-
const session = makeSession()
|
|
46
|
-
const req = { session }
|
|
47
|
-
flashNotifications(req, [meta('Saved')])
|
|
48
|
-
flashNotifications(req, []) // should not clobber
|
|
49
|
-
session.advance()
|
|
50
|
-
const got = consumeFlashedNotifications(req)
|
|
51
|
-
assert.equal(got.length, 1)
|
|
52
|
-
assert.equal(got[0]!.title, 'Saved')
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('undefined notifications no-op', () => {
|
|
56
|
-
const session = makeSession()
|
|
57
|
-
const req = { session }
|
|
58
|
-
flashNotifications(req, undefined)
|
|
59
|
-
session.advance()
|
|
60
|
-
assert.deepEqual(consumeFlashedNotifications(req), [])
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('missing session: writes are silently dropped, reads return []', () => {
|
|
64
|
-
const req = {} // no session at all
|
|
65
|
-
assert.doesNotThrow(() => flashNotifications(req, [meta('Saved')]))
|
|
66
|
-
assert.deepEqual(consumeFlashedNotifications(req), [])
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('undefined req: both helpers no-op', () => {
|
|
70
|
-
assert.doesNotThrow(() => flashNotifications(undefined, [meta('Saved')]))
|
|
71
|
-
assert.deepEqual(consumeFlashedNotifications(undefined), [])
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('multiple notifications survive the roundtrip in order', () => {
|
|
75
|
-
const session = makeSession()
|
|
76
|
-
const req = { session }
|
|
77
|
-
flashNotifications(req, [meta('first'), meta('second'), meta('third')])
|
|
78
|
-
session.advance()
|
|
79
|
-
const got = consumeFlashedNotifications(req)
|
|
80
|
-
assert.deepEqual(got.map(n => n.title), ['first', 'second', 'third'])
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('consume after no flash returns []', () => {
|
|
84
|
-
const session = makeSession()
|
|
85
|
-
const req = { session }
|
|
86
|
-
session.advance()
|
|
87
|
-
assert.deepEqual(consumeFlashedNotifications(req), [])
|
|
88
|
-
})
|
|
89
|
-
})
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bridge between the form/action lifecycle (which produces
|
|
3
|
-
* `NotificationMeta[]` on success) and the next request's render (which
|
|
4
|
-
* needs them in `viewProps.notifications` so the `Toaster` can fire).
|
|
5
|
-
*
|
|
6
|
-
* Sits on top of `@rudderjs/session`'s built-in flash primitive — see
|
|
7
|
-
* `SessionInstance.flash / getFlash`. We don't ship our own storage; we
|
|
8
|
-
* only namespace the key (`pilotiq:notifications`) and shape the value.
|
|
9
|
-
*
|
|
10
|
-
* If the host app hasn't installed `@rudderjs/session`, `req.session` is
|
|
11
|
-
* undefined; both helpers no-op silently rather than throwing — losing
|
|
12
|
-
* notifications on the 303 path is the same status quo as before flash
|
|
13
|
-
* landed, so an opt-out is the correct fallback.
|
|
14
|
-
*/
|
|
15
|
-
import type { NotificationMeta } from './Notification.js'
|
|
16
|
-
|
|
17
|
-
const FLASH_KEY = 'pilotiq:notifications'
|
|
18
|
-
|
|
19
|
-
interface FlashCapableSession {
|
|
20
|
-
flash(key: string, value: unknown): void
|
|
21
|
-
getFlash<T>(key: string, fallback?: T): T | undefined
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Pull a `FlashCapableSession` off any request-like object. `AppRequest`
|
|
26
|
-
* doesn't declare `session` in `@rudderjs/contracts` directly — the field
|
|
27
|
-
* is added via module augmentation in `@rudderjs/session`, which pilotiq
|
|
28
|
-
* doesn't peer-depend on (sessions are optional in the host). We duck-type
|
|
29
|
-
* at runtime so the helpers compile against unaugmented `AppRequest` and
|
|
30
|
-
* still find a real `SessionInstance` when one's mounted.
|
|
31
|
-
*/
|
|
32
|
-
function getSession(req: unknown): FlashCapableSession | undefined {
|
|
33
|
-
if (!req || typeof req !== 'object') return undefined
|
|
34
|
-
const session = (req as { session?: unknown }).session
|
|
35
|
-
if (!session || typeof session !== 'object') return undefined
|
|
36
|
-
const s = session as Record<string, unknown>
|
|
37
|
-
if (typeof s['flash'] !== 'function' || typeof s['getFlash'] !== 'function') return undefined
|
|
38
|
-
return session as FlashCapableSession
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Stash notifications on the session for the next request to consume.
|
|
43
|
-
* Called from POST handlers right before `res.redirect(..., 303)`.
|
|
44
|
-
*
|
|
45
|
-
* Invariant: call at most once per response. The underlying
|
|
46
|
-
* `session.flash(key, value)` overwrites any previously flashed value
|
|
47
|
-
* under the same key, so coalescing across multiple calls would silently
|
|
48
|
-
* lose the earlier batch.
|
|
49
|
-
*
|
|
50
|
-
* No-ops silently when the host app hasn't installed `@rudderjs/session`.
|
|
51
|
-
*/
|
|
52
|
-
export function flashNotifications(
|
|
53
|
-
req: unknown,
|
|
54
|
-
notifications: ReadonlyArray<NotificationMeta> | undefined,
|
|
55
|
-
): void {
|
|
56
|
-
if (!notifications || notifications.length === 0) return
|
|
57
|
-
const session = getSession(req)
|
|
58
|
-
if (!session) return
|
|
59
|
-
session.flash(FLASH_KEY, [...notifications])
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Read & clear the flashed notifications (consume-once semantics — the
|
|
64
|
-
* session driver clears the previous-request flash slot on save). Returns
|
|
65
|
-
* an empty array when there's nothing flashed or no session is installed.
|
|
66
|
-
*/
|
|
67
|
-
export function consumeFlashedNotifications(req: unknown): NotificationMeta[] {
|
|
68
|
-
const session = getSession(req)
|
|
69
|
-
if (!session) return []
|
|
70
|
-
return session.getFlash<NotificationMeta[]>(FLASH_KEY) ?? []
|
|
71
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
Notification,
|
|
3
|
-
type NotificationType,
|
|
4
|
-
type NotificationMeta,
|
|
5
|
-
_resetNotificationIdSeq,
|
|
6
|
-
} from './Notification.js'
|
|
7
|
-
|
|
8
|
-
export {
|
|
9
|
-
resolveSavedNotification,
|
|
10
|
-
type SavedNotificationMode,
|
|
11
|
-
} from './resolveSavedNotification.js'
|
|
12
|
-
|
|
13
|
-
export {
|
|
14
|
-
flashNotifications,
|
|
15
|
-
consumeFlashedNotifications,
|
|
16
|
-
} from './flash.js'
|
|
17
|
-
|
|
18
|
-
export type { Notifiable } from './types.js'
|
|
19
|
-
|
|
20
|
-
export {
|
|
21
|
-
listForUser,
|
|
22
|
-
unreadCount,
|
|
23
|
-
findOneForUser as findDatabaseNotificationForUser,
|
|
24
|
-
markAsRead,
|
|
25
|
-
markAsUnread,
|
|
26
|
-
markAllAsRead,
|
|
27
|
-
persist as persistDatabaseNotification,
|
|
28
|
-
type DatabaseNotificationMeta,
|
|
29
|
-
type ListOptions as DatabaseNotificationListOptions,
|
|
30
|
-
type ListResult as DatabaseNotificationListResult,
|
|
31
|
-
} from './database.js'
|
|
32
|
-
|
|
33
|
-
export type {
|
|
34
|
-
NotificationActionMeta,
|
|
35
|
-
NotificationActionHandler,
|
|
36
|
-
NotificationActionContext,
|
|
37
|
-
NotificationActionResult,
|
|
38
|
-
} from './types.js'
|
|
39
|
-
|
|
40
|
-
export {
|
|
41
|
-
push as pushBroadcastNotification,
|
|
42
|
-
notificationChannel,
|
|
43
|
-
NOTIFICATION_CREATED_EVENT,
|
|
44
|
-
type PushOptions as PushBroadcastOptions,
|
|
45
|
-
} from './broadcast.js'
|