spine-framework 0.4.3 → 0.5.0-rc.4
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/.env.example +13 -3
- package/.framework/cli/commands/bootstrap.ts +392 -0
- package/.framework/cli/commands/dev.ts +91 -218
- package/.framework/cli/commands/doctor.ts +132 -26
- package/.framework/cli/commands/generate.ts +38 -298
- package/.framework/cli/commands/init.ts +20 -1
- package/.framework/cli/commands/migrate.ts +36 -30
- package/.framework/cli/commands/migrations.ts +16 -56
- package/.framework/cli/commands/upgrade.ts +35 -0
- package/.framework/cli/commands/validate-schema.ts +147 -0
- package/.framework/cli/context.ts +223 -110
- package/.framework/cli/index.ts +6 -0
- package/.framework/cli/lib/project-files.ts +241 -1
- package/.framework/functions/_shared/agent-runner.ts +1000 -81
- package/.framework/functions/_shared/cil.ts +2 -1
- package/.framework/functions/_shared/db.ts +3 -6
- package/.framework/functions/_shared/hash-api-key.ts +183 -79
- package/.framework/functions/_shared/kb.ts +61 -7
- package/.framework/functions/_shared/logging.ts +562 -224
- package/.framework/functions/_shared/message-sequence.ts +53 -0
- package/.framework/functions/_shared/middleware.ts +47 -44
- package/.framework/functions/_shared/pipeline-runner.ts +150 -37
- package/.framework/functions/_shared/principal.ts +9 -6
- package/.framework/functions/_shared/trigger-engine.ts +149 -40
- package/.framework/functions/admin-data.ts +48 -31
- package/.framework/functions/ai-agents.ts +103 -33
- package/.framework/functions/api-keys.ts +301 -122
- package/.framework/functions/apps.ts +43 -26
- package/.framework/functions/cil-ingest.ts +48 -15
- package/.framework/functions/debug-auth.ts +14 -8
- package/.framework/functions/deliveries.ts +39 -12
- package/.framework/functions/embeddings.ts +145 -76
- package/.framework/functions/integration-outbound.ts +67 -27
- package/.framework/functions/integrations.ts +254 -127
- package/.framework/functions/logs.ts +273 -133
- package/.framework/functions/observability.ts +277 -147
- package/.framework/functions/pipeline-executions.ts +1 -1
- package/.framework/functions/pipeline-runner-background.ts +120 -0
- package/.framework/functions/pipelines.ts +14 -9
- package/.framework/functions/prompt-configs.ts +7 -4
- package/.framework/functions/roles.ts +8 -5
- package/.framework/functions/security.ts +30 -17
- package/.framework/functions/system-cron.ts +250 -128
- package/.framework/functions/tests.ts +7 -6
- package/.framework/functions/timers.ts +310 -107
- package/.framework/functions/trigger-scheduler.ts +65 -5
- package/.framework/functions/triggers.ts +28 -13
- package/.framework/functions/types.ts +13 -10
- package/.framework/migrations/064_episode_leases.sql +196 -0
- package/.framework/migrations/065_execution_tables_updated_at.sql +20 -0
- package/.framework/migrations/066_episode_steal_thread_merge.sql +148 -0
- package/.framework/migrations/067_episode_lease_stale_steal.sql +93 -0
- package/.framework/migrations/068_cron_schedule_timer_rpcs.sql +251 -0
- package/.framework/migrations/069_log_stats_window.sql +54 -0
- package/.framework/migrations/070_app_type_schema_rpcs.sql +166 -0
- package/.framework/migrations/071_timer_interval_keys.sql +96 -0
- package/.framework/migrations/072_slug_autofill.sql +77 -0
- package/.framework/migrations/073_seed_member_roles.sql +26 -0
- package/.framework/schemas/spine.config.schema.json +62 -0
- package/.framework/src/App.tsx +3 -1
- package/.framework/src/apps/admin/index.tsx +1141 -775
- package/.framework/src/components/ErrorBoundary.tsx +136 -0
- package/.framework/src/components/admin/AdminListPage.tsx +81 -73
- package/.framework/src/components/admin/AdminSidebar.tsx +92 -81
- package/.framework/src/components/admin/EntitySelect.tsx +181 -0
- package/.framework/src/components/admin/PageInstructions.tsx +86 -127
- package/.framework/src/components/app-shell/GenericAppShell.tsx +3 -1
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
- package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
- package/.framework/src/components/auth/TurnstileWidget.tsx +9 -6
- package/.framework/src/components/kb/KBArticleEditor.tsx +49 -6
- package/.framework/src/components/layout/Layout.tsx +4 -2
- package/.framework/src/components/layout/Sidebar.tsx +304 -179
- package/.framework/src/components/runtime/DataDetailPage.tsx +67 -35
- package/.framework/src/components/runtime/DataHeader.tsx +96 -48
- package/.framework/src/components/runtime/DataListPage.tsx +3 -26
- package/.framework/src/components/runtime/DataStats.tsx +10 -10
- package/.framework/src/components/runtime/DataTable.tsx +12 -10
- package/.framework/src/components/ui/sheet.tsx +24 -26
- package/.framework/src/components/ui/textarea.tsx +74 -48
- package/.framework/src/hooks/useBranding.ts +47 -0
- package/.framework/src/hooks/useEntityList.ts +12 -9
- package/.framework/src/hooks/useListSchema.ts +108 -58
- package/.framework/src/instructions/README.md +57 -0
- package/.framework/src/instructions/ai-agents.md +69 -0
- package/.framework/src/instructions/alerts.md +80 -0
- package/.framework/src/instructions/api-keys.md +87 -0
- package/.framework/src/instructions/apps.md +77 -0
- package/.framework/src/instructions/concepts/ai-and-prompts.md +150 -0
- package/.framework/src/instructions/concepts/apps-and-renderers.md +162 -0
- package/.framework/src/instructions/concepts/auth-and-api-keys.md +133 -0
- package/.framework/src/instructions/concepts/automation.md +186 -0
- package/.framework/src/instructions/concepts/concurrency-and-idempotency.md +125 -0
- package/.framework/src/instructions/concepts/core-vs-custom.md +124 -0
- package/.framework/src/instructions/concepts/design-schema.md +152 -0
- package/.framework/src/instructions/concepts/integrations-and-webhooks.md +127 -0
- package/.framework/src/instructions/concepts/observability.md +116 -0
- package/.framework/src/instructions/concepts/permissions.md +150 -0
- package/.framework/src/instructions/concepts/runtime-records.md +111 -0
- package/.framework/src/instructions/concepts/scope-and-accounts.md +112 -0
- package/.framework/src/instructions/concepts/spine-way.md +172 -0
- package/.framework/src/instructions/deliveries.md +59 -0
- package/.framework/src/instructions/guide/00-orientation.md +106 -0
- package/.framework/src/instructions/guide/01-install-and-first-run.md +253 -0
- package/.framework/src/instructions/guide/02-project-map.md +147 -0
- package/.framework/src/instructions/guide/03-domain-model.md +223 -0
- package/.framework/src/instructions/guide/04-first-app.md +227 -0
- package/.framework/src/instructions/guide/05-glossary.md +183 -0
- package/.framework/src/instructions/integrations.md +96 -0
- package/.framework/src/instructions/internal/cil-markers.md +83 -0
- package/.framework/src/instructions/invites.md +60 -0
- package/.framework/src/instructions/kb.md +80 -0
- package/.framework/src/instructions/logs.md +77 -0
- package/.framework/src/instructions/observability.md +61 -0
- package/.framework/src/instructions/operations/deployment.md +115 -0
- package/.framework/src/instructions/operations/migrations.md +125 -0
- package/.framework/src/instructions/operations/security-checklist.md +148 -0
- package/.framework/src/instructions/operations/troubleshooting.md +170 -0
- package/.framework/src/instructions/operations/upgrade.md +116 -0
- package/.framework/src/instructions/pipeline-executions.md +62 -0
- package/.framework/src/instructions/pipelines.md +78 -0
- package/.framework/src/instructions/prompts.md +95 -0
- package/.framework/src/instructions/reference/api/admin-data.md +162 -0
- package/.framework/src/instructions/reference/api/auth.md +113 -0
- package/.framework/src/instructions/reference/api/errors-and-envelopes.md +113 -0
- package/.framework/src/instructions/reference/app-manifest.md +150 -0
- package/.framework/src/instructions/reference/cli.md +124 -0
- package/.framework/src/instructions/reference/database.md +132 -0
- package/.framework/src/instructions/reference/design-schema.md +280 -0
- package/.framework/src/instructions/reference/env-vars.md +133 -0
- package/.framework/src/instructions/reference/functions/account-nodes.md +75 -0
- package/.framework/src/instructions/reference/functions/admin-data.md +195 -0
- package/.framework/src/instructions/reference/functions/admin-funnel.md +90 -0
- package/.framework/src/instructions/reference/functions/admin-install-funnel.md +107 -0
- package/.framework/src/instructions/reference/functions/admin-signals.md +102 -0
- package/.framework/src/instructions/reference/functions/ai-agents.md +148 -0
- package/.framework/src/instructions/reference/functions/ai-interactions.md +156 -0
- package/.framework/src/instructions/reference/functions/api-keys.md +130 -0
- package/.framework/src/instructions/reference/functions/apps.md +140 -0
- package/.framework/src/instructions/reference/functions/auth.md +99 -0
- package/.framework/src/instructions/reference/functions/debug-auth.md +91 -0
- package/.framework/src/instructions/reference/functions/deliveries.md +110 -0
- package/.framework/src/instructions/reference/functions/embeddings.md +166 -0
- package/.framework/src/instructions/reference/functions/eval-harness.md +149 -0
- package/.framework/src/instructions/reference/functions/index.md +103 -0
- package/.framework/src/instructions/reference/functions/integration-outbound.md +200 -0
- package/.framework/src/instructions/reference/functions/integration-routes.md +151 -0
- package/.framework/src/instructions/reference/functions/integrations.md +130 -0
- package/.framework/src/instructions/reference/functions/invites.md +182 -0
- package/.framework/src/instructions/reference/functions/item-progress.md +107 -0
- package/.framework/src/instructions/reference/functions/kb.md +166 -0
- package/.framework/src/instructions/reference/functions/logs.md +141 -0
- package/.framework/src/instructions/reference/functions/observability.md +105 -0
- package/.framework/src/instructions/reference/functions/pipeline-executions.md +132 -0
- package/.framework/src/instructions/reference/functions/pipeline-runner-background.md +121 -0
- package/.framework/src/instructions/reference/functions/pipelines.md +136 -0
- package/.framework/src/instructions/reference/functions/prompt-configs.md +121 -0
- package/.framework/src/instructions/reference/functions/roles.md +121 -0
- package/.framework/src/instructions/reference/functions/security.md +140 -0
- package/.framework/src/instructions/reference/functions/system-cron.md +136 -0
- package/.framework/src/instructions/reference/functions/system.md +106 -0
- package/.framework/src/instructions/reference/functions/tests.md +97 -0
- package/.framework/src/instructions/reference/functions/timers.md +117 -0
- package/.framework/src/instructions/reference/functions/trigger-scheduler.md +102 -0
- package/.framework/src/instructions/reference/functions/triggers.md +140 -0
- package/.framework/src/instructions/reference/functions/types.md +146 -0
- package/.framework/src/instructions/reference/seeds.md +126 -0
- package/.framework/src/instructions/reference/shared-modules.md +127 -0
- package/.framework/src/instructions/reference/spine-config.md +114 -0
- package/.framework/src/instructions/roles.md +80 -0
- package/.framework/src/instructions/runtime-detail.md +70 -0
- package/.framework/src/instructions/runtime-list.md +77 -0
- package/.framework/src/instructions/security.md +67 -0
- package/.framework/src/instructions/tasks/add-a-pipeline.md +110 -0
- package/.framework/src/instructions/tasks/add-a-role.md +96 -0
- package/.framework/src/instructions/tasks/add-a-timer.md +114 -0
- package/.framework/src/instructions/tasks/add-a-trigger.md +106 -0
- package/.framework/src/instructions/tasks/add-a-type.md +151 -0
- package/.framework/src/instructions/tasks/add-a-webhook.md +108 -0
- package/.framework/src/instructions/tasks/add-an-ai-agent.md +108 -0
- package/.framework/src/instructions/tasks/add-an-integration.md +124 -0
- package/.framework/src/instructions/tasks/build-a-custom-page.md +118 -0
- package/.framework/src/instructions/tasks/package-and-install-an-app.md +127 -0
- package/.framework/src/instructions/tasks/test-your-code.md +108 -0
- package/.framework/src/instructions/tasks/write-a-function.md +132 -0
- package/.framework/src/instructions/tasks/write-a-migration.md +110 -0
- package/.framework/src/instructions/testing.md +59 -0
- package/.framework/src/instructions/timers.md +82 -0
- package/.framework/src/instructions/triggers.md +69 -0
- package/.framework/src/instructions/types.md +93 -0
- package/.framework/src/lib/branding.ts +128 -0
- package/.framework/src/pages/NoAppPage.tsx +3 -1
- package/.framework/src/pages/NotFoundPage.tsx +62 -31
- package/.framework/src/pages/SetupPage.tsx +5 -2
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +117 -106
- package/.framework/src/pages/admin/AIAgentsPage.tsx +86 -48
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +153 -152
- package/.framework/src/pages/admin/APIKeysPage.tsx +91 -90
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +29 -18
- package/.framework/src/pages/admin/AppDetailPage.tsx +6 -60
- package/.framework/src/pages/admin/AppsPage.tsx +104 -66
- package/.framework/src/pages/admin/DeliveriesPage.tsx +5 -5
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +5 -23
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +5 -26
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +7 -7
- package/.framework/src/pages/admin/IntegrationsPage.tsx +85 -43
- package/.framework/src/pages/admin/LogsPage.tsx +144 -76
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +223 -140
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +134 -113
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +108 -45
- package/.framework/src/pages/admin/PipelinesPage.tsx +160 -115
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +96 -98
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +101 -59
- package/.framework/src/pages/admin/RoleDetailPage.tsx +90 -79
- package/.framework/src/pages/admin/RolesPage.tsx +86 -43
- package/.framework/src/pages/admin/SecurityPage.tsx +1 -1
- package/.framework/src/pages/admin/TestingDashboard.tsx +16 -28
- package/.framework/src/pages/admin/TimerDetailPage.tsx +117 -100
- package/.framework/src/pages/admin/TimersPage.tsx +90 -62
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +90 -83
- package/.framework/src/pages/admin/TriggersPage.tsx +124 -99
- package/.framework/src/pages/admin/TypeDetailPage.tsx +152 -138
- package/.framework/src/pages/admin/TypesPage.tsx +90 -48
- package/.framework/src/pages/admin/cil/CILAgentPage.tsx +17 -25
- package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +17 -27
- package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +72 -40
- package/.framework/src/pages/admin/cil/CILFileListPage.tsx +321 -129
- package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +18 -29
- package/.framework/src/pages/admin/cil/CILGraphPage.tsx +26 -38
- package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +34 -46
- package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +189 -103
- package/.framework/src/pages/admin/cil/CILSearchPage.tsx +246 -136
- package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +11 -22
- package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +9 -20
- package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +26 -38
- package/.framework/src/pages/admin/kb/KBAdminPage.tsx +7 -26
- package/.framework/src/pages/auth/AuthConfirmPage.tsx +3 -1
- package/.framework/src/pages/auth/ForgotPasswordPage.tsx +3 -1
- package/.framework/src/pages/auth/LoginPage.tsx +4 -2
- package/.framework/src/pages/auth/RegisterPage.tsx +3 -1
- package/.framework/templates/agent/AGENTS.md +150 -0
- package/.framework/templates/agent/skills/spine-add-ai-agent/SKILL.md +58 -0
- package/.framework/templates/agent/skills/spine-add-automation/SKILL.md +62 -0
- package/.framework/templates/agent/skills/spine-add-integration/SKILL.md +63 -0
- package/.framework/templates/agent/skills/spine-architect/SKILL.md +86 -0
- package/.framework/templates/agent/skills/spine-install-app/SKILL.md +72 -0
- package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
- package/.framework/templates/agent/skills/spine-new-app/SKILL.md +67 -0
- package/.framework/templates/agent/skills/spine-new-function/SKILL.md +65 -0
- package/.framework/templates/agent/skills/spine-new-type/SKILL.md +65 -0
- package/.framework/templates/agent/skills/spine-schema/SKILL.md +77 -0
- package/.framework/templates/agent/skills/spine-test/SKILL.md +67 -0
- package/.framework/templates/agent/skills/spine-troubleshoot/SKILL.md +74 -0
- package/.framework/templates/agent/skills/spine-upgrade/SKILL.md +69 -0
- package/.framework/templates/project.json +5 -1
- package/.framework/tests/unit/account-scoping-gates.test.ts +392 -0
- package/.framework/tests/unit/agent-files.test.ts +355 -0
- package/.framework/tests/unit/agent-runner.test.ts +496 -47
- package/.framework/tests/unit/api-keys-create-scope-gate.test.ts +160 -0
- package/.framework/tests/unit/api-keys-omit-secret-fields.test.ts +38 -0
- package/.framework/tests/unit/bootstrap.test.ts +242 -0
- package/.framework/tests/unit/cli-context-api-key.test.ts +114 -0
- package/.framework/tests/unit/debug-auth.test.ts +38 -0
- package/.framework/tests/unit/deliveries-account-scoping.test.ts +185 -0
- package/.framework/tests/unit/episode-lease.test.ts +695 -0
- package/.framework/tests/unit/integration-outbound-credentials.test.ts +167 -0
- package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
- package/.framework/tests/unit/kb-ingest-lease.test.ts +19 -0
- package/.framework/tests/unit/logging-app-id.test.ts +75 -0
- package/.framework/tests/unit/pipeline-runner-background.test.ts +120 -0
- package/.framework/tests/unit/pipeline-runner.test.ts +153 -1
- package/.framework/tests/unit/principal-cache.test.ts +15 -0
- package/.framework/tests/unit/system-context-denial.test.ts +145 -0
- package/.framework/tests/unit/system-cron.test.ts +240 -0
- package/.framework/tests/unit/timers.test.ts +159 -0
- package/.framework/tests/unit/trigger-engine.test.ts +278 -91
- package/.framework/tests/unit/trigger-scheduler.test.ts +78 -0
- package/README.md +125 -166
- package/config/tsconfig.cli.json +2 -2
- package/netlify.toml +5 -0
- package/package.json +15 -4
- package/templates/project/package-lock.json +2 -2
- package/.framework/docs/admin-routes.md +0 -76
- package/.framework/docs/api-docs-progress.md +0 -38
- package/.framework/docs/api-governance.md +0 -146
- package/.framework/docs/api-testing-results.md +0 -212
- package/.framework/docs/assembly-launch-guide.md +0 -138
- package/.framework/docs/audit-results.md +0 -590
- package/.framework/docs/db-api-inventory.md +0 -95
- package/.framework/docs/gap-remediation-backlog.md +0 -103
- package/.framework/docs/ui-adoption-verification.md +0 -111
- package/.framework/docs/ui-api-coverage.md +0 -84
- package/.framework/docs/v2-compatibility-audit.md +0 -228
- package/.framework/mcp/index.ts +0 -133
- package/.framework/mcp/load-env.ts +0 -60
- package/.framework/mcp/shared/app-manifest.ts +0 -275
- package/.framework/mcp/shared/db.ts +0 -477
- package/.framework/mcp/shared/kb.ts +0 -118
- package/.framework/mcp/tools/architect.ts +0 -431
- package/.framework/mcp/tools/cil/context.ts +0 -345
- package/.framework/mcp/tools/cil/graph.ts +0 -242
- package/.framework/mcp/tools/cil/ingest.ts +0 -1217
- package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +0 -693
- package/.framework/mcp/tools/cil/pipeline.ts +0 -767
- package/.framework/mcp/tools/cil/project.ts +0 -791
- package/.framework/mcp/tools/cil/retrieval.ts +0 -336
- package/.framework/mcp/tools/cil/search-core.ts +0 -454
- package/.framework/mcp/tools/cil/search.ts +0 -97
- package/.framework/mcp/tools/cil/suggest.ts +0 -186
- package/.framework/mcp/tools/cil/validate.ts +0 -1369
- package/.framework/mcp/tools/kb/delete.ts +0 -139
- package/.framework/mcp/tools/kb/index.ts +0 -52
- package/.framework/mcp/tools/kb/ingest.ts +0 -84
- package/.framework/mcp/tools/kb/list-types.ts +0 -102
- package/.framework/mcp/tools/kb/publish.ts +0 -100
- package/.framework/mcp/tools/kb/reindex.ts +0 -86
- package/.framework/mcp/tools/kb/revoke.ts +0 -85
- package/.framework/mcp/tools/kb/search.ts +0 -101
- package/.framework/mcp/tools/kb/upsert.ts +0 -125
- package/.framework/mcp/tools/observability.ts +0 -510
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Build a custom page
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: task
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/src/components/CustomAppLoader.tsx
|
|
9
|
+
- .framework/src/components/AppWrapper.tsx
|
|
10
|
+
- .framework/src/components/app-shell/GenericAppShell.tsx
|
|
11
|
+
- .framework/src/lib/api.ts
|
|
12
|
+
- .framework/cli/commands/create-app.ts
|
|
13
|
+
- scripts/assemble.sh
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Build a custom page
|
|
17
|
+
|
|
18
|
+
## Goal
|
|
19
|
+
|
|
20
|
+
Ship a React page inside a custom app (`renderer: 'custom'`) instead of the
|
|
21
|
+
generic list/detail renderer. See
|
|
22
|
+
[Apps and renderers](../concepts/apps-and-renderers.md) for the model.
|
|
23
|
+
|
|
24
|
+
## When to go custom
|
|
25
|
+
|
|
26
|
+
Stay on `renderer: 'generic'` when the app is CRUD over typed records — the
|
|
27
|
+
generic renderer gives you list/detail/create/sort/permission-aware editing
|
|
28
|
+
for free from `views.default_list` / `views.default_detail`. Go custom for
|
|
29
|
+
dashboards, wizards, bespoke layouts, or anything not expressible as
|
|
30
|
+
schema-driven list/detail — `GenericAppShell` only generates routes for
|
|
31
|
+
nav items carrying `type_slug`
|
|
32
|
+
(`.framework/src/components/app-shell/GenericAppShell.tsx`).
|
|
33
|
+
|
|
34
|
+
## Steps
|
|
35
|
+
|
|
36
|
+
1. **Scaffold the app** (or reuse one):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx spine-framework create-app <slug> --role <role-slug>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Writes `custom/apps/<slug>/` with `manifest.json`, `index.tsx`, and
|
|
43
|
+
`components/`, and inserts the `apps` row with `renderer: 'custom'`
|
|
44
|
+
(`.framework/cli/commands/create-app.ts`). Pass `--role` a slug
|
|
45
|
+
that exists in `roles` — the default `member` is seeded by
|
|
46
|
+
`073_seed_member_roles.sql`.
|
|
47
|
+
|
|
48
|
+
2. **Add pages under `custom/apps/<slug>/pages/`.** `CustomAppLoader`
|
|
49
|
+
lazy-imports only `custom/apps/<slug>/index.tsx`
|
|
50
|
+
(`import.meta.glob('../../../custom/apps/*/index.tsx')`,
|
|
51
|
+
`.framework/src/components/CustomAppLoader.tsx`) — `index.tsx`
|
|
52
|
+
default-exports the component (usually its own `<Routes>`) and imports
|
|
53
|
+
the page components itself.
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// custom/apps/<slug>/pages/DashboardPage.tsx
|
|
57
|
+
export default function DashboardPage() { /* ... */ }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
3. **Set the app row.** `renderer: 'custom'`, `route_prefix: '/<slug>'`,
|
|
61
|
+
`min_role` / `required_roles`, `nav_items` for the sidebar. `AppWrapper`
|
|
62
|
+
enforces auth + role before mounting your app
|
|
63
|
+
(`.framework/src/components/AppWrapper.tsx`).
|
|
64
|
+
|
|
65
|
+
4. **Fetch data through `/api/admin-data`** — never write a bespoke CRUD
|
|
66
|
+
function:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { apiFetch } from '@core/lib/api'
|
|
70
|
+
|
|
71
|
+
const res = await apiFetch(
|
|
72
|
+
'/api/admin-data?action=list&entity=items&type_slug=ticket&limit=50')
|
|
73
|
+
const { data } = await res.json()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`apiFetch` attaches the Supabase Bearer token and the `X-Account-Id`
|
|
77
|
+
header (`.framework/src/lib/api.ts`). Find-or-create related records
|
|
78
|
+
(e.g. a thread for a work item) through the same endpoint — see
|
|
79
|
+
[Build your first app — Step 4](../guide/04-first-app.md) for the full
|
|
80
|
+
call shapes.
|
|
81
|
+
|
|
82
|
+
5. **Rebuild and run.** `npm run assemble` (or `npm run dev` / `dev:netlify`,
|
|
83
|
+
which run assembly automatically). Custom functions go in
|
|
84
|
+
`custom/functions/` prefixed `custom_` — assembly hard-fails on a
|
|
85
|
+
basename collision with a framework function
|
|
86
|
+
(`scripts/assemble-functions.sh`).
|
|
87
|
+
|
|
88
|
+
## Verify
|
|
89
|
+
|
|
90
|
+
- Navigate to `/<slug>` — `CustomAppLoader` resolves your `index.tsx`
|
|
91
|
+
(a missing entry shows the `App "<slug>" not found` error naming the
|
|
92
|
+
expected path).
|
|
93
|
+
- `GET /api/apps?action=get&slug=<slug>` shows `renderer: 'custom'` and the
|
|
94
|
+
right `route_prefix`.
|
|
95
|
+
- `npm run typecheck` compiles the page.
|
|
96
|
+
|
|
97
|
+
## Common mistakes
|
|
98
|
+
|
|
99
|
+
- **A bespoke CRUD endpoint.** Custom Netlify functions that
|
|
100
|
+
create/update/list runtime records bypass `admin-data`'s permission,
|
|
101
|
+
type-resolution, audit, and trigger logic — call `/api/admin-data` from
|
|
102
|
+
the client instead (see [Write a function](./write-a-function.md) for the
|
|
103
|
+
cases where a custom function *is* allowed).
|
|
104
|
+
- **Forgetting `index.tsx` is the only entry.** New pages in `pages/` are
|
|
105
|
+
invisible until `index.tsx` routes to them.
|
|
106
|
+
- **`type_slug` nav items on a custom app** — they only do something under
|
|
107
|
+
the generic renderer; use plain `path` nav items and route them yourself.
|
|
108
|
+
- **Editing before re-assembling** — `CustomAppLoader`'s glob is evaluated
|
|
109
|
+
at build time; new files need an assemble/dev restart.
|
|
110
|
+
|
|
111
|
+
## Reference
|
|
112
|
+
|
|
113
|
+
- [Apps and renderers](../concepts/apps-and-renderers.md) ·
|
|
114
|
+
[Runtime records](../concepts/runtime-records.md)
|
|
115
|
+
- [App manifest reference](../reference/app-manifest.md) ·
|
|
116
|
+
[admin-data function reference](../reference/functions/admin-data.md)
|
|
117
|
+
- [Build your first app](../guide/04-first-app.md) ·
|
|
118
|
+
[Anatomy of an installed project](../guide/02-project-map.md)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Package and install an app
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: task
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/cli/commands/install-app.ts
|
|
9
|
+
- .framework/cli/commands/validate-app.ts
|
|
10
|
+
- .framework/functions/_shared/app-manifest.ts
|
|
11
|
+
- .framework/cli/commands/migrate.ts
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Package and install an app
|
|
15
|
+
|
|
16
|
+
## Goal
|
|
17
|
+
|
|
18
|
+
Package a `custom/apps/<slug>/` directory as an npm package and install it
|
|
19
|
+
into a Spine project — files copied, `apps` row upserted, migrations run,
|
|
20
|
+
seeds applied. Field-level manifest semantics are in
|
|
21
|
+
[App manifest reference](../reference/app-manifest.md) and
|
|
22
|
+
[Seeds reference](../reference/seeds.md).
|
|
23
|
+
|
|
24
|
+
## Steps
|
|
25
|
+
|
|
26
|
+
1. **Author `manifest.json`.** Required fields the installer validates:
|
|
27
|
+
`slug` (must equal the directory name and match
|
|
28
|
+
`^[a-z][a-z0-9-]*$`), plus `name`, `route_prefix`, `renderer`,
|
|
29
|
+
`min_role` / `required_roles`, `nav_items`, `config`, `registration`.
|
|
30
|
+
See [App manifest reference](../reference/app-manifest.md) for every
|
|
31
|
+
field.
|
|
32
|
+
|
|
33
|
+
2. **Author seed files.** The `seed` block is a list of
|
|
34
|
+
`{ file, table, conflict, inject_app_id?, ignore_duplicates?, permissions_patch? }`
|
|
35
|
+
entries, each pointing at a JSON file of rows
|
|
36
|
+
(`.framework/cli/commands/install-app.ts`):
|
|
37
|
+
|
|
38
|
+
- `conflict` — the `onConflict` column list for the upsert. Types use
|
|
39
|
+
`conflict: "kind,slug"` with `inject_app_id: false` — types are
|
|
40
|
+
**global**, not duplicated per install.
|
|
41
|
+
- `inject_app_id` — stamps `app_id` on each seeded row (default `true`);
|
|
42
|
+
set `false` for global tables like `types`.
|
|
43
|
+
- `ignore_duplicates` — `ignoreDuplicates` on the upsert; `types` rows
|
|
44
|
+
always get it (`.framework/cli/commands/install-app.ts`).
|
|
45
|
+
- `permissions_patch` — a types-table entry that merges
|
|
46
|
+
`record_permissions` into existing design schemas instead of
|
|
47
|
+
overwriting; ranks just after `types` in apply order.
|
|
48
|
+
- If no `seed` array is declared, the conventional files
|
|
49
|
+
`seed/roles.json`, `seed/types.json`, `seed/link-types.json` are used
|
|
50
|
+
(`.framework/cli/commands/install-app.ts`).
|
|
51
|
+
|
|
52
|
+
Seeds apply in dependency order — `roles`, `types`, `link_types`,
|
|
53
|
+
`actions`, `ai_agents`, `prompt_configs`, `pipelines`, `triggers` —
|
|
54
|
+
regardless of declaration order
|
|
55
|
+
(`.framework/cli/commands/install-app.ts`). Pipeline seeds are
|
|
56
|
+
validated against `public.actions` stage slugs, and trigger seeds
|
|
57
|
+
resolve `pipeline_slug` → `pipeline_id`.
|
|
58
|
+
|
|
59
|
+
3. **Package it.** Ship the app directory as an npm package whose files
|
|
60
|
+
include `index.tsx`, `manifest.json`, and optionally `pages/`,
|
|
61
|
+
`components/`, `hooks/`, `utils/`, `functions/`, `seed/`, `public/` —
|
|
62
|
+
the installer copies exactly that allowlist
|
|
63
|
+
(`.framework/cli/commands/install-app.ts`). Validate locally
|
|
64
|
+
with `npx spine-framework validate-app`.
|
|
65
|
+
|
|
66
|
+
4. **Install.**
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx spine-framework install-app <package>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Flow: `npm install <package>` → resolve slug → copy files to
|
|
73
|
+
`custom/apps/<slug>/` (a reinstall preserves the local `manifest.json`)
|
|
74
|
+
→ `runMigrations` → `seedApp` → sync the `registration` block into
|
|
75
|
+
`apps.config` and `spine.config.json`
|
|
76
|
+
(`.framework/cli/commands/install-app.ts`). Requires
|
|
77
|
+
`SUPABASE_URL` + `SUPABASE_DB_PASSWORD` + `SUPABASE_SERVICE_ROLE_KEY`;
|
|
78
|
+
`--no-seed` skips all DB work.
|
|
79
|
+
|
|
80
|
+
5. **Update paths.**
|
|
81
|
+
|
|
82
|
+
- `update-app <package>` — `npm install @latest` + copy files + sync
|
|
83
|
+
registration. **It does not re-run migrations or seeds.**
|
|
84
|
+
- `update-db-app <slug>` — upserts the `apps` row from the local
|
|
85
|
+
`manifest.json` and syncs registration; no npm, no file changes.
|
|
86
|
+
- `seed-app <slug>` — re-runs just the seed block for a local app;
|
|
87
|
+
`--reconcile-owned-types` interactively updates `manifest.owned_types`
|
|
88
|
+
slugs whose DB `design_schema` drifted from the seed.
|
|
89
|
+
|
|
90
|
+
6. **Uninstall.** `uninstall-app <package>` removes
|
|
91
|
+
`custom/apps/<slug>/` and runs `npm uninstall` — and nothing else. The
|
|
92
|
+
`--keep-data` flag is parsed but never read: seeded rows are **not**
|
|
93
|
+
deleted either way (`.framework/cli/commands/install-app.ts`).
|
|
94
|
+
|
|
95
|
+
## Verify
|
|
96
|
+
|
|
97
|
+
- `npx spine-framework status` lists the installed app and version.
|
|
98
|
+
- `GET /api/apps?action=get&slug=<slug>` shows the upserted row;
|
|
99
|
+
`migrations status` shows migration state.
|
|
100
|
+
- Seeded rows exist: `GET /api/types?action=list` (or the seeded table's
|
|
101
|
+
endpoint) includes the app's types/roles.
|
|
102
|
+
|
|
103
|
+
## Common mistakes
|
|
104
|
+
|
|
105
|
+
- **Types not global.** Forgetting `inject_app_id: false` on a types seed
|
|
106
|
+
injects `app_id` into global type rows — and `conflict: "kind,slug"`
|
|
107
|
+
means a second app with the same slug silently skips or errors on
|
|
108
|
+
schema drift. Prefix slugs.
|
|
109
|
+
- **Expecting `uninstall --keep-data` to matter** — it is a no-op; seeded
|
|
110
|
+
rows always survive uninstall.
|
|
111
|
+
- **`update-app` doesn't re-seed** — new seed rows in the package never
|
|
112
|
+
reach the DB; run `seed-app <slug>` or `update-db-app` after.
|
|
113
|
+
- **Seed rows referencing unseeded slugs** — `owned_types`,
|
|
114
|
+
`pipeline_slug`, and `send_notification` recipients are validated
|
|
115
|
+
fail-closed before any DB writes; fix the manifest, don't retry.
|
|
116
|
+
- **`SUPABASE_URL` must be a hosted `https://<ref>.supabase.co` URL** —
|
|
117
|
+
the migrator rejects anything else before connecting
|
|
118
|
+
(`.framework/cli/commands/migrate.ts`).
|
|
119
|
+
|
|
120
|
+
## Reference
|
|
121
|
+
|
|
122
|
+
- [App manifest reference](../reference/app-manifest.md) ·
|
|
123
|
+
[Seeds reference](../reference/seeds.md) ·
|
|
124
|
+
[CLI reference](../reference/cli.md) ·
|
|
125
|
+
[spine.config.json reference](../reference/spine-config.md)
|
|
126
|
+
- [Apps and renderers](../concepts/apps-and-renderers.md) ·
|
|
127
|
+
[Write a migration](./write-a-migration.md)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Test your code
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: task
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- vitest.config.ts
|
|
9
|
+
- .framework/tests/setup.ts
|
|
10
|
+
- .framework/tests/reporter.ts
|
|
11
|
+
- package.json
|
|
12
|
+
- config/tsconfig.json
|
|
13
|
+
- config/tsconfig.functions.json
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Test your code
|
|
17
|
+
|
|
18
|
+
## Goal
|
|
19
|
+
|
|
20
|
+
Run the Vitest suite and the TypeScript checks, and place new tests where
|
|
21
|
+
the runner picks them up.
|
|
22
|
+
|
|
23
|
+
## Steps
|
|
24
|
+
|
|
25
|
+
1. **Know the runners** (`package.json` scripts):
|
|
26
|
+
|
|
27
|
+
- `npm run test` → `vitest run` — everything matching the `include`
|
|
28
|
+
globs in `vitest.config.ts`: `.framework/tests/unit/**/*.test.ts`,
|
|
29
|
+
`.framework/tests/api/**/*.test.ts`, `.framework/tests/integration/**/*.test.ts`,
|
|
30
|
+
and `custom/tests/**/*.test.ts`.
|
|
31
|
+
- `npm run test:unit` — the unit suite only.
|
|
32
|
+
- `npm run test:integration` — the integration suite (needs real env,
|
|
33
|
+
see below).
|
|
34
|
+
- `npm run test:core` — the core-isolation suite.
|
|
35
|
+
- `npm run typecheck` → `tsc -b config/tsconfig.json` — frontend +
|
|
36
|
+
CLI. Functions are **not** covered here: check them separately with
|
|
37
|
+
`npx tsc --noEmit -p config/tsconfig.functions.json` after
|
|
38
|
+
`npm run assemble` (it typechecks the assembled layout).
|
|
39
|
+
- `npx spine-framework test [suite]` — the CLI wrapper
|
|
40
|
+
(`--watch`, `--reporter`, `--json`; see [CLI reference](../reference/cli.md)).
|
|
41
|
+
|
|
42
|
+
2. **Put tests where the globs look.** New project tests belong in
|
|
43
|
+
`custom/tests/**/*.test.ts` — picked up automatically, and they keep
|
|
44
|
+
`.framework/` untouched. Framework-level additions go under
|
|
45
|
+
`.framework/tests/unit/`. Test files inside `custom/functions/` are
|
|
46
|
+
excluded from function assembly (`*-test.ts`, `*.test.ts` are skipped
|
|
47
|
+
by `scripts/assemble-functions.sh`), but naming convention is still
|
|
48
|
+
`*.test.ts`.
|
|
49
|
+
|
|
50
|
+
3. **Understand the env layering.** `vitest.config.ts` loads a root
|
|
51
|
+
`.env.test` file (if present) into `process.env` **before** any module
|
|
52
|
+
graph runs — that is how integration/API tests get real Supabase
|
|
53
|
+
credentials before `db.ts`/`principal.ts` call `createClient` at import
|
|
54
|
+
time. `.framework/tests/setup.ts` then stubs `SUPABASE_URL`,
|
|
55
|
+
`SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY` so unit tests never
|
|
56
|
+
crash on missing env. Integration helpers additionally require
|
|
57
|
+
`SPINE_TEST_ACCOUNT_ID`; API-surface tests use `SPINE_DEV_URL`,
|
|
58
|
+
`SPINE_DEV_JWT`, `SPINE_TEST_API_KEY` (see
|
|
59
|
+
[Environment variables](../reference/env-vars.md)).
|
|
60
|
+
|
|
61
|
+
4. **Mock at module boundaries.** The config sets `isolate: true` — every
|
|
62
|
+
test file gets a fresh module registry, so `vi.mock` on
|
|
63
|
+
`./_shared/db`-style imports works per file. Existing unit suites mock
|
|
64
|
+
the Supabase client, principal resolution, and the LLM/embedding calls
|
|
65
|
+
rather than hitting the network (browse `.framework/tests/unit/` for
|
|
66
|
+
patterns — `pipeline-runner.test.ts`, `permissions.test.ts`,
|
|
67
|
+
`principal-cache.test.ts`).
|
|
68
|
+
|
|
69
|
+
5. **Integration tests need a real project.** They exercise admin-data,
|
|
70
|
+
triggers, pipelines, and schema validation against a live Supabase
|
|
71
|
+
project using a service-role client and a seeded test account —
|
|
72
|
+
`custom/tests` and the unit suite run with no credentials, the
|
|
73
|
+
integration suite does not. `passWithNoTests: true` means a missing
|
|
74
|
+
env never fails the suite outright, so check that tests actually ran.
|
|
75
|
+
|
|
76
|
+
## Verify
|
|
77
|
+
|
|
78
|
+
- `npm run test` exits 0; `vitest` prints per-file pass counts. A custom
|
|
79
|
+
reporter that persists runs to `public.test_runs` exists at
|
|
80
|
+
`.framework/tests/reporter.ts` but is **not** registered — the config
|
|
81
|
+
uses `reporters: ['default']`, so opt in with `--reporter` if you want
|
|
82
|
+
persisted runs.
|
|
83
|
+
- `npm run typecheck` and
|
|
84
|
+
`npx tsc --noEmit -p config/tsconfig.functions.json` are both clean.
|
|
85
|
+
- `npx spine-framework doctor` sanity-checks the environment when tests
|
|
86
|
+
behave oddly ([CLI reference](../reference/cli.md)).
|
|
87
|
+
|
|
88
|
+
## Common mistakes
|
|
89
|
+
|
|
90
|
+
- **`typecheck` doesn't cover functions.** `config/tsconfig.json` excludes
|
|
91
|
+
the functions project by design — always run the
|
|
92
|
+
`tsconfig.functions.json` check too, and only after `npm run assemble`.
|
|
93
|
+
- **Assuming integration tests ran.** With no `.env.test`, integration
|
|
94
|
+
files that can't build a client may skip silently — grep the output for
|
|
95
|
+
your file's pass count.
|
|
96
|
+
- **Importing `createClient` at module top-level in a unit test.** Setup
|
|
97
|
+
stubs the env, but a module that builds a client at import time before
|
|
98
|
+
`setupFiles` runs still fails — mock the module or import lazily.
|
|
99
|
+
- **Writing repo-only paths into `custom/tests`** — tests there must run
|
|
100
|
+
in an installed project too; import from `custom/` and assembled
|
|
101
|
+
aliases, not from repo internals.
|
|
102
|
+
|
|
103
|
+
## Reference
|
|
104
|
+
|
|
105
|
+
- [CLI reference](../reference/cli.md) — `test`, `doctor`
|
|
106
|
+
- [Environment variables](../reference/env-vars.md) — `SPINE_TEST_*`,
|
|
107
|
+
`SPINE_DEV_*`, `SUPABASE_*`
|
|
108
|
+
- [Write a function](./write-a-function.md) — assembled-layout imports
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Write a function
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: task
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/_shared/middleware.ts
|
|
9
|
+
- .framework/functions/_shared/db.ts
|
|
10
|
+
- .framework/functions/_shared/permissions.ts
|
|
11
|
+
- .framework/functions/_shared/logging.ts
|
|
12
|
+
- .framework/functions/timers.ts
|
|
13
|
+
- .framework/public/_redirects
|
|
14
|
+
- scripts/assemble-functions.sh
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Write a function
|
|
18
|
+
|
|
19
|
+
## Goal
|
|
20
|
+
|
|
21
|
+
Add a custom Netlify function when — and only when — the generic
|
|
22
|
+
`/api/admin-data` endpoint cannot cover the case: specialized auth, inbound
|
|
23
|
+
webhooks, heavy compute, or bespoke orchestration. Record CRUD must go
|
|
24
|
+
through `admin-data` so permission, validation, audit, and trigger logic
|
|
25
|
+
stay shared ([Runtime records](../concepts/runtime-records.md)).
|
|
26
|
+
|
|
27
|
+
## Steps
|
|
28
|
+
|
|
29
|
+
1. **Decide it is warranted.** `admin-data` already covers list/get/create/
|
|
30
|
+
update/remove/stats across every runtime table with permissions and
|
|
31
|
+
triggers enforced ([admin-data reference](../reference/functions/admin-data.md)).
|
|
32
|
+
Legitimate custom functions: public webhook receivers, scheduled
|
|
33
|
+
sweeps, compute-heavy transforms, auth-bridging endpoints.
|
|
34
|
+
|
|
35
|
+
2. **Create the file.** Put it in `custom/functions/` (or
|
|
36
|
+
`custom/apps/<slug>/functions/` for app-scoped code) with a `custom_`
|
|
37
|
+
prefix — assembly copies `.framework/functions`, `custom/functions`,
|
|
38
|
+
and `custom/apps/*/functions` into one flat Netlify dir and **fails the
|
|
39
|
+
build on a basename collision**
|
|
40
|
+
(`scripts/assemble-functions.sh`). All functions are reachable at
|
|
41
|
+
`/api/<basename>` via the `_redirects` rule
|
|
42
|
+
(`.framework/public/_redirects`).
|
|
43
|
+
|
|
44
|
+
Imports resolve in the **assembled** layout, not the source tree:
|
|
45
|
+
`./_shared/*` and `./<other-function>` are sibling paths inside
|
|
46
|
+
`.assembled/netlify/functions/`, and `@shared/*` is an alias to the
|
|
47
|
+
same dir (`config/tsconfig.functions.json`). A function's own
|
|
48
|
+
`functions/_shared -> .framework/functions/_shared` symlink (as the
|
|
49
|
+
existing app functions dirs carry) makes `./_shared` resolve in the
|
|
50
|
+
source tree too.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// custom/functions/custom_my-endpoint.ts
|
|
54
|
+
import { createHandler, error, json } from './_shared/middleware'
|
|
55
|
+
import { adminDb } from './_shared/db'
|
|
56
|
+
import { PermissionEngine } from './_shared/permissions'
|
|
57
|
+
import { emitLog } from './_shared/logging'
|
|
58
|
+
|
|
59
|
+
export const handler = createHandler(async (ctx, body) => {
|
|
60
|
+
// ctx.requestId, ctx.principal, ctx.db, ctx.accountId, ctx.appId,
|
|
61
|
+
// ctx.query, ctx.headers, ctx.trace — all populated by middleware
|
|
62
|
+
if (!PermissionEngine.isSystemAdmin(ctx)) {
|
|
63
|
+
throw new Error('System admin required')
|
|
64
|
+
}
|
|
65
|
+
const { data, error: err } = await ctx.db
|
|
66
|
+
.from('items').select('id,title').eq('is_active', true).limit(10)
|
|
67
|
+
if (err) throw err
|
|
68
|
+
return data
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
3. **Follow the `createHandler` contract** (`.framework/functions/_shared/middleware.ts`):
|
|
73
|
+
|
|
74
|
+
- The middleware resolves the principal (`x-api-key` → internal secret →
|
|
75
|
+
Bearer JWT), builds `RequestContext`, and wraps your return value in
|
|
76
|
+
the `{ data, error, meta }` envelope. Thrown `Error`s become
|
|
77
|
+
`{ error }` responses — don't build response objects yourself.
|
|
78
|
+
- `ctx.db` is the principal-scoped client (humans get a user-scoped,
|
|
79
|
+
RLS-bound client; machines/anonymous get `adminDb`).
|
|
80
|
+
- `ctx.query` merges `queryStringParameters`; `ctx.query.method` carries
|
|
81
|
+
the HTTP method. Functions dispatching on `?action=` use a
|
|
82
|
+
`switch (ctx.query.action)` (see `.framework/functions/timers.ts`).
|
|
83
|
+
- For endpoints that must accept anonymous or raw requests, use
|
|
84
|
+
`createPublicHandler` instead (same file, `createPublicHandler`).
|
|
85
|
+
|
|
86
|
+
4. **Use the `_shared` modules, don't reinvent them** — `db` (`adminDb`,
|
|
87
|
+
`getUserDb`), `permissions` (`PermissionEngine`, `isSystemAdmin`,
|
|
88
|
+
`sanitizeRecordData`), `principal` (`resolvePrincipal`,
|
|
89
|
+
`SYSTEM_PRINCIPAL`), `audit`/`logging` (`emitLog`, `emitAudit`),
|
|
90
|
+
`resolve-ids`, `rate-limit` — see
|
|
91
|
+
[Shared modules reference](../reference/shared-modules.md) and
|
|
92
|
+
[Functions index](../reference/functions/index.md).
|
|
93
|
+
|
|
94
|
+
5. **Assemble and run.** `npm run assemble && npm run dev:netlify`, then
|
|
95
|
+
`curl http://localhost:8888/api/custom_my-endpoint` — see
|
|
96
|
+
[Install and first run](../guide/01-install-and-first-run.md).
|
|
97
|
+
Function typecheck is separate from `npm run typecheck`:
|
|
98
|
+
`npx tsc --noEmit -p config/tsconfig.functions.json` checks the
|
|
99
|
+
assembled output (run it after `assemble`).
|
|
100
|
+
|
|
101
|
+
## Verify
|
|
102
|
+
|
|
103
|
+
- `GET /api/custom_my-endpoint` returns the `{ data, error, meta }`
|
|
104
|
+
envelope; unauthenticated calls return `401 Authentication required`
|
|
105
|
+
from the middleware, not from your code.
|
|
106
|
+
- `npm run assemble` passes (no basename collision error) and
|
|
107
|
+
`npm run typecheck` compiles.
|
|
108
|
+
|
|
109
|
+
## Common mistakes
|
|
110
|
+
|
|
111
|
+
- **Duplicating a framework basename.** Assembly hard-fails naming both
|
|
112
|
+
sources — prefix custom files `custom_`.
|
|
113
|
+
- **Returning raw Response objects.** `createHandler` expects a return
|
|
114
|
+
value (serialized into `data`) or a thrown error — don't emit Netlify
|
|
115
|
+
`{statusCode, body}` shapes yourself inside a `createHandler` function.
|
|
116
|
+
- **Using `adminDb` for user-facing reads.** It bypasses RLS — prefer
|
|
117
|
+
`ctx.db` unless you intentionally need service-role access.
|
|
118
|
+
- **Putting secrets in the response.** `sanitizeRecordData` /
|
|
119
|
+
`omitSecretFields` exist for a reason — strip credentials before
|
|
120
|
+
returning.
|
|
121
|
+
- **Writing record CRUD** — if the function creates/updates `items`,
|
|
122
|
+
`threads`, etc., you are bypassing `admin-data`; call it via
|
|
123
|
+
`adminDataCreate`/`/api/admin-data` instead.
|
|
124
|
+
|
|
125
|
+
## Reference
|
|
126
|
+
|
|
127
|
+
- [Shared modules reference](../reference/shared-modules.md) ·
|
|
128
|
+
[Functions index](../reference/functions/index.md) ·
|
|
129
|
+
[Auth and API keys](../concepts/auth-and-api-keys.md)
|
|
130
|
+
- [admin-data reference](../reference/functions/admin-data.md) ·
|
|
131
|
+
[Build a custom page](./build-a-custom-page.md) ·
|
|
132
|
+
[Anatomy of an installed project](../guide/02-project-map.md)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Write a migration
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: task
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/cli/commands/migrate.ts
|
|
9
|
+
- .framework/migrations/068_cron_schedule_timer_rpcs.sql
|
|
10
|
+
- .framework/migrations/053_builtin_action_seeds.sql
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Write a migration
|
|
14
|
+
|
|
15
|
+
## Goal
|
|
16
|
+
|
|
17
|
+
Add a `.sql` file that the `migrate` command applies exactly once, in order,
|
|
18
|
+
tracked in the `_spine_migrations` ledger table. See
|
|
19
|
+
[Database reference](../reference/database.md) for the table map.
|
|
20
|
+
|
|
21
|
+
## Steps
|
|
22
|
+
|
|
23
|
+
1. **Pick the directory.**
|
|
24
|
+
|
|
25
|
+
- `custom/migrations/` — project/app migrations (**default choice**).
|
|
26
|
+
`collectMigrations` reads `.framework/migrations/*.sql` first, then
|
|
27
|
+
`custom/migrations/*.sql`, each sorted lexically — custom migrations
|
|
28
|
+
always run after every framework migration
|
|
29
|
+
(`.framework/cli/commands/migrate.ts`).
|
|
30
|
+
- `.framework/migrations/` — only for framework-owned changes (you are
|
|
31
|
+
editing the framework itself).
|
|
32
|
+
|
|
33
|
+
2. **Pick the number.** Framework files use `NNN_name.sql` with zero-padded
|
|
34
|
+
sequence numbers. Occupied ranges today: `001`–`021`, `032`–`038`,
|
|
35
|
+
`042`–`062`, `064`–`069`; gaps `22`–`31`, `39`–`41`, `63` are reserved/
|
|
36
|
+
consolidated — **do not reuse gap numbers** (see
|
|
37
|
+
[Database reference](../reference/database.md)). Custom migrations get
|
|
38
|
+
their own files in `custom/migrations/`; filenames are tracked verbatim
|
|
39
|
+
in `_spine_migrations`, so keep them globally unique.
|
|
40
|
+
|
|
41
|
+
3. **Write idempotent SQL.** Migrations run once per filename — but
|
|
42
|
+
installs re-run them against databases in unknown drifted states, so
|
|
43
|
+
guard everything:
|
|
44
|
+
|
|
45
|
+
```sql
|
|
46
|
+
ALTER TABLE public.my_table
|
|
47
|
+
ADD COLUMN IF NOT EXISTS my_col text;
|
|
48
|
+
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_my_table_col
|
|
50
|
+
ON public.my_table (my_col);
|
|
51
|
+
|
|
52
|
+
INSERT INTO public.roles (slug, name, is_active)
|
|
53
|
+
SELECT 'my_role', 'My Role', true
|
|
54
|
+
WHERE NOT EXISTS (SELECT 1 FROM public.roles WHERE slug = 'my_role');
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`CREATE TABLE IF NOT EXISTS`, `ADD COLUMN IF NOT EXISTS`,
|
|
58
|
+
`DROP … IF EXISTS`, `ON CONFLICT … DO NOTHING`, and
|
|
59
|
+
`INSERT … SELECT … WHERE NOT EXISTS` are the house patterns (see
|
|
60
|
+
`.framework/migrations/053_builtin_action_seeds.sql`).
|
|
61
|
+
|
|
62
|
+
4. **Apply it.**
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx spine-framework migrate # framework + custom
|
|
66
|
+
npx spine-framework migrate --dry-run # list pending only
|
|
67
|
+
npx spine-framework migrate --framework-only
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Requires `SUPABASE_URL` (a hosted `https://<ref>.supabase.co` URL — the
|
|
71
|
+
CLI rejects anything else before connecting) and
|
|
72
|
+
`SUPABASE_DB_PASSWORD` or `--db-password`
|
|
73
|
+
(`.framework/cli/commands/migrate.ts`).
|
|
74
|
+
|
|
75
|
+
5. **Check state.** `npx spine-framework migrations list` reads the
|
|
76
|
+
`_spine_migrations` ledger; `migrations status` diffs local files vs
|
|
77
|
+
applied — see [CLI reference](../reference/cli.md).
|
|
78
|
+
|
|
79
|
+
## Verify
|
|
80
|
+
|
|
81
|
+
- `_spine_migrations` contains the filename: each file runs in a
|
|
82
|
+
transaction, records `INSERT INTO _spine_migrations (filename)`, and the
|
|
83
|
+
runner **stops at the first failure** — fix and re-run
|
|
84
|
+
(`.framework/cli/commands/migrate.ts`).
|
|
85
|
+
- Re-running `migrate` reports the file as skipped.
|
|
86
|
+
|
|
87
|
+
## Common mistakes
|
|
88
|
+
|
|
89
|
+
- **Renaming a file after it ran.** Tracking is by filename — a rename
|
|
90
|
+
makes it a new migration and re-applies the SQL.
|
|
91
|
+
- **Non-idempotent DDL.** `ALTER TABLE … ADD COLUMN` without
|
|
92
|
+
`IF NOT EXISTS` fails on the second run (e.g. a fresh install that
|
|
93
|
+
already got the column from a later reconciliation migration).
|
|
94
|
+
- **Gap numbers.** `022`–`031`, `039`–`041`, `063` are reserved — using
|
|
95
|
+
them collides with framework history.
|
|
96
|
+
- **Depending on framework ordering.** Custom files all sort *after* every
|
|
97
|
+
framework file; you cannot interleave a custom migration between
|
|
98
|
+
framework ones — express dependencies inside the SQL (e.g.
|
|
99
|
+
`CREATE OR REPLACE`, existence checks).
|
|
100
|
+
- **`SUPABASE_URL` with a port, path, or credentials** — rejected by the
|
|
101
|
+
CLI before it ever connects.
|
|
102
|
+
|
|
103
|
+
## Reference
|
|
104
|
+
|
|
105
|
+
- [Database reference](../reference/database.md) — table map, numbering
|
|
106
|
+
gaps
|
|
107
|
+
- [CLI reference](../reference/cli.md) — `migrate`, `migrations list`,
|
|
108
|
+
`migrations status`
|
|
109
|
+
- [Package and install an app](./package-and-install-an-app.md) — app
|
|
110
|
+
installers run `migrate` automatically before seeding
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Testing
|
|
3
|
+
audience: both
|
|
4
|
+
kind: entity
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/src/pages/admin/TestingDashboard.tsx
|
|
9
|
+
- .framework/src/pages/admin/TestRunDetailPage.tsx
|
|
10
|
+
- .framework/functions/tests.ts
|
|
11
|
+
- .framework/migrations/001_core_schema.sql
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## What this is for
|
|
15
|
+
|
|
16
|
+
The **Testing** page (`/spine-framework/admin/testing`) is a dashboard over `test_runs`/`test_results` — rows written by external test harnesses (e.g. suite runs triggered from the IDE). It summarizes per-suite health and lists recent runs so you can spot a red suite without opening the CI output.
|
|
17
|
+
|
|
18
|
+
## Fields and settings
|
|
19
|
+
|
|
20
|
+
- **Suite cards** — one per suite (`unit`, `integration`, `api`, `ui`): last status, last run time, last pass/fail/total counts, and total runs (last 30 days).
|
|
21
|
+
- **Recent runs table** — last 20 runs: suite, status (`passed`/`failed`/`running`/etc.), started time, passed, failed, total, duration.
|
|
22
|
+
- **Row click** — navigates to `testing/:run_id` for the individual test-case results (`name`, `status`, `message`, `duration_ms` per case).
|
|
23
|
+
- No actions on this page — runs are produced by harnesses, not created here.
|
|
24
|
+
|
|
25
|
+
**Downstream effects**
|
|
26
|
+
|
|
27
|
+
- Backed by `/api/tests` (function `tests`): `action=stats` aggregates the last 30 days by suite; `action=list` returns recent runs; `action=get&id=` returns one run plus its ordered `test_results`. All require system context.
|
|
28
|
+
- `system` health checks (`/api/system`) and `spine-framework doctor` also read `test_runs` — a stale suite shows up in deploy health.
|
|
29
|
+
- Rows are append-only; there is no cleanup action on this API today.
|
|
30
|
+
|
|
31
|
+
## Minimal configuration
|
|
32
|
+
|
|
33
|
+
Nothing on the page — have your harness insert `test_runs` (+ `test_results` per case) and the dashboard populates itself.
|
|
34
|
+
|
|
35
|
+
## Full configuration
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
GET /.netlify/functions/tests?action=stats # per-suite 30-day rollup
|
|
39
|
+
GET /.netlify/functions/tests?action=list&limit=20
|
|
40
|
+
GET /.netlify/functions/tests?action=get&id=<run uuid> # run + ordered test_results
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Example
|
|
44
|
+
|
|
45
|
+
A nightly job runs the `integration` suite and writes a `test_runs` row with `failed: 2`. The dashboard's Integration card flips red with last-run counts; click the row to see which two `test_results` failed and their messages.
|
|
46
|
+
|
|
47
|
+
## Core files
|
|
48
|
+
|
|
49
|
+
- `.framework/src/pages/admin/TestingDashboard.tsx` — this page
|
|
50
|
+
- `.framework/src/pages/admin/TestRunDetailPage.tsx` — run detail route (`testing/:run_id`)
|
|
51
|
+
- `.framework/functions/tests.ts` — API (`stats`, `list`, `get`)
|
|
52
|
+
- Tables: `public.test_runs`, `public.test_results`
|
|
53
|
+
|
|
54
|
+
## Agent notes
|
|
55
|
+
|
|
56
|
+
- Known mismatch: `TestRunDetailPage` fetches `/api/test-runs?action=...`, but the shipped function is `tests` — the detail route currently fails to load. Use `tests?action=get&id=` until the page is repointed.
|
|
57
|
+
- The suites list is hardcoded (`unit`, `integration`, `api`, `ui`) — a run with a different `suite` value won't get a card, though it still appears in Recent runs.
|
|
58
|
+
- `stats` is a fixed 30-day window capped at 200 rows — high-volume suites need direct table queries for longer trends.
|
|
59
|
+
- Nothing here triggers runs; the "no runs yet" empty state points at running suites from your IDE/CI.
|