spine-framework 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.framework/README.md +129 -0
- package/.framework/cli/bin.cjs +14 -0
- package/.framework/cli/commands/agents.ts +153 -0
- package/.framework/cli/commands/auth.ts +94 -0
- package/.framework/cli/commands/create-app.ts +185 -0
- package/.framework/cli/commands/dev.ts +295 -0
- package/.framework/cli/commands/doctor.ts +442 -0
- package/.framework/cli/commands/generate.ts +332 -0
- package/.framework/cli/commands/init.ts +272 -0
- package/.framework/cli/commands/install-app.ts +391 -0
- package/.framework/cli/commands/items.ts +253 -0
- package/.framework/cli/commands/migrations.ts +141 -0
- package/.framework/cli/commands/pipelines.ts +166 -0
- package/.framework/cli/commands/status.ts +197 -0
- package/.framework/cli/commands/system.ts +184 -0
- package/.framework/cli/commands/test.ts +227 -0
- package/.framework/cli/commands/uninstall-app.ts +166 -0
- package/.framework/cli/context.ts +268 -0
- package/.framework/cli/env-loader.ts +36 -0
- package/.framework/cli/index.ts +106 -0
- package/.framework/cli/welcome.cjs +45 -0
- package/.framework/docs/API.md +384 -0
- package/.framework/docs/STABILITY.md +52 -0
- package/.framework/docs/admin-routes.md +76 -0
- package/.framework/docs/api-docs-progress.md +38 -0
- package/.framework/docs/api-governance.md +146 -0
- package/.framework/docs/api-testing-results.md +212 -0
- package/.framework/docs/apis/admin-configs.md +567 -0
- package/.framework/docs/apis/admin-data.md +272 -0
- package/.framework/docs/apis/index.md +231 -0
- package/.framework/docs/apis/internal.md +295 -0
- package/.framework/docs/apis/runtime.md +537 -0
- package/.framework/docs/assembly-launch-guide.md +138 -0
- package/.framework/docs/audit-results.md +590 -0
- package/.framework/docs/authorization-model.md +170 -0
- package/.framework/docs/db-api-inventory.md +95 -0
- package/.framework/docs/examples/custom-app/README.md +77 -0
- package/.framework/docs/examples/custom-function/README.md +27 -0
- package/.framework/docs/examples/custom-function/handler.ts +48 -0
- package/.framework/docs/examples/custom-webhook/README.md +68 -0
- package/.framework/docs/gap-remediation-backlog.md +103 -0
- package/.framework/docs/guides/cli-guide.md +224 -0
- package/.framework/docs/guides/getting-started.md +103 -0
- package/.framework/docs/guides/import-guide.md +193 -0
- package/.framework/docs/guides/testing-guide.md +229 -0
- package/.framework/docs/permission-examples.md +326 -0
- package/.framework/docs/ui-adoption-verification.md +111 -0
- package/.framework/docs/ui-api-coverage.md +84 -0
- package/.framework/docs/v2-compatibility-audit.md +228 -0
- package/.framework/functions/.gitkeep +1 -0
- package/.framework/functions/_shared/agent-runner.ts +1097 -0
- package/.framework/functions/_shared/app-manifest.ts +184 -0
- package/.framework/functions/_shared/audit.ts +150 -0
- package/.framework/functions/_shared/db.ts +174 -0
- package/.framework/functions/_shared/index.ts +382 -0
- package/.framework/functions/_shared/middleware.ts +490 -0
- package/.framework/functions/_shared/permissions.ts +1325 -0
- package/.framework/functions/_shared/pipeline-runner.ts +731 -0
- package/.framework/functions/_shared/principal.ts +760 -0
- package/.framework/functions/_shared/schema-utils.ts +967 -0
- package/.framework/functions/_shared/testing.ts +258 -0
- package/.framework/functions/_shared/trigger-engine.ts +425 -0
- package/.framework/functions/_shared/webhook-registration.ts +168 -0
- package/.framework/functions/_shared/webhook-registry.ts +129 -0
- package/.framework/functions/account-nodes.ts +111 -0
- package/.framework/functions/admin-data.ts +606 -0
- package/.framework/functions/ai-agents.ts +323 -0
- package/.framework/functions/api-keys.ts +376 -0
- package/.framework/functions/apps.ts +483 -0
- package/.framework/functions/auth.ts +196 -0
- package/.framework/functions/debug-auth.ts +107 -0
- package/.framework/functions/embeddings.ts +556 -0
- package/.framework/functions/integration-routes.ts +523 -0
- package/.framework/functions/integrations.ts +319 -0
- package/.framework/functions/item-progress.ts +272 -0
- package/.framework/functions/logs.ts +438 -0
- package/.framework/functions/observability.ts +275 -0
- package/.framework/functions/pipeline-executions.ts +494 -0
- package/.framework/functions/pipelines.ts +485 -0
- package/.framework/functions/prompt-configs.ts +339 -0
- package/.framework/functions/roles.ts +387 -0
- package/.framework/functions/system-cron.ts +742 -0
- package/.framework/functions/system.ts +323 -0
- package/.framework/functions/tests.ts +119 -0
- package/.framework/functions/timers.ts +357 -0
- package/.framework/functions/triggers.ts +563 -0
- package/.framework/functions/types.ts +604 -0
- package/.framework/migrations/000_foundation.sql +1256 -0
- package/.framework/migrations/001_seed.sql +92 -0
- package/.framework/migrations/002_seed_constraints.sql +13 -0
- package/.framework/migrations/003_auth_user_trigger.sql +59 -0
- package/.framework/src/App.tsx +126 -0
- package/.framework/src/apps/admin/index.tsx +173 -0
- package/.framework/src/components/AppWrapper.tsx +56 -0
- package/.framework/src/components/CustomAppLoader.tsx +116 -0
- package/.framework/src/components/admin/AdminListPage.tsx +151 -0
- package/.framework/src/components/admin/AdminSidebar.tsx +166 -0
- package/.framework/src/components/admin/AdminStatsCard.tsx +62 -0
- package/.framework/src/components/admin/SortableTableHeader.tsx +42 -0
- package/.framework/src/components/app-shell/GenericAppShell.tsx +181 -0
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +200 -0
- package/.framework/src/components/app-shell/GenericListPage.tsx +116 -0
- package/.framework/src/components/app-sidebar.tsx +228 -0
- package/.framework/src/components/auth/ProtectedRoute.tsx +88 -0
- package/.framework/src/components/layout/AppShell.tsx +91 -0
- package/.framework/src/components/layout/Header.tsx +88 -0
- package/.framework/src/components/layout/Layout.tsx +95 -0
- package/.framework/src/components/layout/Sidebar.tsx +329 -0
- package/.framework/src/components/runtime/DataDetailHeader.tsx +77 -0
- package/.framework/src/components/runtime/DataDetailPage.tsx +171 -0
- package/.framework/src/components/runtime/DataFilters.tsx +91 -0
- package/.framework/src/components/runtime/DataHeader.tsx +68 -0
- package/.framework/src/components/runtime/DataListPage.tsx +124 -0
- package/.framework/src/components/runtime/DataStats.tsx +70 -0
- package/.framework/src/components/runtime/DataTable.tsx +174 -0
- package/.framework/src/components/runtime/SchemaDetailForm.tsx +134 -0
- package/.framework/src/components/runtime/index.ts +18 -0
- package/.framework/src/components/search-form.tsx +29 -0
- package/.framework/src/components/shared/AgentView.tsx +213 -0
- package/.framework/src/components/shared/FieldRenderer.tsx +478 -0
- package/.framework/src/components/shared/SchemaFields.tsx +226 -0
- package/.framework/src/components/ui/DataTable.tsx +343 -0
- package/.framework/src/components/ui/Form.tsx +281 -0
- package/.framework/src/components/ui/ItemCard.tsx +296 -0
- package/.framework/src/components/ui/ItemListView.tsx +308 -0
- package/.framework/src/components/ui/LoadingSpinner.tsx +52 -0
- package/.framework/src/components/ui/Modal.tsx +61 -0
- package/.framework/src/components/ui/RichTextEditor.tsx +210 -0
- package/.framework/src/components/ui/accordion.tsx +82 -0
- package/.framework/src/components/ui/alert-dialog.tsx +197 -0
- package/.framework/src/components/ui/alert.tsx +76 -0
- package/.framework/src/components/ui/aspect-ratio.tsx +11 -0
- package/.framework/src/components/ui/avatar.tsx +110 -0
- package/.framework/src/components/ui/badge.tsx +49 -0
- package/.framework/src/components/ui/breadcrumb.tsx +122 -0
- package/.framework/src/components/ui/button-group.tsx +83 -0
- package/.framework/src/components/ui/button.tsx +65 -0
- package/.framework/src/components/ui/calendar.tsx +222 -0
- package/.framework/src/components/ui/card.tsx +100 -0
- package/.framework/src/components/ui/carousel.tsx +240 -0
- package/.framework/src/components/ui/chart.tsx +373 -0
- package/.framework/src/components/ui/checkbox.tsx +31 -0
- package/.framework/src/components/ui/collapsible.tsx +33 -0
- package/.framework/src/components/ui/combobox.tsx +299 -0
- package/.framework/src/components/ui/command.tsx +193 -0
- package/.framework/src/components/ui/context-menu.tsx +261 -0
- package/.framework/src/components/ui/dialog.tsx +165 -0
- package/.framework/src/components/ui/direction.tsx +22 -0
- package/.framework/src/components/ui/drawer.tsx +132 -0
- package/.framework/src/components/ui/dropdown-menu.tsx +269 -0
- package/.framework/src/components/ui/empty.tsx +104 -0
- package/.framework/src/components/ui/field.tsx +238 -0
- package/.framework/src/components/ui/hover-card.tsx +42 -0
- package/.framework/src/components/ui/input-group.tsx +153 -0
- package/.framework/src/components/ui/input-otp.tsx +87 -0
- package/.framework/src/components/ui/input.tsx +19 -0
- package/.framework/src/components/ui/item.tsx +196 -0
- package/.framework/src/components/ui/kbd.tsx +26 -0
- package/.framework/src/components/ui/label.tsx +22 -0
- package/.framework/src/components/ui/menubar.tsx +277 -0
- package/.framework/src/components/ui/native-select.tsx +61 -0
- package/.framework/src/components/ui/navigation-menu.tsx +164 -0
- package/.framework/src/components/ui/pagination.tsx +129 -0
- package/.framework/src/components/ui/popover.tsx +87 -0
- package/.framework/src/components/ui/progress.tsx +31 -0
- package/.framework/src/components/ui/radio-group.tsx +42 -0
- package/.framework/src/components/ui/resizable.tsx +50 -0
- package/.framework/src/components/ui/scroll-area.tsx +53 -0
- package/.framework/src/components/ui/select.tsx +195 -0
- package/.framework/src/components/ui/separator.tsx +26 -0
- package/.framework/src/components/ui/sheet.tsx +145 -0
- package/.framework/src/components/ui/sidebar.tsx +706 -0
- package/.framework/src/components/ui/skeleton.tsx +13 -0
- package/.framework/src/components/ui/slider.tsx +59 -0
- package/.framework/src/components/ui/sonner.tsx +47 -0
- package/.framework/src/components/ui/spinner.tsx +10 -0
- package/.framework/src/components/ui/switch.tsx +33 -0
- package/.framework/src/components/ui/table-primitives.tsx +141 -0
- package/.framework/src/components/ui/table.tsx +114 -0
- package/.framework/src/components/ui/tabs.tsx +90 -0
- package/.framework/src/components/ui/textarea.tsx +18 -0
- package/.framework/src/components/ui/toggle-group.tsx +89 -0
- package/.framework/src/components/ui/toggle.tsx +45 -0
- package/.framework/src/components/ui/tooltip.tsx +57 -0
- package/.framework/src/contexts/AppContext.tsx +133 -0
- package/.framework/src/contexts/AuthContext.tsx +371 -0
- package/.framework/src/hooks/use-mobile.ts +19 -0
- package/.framework/src/hooks/useApi.ts +526 -0
- package/.framework/src/hooks/useApps.ts +114 -0
- package/.framework/src/hooks/useEntityList.ts +190 -0
- package/.framework/src/hooks/useEntityRecord.ts +308 -0
- package/.framework/src/hooks/useForm.ts +307 -0
- package/.framework/src/hooks/useListSchema.ts +264 -0
- package/.framework/src/hooks/useSchemaRecord.ts +223 -0
- package/.framework/src/index.css +128 -0
- package/.framework/src/lib/api.ts +156 -0
- package/.framework/src/lib/supabase.ts +94 -0
- package/.framework/src/lib/utils.ts +317 -0
- package/.framework/src/main.tsx +27 -0
- package/.framework/src/pages/DashboardPage.tsx +181 -0
- package/.framework/src/pages/NotFoundPage.tsx +39 -0
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +161 -0
- package/.framework/src/pages/admin/AIAgentsPage.tsx +318 -0
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +199 -0
- package/.framework/src/pages/admin/APIKeysPage.tsx +303 -0
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +523 -0
- package/.framework/src/pages/admin/AppDetailPage.tsx +493 -0
- package/.framework/src/pages/admin/AppsPage.tsx +355 -0
- package/.framework/src/pages/admin/DesignedPage.tsx +491 -0
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +534 -0
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +424 -0
- package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +176 -0
- package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +109 -0
- package/.framework/src/pages/admin/IntegratedDashboard.tsx +402 -0
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +187 -0
- package/.framework/src/pages/admin/IntegrationsPage.tsx +301 -0
- package/.framework/src/pages/admin/LogsPage.tsx +283 -0
- package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +85 -0
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +470 -0
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +183 -0
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +279 -0
- package/.framework/src/pages/admin/PipelinesPage.tsx +390 -0
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +299 -0
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +292 -0
- package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +434 -0
- package/.framework/src/pages/admin/RoleDetailPage.tsx +273 -0
- package/.framework/src/pages/admin/RolesPage.tsx +292 -0
- package/.framework/src/pages/admin/SelectTestPage.tsx +61 -0
- package/.framework/src/pages/admin/ShadcnTestPage.tsx +588 -0
- package/.framework/src/pages/admin/SimpleDashboard.tsx +387 -0
- package/.framework/src/pages/admin/TestRunDetailPage.tsx +172 -0
- package/.framework/src/pages/admin/TestingDashboard.tsx +257 -0
- package/.framework/src/pages/admin/TimerDetailPage.tsx +151 -0
- package/.framework/src/pages/admin/TimersPage.tsx +376 -0
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +149 -0
- package/.framework/src/pages/admin/TriggersPage.tsx +381 -0
- package/.framework/src/pages/admin/TypeDetailPage.tsx +694 -0
- package/.framework/src/pages/admin/TypesPage.tsx +295 -0
- package/.framework/src/pages/auth/LoginPage.tsx +188 -0
- package/.framework/src/pages/auth/RegisterPage.tsx +163 -0
- package/.framework/src/pages/spine-framework/APIPage.tsx +17 -0
- package/.framework/src/pages/spine-framework/CLIPage.tsx +25 -0
- package/.framework/src/types/auth.ts +125 -0
- package/.framework/src/types/types.ts +407 -0
- package/STRUCTURE.md +150 -0
- package/config/components.json +25 -0
- package/config/deno.lock +108 -0
- package/config/package-lock.json +17183 -0
- package/config/postcss.config.cjs +10 -0
- package/config/tailwind.config.cjs +78 -0
- package/config/tsconfig.build.json +32 -0
- package/config/tsconfig.cli.json +18 -0
- package/config/tsconfig.json +41 -0
- package/config/tsconfig.node.json +17 -0
- package/config/tsconfig.node.tsbuildinfo +1 -0
- package/config/tsconfig.tsbuildinfo +1 -0
- package/config/typedoc.json +16 -0
- package/config/vite.config.d.ts +2 -0
- package/config/vite.config.ts +72 -0
- package/dist/cli/commands/agents.d.ts +39 -0
- package/dist/cli/commands/agents.d.ts.map +1 -0
- package/dist/cli/commands/auth.d.ts +36 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/create-app.d.ts +23 -0
- package/dist/cli/commands/create-app.d.ts.map +1 -0
- package/dist/cli/commands/dev.d.ts +39 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/doctor.d.ts +42 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/generate.d.ts +36 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/init.d.ts +30 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/install-app.d.ts +30 -0
- package/dist/cli/commands/install-app.d.ts.map +1 -0
- package/dist/cli/commands/items.d.ts +45 -0
- package/dist/cli/commands/items.d.ts.map +1 -0
- package/dist/cli/commands/migrations.d.ts +41 -0
- package/dist/cli/commands/migrations.d.ts.map +1 -0
- package/dist/cli/commands/pipelines.d.ts +40 -0
- package/dist/cli/commands/pipelines.d.ts.map +1 -0
- package/dist/cli/commands/status.d.ts +23 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/system.d.ts +29 -0
- package/dist/cli/commands/system.d.ts.map +1 -0
- package/dist/cli/commands/test.d.ts +46 -0
- package/dist/cli/commands/test.d.ts.map +1 -0
- package/dist/cli/commands/uninstall-app.d.ts +23 -0
- package/dist/cli/commands/uninstall-app.d.ts.map +1 -0
- package/dist/cli/context.d.ts +88 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/env-loader.d.ts +14 -0
- package/dist/cli/env-loader.d.ts.map +1 -0
- package/dist/cli/index.d.ts +41 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/functions/_shared/agent-runner.d.ts +156 -0
- package/dist/functions/_shared/agent-runner.d.ts.map +1 -0
- package/dist/functions/_shared/app-manifest.d.ts +68 -0
- package/dist/functions/_shared/app-manifest.d.ts.map +1 -0
- package/dist/functions/_shared/audit.d.ts +91 -0
- package/dist/functions/_shared/audit.d.ts.map +1 -0
- package/dist/functions/_shared/db.d.ts +125 -0
- package/dist/functions/_shared/db.d.ts.map +1 -0
- package/dist/functions/_shared/index.d.ts +298 -0
- package/dist/functions/_shared/index.d.ts.map +1 -0
- package/dist/functions/_shared/middleware.d.ts +315 -0
- package/dist/functions/_shared/middleware.d.ts.map +1 -0
- package/dist/functions/_shared/permissions.d.ts +626 -0
- package/dist/functions/_shared/permissions.d.ts.map +1 -0
- package/dist/functions/_shared/pipeline-runner.d.ts +124 -0
- package/dist/functions/_shared/pipeline-runner.d.ts.map +1 -0
- package/dist/functions/_shared/principal.d.ts +284 -0
- package/dist/functions/_shared/principal.d.ts.map +1 -0
- package/dist/functions/_shared/schema-utils.d.ts +181 -0
- package/dist/functions/_shared/schema-utils.d.ts.map +1 -0
- package/dist/functions/_shared/testing.d.ts +172 -0
- package/dist/functions/_shared/testing.d.ts.map +1 -0
- package/dist/functions/_shared/trigger-engine.d.ts +140 -0
- package/dist/functions/_shared/trigger-engine.d.ts.map +1 -0
- package/dist/functions/_shared/webhook-registration.d.ts +81 -0
- package/dist/functions/_shared/webhook-registration.d.ts.map +1 -0
- package/dist/functions/_shared/webhook-registry.d.ts +57 -0
- package/dist/functions/_shared/webhook-registry.d.ts.map +1 -0
- package/dist/functions/account-nodes.d.ts +48 -0
- package/dist/functions/account-nodes.d.ts.map +1 -0
- package/dist/functions/admin-data.d.ts +178 -0
- package/dist/functions/admin-data.d.ts.map +1 -0
- package/dist/functions/ai-agents.d.ts +125 -0
- package/dist/functions/ai-agents.d.ts.map +1 -0
- package/dist/functions/api-keys.d.ts +140 -0
- package/dist/functions/api-keys.d.ts.map +1 -0
- package/dist/functions/apps.d.ts +163 -0
- package/dist/functions/apps.d.ts.map +1 -0
- package/dist/functions/auth.d.ts +74 -0
- package/dist/functions/auth.d.ts.map +1 -0
- package/dist/functions/debug-auth.d.ts +33 -0
- package/dist/functions/debug-auth.d.ts.map +1 -0
- package/dist/functions/embeddings.d.ts +205 -0
- package/dist/functions/embeddings.d.ts.map +1 -0
- package/dist/functions/integration-routes.d.ts +45 -0
- package/dist/functions/integration-routes.d.ts.map +1 -0
- package/dist/functions/integrations.d.ts +124 -0
- package/dist/functions/integrations.d.ts.map +1 -0
- package/dist/functions/item-progress.d.ts +41 -0
- package/dist/functions/item-progress.d.ts.map +1 -0
- package/dist/functions/logs.d.ts +162 -0
- package/dist/functions/logs.d.ts.map +1 -0
- package/dist/functions/observability.d.ts +123 -0
- package/dist/functions/observability.d.ts.map +1 -0
- package/dist/functions/pipeline-executions.d.ts +190 -0
- package/dist/functions/pipeline-executions.d.ts.map +1 -0
- package/dist/functions/pipelines.d.ts +171 -0
- package/dist/functions/pipelines.d.ts.map +1 -0
- package/dist/functions/prompt-configs.d.ts +125 -0
- package/dist/functions/prompt-configs.d.ts.map +1 -0
- package/dist/functions/roles.d.ts +118 -0
- package/dist/functions/roles.d.ts.map +1 -0
- package/dist/functions/system-cron.d.ts +65 -0
- package/dist/functions/system-cron.d.ts.map +1 -0
- package/dist/functions/system.d.ts +29 -0
- package/dist/functions/system.d.ts.map +1 -0
- package/dist/functions/tests.d.ts +28 -0
- package/dist/functions/tests.d.ts.map +1 -0
- package/dist/functions/timers.d.ts +139 -0
- package/dist/functions/timers.d.ts.map +1 -0
- package/dist/functions/triggers.d.ts +203 -0
- package/dist/functions/triggers.d.ts.map +1 -0
- package/dist/functions/types.d.ts +151 -0
- package/dist/functions/types.d.ts.map +1 -0
- package/dist/src/types/types.d.ts +364 -0
- package/dist/src/types/types.d.ts.map +1 -0
- package/package.json +192 -0
- package/scripts/app-install-cli.ts +286 -0
- package/scripts/assemble-frontend.sh +79 -0
- package/scripts/assemble-functions.sh +62 -0
- package/scripts/assemble.sh +35 -0
- package/scripts/boundary-check.sh +106 -0
- package/scripts/build-manifest.sh +80 -0
- package/scripts/check-core-integrity.sh +82 -0
- package/scripts/ingest-chunks.cjs +202 -0
- package/scripts/kb-chunk-parser.cjs +312 -0
- package/scripts/kb-chunk-parser.ts +330 -0
- package/scripts/load-test-app-install.ts +484 -0
- package/scripts/netlify-dev-wrapper.sh +22 -0
- package/scripts/verify-integrity.sh +69 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module testing
|
|
3
|
+
* @audience custom-developer
|
|
4
|
+
* @layer shared-util
|
|
5
|
+
* @stability evolving
|
|
6
|
+
*
|
|
7
|
+
* Testing utilities for custom code developers.
|
|
8
|
+
* Use these to test your custom functions without full deployment.
|
|
9
|
+
*
|
|
10
|
+
* **Usage in custom function tests:**
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { makeTestContext, mockPrincipal, cleanup } from '@core/testing'
|
|
13
|
+
*
|
|
14
|
+
* describe('My Custom Handler', () => {
|
|
15
|
+
* it('should process items', async () => {
|
|
16
|
+
* const ctx = makeTestContext({
|
|
17
|
+
* principal: mockPrincipal({ roles: ['member'] }),
|
|
18
|
+
* accountId: 'test-account'
|
|
19
|
+
* })
|
|
20
|
+
*
|
|
21
|
+
* const result = await myHandler(mockEvent, ctx)
|
|
22
|
+
* expect(result.status).toBe('success')
|
|
23
|
+
*
|
|
24
|
+
* cleanup()
|
|
25
|
+
* })
|
|
26
|
+
* })
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @seeAlso .framework/tests/unit/core-isolation.test.ts (examples)
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { adminDb } from './db'
|
|
33
|
+
|
|
34
|
+
export interface TestContext {
|
|
35
|
+
db: typeof adminDb
|
|
36
|
+
principal: TestPrincipal
|
|
37
|
+
logger: TestLogger
|
|
38
|
+
accountId: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TestPrincipal {
|
|
42
|
+
id: string
|
|
43
|
+
account_id: string
|
|
44
|
+
roles: string[]
|
|
45
|
+
permissions: Record<string, any>
|
|
46
|
+
email?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface TestLogger {
|
|
50
|
+
info: (msg: string, meta?: any) => void
|
|
51
|
+
warn: (msg: string, meta?: any) => void
|
|
52
|
+
error: (msg: string, meta?: any) => void
|
|
53
|
+
debug: (msg: string, meta?: any) => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Creates a mock principal for testing.
|
|
58
|
+
*
|
|
59
|
+
* @param overrides - Override default principal properties
|
|
60
|
+
* @returns Mock principal object
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* const admin = mockPrincipal({
|
|
65
|
+
* roles: ['system_admin'],
|
|
66
|
+
* permissions: { all: true }
|
|
67
|
+
* })
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export function mockPrincipal(overrides: Partial<TestPrincipal> = {}): TestPrincipal {
|
|
71
|
+
return {
|
|
72
|
+
id: 'test-user-id',
|
|
73
|
+
account_id: 'test-account-id',
|
|
74
|
+
roles: ['member'],
|
|
75
|
+
permissions: {},
|
|
76
|
+
email: 'test@example.com',
|
|
77
|
+
...overrides
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Creates a mock logger that captures logs for assertions.
|
|
83
|
+
*
|
|
84
|
+
* @returns TestLogger with log capture
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* const logger = mockLogger()
|
|
89
|
+
* await myFunction(logger)
|
|
90
|
+
* expect(logger.getLogs()).toContain('Processing started')
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export function mockLogger(): TestLogger & { getLogs: () => string[] } {
|
|
94
|
+
const logs: string[] = []
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
info: (msg: string, meta?: any) => {
|
|
98
|
+
logs.push(`INFO: ${msg}`)
|
|
99
|
+
if (meta) console.log('INFO:', msg, meta)
|
|
100
|
+
},
|
|
101
|
+
warn: (msg: string, meta?: any) => {
|
|
102
|
+
logs.push(`WARN: ${msg}`)
|
|
103
|
+
if (meta) console.warn('WARN:', msg, meta)
|
|
104
|
+
},
|
|
105
|
+
error: (msg: string, meta?: any) => {
|
|
106
|
+
logs.push(`ERROR: ${msg}`)
|
|
107
|
+
if (meta) console.error('ERROR:', msg, meta)
|
|
108
|
+
},
|
|
109
|
+
debug: (msg: string, meta?: any) => {
|
|
110
|
+
logs.push(`DEBUG: ${msg}`)
|
|
111
|
+
if (meta) console.debug('DEBUG:', msg, meta)
|
|
112
|
+
},
|
|
113
|
+
getLogs: () => [...logs]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Creates a test context for handler testing.
|
|
119
|
+
*
|
|
120
|
+
* @param options - Test context configuration
|
|
121
|
+
* @returns TestContext ready for handler
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* const ctx = makeTestContext({
|
|
126
|
+
* principal: mockPrincipal({ roles: ['operator'] }),
|
|
127
|
+
* accountId: 'my-account'
|
|
128
|
+
* })
|
|
129
|
+
*
|
|
130
|
+
* const result = await handler(mockEvent, ctx)
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export function makeTestContext(options: {
|
|
134
|
+
principal?: TestPrincipal
|
|
135
|
+
accountId?: string
|
|
136
|
+
logger?: TestLogger
|
|
137
|
+
} = {}): TestContext {
|
|
138
|
+
return {
|
|
139
|
+
db: adminDb,
|
|
140
|
+
principal: options.principal || mockPrincipal(),
|
|
141
|
+
logger: options.logger || mockLogger(),
|
|
142
|
+
accountId: options.accountId || options.principal?.account_id || 'test-account'
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Creates a mock API Gateway event.
|
|
148
|
+
*
|
|
149
|
+
* @param overrides - Override default event properties
|
|
150
|
+
* @returns Mock event object
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```ts
|
|
154
|
+
* const event = mockEvent({
|
|
155
|
+
* httpMethod: 'POST',
|
|
156
|
+
* body: JSON.stringify({ item_id: '123' })
|
|
157
|
+
* })
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export function mockEvent(overrides: any = {}): any {
|
|
161
|
+
return {
|
|
162
|
+
httpMethod: 'GET',
|
|
163
|
+
path: '/api/test',
|
|
164
|
+
headers: {},
|
|
165
|
+
queryStringParameters: {},
|
|
166
|
+
body: null,
|
|
167
|
+
...overrides
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Creates a mock Netlify function context.
|
|
173
|
+
*
|
|
174
|
+
* @returns Mock context object
|
|
175
|
+
*/
|
|
176
|
+
export function mockNetlifyContext(): any {
|
|
177
|
+
return {
|
|
178
|
+
awsRequestId: 'test-request-id',
|
|
179
|
+
callbackWaitsForEmptyEventLoop: true,
|
|
180
|
+
functionName: 'test-function',
|
|
181
|
+
memoryLimitInMB: '1024',
|
|
182
|
+
invokedFunctionArn: 'arn:aws:lambda:test',
|
|
183
|
+
getRemainingTimeInMillis: () => 30000
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Cleanup function for tests.
|
|
189
|
+
* Clears caches and resets state.
|
|
190
|
+
*/
|
|
191
|
+
export function cleanup(): void {
|
|
192
|
+
// Clear any caches or state that might persist between tests
|
|
193
|
+
// This is a placeholder - actual implementation would clear specific state
|
|
194
|
+
console.log('Test cleanup completed')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Setup helper for test files.
|
|
199
|
+
* Returns utilities commonly needed in tests.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```ts
|
|
203
|
+
* import { setupTests } from '@core/testing'
|
|
204
|
+
*
|
|
205
|
+
* const { makeTestContext, mockPrincipal, cleanup } = setupTests()
|
|
206
|
+
*
|
|
207
|
+
* describe('My Tests', () => {
|
|
208
|
+
* afterEach(cleanup)
|
|
209
|
+
* // ... tests
|
|
210
|
+
* })
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
export function setupTests() {
|
|
214
|
+
return {
|
|
215
|
+
makeTestContext,
|
|
216
|
+
mockPrincipal,
|
|
217
|
+
mockLogger,
|
|
218
|
+
mockEvent,
|
|
219
|
+
mockNetlifyContext,
|
|
220
|
+
cleanup
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Asserts that a handler response matches expected structure.
|
|
226
|
+
*
|
|
227
|
+
* @param response - Handler response
|
|
228
|
+
* @param expectedStatus - Expected status code
|
|
229
|
+
*/
|
|
230
|
+
export function expectSuccessResponse(
|
|
231
|
+
response: { statusCode?: number; body?: string },
|
|
232
|
+
expectedStatus: number = 200
|
|
233
|
+
): void {
|
|
234
|
+
expect(response.statusCode).toBe(expectedStatus)
|
|
235
|
+
|
|
236
|
+
if (response.body) {
|
|
237
|
+
const body = JSON.parse(response.body)
|
|
238
|
+
expect(body.error).toBeUndefined()
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Asserts that a handler returned an error.
|
|
244
|
+
*
|
|
245
|
+
* @param response - Handler response
|
|
246
|
+
* @param expectedStatus - Expected error status code
|
|
247
|
+
*/
|
|
248
|
+
export function expectErrorResponse(
|
|
249
|
+
response: { statusCode?: number; body?: string },
|
|
250
|
+
expectedStatus: number = 400
|
|
251
|
+
): void {
|
|
252
|
+
expect(response.statusCode).toBe(expectedStatus)
|
|
253
|
+
|
|
254
|
+
if (response.body) {
|
|
255
|
+
const body = JSON.parse(response.body)
|
|
256
|
+
expect(body.error || body.message).toBeDefined()
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module trigger-engine
|
|
3
|
+
* @audience both
|
|
4
|
+
* @layer shared-core
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* Event-driven trigger dispatch system. API handlers call `fire*Triggers` after
|
|
8
|
+
* a successful write to check whether any active triggers should fire. Matching
|
|
9
|
+
* triggers cause `runPipeline` to be invoked with structured `triggerData`.
|
|
10
|
+
*
|
|
11
|
+
* Public surface:
|
|
12
|
+
* - `checkAndFireTriggers` — full trigger evaluation and firing loop
|
|
13
|
+
* - `fireCreateTriggers` — convenience wrapper for `*_created` events
|
|
14
|
+
* - `fireUpdateTriggers` — convenience wrapper for `*_updated` events
|
|
15
|
+
* - `fireDeleteTriggers` — convenience wrapper for `*_deleted` events
|
|
16
|
+
*
|
|
17
|
+
* Trigger condition evaluation (in order, all must pass):
|
|
18
|
+
* 1. `config.entity_type` — exact match on entityType param
|
|
19
|
+
* 2. `config.type_slug` — match on `entityData.type_slug` (items only)
|
|
20
|
+
* 3. `config.filters` — field-level predicates with operators
|
|
21
|
+
* (`$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$exists`)
|
|
22
|
+
*
|
|
23
|
+
* INVARIANT: trigger evaluation and firing errors are caught per-trigger —
|
|
24
|
+
* a single failing trigger never prevents other triggers from firing.
|
|
25
|
+
* INVARIANT: `checkAndFireTriggers` never throws — all errors are caught
|
|
26
|
+
* and logged. Callers do not need to wrap in try/catch.
|
|
27
|
+
* INVARIANT: `adminDb` (service role) is used for trigger lookups and stats
|
|
28
|
+
* updates; `runPipeline` then uses the passed `ctx` for stage execution.
|
|
29
|
+
*
|
|
30
|
+
* @seeAlso pipeline-runner.ts (runPipeline — called when a trigger fires)
|
|
31
|
+
* @seeAlso audit.ts (emitAudit for trigger.fired / trigger.failed)
|
|
32
|
+
* @seeAlso index.ts (fire*Triggers re-exported for v2-custom/ and CLI)
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { CoreContext } from './middleware'
|
|
36
|
+
import { adminDb } from './db'
|
|
37
|
+
import { emitAudit } from './audit'
|
|
38
|
+
import { runPipeline } from './pipeline-runner'
|
|
39
|
+
|
|
40
|
+
// ─── TYPES ───────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* All entity lifecycle event types that triggers can subscribe to.
|
|
44
|
+
*
|
|
45
|
+
* Format: `<entity>_<lifecycle>` where entity is one of:
|
|
46
|
+
* `item | account | person | thread | message | attachment | link`
|
|
47
|
+
* and lifecycle is `created | updated | deleted`.
|
|
48
|
+
*
|
|
49
|
+
* @calledBy checkAndFireTriggers, fireCreateTriggers, fireUpdateTriggers,
|
|
50
|
+
* fireDeleteTriggers, all API handlers that write to these entity tables
|
|
51
|
+
*/
|
|
52
|
+
export type EventType =
|
|
53
|
+
| 'item_created' | 'item_updated' | 'item_deleted'
|
|
54
|
+
| 'account_created' | 'account_updated' | 'account_deleted'
|
|
55
|
+
| 'person_created' | 'person_updated' | 'person_deleted'
|
|
56
|
+
| 'thread_created' | 'thread_updated' | 'thread_deleted'
|
|
57
|
+
| 'message_created' | 'message_updated' | 'message_deleted'
|
|
58
|
+
| 'attachment_created' | 'attachment_updated' | 'attachment_deleted'
|
|
59
|
+
| 'link_created' | 'link_updated' | 'link_deleted'
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Trigger record shape as loaded from the `triggers` table.
|
|
63
|
+
*
|
|
64
|
+
* @inputSpec event_type: EventType string stored in triggers.event_type
|
|
65
|
+
* @inputSpec pipeline_id: UUID — target pipeline to run when this trigger fires
|
|
66
|
+
* @inputSpec config.filters: Record<string, any> — field-level predicates
|
|
67
|
+
* @inputSpec config.entity_type: string | undefined — entity type filter
|
|
68
|
+
* @inputSpec config.type_slug: string | undefined — item type slug filter
|
|
69
|
+
*/
|
|
70
|
+
interface Trigger {
|
|
71
|
+
id: string
|
|
72
|
+
name: string
|
|
73
|
+
event_type: string
|
|
74
|
+
pipeline_id: string
|
|
75
|
+
config: {
|
|
76
|
+
filters?: Record<string, any>
|
|
77
|
+
entity_type?: string
|
|
78
|
+
type_slug?: string
|
|
79
|
+
}
|
|
80
|
+
is_active: boolean
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── PRIMARY EXPORT ────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Queries active triggers for `eventType`, evaluates each against the entity
|
|
87
|
+
* data, and fires the associated pipeline for every matching trigger.
|
|
88
|
+
*
|
|
89
|
+
* Execution per trigger:
|
|
90
|
+
* 1. `evaluateTriggerConditions` — all conditions must pass
|
|
91
|
+
* 2. `runPipeline(trigger.pipeline_id, triggerData, ctx)` — fire pipeline
|
|
92
|
+
* 3. Update `triggers.last_triggered` and increment `trigger_count` via RPC
|
|
93
|
+
* 4. Emit `trigger.fired` audit log (or `trigger.failed` on error)
|
|
94
|
+
*
|
|
95
|
+
* `triggerData` passed to the pipeline:
|
|
96
|
+
* ```json
|
|
97
|
+
* {
|
|
98
|
+
* "event": "item_updated",
|
|
99
|
+
* "entity": { "type": "item", "id": "<uuid>", "data": {...} },
|
|
100
|
+
* "trigger": { "id": "<uuid>", "name": "My Trigger" },
|
|
101
|
+
* "fired_at": "2024-01-15T10:00:00.000Z"
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* @param eventType - The lifecycle event (e.g. 'item_updated')
|
|
106
|
+
* @param entityType - The entity table name (e.g. 'item', 'person')
|
|
107
|
+
* @param entityId - UUID of the entity that changed
|
|
108
|
+
* @param entityData - Full record data for condition evaluation
|
|
109
|
+
* @param ctx - CoreContext with requestId, accountId, and principal
|
|
110
|
+
* @returns Promise<void> — always resolves; never throws
|
|
111
|
+
* @throws never — all errors are caught per-trigger and logged
|
|
112
|
+
* @inputSpec eventType: EventType — must be one of the 21 defined event types
|
|
113
|
+
* @inputSpec entityId: string — UUID of the changed entity
|
|
114
|
+
* @inputSpec entityData: any — full record (used for condition evaluation)
|
|
115
|
+
* @outputSpec void
|
|
116
|
+
* @sideEffects DB read: triggers table
|
|
117
|
+
* @sideEffects DB write (per matching trigger): triggers.last_triggered, trigger_count
|
|
118
|
+
* @sideEffects Calls runPipeline (per matching trigger) — which writes pipeline_executions
|
|
119
|
+
* @sideEffects DB write: emitAudit (trigger.fired or trigger.failed per trigger)
|
|
120
|
+
* @calledBy All API handlers after successful create/update/delete writes
|
|
121
|
+
* @calledBy fireCreateTriggers, fireUpdateTriggers, fireDeleteTriggers
|
|
122
|
+
* @calls evaluateTriggerConditions, runPipeline, emitAudit
|
|
123
|
+
* @testUnit tests/unit/trigger-engine.test.ts
|
|
124
|
+
* @testIntegration tests/integration/trigger-engine.test.ts
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts
|
|
128
|
+
* // In an API handler after a successful item update:
|
|
129
|
+
* await fireUpdateTriggers('item', updatedItem.id, updatedItem, ctx)
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export async function checkAndFireTriggers(
|
|
133
|
+
eventType: EventType,
|
|
134
|
+
entityType: string,
|
|
135
|
+
entityId: string,
|
|
136
|
+
entityData: any,
|
|
137
|
+
ctx: CoreContext
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
// Find matching active triggers
|
|
140
|
+
const { data: triggers, error } = await adminDb
|
|
141
|
+
.from('triggers')
|
|
142
|
+
.select('*')
|
|
143
|
+
.eq('event_type', eventType)
|
|
144
|
+
.eq('is_active', true)
|
|
145
|
+
.order('created_at')
|
|
146
|
+
|
|
147
|
+
if (error) {
|
|
148
|
+
console.error(`[${ctx.requestId}] Trigger query error:`, error)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!triggers || triggers.length === 0) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Evaluate each trigger's conditions
|
|
157
|
+
for (const trigger of triggers) {
|
|
158
|
+
try {
|
|
159
|
+
const shouldFire = evaluateTriggerConditions(trigger, entityType, entityData)
|
|
160
|
+
|
|
161
|
+
if (!shouldFire) {
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Prepare trigger data for pipeline
|
|
166
|
+
const triggerData = {
|
|
167
|
+
event: eventType,
|
|
168
|
+
entity: {
|
|
169
|
+
type: entityType,
|
|
170
|
+
id: entityId,
|
|
171
|
+
data: entityData
|
|
172
|
+
},
|
|
173
|
+
trigger: {
|
|
174
|
+
id: trigger.id,
|
|
175
|
+
name: trigger.name
|
|
176
|
+
},
|
|
177
|
+
fired_at: new Date().toISOString()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Fire the pipeline
|
|
181
|
+
const result = await runPipeline(trigger.pipeline_id, triggerData, ctx)
|
|
182
|
+
|
|
183
|
+
// Update trigger statistics
|
|
184
|
+
await adminDb
|
|
185
|
+
.from('triggers')
|
|
186
|
+
.update({
|
|
187
|
+
last_triggered: new Date().toISOString(),
|
|
188
|
+
trigger_count: adminDb.rpc('increment_trigger_count', { p_trigger_id: trigger.id })
|
|
189
|
+
})
|
|
190
|
+
.eq('id', trigger.id)
|
|
191
|
+
|
|
192
|
+
await emitAudit(ctx, 'trigger.fired', {
|
|
193
|
+
type: 'trigger',
|
|
194
|
+
id: trigger.id,
|
|
195
|
+
account_id: ctx.accountId ?? undefined
|
|
196
|
+
}, {
|
|
197
|
+
event_type: eventType,
|
|
198
|
+
entity_type: entityType,
|
|
199
|
+
entity_id: entityId,
|
|
200
|
+
pipeline_id: trigger.pipeline_id,
|
|
201
|
+
execution_id: result.executionId,
|
|
202
|
+
execution_status: result.status
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
} catch (error: any) {
|
|
206
|
+
console.error(`[${ctx.requestId}] Trigger ${trigger.id} failed:`, error)
|
|
207
|
+
|
|
208
|
+
await emitAudit(ctx, 'trigger.failed', {
|
|
209
|
+
type: 'trigger',
|
|
210
|
+
id: trigger.id,
|
|
211
|
+
account_id: ctx.accountId ?? undefined
|
|
212
|
+
}, {
|
|
213
|
+
event_type: eventType,
|
|
214
|
+
error: error.message
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ─── CONDITION EVALUATION ───────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Returns true if all of a trigger's conditions match the entity event.
|
|
224
|
+
*
|
|
225
|
+
* Condition checks (in order):
|
|
226
|
+
* 1. `config.entity_type` must equal `entityType` (if set)
|
|
227
|
+
* 2. `config.type_slug` must equal `entityData.type_slug` (if set)
|
|
228
|
+
* 3. Each `config.filters` key must match the entity field:
|
|
229
|
+
* - Array: value must be in the array
|
|
230
|
+
* - Object: `evaluateOperator` checks each `$op: expected` pair
|
|
231
|
+
* - Scalar: exact equality
|
|
232
|
+
*
|
|
233
|
+
* @param trigger - Trigger record with config
|
|
234
|
+
* @param entityType - Entity table name from the calling handler
|
|
235
|
+
* @param entityData - Full record data for field-level filter checks
|
|
236
|
+
* @returns boolean — true if all conditions pass; false to skip this trigger
|
|
237
|
+
* @throws never
|
|
238
|
+
* @calledBy checkAndFireTriggers (per trigger in the matching set)
|
|
239
|
+
* @calls getNestedValue, evaluateOperator
|
|
240
|
+
*/
|
|
241
|
+
function evaluateTriggerConditions(
|
|
242
|
+
trigger: Trigger,
|
|
243
|
+
entityType: string,
|
|
244
|
+
entityData: any
|
|
245
|
+
): boolean {
|
|
246
|
+
const config = trigger.config || {}
|
|
247
|
+
|
|
248
|
+
// Check entity type filter
|
|
249
|
+
if (config.entity_type && config.entity_type !== entityType) {
|
|
250
|
+
return false
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check type_slug filter (for items)
|
|
254
|
+
if (config.type_slug && entityData?.type_slug !== config.type_slug) {
|
|
255
|
+
return false
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check custom filters
|
|
259
|
+
if (config.filters) {
|
|
260
|
+
for (const [key, expectedValue] of Object.entries(config.filters)) {
|
|
261
|
+
const actualValue = getNestedValue(entityData, key)
|
|
262
|
+
|
|
263
|
+
if (Array.isArray(expectedValue)) {
|
|
264
|
+
// Array means "any of these values"
|
|
265
|
+
if (!expectedValue.includes(actualValue)) {
|
|
266
|
+
return false
|
|
267
|
+
}
|
|
268
|
+
} else if (typeof expectedValue === 'object' && expectedValue !== null) {
|
|
269
|
+
// Object means operators: { $gt: 5, $lt: 10 }
|
|
270
|
+
if (!evaluateOperator(actualValue, expectedValue)) {
|
|
271
|
+
return false
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
// Simple equality
|
|
275
|
+
if (actualValue !== expectedValue) {
|
|
276
|
+
return false
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return true
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Extracts a value from a nested object using dot-notation path.
|
|
287
|
+
*
|
|
288
|
+
* @param obj - Source object
|
|
289
|
+
* @param path - Dot-separated key path (e.g. 'data.status', 'type_slug')
|
|
290
|
+
* @returns The value at the path, or `undefined` if any segment is missing
|
|
291
|
+
* @throws never
|
|
292
|
+
* @calledBy evaluateTriggerConditions (filter key resolution)
|
|
293
|
+
*/
|
|
294
|
+
function getNestedValue(obj: any, path: string): any {
|
|
295
|
+
return path.split('.').reduce((current, key) => {
|
|
296
|
+
return current?.[key]
|
|
297
|
+
}, obj)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Evaluates MongoDB-style operator conditions against an actual value.
|
|
302
|
+
*
|
|
303
|
+
* Supported operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`,
|
|
304
|
+
* `$in`, `$nin`, `$exists`. Unknown operators return false.
|
|
305
|
+
*
|
|
306
|
+
* @param actual - Actual field value from the entity
|
|
307
|
+
* @param operators - Object of `{ $op: expectedValue }` pairs
|
|
308
|
+
* @returns boolean — true only if all operators pass
|
|
309
|
+
* @throws never — warns on unknown operators
|
|
310
|
+
* @inputSpec operators: Record<string, any> — all keys must start with `$`
|
|
311
|
+
* @calledBy evaluateTriggerConditions (object filter branch)
|
|
312
|
+
*/
|
|
313
|
+
function evaluateOperator(actual: any, operators: Record<string, any>): boolean {
|
|
314
|
+
for (const [op, expected] of Object.entries(operators)) {
|
|
315
|
+
switch (op) {
|
|
316
|
+
case '$eq':
|
|
317
|
+
if (actual !== expected) return false
|
|
318
|
+
break
|
|
319
|
+
case '$ne':
|
|
320
|
+
if (actual === expected) return false
|
|
321
|
+
break
|
|
322
|
+
case '$gt':
|
|
323
|
+
if (!(actual > expected)) return false
|
|
324
|
+
break
|
|
325
|
+
case '$gte':
|
|
326
|
+
if (!(actual >= expected)) return false
|
|
327
|
+
break
|
|
328
|
+
case '$lt':
|
|
329
|
+
if (!(actual < expected)) return false
|
|
330
|
+
break
|
|
331
|
+
case '$lte':
|
|
332
|
+
if (!(actual <= expected)) return false
|
|
333
|
+
break
|
|
334
|
+
case '$in':
|
|
335
|
+
if (!Array.isArray(expected) || !expected.includes(actual)) return false
|
|
336
|
+
break
|
|
337
|
+
case '$nin':
|
|
338
|
+
if (!Array.isArray(expected) || expected.includes(actual)) return false
|
|
339
|
+
break
|
|
340
|
+
case '$exists':
|
|
341
|
+
const exists = actual !== undefined && actual !== null
|
|
342
|
+
if (exists !== expected) return false
|
|
343
|
+
break
|
|
344
|
+
default:
|
|
345
|
+
console.warn(`Unknown operator: ${op}`)
|
|
346
|
+
return false
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return true
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ─── CONVENIENCE HELPERS ────────────────────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Fires `<entityType>_created` triggers. Thin wrapper around
|
|
356
|
+
* `checkAndFireTriggers` that constructs the correct EventType.
|
|
357
|
+
*
|
|
358
|
+
* @param entityType - Entity table name (e.g. 'item', 'person')
|
|
359
|
+
* @param entityId - UUID of the newly created entity
|
|
360
|
+
* @param entityData - The created record (for condition evaluation)
|
|
361
|
+
* @param ctx - CoreContext
|
|
362
|
+
* @returns Promise<void> — always resolves
|
|
363
|
+
* @throws never
|
|
364
|
+
* @sideEffects same as checkAndFireTriggers
|
|
365
|
+
* @calledBy Any API handler's create path (e.g. items.ts, people.ts)
|
|
366
|
+
* @calls checkAndFireTriggers
|
|
367
|
+
* @testUnit tests/unit/trigger-engine.test.ts — 'fireCreateTriggers'
|
|
368
|
+
*/
|
|
369
|
+
export async function fireCreateTriggers(
|
|
370
|
+
entityType: string,
|
|
371
|
+
entityId: string,
|
|
372
|
+
entityData: any,
|
|
373
|
+
ctx: CoreContext
|
|
374
|
+
): Promise<void> {
|
|
375
|
+
const eventType = `${entityType}_created` as EventType
|
|
376
|
+
await checkAndFireTriggers(eventType, entityType, entityId, entityData, ctx)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Fires `<entityType>_updated` triggers. Thin wrapper around
|
|
381
|
+
* `checkAndFireTriggers` that constructs the correct EventType.
|
|
382
|
+
*
|
|
383
|
+
* @param entityType - Entity table name (e.g. 'item', 'person')
|
|
384
|
+
* @param entityId - UUID of the updated entity
|
|
385
|
+
* @param entityData - The updated record (for condition evaluation)
|
|
386
|
+
* @param ctx - CoreContext
|
|
387
|
+
* @returns Promise<void> — always resolves
|
|
388
|
+
* @throws never
|
|
389
|
+
* @sideEffects same as checkAndFireTriggers
|
|
390
|
+
* @calledBy Any API handler's update path
|
|
391
|
+
* @calls checkAndFireTriggers
|
|
392
|
+
*/
|
|
393
|
+
export async function fireUpdateTriggers(
|
|
394
|
+
entityType: string,
|
|
395
|
+
entityId: string,
|
|
396
|
+
entityData: any,
|
|
397
|
+
ctx: CoreContext
|
|
398
|
+
): Promise<void> {
|
|
399
|
+
const eventType = `${entityType}_updated` as EventType
|
|
400
|
+
await checkAndFireTriggers(eventType, entityType, entityId, entityData, ctx)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Fires `<entityType>_deleted` triggers. Thin wrapper around
|
|
405
|
+
* `checkAndFireTriggers` that constructs the correct EventType.
|
|
406
|
+
*
|
|
407
|
+
* @param entityType - Entity table name (e.g. 'item', 'person')
|
|
408
|
+
* @param entityId - UUID of the deleted entity
|
|
409
|
+
* @param entityData - The record snapshot before deletion (for condition evaluation)
|
|
410
|
+
* @param ctx - CoreContext
|
|
411
|
+
* @returns Promise<void> — always resolves
|
|
412
|
+
* @throws never
|
|
413
|
+
* @sideEffects same as checkAndFireTriggers
|
|
414
|
+
* @calledBy Any API handler's delete path
|
|
415
|
+
* @calls checkAndFireTriggers
|
|
416
|
+
*/
|
|
417
|
+
export async function fireDeleteTriggers(
|
|
418
|
+
entityType: string,
|
|
419
|
+
entityId: string,
|
|
420
|
+
entityData: any,
|
|
421
|
+
ctx: CoreContext
|
|
422
|
+
): Promise<void> {
|
|
423
|
+
const eventType = `${entityType}_deleted` as EventType
|
|
424
|
+
await checkAndFireTriggers(eventType, entityType, entityId, entityData, ctx)
|
|
425
|
+
}
|