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,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module src/hooks/useSchemaRecord
|
|
3
|
+
* @audience installer
|
|
4
|
+
* @layer frontend-hook
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* Schema-record data management hooks. Two complementary hooks:
|
|
8
|
+
*
|
|
9
|
+
* - **`useSchemaRecord`** — given an already-fetched record and its schema
|
|
10
|
+
* type, returns structured `FieldDefinition[]`, a managed `data` state
|
|
11
|
+
* seeded from `record.data` with schema defaults, and field setters.
|
|
12
|
+
* Used on detail/edit pages where the record is fetched by a parent hook.
|
|
13
|
+
*
|
|
14
|
+
* - **`useTypeSelection`** — fetches all types of a given `kind` and exposes
|
|
15
|
+
* a `selectedTypeId` state. Used on create pages for the type-first flow
|
|
16
|
+
* where the user picks a type before filling in fields.
|
|
17
|
+
*
|
|
18
|
+
* **Data seeding invariant:** `useSchemaRecord` re-seeds `data` state only
|
|
19
|
+
* when `record.id` or `schemaType.id` changes, not on every render. This
|
|
20
|
+
* prevents losing unsaved edits during re-renders.
|
|
21
|
+
*
|
|
22
|
+
* @seeAlso src/hooks/useEntityRecord.ts (fetches the record passed here)
|
|
23
|
+
* @seeAlso src/hooks/useForm.ts (consumes FieldDefinition[] from this hook)
|
|
24
|
+
* @seeAlso src/types/types.ts (FieldDefinition, ItemType)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
28
|
+
import { apiFetch } from '../lib/api'
|
|
29
|
+
import { FieldDefinition, ItemType } from '../types/types'
|
|
30
|
+
|
|
31
|
+
// ─── TYPES ───────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A record from the `types` table as loaded for schema resolution.
|
|
35
|
+
* @prop design_schema - Full design schema including fields and views
|
|
36
|
+
*/
|
|
37
|
+
export interface SchemaType {
|
|
38
|
+
id: string
|
|
39
|
+
name: string
|
|
40
|
+
slug: string
|
|
41
|
+
kind?: string
|
|
42
|
+
icon?: string
|
|
43
|
+
color?: string
|
|
44
|
+
app_id?: string
|
|
45
|
+
design_schema: ItemType['design_schema']
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A generic record that may carry its resolved type in a nested `type` join.
|
|
50
|
+
* @prop data - The JSONB `data` column; all custom field values live here
|
|
51
|
+
*/
|
|
52
|
+
export interface SchemaRecord {
|
|
53
|
+
id: string
|
|
54
|
+
type_id?: string
|
|
55
|
+
type?: SchemaType
|
|
56
|
+
data: Record<string, any>
|
|
57
|
+
[key: string]: any
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Options for `useSchemaRecord`. */
|
|
61
|
+
export interface UseSchemaRecordOptions {
|
|
62
|
+
typeApiKind: string // e.g. 'account', 'person', 'item'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Return value of `useSchemaRecord`.
|
|
67
|
+
*
|
|
68
|
+
* @prop fields - Ordered `FieldDefinition[]` from schema; empty if no schema
|
|
69
|
+
* @prop data - Managed field values (seeded from `record.data` + defaults)
|
|
70
|
+
* @prop setData - Replace the entire data map
|
|
71
|
+
* @prop setField - Update a single field by name
|
|
72
|
+
* @prop schemaType - The resolved schema type (passed through)
|
|
73
|
+
* @prop loading - Always false (data is derived, not fetched here)
|
|
74
|
+
* @prop error - Always null
|
|
75
|
+
*/
|
|
76
|
+
export interface UseSchemaRecordResult {
|
|
77
|
+
fields: FieldDefinition[]
|
|
78
|
+
data: Record<string, any>
|
|
79
|
+
setData: (data: Record<string, any>) => void
|
|
80
|
+
setField: (name: string, value: any) => void
|
|
81
|
+
schemaType: SchemaType | null
|
|
82
|
+
loading: boolean
|
|
83
|
+
error: string | null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── useSchemaRecord ─────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Given an already-fetched record and its resolved schema type, returns
|
|
90
|
+
* structured field definitions and a managed data state for the record's
|
|
91
|
+
* `.data` JSONB column, with schema defaults seeded on load.
|
|
92
|
+
*
|
|
93
|
+
* Does NOT fetch anything — this is a pure derived-state hook.
|
|
94
|
+
*
|
|
95
|
+
* @param record - Fetched record (or null while loading)
|
|
96
|
+
* @param schemaType - Resolved schema type (or null while loading)
|
|
97
|
+
* @returns `UseSchemaRecordResult`
|
|
98
|
+
*
|
|
99
|
+
* @inputSpec record.data: Record<string, any> | undefined — custom field values
|
|
100
|
+
* @inputSpec schemaType.design_schema.fields: FieldDefinition map
|
|
101
|
+
* @outputSpec fields: FieldDefinition[] — ordered array with `.name` injected
|
|
102
|
+
* @sideEffects React state mutations (data re-seed on record/type change only)
|
|
103
|
+
* @calledBy DataDetailPage.tsx, create pages
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```tsx
|
|
107
|
+
* const { fields, data, setField } = useSchemaRecord(record, schemaType)
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export function useSchemaRecord(
|
|
111
|
+
record: SchemaRecord | null,
|
|
112
|
+
schemaType: SchemaType | null
|
|
113
|
+
): UseSchemaRecordResult {
|
|
114
|
+
const [data, setDataState] = useState<Record<string, any>>({})
|
|
115
|
+
|
|
116
|
+
// Re-seed data state only when the record id or schema type id changes
|
|
117
|
+
const recordId = record?.id ?? null
|
|
118
|
+
const schemaTypeId = schemaType?.id ?? null
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!record) return
|
|
122
|
+
|
|
123
|
+
const base = record.data ? { ...record.data } : {}
|
|
124
|
+
|
|
125
|
+
// Seed defaults for fields that have no value
|
|
126
|
+
if (schemaType?.design_schema?.fields) {
|
|
127
|
+
for (const [name, field] of Object.entries(schemaType.design_schema.fields)) {
|
|
128
|
+
if (base[name] === undefined && (field as any).defaultValue !== undefined) {
|
|
129
|
+
base[name] = (field as any).defaultValue
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
setDataState(base)
|
|
135
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
136
|
+
}, [recordId, schemaTypeId])
|
|
137
|
+
|
|
138
|
+
const setData = useCallback((newData: Record<string, any>) => {
|
|
139
|
+
setDataState(newData)
|
|
140
|
+
}, [])
|
|
141
|
+
|
|
142
|
+
const setField = useCallback((name: string, value: any) => {
|
|
143
|
+
setDataState(prev => ({ ...prev, [name]: value }))
|
|
144
|
+
}, [])
|
|
145
|
+
|
|
146
|
+
const fields: FieldDefinition[] = schemaType?.design_schema?.fields
|
|
147
|
+
? Object.entries(schemaType.design_schema.fields).map(([name, field]) => ({ ...field, name }))
|
|
148
|
+
: []
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
fields,
|
|
152
|
+
data,
|
|
153
|
+
setData,
|
|
154
|
+
setField,
|
|
155
|
+
schemaType,
|
|
156
|
+
loading: false,
|
|
157
|
+
error: null
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─── useTypeSelection ─────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Return value of `useTypeSelection`.
|
|
165
|
+
*
|
|
166
|
+
* @prop types - All active types of the requested kind
|
|
167
|
+
* @prop selectedType - The full `SchemaType` for `selectedTypeId`, or null
|
|
168
|
+
* @prop selectedTypeId - Controlled string state for the `<select>` value
|
|
169
|
+
* @prop setSelectedTypeId - Setter for `selectedTypeId`
|
|
170
|
+
* @prop loading - True while fetching types
|
|
171
|
+
* @prop error - Error message or null
|
|
172
|
+
*/
|
|
173
|
+
export interface UseTypeSelectionResult {
|
|
174
|
+
types: SchemaType[]
|
|
175
|
+
selectedType: SchemaType | null
|
|
176
|
+
selectedTypeId: string
|
|
177
|
+
setSelectedTypeId: (id: string) => void
|
|
178
|
+
loading: boolean
|
|
179
|
+
error: string | null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Fetches all active types of a given `kind` and exposes a controlled
|
|
184
|
+
* `selectedTypeId` state for the type-first create flow.
|
|
185
|
+
*
|
|
186
|
+
* @param kind - Type kind: `'account'`, `'person'`, `'item'`, etc.
|
|
187
|
+
* @returns `UseTypeSelectionResult`
|
|
188
|
+
*
|
|
189
|
+
* @inputSpec kind: string — passed as `?kind=<kind>&is_active=true` to `/api/types`
|
|
190
|
+
* @outputSpec types: SchemaType[] — active types; empty while loading
|
|
191
|
+
* @sideEffects Network request via apiFetch on mount and kind change
|
|
192
|
+
* @calledBy Create pages (e.g. AccountCreatePage, ItemCreatePage)
|
|
193
|
+
*/
|
|
194
|
+
export function useTypeSelection(kind: string): UseTypeSelectionResult {
|
|
195
|
+
const [types, setTypes] = useState<SchemaType[]>([])
|
|
196
|
+
const [selectedTypeId, setSelectedTypeId] = useState('')
|
|
197
|
+
const [loading, setLoading] = useState(true)
|
|
198
|
+
const [error, setError] = useState<string | null>(null)
|
|
199
|
+
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
let cancelled = false
|
|
202
|
+
setLoading(true)
|
|
203
|
+
apiFetch(`/api/types?action=list&kind=${kind}&is_active=true`)
|
|
204
|
+
.then(r => r.json())
|
|
205
|
+
.then(result => {
|
|
206
|
+
if (!cancelled) {
|
|
207
|
+
setTypes(result.data || [])
|
|
208
|
+
setLoading(false)
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
.catch(err => {
|
|
212
|
+
if (!cancelled) {
|
|
213
|
+
setError(err.message || 'Failed to load types')
|
|
214
|
+
setLoading(false)
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
return () => { cancelled = true }
|
|
218
|
+
}, [kind])
|
|
219
|
+
|
|
220
|
+
const selectedType = types.find(t => t.id === selectedTypeId) ?? null
|
|
221
|
+
|
|
222
|
+
return { types, selectedType, selectedTypeId, setSelectedTypeId, loading, error }
|
|
223
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
|
|
2
|
+
|
|
3
|
+
@tailwind base;
|
|
4
|
+
@tailwind components;
|
|
5
|
+
@tailwind utilities;
|
|
6
|
+
|
|
7
|
+
@layer base {
|
|
8
|
+
:root {
|
|
9
|
+
--background: 0 0% 100%;
|
|
10
|
+
--foreground: 240 10% 3.9%;
|
|
11
|
+
--card: 0 0% 100%;
|
|
12
|
+
--card-foreground: 240 10% 3.9%;
|
|
13
|
+
--popover: 0 0% 100%;
|
|
14
|
+
--popover-foreground: 240 10% 3.9%;
|
|
15
|
+
--primary: 221.2 83.2% 53.3%;
|
|
16
|
+
--primary-foreground: 210 40% 98%;
|
|
17
|
+
--secondary: 240 4.8% 95.9%;
|
|
18
|
+
--secondary-foreground: 240 5.9% 10%;
|
|
19
|
+
--muted: 240 4.8% 95.9%;
|
|
20
|
+
--muted-foreground: 240 3.8% 46.1%;
|
|
21
|
+
--accent: 240 4.8% 95.9%;
|
|
22
|
+
--accent-foreground: 240 5.9% 10%;
|
|
23
|
+
--destructive: 0 84.2% 60.2%;
|
|
24
|
+
--destructive-foreground: 0 0% 98%;
|
|
25
|
+
--border: 240 5.9% 90%;
|
|
26
|
+
--input: 240 5.9% 90%;
|
|
27
|
+
--ring: 221.2 83.2% 53.3%;
|
|
28
|
+
--radius: 0.3rem;
|
|
29
|
+
--chart-1: 221.2 83.2% 53.3%;
|
|
30
|
+
--chart-2: 212 96% 78%;
|
|
31
|
+
--chart-3: 199 98% 48%;
|
|
32
|
+
--chart-4: 245 58% 51%;
|
|
33
|
+
--chart-5: 258 60% 44%;
|
|
34
|
+
--sidebar: 0 0% 98%;
|
|
35
|
+
--sidebar-foreground: 240 5.3% 26.1%;
|
|
36
|
+
--sidebar-primary: 221.2 83.2% 53.3%;
|
|
37
|
+
--sidebar-primary-foreground: 0 0% 100%;
|
|
38
|
+
--sidebar-accent: 240 4.8% 95.9%;
|
|
39
|
+
--sidebar-accent-foreground: 240 5.9% 10%;
|
|
40
|
+
--sidebar-border: 220 13% 91%;
|
|
41
|
+
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.dark {
|
|
45
|
+
--background: 240 10% 3.9%;
|
|
46
|
+
--foreground: 0 0% 98%;
|
|
47
|
+
--card: 240 10% 3.9%;
|
|
48
|
+
--card-foreground: 0 0% 98%;
|
|
49
|
+
--popover: 240 10% 3.9%;
|
|
50
|
+
--popover-foreground: 0 0% 98%;
|
|
51
|
+
--primary: 217.2 91.2% 59.8%;
|
|
52
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
53
|
+
--secondary: 240 3.7% 15.9%;
|
|
54
|
+
--secondary-foreground: 0 0% 98%;
|
|
55
|
+
--muted: 240 3.7% 15.9%;
|
|
56
|
+
--muted-foreground: 240 5% 64.9%;
|
|
57
|
+
--accent: 240 3.7% 15.9%;
|
|
58
|
+
--accent-foreground: 0 0% 98%;
|
|
59
|
+
--destructive: 0 62.8% 30.6%;
|
|
60
|
+
--destructive-foreground: 0 0% 98%;
|
|
61
|
+
--border: 240 3.7% 15.9%;
|
|
62
|
+
--input: 240 3.7% 15.9%;
|
|
63
|
+
--ring: 217.2 91.2% 59.8%;
|
|
64
|
+
--chart-1: 217.2 91.2% 59.8%;
|
|
65
|
+
--chart-2: 212 96% 78%;
|
|
66
|
+
--chart-3: 199 98% 60%;
|
|
67
|
+
--chart-4: 245 58% 61%;
|
|
68
|
+
--chart-5: 258 60% 54%;
|
|
69
|
+
--sidebar: 240 5.9% 10%;
|
|
70
|
+
--sidebar-foreground: 240 4.8% 95.9%;
|
|
71
|
+
--sidebar-primary: 224.3 76.3% 48%;
|
|
72
|
+
--sidebar-primary-foreground: 0 0% 100%;
|
|
73
|
+
--sidebar-accent: 240 3.7% 15.9%;
|
|
74
|
+
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
|
75
|
+
--sidebar-border: 240 3.7% 15.9%;
|
|
76
|
+
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@layer base {
|
|
81
|
+
* {
|
|
82
|
+
@apply border-border;
|
|
83
|
+
}
|
|
84
|
+
html, body, #root {
|
|
85
|
+
height: 100%;
|
|
86
|
+
}
|
|
87
|
+
body {
|
|
88
|
+
@apply bg-background text-foreground;
|
|
89
|
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
90
|
+
font-feature-settings: "rlig" 1, "calt" 1;
|
|
91
|
+
-webkit-font-smoothing: antialiased;
|
|
92
|
+
-moz-osx-font-smoothing: grayscale;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@layer utilities {
|
|
97
|
+
.text-balance {
|
|
98
|
+
text-wrap: balance;
|
|
99
|
+
}
|
|
100
|
+
.scrollbar-hide,
|
|
101
|
+
.no-scrollbar {
|
|
102
|
+
-ms-overflow-style: none;
|
|
103
|
+
scrollbar-width: none;
|
|
104
|
+
}
|
|
105
|
+
.scrollbar-hide::-webkit-scrollbar,
|
|
106
|
+
.no-scrollbar::-webkit-scrollbar {
|
|
107
|
+
display: none;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Tailwind v4 CSS variable shorthand — sidebar layout */
|
|
111
|
+
.w-\(--sidebar-width\) { width: var(--sidebar-width); }
|
|
112
|
+
.w-\(--sidebar-width-icon\) { width: var(--sidebar-width-icon); }
|
|
113
|
+
.max-w-\(--skeleton-width\) { max-width: var(--skeleton-width); }
|
|
114
|
+
|
|
115
|
+
/* Tailwind v4 outline-hidden */
|
|
116
|
+
.outline-hidden { outline: 2px solid transparent; outline-offset: 2px; }
|
|
117
|
+
|
|
118
|
+
/* Tailwind v4 !important suffix variants used in sidebar collapse */
|
|
119
|
+
[data-collapsible="icon"] .group-data-\[collapsible\=icon\]\:size-8\!,
|
|
120
|
+
[data-collapsible="icon"].group .group-data-\[collapsible\=icon\]\:size-8\! {
|
|
121
|
+
width: 2rem !important;
|
|
122
|
+
height: 2rem !important;
|
|
123
|
+
}
|
|
124
|
+
[data-collapsible="icon"] .group-data-\[collapsible\=icon\]\:p-2\!,
|
|
125
|
+
[data-collapsible="icon"].group .group-data-\[collapsible\=icon\]\:p-2\! {
|
|
126
|
+
padding: 0.5rem !important;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module src/lib/api
|
|
3
|
+
* @audience installer
|
|
4
|
+
* @layer frontend-hook
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* Authenticated fetch wrapper and account context for all Spine frontend
|
|
8
|
+
* API calls. Injects the Supabase JWT and `X-Account-Id` header into every
|
|
9
|
+
* request made via `apiFetch`.
|
|
10
|
+
*
|
|
11
|
+
* **Account context** is a module-level singleton set once after login:
|
|
12
|
+
* ```ts
|
|
13
|
+
* setAccountId(ctx.account_id) // called in AuthContext after auth.context()
|
|
14
|
+
* ```
|
|
15
|
+
* All subsequent `apiFetch` calls include `X-Account-Id` automatically.
|
|
16
|
+
*
|
|
17
|
+
* INVARIANT: Call `setAccountId` before making any authenticated API requests.
|
|
18
|
+
* If `_accountId` is null, the backend may reject scoped requests.
|
|
19
|
+
*
|
|
20
|
+
* @seeAlso src/lib/supabase.ts (supabase client used for getSession)
|
|
21
|
+
* @seeAlso src/hooks/useApi.ts (wraps apiFetch with React state management)
|
|
22
|
+
* @seeAlso src/contexts/AuthContext.tsx (calls setAccountId after login)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { supabase } from './supabase'
|
|
26
|
+
|
|
27
|
+
// ─── ACCOUNT CONTEXT ─────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
let _accountId: string | null = null
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sets the module-level account ID injected into all subsequent `apiFetch`
|
|
33
|
+
* calls as `X-Account-Id`. Pass `null` to clear (e.g. on logout).
|
|
34
|
+
*
|
|
35
|
+
* @param id - UUID of the active account, or null to clear
|
|
36
|
+
* @sideEffects Mutates module-level `_accountId`
|
|
37
|
+
* @calledBy src/contexts/AuthContext.tsx (after successful login)
|
|
38
|
+
*/
|
|
39
|
+
export function setAccountId(id: string | null) {
|
|
40
|
+
_accountId = id
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns the currently active account ID, or null if not set.
|
|
45
|
+
*
|
|
46
|
+
* @returns UUID string or null
|
|
47
|
+
* @sideEffects none
|
|
48
|
+
* @calledBy src/hooks/useApi.ts, any component needing the current account scope
|
|
49
|
+
*/
|
|
50
|
+
export function getAccountId(): string | null {
|
|
51
|
+
return _accountId
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── INTERNAL HELPERS ─────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Normalises any `HeadersInit` variant to a plain `Record<string, string>`
|
|
58
|
+
* so headers can be safely spread and merged.
|
|
59
|
+
* @throws never
|
|
60
|
+
*/
|
|
61
|
+
function normalizeHeaders(headers?: HeadersInit): Record<string, string> {
|
|
62
|
+
if (!headers) return {}
|
|
63
|
+
|
|
64
|
+
if (headers instanceof Headers) {
|
|
65
|
+
return Object.fromEntries(headers.entries())
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (Array.isArray(headers)) {
|
|
69
|
+
return Object.fromEntries(headers)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return Object.fromEntries(
|
|
73
|
+
Object.entries(headers).map(([key, value]) => [key, String(value)])
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── AUTH HEADERS ─────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Builds the auth header map for a request: `Authorization: Bearer <jwt>`
|
|
81
|
+
* and, if set, `X-Account-Id: <accountId>`.
|
|
82
|
+
*
|
|
83
|
+
* Called internally by `apiFetch` — do not call directly unless constructing
|
|
84
|
+
* a manual `fetch` outside of `apiFetch`.
|
|
85
|
+
*
|
|
86
|
+
* @returns Plain header object; empty if no active session
|
|
87
|
+
* @throws never — missing session returns empty headers, not an error
|
|
88
|
+
* @sideEffects DB read: supabase.auth.getSession (reads localStorage)
|
|
89
|
+
* @calledBy apiFetch
|
|
90
|
+
*/
|
|
91
|
+
export async function getAuthHeaders(): Promise<Record<string, string>> {
|
|
92
|
+
const headers: Record<string, string> = {}
|
|
93
|
+
|
|
94
|
+
const { data: { session } } = await supabase.auth.getSession()
|
|
95
|
+
if (session?.access_token) {
|
|
96
|
+
headers['Authorization'] = `Bearer ${session.access_token}`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (_accountId) {
|
|
100
|
+
headers['X-Account-Id'] = _accountId
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return headers
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── FETCH WRAPPER ────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Authenticated fetch wrapper. Injects `Authorization` + `X-Account-Id`
|
|
110
|
+
* headers, strips invalid Bearer values (`null`/`undefined`/empty), and
|
|
111
|
+
* forwards all other options (including `signal` for AbortController).
|
|
112
|
+
*
|
|
113
|
+
* Use this for ALL Spine API calls from the frontend. Never call `fetch`
|
|
114
|
+
* directly for API routes.
|
|
115
|
+
*
|
|
116
|
+
* @param path - API path, e.g. `'/.netlify/functions/items'`
|
|
117
|
+
* @param options - Standard `RequestInit` options (method, body, signal, etc.)
|
|
118
|
+
* @returns Raw `Response` — callers must check `response.ok` / status
|
|
119
|
+
* @throws Network errors from `fetch` (e.g. `TypeError: Failed to fetch`)
|
|
120
|
+
* @inputSpec path: string — relative URL to a Netlify function
|
|
121
|
+
* @inputSpec options.signal: AbortSignal | undefined — forwarded for cancellation
|
|
122
|
+
* @outputSpec Response — with auth headers injected; not yet parsed
|
|
123
|
+
* @sideEffects Network request; reads localStorage via getAuthHeaders
|
|
124
|
+
* @calledBy src/hooks/useApi.ts, useEntityList.ts, useEntityRecord.ts
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts
|
|
128
|
+
* const res = await apiFetch('/.netlify/functions/items?action=list')
|
|
129
|
+
* if (!res.ok) throw new Error(await res.text())
|
|
130
|
+
* const { data } = await res.json()
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export async function apiFetch(path: string, options: RequestInit = {}): Promise<Response> {
|
|
134
|
+
console.log('apiFetch called with:', { path, options, signal: options.signal })
|
|
135
|
+
const authHeaders = await getAuthHeaders()
|
|
136
|
+
const optionHeaders = normalizeHeaders(options.headers)
|
|
137
|
+
const optionAuthorization = optionHeaders.Authorization || optionHeaders.authorization
|
|
138
|
+
|
|
139
|
+
if (
|
|
140
|
+
optionAuthorization &&
|
|
141
|
+
['Bearer null', 'Bearer undefined', 'null', 'undefined', ''].includes(optionAuthorization.trim())
|
|
142
|
+
) {
|
|
143
|
+
delete optionHeaders.Authorization
|
|
144
|
+
delete optionHeaders.authorization
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const fetchOptions = {
|
|
148
|
+
...options,
|
|
149
|
+
headers: {
|
|
150
|
+
...authHeaders,
|
|
151
|
+
...optionHeaders,
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
console.log('apiFetch final options:', { fetchOptions, signal: fetchOptions.signal })
|
|
155
|
+
return fetch(path, fetchOptions)
|
|
156
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module src/lib/supabase
|
|
3
|
+
* @audience installer
|
|
4
|
+
* @layer frontend-hook
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* Browser-side Supabase client singleton and session helpers for the Spine
|
|
8
|
+
* frontend. Reads `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` from
|
|
9
|
+
* Vite env at build time; throws immediately on missing values so
|
|
10
|
+
* misconfigured builds fail loudly rather than silently.
|
|
11
|
+
*
|
|
12
|
+
* INVARIANT: `supabase` is a module-level singleton — do not call
|
|
13
|
+
* `createClient` again in components or hooks. Import this module instead.
|
|
14
|
+
*
|
|
15
|
+
* @seeAlso src/lib/api.ts (uses supabase.auth.getSession for apiFetch headers)
|
|
16
|
+
* @seeAlso src/contexts/AuthContext.tsx (wraps supabase.auth for React state)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createClient } from '@supabase/supabase-js'
|
|
20
|
+
|
|
21
|
+
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
|
|
22
|
+
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
|
|
23
|
+
|
|
24
|
+
if (!supabaseUrl || !supabaseAnonKey) {
|
|
25
|
+
throw new Error('Missing Supabase environment variables')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── CLIENT SINGLETON ─────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Browser-side Supabase client with persistent session, auto-refresh, and
|
|
32
|
+
* URL-based session detection enabled. This is the only Supabase client
|
|
33
|
+
* instance in the frontend; all auth and RLS-scoped queries flow through it.
|
|
34
|
+
*
|
|
35
|
+
* @inputSpec VITE_SUPABASE_URL: string — valid Supabase project URL
|
|
36
|
+
* @inputSpec VITE_SUPABASE_ANON_KEY: string — anon (public) key
|
|
37
|
+
* @throws Error('Missing Supabase environment variables') at module load if
|
|
38
|
+
* either env var is absent
|
|
39
|
+
* @sideEffects Reads localStorage for persisted session on construction
|
|
40
|
+
* @calledBy src/lib/api.ts (getAuthHeaders), src/contexts/AuthContext.tsx
|
|
41
|
+
*/
|
|
42
|
+
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
|
43
|
+
auth: {
|
|
44
|
+
persistSession: true,
|
|
45
|
+
autoRefreshToken: true,
|
|
46
|
+
detectSessionInUrl: true,
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// ─── SESSION HELPERS ─────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Returns the current Supabase auth session, or `null` if unauthenticated.
|
|
54
|
+
*
|
|
55
|
+
* @returns Session object or null
|
|
56
|
+
* @throws Supabase AuthError on unexpected failure
|
|
57
|
+
* @sideEffects none (read-only)
|
|
58
|
+
* @calledBy AuthContext.tsx, login flows
|
|
59
|
+
*/
|
|
60
|
+
export const getCurrentSession = async () => {
|
|
61
|
+
const { data: { session }, error } = await supabase.auth.getSession()
|
|
62
|
+
if (error) throw error
|
|
63
|
+
return session
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns the currently authenticated Supabase user, or `null` if not signed in.
|
|
68
|
+
*
|
|
69
|
+
* @returns User object or null
|
|
70
|
+
* @throws Supabase AuthError on unexpected failure
|
|
71
|
+
* @sideEffects none (read-only)
|
|
72
|
+
* @calledBy AuthContext.tsx
|
|
73
|
+
*/
|
|
74
|
+
export const getCurrentUser = async () => {
|
|
75
|
+
const { data: { user }, error } = await supabase.auth.getUser()
|
|
76
|
+
if (error) throw error
|
|
77
|
+
return user
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Forces a session token refresh. Call this when a request returns 401 due
|
|
82
|
+
* to an expired JWT. Supabase auto-refresh normally handles this, but this
|
|
83
|
+
* function is exposed for explicit refresh flows.
|
|
84
|
+
*
|
|
85
|
+
* @returns `{ session, user }` after refresh
|
|
86
|
+
* @throws Supabase AuthError if refresh fails (e.g. refresh token expired)
|
|
87
|
+
* @sideEffects Updates localStorage session via Supabase client
|
|
88
|
+
* @calledBy AuthContext.tsx (manual token refresh)
|
|
89
|
+
*/
|
|
90
|
+
export const refreshSession = async () => {
|
|
91
|
+
const { data, error } = await supabase.auth.refreshSession()
|
|
92
|
+
if (error) throw error
|
|
93
|
+
return data
|
|
94
|
+
}
|