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,606 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module admin-data
|
|
3
|
+
* @audience both
|
|
4
|
+
* @layer api-handler
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* Generic CRUD API for all runtime entities. Provides list, get, create,
|
|
8
|
+
* update, delete, and stats operations over a validated set of entity tables.
|
|
9
|
+
*
|
|
10
|
+
* **Routed by:** `GET/POST/PATCH/DELETE /.netlify/functions/admin-data`
|
|
11
|
+
*
|
|
12
|
+
* **Dispatch table (query params → handler):**
|
|
13
|
+
* | method | ?action | ?id present | Handler |
|
|
14
|
+
* |--------|---------|-------------|---------|
|
|
15
|
+
* | GET | list | any | `list` |
|
|
16
|
+
* | GET | get | any | `get` |
|
|
17
|
+
* | GET | stats | any | `stats` |
|
|
18
|
+
* | GET | — | yes | `get` |
|
|
19
|
+
* | GET | — | no | `list` |
|
|
20
|
+
* | POST | — | — | `create`|
|
|
21
|
+
* | PATCH | — | — | `update`|
|
|
22
|
+
* | DELETE | — | — | `remove`|
|
|
23
|
+
*
|
|
24
|
+
* **Authorization:** All DB reads/writes use `ctx.db` (RLS-scoped client).
|
|
25
|
+
* RLS policies on each table enforce account hierarchy access automatically.
|
|
26
|
+
* `adminDb` is used only for lookups that need bypass (type resolution).
|
|
27
|
+
*
|
|
28
|
+
* **Valid entities:** `accounts`, `people`, `items`, `threads`, `messages`,
|
|
29
|
+
* `links`, `attachments`, `watchers`.
|
|
30
|
+
*
|
|
31
|
+
* INVARIANT: every `create` call requires `type_id` in the body — all runtime
|
|
32
|
+
* records must reference a type.
|
|
33
|
+
* INVARIANT: trigger dispatch (create/update/delete) is fire-and-forget —
|
|
34
|
+
* trigger failures are logged but never surface to the caller.
|
|
35
|
+
* INVARIANT: all returned records are passed through `sanitizeRecordData`
|
|
36
|
+
* before being returned — field-level permission stripping is always applied.
|
|
37
|
+
*
|
|
38
|
+
* @seeAlso middleware.ts (createHandler, CoreContext)
|
|
39
|
+
* @seeAlso permissions.ts (sanitizeRecordData)
|
|
40
|
+
* @seeAlso trigger-engine.ts (fire*Triggers)
|
|
41
|
+
* @seeAlso types.ts (types table, design_schema, validation_schema)
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { createHandler } from './_shared/middleware'
|
|
45
|
+
import { sanitizeRecordData } from './_shared/permissions'
|
|
46
|
+
import { adminDb } from './_shared/db'
|
|
47
|
+
import { fireCreateTriggers, fireUpdateTriggers, fireDeleteTriggers } from './_shared/trigger-engine'
|
|
48
|
+
|
|
49
|
+
// ─── CONSTANTS ────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
const PERMISSIONS_ALL = {
|
|
52
|
+
record_permissions: { all: ['create', 'read', 'update', 'delete'] },
|
|
53
|
+
fields: {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Allowlist of entity table names accepted by this handler.
|
|
58
|
+
* Any entity string not in this set causes a 400-equivalent throw.
|
|
59
|
+
*/
|
|
60
|
+
const VALID_ENTITIES = ['accounts', 'people', 'items', 'threads', 'messages', 'links', 'attachments', 'watchers', 'item_progress']
|
|
61
|
+
|
|
62
|
+
// ─── HANDLERS ─────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
// ─── CHUNK_START: ADMIN_DATA_LIST ──────────────────────────────────────────────
|
|
65
|
+
/**
|
|
66
|
+
* @chunk-id ADMIN_DATA_LIST_1_0_0
|
|
67
|
+
* @version 1.0.0
|
|
68
|
+
* @hash 780f1f0c81ea3bff78d40970ba4b03e3211d892bb883e6a099c6e0a55ee53b0c
|
|
69
|
+
* @macro Entity List Handler
|
|
70
|
+
* @micro Lists records with filtering, search, sorting, and pagination
|
|
71
|
+
* @inputs ctx: CoreContext — Request context with principal and database
|
|
72
|
+
* @inputs _body: any — Request body (unused for GET)
|
|
73
|
+
* @outputs Array of sanitized records or {data, schema, view} with view config
|
|
74
|
+
* @depends-on [createHandler, sanitizeRecordData, adminDb, getSearchField]
|
|
75
|
+
* @depended-by [Netlify function routing]
|
|
76
|
+
* @side-effects [DB queries, permission sanitization]
|
|
77
|
+
* @tags admin, crud, list, pagination, search
|
|
78
|
+
*/
|
|
79
|
+
export const list = createHandler(async (ctx, _body) => {
|
|
80
|
+
// Extract all reserved query params to prevent them from being used as column filters
|
|
81
|
+
const { entity, action, method, search, sort_field = 'created_at', sort_direction = 'desc', limit = 50, offset = 0, type_slug, view: viewSlug, ...filters } = ctx.query || {}
|
|
82
|
+
|
|
83
|
+
if (!entity || !VALID_ENTITIES.includes(entity)) {
|
|
84
|
+
throw new Error('Valid entity parameter is required')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Use ctx.db - RLS-scoped client based on principal
|
|
88
|
+
// RLS policies enforce account hierarchy access automatically
|
|
89
|
+
let query = ctx.db.from(entity).select('*')
|
|
90
|
+
|
|
91
|
+
// Apply type_slug filter if provided (for schema-driven entities)
|
|
92
|
+
if (type_slug && entity === 'items') {
|
|
93
|
+
// Look up the type ID from the slug
|
|
94
|
+
const { data: typeRecord } = await adminDb
|
|
95
|
+
.from('types')
|
|
96
|
+
.select('id')
|
|
97
|
+
.eq('slug', type_slug)
|
|
98
|
+
.eq('is_active', true)
|
|
99
|
+
.single()
|
|
100
|
+
|
|
101
|
+
if (typeRecord) {
|
|
102
|
+
query = query.eq('type_id', typeRecord.id)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Apply search if provided
|
|
107
|
+
if (search) {
|
|
108
|
+
// Search in display field based on entity
|
|
109
|
+
const searchField = getSearchField(entity)
|
|
110
|
+
query = query.ilike(searchField, `%${search}%`)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Apply filters
|
|
114
|
+
Object.entries(filters).forEach(([key, value]) => {
|
|
115
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
116
|
+
if (key === 'is_active' || key === 'is_verified' || key === 'is_primary') {
|
|
117
|
+
query = query.eq(key, value === 'true')
|
|
118
|
+
} else {
|
|
119
|
+
query = query.eq(key, value)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// Apply sorting
|
|
125
|
+
query = query.order(sort_field, { ascending: sort_direction === 'asc' })
|
|
126
|
+
|
|
127
|
+
// Get total count (RLS filters automatically)
|
|
128
|
+
const { count, error: countError } = await ctx.db.from(entity)
|
|
129
|
+
.select('*', { count: 'exact', head: true })
|
|
130
|
+
|
|
131
|
+
if (countError) {
|
|
132
|
+
console.error('List count error:', countError)
|
|
133
|
+
throw new Error(countError.message || 'Database error getting count')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Apply pagination
|
|
137
|
+
query = query.range(parseInt(offset.toString()), parseInt(offset.toString()) + parseInt(limit.toString()) - 1)
|
|
138
|
+
|
|
139
|
+
const { data, error: err } = await query
|
|
140
|
+
|
|
141
|
+
if (err) {
|
|
142
|
+
console.error('List query error:', err)
|
|
143
|
+
throw new Error(err.message || 'Database error listing records')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// RLS policies already filtered the data - just sanitize
|
|
147
|
+
const sanitizedData = []
|
|
148
|
+
for (const record of data || []) {
|
|
149
|
+
const sanitizedRecord = await sanitizeRecordData(ctx, record, entity)
|
|
150
|
+
sanitizedData.push(sanitizedRecord)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// If ?view=slug was requested, resolve schema + view config from the type record
|
|
154
|
+
if (viewSlug && type_slug) {
|
|
155
|
+
const { data: typeRecord } = await adminDb
|
|
156
|
+
.from('types')
|
|
157
|
+
.select('design_schema')
|
|
158
|
+
.eq('slug', type_slug)
|
|
159
|
+
.eq('is_active', true)
|
|
160
|
+
.single()
|
|
161
|
+
|
|
162
|
+
if (typeRecord?.design_schema) {
|
|
163
|
+
const schema = typeRecord.design_schema
|
|
164
|
+
const resolvedView = schema.views?.[viewSlug] || null
|
|
165
|
+
return { data: sanitizedData, schema, view: resolvedView }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return sanitizedData
|
|
170
|
+
})
|
|
171
|
+
// ─── CHUNK_END: ADMIN_DATA_LIST ────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
// ─── CHUNK_START: ADMIN_DATA_GET ──────────────────────────────────────────────
|
|
174
|
+
/**
|
|
175
|
+
* @chunk-id ADMIN_DATA_GET_1_0_0
|
|
176
|
+
* @version 1.0.0
|
|
177
|
+
* @hash 7ee27702615d7ebd144e065d399635ddf7664845685345d8a8f316449f11a7cf
|
|
178
|
+
* @macro Entity Get Handler
|
|
179
|
+
* @micro Returns a single record by ID with optional view configuration
|
|
180
|
+
* @inputs ctx: CoreContext — Request context with principal and database
|
|
181
|
+
* @inputs _body: any — Request body (unused for GET)
|
|
182
|
+
* @outputs Sanitized record or {data, schema, view} with view config
|
|
183
|
+
* @depends-on [createHandler, sanitizeRecordData]
|
|
184
|
+
* @depended-by [Netlify function routing]
|
|
185
|
+
* @side-effects [DB single row query, permission sanitization]
|
|
186
|
+
* @tags admin, crud, get, single-record
|
|
187
|
+
*/
|
|
188
|
+
export const get = createHandler(async (ctx, _body) => {
|
|
189
|
+
const { entity, id, view: viewSlug } = ctx.query || {}
|
|
190
|
+
|
|
191
|
+
if (!entity || !VALID_ENTITIES.includes(entity)) {
|
|
192
|
+
throw new Error('Valid entity parameter is required')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!id) {
|
|
196
|
+
throw new Error('ID parameter is required')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// RLS will filter based on account hierarchy access
|
|
200
|
+
const { data, error: err } = await ctx.db.from(entity)
|
|
201
|
+
.select('*')
|
|
202
|
+
.eq('id', id)
|
|
203
|
+
.single()
|
|
204
|
+
|
|
205
|
+
if (err) throw err
|
|
206
|
+
|
|
207
|
+
if (!data) {
|
|
208
|
+
throw new Error('Record not found')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const sanitizedRecord = await sanitizeRecordData(ctx, data, entity)
|
|
212
|
+
|
|
213
|
+
// If ?view=slug was requested, include schema + resolved view from the record's stamped schema
|
|
214
|
+
if (viewSlug && sanitizedRecord?.design_schema) {
|
|
215
|
+
const schema = sanitizedRecord.design_schema
|
|
216
|
+
const resolvedView = schema.views?.[viewSlug] || null
|
|
217
|
+
return { data: sanitizedRecord, schema, view: resolvedView }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return sanitizedRecord
|
|
221
|
+
})
|
|
222
|
+
// ─── CHUNK_END: ADMIN_DATA_GET ────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Creates a new record for an entity. Stamps `design_schema`,
|
|
226
|
+
* `validation_schema`, audit fields, and `account_id` from the resolved type.
|
|
227
|
+
* Fires `*_created` triggers asynchronously after DB insert.
|
|
228
|
+
*
|
|
229
|
+
* Body params:
|
|
230
|
+
* - `entity` (required) — one of VALID_ENTITIES
|
|
231
|
+
* - `type_id` (required) — UUID of an active type record
|
|
232
|
+
* - all other fields are passed through to the insert
|
|
233
|
+
*
|
|
234
|
+
* @returns Sanitized created record
|
|
235
|
+
* @throws Error('Valid entity parameter is required')
|
|
236
|
+
* @throws Error('type_id is required')
|
|
237
|
+
* @throws Error('type_id not found') — if type UUID doesn't exist
|
|
238
|
+
* @throws Error('type_id references an inactive type')
|
|
239
|
+
* @throws PostgREST error on RLS INSERT denial
|
|
240
|
+
* @inputSpec type_id: string — valid UUID of active type record
|
|
241
|
+
* @inputSpec body fields: Record<string, any> — record field values
|
|
242
|
+
* @outputSpec sanitized created record
|
|
243
|
+
* @sideEffects DB write: entity table (INSERT)
|
|
244
|
+
* @sideEffects DB read: types table (type resolution)
|
|
245
|
+
* @sideEffects fire-and-forget: fireCreateTriggers
|
|
246
|
+
* @calledBy handler (POST)
|
|
247
|
+
* @calls sanitizeRecordData, adminDb (type lookup), fireCreateTriggers
|
|
248
|
+
* @testUnit tests/unit/admin-data.test.ts — 'create'
|
|
249
|
+
* @testIntegration tests/integration/admin-data.test.ts — 'create'
|
|
250
|
+
*/
|
|
251
|
+
export const create = createHandler(async (ctx, body) => {
|
|
252
|
+
const entity = body?.entity || ctx.query?.entity
|
|
253
|
+
const { entity: _e, design_schema: _ds, validation_schema: _vs, account_id: _ai, ...recordData } = body || {}
|
|
254
|
+
|
|
255
|
+
if (!entity || !VALID_ENTITIES.includes(entity)) {
|
|
256
|
+
const e: any = new Error('Valid entity parameter is required'); e.statusCode = 400; throw e
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// type_id is required on all runtime record creation
|
|
260
|
+
if (!recordData.type_id) {
|
|
261
|
+
const e: any = new Error('type_id is required — every runtime record must reference a type'); e.statusCode = 400; throw e
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Look up the type to stamp design_schema and validation_schema
|
|
265
|
+
const { data: typeRecord, error: typeErr } = await adminDb
|
|
266
|
+
.from('types')
|
|
267
|
+
.select('id, design_schema, validation_schema, is_active')
|
|
268
|
+
.eq('id', recordData.type_id)
|
|
269
|
+
.single()
|
|
270
|
+
|
|
271
|
+
if (typeErr || !typeRecord) {
|
|
272
|
+
throw new Error(`type_id not found: ${recordData.type_id}`)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!typeRecord.is_active) {
|
|
276
|
+
throw new Error(`type_id references an inactive type: ${recordData.type_id}`)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Ensure the type has at least permissions=ALL (defensive — migration 062 guarantees this)
|
|
280
|
+
let designSchema = typeRecord.design_schema || {}
|
|
281
|
+
if (!designSchema.record_permissions) {
|
|
282
|
+
designSchema = { ...PERMISSIONS_ALL, ...designSchema }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Resolve account_id and propagate scope via parent inheritance (Option A)
|
|
286
|
+
// Priority: parent reference in body > type's own scope declaration
|
|
287
|
+
let recordAccountId = ctx.accountId
|
|
288
|
+
const parentRef = recordData.thread_id || recordData.target_id || recordData.parent_id
|
|
289
|
+
if (parentRef) {
|
|
290
|
+
const parentTable = recordData.thread_id ? 'threads' : 'items'
|
|
291
|
+
const { data: parentRecord } = await ctx.db
|
|
292
|
+
.from(parentTable)
|
|
293
|
+
.select('account_id, design_schema')
|
|
294
|
+
.eq('id', parentRef)
|
|
295
|
+
.maybeSingle()
|
|
296
|
+
if (!parentRecord) {
|
|
297
|
+
const e: any = new Error('Parent record not found or not accessible'); e.statusCode = 403; throw e
|
|
298
|
+
}
|
|
299
|
+
recordAccountId = parentRecord.account_id
|
|
300
|
+
const parentScope = parentRecord.design_schema?.scope
|
|
301
|
+
if (parentScope) {
|
|
302
|
+
designSchema = { ...designSchema, scope: parentScope }
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
const scope: string = designSchema.scope ?? 'account'
|
|
306
|
+
if (scope === 'platform') {
|
|
307
|
+
const { data: sysAccount } = await adminDb
|
|
308
|
+
.from('accounts')
|
|
309
|
+
.select('id')
|
|
310
|
+
.eq('slug', 'spine-system')
|
|
311
|
+
.single()
|
|
312
|
+
if (sysAccount?.id) recordAccountId = sysAccount.id
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Add audit fields + stamped schema
|
|
317
|
+
const dataToInsert = {
|
|
318
|
+
...recordData,
|
|
319
|
+
design_schema: designSchema,
|
|
320
|
+
validation_schema: typeRecord.validation_schema || {},
|
|
321
|
+
created_by: ctx.principal?.id,
|
|
322
|
+
account_id: recordAccountId,
|
|
323
|
+
created_at: new Date().toISOString(),
|
|
324
|
+
updated_at: new Date().toISOString()
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// RLS will check if user has INSERT permission on this account
|
|
328
|
+
const { data, error: err } = await ctx.db.from(entity)
|
|
329
|
+
.insert(dataToInsert)
|
|
330
|
+
.select()
|
|
331
|
+
.single()
|
|
332
|
+
|
|
333
|
+
if (err) throw err
|
|
334
|
+
|
|
335
|
+
// Fire triggers asynchronously (don't block response)
|
|
336
|
+
const entityData = { ...dataToInsert, id: data.id }
|
|
337
|
+
fireCreateTriggers(entity, data.id, entityData, ctx).catch(console.error)
|
|
338
|
+
|
|
339
|
+
return await sanitizeRecordData(ctx, data, entity)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Updates an existing record by ID. Stamps `updated_by` and `updated_at`.
|
|
344
|
+
* Fires `*_updated` triggers asynchronously after DB update.
|
|
345
|
+
*
|
|
346
|
+
* Query params: `entity` (required), `id` (required)
|
|
347
|
+
* Body: partial record fields to update (no schema re-stamping on update)
|
|
348
|
+
*
|
|
349
|
+
* @returns Sanitized updated record
|
|
350
|
+
* @throws Error('Valid entity parameter is required')
|
|
351
|
+
* @throws Error('ID is required for update')
|
|
352
|
+
* @throws PostgREST error on RLS UPDATE denial
|
|
353
|
+
* @inputSpec id: string — valid UUID of existing record
|
|
354
|
+
* @inputSpec body: Partial<Record> — fields to patch
|
|
355
|
+
* @outputSpec sanitized updated record
|
|
356
|
+
* @sideEffects DB write: entity table (UPDATE)
|
|
357
|
+
* @sideEffects fire-and-forget: fireUpdateTriggers
|
|
358
|
+
* @calledBy handler (PATCH)
|
|
359
|
+
* @calls sanitizeRecordData, fireUpdateTriggers
|
|
360
|
+
* @testUnit tests/unit/admin-data.test.ts — 'update'
|
|
361
|
+
*/
|
|
362
|
+
export const update = createHandler(async (ctx, body) => {
|
|
363
|
+
const { entity, id } = ctx.query || {}
|
|
364
|
+
// Strip server-only fields — client must never override these on update
|
|
365
|
+
const { design_schema: _ds, validation_schema: _vs, account_id: _ai, type_id: _ti, ...recordData } = body || {}
|
|
366
|
+
|
|
367
|
+
if (!entity || !VALID_ENTITIES.includes(entity)) {
|
|
368
|
+
throw new Error('Valid entity parameter is required')
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (!id) {
|
|
372
|
+
throw new Error('ID is required for update')
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Add audit fields
|
|
376
|
+
const dataToUpdate = {
|
|
377
|
+
...recordData,
|
|
378
|
+
updated_by: ctx.principal?.id,
|
|
379
|
+
updated_at: new Date().toISOString()
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// RLS will check UPDATE permission on this record
|
|
383
|
+
const { data, error: err } = await ctx.db.from(entity)
|
|
384
|
+
.update(dataToUpdate)
|
|
385
|
+
.eq('id', id)
|
|
386
|
+
.select()
|
|
387
|
+
.single()
|
|
388
|
+
|
|
389
|
+
if (err) throw err
|
|
390
|
+
|
|
391
|
+
// Fire triggers asynchronously (don't block response)
|
|
392
|
+
const entityData = { ...data, ...dataToUpdate }
|
|
393
|
+
fireUpdateTriggers(entity, id, entityData, ctx).catch(console.error)
|
|
394
|
+
|
|
395
|
+
return await sanitizeRecordData(ctx, data, entity)
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Deletes a record by ID. Defaults to soft delete (`is_active = false`) for
|
|
400
|
+
* entities that support it; falls back to hard delete otherwise.
|
|
401
|
+
*
|
|
402
|
+
* Query params:
|
|
403
|
+
* - `entity` (required) — one of VALID_ENTITIES
|
|
404
|
+
* - `id` (required) — UUID of the record
|
|
405
|
+
* - `soft` (default: 'true') — set to 'false' to force hard delete
|
|
406
|
+
*
|
|
407
|
+
* Soft-delete-capable entities: `accounts`, `people`, `items`, `threads`,
|
|
408
|
+
* `messages`, `watchers`. All others always receive a hard delete.
|
|
409
|
+
*
|
|
410
|
+
* @returns `{ deleted: true, soft: true, data }` (soft) or `{ deleted: true, soft: false }` (hard)
|
|
411
|
+
* @throws Error('Valid entity parameter is required')
|
|
412
|
+
* @throws Error('ID is required for delete')
|
|
413
|
+
* @throws PostgREST error on RLS DELETE denial
|
|
414
|
+
* @inputSpec id: string — valid UUID
|
|
415
|
+
* @inputSpec soft: 'true' | 'false'
|
|
416
|
+
* @outputSpec { deleted: boolean, soft: boolean, data?: sanitizedRecord }
|
|
417
|
+
* @sideEffects DB write: UPDATE is_active=false (soft) or DELETE (hard)
|
|
418
|
+
* @sideEffects fire-and-forget: fireDeleteTriggers
|
|
419
|
+
* @calledBy handler (DELETE)
|
|
420
|
+
* @calls sanitizeRecordData, entitySupportsSoftDelete, fireDeleteTriggers
|
|
421
|
+
* @testUnit tests/unit/admin-data.test.ts — 'remove'
|
|
422
|
+
*/
|
|
423
|
+
export const remove = createHandler(async (ctx, _body) => {
|
|
424
|
+
const { entity, id, soft = 'true' } = ctx.query || {}
|
|
425
|
+
|
|
426
|
+
if (!entity || !VALID_ENTITIES.includes(entity)) {
|
|
427
|
+
throw new Error('Valid entity parameter is required')
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!id) {
|
|
431
|
+
throw new Error('ID is required for delete')
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const isSoftDelete = soft === 'true'
|
|
435
|
+
|
|
436
|
+
if (isSoftDelete && entitySupportsSoftDelete(entity)) {
|
|
437
|
+
// Soft delete - set is_active to false
|
|
438
|
+
// RLS will check DELETE permission on this record
|
|
439
|
+
const { data, error: err } = await ctx.db.from(entity)
|
|
440
|
+
.update({
|
|
441
|
+
is_active: false,
|
|
442
|
+
updated_by: ctx.principal?.id,
|
|
443
|
+
updated_at: new Date().toISOString()
|
|
444
|
+
})
|
|
445
|
+
.eq('id', id)
|
|
446
|
+
.select()
|
|
447
|
+
.single()
|
|
448
|
+
|
|
449
|
+
if (err) throw err
|
|
450
|
+
return { deleted: true, soft: true, data: await sanitizeRecordData(ctx, data, entity) }
|
|
451
|
+
} else {
|
|
452
|
+
// Hard delete
|
|
453
|
+
// RLS will check DELETE permission on this record
|
|
454
|
+
const { error: err } = await ctx.db.from(entity)
|
|
455
|
+
.delete()
|
|
456
|
+
.eq('id', id)
|
|
457
|
+
|
|
458
|
+
if (err) throw err
|
|
459
|
+
return { deleted: true, soft: false }
|
|
460
|
+
}
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
// ─── HELPERS ─────────────────────────────────────────────────────────────────
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Returns the primary text field to use for `ilike` search for a given entity.
|
|
467
|
+
* Falls back to 'id' for unmapped entities.
|
|
468
|
+
*
|
|
469
|
+
* @calledBy list (search param handling)
|
|
470
|
+
*/
|
|
471
|
+
// ─── CHUNK_START: ADMIN_DATA_GET_SEARCH_FIELD ──────────────────────────────────────────────
|
|
472
|
+
/**
|
|
473
|
+
* @chunk-id ADMIN_DATA_GET_SEARCH_FIELD_1_0_0
|
|
474
|
+
* @version 1.0.0
|
|
475
|
+
* @hash 7c2615cf66a522b9b0df96cfbc8dfc50a2650016e967ceffd9172a770c78a26a
|
|
476
|
+
* @macro Search Field Resolver
|
|
477
|
+
* @micro Returns the primary display field for search functionality
|
|
478
|
+
* @inputs entity: string — Entity name from VALID_ENTITIES
|
|
479
|
+
* @outputs string — Column name to search in
|
|
480
|
+
* @depends-on [none]
|
|
481
|
+
* @depended-by [list]
|
|
482
|
+
* @side-effects [none]
|
|
483
|
+
* @tags admin, search, field-mapping
|
|
484
|
+
*/
|
|
485
|
+
function getSearchField(entity: string): string {
|
|
486
|
+
const searchFields: Record<string, string> = {
|
|
487
|
+
accounts: 'display_name',
|
|
488
|
+
people: 'full_name',
|
|
489
|
+
items: 'title',
|
|
490
|
+
threads: 'title',
|
|
491
|
+
messages: 'content',
|
|
492
|
+
links: 'link_type',
|
|
493
|
+
attachments: 'filename',
|
|
494
|
+
watchers: 'watch_type',
|
|
495
|
+
item_progress: 'title'
|
|
496
|
+
}
|
|
497
|
+
return searchFields[entity] || 'id'
|
|
498
|
+
}
|
|
499
|
+
// ─── CHUNK_END: ADMIN_DATA_GET_SEARCH_FIELD ────────────────────────────────────────────────
|
|
500
|
+
|
|
501
|
+
// ─── CHUNK_START: ADMIN_DATA_ENTITY_SUPPORTS_SOFT_DELETE ──────────────────────────────────────────────
|
|
502
|
+
/**
|
|
503
|
+
* @chunk-id ADMIN_DATA_ENTITY_SUPPORTS_SOFT_DELETE_1_0_0
|
|
504
|
+
* @version 1.0.0
|
|
505
|
+
* @hash 0438e650a11bfb4c25b90939780127d8912bdc5caeb772fe3049381d6497412d
|
|
506
|
+
* @macro Soft Delete Support Checker
|
|
507
|
+
* @micro Returns true if entity supports soft delete via is_active column
|
|
508
|
+
* @inputs entity: string — Entity name from VALID_ENTITIES
|
|
509
|
+
* @outputs boolean — True if entity has is_active column
|
|
510
|
+
* @depends-on [none]
|
|
511
|
+
* @depended-by [remove]
|
|
512
|
+
* @side-effects [none]
|
|
513
|
+
* @tags admin, soft-delete, entity-check
|
|
514
|
+
*/
|
|
515
|
+
function entitySupportsSoftDelete(entity: string): boolean {
|
|
516
|
+
const softDeleteEntities = ['accounts', 'people', 'items', 'threads', 'messages', 'watchers', 'item_progress']
|
|
517
|
+
return softDeleteEntities.includes(entity)
|
|
518
|
+
}
|
|
519
|
+
// ─── CHUNK_END: ADMIN_DATA_ENTITY_SUPPORTS_SOFT_DELETE ────────────────────────────────────────────────
|
|
520
|
+
|
|
521
|
+
// ─── CHUNK_START: ADMIN_DATA_STATS ──────────────────────────────────────────────
|
|
522
|
+
/**
|
|
523
|
+
* @chunk-id ADMIN_DATA_STATS_1_0_0
|
|
524
|
+
* @version 1.0.0
|
|
525
|
+
* @hash 0bed523a260ab3959cb4e7150cca044f4e97ce91d7ae3386ab14b5f4182d74d7
|
|
526
|
+
* @macro Entity Stats Handler
|
|
527
|
+
* @micro Returns total record count for an entity scoped by account
|
|
528
|
+
* @inputs ctx: CoreContext — Request context with principal and database
|
|
529
|
+
* @inputs _body: any — Request body (unused for GET)
|
|
530
|
+
* @outputs {entity: string, count: number} — Entity name and record count
|
|
531
|
+
* @depends-on [createHandler]
|
|
532
|
+
* @depended-by [Netlify function routing]
|
|
533
|
+
* @side-effects [DB count query with RLS filtering]
|
|
534
|
+
* @tags admin, stats, count, entity
|
|
535
|
+
*/
|
|
536
|
+
export const stats = createHandler(async (ctx, _body) => {
|
|
537
|
+
const { entity } = ctx.query || {}
|
|
538
|
+
|
|
539
|
+
if (!entity || !VALID_ENTITIES.includes(entity)) {
|
|
540
|
+
throw new Error('Valid entity parameter is required')
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// RLS will filter count based on account hierarchy access
|
|
544
|
+
const { count, error: err } = await ctx.db.from(entity)
|
|
545
|
+
.select('*', { count: 'exact', head: true })
|
|
546
|
+
|
|
547
|
+
if (err) throw err
|
|
548
|
+
|
|
549
|
+
return { entity, count }
|
|
550
|
+
})
|
|
551
|
+
// ─── CHUNK_END: ADMIN_DATA_STATS ────────────────────────────────────────────────
|
|
552
|
+
|
|
553
|
+
// ─── MAIN HANDLER ────────────────────────────────────────────────────────────
|
|
554
|
+
|
|
555
|
+
// ─── CHUNK_START: ADMIN_DATA_HANDLER ──────────────────────────────────────────────
|
|
556
|
+
/**
|
|
557
|
+
* @chunk-id ADMIN_DATA_HANDLER_1_0_0
|
|
558
|
+
* @version 1.0.0
|
|
559
|
+
* @hash edceffa0d5c8b3c05b1a89188a274a586b6ebe8793ede427e2326876d7090df9
|
|
560
|
+
* @macro Admin Data Router
|
|
561
|
+
* @micro Routes HTTP methods and actions to appropriate CRUD handlers
|
|
562
|
+
* @inputs ctx: CoreContext — Request context with principal and database
|
|
563
|
+
* @inputs body: any — Request body for POST/PATCH operations
|
|
564
|
+
* @outputs Varies — Depends on routed handler (list/get/create/update/remove/stats)
|
|
565
|
+
* @depends-on [createHandler, list, get, create, update, remove, stats]
|
|
566
|
+
* @depended-by [Netlify function routing]
|
|
567
|
+
* @side-effects [Delegates to appropriate handler]
|
|
568
|
+
* @tags admin, router, crud, netlify-function
|
|
569
|
+
*/
|
|
570
|
+
export const handler = createHandler(async (ctx, body) => {
|
|
571
|
+
const { action } = ctx.query || {}
|
|
572
|
+
const method = ctx.query?.method || 'GET'
|
|
573
|
+
|
|
574
|
+
switch (action) {
|
|
575
|
+
case 'list':
|
|
576
|
+
if (method === 'GET') {
|
|
577
|
+
return await list(ctx, body)
|
|
578
|
+
}
|
|
579
|
+
break
|
|
580
|
+
case 'get':
|
|
581
|
+
if (method === 'GET') {
|
|
582
|
+
return await get(ctx, body)
|
|
583
|
+
}
|
|
584
|
+
break
|
|
585
|
+
case 'stats':
|
|
586
|
+
if (method === 'GET') {
|
|
587
|
+
return await stats(ctx, body)
|
|
588
|
+
}
|
|
589
|
+
break
|
|
590
|
+
default:
|
|
591
|
+
if (method === 'GET' && ctx.query?.id) {
|
|
592
|
+
return await get(ctx, body)
|
|
593
|
+
} else if (method === 'GET') {
|
|
594
|
+
return await list(ctx, body)
|
|
595
|
+
} else if (method === 'POST') {
|
|
596
|
+
return await create(ctx, body)
|
|
597
|
+
} else if (method === 'PATCH') {
|
|
598
|
+
return await update(ctx, body)
|
|
599
|
+
} else if (method === 'DELETE') {
|
|
600
|
+
return await remove(ctx, body)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
throw new Error('Invalid action or method')
|
|
605
|
+
})
|
|
606
|
+
// ─── CHUNK_END: ADMIN_DATA_HANDLER ────────────────────────────────────────────────
|