spine-framework 0.3.124 → 0.3.126
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/cli/commands/agents.ts +35 -37
- package/.framework/cli/commands/auth.ts +35 -34
- package/.framework/cli/commands/create-app.ts +104 -40
- package/.framework/cli/commands/dev.ts +104 -69
- package/.framework/cli/commands/doctor.ts +179 -69
- package/.framework/cli/commands/generate.ts +83 -143
- package/.framework/cli/commands/init.ts +72 -34
- package/.framework/cli/commands/install-app.ts +260 -125
- package/.framework/cli/commands/items.ts +33 -44
- package/.framework/cli/commands/migrate.ts +122 -40
- package/.framework/cli/commands/migrations.ts +33 -40
- package/.framework/cli/commands/pipelines.ts +31 -39
- package/.framework/cli/commands/status.ts +47 -23
- package/.framework/cli/commands/system.ts +47 -37
- package/.framework/cli/commands/test.ts +112 -68
- package/.framework/cli/commands/validate-app.ts +215 -121
- package/.framework/cli/context.ts +112 -130
- package/.framework/cli/env-loader.ts +47 -18
- package/.framework/cli/index.ts +81 -42
- package/.framework/docs/runtime-types.md +1024 -0
- package/.framework/functions/_shared/agent-runner.ts +1296 -961
- package/.framework/functions/_shared/app-manifest.ts +82 -68
- package/.framework/functions/_shared/audit.ts +135 -83
- package/.framework/functions/_shared/{cil-state.ts → cil.ts} +158 -56
- package/.framework/functions/_shared/db.ts +80 -126
- package/.framework/functions/_shared/hash-api-key.ts +63 -0
- package/.framework/functions/_shared/index.ts +224 -335
- package/.framework/functions/_shared/kb.ts +319 -0
- package/.framework/functions/_shared/llm.ts +92 -0
- package/.framework/functions/_shared/logging.ts +288 -0
- package/.framework/functions/_shared/middleware.ts +329 -365
- package/.framework/functions/_shared/outcomes.ts +36 -0
- package/.framework/functions/_shared/permissions.ts +359 -643
- package/.framework/functions/_shared/pipeline-runner.ts +243 -316
- package/.framework/functions/_shared/principal.ts +376 -468
- package/.framework/functions/_shared/resolve-credentials.ts +102 -0
- package/.framework/functions/_shared/resolve-ids.ts +188 -16
- package/.framework/functions/_shared/retry-deliveries.ts +104 -0
- package/.framework/functions/_shared/schema-utils.ts +611 -373
- package/.framework/functions/_shared/testing.ts +207 -137
- package/.framework/functions/_shared/tracing.ts +276 -0
- package/.framework/functions/_shared/trigger-engine.ts +159 -211
- package/.framework/functions/_shared/webhook-registration.ts +95 -81
- package/.framework/functions/_shared/webhook-registry.ts +153 -75
- package/.framework/functions/account-nodes.ts +31 -53
- package/.framework/functions/adapters/generic-webhook-inbound.ts +56 -0
- package/.framework/functions/adapters/generic-webhook-outbound.ts +43 -0
- package/.framework/functions/adapters/openai-outbound.ts +40 -0
- package/.framework/functions/admin-data.ts +685 -537
- package/.framework/functions/admin-funnel.ts +31 -20
- package/.framework/functions/admin-install-funnel.ts +47 -28
- package/.framework/functions/admin-signals.ts +47 -29
- package/.framework/functions/ai-agents.ts +122 -147
- package/.framework/functions/ai-interactions.ts +180 -90
- package/.framework/functions/api-keys.ts +250 -192
- package/.framework/functions/apps.ts +203 -197
- package/.framework/functions/auth.ts +98 -91
- package/.framework/functions/cil-agent-ingest.ts +773 -0
- package/.framework/functions/cil-agent.ts +592 -0
- package/.framework/functions/cil-callmap.ts +1227 -0
- package/.framework/functions/cil-config.ts +583 -0
- package/.framework/functions/cil-context.ts +445 -0
- package/.framework/functions/cil-doc.ts +1752 -0
- package/.framework/functions/cil-embeddings.ts +1202 -0
- package/.framework/functions/cil-graph.ts +585 -0
- package/.framework/functions/cil-ingest.ts +2416 -0
- package/.framework/functions/cil-pipeline.ts +318 -0
- package/.framework/functions/cil-project.ts +499 -0
- package/.framework/functions/cil-validate.ts +469 -0
- package/.framework/functions/debug-auth.ts +73 -53
- package/.framework/functions/deliveries.ts +117 -0
- package/.framework/functions/embeddings.ts +528 -516
- package/.framework/functions/eval-harness.ts +158 -106
- package/.framework/functions/integration-outbound.ts +311 -0
- package/.framework/functions/integration-routes.ts +240 -466
- package/.framework/functions/integrations.ts +132 -140
- package/.framework/functions/invites.ts +401 -201
- package/.framework/functions/item-progress.ts +113 -53
- package/.framework/functions/kb.ts +28 -0
- package/.framework/functions/logs.ts +239 -222
- package/.framework/functions/observability.ts +308 -259
- package/.framework/functions/pipeline-executions.ts +179 -235
- package/.framework/functions/pipelines.ts +163 -205
- package/.framework/functions/prompt-configs.ts +109 -156
- package/.framework/functions/roles.ts +97 -147
- package/.framework/functions/system-cron.ts +132 -218
- package/.framework/functions/system.ts +112 -52
- package/.framework/functions/tests.ts +32 -28
- package/.framework/functions/timers.ts +162 -210
- package/.framework/functions/triggers.ts +282 -346
- package/.framework/functions/types.ts +283 -400
- package/.framework/migrations/001_core_schema.sql +630 -0
- package/.framework/migrations/002_core_rls_and_functions.sql +532 -0
- package/.framework/migrations/{003_system_data.sql → 003_core_system_data.sql} +26 -153
- package/.framework/migrations/004_core_rpc_and_broker.sql +421 -0
- package/.framework/migrations/005_core_observability.sql +598 -0
- package/.framework/migrations/006_security_hardening_rls.sql +97 -0
- package/.framework/migrations/007_kb_article_list.sql +237 -0
- package/.framework/migrations/008_items_target.sql +8 -0
- package/.framework/migrations/009_roles_slug_unique.sql +11 -0
- package/.framework/public/spine.config.json +91 -0
- package/.framework/scripts/app-install-cli.ts +112 -98
- package/.framework/src/App.tsx +67 -20
- package/.framework/src/apps/admin/index.tsx +97 -27
- package/.framework/src/components/AppWrapper.tsx +50 -11
- package/.framework/src/components/CustomAppLoader.tsx +131 -22
- package/.framework/src/components/admin/AdminListPage.tsx +75 -8
- package/.framework/src/components/admin/AdminSidebar.tsx +148 -36
- package/.framework/src/components/admin/AdminStatsCard.tsx +48 -29
- package/.framework/src/components/admin/PageInstructions.tsx +49 -0
- package/.framework/src/components/admin/SortableTableHeader.tsx +49 -2
- package/.framework/src/components/app-shell/GenericAppShell.tsx +69 -11
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +55 -10
- package/.framework/src/components/app-shell/GenericListPage.tsx +76 -9
- package/.framework/src/components/app-sidebar.tsx +50 -3
- package/.framework/src/components/auth/ProtectedRoute.tsx +53 -45
- package/.framework/src/components/kb/KBArticleEditor.tsx +409 -0
- package/.framework/src/components/kb/KBArticleList.tsx +557 -0
- package/.framework/src/components/layout/AppShell.tsx +54 -16
- package/.framework/src/components/layout/Header.tsx +50 -38
- package/.framework/src/components/layout/Layout.tsx +48 -32
- package/.framework/src/components/layout/Sidebar.tsx +167 -16
- package/.framework/src/components/runtime/DataDetailHeader.tsx +33 -1
- package/.framework/src/components/runtime/DataDetailPage.tsx +289 -4
- package/.framework/src/components/runtime/DataFilters.tsx +78 -1
- package/.framework/src/components/runtime/DataHeader.tsx +32 -29
- package/.framework/src/components/runtime/DataListPage.tsx +161 -2
- package/.framework/src/components/runtime/DataStats.tsx +48 -1
- package/.framework/src/components/runtime/DataTable.tsx +153 -1
- package/.framework/src/components/runtime/SchemaDetailForm.tsx +77 -46
- package/.framework/src/components/runtime/index.ts +16 -9
- package/.framework/src/components/search-form.tsx +33 -1
- package/.framework/src/components/shared/AgentView.tsx +48 -51
- package/.framework/src/components/shared/FieldRenderer.tsx +84 -67
- package/.framework/src/components/shared/InviteManager.tsx +113 -52
- package/.framework/src/components/shared/MemberList.tsx +182 -0
- package/.framework/src/components/shared/SchemaFields.tsx +96 -78
- package/.framework/src/components/ui/DataTable.tsx +86 -56
- package/.framework/src/components/ui/Form.tsx +188 -109
- package/.framework/src/components/ui/ItemCard.tsx +86 -58
- package/.framework/src/components/ui/ItemListView.tsx +88 -61
- package/.framework/src/components/ui/LoadingSpinner.tsx +52 -27
- package/.framework/src/components/ui/Modal.tsx +81 -4
- package/.framework/src/components/ui/RichTextEditor.tsx +101 -9
- package/.framework/src/components/ui/accordion.tsx +97 -5
- package/.framework/src/components/ui/alert-dialog.tsx +225 -13
- package/.framework/src/components/ui/alert.tsx +113 -6
- package/.framework/src/components/ui/aspect-ratio.tsx +49 -2
- package/.framework/src/components/ui/avatar.tsx +129 -7
- package/.framework/src/components/ui/badge.tsx +65 -3
- package/.framework/src/components/ui/breadcrumb.tsx +145 -8
- package/.framework/src/components/ui/button-group.tsx +81 -4
- package/.framework/src/components/ui/button.tsx +49 -2
- package/.framework/src/components/ui/calendar.tsx +65 -3
- package/.framework/src/components/ui/card.tsx +145 -8
- package/.framework/src/components/ui/carousel.tsx +129 -7
- package/.framework/src/components/ui/chart.tsx +161 -10
- package/.framework/src/components/ui/checkbox.tsx +49 -2
- package/.framework/src/components/ui/collapsible.tsx +81 -5
- package/.framework/src/components/ui/combobox.tsx +305 -20
- package/.framework/src/components/ui/command.tsx +177 -11
- package/.framework/src/components/ui/context-menu.tsx +273 -17
- package/.framework/src/components/ui/dialog.tsx +209 -27
- package/.framework/src/components/ui/direction.tsx +64 -4
- package/.framework/src/components/ui/drawer.tsx +193 -12
- package/.framework/src/components/ui/dropdown-menu.tsx +273 -18
- package/.framework/src/components/ui/empty.tsx +145 -9
- package/.framework/src/components/ui/field.tsx +209 -14
- package/.framework/src/components/ui/hover-card.tsx +81 -4
- package/.framework/src/components/ui/input-group.tsx +162 -10
- package/.framework/src/components/ui/input-otp.tsx +98 -5
- package/.framework/src/components/ui/input.tsx +49 -2
- package/.framework/src/components/ui/item.tsx +225 -13
- package/.framework/src/components/ui/kbd.tsx +65 -3
- package/.framework/src/components/ui/label.tsx +49 -2
- package/.framework/src/components/ui/menubar.tsx +289 -17
- package/.framework/src/components/ui/native-select.tsx +97 -5
- package/.framework/src/components/ui/navigation-menu.tsx +177 -10
- package/.framework/src/components/ui/pagination.tsx +161 -9
- package/.framework/src/components/ui/popover.tsx +145 -8
- package/.framework/src/components/ui/progress.tsx +49 -2
- package/.framework/src/components/ui/radio-group.tsx +65 -3
- package/.framework/src/components/ui/resizable.tsx +82 -4
- package/.framework/src/components/ui/scroll-area.tsx +65 -3
- package/.framework/src/components/ui/select.tsx +193 -11
- package/.framework/src/components/ui/separator.tsx +49 -2
- package/.framework/src/components/ui/sheet.tsx +193 -11
- package/.framework/src/components/ui/sidebar.tsx +481 -38
- package/.framework/src/components/ui/skeleton.tsx +49 -2
- package/.framework/src/components/ui/slider.tsx +49 -2
- package/.framework/src/components/ui/sonner.tsx +49 -2
- package/.framework/src/components/ui/spinner.tsx +49 -2
- package/.framework/src/components/ui/switch.tsx +49 -2
- package/.framework/src/components/ui/table-primitives.tsx +160 -33
- package/.framework/src/components/ui/table.tsx +161 -9
- package/.framework/src/components/ui/tabs.tsx +113 -6
- package/.framework/src/components/ui/textarea.tsx +49 -2
- package/.framework/src/components/ui/toggle-group.tsx +81 -4
- package/.framework/src/components/ui/toggle.tsx +65 -3
- package/.framework/src/components/ui/tooltip.tsx +97 -5
- package/.framework/src/contexts/AppContext.tsx +129 -45
- package/.framework/src/contexts/AuthContext.tsx +187 -144
- package/.framework/src/hooks/use-mobile.ts +48 -2
- package/.framework/src/hooks/useApi.ts +208 -159
- package/.framework/src/hooks/useAppPath.ts +32 -23
- package/.framework/src/hooks/useApps.ts +51 -20
- package/.framework/src/hooks/useEntityList.ts +32 -78
- package/.framework/src/hooks/useEntityRecord.ts +35 -96
- package/.framework/src/hooks/useForm.ts +32 -92
- package/.framework/src/hooks/useListSchema.ts +49 -78
- package/.framework/src/hooks/useSchemaRecord.ts +114 -105
- package/.framework/src/lib/api.ts +195 -116
- package/.framework/src/lib/supabase.ts +98 -64
- package/.framework/src/lib/utils.ts +287 -151
- package/.framework/src/main.tsx +49 -3
- package/.framework/src/pages/DashboardPage.tsx +52 -29
- package/.framework/src/pages/NotFoundPage.tsx +32 -10
- package/.framework/src/pages/SetupPage.tsx +32 -12
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +119 -10
- package/.framework/src/pages/admin/AIAgentsPage.tsx +48 -19
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +187 -17
- package/.framework/src/pages/admin/APIKeysPage.tsx +98 -22
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +113 -26
- package/.framework/src/pages/admin/AppDetailPage.tsx +125 -8
- package/.framework/src/pages/admin/AppsPage.tsx +49 -2
- package/.framework/src/pages/admin/DeliveriesPage.tsx +196 -0
- package/.framework/src/pages/admin/DesignedPage.tsx +32 -18
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +215 -484
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +116 -319
- package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +34 -9
- package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +34 -9
- package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -16
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +183 -26
- package/.framework/src/pages/admin/IntegrationsPage.tsx +48 -19
- package/.framework/src/pages/admin/InvitesPage.tsx +48 -14
- package/.framework/src/pages/admin/LogsPage.tsx +422 -75
- package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +34 -9
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +113 -24
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +206 -34
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +50 -17
- package/.framework/src/pages/admin/PipelinesPage.tsx +160 -21
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +378 -88
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +125 -16
- package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +49 -10
- package/.framework/src/pages/admin/RoleDetailPage.tsx +130 -10
- package/.framework/src/pages/admin/RolesPage.tsx +171 -18
- package/.framework/src/pages/admin/SelectTestPage.tsx +34 -9
- package/.framework/src/pages/admin/ShadcnTestPage.tsx +49 -11
- package/.framework/src/pages/admin/SimpleDashboard.tsx +49 -10
- package/.framework/src/pages/admin/TestRunDetailPage.tsx +49 -4
- package/.framework/src/pages/admin/TestingDashboard.tsx +98 -28
- package/.framework/src/pages/admin/TimerDetailPage.tsx +143 -33
- package/.framework/src/pages/admin/TimersPage.tsx +48 -20
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +118 -30
- package/.framework/src/pages/admin/TriggersPage.tsx +64 -19
- package/.framework/src/pages/admin/TypeDetailPage.tsx +138 -15
- package/.framework/src/pages/admin/TypesPage.tsx +48 -20
- package/.framework/src/pages/admin/cil/CILAgentPage.tsx +465 -0
- package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +787 -0
- package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +699 -0
- package/.framework/src/pages/admin/cil/CILFileListPage.tsx +647 -0
- package/.framework/src/pages/admin/cil/CILGraphPage.tsx +616 -0
- package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +1402 -0
- package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +614 -0
- package/.framework/src/pages/admin/cil/CILSearchPage.tsx +571 -0
- package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +731 -0
- package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
- package/.framework/src/pages/auth/AuthConfirmPage.tsx +52 -51
- package/.framework/src/pages/auth/ForgotPasswordPage.tsx +31 -22
- package/.framework/src/pages/auth/LoginPage.tsx +50 -23
- package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -15
- package/.framework/src/pages/auth/RegisterPage.tsx +281 -317
- package/.framework/src/pages/spine-framework/APIPage.tsx +33 -0
- package/.framework/src/pages/spine-framework/CLIPage.tsx +33 -0
- package/.framework/src/types/auth.ts +128 -71
- package/.framework/src/types/types.ts +373 -157
- package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +39 -0
- package/custom/.devin/skills/spine-workbench-idea/SKILL.md +36 -0
- package/custom/.devin/skills/spine-workbench-implement/SKILL.md +46 -0
- package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +28 -0
- package/custom/.devin/skills/spine-workbench-plan/SKILL.md +38 -0
- package/docs/README.md +9 -130
- package/netlify.toml +5 -2
- package/package.json +13 -25
- package/scripts/kb-ingest-docs.ts +203 -0
- package/spine.config.json +65 -4
- package/.framework/migrations/001_schema.sql +0 -425
- package/.framework/migrations/002_rls_and_functions.sql +0 -763
- package/.framework/migrations/004_enhancements.sql +0 -43
- package/.framework/migrations/005_auth_user_trigger.sql +0 -105
- package/.framework/migrations/006_fix_current_actor_id.sql +0 -21
- package/.framework/migrations/016_invites.sql +0 -27
- package/.framework/migrations/017_fix_get_account_apps.sql +0 -78
- package/.framework/migrations/018_apps_required_roles.sql +0 -17
- package/.framework/migrations/019_triggers_unique_constraint.sql +0 -13
- package/.framework/migrations/020_get_account_apps_role_filter.sql +0 -117
- package/.framework/migrations/021_app_domains.sql +0 -112
- package/.framework/migrations/022_route_prefix_domain_unique.sql +0 -21
- package/.framework/migrations/023_match_embeddings_rpc.sql +0 -78
- package/.framework/migrations/024_messages_account_id.sql +0 -9
- package/.framework/migrations/025_retype_threads_messages.sql +0 -240
- package/.framework/migrations/026_fix_rls_policies.sql +0 -47
- package/.framework/migrations/027_embedding_lifecycle.sql +0 -176
- package/.framework/migrations/028_fix_message_data_permissions.sql +0 -49
- package/.framework/migrations/029_tighten_support_triage_prompt.sql +0 -41
- package/.framework/migrations/030_messages_updated_by.sql +0 -6
- package/.framework/migrations/031_seed_upsert_constraints.sql +0 -35
- package/docs/APPS.md +0 -232
- package/docs/ARCHITECTURE.md +0 -246
- package/docs/INSTALL.md +0 -155
- package/docs/design-schema-spec.md +0 -1837
- package/docs/dev-guides/01-create-an-app.md +0 -183
- package/docs/dev-guides/02-create-a-function.md +0 -178
- package/docs/dev-guides/03-create-a-component.md +0 -178
- package/docs/dev-guides/04-webhook-handlers.md +0 -129
- package/docs/dev-guides/05-testing.md +0 -168
- package/docs/dev-guides/README.md +0 -40
- package/docs/npm-packages-plan.md +0 -288
- package/docs/spine-core--api-reference.md +0 -822
|
@@ -1,98 +1,47 @@
|
|
|
1
|
-
// ─── CHUNK_START: ADMIN_DATA_OVERVIEW ──────────────────────────────────────────────
|
|
2
|
-
/**
|
|
3
|
-
* @module admin-data
|
|
4
|
-
* @audience both
|
|
5
|
-
* @layer api-handler
|
|
6
|
-
* @stability stable
|
|
7
|
-
*
|
|
8
|
-
* Generic CRUD API for all runtime entities. Provides list, get, create,
|
|
9
|
-
* update, delete, and stats operations over a validated set of entity tables.
|
|
10
|
-
*
|
|
11
|
-
* **Routed by:** `GET/POST/PATCH/DELETE /.netlify/functions/admin-data`
|
|
12
|
-
*
|
|
13
|
-
* **Dispatch table (query params → handler):**
|
|
14
|
-
* | method | ?action | ?id present | Handler |
|
|
15
|
-
* |--------|---------|-------------|---------|
|
|
16
|
-
* | GET | list | any | `list` |
|
|
17
|
-
* | GET | get | any | `get` |
|
|
18
|
-
* | GET | stats | any | `stats` |
|
|
19
|
-
* | GET | — | yes | `get` |
|
|
20
|
-
* | GET | — | no | `list` |
|
|
21
|
-
* | POST | — | — | `create`|
|
|
22
|
-
* | PATCH | — | — | `update`|
|
|
23
|
-
* | DELETE | — | — | `remove`|
|
|
24
|
-
*
|
|
25
|
-
* **Authorization:** All DB reads/writes use `ctx.db` (RLS-scoped client).
|
|
26
|
-
* RLS policies on each table enforce account hierarchy access automatically.
|
|
27
|
-
* `adminDb` is used only for lookups that need bypass (type resolution).
|
|
28
|
-
*
|
|
29
|
-
* **Valid entities:** `accounts`, `people`, `items`, `threads`, `messages`,
|
|
30
|
-
* `links`, `attachments`, `watchers`.
|
|
31
|
-
*
|
|
32
|
-
* INVARIANT: every `create` call requires `type_id` in the body — all runtime
|
|
33
|
-
* records must reference a type.
|
|
34
|
-
* INVARIANT: trigger dispatch (create/update/delete) is fire-and-forget —
|
|
35
|
-
* trigger failures are logged but never surface to the caller.
|
|
36
|
-
* INVARIANT: all returned records are passed through `sanitizeRecordData`
|
|
37
|
-
* before being returned — field-level permission stripping is always applied.
|
|
38
|
-
*
|
|
39
|
-
* @seeAlso middleware.ts (createHandler, CoreContext)
|
|
40
|
-
* @seeAlso permissions.ts (sanitizeRecordData)
|
|
41
|
-
* @seeAlso trigger-engine.ts (fire*Triggers)
|
|
42
|
-
* @seeAlso types.ts (types table, design_schema, validation_schema)
|
|
43
|
-
*/
|
|
44
1
|
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
// CIL[BEGIN chunk_id="admin-data_module_header_1784019432" type="module_header" identity="imports" parent="none"]
|
|
11
|
+
// CIL[DOC-MACRO purpose="Import dependencies for generic CRUD operations"]
|
|
12
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Provide middleware, permissions, database access, type resolution, and triggers"]
|
|
13
|
+
// CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\".framework/functions/_shared/db.ts\",\".framework/src/pages/admin/TypeDetailPage.tsx\",\".framework/functions/_shared/principal.ts\",\".framework/functions/_shared/schema-utils.ts\",\"\\\"]"]
|
|
14
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="imported_modules,constants" errors="none" side_effects="none"]
|
|
15
|
+
// CIL[DOC-MICRO logic_summary="Imports modules and defines entity validation constants" notable_decisions="[]"]
|
|
16
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
17
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
18
|
+
// CIL[DOC-AUDIENCE audience="developer"]
|
|
19
|
+
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: custom/apps/cortex/pages/operations/OperationsDashboard.tsx, .framework/functions/_shared/db.ts, .framework/src/pages/admin/TypeDetailPage.tsx, .framework/functions/_shared/principal.ts, .framework/functions/_shared/schema-utils.ts, \."]
|
|
20
|
+
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
21
|
+
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
22
|
+
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
23
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
24
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
45
25
|
import { createHandler } from './_shared/middleware'
|
|
46
|
-
import { sanitizeRecordData } from './_shared/permissions'
|
|
26
|
+
import { sanitizeRecordData, resolveFirstSurfacePermissions, validateUpdatePermissions } from './_shared/permissions'
|
|
47
27
|
import { adminDb } from './_shared/db'
|
|
48
|
-
import {
|
|
28
|
+
import { isSystemAdmin } from './_shared/principal'
|
|
29
|
+
import { resolveTypeId, resolveTypeIds, resolveTypeRecord } from './_shared/resolve-ids'
|
|
49
30
|
import { fireCreateTriggers, fireUpdateTriggers, fireDeleteTriggers } from './_shared/trigger-engine'
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// ─── CHUNK_START: ADMIN_DATA_MODULE_HEADER ──────────────────────────────────────────────
|
|
53
|
-
/**
|
|
54
|
-
* @chunk-id ADMIN_DATA_MODULE_HEADER_1_0_0
|
|
55
|
-
* @version 1.0.0
|
|
56
|
-
* @hash TBD
|
|
57
|
-
* @macro Module Header
|
|
58
|
-
* @micro Module-level documentation with routing table and invariants
|
|
59
|
-
* @inputs none
|
|
60
|
-
* @outputs none
|
|
61
|
-
* @depends-on [none]
|
|
62
|
-
* @depended-by [all chunks]
|
|
63
|
-
* @side-effects [none]
|
|
64
|
-
* @tags admin, module, routing, invariants
|
|
65
|
-
*/
|
|
66
|
-
// ─── CHUNK_END: ADMIN_DATA_MODULE_HEADER ────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
// ─── CHUNK_START: ADMIN_DATA_IMPORTS_AND_CONSTANTS ──────────────────────────────────────────────
|
|
69
|
-
/**
|
|
70
|
-
* @chunk-id ADMIN_DATA_IMPORTS_AND_CONSTANTS_1_0_0
|
|
71
|
-
* @version 1.0.0
|
|
72
|
-
* @hash TBD
|
|
73
|
-
* @macro Imports and Constants
|
|
74
|
-
* @micro Module imports and constant definitions
|
|
75
|
-
* @inputs none
|
|
76
|
-
* @outputs none
|
|
77
|
-
* @depends-on [none]
|
|
78
|
-
* @depended-by [all handlers]
|
|
79
|
-
* @side-effects [none]
|
|
80
|
-
* @tags admin, imports, constants
|
|
81
|
-
*/
|
|
31
|
+
import { withAdminLogging } from './_shared/logging'
|
|
82
32
|
|
|
83
33
|
const PERMISSIONS_ALL = {
|
|
84
34
|
record_permissions: { all: ['create', 'read', 'update', 'delete'] },
|
|
85
35
|
fields: {}
|
|
86
36
|
}
|
|
87
37
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
*/
|
|
92
|
-
const VALID_ENTITIES = ['accounts', 'people', 'items', 'threads', 'messages', 'links', 'attachments', 'watchers', 'item_progress']
|
|
38
|
+
const VALID_ENTITIES = ['accounts', 'people', 'items', 'threads', 'messages', 'links', 'attachments', 'watchers', 'item_progress', 'logs', 'test_runs', 'observability', 'test_cases', 'types', 'link_types', 'ai_agents', 'prompt_configs']
|
|
39
|
+
|
|
40
|
+
const RAW_ENTITIES = ['logs', 'test_runs', 'observability', 'test_cases']
|
|
93
41
|
|
|
94
|
-
// Map admin-data entity names to the kind field used in the types table
|
|
95
42
|
const ENTITY_TO_KIND: Record<string, string> = {
|
|
43
|
+
accounts: 'account',
|
|
44
|
+
people: 'person',
|
|
96
45
|
items: 'item',
|
|
97
46
|
threads: 'thread',
|
|
98
47
|
messages: 'message',
|
|
@@ -101,440 +50,643 @@ const ENTITY_TO_KIND: Record<string, string> = {
|
|
|
101
50
|
watchers: 'watcher',
|
|
102
51
|
item_progress: 'item_progress'
|
|
103
52
|
}
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
*/
|
|
120
|
-
// ─── CHUNK_END: ADMIN_DATA_HANDLERS_SECTION ────────────────────────────────────────────────
|
|
121
|
-
|
|
122
|
-
// ─── CHUNK_START: ADMIN_DATA_LIST ──────────────────────────────────────────────
|
|
123
|
-
/**
|
|
124
|
-
* @chunk-id ADMIN_DATA_LIST_1_0_0
|
|
125
|
-
* @version 1.0.0
|
|
126
|
-
* @hash 780f1f0c81ea3bff78d40970ba4b03e3211d892bb883e6a099c6e0a55ee53b0c
|
|
127
|
-
* @macro Entity List Handler
|
|
128
|
-
* @micro Lists records with filtering, search, sorting, and pagination
|
|
129
|
-
* @inputs ctx: CoreContext — Request context with principal and database
|
|
130
|
-
* @inputs _body: any — Request body (unused for GET)
|
|
131
|
-
* @outputs Array of sanitized records or {data, schema, view} with view config
|
|
132
|
-
* @depends-on [createHandler, sanitizeRecordData, adminDb, getSearchField]
|
|
133
|
-
* @depended-by [Netlify function routing]
|
|
134
|
-
* @side-effects [DB queries, permission sanitization]
|
|
135
|
-
* @tags admin, crud, list, pagination, search
|
|
136
|
-
*/
|
|
53
|
+
// CIL[END chunk_id="admin-data_module_header_1784019432"]
|
|
54
|
+
// CIL[BEGIN chunk_id="admin-data_list_1784019432" type="function" identity="list" parent="none"]
|
|
55
|
+
// CIL[DOC-MACRO purpose="List entity records with filtering and pagination"]
|
|
56
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Query, filter, paginate, and sanitize entity records"]
|
|
57
|
+
// CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[\"custom/apps/portal/pages/ContentPage.tsx\",\".framework/src/pages/admin/TypeDetailPage.tsx\",\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\".framework/src/components/ui/dialog.tsx\",\".framework/functions/_shared/principal.ts\",\"\\\"]"]
|
|
58
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="sanitized_records[] or {data,schema,view}" errors="entity_required,database_error" side_effects="database_queries,permission_filtering"]
|
|
59
|
+
// CIL[DOC-MICRO logic_summary="Builds query with filters, applies pagination, sanitizes results, optionally resolves views" notable_decisions="[]"]
|
|
60
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="complex_dependencies"]
|
|
61
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
62
|
+
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: custom/apps/portal/pages/ContentPage.tsx, .framework/src/pages/admin/TypeDetailPage.tsx, custom/apps/cortex/pages/operations/OperationsDashboard.tsx, .framework/src/components/ui/dialog.tsx, .framework/functions/_shared/principal.ts, \."]
|
|
63
|
+
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
64
|
+
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
65
|
+
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
66
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
67
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
137
68
|
export const list = createHandler(async (ctx, _body) => {
|
|
138
|
-
|
|
139
|
-
|
|
69
|
+
const { entity, action, method, search, sort_field = 'created_at', sort_direction = 'desc', limit = 50, offset = 0, type_slug, type_slugs, view: viewSlug, ...filters } = ctx.query || {}
|
|
70
|
+
|
|
71
|
+
return await withAdminLogging(
|
|
72
|
+
ctx,
|
|
73
|
+
{
|
|
74
|
+
entity: entity || 'unknown',
|
|
75
|
+
action: 'list',
|
|
76
|
+
method: method || 'GET',
|
|
77
|
+
filters,
|
|
78
|
+
typeSlug: type_slug
|
|
79
|
+
},
|
|
80
|
+
async () => {
|
|
81
|
+
if (!entity || !VALID_ENTITIES.includes(entity)) {
|
|
82
|
+
throw new Error('Valid entity parameter is required')
|
|
83
|
+
}
|
|
140
84
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
85
|
+
const db = (ctx.principal?.isInternal || isSystemAdmin(ctx.principal)) ? adminDb : ctx.db
|
|
86
|
+
let query = db.from(entity).select('*')
|
|
144
87
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
88
|
+
const targetSlugs: string[] = []
|
|
89
|
+
if (type_slug) targetSlugs.push(type_slug)
|
|
90
|
+
if (type_slugs) {
|
|
91
|
+
const raw = Array.isArray(type_slugs) ? type_slugs : (type_slugs as string).split(',').map(s => s.trim()).filter(Boolean)
|
|
92
|
+
targetSlugs.push(...raw)
|
|
93
|
+
}
|
|
94
|
+
if (targetSlugs.length) {
|
|
95
|
+
const kind = ENTITY_TO_KIND[entity]
|
|
96
|
+
if (kind) {
|
|
97
|
+
try {
|
|
98
|
+
const resolved = await resolveTypeIds(targetSlugs.map(slug => ({ kind, slug })))
|
|
99
|
+
const typeIds = Object.values(resolved)
|
|
100
|
+
if (typeIds.length === 1) {
|
|
101
|
+
query = query.eq('type_id', typeIds[0])
|
|
102
|
+
} else if (typeIds.length > 1) {
|
|
103
|
+
query = query.in('type_id', typeIds)
|
|
104
|
+
}
|
|
105
|
+
} catch (err: any) {
|
|
106
|
+
if (err?.message?.includes('Type not found')) {
|
|
107
|
+
return []
|
|
108
|
+
}
|
|
109
|
+
throw err
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
148
113
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (kind) {
|
|
153
|
-
// Resolve the type ID from the slug via the shared resolver
|
|
154
|
-
const typeId = await resolveTypeId(kind, type_slug)
|
|
155
|
-
if (typeId) {
|
|
156
|
-
query = query.eq('type_id', typeId)
|
|
114
|
+
if (search) {
|
|
115
|
+
const searchField = getSearchField(entity)
|
|
116
|
+
query = query.ilike(searchField, `%${search}%`)
|
|
157
117
|
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
118
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
119
|
+
Object.entries(filters).forEach(([key, value]) => {
|
|
120
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
121
|
+
if (key === 'is_active' || key === 'is_verified' || key === 'is_primary') {
|
|
122
|
+
query = query.eq(key, value === 'true')
|
|
123
|
+
} else {
|
|
124
|
+
query = query.eq(key, value)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
})
|
|
167
128
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
129
|
+
query = query.order(sort_field, { ascending: sort_direction === 'asc' })
|
|
130
|
+
|
|
131
|
+
const { count, error: countError } = await db.from(entity)
|
|
132
|
+
.select('*', { count: 'exact', head: true })
|
|
133
|
+
|
|
134
|
+
if (countError) {
|
|
135
|
+
console.error('List count error:', countError)
|
|
136
|
+
throw new Error(countError.message || 'Database error getting count')
|
|
175
137
|
}
|
|
176
|
-
}
|
|
177
|
-
})
|
|
178
138
|
|
|
179
|
-
|
|
180
|
-
query = query.order(sort_field, { ascending: sort_direction === 'asc' })
|
|
139
|
+
query = query.range(parseInt(offset.toString()), parseInt(offset.toString()) + parseInt(limit.toString()) - 1)
|
|
181
140
|
|
|
182
|
-
|
|
183
|
-
const { count, error: countError } = await ctx.db.from(entity)
|
|
184
|
-
.select('*', { count: 'exact', head: true })
|
|
141
|
+
const { data, error: err } = await query
|
|
185
142
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
143
|
+
if (err) {
|
|
144
|
+
console.error('List query error:', err)
|
|
145
|
+
throw new Error(err.message || 'Database error listing records')
|
|
146
|
+
}
|
|
190
147
|
|
|
191
|
-
|
|
192
|
-
|
|
148
|
+
const sanitizedData = []
|
|
149
|
+
for (const record of data || []) {
|
|
150
|
+
const sanitizedRecord = await sanitizeRecordData(ctx, record, entity)
|
|
151
|
+
sanitizedData.push(sanitizedRecord)
|
|
152
|
+
}
|
|
193
153
|
|
|
194
|
-
|
|
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
|
+
}
|
|
195
168
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
169
|
+
return sanitizedData
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
})
|
|
173
|
+
// CIL[END chunk_id="admin-data_list_1784019432"]
|
|
174
|
+
// CIL[BEGIN chunk_id="admin-data_get_1784019432" type="function" identity="get" parent="none"]
|
|
175
|
+
// CIL[DOC-MACRO purpose="Get single entity record by ID"]
|
|
176
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Retrieve and sanitize individual entity record"]
|
|
177
|
+
// CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[\".framework/functions/_shared/schema-utils.ts\",\".framework/src/pages/admin/TypeDetailPage.tsx\",\".framework/src/pages/admin/RolesPage.tsx\",\"custom/apps/cortex/functions/custom_cil-embeddings.ts\",\"\\\"]"]
|
|
178
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="sanitized_record or {data,schema,view}" errors="entity_required,id_required,record_not_found" side_effects="database_query,permission_filtering"]
|
|
179
|
+
// CIL[DOC-MICRO logic_summary="Queries single record by ID, applies permission sanitization, optionally resolves view" notable_decisions="[]"]
|
|
180
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
181
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
182
|
+
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: .framework/functions/_shared/schema-utils.ts, .framework/src/pages/admin/TypeDetailPage.tsx, .framework/src/pages/admin/RolesPage.tsx, custom/apps/cortex/functions/custom_cil-embeddings.ts, \."]
|
|
183
|
+
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
184
|
+
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
185
|
+
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
186
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
187
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
188
|
+
async function attachLineage(record: any, ctx: any): Promise<void> {
|
|
189
|
+
const parentFields = ['epic_id', 'sprint_id', 'initiative_id', 'discovery_id', 'idea_id']
|
|
190
|
+
const childrenMap: Record<string, { field: string; childSlugs: string[] }> = {
|
|
191
|
+
workbench_idea: { field: 'idea_id', childSlugs: ['workbench_discovery'] },
|
|
192
|
+
workbench_discovery: { field: 'discovery_id', childSlugs: ['workbench_initiative'] },
|
|
193
|
+
workbench_initiative: { field: 'initiative_id', childSlugs: ['workbench_epic', 'workbench_work_item'] },
|
|
194
|
+
workbench_epic: { field: 'epic_id', childSlugs: ['workbench_work_item', 'workbench_sprint'] },
|
|
195
|
+
workbench_sprint: { field: 'sprint_id', childSlugs: ['workbench_work_item'] },
|
|
196
|
+
workbench_work_item: { field: '', childSlugs: [] },
|
|
199
197
|
}
|
|
200
198
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
199
|
+
async function parentIdFor(current: any, visited: Set<string>): Promise<string | null> {
|
|
200
|
+
const { data: links } = await ctx.db.from('links').select('target_id').eq('source_id', current.id).eq('link_type', 'workbench_originated_from').eq('source_type', 'items').limit(1)
|
|
201
|
+
const linkTarget = links?.[0]?.target_id
|
|
202
|
+
if (linkTarget && !visited.has(linkTarget)) return linkTarget
|
|
203
|
+
const d = current.data || {}
|
|
204
|
+
for (const key of parentFields) {
|
|
205
|
+
const id = d[key]
|
|
206
|
+
if (id && !visited.has(id)) return id
|
|
207
|
+
}
|
|
208
|
+
return null
|
|
206
209
|
}
|
|
207
210
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
211
|
+
async function getParents(record: any): Promise<any[]> {
|
|
212
|
+
const parents: any[] = []
|
|
213
|
+
const visited = new Set<string>([record.id])
|
|
214
|
+
let current: any = record
|
|
215
|
+
for (let i = 0; i < 5; i++) {
|
|
216
|
+
const parentId = await parentIdFor(current, visited)
|
|
217
|
+
if (!parentId) break
|
|
218
|
+
visited.add(parentId)
|
|
219
|
+
const { data, error } = await ctx.db.from('items').select('*').eq('id', parentId).single()
|
|
220
|
+
if (error || !data) break
|
|
221
|
+
current = data
|
|
222
|
+
const sanitized = await sanitizeRecordData(ctx, data, 'items')
|
|
223
|
+
parents.push(sanitized)
|
|
221
224
|
}
|
|
225
|
+
return parents
|
|
222
226
|
}
|
|
223
227
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
228
|
+
async function getChildren(record: any): Promise<any[]> {
|
|
229
|
+
const childIds = new Set<string>()
|
|
230
|
+
const { data: linkRows } = await ctx.db.from('links').select('source_id').eq('target_id', record.id).eq('link_type', 'workbench_originated_from').eq('target_type', 'items').limit(50)
|
|
231
|
+
for (const row of linkRows || []) {
|
|
232
|
+
if (row.source_id && row.source_id !== record.id) childIds.add(row.source_id)
|
|
233
|
+
}
|
|
234
|
+
const { data: typeRow } = await adminDb.from('types').select('slug').eq('id', record.type_id).single()
|
|
235
|
+
const cfg = typeRow?.slug ? childrenMap[typeRow.slug] : null
|
|
236
|
+
if (cfg?.childSlugs.length) {
|
|
237
|
+
try {
|
|
238
|
+
const pairs = cfg.childSlugs.map(slug => ({ kind: 'item', slug }))
|
|
239
|
+
const resolved = await resolveTypeIds(pairs)
|
|
240
|
+
const typeIds = Object.values(resolved)
|
|
241
|
+
if (typeIds.length) {
|
|
242
|
+
const { data: dataChildren } = await ctx.db.from('items').select('id').in('type_id', typeIds).eq(`data->>${cfg.field}`, record.id)
|
|
243
|
+
for (const child of dataChildren || []) {
|
|
244
|
+
if (child.id && child.id !== record.id) childIds.add(child.id)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch (e) {
|
|
248
|
+
// ignore missing child types
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (childIds.size === 0) return []
|
|
252
|
+
const { data: rawChildren } = await ctx.db.from('items').select('*').in('id', Array.from(childIds))
|
|
253
|
+
const children: any[] = []
|
|
254
|
+
for (const child of rawChildren || []) {
|
|
255
|
+
if (child.id === record.id) continue
|
|
256
|
+
children.push(await sanitizeRecordData(ctx, child, 'items'))
|
|
257
|
+
}
|
|
258
|
+
return children
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const [parents, children] = await Promise.all([getParents(record), getChildren(record)])
|
|
262
|
+
|
|
263
|
+
const typeIds = new Set<string>()
|
|
264
|
+
typeIds.add(record.type_id)
|
|
265
|
+
for (const p of parents) if (p?.type_id) typeIds.add(p.type_id)
|
|
266
|
+
for (const c of children) if (c?.type_id) typeIds.add(c.type_id)
|
|
267
|
+
const { data: typeRows } = await adminDb.from('types').select('id, slug').in('id', Array.from(typeIds))
|
|
268
|
+
const slugById = new Map((typeRows || []).map((t: any) => [t.id, t.slug] as [string, string]))
|
|
269
|
+
record.type_slug = slugById.get(record.type_id) || ''
|
|
270
|
+
for (const p of parents) p.type_slug = slugById.get(p.type_id) || ''
|
|
271
|
+
for (const c of children) c.type_slug = slugById.get(c.type_id) || ''
|
|
272
|
+
|
|
273
|
+
record.parents = parents
|
|
274
|
+
record.children = children
|
|
275
|
+
}
|
|
276
|
+
|
|
243
277
|
export const get = createHandler(async (ctx, _body) => {
|
|
244
278
|
const { entity, id, view: viewSlug } = ctx.query || {}
|
|
279
|
+
const method = ctx.query?.method || 'GET'
|
|
245
280
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
281
|
+
return await withAdminLogging(
|
|
282
|
+
ctx,
|
|
283
|
+
{
|
|
284
|
+
entity: entity || 'unknown',
|
|
285
|
+
action: 'get',
|
|
286
|
+
method,
|
|
287
|
+
typeSlug: viewSlug
|
|
288
|
+
},
|
|
289
|
+
async () => {
|
|
290
|
+
if (!entity || !VALID_ENTITIES.includes(entity)) {
|
|
291
|
+
throw new Error('Valid entity parameter is required')
|
|
292
|
+
}
|
|
249
293
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
294
|
+
if (!id) {
|
|
295
|
+
throw new Error('ID parameter is required')
|
|
296
|
+
}
|
|
253
297
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
.single()
|
|
298
|
+
const { data, error: err } = await ctx.db.from(entity)
|
|
299
|
+
.select('*')
|
|
300
|
+
.eq('id', id)
|
|
301
|
+
.single()
|
|
259
302
|
|
|
260
|
-
|
|
303
|
+
if (err) throw err
|
|
261
304
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
305
|
+
if (!data) {
|
|
306
|
+
throw new Error('Record not found')
|
|
307
|
+
}
|
|
265
308
|
|
|
266
|
-
|
|
309
|
+
const sanitizedRecord = await sanitizeRecordData(ctx, data, entity)
|
|
267
310
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
311
|
+
if (entity === 'items') {
|
|
312
|
+
await attachLineage(sanitizedRecord, ctx)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (viewSlug && sanitizedRecord?.design_schema) {
|
|
316
|
+
const schema = sanitizedRecord.design_schema
|
|
317
|
+
const resolvedView = schema.views?.[viewSlug] || null
|
|
318
|
+
return { data: sanitizedRecord, schema, view: resolvedView }
|
|
319
|
+
}
|
|
274
320
|
|
|
275
|
-
|
|
321
|
+
return sanitizedRecord
|
|
322
|
+
}
|
|
323
|
+
)
|
|
276
324
|
})
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
325
|
+
// CIL[END chunk_id="admin-data_get_1784019432"]
|
|
326
|
+
// CIL[BEGIN chunk_id="admin-data_create_1784019432" type="function" identity="create" parent="none"]
|
|
327
|
+
// CIL[DOC-LEARNING topic="runtime-type:generic/stamping" content="Every runtime record is stamped with type.design_schema and type.validation_schema from the types table. resolveTypeRecord resolves kind+slug or type_id. admin-data create inherits scope from parent, applies PERMISSIONS_ALL default when record_permissions is missing, sets system fields on the row and non-system fields in record.data, then fires triggers. This applies to all kinds: account, person, item, thread, message, link, attachment, watcher."]
|
|
328
|
+
// CIL[DOC-NOTE type="design_schema_caveat" content="If a type row lacks record_permissions, the created record receives PERMISSIONS_ALL but permissions.ts may still reduce it to id at read time if the stamped schema is later cleared. Missing scope defaults to account inheritance; ensure types define scope explicitly."]
|
|
329
|
+
// CIL[DOC-MACRO purpose="Create new entity record with type resolution"]
|
|
330
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Validate, resolve type, create record with inheritance and triggers"]
|
|
331
|
+
// CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[\".framework/src/pages/admin/TypeDetailPage.tsx\",\".framework/functions/_shared/db.ts\",\".framework/src/components/ui/dialog.tsx\",\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\".framework/functions/_shared/schema-utils.ts\",\"\\\"]"]
|
|
332
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="created_record" errors="entity_required,type_resolution_failed,parent_not_found" side_effects="database_insert,trigger_fire,permission_inheritance"]
|
|
333
|
+
// CIL[DOC-MICRO logic_summary="Resolves type, handles account inheritance, applies defaults, fires triggers" notable_decisions="[]"]
|
|
334
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="complex_dependencies"]
|
|
335
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
336
|
+
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: .framework/src/pages/admin/TypeDetailPage.tsx, .framework/functions/_shared/db.ts, .framework/src/components/ui/dialog.tsx, custom/apps/cortex/pages/operations/OperationsDashboard.tsx, .framework/functions/_shared/schema-utils.ts, \."]
|
|
337
|
+
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
338
|
+
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
339
|
+
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
340
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
341
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
294
342
|
export const create = createHandler(async (ctx, body) => {
|
|
295
343
|
const entity = body?.entity || ctx.query?.entity
|
|
296
|
-
const
|
|
344
|
+
const type_slug = body?.type_slug
|
|
345
|
+
const method = ctx.query?.method || 'POST'
|
|
346
|
+
|
|
347
|
+
return await withAdminLogging(
|
|
348
|
+
ctx,
|
|
349
|
+
{
|
|
350
|
+
entity: entity || 'unknown',
|
|
351
|
+
action: 'create',
|
|
352
|
+
method,
|
|
353
|
+
typeSlug: type_slug
|
|
354
|
+
},
|
|
355
|
+
async () => {
|
|
356
|
+
const { entity: _e, type_slug: _ts, design_schema: _ds, validation_schema: _vs, account_id: requestedAccountId, id: _id, ...recordData } = body || {}
|
|
357
|
+
|
|
358
|
+
if (!entity || !VALID_ENTITIES.includes(entity)) {
|
|
359
|
+
const e: any = new Error('Valid entity parameter is required'); e.statusCode = 400; throw e
|
|
360
|
+
}
|
|
297
361
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
362
|
+
if (RAW_ENTITIES.includes(entity)) {
|
|
363
|
+
const allowed: Record<string, string[]> = {
|
|
364
|
+
logs: ['level', 'message', 'context', 'source', 'source_type', 'source_id', 'person_id', 'account_id', 'app_id', 'created_by', 'metadata', 'created_at'],
|
|
365
|
+
test_runs: ['suite', 'status', 'started_at', 'finished_at', 'duration_ms', 'total', 'passed', 'failed', 'skipped', 'triggered_by', 'account_id'],
|
|
366
|
+
observability: ['app_id', 'account_id', 'type_id', 'metric_name', 'metric_value', 'metric_unit', 'context', 'metadata', 'created_at'],
|
|
367
|
+
test_cases: ['app_id', 'account_id', 'type_id', 'slug', 'name', 'description', 'config', 'metadata', 'is_active', 'is_system', 'created_at', 'updated_at']
|
|
368
|
+
}
|
|
369
|
+
const fields = allowed[entity] || []
|
|
370
|
+
const raw: any = {}
|
|
371
|
+
for (const key of fields) {
|
|
372
|
+
if (recordData[key] !== undefined) raw[key] = recordData[key]
|
|
373
|
+
}
|
|
374
|
+
if (fields.includes('account_id') && raw.account_id === undefined) raw.account_id = ctx.accountId
|
|
375
|
+
if (entity === 'logs' && !raw.created_at) raw.created_at = new Date().toISOString()
|
|
376
|
+
if (entity === 'logs' && (!raw.level || !raw.message)) { const e: any = new Error('level and message are required for logs'); e.statusCode = 400; throw e }
|
|
377
|
+
if (entity === 'test_runs' && !raw.suite) { const e: any = new Error('suite is required for test_runs'); e.statusCode = 400; throw e }
|
|
378
|
+
if (entity === 'observability' && (!raw.metric_name || raw.metric_value === undefined)) { const e: any = new Error('metric_name and metric_value are required for observability'); e.statusCode = 400; throw e }
|
|
379
|
+
if (entity === 'test_cases' && (!raw.slug || !raw.name)) { const e: any = new Error('slug and name are required for test_cases'); e.statusCode = 400; throw e }
|
|
380
|
+
|
|
381
|
+
const { data, error: err } = await ctx.db.from(entity).insert(raw).select().single()
|
|
382
|
+
if (err) throw err
|
|
383
|
+
fireCreateTriggers(entity, data.id, raw, ctx).catch(console.error)
|
|
384
|
+
return await sanitizeRecordData(ctx, data, entity)
|
|
385
|
+
}
|
|
301
386
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
387
|
+
let typeRecord: { id: string; design_schema: Record<string, any>; validation_schema: Record<string, any> }
|
|
388
|
+
|
|
389
|
+
if (body?.type_slug) {
|
|
390
|
+
const kind = ENTITY_TO_KIND[entity]
|
|
391
|
+
if (!kind) {
|
|
392
|
+
const e: any = new Error(`type_slug is not supported for entity: ${entity}`); e.statusCode = 400; throw e
|
|
393
|
+
}
|
|
394
|
+
typeRecord = await resolveTypeRecord(kind, body.type_slug)
|
|
395
|
+
} else if (recordData.type_id) {
|
|
396
|
+
const { data, error: typeErr } = await adminDb
|
|
397
|
+
.from('types')
|
|
398
|
+
.select('id, design_schema, validation_schema, is_active')
|
|
399
|
+
.eq('id', recordData.type_id)
|
|
400
|
+
.single()
|
|
401
|
+
if (typeErr || !data) throw new Error(`type_id not found: ${recordData.type_id}`)
|
|
402
|
+
if (!data.is_active) throw new Error(`type_id references an inactive type: ${recordData.type_id}`)
|
|
403
|
+
typeRecord = { id: data.id, design_schema: data.design_schema || {}, validation_schema: data.validation_schema || {} }
|
|
404
|
+
} else {
|
|
405
|
+
const e: any = new Error('type_id or type_slug is required — every runtime record must reference a type'); e.statusCode = 400; throw e
|
|
406
|
+
}
|
|
311
407
|
|
|
312
|
-
|
|
313
|
-
if (!typeId) {
|
|
314
|
-
const e: any = new Error('type_id or type_slug is required — every runtime record must reference a type'); e.statusCode = 400; throw e
|
|
315
|
-
}
|
|
316
|
-
recordData.type_id = typeId
|
|
408
|
+
recordData.type_id = typeRecord.id
|
|
317
409
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
.eq('id', typeId)
|
|
323
|
-
.single()
|
|
410
|
+
let designSchema = typeRecord.design_schema || {}
|
|
411
|
+
if (!designSchema.record_permissions) {
|
|
412
|
+
designSchema = { ...PERMISSIONS_ALL, ...designSchema }
|
|
413
|
+
}
|
|
324
414
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
415
|
+
let recordAccountId = requestedAccountId || ctx.accountId
|
|
416
|
+
const parentRef = recordData.thread_id || recordData.target_id || recordData.parent_id
|
|
417
|
+
if (parentRef) {
|
|
418
|
+
const parentTable = recordData.thread_id
|
|
419
|
+
? 'threads'
|
|
420
|
+
: recordData.target_type === 'account'
|
|
421
|
+
? 'accounts'
|
|
422
|
+
: recordData.target_type === 'person'
|
|
423
|
+
? 'people'
|
|
424
|
+
: 'items'
|
|
425
|
+
const parentColumns = parentTable === 'accounts' ? 'id' : 'account_id, design_schema'
|
|
426
|
+
const { data: parentRecord } = await ctx.db
|
|
427
|
+
.from(parentTable)
|
|
428
|
+
.select(parentColumns)
|
|
429
|
+
.eq('id', parentRef)
|
|
430
|
+
.maybeSingle()
|
|
431
|
+
if (!parentRecord) {
|
|
432
|
+
const e: any = new Error('Parent record not found or not accessible'); e.statusCode = 403; throw e
|
|
433
|
+
}
|
|
434
|
+
if (parentTable === 'accounts') {
|
|
435
|
+
recordAccountId = parentRecord.id
|
|
436
|
+
} else {
|
|
437
|
+
recordAccountId = parentRecord.account_id
|
|
438
|
+
const parentScope = parentRecord.design_schema?.scope
|
|
439
|
+
if (parentScope) {
|
|
440
|
+
designSchema = { ...designSchema, scope: parentScope }
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
328
444
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
445
|
+
const resolvedScope: string = designSchema.scope ?? 'account'
|
|
446
|
+
if (resolvedScope === 'platform') {
|
|
447
|
+
const { data: sysAccount } = await adminDb
|
|
448
|
+
.from('accounts')
|
|
449
|
+
.select('id')
|
|
450
|
+
.eq('slug', 'spine-system')
|
|
451
|
+
.single()
|
|
452
|
+
if (sysAccount?.id) recordAccountId = sysAccount.id
|
|
453
|
+
}
|
|
332
454
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
455
|
+
const dataToInsert: Record<string, any> = {
|
|
456
|
+
...recordData,
|
|
457
|
+
design_schema: designSchema,
|
|
458
|
+
validation_schema: typeRecord.validation_schema || {},
|
|
459
|
+
created_by: ctx.principal?.id,
|
|
460
|
+
created_at: new Date().toISOString(),
|
|
461
|
+
updated_at: new Date().toISOString()
|
|
462
|
+
}
|
|
338
463
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const parentTable = recordData.thread_id ? 'threads' : 'items'
|
|
346
|
-
const { data: parentRecord } = await ctx.db
|
|
347
|
-
.from(parentTable)
|
|
348
|
-
.select('account_id, design_schema')
|
|
349
|
-
.eq('id', parentRef)
|
|
350
|
-
.maybeSingle()
|
|
351
|
-
if (!parentRecord) {
|
|
352
|
-
const e: any = new Error('Parent record not found or not accessible'); e.statusCode = 403; throw e
|
|
353
|
-
}
|
|
354
|
-
recordAccountId = parentRecord.account_id
|
|
355
|
-
const parentScope = parentRecord.design_schema?.scope
|
|
356
|
-
if (parentScope) {
|
|
357
|
-
designSchema = { ...designSchema, scope: parentScope }
|
|
358
|
-
}
|
|
359
|
-
}
|
|
464
|
+
// accounts is the source-of-truth table; it does not have an account_id column
|
|
465
|
+
if (entity !== 'accounts') {
|
|
466
|
+
dataToInsert.account_id = recordAccountId
|
|
467
|
+
} else if (dataToInsert.parent_id === undefined) {
|
|
468
|
+
dataToInsert.parent_id = ctx.principal?.accountId || null
|
|
469
|
+
}
|
|
360
470
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
471
|
+
const permissionAccountId = entity === 'accounts' ? ctx.principal?.accountId : recordAccountId
|
|
472
|
+
const typeSlugForPermissions = type_slug || ENTITY_TO_KIND[entity]
|
|
473
|
+
const perms = await resolveFirstSurfacePermissions(
|
|
474
|
+
ctx.principal?.id,
|
|
475
|
+
permissionAccountId,
|
|
476
|
+
typeSlugForPermissions,
|
|
477
|
+
'create',
|
|
478
|
+
designSchema
|
|
479
|
+
)
|
|
480
|
+
if (!perms.canCreate) {
|
|
481
|
+
const e: any = new Error('Insufficient permissions to create this record')
|
|
482
|
+
e.statusCode = 403
|
|
483
|
+
throw e
|
|
484
|
+
}
|
|
372
485
|
|
|
373
|
-
|
|
374
|
-
const dataToInsert = {
|
|
375
|
-
...recordData,
|
|
376
|
-
design_schema: designSchema,
|
|
377
|
-
validation_schema: typeRecord.validation_schema || {},
|
|
378
|
-
created_by: ctx.principal?.id,
|
|
379
|
-
account_id: recordAccountId,
|
|
380
|
-
created_at: new Date().toISOString(),
|
|
381
|
-
updated_at: new Date().toISOString()
|
|
382
|
-
}
|
|
486
|
+
const insertDb = entity === 'accounts' ? adminDb : ctx.db
|
|
383
487
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
.single()
|
|
488
|
+
const { data, error: err } = await insertDb.from(entity)
|
|
489
|
+
.insert(dataToInsert)
|
|
490
|
+
.select()
|
|
491
|
+
.single()
|
|
389
492
|
|
|
390
|
-
|
|
493
|
+
if (err) throw err
|
|
391
494
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
fireCreateTriggers(entity, data.id, entityData, ctx).catch(console.error)
|
|
495
|
+
const entityData = { ...dataToInsert, id: data.id }
|
|
496
|
+
fireCreateTriggers(entity, data.id, entityData, ctx).catch(console.error)
|
|
395
497
|
|
|
396
|
-
|
|
498
|
+
return await sanitizeRecordData(ctx, data, entity)
|
|
499
|
+
}
|
|
500
|
+
)
|
|
397
501
|
})
|
|
398
|
-
//
|
|
399
|
-
|
|
400
|
-
//
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
* @tags admin, crud, update
|
|
414
|
-
*/
|
|
502
|
+
// CIL[END chunk_id="admin-data_create_1784019432"]
|
|
503
|
+
// CIL[BEGIN chunk_id="admin-data_update_1784019432" type="function" identity="update" parent="none"]
|
|
504
|
+
// CIL[DOC-MACRO purpose="Update existing entity record"]
|
|
505
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Patch allowed fields on entity record with triggers"]
|
|
506
|
+
// CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[\".framework/src/pages/admin/TypeDetailPage.tsx\"]"]
|
|
507
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="updated_record" errors="entity_required,id_required" side_effects="database_update,trigger_fire"]
|
|
508
|
+
// CIL[DOC-MICRO logic_summary="Filters protected fields, adds audit fields, updates record, fires triggers" notable_decisions="[]"]
|
|
509
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
510
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
511
|
+
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: .framework/src/pages/admin/TypeDetailPage.tsx."]
|
|
512
|
+
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
513
|
+
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
514
|
+
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
515
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
516
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
415
517
|
export const update = createHandler(async (ctx, body) => {
|
|
416
518
|
const { entity, id } = ctx.query || {}
|
|
417
|
-
|
|
418
|
-
const
|
|
519
|
+
const method = ctx.query?.method || 'PATCH'
|
|
520
|
+
const type_slug = body?.type_slug
|
|
521
|
+
|
|
522
|
+
return await withAdminLogging(
|
|
523
|
+
ctx,
|
|
524
|
+
{
|
|
525
|
+
entity: entity || 'unknown',
|
|
526
|
+
action: 'update',
|
|
527
|
+
method,
|
|
528
|
+
typeSlug: type_slug
|
|
529
|
+
},
|
|
530
|
+
async () => {
|
|
531
|
+
const { design_schema: _ds, validation_schema: _vs, type_id: _ti, type_slug: _ts, id: _id, account_id: requestedAccountId, ...recordData } = body || {}
|
|
532
|
+
|
|
533
|
+
if (!entity || !VALID_ENTITIES.includes(entity)) {
|
|
534
|
+
throw new Error('Valid entity parameter is required')
|
|
535
|
+
}
|
|
419
536
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
537
|
+
if (!id) {
|
|
538
|
+
throw new Error('ID is required for update')
|
|
539
|
+
}
|
|
423
540
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
541
|
+
const { data: existing, error: existingErr } = await ctx.db
|
|
542
|
+
.from(entity)
|
|
543
|
+
.select('*')
|
|
544
|
+
.eq('id', id)
|
|
545
|
+
.single()
|
|
427
546
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
547
|
+
if (existingErr || !existing) {
|
|
548
|
+
throw new Error('Record not found or access denied')
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (entity === 'people' && requestedAccountId !== undefined) {
|
|
552
|
+
recordData.account_id = requestedAccountId
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const typeSlugForPermissions = type_slug || entity
|
|
556
|
+
const validation = await validateUpdatePermissions(
|
|
557
|
+
ctx,
|
|
558
|
+
recordData,
|
|
559
|
+
existing,
|
|
560
|
+
typeSlugForPermissions
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
if (!validation.valid) {
|
|
564
|
+
const e: any = new Error(validation.error || 'Insufficient permissions to update this record')
|
|
565
|
+
e.statusCode = 403
|
|
566
|
+
throw e
|
|
567
|
+
}
|
|
434
568
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
.select()
|
|
440
|
-
.single()
|
|
569
|
+
const sanitizedUpdate = validation.sanitizedData || recordData
|
|
570
|
+
const dataToUpdate = RAW_ENTITIES.includes(entity)
|
|
571
|
+
? { ...sanitizedUpdate }
|
|
572
|
+
: { ...sanitizedUpdate, updated_by: ctx.principal?.id, updated_at: new Date().toISOString() }
|
|
441
573
|
|
|
442
|
-
|
|
574
|
+
const { data, error: err } = await ctx.db.from(entity)
|
|
575
|
+
.update(dataToUpdate)
|
|
576
|
+
.eq('id', id)
|
|
577
|
+
.select()
|
|
578
|
+
.single()
|
|
443
579
|
|
|
444
|
-
|
|
445
|
-
const entityData = { ...data, ...dataToUpdate }
|
|
446
|
-
fireUpdateTriggers(entity, id, entityData, ctx).catch(console.error)
|
|
580
|
+
if (err) throw err
|
|
447
581
|
|
|
448
|
-
|
|
582
|
+
const entityData = { ...data, ...dataToUpdate }
|
|
583
|
+
fireUpdateTriggers(entity, id, entityData, ctx).catch(console.error)
|
|
584
|
+
|
|
585
|
+
return await sanitizeRecordData(ctx, data, entity)
|
|
586
|
+
}
|
|
587
|
+
)
|
|
449
588
|
})
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
* @tags admin, crud, delete, soft-delete
|
|
466
|
-
*/
|
|
589
|
+
// CIL[END chunk_id="admin-data_update_1784019432"]
|
|
590
|
+
// CIL[BEGIN chunk_id="admin-data_remove_1784019432" type="function" identity="remove" parent="none"]
|
|
591
|
+
// CIL[DOC-MACRO purpose="Delete entity record with soft delete support"]
|
|
592
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Perform hard or soft delete with triggers"]
|
|
593
|
+
// CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[]"]
|
|
594
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="delete_confirmation" errors="entity_required,id_required" side_effects="database_delete,trigger_fire"]
|
|
595
|
+
// CIL[DOC-MICRO logic_summary="Checks soft delete support, performs appropriate delete method, fires triggers" notable_decisions="[]"]
|
|
596
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
597
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
598
|
+
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
|
|
599
|
+
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
600
|
+
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
601
|
+
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
602
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
603
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
467
604
|
export const remove = createHandler(async (ctx, _body) => {
|
|
468
605
|
const { entity, id, soft = 'true' } = ctx.query || {}
|
|
606
|
+
const method = ctx.query?.method || 'DELETE'
|
|
607
|
+
|
|
608
|
+
return await withAdminLogging(
|
|
609
|
+
ctx,
|
|
610
|
+
{
|
|
611
|
+
entity: entity || 'unknown',
|
|
612
|
+
action: 'remove',
|
|
613
|
+
method
|
|
614
|
+
},
|
|
615
|
+
async () => {
|
|
616
|
+
if (!entity || !VALID_ENTITIES.includes(entity)) {
|
|
617
|
+
throw new Error('Valid entity parameter is required')
|
|
618
|
+
}
|
|
469
619
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
620
|
+
if (!id) {
|
|
621
|
+
throw new Error('ID is required for delete')
|
|
622
|
+
}
|
|
473
623
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
624
|
+
const { data: existing, error: existingErr } = await ctx.db
|
|
625
|
+
.from(entity)
|
|
626
|
+
.select('*')
|
|
627
|
+
.eq('id', id)
|
|
628
|
+
.single()
|
|
629
|
+
|
|
630
|
+
if (existingErr || !existing) {
|
|
631
|
+
throw new Error('Record not found or access denied')
|
|
632
|
+
}
|
|
477
633
|
|
|
478
|
-
|
|
634
|
+
const permissionAccountId = existing.account_id || ctx.principal?.accountId
|
|
635
|
+
const perms = await resolveFirstSurfacePermissions(
|
|
636
|
+
ctx.principal?.id,
|
|
637
|
+
permissionAccountId,
|
|
638
|
+
entity,
|
|
639
|
+
'delete',
|
|
640
|
+
existing.design_schema
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
if (!perms.canDelete) {
|
|
644
|
+
const e: any = new Error('Insufficient permissions to delete this record')
|
|
645
|
+
e.statusCode = 403
|
|
646
|
+
throw e
|
|
647
|
+
}
|
|
479
648
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
649
|
+
const isSoftDelete = soft === 'true'
|
|
650
|
+
|
|
651
|
+
if (isSoftDelete && entitySupportsSoftDelete(entity)) {
|
|
652
|
+
const { data, error: err } = await ctx.db.from(entity)
|
|
653
|
+
.update({
|
|
654
|
+
is_active: false,
|
|
655
|
+
updated_by: ctx.principal?.id,
|
|
656
|
+
updated_at: new Date().toISOString()
|
|
657
|
+
})
|
|
658
|
+
.eq('id', id)
|
|
659
|
+
.select()
|
|
660
|
+
.single()
|
|
661
|
+
|
|
662
|
+
if (err) throw err
|
|
663
|
+
return { deleted: true, soft: true, data: await sanitizeRecordData(ctx, data, entity) }
|
|
664
|
+
} else {
|
|
665
|
+
const { error: err } = await ctx.db.from(entity)
|
|
666
|
+
.delete()
|
|
667
|
+
.eq('id', id)
|
|
668
|
+
|
|
669
|
+
if (err) throw err
|
|
670
|
+
return { deleted: true, soft: false }
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
)
|
|
505
674
|
})
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
//
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
*/
|
|
522
|
-
// ─── CHUNK_END: ADMIN_DATA_HELPERS_SECTION ────────────────────────────────────────────────
|
|
523
|
-
|
|
524
|
-
// ─── CHUNK_START: ADMIN_DATA_GET_SEARCH_FIELD ──────────────────────────────────────────────
|
|
525
|
-
/**
|
|
526
|
-
* @chunk-id ADMIN_DATA_GET_SEARCH_FIELD_1_0_0
|
|
527
|
-
* @version 1.0.0
|
|
528
|
-
* @hash 7c2615cf66a522b9b0df96cfbc8dfc50a2650016e967ceffd9172a770c78a26a
|
|
529
|
-
* @macro Search Field Resolver
|
|
530
|
-
* @micro Returns the primary display field for search functionality
|
|
531
|
-
* @inputs entity: string — Entity name from VALID_ENTITIES
|
|
532
|
-
* @outputs string — Column name to search in
|
|
533
|
-
* @depends-on [none]
|
|
534
|
-
* @depended-by [list]
|
|
535
|
-
* @side-effects [none]
|
|
536
|
-
* @tags admin, search, field-mapping
|
|
537
|
-
*/
|
|
675
|
+
// CIL[END chunk_id="admin-data_remove_1784019432"]
|
|
676
|
+
// CIL[BEGIN chunk_id="admin-data_getsearchfield_1784019432" type="function" identity="getSearchField" parent="none"]
|
|
677
|
+
// CIL[DOC-MACRO purpose="Return search field mapping for entity"]
|
|
678
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Provide field name for search functionality"]
|
|
679
|
+
// CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[]"]
|
|
680
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="search_field:string" errors="none" side_effects="none"]
|
|
681
|
+
// CIL[DOC-MICRO logic_summary="Maps entity types to their primary display fields for search" notable_decisions="[]"]
|
|
682
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
683
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
684
|
+
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
|
|
685
|
+
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
686
|
+
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
687
|
+
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
688
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
689
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
538
690
|
function getSearchField(entity: string): string {
|
|
539
691
|
const searchFields: Record<string, string> = {
|
|
540
692
|
accounts: 'display_name',
|
|
@@ -545,114 +697,110 @@ function getSearchField(entity: string): string {
|
|
|
545
697
|
links: 'link_type',
|
|
546
698
|
attachments: 'filename',
|
|
547
699
|
watchers: 'watch_type',
|
|
548
|
-
item_progress: 'title'
|
|
700
|
+
item_progress: 'title',
|
|
701
|
+
logs: 'message',
|
|
702
|
+
test_runs: 'suite',
|
|
703
|
+
observability: 'metric_name',
|
|
704
|
+
test_cases: 'name'
|
|
549
705
|
}
|
|
550
706
|
return searchFields[entity] || 'id'
|
|
551
707
|
}
|
|
552
|
-
//
|
|
553
|
-
|
|
554
|
-
//
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
*/
|
|
708
|
+
// CIL[END chunk_id="admin-data_getsearchfield_1784019432"]
|
|
709
|
+
// CIL[BEGIN chunk_id="admin-data_entitysupportssoftdelete_1784019432" type="function" identity="entitySupportsSoftDelete" parent="none"]
|
|
710
|
+
// CIL[DOC-MACRO purpose="Check if entity supports soft delete"]
|
|
711
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Determine soft delete capability for entity type"]
|
|
712
|
+
// CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[]"]
|
|
713
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="boolean:supports_soft_delete" errors="none" side_effects="none"]
|
|
714
|
+
// CIL[DOC-MICRO logic_summary="Returns true if entity has is_active column for soft delete" notable_decisions="[]"]
|
|
715
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
716
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
717
|
+
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
|
|
718
|
+
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
719
|
+
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
720
|
+
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
721
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
722
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
568
723
|
function entitySupportsSoftDelete(entity: string): boolean {
|
|
569
|
-
const softDeleteEntities = ['accounts', 'people', 'items', 'threads', 'messages', 'watchers', 'item_progress']
|
|
724
|
+
const softDeleteEntities = ['accounts', 'people', 'items', 'threads', 'messages', 'watchers', 'item_progress', 'test_cases']
|
|
570
725
|
return softDeleteEntities.includes(entity)
|
|
571
726
|
}
|
|
572
|
-
//
|
|
573
|
-
|
|
574
|
-
//
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
* @tags admin, stats, count, entity
|
|
588
|
-
*/
|
|
727
|
+
// CIL[END chunk_id="admin-data_entitysupportssoftdelete_1784019432"]
|
|
728
|
+
// CIL[BEGIN chunk_id="admin-data_stats_1784019432" type="function" identity="stats" parent="none"]
|
|
729
|
+
// CIL[DOC-MACRO purpose="Get record count for entity"]
|
|
730
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Return total record count with RLS filtering"]
|
|
731
|
+
// CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[]"]
|
|
732
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="{entity,count}" errors="entity_required,database_error" side_effects="database_count_query"]
|
|
733
|
+
// CIL[DOC-MICRO logic_summary="Performs count query with RLS filtering applied" notable_decisions="[]"]
|
|
734
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
735
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
736
|
+
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
|
|
737
|
+
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
738
|
+
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
739
|
+
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
740
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
741
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
589
742
|
export const stats = createHandler(async (ctx, _body) => {
|
|
590
743
|
const { entity } = ctx.query || {}
|
|
744
|
+
const method = ctx.query?.method || 'GET'
|
|
591
745
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
746
|
+
return await withAdminLogging(
|
|
747
|
+
ctx,
|
|
748
|
+
{
|
|
749
|
+
entity: entity || 'unknown',
|
|
750
|
+
action: 'stats',
|
|
751
|
+
method
|
|
752
|
+
},
|
|
753
|
+
async () => {
|
|
754
|
+
if (!entity || !VALID_ENTITIES.includes(entity)) {
|
|
755
|
+
throw new Error('Valid entity parameter is required')
|
|
756
|
+
}
|
|
595
757
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
.select('*', { count: 'exact', head: true })
|
|
758
|
+
const { count, error: err } = await ctx.db.from(entity)
|
|
759
|
+
.select('*', { count: 'exact', head: true })
|
|
599
760
|
|
|
600
|
-
|
|
761
|
+
if (err) throw err
|
|
601
762
|
|
|
602
|
-
|
|
763
|
+
return { entity, count }
|
|
764
|
+
}
|
|
765
|
+
)
|
|
603
766
|
})
|
|
604
|
-
//
|
|
605
|
-
|
|
606
|
-
//
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
*/
|
|
620
|
-
// ─── CHUNK_END: ADMIN_DATA_MAIN_HANDLER_SECTION ────────────────────────────────────────────────
|
|
621
|
-
|
|
622
|
-
// ─── CHUNK_START: ADMIN_DATA_HANDLER ──────────────────────────────────────────────
|
|
623
|
-
/**
|
|
624
|
-
* @chunk-id ADMIN_DATA_HANDLER_1_0_0
|
|
625
|
-
* @version 1.0.0
|
|
626
|
-
* @hash edceffa0d5c8b3c05b1a89188a274a586b6ebe8793ede427e2326876d7090df9
|
|
627
|
-
* @macro Admin Data Router
|
|
628
|
-
* @micro Routes HTTP methods and actions to appropriate CRUD handlers
|
|
629
|
-
* @inputs ctx: CoreContext — Request context with principal and database
|
|
630
|
-
* @inputs body: any — Request body for POST/PATCH operations
|
|
631
|
-
* @outputs Varies — Depends on routed handler (list/get/create/update/remove/stats)
|
|
632
|
-
* @depends-on [createHandler, list, get, create, update, remove, stats]
|
|
633
|
-
* @depended-by [Netlify function routing]
|
|
634
|
-
* @side-effects [Delegates to appropriate handler]
|
|
635
|
-
* @tags admin, router, crud, netlify-function
|
|
636
|
-
*/
|
|
767
|
+
// CIL[END chunk_id="admin-data_stats_1784019432"]
|
|
768
|
+
// CIL[BEGIN chunk_id="admin-data_handler_1784019432" type="function" identity="handler" parent="none"]
|
|
769
|
+
// CIL[DOC-MACRO purpose="Route HTTP methods to appropriate CRUD handlers"]
|
|
770
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Dispatch requests based on method and action parameters"]
|
|
771
|
+
// CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[]"]
|
|
772
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="handler_response" errors="invalid_action_or_method" side_effects="delegates_to_crud_handlers"]
|
|
773
|
+
// CIL[DOC-MICRO logic_summary="Routes GET/POST/PATCH/DELETE to list/get/create/update/remove/stats" notable_decisions="[]"]
|
|
774
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
775
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
776
|
+
// CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
|
|
777
|
+
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
778
|
+
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
779
|
+
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
780
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
781
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
637
782
|
export const handler = createHandler(async (ctx, body) => {
|
|
638
783
|
const { action } = ctx.query || {}
|
|
639
784
|
const method = ctx.query?.method || 'GET'
|
|
640
785
|
|
|
641
786
|
switch (action) {
|
|
642
787
|
case 'list':
|
|
643
|
-
if (method === 'GET')
|
|
644
|
-
return await list(ctx, body)
|
|
645
|
-
}
|
|
788
|
+
if (method === 'GET') return await list(ctx, body)
|
|
646
789
|
break
|
|
647
790
|
case 'get':
|
|
648
|
-
if (method === 'GET')
|
|
649
|
-
return await get(ctx, body)
|
|
650
|
-
}
|
|
791
|
+
if (method === 'GET') return await get(ctx, body)
|
|
651
792
|
break
|
|
652
793
|
case 'stats':
|
|
653
|
-
if (method === 'GET')
|
|
654
|
-
|
|
655
|
-
|
|
794
|
+
if (method === 'GET') return await stats(ctx, body)
|
|
795
|
+
break
|
|
796
|
+
case 'create':
|
|
797
|
+
if (method === 'POST') return await create(ctx, body)
|
|
798
|
+
break
|
|
799
|
+
case 'update':
|
|
800
|
+
if (method === 'PATCH') return await update(ctx, body)
|
|
801
|
+
break
|
|
802
|
+
case 'remove':
|
|
803
|
+
if (method === 'DELETE') return await remove(ctx, body)
|
|
656
804
|
break
|
|
657
805
|
default:
|
|
658
806
|
if (method === 'GET' && ctx.query?.id) {
|
|
@@ -670,4 +818,4 @@ export const handler = createHandler(async (ctx, body) => {
|
|
|
670
818
|
|
|
671
819
|
throw new Error('Invalid action or method')
|
|
672
820
|
})
|
|
673
|
-
//
|
|
821
|
+
// CIL[END chunk_id="admin-data_handler_1784019432"]
|