@useatlas/create 0.0.1
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/README.md +231 -0
- package/index.ts +829 -0
- package/package.json +38 -0
- package/templates/docker/.env.example +67 -0
- package/templates/docker/Dockerfile +52 -0
- package/templates/docker/bin/__tests__/benchmark.test.ts +598 -0
- package/templates/docker/bin/__tests__/duckdb-ingest.test.ts +171 -0
- package/templates/docker/bin/__tests__/eval.test.ts +434 -0
- package/templates/docker/bin/__tests__/matview-partition.test.ts +615 -0
- package/templates/docker/bin/__tests__/multi-source.test.ts +113 -0
- package/templates/docker/bin/__tests__/plugin-cli.test.ts +322 -0
- package/templates/docker/bin/__tests__/profiler-heuristics.test.ts +608 -0
- package/templates/docker/bin/__tests__/query.test.ts +240 -0
- package/templates/docker/bin/__tests__/schema-drift.test.ts +542 -0
- package/templates/docker/bin/__tests__/view-yaml-generation.test.ts +146 -0
- package/templates/docker/bin/atlas.ts +5044 -0
- package/templates/docker/bin/benchmark.ts +695 -0
- package/templates/docker/bin/enrich.ts +559 -0
- package/templates/docker/bin/eval.ts +770 -0
- package/templates/docker/bin/smoke.ts +438 -0
- package/templates/docker/data/.gitkeep +0 -0
- package/templates/docker/data/cybersec.sql +1961 -0
- package/templates/docker/data/demo-semantic/catalog.yml +40 -0
- package/templates/docker/data/demo-semantic/entities/accounts.yml +170 -0
- package/templates/docker/data/demo-semantic/entities/companies.yml +207 -0
- package/templates/docker/data/demo-semantic/entities/people.yml +145 -0
- package/templates/docker/data/demo-semantic/glossary.yml +22 -0
- package/templates/docker/data/demo-semantic/metrics/accounts.yml +38 -0
- package/templates/docker/data/demo-semantic/metrics/companies.yml +89 -0
- package/templates/docker/data/demo.sql +373 -0
- package/templates/docker/data/ecommerce.sql +1690 -0
- package/templates/docker/data/init-demo-db.sql +8 -0
- package/templates/docker/docker-compose.yml +34 -0
- package/templates/docker/docs/deploy.md +390 -0
- package/templates/docker/eslint.config.mjs +18 -0
- package/templates/docker/gitignore +5 -0
- package/templates/docker/next.config.ts +9 -0
- package/templates/docker/package.json +59 -0
- package/templates/docker/postcss.config.mjs +8 -0
- package/templates/docker/public/.gitkeep +0 -0
- package/templates/docker/public/favicon.svg +4 -0
- package/templates/docker/railway.json +13 -0
- package/templates/docker/render.yaml +34 -0
- package/templates/docker/semantic/catalog.yml +5 -0
- package/templates/docker/semantic/entities/.gitkeep +0 -0
- package/templates/docker/semantic/glossary.yml +6 -0
- package/templates/docker/semantic/metrics/.gitkeep +0 -0
- package/templates/docker/sidecar/Dockerfile +28 -0
- package/templates/docker/sidecar/railway.json +14 -0
- package/templates/docker/sidecar/server.ts +188 -0
- package/templates/docker/src/api/__tests__/actions.test.ts +683 -0
- package/templates/docker/src/api/__tests__/admin.test.ts +820 -0
- package/templates/docker/src/api/__tests__/auth.test.ts +165 -0
- package/templates/docker/src/api/__tests__/chat.test.ts +376 -0
- package/templates/docker/src/api/__tests__/conversations.test.ts +555 -0
- package/templates/docker/src/api/__tests__/cors.test.ts +135 -0
- package/templates/docker/src/api/__tests__/health-plugin.test.ts +169 -0
- package/templates/docker/src/api/__tests__/health.test.ts +261 -0
- package/templates/docker/src/api/__tests__/query.test.ts +891 -0
- package/templates/docker/src/api/__tests__/scheduled-tasks.test.ts +601 -0
- package/templates/docker/src/api/__tests__/slack.test.ts +847 -0
- package/templates/docker/src/api/index.ts +117 -0
- package/templates/docker/src/api/routes/actions.ts +274 -0
- package/templates/docker/src/api/routes/admin.ts +757 -0
- package/templates/docker/src/api/routes/auth.ts +48 -0
- package/templates/docker/src/api/routes/chat.ts +465 -0
- package/templates/docker/src/api/routes/conversations.ts +266 -0
- package/templates/docker/src/api/routes/health.ts +287 -0
- package/templates/docker/src/api/routes/openapi.ts +390 -0
- package/templates/docker/src/api/routes/query.ts +318 -0
- package/templates/docker/src/api/routes/scheduled-tasks.ts +467 -0
- package/templates/docker/src/api/routes/slack.ts +611 -0
- package/templates/docker/src/api/server.ts +226 -0
- package/templates/docker/src/app/api/[...route]/route.ts +33 -0
- package/templates/docker/src/app/error.tsx +24 -0
- package/templates/docker/src/app/globals.css +126 -0
- package/templates/docker/src/app/layout.tsx +19 -0
- package/templates/docker/src/app/page.tsx +14 -0
- package/templates/docker/src/global.d.ts +1 -0
- package/templates/docker/src/lib/__tests__/agent-cache.test.ts +437 -0
- package/templates/docker/src/lib/__tests__/agent-dialect.test.ts +114 -0
- package/templates/docker/src/lib/__tests__/agent-health-annotations.test.ts +164 -0
- package/templates/docker/src/lib/__tests__/agent-integration.test.ts +514 -0
- package/templates/docker/src/lib/__tests__/config-actions.test.ts +166 -0
- package/templates/docker/src/lib/__tests__/config.test.ts +1063 -0
- package/templates/docker/src/lib/__tests__/conversations.test.ts +589 -0
- package/templates/docker/src/lib/__tests__/errors.test.ts +256 -0
- package/templates/docker/src/lib/__tests__/logger.test.ts +200 -0
- package/templates/docker/src/lib/__tests__/providers.test.ts +99 -0
- package/templates/docker/src/lib/__tests__/rls.test.ts +435 -0
- package/templates/docker/src/lib/__tests__/scheduled-task-types.test.ts +124 -0
- package/templates/docker/src/lib/__tests__/scheduled-tasks.test.ts +550 -0
- package/templates/docker/src/lib/__tests__/semantic-index.test.ts +547 -0
- package/templates/docker/src/lib/__tests__/semantic-multisource.test.ts +544 -0
- package/templates/docker/src/lib/__tests__/semantic.test.ts +363 -0
- package/templates/docker/src/lib/__tests__/startup-actions.test.ts +452 -0
- package/templates/docker/src/lib/__tests__/startup.test.ts +465 -0
- package/templates/docker/src/lib/__tests__/tracing.test.ts +28 -0
- package/templates/docker/src/lib/action-types.ts +95 -0
- package/templates/docker/src/lib/agent-query.ts +178 -0
- package/templates/docker/src/lib/agent.ts +505 -0
- package/templates/docker/src/lib/api-url.ts +2 -0
- package/templates/docker/src/lib/auth/__tests__/audit.test.ts +418 -0
- package/templates/docker/src/lib/auth/__tests__/byot-integration.test.ts +222 -0
- package/templates/docker/src/lib/auth/__tests__/byot.test.ts +366 -0
- package/templates/docker/src/lib/auth/__tests__/detect.test.ts +190 -0
- package/templates/docker/src/lib/auth/__tests__/managed.test.ts +173 -0
- package/templates/docker/src/lib/auth/__tests__/middleware.test.ts +456 -0
- package/templates/docker/src/lib/auth/__tests__/migrate.test.ts +201 -0
- package/templates/docker/src/lib/auth/__tests__/permissions.test.ts +225 -0
- package/templates/docker/src/lib/auth/__tests__/server.test.ts +34 -0
- package/templates/docker/src/lib/auth/__tests__/simple-key.test.ts +176 -0
- package/templates/docker/src/lib/auth/__tests__/types.test.ts +44 -0
- package/templates/docker/src/lib/auth/audit.ts +89 -0
- package/templates/docker/src/lib/auth/byot.ts +158 -0
- package/templates/docker/src/lib/auth/client.ts +35 -0
- package/templates/docker/src/lib/auth/detect.ts +83 -0
- package/templates/docker/src/lib/auth/managed.ts +73 -0
- package/templates/docker/src/lib/auth/middleware.ts +208 -0
- package/templates/docker/src/lib/auth/migrate.ts +111 -0
- package/templates/docker/src/lib/auth/permissions.ts +156 -0
- package/templates/docker/src/lib/auth/server.ts +142 -0
- package/templates/docker/src/lib/auth/simple-key.ts +92 -0
- package/templates/docker/src/lib/auth/types.ts +49 -0
- package/templates/docker/src/lib/config.ts +704 -0
- package/templates/docker/src/lib/conversation-types.ts +29 -0
- package/templates/docker/src/lib/conversations.ts +270 -0
- package/templates/docker/src/lib/db/__tests__/connection.test.ts +69 -0
- package/templates/docker/src/lib/db/__tests__/duckdb.test.ts +141 -0
- package/templates/docker/src/lib/db/__tests__/internal.test.ts +387 -0
- package/templates/docker/src/lib/db/__tests__/registry-health.test.ts +207 -0
- package/templates/docker/src/lib/db/__tests__/registry-pool-limits.test.ts +156 -0
- package/templates/docker/src/lib/db/__tests__/registry.test.ts +595 -0
- package/templates/docker/src/lib/db/__tests__/salesforce.test.ts +339 -0
- package/templates/docker/src/lib/db/__tests__/snowflake.test.ts +217 -0
- package/templates/docker/src/lib/db/__tests__/source-rate-limit.test.ts +130 -0
- package/templates/docker/src/lib/db/connection.ts +753 -0
- package/templates/docker/src/lib/db/duckdb.ts +122 -0
- package/templates/docker/src/lib/db/internal.ts +273 -0
- package/templates/docker/src/lib/db/salesforce.ts +342 -0
- package/templates/docker/src/lib/db/source-rate-limit.ts +191 -0
- package/templates/docker/src/lib/errors.ts +154 -0
- package/templates/docker/src/lib/logger.ts +98 -0
- package/templates/docker/src/lib/plugins/__tests__/hooks-integration.test.ts +202 -0
- package/templates/docker/src/lib/plugins/__tests__/hooks.test.ts +529 -0
- package/templates/docker/src/lib/plugins/__tests__/migrate.test.ts +521 -0
- package/templates/docker/src/lib/plugins/__tests__/registry.test.ts +346 -0
- package/templates/docker/src/lib/plugins/__tests__/tools.test.ts +49 -0
- package/templates/docker/src/lib/plugins/__tests__/wiring.test.ts +585 -0
- package/templates/docker/src/lib/plugins/hooks.ts +162 -0
- package/templates/docker/src/lib/plugins/index.ts +9 -0
- package/templates/docker/src/lib/plugins/migrate.ts +309 -0
- package/templates/docker/src/lib/plugins/registry.ts +231 -0
- package/templates/docker/src/lib/plugins/tools.ts +39 -0
- package/templates/docker/src/lib/plugins/wiring.ts +291 -0
- package/templates/docker/src/lib/providers.ts +102 -0
- package/templates/docker/src/lib/rls.ts +321 -0
- package/templates/docker/src/lib/scheduled-task-types.ts +132 -0
- package/templates/docker/src/lib/scheduled-tasks.ts +475 -0
- package/templates/docker/src/lib/scheduler/__tests__/delivery.test.ts +192 -0
- package/templates/docker/src/lib/scheduler/__tests__/engine.test.ts +248 -0
- package/templates/docker/src/lib/scheduler/__tests__/format-email.test.ts +96 -0
- package/templates/docker/src/lib/scheduler/__tests__/format-slack.test.ts +78 -0
- package/templates/docker/src/lib/scheduler/__tests__/format-webhook.test.ts +78 -0
- package/templates/docker/src/lib/scheduler/delivery.ts +248 -0
- package/templates/docker/src/lib/scheduler/engine.ts +317 -0
- package/templates/docker/src/lib/scheduler/executor.ts +73 -0
- package/templates/docker/src/lib/scheduler/format-email.ts +109 -0
- package/templates/docker/src/lib/scheduler/format-slack.ts +35 -0
- package/templates/docker/src/lib/scheduler/format-webhook.ts +37 -0
- package/templates/docker/src/lib/scheduler/index.ts +7 -0
- package/templates/docker/src/lib/security.ts +11 -0
- package/templates/docker/src/lib/semantic-index.ts +503 -0
- package/templates/docker/src/lib/semantic.ts +387 -0
- package/templates/docker/src/lib/sidecar-types.ts +16 -0
- package/templates/docker/src/lib/slack/__tests__/api.test.ts +160 -0
- package/templates/docker/src/lib/slack/__tests__/format.test.ts +237 -0
- package/templates/docker/src/lib/slack/__tests__/store.test.ts +188 -0
- package/templates/docker/src/lib/slack/__tests__/threads.test.ts +112 -0
- package/templates/docker/src/lib/slack/__tests__/verify.test.ts +111 -0
- package/templates/docker/src/lib/slack/api.ts +102 -0
- package/templates/docker/src/lib/slack/format.ts +209 -0
- package/templates/docker/src/lib/slack/store.ts +107 -0
- package/templates/docker/src/lib/slack/threads.ts +64 -0
- package/templates/docker/src/lib/slack/verify.ts +71 -0
- package/templates/docker/src/lib/startup.ts +730 -0
- package/templates/docker/src/lib/tools/__tests__/action-permissions.test.ts +594 -0
- package/templates/docker/src/lib/tools/__tests__/custom-validation.test.ts +238 -0
- package/templates/docker/src/lib/tools/__tests__/explore-backend.test.ts +267 -0
- package/templates/docker/src/lib/tools/__tests__/explore-nsjail.test.ts +492 -0
- package/templates/docker/src/lib/tools/__tests__/explore-plugin.test.ts +374 -0
- package/templates/docker/src/lib/tools/__tests__/explore-sdk-compat.test.ts +82 -0
- package/templates/docker/src/lib/tools/__tests__/explore-sidecar.test.ts +208 -0
- package/templates/docker/src/lib/tools/__tests__/registry-actions.test.ts +144 -0
- package/templates/docker/src/lib/tools/__tests__/registry.test.ts +235 -0
- package/templates/docker/src/lib/tools/__tests__/salesforce-tool.test.ts +154 -0
- package/templates/docker/src/lib/tools/__tests__/soql-validation.test.ts +303 -0
- package/templates/docker/src/lib/tools/__tests__/sql-audit.test.ts +225 -0
- package/templates/docker/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +98 -0
- package/templates/docker/src/lib/tools/__tests__/sql-duckdb.test.ts +233 -0
- package/templates/docker/src/lib/tools/__tests__/sql-ratelimit.test.ts +225 -0
- package/templates/docker/src/lib/tools/__tests__/sql.test.ts +1012 -0
- package/templates/docker/src/lib/tools/actions/__tests__/audit.test.ts +211 -0
- package/templates/docker/src/lib/tools/actions/__tests__/email.test.ts +378 -0
- package/templates/docker/src/lib/tools/actions/__tests__/handler.test.ts +681 -0
- package/templates/docker/src/lib/tools/actions/__tests__/jira.test.ts +427 -0
- package/templates/docker/src/lib/tools/actions/audit.ts +47 -0
- package/templates/docker/src/lib/tools/actions/email.ts +191 -0
- package/templates/docker/src/lib/tools/actions/handler.ts +591 -0
- package/templates/docker/src/lib/tools/actions/index.ts +23 -0
- package/templates/docker/src/lib/tools/actions/jira.ts +220 -0
- package/templates/docker/src/lib/tools/explore-nsjail.ts +343 -0
- package/templates/docker/src/lib/tools/explore-sandbox.ts +264 -0
- package/templates/docker/src/lib/tools/explore-sidecar.ts +163 -0
- package/templates/docker/src/lib/tools/explore.ts +379 -0
- package/templates/docker/src/lib/tools/registry.ts +221 -0
- package/templates/docker/src/lib/tools/salesforce.ts +138 -0
- package/templates/docker/src/lib/tools/soql-validation.ts +172 -0
- package/templates/docker/src/lib/tools/sql.ts +680 -0
- package/templates/docker/src/lib/tracing.ts +40 -0
- package/templates/docker/src/lib/utils.ts +6 -0
- package/templates/docker/src/test-setup.ts +38 -0
- package/templates/docker/src/types/vercel-sandbox.d.ts +54 -0
- package/templates/docker/src/ui/components/actions/action-approval-card.tsx +295 -0
- package/templates/docker/src/ui/components/actions/action-status-badge.tsx +50 -0
- package/templates/docker/src/ui/components/admin/admin-layout.tsx +26 -0
- package/templates/docker/src/ui/components/admin/admin-sidebar.tsx +96 -0
- package/templates/docker/src/ui/components/admin/empty-state.tsx +24 -0
- package/templates/docker/src/ui/components/admin/entity-detail.tsx +233 -0
- package/templates/docker/src/ui/components/admin/entity-list.tsx +96 -0
- package/templates/docker/src/ui/components/admin/error-banner.tsx +22 -0
- package/templates/docker/src/ui/components/admin/feature-disabled.tsx +44 -0
- package/templates/docker/src/ui/components/admin/health-badge.tsx +30 -0
- package/templates/docker/src/ui/components/admin/loading-state.tsx +14 -0
- package/templates/docker/src/ui/components/admin/stat-card.tsx +32 -0
- package/templates/docker/src/ui/components/atlas-chat.tsx +370 -0
- package/templates/docker/src/ui/components/chart/chart-detection.ts +261 -0
- package/templates/docker/src/ui/components/chart/result-chart.tsx +375 -0
- package/templates/docker/src/ui/components/chat/api-key-bar.tsx +66 -0
- package/templates/docker/src/ui/components/chat/copy-button.tsx +25 -0
- package/templates/docker/src/ui/components/chat/data-table.tsx +102 -0
- package/templates/docker/src/ui/components/chat/error-banner.tsx +32 -0
- package/templates/docker/src/ui/components/chat/explore-card.tsx +41 -0
- package/templates/docker/src/ui/components/chat/loading-card.tsx +10 -0
- package/templates/docker/src/ui/components/chat/managed-auth-card.tsx +116 -0
- package/templates/docker/src/ui/components/chat/markdown.tsx +72 -0
- package/templates/docker/src/ui/components/chat/sql-block.tsx +30 -0
- package/templates/docker/src/ui/components/chat/sql-result-card.tsx +144 -0
- package/templates/docker/src/ui/components/chat/starter-prompts.ts +6 -0
- package/templates/docker/src/ui/components/chat/tool-part.tsx +40 -0
- package/templates/docker/src/ui/components/chat/typing-indicator.tsx +19 -0
- package/templates/docker/src/ui/components/conversations/conversation-item.tsx +120 -0
- package/templates/docker/src/ui/components/conversations/conversation-list.tsx +66 -0
- package/templates/docker/src/ui/components/conversations/conversation-sidebar.tsx +78 -0
- package/templates/docker/src/ui/components/conversations/delete-confirmation.tsx +27 -0
- package/templates/docker/src/ui/context.tsx +78 -0
- package/templates/docker/src/ui/hooks/use-admin-fetch.ts +104 -0
- package/templates/docker/src/ui/hooks/use-conversations.ts +184 -0
- package/templates/docker/src/ui/hooks/use-dark-mode.ts +17 -0
- package/templates/docker/src/ui/lib/action-types.ts +63 -0
- package/templates/docker/src/ui/lib/helpers.ts +104 -0
- package/templates/docker/src/ui/lib/types.ts +145 -0
- package/templates/docker/tsconfig.json +41 -0
- package/templates/docker/vercel.json +3 -0
- package/templates/nextjs-standalone/.env.example +68 -0
- package/templates/nextjs-standalone/bin/__tests__/benchmark.test.ts +598 -0
- package/templates/nextjs-standalone/bin/__tests__/duckdb-ingest.test.ts +171 -0
- package/templates/nextjs-standalone/bin/__tests__/eval.test.ts +434 -0
- package/templates/nextjs-standalone/bin/__tests__/matview-partition.test.ts +615 -0
- package/templates/nextjs-standalone/bin/__tests__/multi-source.test.ts +113 -0
- package/templates/nextjs-standalone/bin/__tests__/plugin-cli.test.ts +322 -0
- package/templates/nextjs-standalone/bin/__tests__/profiler-heuristics.test.ts +608 -0
- package/templates/nextjs-standalone/bin/__tests__/query.test.ts +240 -0
- package/templates/nextjs-standalone/bin/__tests__/schema-drift.test.ts +542 -0
- package/templates/nextjs-standalone/bin/__tests__/view-yaml-generation.test.ts +146 -0
- package/templates/nextjs-standalone/bin/atlas.ts +5044 -0
- package/templates/nextjs-standalone/bin/benchmark.ts +695 -0
- package/templates/nextjs-standalone/bin/enrich.ts +559 -0
- package/templates/nextjs-standalone/bin/eval.ts +770 -0
- package/templates/nextjs-standalone/bin/smoke.ts +438 -0
- package/templates/nextjs-standalone/data/.gitkeep +0 -0
- package/templates/nextjs-standalone/data/cybersec.sql +1961 -0
- package/templates/nextjs-standalone/data/demo-semantic/catalog.yml +40 -0
- package/templates/nextjs-standalone/data/demo-semantic/entities/accounts.yml +170 -0
- package/templates/nextjs-standalone/data/demo-semantic/entities/companies.yml +207 -0
- package/templates/nextjs-standalone/data/demo-semantic/entities/people.yml +145 -0
- package/templates/nextjs-standalone/data/demo-semantic/glossary.yml +22 -0
- package/templates/nextjs-standalone/data/demo-semantic/metrics/accounts.yml +38 -0
- package/templates/nextjs-standalone/data/demo-semantic/metrics/companies.yml +89 -0
- package/templates/nextjs-standalone/data/demo.sql +373 -0
- package/templates/nextjs-standalone/data/ecommerce.sql +1690 -0
- package/templates/nextjs-standalone/data/init-demo-db.sql +8 -0
- package/templates/nextjs-standalone/docs/deploy.md +390 -0
- package/templates/nextjs-standalone/eslint.config.mjs +18 -0
- package/templates/nextjs-standalone/gitignore +5 -0
- package/templates/nextjs-standalone/next.config.ts +10 -0
- package/templates/nextjs-standalone/package.json +63 -0
- package/templates/nextjs-standalone/postcss.config.mjs +8 -0
- package/templates/nextjs-standalone/semantic/catalog.yml +5 -0
- package/templates/nextjs-standalone/semantic/entities/.gitkeep +0 -0
- package/templates/nextjs-standalone/semantic/glossary.yml +6 -0
- package/templates/nextjs-standalone/semantic/metrics/.gitkeep +0 -0
- package/templates/nextjs-standalone/src/api/__tests__/actions.test.ts +683 -0
- package/templates/nextjs-standalone/src/api/__tests__/admin.test.ts +820 -0
- package/templates/nextjs-standalone/src/api/__tests__/auth.test.ts +165 -0
- package/templates/nextjs-standalone/src/api/__tests__/chat.test.ts +376 -0
- package/templates/nextjs-standalone/src/api/__tests__/conversations.test.ts +555 -0
- package/templates/nextjs-standalone/src/api/__tests__/cors.test.ts +135 -0
- package/templates/nextjs-standalone/src/api/__tests__/health-plugin.test.ts +169 -0
- package/templates/nextjs-standalone/src/api/__tests__/health.test.ts +261 -0
- package/templates/nextjs-standalone/src/api/__tests__/query.test.ts +891 -0
- package/templates/nextjs-standalone/src/api/__tests__/scheduled-tasks.test.ts +601 -0
- package/templates/nextjs-standalone/src/api/__tests__/slack.test.ts +847 -0
- package/templates/nextjs-standalone/src/api/index.ts +117 -0
- package/templates/nextjs-standalone/src/api/routes/actions.ts +274 -0
- package/templates/nextjs-standalone/src/api/routes/admin.ts +757 -0
- package/templates/nextjs-standalone/src/api/routes/auth.ts +48 -0
- package/templates/nextjs-standalone/src/api/routes/chat.ts +465 -0
- package/templates/nextjs-standalone/src/api/routes/conversations.ts +266 -0
- package/templates/nextjs-standalone/src/api/routes/health.ts +287 -0
- package/templates/nextjs-standalone/src/api/routes/openapi.ts +390 -0
- package/templates/nextjs-standalone/src/api/routes/query.ts +318 -0
- package/templates/nextjs-standalone/src/api/routes/scheduled-tasks.ts +467 -0
- package/templates/nextjs-standalone/src/api/routes/slack.ts +611 -0
- package/templates/nextjs-standalone/src/api/server.ts +226 -0
- package/templates/nextjs-standalone/src/app/api/[...route]/route.ts +33 -0
- package/templates/nextjs-standalone/src/app/error.tsx +24 -0
- package/templates/nextjs-standalone/src/app/global-error.tsx +68 -0
- package/templates/nextjs-standalone/src/app/globals.css +126 -0
- package/templates/nextjs-standalone/src/app/layout.tsx +19 -0
- package/templates/nextjs-standalone/src/app/page.tsx +14 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-cache.test.ts +437 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-dialect.test.ts +114 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-health-annotations.test.ts +164 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-integration.test.ts +514 -0
- package/templates/nextjs-standalone/src/lib/__tests__/config-actions.test.ts +166 -0
- package/templates/nextjs-standalone/src/lib/__tests__/config.test.ts +1063 -0
- package/templates/nextjs-standalone/src/lib/__tests__/conversations.test.ts +589 -0
- package/templates/nextjs-standalone/src/lib/__tests__/errors.test.ts +256 -0
- package/templates/nextjs-standalone/src/lib/__tests__/logger.test.ts +200 -0
- package/templates/nextjs-standalone/src/lib/__tests__/providers.test.ts +99 -0
- package/templates/nextjs-standalone/src/lib/__tests__/rls.test.ts +435 -0
- package/templates/nextjs-standalone/src/lib/__tests__/scheduled-task-types.test.ts +124 -0
- package/templates/nextjs-standalone/src/lib/__tests__/scheduled-tasks.test.ts +550 -0
- package/templates/nextjs-standalone/src/lib/__tests__/semantic-index.test.ts +547 -0
- package/templates/nextjs-standalone/src/lib/__tests__/semantic-multisource.test.ts +544 -0
- package/templates/nextjs-standalone/src/lib/__tests__/semantic.test.ts +363 -0
- package/templates/nextjs-standalone/src/lib/__tests__/startup-actions.test.ts +452 -0
- package/templates/nextjs-standalone/src/lib/__tests__/startup.test.ts +465 -0
- package/templates/nextjs-standalone/src/lib/__tests__/tracing.test.ts +28 -0
- package/templates/nextjs-standalone/src/lib/action-types.ts +95 -0
- package/templates/nextjs-standalone/src/lib/agent-query.ts +178 -0
- package/templates/nextjs-standalone/src/lib/agent.ts +505 -0
- package/templates/nextjs-standalone/src/lib/api-url.ts +3 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/audit.test.ts +418 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/byot-integration.test.ts +222 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/byot.test.ts +366 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/detect.test.ts +190 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/managed.test.ts +173 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/middleware.test.ts +456 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/migrate.test.ts +201 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/permissions.test.ts +225 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/server.test.ts +34 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/simple-key.test.ts +176 -0
- package/templates/nextjs-standalone/src/lib/auth/__tests__/types.test.ts +44 -0
- package/templates/nextjs-standalone/src/lib/auth/audit.ts +89 -0
- package/templates/nextjs-standalone/src/lib/auth/byot.ts +158 -0
- package/templates/nextjs-standalone/src/lib/auth/client.ts +23 -0
- package/templates/nextjs-standalone/src/lib/auth/detect.ts +83 -0
- package/templates/nextjs-standalone/src/lib/auth/managed.ts +73 -0
- package/templates/nextjs-standalone/src/lib/auth/middleware.ts +208 -0
- package/templates/nextjs-standalone/src/lib/auth/migrate.ts +111 -0
- package/templates/nextjs-standalone/src/lib/auth/permissions.ts +156 -0
- package/templates/nextjs-standalone/src/lib/auth/server.ts +142 -0
- package/templates/nextjs-standalone/src/lib/auth/simple-key.ts +92 -0
- package/templates/nextjs-standalone/src/lib/auth/types.ts +49 -0
- package/templates/nextjs-standalone/src/lib/config.ts +704 -0
- package/templates/nextjs-standalone/src/lib/conversation-types.ts +29 -0
- package/templates/nextjs-standalone/src/lib/conversations.ts +270 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/connection.test.ts +69 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/duckdb.test.ts +141 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/internal.test.ts +387 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry-health.test.ts +207 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry-pool-limits.test.ts +156 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry.test.ts +595 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/salesforce.test.ts +339 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/snowflake.test.ts +217 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/source-rate-limit.test.ts +130 -0
- package/templates/nextjs-standalone/src/lib/db/connection.ts +753 -0
- package/templates/nextjs-standalone/src/lib/db/duckdb.ts +122 -0
- package/templates/nextjs-standalone/src/lib/db/internal.ts +273 -0
- package/templates/nextjs-standalone/src/lib/db/salesforce.ts +342 -0
- package/templates/nextjs-standalone/src/lib/db/source-rate-limit.ts +191 -0
- package/templates/nextjs-standalone/src/lib/errors.ts +154 -0
- package/templates/nextjs-standalone/src/lib/logger.ts +98 -0
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks-integration.test.ts +202 -0
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks.test.ts +529 -0
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/migrate.test.ts +521 -0
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/registry.test.ts +346 -0
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/tools.test.ts +49 -0
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/wiring.test.ts +585 -0
- package/templates/nextjs-standalone/src/lib/plugins/hooks.ts +162 -0
- package/templates/nextjs-standalone/src/lib/plugins/index.ts +9 -0
- package/templates/nextjs-standalone/src/lib/plugins/migrate.ts +309 -0
- package/templates/nextjs-standalone/src/lib/plugins/registry.ts +231 -0
- package/templates/nextjs-standalone/src/lib/plugins/tools.ts +39 -0
- package/templates/nextjs-standalone/src/lib/plugins/wiring.ts +291 -0
- package/templates/nextjs-standalone/src/lib/providers.ts +102 -0
- package/templates/nextjs-standalone/src/lib/rls.ts +321 -0
- package/templates/nextjs-standalone/src/lib/scheduled-task-types.ts +132 -0
- package/templates/nextjs-standalone/src/lib/scheduled-tasks.ts +475 -0
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/delivery.test.ts +192 -0
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/engine.test.ts +248 -0
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-email.test.ts +96 -0
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-slack.test.ts +78 -0
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-webhook.test.ts +78 -0
- package/templates/nextjs-standalone/src/lib/scheduler/delivery.ts +248 -0
- package/templates/nextjs-standalone/src/lib/scheduler/engine.ts +317 -0
- package/templates/nextjs-standalone/src/lib/scheduler/executor.ts +73 -0
- package/templates/nextjs-standalone/src/lib/scheduler/format-email.ts +109 -0
- package/templates/nextjs-standalone/src/lib/scheduler/format-slack.ts +35 -0
- package/templates/nextjs-standalone/src/lib/scheduler/format-webhook.ts +37 -0
- package/templates/nextjs-standalone/src/lib/scheduler/index.ts +7 -0
- package/templates/nextjs-standalone/src/lib/security.ts +11 -0
- package/templates/nextjs-standalone/src/lib/semantic-index.ts +503 -0
- package/templates/nextjs-standalone/src/lib/semantic.ts +387 -0
- package/templates/nextjs-standalone/src/lib/sidecar-types.ts +16 -0
- package/templates/nextjs-standalone/src/lib/slack/__tests__/api.test.ts +160 -0
- package/templates/nextjs-standalone/src/lib/slack/__tests__/format.test.ts +237 -0
- package/templates/nextjs-standalone/src/lib/slack/__tests__/store.test.ts +188 -0
- package/templates/nextjs-standalone/src/lib/slack/__tests__/threads.test.ts +112 -0
- package/templates/nextjs-standalone/src/lib/slack/__tests__/verify.test.ts +111 -0
- package/templates/nextjs-standalone/src/lib/slack/api.ts +102 -0
- package/templates/nextjs-standalone/src/lib/slack/format.ts +209 -0
- package/templates/nextjs-standalone/src/lib/slack/store.ts +107 -0
- package/templates/nextjs-standalone/src/lib/slack/threads.ts +64 -0
- package/templates/nextjs-standalone/src/lib/slack/verify.ts +71 -0
- package/templates/nextjs-standalone/src/lib/startup.ts +730 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/action-permissions.test.ts +594 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/custom-validation.test.ts +238 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-backend.test.ts +267 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-nsjail.test.ts +492 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-plugin.test.ts +374 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sdk-compat.test.ts +82 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sidecar.test.ts +208 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/registry-actions.test.ts +144 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/registry.test.ts +235 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/salesforce-tool.test.ts +154 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/soql-validation.test.ts +303 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-audit.test.ts +225 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +98 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-duckdb.test.ts +233 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-ratelimit.test.ts +225 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql.test.ts +1012 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/audit.test.ts +211 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/email.test.ts +378 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/handler.test.ts +681 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/jira.test.ts +427 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/audit.ts +47 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/email.ts +191 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/handler.ts +591 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/index.ts +23 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/jira.ts +220 -0
- package/templates/nextjs-standalone/src/lib/tools/explore-nsjail.ts +343 -0
- package/templates/nextjs-standalone/src/lib/tools/explore-sandbox.ts +264 -0
- package/templates/nextjs-standalone/src/lib/tools/explore-sidecar.ts +163 -0
- package/templates/nextjs-standalone/src/lib/tools/explore.ts +379 -0
- package/templates/nextjs-standalone/src/lib/tools/registry.ts +221 -0
- package/templates/nextjs-standalone/src/lib/tools/salesforce.ts +138 -0
- package/templates/nextjs-standalone/src/lib/tools/soql-validation.ts +172 -0
- package/templates/nextjs-standalone/src/lib/tools/sql.ts +680 -0
- package/templates/nextjs-standalone/src/lib/tracing.ts +40 -0
- package/templates/nextjs-standalone/src/lib/utils.ts +6 -0
- package/templates/nextjs-standalone/src/test-setup.ts +38 -0
- package/templates/nextjs-standalone/src/ui/components/actions/action-approval-card.tsx +295 -0
- package/templates/nextjs-standalone/src/ui/components/actions/action-status-badge.tsx +50 -0
- package/templates/nextjs-standalone/src/ui/components/admin/admin-layout.tsx +26 -0
- package/templates/nextjs-standalone/src/ui/components/admin/admin-sidebar.tsx +96 -0
- package/templates/nextjs-standalone/src/ui/components/admin/empty-state.tsx +24 -0
- package/templates/nextjs-standalone/src/ui/components/admin/entity-detail.tsx +233 -0
- package/templates/nextjs-standalone/src/ui/components/admin/entity-list.tsx +96 -0
- package/templates/nextjs-standalone/src/ui/components/admin/error-banner.tsx +22 -0
- package/templates/nextjs-standalone/src/ui/components/admin/feature-disabled.tsx +44 -0
- package/templates/nextjs-standalone/src/ui/components/admin/health-badge.tsx +30 -0
- package/templates/nextjs-standalone/src/ui/components/admin/loading-state.tsx +14 -0
- package/templates/nextjs-standalone/src/ui/components/admin/stat-card.tsx +32 -0
- package/templates/nextjs-standalone/src/ui/components/atlas-chat.tsx +370 -0
- package/templates/nextjs-standalone/src/ui/components/chart/chart-detection.ts +261 -0
- package/templates/nextjs-standalone/src/ui/components/chart/result-chart.tsx +375 -0
- package/templates/nextjs-standalone/src/ui/components/chat/api-key-bar.tsx +66 -0
- package/templates/nextjs-standalone/src/ui/components/chat/copy-button.tsx +25 -0
- package/templates/nextjs-standalone/src/ui/components/chat/data-table.tsx +102 -0
- package/templates/nextjs-standalone/src/ui/components/chat/error-banner.tsx +32 -0
- package/templates/nextjs-standalone/src/ui/components/chat/explore-card.tsx +41 -0
- package/templates/nextjs-standalone/src/ui/components/chat/loading-card.tsx +10 -0
- package/templates/nextjs-standalone/src/ui/components/chat/managed-auth-card.tsx +116 -0
- package/templates/nextjs-standalone/src/ui/components/chat/markdown.tsx +72 -0
- package/templates/nextjs-standalone/src/ui/components/chat/sql-block.tsx +30 -0
- package/templates/nextjs-standalone/src/ui/components/chat/sql-result-card.tsx +144 -0
- package/templates/nextjs-standalone/src/ui/components/chat/starter-prompts.ts +6 -0
- package/templates/nextjs-standalone/src/ui/components/chat/tool-part.tsx +40 -0
- package/templates/nextjs-standalone/src/ui/components/chat/typing-indicator.tsx +19 -0
- package/templates/nextjs-standalone/src/ui/components/conversations/conversation-item.tsx +120 -0
- package/templates/nextjs-standalone/src/ui/components/conversations/conversation-list.tsx +66 -0
- package/templates/nextjs-standalone/src/ui/components/conversations/conversation-sidebar.tsx +78 -0
- package/templates/nextjs-standalone/src/ui/components/conversations/delete-confirmation.tsx +27 -0
- package/templates/nextjs-standalone/src/ui/context.tsx +78 -0
- package/templates/nextjs-standalone/src/ui/hooks/use-admin-fetch.ts +104 -0
- package/templates/nextjs-standalone/src/ui/hooks/use-conversations.ts +184 -0
- package/templates/nextjs-standalone/src/ui/hooks/use-dark-mode.ts +17 -0
- package/templates/nextjs-standalone/src/ui/lib/action-types.ts +63 -0
- package/templates/nextjs-standalone/src/ui/lib/helpers.ts +104 -0
- package/templates/nextjs-standalone/src/ui/lib/types.ts +145 -0
- package/templates/nextjs-standalone/tsconfig.json +32 -0
- package/templates/nextjs-standalone/vercel.json +4 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database connection factory and registry.
|
|
3
|
+
*
|
|
4
|
+
* Supports PostgreSQL (via `pg` Pool), MySQL (via `mysql2/promise`),
|
|
5
|
+
* ClickHouse (via `@clickhouse/client` HTTP transport),
|
|
6
|
+
* Snowflake (via `snowflake-sdk` pool), DuckDB (via `@duckdb/node-api`
|
|
7
|
+
* in-process engine), and Salesforce (via `jsforce`).
|
|
8
|
+
* Database type is detected from the connection URL format:
|
|
9
|
+
* - `postgresql://` or `postgres://` → PostgreSQL
|
|
10
|
+
* - `mysql://` or `mysql2://` → MySQL
|
|
11
|
+
* - `clickhouse://` or `clickhouses://` → ClickHouse
|
|
12
|
+
* - `snowflake://` → Snowflake
|
|
13
|
+
* - `duckdb://` → DuckDB (in-process)
|
|
14
|
+
* - `salesforce://` → Salesforce (SOQL — uses separate DataSource API)
|
|
15
|
+
*
|
|
16
|
+
* Connections are managed via ConnectionRegistry. The default connection
|
|
17
|
+
* auto-initializes from ATLAS_DATASOURCE_URL on first access.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createLogger } from "@atlas/api/lib/logger";
|
|
21
|
+
import { _resetWhitelists } from "@atlas/api/lib/semantic";
|
|
22
|
+
import { createDuckDBConnection, parseDuckDBUrl } from "./duckdb";
|
|
23
|
+
|
|
24
|
+
const log = createLogger("db");
|
|
25
|
+
|
|
26
|
+
export interface QueryResult {
|
|
27
|
+
columns: string[];
|
|
28
|
+
rows: Record<string, unknown>[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DBConnection {
|
|
32
|
+
query(sql: string, timeoutMs?: number): Promise<QueryResult>;
|
|
33
|
+
close(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type DBType = "postgres" | "mysql" | "clickhouse" | "snowflake" | "duckdb" | "salesforce";
|
|
37
|
+
|
|
38
|
+
export type HealthStatus = "healthy" | "degraded" | "unhealthy";
|
|
39
|
+
|
|
40
|
+
export interface HealthCheckResult {
|
|
41
|
+
status: HealthStatus;
|
|
42
|
+
latencyMs: number;
|
|
43
|
+
message?: string;
|
|
44
|
+
checkedAt: Date;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Public metadata about a registered connection (no operational handles). */
|
|
48
|
+
export interface ConnectionMetadata {
|
|
49
|
+
id: string;
|
|
50
|
+
dbType: DBType;
|
|
51
|
+
description?: string;
|
|
52
|
+
health?: HealthCheckResult;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Minimum elapsed time from first failure to current failure before marking unhealthy (5 minutes). */
|
|
56
|
+
const UNHEALTHY_WINDOW_MS = 5 * 60 * 1000;
|
|
57
|
+
/** Number of consecutive failures before marking unhealthy (must also span UNHEALTHY_WINDOW_MS). */
|
|
58
|
+
const UNHEALTHY_THRESHOLD = 3;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract the hostname from a database URL for audit purposes.
|
|
62
|
+
* Never exposes credentials. Returns "(unknown)" on parse failure.
|
|
63
|
+
*/
|
|
64
|
+
export function extractTargetHost(url: string): string {
|
|
65
|
+
try {
|
|
66
|
+
// Normalize known schemes to http:// so URL parser can handle them
|
|
67
|
+
const normalized = url
|
|
68
|
+
.replace(/^(postgresql|postgres|mysql|mysql2|clickhouse|clickhouses|snowflake|duckdb|salesforce):\/\//, "http://");
|
|
69
|
+
const parsed = new URL(normalized);
|
|
70
|
+
return parsed.hostname || "(unknown)";
|
|
71
|
+
} catch {
|
|
72
|
+
return "(unknown)";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Rewrite a `clickhouse://` or `clickhouses://` URL to `http://` or `https://`
|
|
78
|
+
* for the @clickhouse/client HTTP transport.
|
|
79
|
+
*
|
|
80
|
+
* - `clickhouses://` → `https://` (TLS)
|
|
81
|
+
* - `clickhouse://` → `http://` (plain)
|
|
82
|
+
*
|
|
83
|
+
* Warns if port 8443 (the conventional ClickHouse TLS port) is used with
|
|
84
|
+
* the plain `clickhouse://` scheme, since TLS is likely intended.
|
|
85
|
+
*/
|
|
86
|
+
export function rewriteClickHouseUrl(url: string): string {
|
|
87
|
+
if (url.startsWith("clickhouses://")) {
|
|
88
|
+
return url.replace(/^clickhouses:\/\//, "https://");
|
|
89
|
+
}
|
|
90
|
+
// Warn on probable TLS-port + plain-scheme mismatch
|
|
91
|
+
try {
|
|
92
|
+
const parsed = new URL(url.replace(/^clickhouse:\/\//, "http://"));
|
|
93
|
+
if (parsed.port === "8443") {
|
|
94
|
+
log.warn(
|
|
95
|
+
"clickhouse:// with port 8443 detected — did you mean clickhouses:// (TLS)? " +
|
|
96
|
+
"Port 8443 is the conventional ClickHouse TLS port."
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// URL parsing failure is handled downstream; skip warning
|
|
101
|
+
}
|
|
102
|
+
return url.replace(/^clickhouse:\/\//, "http://");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Detect database type from a connection string or ATLAS_DATASOURCE_URL.
|
|
107
|
+
* PostgreSQL URLs start with `postgresql://` or `postgres://`.
|
|
108
|
+
* MySQL URLs start with `mysql://` or `mysql2://`.
|
|
109
|
+
* ClickHouse URLs start with `clickhouse://` or `clickhouses://`.
|
|
110
|
+
* Snowflake URLs start with `snowflake://`.
|
|
111
|
+
* DuckDB URLs start with `duckdb://`.
|
|
112
|
+
* Salesforce URLs start with `salesforce://`.
|
|
113
|
+
* Throws if the URL does not match a supported database type.
|
|
114
|
+
*/
|
|
115
|
+
export function detectDBType(url?: string): DBType {
|
|
116
|
+
const connStr = url ?? process.env.ATLAS_DATASOURCE_URL ?? "";
|
|
117
|
+
if (!connStr) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
"No database URL provided. Set ATLAS_DATASOURCE_URL to a PostgreSQL (postgresql://...), MySQL (mysql://...), ClickHouse (clickhouse://... or clickhouses://...), Snowflake (snowflake://...), DuckDB (duckdb://...), or Salesforce (salesforce://...) connection string."
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (connStr.startsWith("postgresql://") || connStr.startsWith("postgres://")) {
|
|
123
|
+
return "postgres";
|
|
124
|
+
}
|
|
125
|
+
if (connStr.startsWith("mysql://") || connStr.startsWith("mysql2://")) {
|
|
126
|
+
return "mysql";
|
|
127
|
+
}
|
|
128
|
+
if (connStr.startsWith("clickhouse://") || connStr.startsWith("clickhouses://")) {
|
|
129
|
+
return "clickhouse";
|
|
130
|
+
}
|
|
131
|
+
if (connStr.startsWith("snowflake://")) {
|
|
132
|
+
return "snowflake";
|
|
133
|
+
}
|
|
134
|
+
if (connStr.startsWith("duckdb://")) {
|
|
135
|
+
return "duckdb";
|
|
136
|
+
}
|
|
137
|
+
if (connStr.startsWith("salesforce://")) {
|
|
138
|
+
return "salesforce";
|
|
139
|
+
}
|
|
140
|
+
const scheme = connStr.split("://")[0] || "(empty)";
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Unsupported database URL scheme "${scheme}://". ` +
|
|
143
|
+
"ATLAS_DATASOURCE_URL must start with postgresql://, postgres://, mysql://, mysql2://, clickhouse://, clickhouses://, snowflake://, duckdb://, or salesforce://."
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface ConnectionConfig {
|
|
148
|
+
/** Database connection string (postgresql://, mysql://, clickhouse://, snowflake://, duckdb://, or salesforce://). */
|
|
149
|
+
url: string;
|
|
150
|
+
/** PostgreSQL schema name (sets search_path). Ignored for MySQL, ClickHouse, Snowflake, and DuckDB. */
|
|
151
|
+
schema?: string;
|
|
152
|
+
/** Human-readable description shown in the agent system prompt. */
|
|
153
|
+
description?: string;
|
|
154
|
+
/** Max connections in the pool for this datasource. Default 10. */
|
|
155
|
+
maxConnections?: number;
|
|
156
|
+
/** Idle timeout in milliseconds before a connection is closed. Default 30000. Only applies to PostgreSQL pools. */
|
|
157
|
+
idleTimeoutMs?: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Regex for valid SQL identifiers (used for schema name validation). */
|
|
161
|
+
const VALID_SQL_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
162
|
+
|
|
163
|
+
function createPostgresDB(config: ConnectionConfig): DBConnection {
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
165
|
+
const { Pool } = require("pg");
|
|
166
|
+
|
|
167
|
+
const pgSchema = config.schema;
|
|
168
|
+
|
|
169
|
+
// Validate schema at initialization time to prevent SQL injection
|
|
170
|
+
if (pgSchema && !VALID_SQL_IDENTIFIER.test(pgSchema)) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Invalid schema "${pgSchema}". Must be a valid SQL identifier (letters, digits, underscores).`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const pool = new Pool({
|
|
177
|
+
connectionString: config.url,
|
|
178
|
+
max: config.maxConnections ?? 10,
|
|
179
|
+
idleTimeoutMillis: config.idleTimeoutMs ?? 30000,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const needsSchema = !!(pgSchema && pgSchema !== "public");
|
|
183
|
+
|
|
184
|
+
// Track which physical connections have had search_path set (once per connection,
|
|
185
|
+
// not per query). WeakSet lets GC reclaim entries when pg-pool drops a connection.
|
|
186
|
+
const initializedClients = needsSchema ? new WeakSet<object>() : null;
|
|
187
|
+
|
|
188
|
+
// One-time schema existence check, guarded by a shared Promise so concurrent
|
|
189
|
+
// first queries don't all hit pg_namespace redundantly.
|
|
190
|
+
let schemaCheckPromise: Promise<void> | null = null;
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
async query(sql: string, timeoutMs = 30000) {
|
|
194
|
+
const client = await pool.connect();
|
|
195
|
+
try {
|
|
196
|
+
// Verify the schema exists (once, shared across concurrent callers).
|
|
197
|
+
// Must run BEFORE setting search_path so no query executes against a
|
|
198
|
+
// non-existent schema.
|
|
199
|
+
if (needsSchema && !schemaCheckPromise) {
|
|
200
|
+
schemaCheckPromise = (async () => {
|
|
201
|
+
const check = await client.query(
|
|
202
|
+
"SELECT 1 FROM pg_namespace WHERE nspname = $1",
|
|
203
|
+
[pgSchema]
|
|
204
|
+
);
|
|
205
|
+
if (check.rows.length === 0) {
|
|
206
|
+
schemaCheckPromise = null; // allow retry after error
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Schema "${pgSchema}" does not exist in the database. Check ATLAS_SCHEMA in your .env file.`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
})();
|
|
212
|
+
}
|
|
213
|
+
if (schemaCheckPromise) await schemaCheckPromise;
|
|
214
|
+
|
|
215
|
+
// Set search_path once per physical connection (not per query)
|
|
216
|
+
if (needsSchema && initializedClients && !initializedClients.has(client)) {
|
|
217
|
+
await client.query(`SET search_path TO "${pgSchema}", public`);
|
|
218
|
+
initializedClients.add(client);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
await client.query(`SET statement_timeout = ${timeoutMs}`);
|
|
222
|
+
const result = await client.query(sql);
|
|
223
|
+
const columns = result.fields.map(
|
|
224
|
+
(f: { name: string }) => f.name
|
|
225
|
+
);
|
|
226
|
+
return { columns, rows: result.rows };
|
|
227
|
+
} finally {
|
|
228
|
+
client.release();
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
async close() {
|
|
232
|
+
await pool.end();
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function createMySQLDB(config: ConnectionConfig): DBConnection {
|
|
238
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
239
|
+
const mysql = require("mysql2/promise");
|
|
240
|
+
const pool = mysql.createPool({
|
|
241
|
+
uri: config.url,
|
|
242
|
+
connectionLimit: config.maxConnections ?? 10,
|
|
243
|
+
idleTimeout: config.idleTimeoutMs ?? 30000,
|
|
244
|
+
supportBigNumbers: true,
|
|
245
|
+
bigNumberStrings: true,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
async query(sql: string, timeoutMs = 30000) {
|
|
250
|
+
const conn = await pool.getConnection();
|
|
251
|
+
try {
|
|
252
|
+
// Defense-in-depth: read-only session prevents DML even if validation has a bug
|
|
253
|
+
await conn.execute('SET SESSION TRANSACTION READ ONLY');
|
|
254
|
+
// Per-query timeout via session variable (works for all query shapes including CTEs)
|
|
255
|
+
const safeTimeout = Number.isFinite(timeoutMs) ? Math.max(0, Math.floor(timeoutMs)) : 30000;
|
|
256
|
+
await conn.execute(`SET SESSION MAX_EXECUTION_TIME = ${safeTimeout}`);
|
|
257
|
+
const [rows, fields] = await conn.execute(sql);
|
|
258
|
+
const columns = (fields as { name: string }[]).map((f) => f.name);
|
|
259
|
+
return { columns, rows: rows as Record<string, unknown>[] };
|
|
260
|
+
} finally {
|
|
261
|
+
conn.release();
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
async close() {
|
|
265
|
+
await pool.end();
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function createClickHouseDB(config: ConnectionConfig): DBConnection {
|
|
271
|
+
let createClient: (opts: Record<string, unknown>) => unknown;
|
|
272
|
+
try {
|
|
273
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
274
|
+
({ createClient } = require("@clickhouse/client"));
|
|
275
|
+
} catch {
|
|
276
|
+
throw new Error(
|
|
277
|
+
"ClickHouse support requires the @clickhouse/client package. Install it with: bun add @clickhouse/client"
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const httpUrl = rewriteClickHouseUrl(config.url);
|
|
282
|
+
|
|
283
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
284
|
+
const client = (createClient as any)({ url: httpUrl });
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
async query(sql: string, timeoutMs = 30000) {
|
|
288
|
+
let result;
|
|
289
|
+
try {
|
|
290
|
+
result = await client.query({
|
|
291
|
+
query: sql,
|
|
292
|
+
format: "JSON",
|
|
293
|
+
clickhouse_settings: {
|
|
294
|
+
max_execution_time: Math.ceil(timeoutMs / 1000),
|
|
295
|
+
readonly: 1,
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
} catch (err) {
|
|
299
|
+
throw new Error(`ClickHouse query failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
300
|
+
}
|
|
301
|
+
const json = await result.json();
|
|
302
|
+
if (!json.meta || !Array.isArray(json.meta)) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
"ClickHouse query returned an unexpected response: missing or invalid 'meta' field. " +
|
|
305
|
+
"Ensure the query uses JSON format and returns a valid result set."
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
const columns = (json.meta as { name: string }[]).map((m: { name: string }) => m.name);
|
|
309
|
+
return { columns, rows: json.data as Record<string, unknown>[] };
|
|
310
|
+
},
|
|
311
|
+
async close() {
|
|
312
|
+
try {
|
|
313
|
+
await client.close();
|
|
314
|
+
} catch (err) {
|
|
315
|
+
log.warn(
|
|
316
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
317
|
+
"Failed to close ClickHouse client"
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Parse a Snowflake connection URL into SDK ConnectionOptions.
|
|
326
|
+
* Format: snowflake://user:pass@account/database/schema?warehouse=WH&role=ROLE
|
|
327
|
+
*
|
|
328
|
+
* - `account` can be a plain account identifier (e.g. `xy12345`) or a
|
|
329
|
+
* fully-qualified account locator (e.g. `xy12345.us-east-1`).
|
|
330
|
+
* - `/database` and `/database/schema` path segments are optional.
|
|
331
|
+
* - Query parameters: `warehouse`, `role` (case-insensitive).
|
|
332
|
+
*/
|
|
333
|
+
export function parseSnowflakeURL(url: string): {
|
|
334
|
+
account: string;
|
|
335
|
+
username: string;
|
|
336
|
+
password: string;
|
|
337
|
+
database?: string;
|
|
338
|
+
schema?: string;
|
|
339
|
+
warehouse?: string;
|
|
340
|
+
role?: string;
|
|
341
|
+
} {
|
|
342
|
+
const parsed = new URL(url);
|
|
343
|
+
if (parsed.protocol !== "snowflake:") {
|
|
344
|
+
throw new Error(`Invalid Snowflake URL: expected snowflake:// scheme, got "${parsed.protocol}"`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const account = parsed.hostname;
|
|
348
|
+
if (!account) {
|
|
349
|
+
throw new Error("Invalid Snowflake URL: missing account identifier in hostname.");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const username = decodeURIComponent(parsed.username);
|
|
353
|
+
const password = decodeURIComponent(parsed.password);
|
|
354
|
+
if (!username) {
|
|
355
|
+
throw new Error("Invalid Snowflake URL: missing username.");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Path segments: /database or /database/schema
|
|
359
|
+
const pathSegments = parsed.pathname.split("/").filter(Boolean);
|
|
360
|
+
const database = pathSegments[0] || undefined;
|
|
361
|
+
const schema = pathSegments[1] || undefined;
|
|
362
|
+
|
|
363
|
+
const warehouse = parsed.searchParams.get("warehouse") ?? undefined;
|
|
364
|
+
const role = parsed.searchParams.get("role") ?? undefined;
|
|
365
|
+
|
|
366
|
+
return { account, username, password, database, schema, warehouse, role };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function createSnowflakeDB(config: ConnectionConfig): DBConnection {
|
|
370
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
371
|
+
const snowflake = require("snowflake-sdk") as typeof import("snowflake-sdk");
|
|
372
|
+
|
|
373
|
+
// Suppress noisy SDK logging
|
|
374
|
+
snowflake.configure({ logLevel: "ERROR" });
|
|
375
|
+
|
|
376
|
+
const opts = parseSnowflakeURL(config.url);
|
|
377
|
+
|
|
378
|
+
const pool = snowflake.createPool(
|
|
379
|
+
{
|
|
380
|
+
account: opts.account,
|
|
381
|
+
username: opts.username,
|
|
382
|
+
password: opts.password,
|
|
383
|
+
database: opts.database,
|
|
384
|
+
schema: opts.schema,
|
|
385
|
+
warehouse: opts.warehouse,
|
|
386
|
+
role: opts.role,
|
|
387
|
+
application: "Atlas",
|
|
388
|
+
},
|
|
389
|
+
{ max: config.maxConnections ?? 10, min: 0 },
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
log.warn(
|
|
393
|
+
"Snowflake has no session-level read-only mode — Atlas enforces SELECT-only " +
|
|
394
|
+
"via SQL validation (regex + AST). For defense-in-depth, configure the " +
|
|
395
|
+
"Snowflake connection with a role granted SELECT privileges only " +
|
|
396
|
+
"(e.g. GRANT SELECT ON ALL TABLES IN SCHEMA <schema> TO ROLE atlas_readonly).",
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
async query(sql: string, timeoutMs = 30000) {
|
|
401
|
+
const timeoutSec = Math.max(1, Math.floor(timeoutMs / 1000));
|
|
402
|
+
return pool.use(async (conn) => {
|
|
403
|
+
// Set session-level statement timeout (seconds)
|
|
404
|
+
await new Promise<void>((resolve, reject) => {
|
|
405
|
+
conn.execute({
|
|
406
|
+
sqlText: `ALTER SESSION SET STATEMENT_TIMEOUT_IN_SECONDS = ${timeoutSec}`,
|
|
407
|
+
complete: (err) => (err ? reject(err) : resolve()),
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Tag all Atlas queries for audit trail in QUERY_HISTORY (best-effort —
|
|
412
|
+
// insufficient privileges or transient errors should not block the query)
|
|
413
|
+
try {
|
|
414
|
+
await new Promise<void>((resolve, reject) => {
|
|
415
|
+
conn.execute({
|
|
416
|
+
sqlText: `ALTER SESSION SET QUERY_TAG = 'atlas:readonly'`,
|
|
417
|
+
complete: (err) => (err ? reject(err) : resolve()),
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
} catch (tagErr) {
|
|
421
|
+
log.warn(`Failed to set QUERY_TAG on Snowflake session — query will proceed without audit tag: ${tagErr instanceof Error ? tagErr.message : String(tagErr)}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Execute the actual query
|
|
425
|
+
return new Promise<QueryResult>((resolve, reject) => {
|
|
426
|
+
conn.execute({
|
|
427
|
+
sqlText: sql,
|
|
428
|
+
complete: (err, stmt, rows) => {
|
|
429
|
+
if (err) return reject(err);
|
|
430
|
+
const columns = (stmt?.getColumns() ?? []).map((c) => c.getName());
|
|
431
|
+
const resultRows = (rows ?? []) as Record<string, unknown>[];
|
|
432
|
+
resolve({ columns, rows: resultRows });
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
},
|
|
438
|
+
async close() {
|
|
439
|
+
await pool.drain();
|
|
440
|
+
await pool.clear();
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function createConnection(dbType: DBType, config: ConnectionConfig): DBConnection {
|
|
446
|
+
switch (dbType) {
|
|
447
|
+
case "postgres":
|
|
448
|
+
return createPostgresDB(config);
|
|
449
|
+
case "mysql":
|
|
450
|
+
return createMySQLDB(config);
|
|
451
|
+
case "clickhouse":
|
|
452
|
+
return createClickHouseDB(config);
|
|
453
|
+
case "snowflake":
|
|
454
|
+
return createSnowflakeDB(config);
|
|
455
|
+
case "duckdb":
|
|
456
|
+
return createDuckDBConnection(parseDuckDBUrl(config.url));
|
|
457
|
+
case "salesforce":
|
|
458
|
+
throw new Error(
|
|
459
|
+
"Salesforce uses SOQL, not SQL. Use the Salesforce DataSource API instead. " +
|
|
460
|
+
"See packages/api/src/lib/db/salesforce.ts for the Salesforce adapter."
|
|
461
|
+
);
|
|
462
|
+
default: {
|
|
463
|
+
const _exhaustive: never = dbType;
|
|
464
|
+
throw new Error(`Unknown database type: ${_exhaustive}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// --- Connection Registry ---
|
|
470
|
+
|
|
471
|
+
interface RegistryEntry {
|
|
472
|
+
conn: DBConnection;
|
|
473
|
+
dbType: DBType;
|
|
474
|
+
description?: string;
|
|
475
|
+
lastQueryAt: number;
|
|
476
|
+
config?: ConnectionConfig;
|
|
477
|
+
targetHost: string;
|
|
478
|
+
consecutiveFailures: number;
|
|
479
|
+
lastHealth: HealthCheckResult | null;
|
|
480
|
+
firstFailureAt: number | null;
|
|
481
|
+
/** Custom query validator (mirrors QueryValidationResult from plugin-sdk). */
|
|
482
|
+
validate?: (query: string) => { valid: boolean; reason?: string };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Named connection registry. Connections can be created from a ConnectionConfig
|
|
487
|
+
* (URL + optional schema) via register(), or injected as pre-built DBConnection
|
|
488
|
+
* instances via registerDirect(). The "default" connection auto-initializes from
|
|
489
|
+
* ATLAS_DATASOURCE_URL on first access via getDefault().
|
|
490
|
+
*/
|
|
491
|
+
export class ConnectionRegistry {
|
|
492
|
+
private entries = new Map<string, RegistryEntry>();
|
|
493
|
+
private maxTotalConnections = 100;
|
|
494
|
+
private healthCheckInterval: ReturnType<typeof setInterval> | null = null;
|
|
495
|
+
|
|
496
|
+
setMaxTotalConnections(n: number): void {
|
|
497
|
+
this.maxTotalConnections = n;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private _totalPoolSlots(): number {
|
|
501
|
+
let total = 0;
|
|
502
|
+
for (const entry of this.entries.values()) {
|
|
503
|
+
// ClickHouse uses a stateless HTTP client and DuckDB is in-process —
|
|
504
|
+
// neither maintains a connection pool, so count them as 1 slot each.
|
|
505
|
+
if (entry.dbType === "clickhouse" || entry.dbType === "duckdb") {
|
|
506
|
+
total += 1;
|
|
507
|
+
} else {
|
|
508
|
+
total += entry.config?.maxConnections ?? 10;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return total;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private _evictLRU(): void {
|
|
515
|
+
let oldest: { id: string; entry: RegistryEntry } | null = null;
|
|
516
|
+
for (const [id, entry] of this.entries) {
|
|
517
|
+
if (id === "default") continue;
|
|
518
|
+
if (!oldest || entry.lastQueryAt < oldest.entry.lastQueryAt) {
|
|
519
|
+
oldest = { id, entry };
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (oldest) {
|
|
523
|
+
log.info({ connectionId: oldest.id }, "Evicting LRU connection to free pool capacity");
|
|
524
|
+
oldest.entry.conn.close().catch((err) => {
|
|
525
|
+
log.warn({ err: err instanceof Error ? err.message : String(err), connectionId: oldest!.id }, "Failed to close evicted connection");
|
|
526
|
+
});
|
|
527
|
+
this.entries.delete(oldest.id);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
register(id: string, config: ConnectionConfig): void {
|
|
532
|
+
const dbType = detectDBType(config.url);
|
|
533
|
+
const newConn = createConnection(dbType, config);
|
|
534
|
+
const existing = this.entries.get(id);
|
|
535
|
+
const targetHost = extractTargetHost(config.url);
|
|
536
|
+
|
|
537
|
+
// Check LRU cap — only for new entries (re-registrations replace in-place)
|
|
538
|
+
if (!existing) {
|
|
539
|
+
const newSlots = config.maxConnections ?? 10;
|
|
540
|
+
while (this._totalPoolSlots() + newSlots > this.maxTotalConnections && this.entries.size > 0) {
|
|
541
|
+
this._evictLRU();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
this.entries.set(id, {
|
|
546
|
+
conn: newConn,
|
|
547
|
+
dbType,
|
|
548
|
+
description: config.description,
|
|
549
|
+
lastQueryAt: Date.now(),
|
|
550
|
+
config,
|
|
551
|
+
targetHost,
|
|
552
|
+
consecutiveFailures: 0,
|
|
553
|
+
lastHealth: null,
|
|
554
|
+
firstFailureAt: null,
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
if (existing) {
|
|
558
|
+
existing.conn.close().catch((err) => {
|
|
559
|
+
log.warn({ err: err instanceof Error ? err.message : String(err), connectionId: id }, "Failed to close previous connection during re-registration");
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** Register a pre-built connection (e.g. for benchmark harness or datasource plugin). */
|
|
565
|
+
registerDirect(
|
|
566
|
+
id: string,
|
|
567
|
+
conn: DBConnection,
|
|
568
|
+
dbType: DBType,
|
|
569
|
+
description?: string,
|
|
570
|
+
validate?: (query: string) => { valid: boolean; reason?: string },
|
|
571
|
+
): void {
|
|
572
|
+
const existing = this.entries.get(id);
|
|
573
|
+
this.entries.set(id, {
|
|
574
|
+
conn,
|
|
575
|
+
dbType,
|
|
576
|
+
description,
|
|
577
|
+
lastQueryAt: Date.now(),
|
|
578
|
+
targetHost: "(direct)",
|
|
579
|
+
consecutiveFailures: 0,
|
|
580
|
+
lastHealth: null,
|
|
581
|
+
firstFailureAt: null,
|
|
582
|
+
validate,
|
|
583
|
+
});
|
|
584
|
+
if (existing) {
|
|
585
|
+
existing.conn.close().catch((err) => {
|
|
586
|
+
log.warn({ err: err instanceof Error ? err.message : String(err), connectionId: id }, "Failed to close previous connection during re-registration");
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
get(id: string): DBConnection {
|
|
592
|
+
const entry = this.entries.get(id);
|
|
593
|
+
if (!entry) {
|
|
594
|
+
throw new Error(`Connection "${id}" is not registered.`);
|
|
595
|
+
}
|
|
596
|
+
entry.lastQueryAt = Date.now();
|
|
597
|
+
return entry.conn;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
getDBType(id: string): DBType {
|
|
601
|
+
const entry = this.entries.get(id);
|
|
602
|
+
if (!entry) throw new Error(`Connection "${id}" is not registered.`);
|
|
603
|
+
return entry.dbType;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** Return the hostname (without credentials) for a registered connection. Returns "(unknown)" if not registered. */
|
|
607
|
+
getTargetHost(id: string): string {
|
|
608
|
+
const entry = this.entries.get(id);
|
|
609
|
+
if (!entry) return "(unknown)";
|
|
610
|
+
return entry.targetHost;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Return the custom query validator for a connection, if one was registered. Callers must verify connection existence first. */
|
|
614
|
+
getValidator(id: string): ((query: string) => { valid: boolean; reason?: string }) | undefined {
|
|
615
|
+
return this.entries.get(id)?.validate;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
getDefault(): DBConnection {
|
|
619
|
+
if (!this.entries.has("default")) {
|
|
620
|
+
if (!process.env.ATLAS_DATASOURCE_URL) {
|
|
621
|
+
throw new Error(
|
|
622
|
+
"No analytics datasource configured. Set ATLAS_DATASOURCE_URL to a PostgreSQL, MySQL, ClickHouse, Snowflake, DuckDB, or Salesforce connection string."
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
this.register("default", {
|
|
626
|
+
url: process.env.ATLAS_DATASOURCE_URL,
|
|
627
|
+
schema: process.env.ATLAS_SCHEMA,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
const entry = this.entries.get("default")!;
|
|
631
|
+
entry.lastQueryAt = Date.now();
|
|
632
|
+
return entry.conn;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
list(): string[] {
|
|
636
|
+
return Array.from(this.entries.keys());
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** Metadata for all registered connections. Used by the agent system prompt. */
|
|
640
|
+
describe(): ConnectionMetadata[] {
|
|
641
|
+
return Array.from(this.entries.entries()).map(([id, entry]) => ({
|
|
642
|
+
id,
|
|
643
|
+
dbType: entry.dbType,
|
|
644
|
+
description: entry.description,
|
|
645
|
+
...(entry.lastHealth ? { health: entry.lastHealth } : {}),
|
|
646
|
+
}));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** Run a health check for a specific connection. */
|
|
650
|
+
async healthCheck(id: string): Promise<HealthCheckResult> {
|
|
651
|
+
const entry = this.entries.get(id);
|
|
652
|
+
if (!entry) {
|
|
653
|
+
throw new Error(`Connection "${id}" is not registered.`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const start = performance.now();
|
|
657
|
+
try {
|
|
658
|
+
await entry.conn.query("SELECT 1", 5000);
|
|
659
|
+
const latencyMs = Math.round(performance.now() - start);
|
|
660
|
+
entry.consecutiveFailures = 0;
|
|
661
|
+
entry.firstFailureAt = null;
|
|
662
|
+
const result: HealthCheckResult = {
|
|
663
|
+
status: "healthy",
|
|
664
|
+
latencyMs,
|
|
665
|
+
checkedAt: new Date(),
|
|
666
|
+
};
|
|
667
|
+
entry.lastHealth = result;
|
|
668
|
+
return result;
|
|
669
|
+
} catch (err) {
|
|
670
|
+
const latencyMs = Math.round(performance.now() - start);
|
|
671
|
+
entry.consecutiveFailures++;
|
|
672
|
+
if (entry.firstFailureAt === null) {
|
|
673
|
+
entry.firstFailureAt = Date.now();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const failureSpan = Date.now() - entry.firstFailureAt;
|
|
677
|
+
let status: HealthStatus;
|
|
678
|
+
if (entry.consecutiveFailures >= UNHEALTHY_THRESHOLD && failureSpan >= UNHEALTHY_WINDOW_MS) {
|
|
679
|
+
status = "unhealthy";
|
|
680
|
+
} else {
|
|
681
|
+
status = "degraded";
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const result: HealthCheckResult = {
|
|
685
|
+
status,
|
|
686
|
+
latencyMs,
|
|
687
|
+
message: err instanceof Error ? err.message : String(err),
|
|
688
|
+
checkedAt: new Date(),
|
|
689
|
+
};
|
|
690
|
+
entry.lastHealth = result;
|
|
691
|
+
return result;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** Start periodic health checks for all connections. Idempotent. */
|
|
696
|
+
startHealthChecks(intervalMs = 60_000): void {
|
|
697
|
+
if (this.healthCheckInterval) return;
|
|
698
|
+
this.healthCheckInterval = setInterval(() => {
|
|
699
|
+
for (const id of this.entries.keys()) {
|
|
700
|
+
this.healthCheck(id).catch((err) => {
|
|
701
|
+
log.warn({ err: err instanceof Error ? err.message : String(err), connectionId: id }, "Periodic health check failed");
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
}, intervalMs);
|
|
705
|
+
this.healthCheckInterval.unref();
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/** Stop periodic health checks. */
|
|
709
|
+
stopHealthChecks(): void {
|
|
710
|
+
if (this.healthCheckInterval) {
|
|
711
|
+
clearInterval(this.healthCheckInterval);
|
|
712
|
+
this.healthCheckInterval = null;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Graceful shutdown: stop health checks, close all connections (awaited), and
|
|
718
|
+
* reset whitelists. Use this in production shutdown paths instead of _reset().
|
|
719
|
+
*/
|
|
720
|
+
async shutdown(): Promise<void> {
|
|
721
|
+
this.stopHealthChecks();
|
|
722
|
+
const closing: Promise<void>[] = [];
|
|
723
|
+
for (const [id, entry] of this.entries.entries()) {
|
|
724
|
+
closing.push(
|
|
725
|
+
entry.conn.close().catch((err) => {
|
|
726
|
+
log.warn({ err: err instanceof Error ? err.message : String(err), connectionId: id }, "Failed to close connection during shutdown");
|
|
727
|
+
}),
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
await Promise.all(closing);
|
|
731
|
+
this.entries.clear();
|
|
732
|
+
_resetWhitelists();
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** Clears all registered connections and resets the table whitelist cache. Used during graceful shutdown, tests, and the benchmark harness. */
|
|
736
|
+
_reset(): void {
|
|
737
|
+
this.stopHealthChecks();
|
|
738
|
+
for (const [id, entry] of this.entries.entries()) {
|
|
739
|
+
entry.conn.close().catch((err) => {
|
|
740
|
+
log.warn({ err: err instanceof Error ? err.message : String(err), connectionId: id }, "Failed to close connection during registry reset");
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
this.entries.clear();
|
|
744
|
+
_resetWhitelists();
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export const connections = new ConnectionRegistry();
|
|
749
|
+
|
|
750
|
+
/** Backward-compatible singleton — delegates to the connection registry. */
|
|
751
|
+
export function getDB(): DBConnection {
|
|
752
|
+
return connections.getDefault();
|
|
753
|
+
}
|