spine-framework 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.framework/README.md +129 -0
- package/.framework/cli/bin.cjs +14 -0
- package/.framework/cli/commands/agents.ts +153 -0
- package/.framework/cli/commands/auth.ts +94 -0
- package/.framework/cli/commands/create-app.ts +185 -0
- package/.framework/cli/commands/dev.ts +295 -0
- package/.framework/cli/commands/doctor.ts +442 -0
- package/.framework/cli/commands/generate.ts +332 -0
- package/.framework/cli/commands/init.ts +272 -0
- package/.framework/cli/commands/install-app.ts +391 -0
- package/.framework/cli/commands/items.ts +253 -0
- package/.framework/cli/commands/migrations.ts +141 -0
- package/.framework/cli/commands/pipelines.ts +166 -0
- package/.framework/cli/commands/status.ts +197 -0
- package/.framework/cli/commands/system.ts +184 -0
- package/.framework/cli/commands/test.ts +227 -0
- package/.framework/cli/commands/uninstall-app.ts +166 -0
- package/.framework/cli/context.ts +268 -0
- package/.framework/cli/env-loader.ts +36 -0
- package/.framework/cli/index.ts +106 -0
- package/.framework/cli/welcome.cjs +45 -0
- package/.framework/docs/API.md +384 -0
- package/.framework/docs/STABILITY.md +52 -0
- package/.framework/docs/admin-routes.md +76 -0
- package/.framework/docs/api-docs-progress.md +38 -0
- package/.framework/docs/api-governance.md +146 -0
- package/.framework/docs/api-testing-results.md +212 -0
- package/.framework/docs/apis/admin-configs.md +567 -0
- package/.framework/docs/apis/admin-data.md +272 -0
- package/.framework/docs/apis/index.md +231 -0
- package/.framework/docs/apis/internal.md +295 -0
- package/.framework/docs/apis/runtime.md +537 -0
- package/.framework/docs/assembly-launch-guide.md +138 -0
- package/.framework/docs/audit-results.md +590 -0
- package/.framework/docs/authorization-model.md +170 -0
- package/.framework/docs/db-api-inventory.md +95 -0
- package/.framework/docs/examples/custom-app/README.md +77 -0
- package/.framework/docs/examples/custom-function/README.md +27 -0
- package/.framework/docs/examples/custom-function/handler.ts +48 -0
- package/.framework/docs/examples/custom-webhook/README.md +68 -0
- package/.framework/docs/gap-remediation-backlog.md +103 -0
- package/.framework/docs/guides/cli-guide.md +224 -0
- package/.framework/docs/guides/getting-started.md +103 -0
- package/.framework/docs/guides/import-guide.md +193 -0
- package/.framework/docs/guides/testing-guide.md +229 -0
- package/.framework/docs/permission-examples.md +326 -0
- package/.framework/docs/ui-adoption-verification.md +111 -0
- package/.framework/docs/ui-api-coverage.md +84 -0
- package/.framework/docs/v2-compatibility-audit.md +228 -0
- package/.framework/functions/.gitkeep +1 -0
- package/.framework/functions/_shared/agent-runner.ts +1097 -0
- package/.framework/functions/_shared/app-manifest.ts +184 -0
- package/.framework/functions/_shared/audit.ts +150 -0
- package/.framework/functions/_shared/db.ts +174 -0
- package/.framework/functions/_shared/index.ts +382 -0
- package/.framework/functions/_shared/middleware.ts +490 -0
- package/.framework/functions/_shared/permissions.ts +1325 -0
- package/.framework/functions/_shared/pipeline-runner.ts +731 -0
- package/.framework/functions/_shared/principal.ts +760 -0
- package/.framework/functions/_shared/schema-utils.ts +967 -0
- package/.framework/functions/_shared/testing.ts +258 -0
- package/.framework/functions/_shared/trigger-engine.ts +425 -0
- package/.framework/functions/_shared/webhook-registration.ts +168 -0
- package/.framework/functions/_shared/webhook-registry.ts +129 -0
- package/.framework/functions/account-nodes.ts +111 -0
- package/.framework/functions/admin-data.ts +606 -0
- package/.framework/functions/ai-agents.ts +323 -0
- package/.framework/functions/api-keys.ts +376 -0
- package/.framework/functions/apps.ts +483 -0
- package/.framework/functions/auth.ts +196 -0
- package/.framework/functions/debug-auth.ts +107 -0
- package/.framework/functions/embeddings.ts +556 -0
- package/.framework/functions/integration-routes.ts +523 -0
- package/.framework/functions/integrations.ts +319 -0
- package/.framework/functions/item-progress.ts +272 -0
- package/.framework/functions/logs.ts +438 -0
- package/.framework/functions/observability.ts +275 -0
- package/.framework/functions/pipeline-executions.ts +494 -0
- package/.framework/functions/pipelines.ts +485 -0
- package/.framework/functions/prompt-configs.ts +339 -0
- package/.framework/functions/roles.ts +387 -0
- package/.framework/functions/system-cron.ts +742 -0
- package/.framework/functions/system.ts +323 -0
- package/.framework/functions/tests.ts +119 -0
- package/.framework/functions/timers.ts +357 -0
- package/.framework/functions/triggers.ts +563 -0
- package/.framework/functions/types.ts +604 -0
- package/.framework/migrations/000_foundation.sql +1256 -0
- package/.framework/migrations/001_seed.sql +92 -0
- package/.framework/migrations/002_seed_constraints.sql +13 -0
- package/.framework/migrations/003_auth_user_trigger.sql +59 -0
- package/.framework/src/App.tsx +126 -0
- package/.framework/src/apps/admin/index.tsx +173 -0
- package/.framework/src/components/AppWrapper.tsx +56 -0
- package/.framework/src/components/CustomAppLoader.tsx +116 -0
- package/.framework/src/components/admin/AdminListPage.tsx +151 -0
- package/.framework/src/components/admin/AdminSidebar.tsx +166 -0
- package/.framework/src/components/admin/AdminStatsCard.tsx +62 -0
- package/.framework/src/components/admin/SortableTableHeader.tsx +42 -0
- package/.framework/src/components/app-shell/GenericAppShell.tsx +181 -0
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +200 -0
- package/.framework/src/components/app-shell/GenericListPage.tsx +116 -0
- package/.framework/src/components/app-sidebar.tsx +228 -0
- package/.framework/src/components/auth/ProtectedRoute.tsx +88 -0
- package/.framework/src/components/layout/AppShell.tsx +91 -0
- package/.framework/src/components/layout/Header.tsx +88 -0
- package/.framework/src/components/layout/Layout.tsx +95 -0
- package/.framework/src/components/layout/Sidebar.tsx +329 -0
- package/.framework/src/components/runtime/DataDetailHeader.tsx +77 -0
- package/.framework/src/components/runtime/DataDetailPage.tsx +171 -0
- package/.framework/src/components/runtime/DataFilters.tsx +91 -0
- package/.framework/src/components/runtime/DataHeader.tsx +68 -0
- package/.framework/src/components/runtime/DataListPage.tsx +124 -0
- package/.framework/src/components/runtime/DataStats.tsx +70 -0
- package/.framework/src/components/runtime/DataTable.tsx +174 -0
- package/.framework/src/components/runtime/SchemaDetailForm.tsx +134 -0
- package/.framework/src/components/runtime/index.ts +18 -0
- package/.framework/src/components/search-form.tsx +29 -0
- package/.framework/src/components/shared/AgentView.tsx +213 -0
- package/.framework/src/components/shared/FieldRenderer.tsx +478 -0
- package/.framework/src/components/shared/SchemaFields.tsx +226 -0
- package/.framework/src/components/ui/DataTable.tsx +343 -0
- package/.framework/src/components/ui/Form.tsx +281 -0
- package/.framework/src/components/ui/ItemCard.tsx +296 -0
- package/.framework/src/components/ui/ItemListView.tsx +308 -0
- package/.framework/src/components/ui/LoadingSpinner.tsx +52 -0
- package/.framework/src/components/ui/Modal.tsx +61 -0
- package/.framework/src/components/ui/RichTextEditor.tsx +210 -0
- package/.framework/src/components/ui/accordion.tsx +82 -0
- package/.framework/src/components/ui/alert-dialog.tsx +197 -0
- package/.framework/src/components/ui/alert.tsx +76 -0
- package/.framework/src/components/ui/aspect-ratio.tsx +11 -0
- package/.framework/src/components/ui/avatar.tsx +110 -0
- package/.framework/src/components/ui/badge.tsx +49 -0
- package/.framework/src/components/ui/breadcrumb.tsx +122 -0
- package/.framework/src/components/ui/button-group.tsx +83 -0
- package/.framework/src/components/ui/button.tsx +65 -0
- package/.framework/src/components/ui/calendar.tsx +222 -0
- package/.framework/src/components/ui/card.tsx +100 -0
- package/.framework/src/components/ui/carousel.tsx +240 -0
- package/.framework/src/components/ui/chart.tsx +373 -0
- package/.framework/src/components/ui/checkbox.tsx +31 -0
- package/.framework/src/components/ui/collapsible.tsx +33 -0
- package/.framework/src/components/ui/combobox.tsx +299 -0
- package/.framework/src/components/ui/command.tsx +193 -0
- package/.framework/src/components/ui/context-menu.tsx +261 -0
- package/.framework/src/components/ui/dialog.tsx +165 -0
- package/.framework/src/components/ui/direction.tsx +22 -0
- package/.framework/src/components/ui/drawer.tsx +132 -0
- package/.framework/src/components/ui/dropdown-menu.tsx +269 -0
- package/.framework/src/components/ui/empty.tsx +104 -0
- package/.framework/src/components/ui/field.tsx +238 -0
- package/.framework/src/components/ui/hover-card.tsx +42 -0
- package/.framework/src/components/ui/input-group.tsx +153 -0
- package/.framework/src/components/ui/input-otp.tsx +87 -0
- package/.framework/src/components/ui/input.tsx +19 -0
- package/.framework/src/components/ui/item.tsx +196 -0
- package/.framework/src/components/ui/kbd.tsx +26 -0
- package/.framework/src/components/ui/label.tsx +22 -0
- package/.framework/src/components/ui/menubar.tsx +277 -0
- package/.framework/src/components/ui/native-select.tsx +61 -0
- package/.framework/src/components/ui/navigation-menu.tsx +164 -0
- package/.framework/src/components/ui/pagination.tsx +129 -0
- package/.framework/src/components/ui/popover.tsx +87 -0
- package/.framework/src/components/ui/progress.tsx +31 -0
- package/.framework/src/components/ui/radio-group.tsx +42 -0
- package/.framework/src/components/ui/resizable.tsx +50 -0
- package/.framework/src/components/ui/scroll-area.tsx +53 -0
- package/.framework/src/components/ui/select.tsx +195 -0
- package/.framework/src/components/ui/separator.tsx +26 -0
- package/.framework/src/components/ui/sheet.tsx +145 -0
- package/.framework/src/components/ui/sidebar.tsx +706 -0
- package/.framework/src/components/ui/skeleton.tsx +13 -0
- package/.framework/src/components/ui/slider.tsx +59 -0
- package/.framework/src/components/ui/sonner.tsx +47 -0
- package/.framework/src/components/ui/spinner.tsx +10 -0
- package/.framework/src/components/ui/switch.tsx +33 -0
- package/.framework/src/components/ui/table-primitives.tsx +141 -0
- package/.framework/src/components/ui/table.tsx +114 -0
- package/.framework/src/components/ui/tabs.tsx +90 -0
- package/.framework/src/components/ui/textarea.tsx +18 -0
- package/.framework/src/components/ui/toggle-group.tsx +89 -0
- package/.framework/src/components/ui/toggle.tsx +45 -0
- package/.framework/src/components/ui/tooltip.tsx +57 -0
- package/.framework/src/contexts/AppContext.tsx +133 -0
- package/.framework/src/contexts/AuthContext.tsx +371 -0
- package/.framework/src/hooks/use-mobile.ts +19 -0
- package/.framework/src/hooks/useApi.ts +526 -0
- package/.framework/src/hooks/useApps.ts +114 -0
- package/.framework/src/hooks/useEntityList.ts +190 -0
- package/.framework/src/hooks/useEntityRecord.ts +308 -0
- package/.framework/src/hooks/useForm.ts +307 -0
- package/.framework/src/hooks/useListSchema.ts +264 -0
- package/.framework/src/hooks/useSchemaRecord.ts +223 -0
- package/.framework/src/index.css +128 -0
- package/.framework/src/lib/api.ts +156 -0
- package/.framework/src/lib/supabase.ts +94 -0
- package/.framework/src/lib/utils.ts +317 -0
- package/.framework/src/main.tsx +27 -0
- package/.framework/src/pages/DashboardPage.tsx +181 -0
- package/.framework/src/pages/NotFoundPage.tsx +39 -0
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +161 -0
- package/.framework/src/pages/admin/AIAgentsPage.tsx +318 -0
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +199 -0
- package/.framework/src/pages/admin/APIKeysPage.tsx +303 -0
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +523 -0
- package/.framework/src/pages/admin/AppDetailPage.tsx +493 -0
- package/.framework/src/pages/admin/AppsPage.tsx +355 -0
- package/.framework/src/pages/admin/DesignedPage.tsx +491 -0
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +534 -0
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +424 -0
- package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +176 -0
- package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +109 -0
- package/.framework/src/pages/admin/IntegratedDashboard.tsx +402 -0
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +187 -0
- package/.framework/src/pages/admin/IntegrationsPage.tsx +301 -0
- package/.framework/src/pages/admin/LogsPage.tsx +283 -0
- package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +85 -0
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +470 -0
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +183 -0
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +279 -0
- package/.framework/src/pages/admin/PipelinesPage.tsx +390 -0
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +299 -0
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +292 -0
- package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +434 -0
- package/.framework/src/pages/admin/RoleDetailPage.tsx +273 -0
- package/.framework/src/pages/admin/RolesPage.tsx +292 -0
- package/.framework/src/pages/admin/SelectTestPage.tsx +61 -0
- package/.framework/src/pages/admin/ShadcnTestPage.tsx +588 -0
- package/.framework/src/pages/admin/SimpleDashboard.tsx +387 -0
- package/.framework/src/pages/admin/TestRunDetailPage.tsx +172 -0
- package/.framework/src/pages/admin/TestingDashboard.tsx +257 -0
- package/.framework/src/pages/admin/TimerDetailPage.tsx +151 -0
- package/.framework/src/pages/admin/TimersPage.tsx +376 -0
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +149 -0
- package/.framework/src/pages/admin/TriggersPage.tsx +381 -0
- package/.framework/src/pages/admin/TypeDetailPage.tsx +694 -0
- package/.framework/src/pages/admin/TypesPage.tsx +295 -0
- package/.framework/src/pages/auth/LoginPage.tsx +188 -0
- package/.framework/src/pages/auth/RegisterPage.tsx +163 -0
- package/.framework/src/pages/spine-framework/APIPage.tsx +17 -0
- package/.framework/src/pages/spine-framework/CLIPage.tsx +25 -0
- package/.framework/src/types/auth.ts +125 -0
- package/.framework/src/types/types.ts +407 -0
- package/STRUCTURE.md +150 -0
- package/config/components.json +25 -0
- package/config/deno.lock +108 -0
- package/config/package-lock.json +17183 -0
- package/config/postcss.config.cjs +10 -0
- package/config/tailwind.config.cjs +78 -0
- package/config/tsconfig.build.json +32 -0
- package/config/tsconfig.cli.json +18 -0
- package/config/tsconfig.json +41 -0
- package/config/tsconfig.node.json +17 -0
- package/config/tsconfig.node.tsbuildinfo +1 -0
- package/config/tsconfig.tsbuildinfo +1 -0
- package/config/typedoc.json +16 -0
- package/config/vite.config.d.ts +2 -0
- package/config/vite.config.ts +72 -0
- package/dist/cli/commands/agents.d.ts +39 -0
- package/dist/cli/commands/agents.d.ts.map +1 -0
- package/dist/cli/commands/auth.d.ts +36 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/create-app.d.ts +23 -0
- package/dist/cli/commands/create-app.d.ts.map +1 -0
- package/dist/cli/commands/dev.d.ts +39 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/doctor.d.ts +42 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/generate.d.ts +36 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/init.d.ts +30 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/install-app.d.ts +30 -0
- package/dist/cli/commands/install-app.d.ts.map +1 -0
- package/dist/cli/commands/items.d.ts +45 -0
- package/dist/cli/commands/items.d.ts.map +1 -0
- package/dist/cli/commands/migrations.d.ts +41 -0
- package/dist/cli/commands/migrations.d.ts.map +1 -0
- package/dist/cli/commands/pipelines.d.ts +40 -0
- package/dist/cli/commands/pipelines.d.ts.map +1 -0
- package/dist/cli/commands/status.d.ts +23 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/system.d.ts +29 -0
- package/dist/cli/commands/system.d.ts.map +1 -0
- package/dist/cli/commands/test.d.ts +46 -0
- package/dist/cli/commands/test.d.ts.map +1 -0
- package/dist/cli/commands/uninstall-app.d.ts +23 -0
- package/dist/cli/commands/uninstall-app.d.ts.map +1 -0
- package/dist/cli/context.d.ts +88 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/env-loader.d.ts +14 -0
- package/dist/cli/env-loader.d.ts.map +1 -0
- package/dist/cli/index.d.ts +41 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/functions/_shared/agent-runner.d.ts +156 -0
- package/dist/functions/_shared/agent-runner.d.ts.map +1 -0
- package/dist/functions/_shared/app-manifest.d.ts +68 -0
- package/dist/functions/_shared/app-manifest.d.ts.map +1 -0
- package/dist/functions/_shared/audit.d.ts +91 -0
- package/dist/functions/_shared/audit.d.ts.map +1 -0
- package/dist/functions/_shared/db.d.ts +125 -0
- package/dist/functions/_shared/db.d.ts.map +1 -0
- package/dist/functions/_shared/index.d.ts +298 -0
- package/dist/functions/_shared/index.d.ts.map +1 -0
- package/dist/functions/_shared/middleware.d.ts +315 -0
- package/dist/functions/_shared/middleware.d.ts.map +1 -0
- package/dist/functions/_shared/permissions.d.ts +626 -0
- package/dist/functions/_shared/permissions.d.ts.map +1 -0
- package/dist/functions/_shared/pipeline-runner.d.ts +124 -0
- package/dist/functions/_shared/pipeline-runner.d.ts.map +1 -0
- package/dist/functions/_shared/principal.d.ts +284 -0
- package/dist/functions/_shared/principal.d.ts.map +1 -0
- package/dist/functions/_shared/schema-utils.d.ts +181 -0
- package/dist/functions/_shared/schema-utils.d.ts.map +1 -0
- package/dist/functions/_shared/testing.d.ts +172 -0
- package/dist/functions/_shared/testing.d.ts.map +1 -0
- package/dist/functions/_shared/trigger-engine.d.ts +140 -0
- package/dist/functions/_shared/trigger-engine.d.ts.map +1 -0
- package/dist/functions/_shared/webhook-registration.d.ts +81 -0
- package/dist/functions/_shared/webhook-registration.d.ts.map +1 -0
- package/dist/functions/_shared/webhook-registry.d.ts +57 -0
- package/dist/functions/_shared/webhook-registry.d.ts.map +1 -0
- package/dist/functions/account-nodes.d.ts +48 -0
- package/dist/functions/account-nodes.d.ts.map +1 -0
- package/dist/functions/admin-data.d.ts +178 -0
- package/dist/functions/admin-data.d.ts.map +1 -0
- package/dist/functions/ai-agents.d.ts +125 -0
- package/dist/functions/ai-agents.d.ts.map +1 -0
- package/dist/functions/api-keys.d.ts +140 -0
- package/dist/functions/api-keys.d.ts.map +1 -0
- package/dist/functions/apps.d.ts +163 -0
- package/dist/functions/apps.d.ts.map +1 -0
- package/dist/functions/auth.d.ts +74 -0
- package/dist/functions/auth.d.ts.map +1 -0
- package/dist/functions/debug-auth.d.ts +33 -0
- package/dist/functions/debug-auth.d.ts.map +1 -0
- package/dist/functions/embeddings.d.ts +205 -0
- package/dist/functions/embeddings.d.ts.map +1 -0
- package/dist/functions/integration-routes.d.ts +45 -0
- package/dist/functions/integration-routes.d.ts.map +1 -0
- package/dist/functions/integrations.d.ts +124 -0
- package/dist/functions/integrations.d.ts.map +1 -0
- package/dist/functions/item-progress.d.ts +41 -0
- package/dist/functions/item-progress.d.ts.map +1 -0
- package/dist/functions/logs.d.ts +162 -0
- package/dist/functions/logs.d.ts.map +1 -0
- package/dist/functions/observability.d.ts +123 -0
- package/dist/functions/observability.d.ts.map +1 -0
- package/dist/functions/pipeline-executions.d.ts +190 -0
- package/dist/functions/pipeline-executions.d.ts.map +1 -0
- package/dist/functions/pipelines.d.ts +171 -0
- package/dist/functions/pipelines.d.ts.map +1 -0
- package/dist/functions/prompt-configs.d.ts +125 -0
- package/dist/functions/prompt-configs.d.ts.map +1 -0
- package/dist/functions/roles.d.ts +118 -0
- package/dist/functions/roles.d.ts.map +1 -0
- package/dist/functions/system-cron.d.ts +65 -0
- package/dist/functions/system-cron.d.ts.map +1 -0
- package/dist/functions/system.d.ts +29 -0
- package/dist/functions/system.d.ts.map +1 -0
- package/dist/functions/tests.d.ts +28 -0
- package/dist/functions/tests.d.ts.map +1 -0
- package/dist/functions/timers.d.ts +139 -0
- package/dist/functions/timers.d.ts.map +1 -0
- package/dist/functions/triggers.d.ts +203 -0
- package/dist/functions/triggers.d.ts.map +1 -0
- package/dist/functions/types.d.ts +151 -0
- package/dist/functions/types.d.ts.map +1 -0
- package/dist/src/types/types.d.ts +364 -0
- package/dist/src/types/types.d.ts.map +1 -0
- package/package.json +192 -0
- package/scripts/app-install-cli.ts +286 -0
- package/scripts/assemble-frontend.sh +79 -0
- package/scripts/assemble-functions.sh +62 -0
- package/scripts/assemble.sh +35 -0
- package/scripts/boundary-check.sh +106 -0
- package/scripts/build-manifest.sh +80 -0
- package/scripts/check-core-integrity.sh +82 -0
- package/scripts/ingest-chunks.cjs +202 -0
- package/scripts/kb-chunk-parser.cjs +312 -0
- package/scripts/kb-chunk-parser.ts +330 -0
- package/scripts/load-test-app-install.ts +484 -0
- package/scripts/netlify-dev-wrapper.sh +22 -0
- package/scripts/verify-integrity.sh +69 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module src/hooks/useApi
|
|
3
|
+
* @audience installer
|
|
4
|
+
* @layer frontend-hook
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* Low-level React hooks for async API calls with loading/error state,
|
|
8
|
+
* AbortController-based cancellation, and optional pagination/mutation
|
|
9
|
+
* variants. These are the primitives all higher-level data hooks
|
|
10
|
+
* (`useEntityList`, `useEntityRecord`) build on top of.
|
|
11
|
+
*
|
|
12
|
+
* **Exports:**
|
|
13
|
+
* | Hook | Purpose |
|
|
14
|
+
* |-------------------|-----------------------------------------------------|
|
|
15
|
+
* | `useApi` | Single async call with abort + route-change re-fetch|
|
|
16
|
+
* | `usePaginatedApi` | Paginated async call with page/size controls |
|
|
17
|
+
* | `useMutation` | Write operation (create/update/delete) with state |
|
|
18
|
+
*
|
|
19
|
+
* **Abort contract:** `useApi` creates a new `AbortController` per
|
|
20
|
+
* invocation. On route navigation (`location.pathname` change) or on
|
|
21
|
+
* unmount, the in-flight request is aborted and state is reset. This
|
|
22
|
+
* prevents stale responses from a previous route populating the next
|
|
23
|
+
* route's data.
|
|
24
|
+
*
|
|
25
|
+
* @seeAlso src/lib/api.ts (apiFetch — passes AbortSignal through)
|
|
26
|
+
* @seeAlso src/hooks/useEntityList.ts (uses useApi)
|
|
27
|
+
* @seeAlso src/hooks/useEntityRecord.ts (uses useApi + useMutation)
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
31
|
+
import { useLocation } from 'react-router-dom'
|
|
32
|
+
|
|
33
|
+
// ─── TYPES ───────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
interface ApiState<T> {
|
|
36
|
+
data: T | null
|
|
37
|
+
loading: boolean
|
|
38
|
+
error: string | null
|
|
39
|
+
lastFetched: Date | null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Options for `useApi`.
|
|
44
|
+
*
|
|
45
|
+
* @prop immediate - If true, executes the function on mount and on route change
|
|
46
|
+
* @prop onSuccess - Callback fired on successful response
|
|
47
|
+
* @prop onError - Callback fired on error (with message string)
|
|
48
|
+
* @prop initialData - Seed value for `data` before first fetch
|
|
49
|
+
*/
|
|
50
|
+
interface UseApiOptions<T> {
|
|
51
|
+
immediate?: boolean
|
|
52
|
+
onSuccess?: (data: T) => void
|
|
53
|
+
onError?: (error: string) => void
|
|
54
|
+
initialData?: T
|
|
55
|
+
deps?: any[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Return value of `useApi`.
|
|
60
|
+
*
|
|
61
|
+
* @prop data - Response data or null
|
|
62
|
+
* @prop loading - True while the request is in flight
|
|
63
|
+
* @prop error - Error message string or null
|
|
64
|
+
* @prop execute - Imperatively trigger the API call (with optional params)
|
|
65
|
+
* @prop reset - Cancel in-flight request and restore to initial state
|
|
66
|
+
* @prop refetch - Alias for `execute()` with no params
|
|
67
|
+
*/
|
|
68
|
+
interface UseApiReturn<T> {
|
|
69
|
+
data: T | null
|
|
70
|
+
loading: boolean
|
|
71
|
+
error: string | null
|
|
72
|
+
execute: (params?: any) => Promise<T>
|
|
73
|
+
reset: () => void
|
|
74
|
+
refetch: () => Promise<T>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── useApi ──────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Generic async API hook with loading/error state and automatic
|
|
81
|
+
* request cancellation.
|
|
82
|
+
*
|
|
83
|
+
* Each call to `execute` cancels any previous in-flight request via
|
|
84
|
+
* `AbortController`. When `immediate: true`, the hook re-executes
|
|
85
|
+
* (and aborts the prior request) whenever `location.pathname` changes.
|
|
86
|
+
*
|
|
87
|
+
* @param apiFunction - Async function to call; receives `{ ...params, signal }`
|
|
88
|
+
* @param options - `UseApiOptions<T>` — see type for details
|
|
89
|
+
* @returns `UseApiReturn<T>` — data, loading, error, execute, reset, refetch
|
|
90
|
+
*
|
|
91
|
+
* @inputSpec apiFunction must forward `signal` to any underlying `apiFetch`
|
|
92
|
+
* call, otherwise cancellation is a no-op.
|
|
93
|
+
* @sideEffects React state mutations; aborts in-flight fetch on cleanup
|
|
94
|
+
* @calledBy useEntityList.ts, useEntityRecord.ts
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```tsx
|
|
98
|
+
* const { data, loading, execute } = useApi(
|
|
99
|
+
* async () => apiFetch('/api/items?action=list').then(r => r.json()),
|
|
100
|
+
* { immediate: true }
|
|
101
|
+
* )
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export function useApi<T>(
|
|
105
|
+
apiFunction: (params?: any) => Promise<T>,
|
|
106
|
+
options: UseApiOptions<T> = {}
|
|
107
|
+
): UseApiReturn<T> {
|
|
108
|
+
const { immediate = false, onSuccess, onError, initialData = null } = options
|
|
109
|
+
const location = useLocation()
|
|
110
|
+
|
|
111
|
+
const apiFunctionRef = useRef(apiFunction)
|
|
112
|
+
const onSuccessRef = useRef(onSuccess)
|
|
113
|
+
const onErrorRef = useRef(onError)
|
|
114
|
+
const immediateRef = useRef(immediate)
|
|
115
|
+
const initialDataRef = useRef(initialData)
|
|
116
|
+
|
|
117
|
+
apiFunctionRef.current = apiFunction
|
|
118
|
+
onSuccessRef.current = onSuccess
|
|
119
|
+
onErrorRef.current = onError
|
|
120
|
+
immediateRef.current = immediate
|
|
121
|
+
initialDataRef.current = initialData
|
|
122
|
+
|
|
123
|
+
const [state, setState] = useState<ApiState<T>>({
|
|
124
|
+
data: initialData,
|
|
125
|
+
loading: false,
|
|
126
|
+
error: null,
|
|
127
|
+
lastFetched: null
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// Stable AbortController ref — replaced on each new fetch cycle
|
|
131
|
+
const abortControllerRef = useRef<AbortController | null>(null)
|
|
132
|
+
|
|
133
|
+
const execute = useCallback(async (params?: any) => {
|
|
134
|
+
// Create a new AbortController for this specific request
|
|
135
|
+
const abortController = new AbortController()
|
|
136
|
+
const { signal } = abortController
|
|
137
|
+
|
|
138
|
+
// Cancel previous request if still running
|
|
139
|
+
abortControllerRef.current?.abort()
|
|
140
|
+
abortControllerRef.current = abortController
|
|
141
|
+
|
|
142
|
+
console.log('useApi execute: starting request', { signalAborted: signal.aborted })
|
|
143
|
+
setState(prev => ({ ...prev, loading: true, error: null }))
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const startTime = Date.now()
|
|
147
|
+
const result = await apiFunctionRef.current({ ...params, signal })
|
|
148
|
+
const duration = Date.now() - startTime
|
|
149
|
+
|
|
150
|
+
// Structured API call log for agentic IDE consumption
|
|
151
|
+
console.log(JSON.stringify({
|
|
152
|
+
type: 'spine_api_call',
|
|
153
|
+
timestamp: new Date().toISOString(),
|
|
154
|
+
duration_ms: duration,
|
|
155
|
+
status: 'success',
|
|
156
|
+
signal_aborted: signal.aborted
|
|
157
|
+
}))
|
|
158
|
+
if (signal.aborted) {
|
|
159
|
+
console.log('useApi execute: request was aborted, returning result')
|
|
160
|
+
return result
|
|
161
|
+
}
|
|
162
|
+
setState({
|
|
163
|
+
data: result,
|
|
164
|
+
loading: false,
|
|
165
|
+
error: null,
|
|
166
|
+
lastFetched: new Date()
|
|
167
|
+
})
|
|
168
|
+
onSuccessRef.current?.(result)
|
|
169
|
+
return result
|
|
170
|
+
} catch (error: any) {
|
|
171
|
+
console.log('useApi execute: request failed', { error, signalAborted: signal.aborted, errorName: error?.name })
|
|
172
|
+
if ((error as any)?.name === 'AbortError') {
|
|
173
|
+
console.log('useApi execute: abort error, throwing')
|
|
174
|
+
throw error
|
|
175
|
+
}
|
|
176
|
+
const errorMessage = error?.message || 'An error occurred'
|
|
177
|
+
setState(prev => ({
|
|
178
|
+
...prev,
|
|
179
|
+
loading: false,
|
|
180
|
+
error: errorMessage
|
|
181
|
+
}))
|
|
182
|
+
onErrorRef.current?.(errorMessage)
|
|
183
|
+
throw error
|
|
184
|
+
}
|
|
185
|
+
}, [])
|
|
186
|
+
|
|
187
|
+
const reset = useCallback(() => {
|
|
188
|
+
abortControllerRef.current?.abort()
|
|
189
|
+
setState({
|
|
190
|
+
data: initialDataRef.current,
|
|
191
|
+
loading: false,
|
|
192
|
+
error: null,
|
|
193
|
+
lastFetched: null
|
|
194
|
+
})
|
|
195
|
+
}, [])
|
|
196
|
+
|
|
197
|
+
const refetch = useCallback(() => {
|
|
198
|
+
return execute()
|
|
199
|
+
}, [execute])
|
|
200
|
+
|
|
201
|
+
// Re-fetch when pathname changes (navigation) — AbortController cancels
|
|
202
|
+
// any previous in-flight request so auth state re-renders don't corrupt data
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
if (!immediateRef.current) return
|
|
205
|
+
setState({
|
|
206
|
+
data: initialDataRef.current,
|
|
207
|
+
loading: false,
|
|
208
|
+
error: null,
|
|
209
|
+
lastFetched: null
|
|
210
|
+
})
|
|
211
|
+
const timeoutId = setTimeout(() => { execute() }, 0)
|
|
212
|
+
return () => {
|
|
213
|
+
clearTimeout(timeoutId)
|
|
214
|
+
abortControllerRef.current?.abort()
|
|
215
|
+
}
|
|
216
|
+
}, [location.pathname, execute])
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
data: state.data,
|
|
220
|
+
loading: state.loading,
|
|
221
|
+
error: state.error,
|
|
222
|
+
execute,
|
|
223
|
+
reset,
|
|
224
|
+
refetch
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ─── usePaginatedApi ─────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
interface PaginatedApiState<T> extends ApiState<T[]> {
|
|
231
|
+
pagination: {
|
|
232
|
+
page: number
|
|
233
|
+
totalPages: number
|
|
234
|
+
totalItems: number
|
|
235
|
+
itemsPerPage: number
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Options for `usePaginatedApi`. Extends `UseApiOptions` with `itemsPerPage`.
|
|
241
|
+
*/
|
|
242
|
+
interface UsePaginatedApiOptions<T> extends UseApiOptions<T[]> {
|
|
243
|
+
itemsPerPage?: number
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Return value of `usePaginatedApi`. Extends `UseApiReturn` with pagination
|
|
248
|
+
* controls.
|
|
249
|
+
*
|
|
250
|
+
* @prop pagination - Current page, totalPages, totalItems, itemsPerPage
|
|
251
|
+
* @prop setPage - Navigate to a specific page (triggers re-fetch if immediate)
|
|
252
|
+
* @prop setItemsPerPage - Change page size, resets to page 1
|
|
253
|
+
* @prop nextPage / prevPage - Convenience page navigation
|
|
254
|
+
* @prop hasNextPage / hasPrevPage - Boundary guards
|
|
255
|
+
*/
|
|
256
|
+
interface UsePaginatedApiReturn<T> extends UseApiReturn<T[]> {
|
|
257
|
+
pagination: PaginatedApiState<T>['pagination']
|
|
258
|
+
setPage: (page: number) => void
|
|
259
|
+
setItemsPerPage: (itemsPerPage: number) => void
|
|
260
|
+
nextPage: () => void
|
|
261
|
+
prevPage: () => void
|
|
262
|
+
hasNextPage: boolean
|
|
263
|
+
hasPrevPage: boolean
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Paginated variant of `useApi`. Manages page and itemsPerPage state and
|
|
268
|
+
* automatically re-fetches when either changes (if `immediate: true`).
|
|
269
|
+
*
|
|
270
|
+
* @param apiFunction - Must accept `{ page, itemsPerPage, ...params }` and
|
|
271
|
+
* return `{ data: T[], pagination: { page, totalPages, totalItems, itemsPerPage } }`
|
|
272
|
+
* @param options - `UsePaginatedApiOptions<T>`
|
|
273
|
+
* @returns `UsePaginatedApiReturn<T>`
|
|
274
|
+
*
|
|
275
|
+
* @sideEffects React state mutations
|
|
276
|
+
* @calledBy admin list pages with server-side pagination
|
|
277
|
+
*/
|
|
278
|
+
export function usePaginatedApi<T>(
|
|
279
|
+
apiFunction: (params: { page: number; itemsPerPage: number; [key: string]: any }) => Promise<{
|
|
280
|
+
data: T[]
|
|
281
|
+
pagination: {
|
|
282
|
+
page: number
|
|
283
|
+
totalPages: number
|
|
284
|
+
totalItems: number
|
|
285
|
+
itemsPerPage: number
|
|
286
|
+
}
|
|
287
|
+
}>,
|
|
288
|
+
options: UsePaginatedApiOptions<T> = {}
|
|
289
|
+
): UsePaginatedApiReturn<T> {
|
|
290
|
+
const { itemsPerPage: defaultItemsPerPage = 20, ...apiOptions } = options
|
|
291
|
+
|
|
292
|
+
const [state, setState] = useState<PaginatedApiState<T>>({
|
|
293
|
+
data: [],
|
|
294
|
+
loading: false,
|
|
295
|
+
error: null,
|
|
296
|
+
lastFetched: null,
|
|
297
|
+
pagination: {
|
|
298
|
+
page: 1,
|
|
299
|
+
totalPages: 0,
|
|
300
|
+
totalItems: 0,
|
|
301
|
+
itemsPerPage: defaultItemsPerPage
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
const execute = useCallback(async (params?: any) => {
|
|
306
|
+
setState(prev => ({ ...prev, loading: true, error: null }))
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const result = await apiFunction({
|
|
310
|
+
page: state.pagination.page,
|
|
311
|
+
itemsPerPage: state.pagination.itemsPerPage,
|
|
312
|
+
...params
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
setState({
|
|
316
|
+
data: result.data,
|
|
317
|
+
loading: false,
|
|
318
|
+
error: null,
|
|
319
|
+
lastFetched: new Date(),
|
|
320
|
+
pagination: result.pagination
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
apiOptions.onSuccess?.(result.data)
|
|
324
|
+
return result.data
|
|
325
|
+
} catch (error: any) {
|
|
326
|
+
const errorMessage = error?.message || 'An error occurred'
|
|
327
|
+
setState(prev => ({
|
|
328
|
+
...prev,
|
|
329
|
+
loading: false,
|
|
330
|
+
error: errorMessage
|
|
331
|
+
}))
|
|
332
|
+
apiOptions.onError?.(errorMessage)
|
|
333
|
+
throw error
|
|
334
|
+
}
|
|
335
|
+
}, [apiFunction, state.pagination.page, state.pagination.itemsPerPage, apiOptions])
|
|
336
|
+
|
|
337
|
+
const setPage = useCallback((page: number) => {
|
|
338
|
+
setState(prev => ({
|
|
339
|
+
...prev,
|
|
340
|
+
pagination: {
|
|
341
|
+
...prev.pagination,
|
|
342
|
+
page
|
|
343
|
+
}
|
|
344
|
+
}))
|
|
345
|
+
}, [])
|
|
346
|
+
|
|
347
|
+
const setItemsPerPage = useCallback((itemsPerPage: number) => {
|
|
348
|
+
setState(prev => ({
|
|
349
|
+
...prev,
|
|
350
|
+
pagination: {
|
|
351
|
+
...prev.pagination,
|
|
352
|
+
itemsPerPage,
|
|
353
|
+
page: 1 // Reset to first page when changing items per page
|
|
354
|
+
}
|
|
355
|
+
}))
|
|
356
|
+
}, [])
|
|
357
|
+
|
|
358
|
+
const nextPage = useCallback(() => {
|
|
359
|
+
if (state.pagination.page < state.pagination.totalPages) {
|
|
360
|
+
setPage(state.pagination.page + 1)
|
|
361
|
+
}
|
|
362
|
+
}, [state.pagination.page, state.pagination.totalPages, setPage])
|
|
363
|
+
|
|
364
|
+
const prevPage = useCallback(() => {
|
|
365
|
+
if (state.pagination.page > 1) {
|
|
366
|
+
setPage(state.pagination.page - 1)
|
|
367
|
+
}
|
|
368
|
+
}, [state.pagination.page, setPage])
|
|
369
|
+
|
|
370
|
+
const reset = useCallback(() => {
|
|
371
|
+
setState({
|
|
372
|
+
data: [],
|
|
373
|
+
loading: false,
|
|
374
|
+
error: null,
|
|
375
|
+
lastFetched: null,
|
|
376
|
+
pagination: {
|
|
377
|
+
page: 1,
|
|
378
|
+
totalPages: 0,
|
|
379
|
+
totalItems: 0,
|
|
380
|
+
itemsPerPage: defaultItemsPerPage
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
}, [defaultItemsPerPage])
|
|
384
|
+
|
|
385
|
+
const refetch = useCallback(() => {
|
|
386
|
+
return execute()
|
|
387
|
+
}, [execute])
|
|
388
|
+
|
|
389
|
+
const hasNextPage = state.pagination.page < state.pagination.totalPages
|
|
390
|
+
const hasPrevPage = state.pagination.page > 1
|
|
391
|
+
|
|
392
|
+
// Auto-execute when page or itemsPerPage changes
|
|
393
|
+
useEffect(() => {
|
|
394
|
+
if (apiOptions.immediate) {
|
|
395
|
+
execute()
|
|
396
|
+
}
|
|
397
|
+
}, [state.pagination.page, state.pagination.itemsPerPage, apiOptions.immediate, execute])
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
data: state.data,
|
|
401
|
+
loading: state.loading,
|
|
402
|
+
error: state.error,
|
|
403
|
+
execute,
|
|
404
|
+
reset,
|
|
405
|
+
refetch,
|
|
406
|
+
pagination: state.pagination,
|
|
407
|
+
setPage,
|
|
408
|
+
setItemsPerPage,
|
|
409
|
+
nextPage,
|
|
410
|
+
prevPage,
|
|
411
|
+
hasNextPage,
|
|
412
|
+
hasPrevPage
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ─── useMutation ─────────────────────────────────────────────────────────────
|
|
417
|
+
|
|
418
|
+
interface MutationState<T> {
|
|
419
|
+
data: T | null
|
|
420
|
+
loading: boolean
|
|
421
|
+
error: string | null
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Options for `useMutation`.
|
|
426
|
+
*
|
|
427
|
+
* @prop onSuccess - Called with the result after a successful mutation
|
|
428
|
+
* @prop onError - Called with the error message on failure
|
|
429
|
+
* @prop onSettled - Called after success or failure (always fires)
|
|
430
|
+
*/
|
|
431
|
+
interface UseMutationOptions<T, P> {
|
|
432
|
+
onSuccess?: (data: T) => void
|
|
433
|
+
onError?: (error: string) => void
|
|
434
|
+
onSettled?: () => void
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Return value of `useMutation`.
|
|
439
|
+
*
|
|
440
|
+
* @prop data - Result of the last successful mutation, or null
|
|
441
|
+
* @prop loading - True while the mutation is in flight
|
|
442
|
+
* @prop error - Error message string or null
|
|
443
|
+
* @prop mutate - Trigger the mutation with typed params
|
|
444
|
+
* @prop reset - Reset state to null/false/null
|
|
445
|
+
*/
|
|
446
|
+
interface UseMutationReturn<T, P> {
|
|
447
|
+
data: T | null
|
|
448
|
+
loading: boolean
|
|
449
|
+
error: string | null
|
|
450
|
+
mutate: (params: P) => Promise<T>
|
|
451
|
+
reset: () => void
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Write-operation hook for create, update, and delete calls. Does not
|
|
456
|
+
* auto-execute — call `mutate(params)` explicitly.
|
|
457
|
+
*
|
|
458
|
+
* @param mutationFunction - Async write function taking typed params
|
|
459
|
+
* @param options - `UseMutationOptions<T, P>`
|
|
460
|
+
* @returns `UseMutationReturn<T, P>` — data, loading, error, mutate, reset
|
|
461
|
+
*
|
|
462
|
+
* @sideEffects React state mutations; triggers `onSuccess/onError/onSettled` callbacks
|
|
463
|
+
* @calledBy useEntityRecord.ts (save and delete mutations)
|
|
464
|
+
*
|
|
465
|
+
* @example
|
|
466
|
+
* ```tsx
|
|
467
|
+
* const { mutate, loading } = useMutation(
|
|
468
|
+
* async (id: string) => apiFetch(`/api/items?action=delete&id=${id}`, { method: 'DELETE' }),
|
|
469
|
+
* { onSuccess: () => navigate('/items') }
|
|
470
|
+
* )
|
|
471
|
+
* ```
|
|
472
|
+
*/
|
|
473
|
+
export function useMutation<T, P = void>(
|
|
474
|
+
mutationFunction: (params: P) => Promise<T>,
|
|
475
|
+
options: UseMutationOptions<T, P> = {}
|
|
476
|
+
): UseMutationReturn<T, P> {
|
|
477
|
+
const { onSuccess, onError, onSettled } = options
|
|
478
|
+
|
|
479
|
+
const [state, setState] = useState<MutationState<T>>({
|
|
480
|
+
data: null,
|
|
481
|
+
loading: false,
|
|
482
|
+
error: null
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
const mutate = useCallback(async (params: P) => {
|
|
486
|
+
setState(prev => ({ ...prev, loading: true, error: null }))
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
const result = await mutationFunction(params)
|
|
490
|
+
setState({
|
|
491
|
+
data: result,
|
|
492
|
+
loading: false,
|
|
493
|
+
error: null
|
|
494
|
+
})
|
|
495
|
+
onSuccess?.(result)
|
|
496
|
+
return result
|
|
497
|
+
} catch (error: any) {
|
|
498
|
+
const errorMessage = error?.message || 'An error occurred'
|
|
499
|
+
setState(prev => ({
|
|
500
|
+
...prev,
|
|
501
|
+
loading: false,
|
|
502
|
+
error: errorMessage
|
|
503
|
+
}))
|
|
504
|
+
onError?.(errorMessage)
|
|
505
|
+
throw error
|
|
506
|
+
} finally {
|
|
507
|
+
onSettled?.()
|
|
508
|
+
}
|
|
509
|
+
}, [mutationFunction, onSuccess, onError, onSettled])
|
|
510
|
+
|
|
511
|
+
const reset = useCallback(() => {
|
|
512
|
+
setState({
|
|
513
|
+
data: null,
|
|
514
|
+
loading: false,
|
|
515
|
+
error: null
|
|
516
|
+
})
|
|
517
|
+
}, [])
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
data: state.data,
|
|
521
|
+
loading: state.loading,
|
|
522
|
+
error: state.error,
|
|
523
|
+
mutate,
|
|
524
|
+
reset
|
|
525
|
+
}
|
|
526
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { apiFetch } from '../lib/api'
|
|
3
|
+
import { useAuth } from '../contexts/AuthContext'
|
|
4
|
+
|
|
5
|
+
export interface AppRecord {
|
|
6
|
+
id: string
|
|
7
|
+
slug: string
|
|
8
|
+
name: string
|
|
9
|
+
description?: string
|
|
10
|
+
icon?: string
|
|
11
|
+
color?: string
|
|
12
|
+
version?: string
|
|
13
|
+
app_type: string
|
|
14
|
+
source: string
|
|
15
|
+
config: Record<string, any>
|
|
16
|
+
nav_items: any[]
|
|
17
|
+
min_role?: string // Deprecated: use required_roles instead
|
|
18
|
+
required_roles?: string[] // New: array of roles that can access this app
|
|
19
|
+
is_active: boolean
|
|
20
|
+
is_system: boolean
|
|
21
|
+
route_prefix: string | null
|
|
22
|
+
renderer: 'generic' | 'custom' | 'none'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface UseAppsResult {
|
|
26
|
+
apps: AppRecord[]
|
|
27
|
+
loading: boolean
|
|
28
|
+
error: string | null
|
|
29
|
+
refetch: () => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Fetches active apps for the current account and filters by user roles.
|
|
34
|
+
* Apps with renderer='none' or route_prefix=null are excluded from routing
|
|
35
|
+
* but still returned for API-only consumers.
|
|
36
|
+
*/
|
|
37
|
+
export function useApps(): UseAppsResult {
|
|
38
|
+
const { user } = useAuth()
|
|
39
|
+
const [apps, setApps] = useState<AppRecord[]>([])
|
|
40
|
+
const [loading, setLoading] = useState(true)
|
|
41
|
+
const [error, setError] = useState<string | null>(null)
|
|
42
|
+
|
|
43
|
+
const fetchApps = async () => {
|
|
44
|
+
if (!user) {
|
|
45
|
+
setApps([])
|
|
46
|
+
setLoading(false)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
setLoading(true)
|
|
52
|
+
setError(null)
|
|
53
|
+
|
|
54
|
+
const response = await apiFetch('/api/apps?action=list')
|
|
55
|
+
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new Error(`Failed to fetch apps: ${response.statusText}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const data = await response.json()
|
|
61
|
+
|
|
62
|
+
if (data.error) {
|
|
63
|
+
throw new Error(data.error)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const allApps: AppRecord[] = data.data || data || []
|
|
67
|
+
|
|
68
|
+
// Filter by user's roles: include app if user has any of the required_roles,
|
|
69
|
+
// or if app has no role restrictions
|
|
70
|
+
const accessible = allApps.filter(app => {
|
|
71
|
+
if (!app.is_active) return false
|
|
72
|
+
|
|
73
|
+
// Support both new required_roles array and legacy min_role
|
|
74
|
+
const requiredRoles = app.required_roles || (app.min_role ? [app.min_role] : [])
|
|
75
|
+
|
|
76
|
+
// No role restriction = accessible to all
|
|
77
|
+
if (requiredRoles.length === 0) return true
|
|
78
|
+
|
|
79
|
+
// User has no roles = no access (unless no restriction)
|
|
80
|
+
if (!user.roles || user.roles.length === 0) return false
|
|
81
|
+
|
|
82
|
+
// system_admin can access everything
|
|
83
|
+
if (user.roles.includes('system_admin')) return true
|
|
84
|
+
|
|
85
|
+
// Check if user has any of the required roles
|
|
86
|
+
return requiredRoles.some(role => user.roles!.includes(role))
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
setApps(accessible)
|
|
90
|
+
} catch (err) {
|
|
91
|
+
setError(err instanceof Error ? err.message : 'Failed to load apps')
|
|
92
|
+
setApps([])
|
|
93
|
+
} finally {
|
|
94
|
+
setLoading(false)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
fetchApps()
|
|
100
|
+
}, [user?.id, user?.account_id])
|
|
101
|
+
|
|
102
|
+
return { apps, loading, error, refetch: fetchApps }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Returns only routable apps (those with a route_prefix and renderer != 'none').
|
|
107
|
+
*/
|
|
108
|
+
export function useRoutableApps() {
|
|
109
|
+
const result = useApps()
|
|
110
|
+
return {
|
|
111
|
+
...result,
|
|
112
|
+
apps: result.apps.filter(app => app.route_prefix != null && app.renderer !== 'none')
|
|
113
|
+
}
|
|
114
|
+
}
|