@useatlas/create 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -18
- package/index.ts +191 -31
- package/package.json +1 -1
- package/templates/docker/.env.example +3 -3
- package/templates/docker/Dockerfile.sidecar +28 -0
- package/templates/docker/bin/__tests__/plugin-cli.test.ts +9 -9
- package/templates/docker/bin/atlas.ts +108 -44
- package/templates/docker/data/demo-semantic/catalog.yml +51 -27
- package/templates/docker/data/demo-semantic/entities/accounts.yml +95 -103
- package/templates/docker/data/demo-semantic/entities/companies.yml +88 -152
- package/templates/docker/data/demo-semantic/entities/people.yml +82 -95
- package/templates/docker/data/demo-semantic/glossary.yml +104 -8
- package/templates/docker/data/demo-semantic/metrics/accounts.yml +62 -23
- package/templates/docker/data/demo-semantic/metrics/companies.yml +52 -78
- package/templates/docker/docker-compose.yml +1 -1
- package/templates/docker/docs/deploy.md +2 -39
- package/templates/docker/package.json +17 -1
- package/templates/docker/semantic/catalog.yml +62 -3
- package/templates/docker/semantic/entities/accounts.yml +162 -0
- package/templates/docker/semantic/entities/companies.yml +143 -0
- package/templates/docker/semantic/entities/people.yml +132 -0
- package/templates/docker/semantic/glossary.yml +116 -4
- package/templates/docker/semantic/metrics/accounts.yml +77 -0
- package/templates/docker/semantic/metrics/companies.yml +63 -0
- package/templates/docker/sidecar/Dockerfile +5 -6
- package/templates/docker/sidecar/railway.json +1 -2
- package/templates/docker/src/api/__tests__/admin.test.ts +7 -7
- package/templates/docker/src/api/__tests__/health-plugin.test.ts +7 -0
- package/templates/docker/src/api/__tests__/health.test.ts +30 -8
- package/templates/docker/src/api/routes/admin.ts +549 -8
- package/templates/docker/src/api/routes/chat.ts +5 -20
- package/templates/docker/src/api/routes/health.ts +39 -27
- package/templates/docker/src/api/routes/openapi.ts +1329 -74
- package/templates/docker/src/api/routes/query.ts +2 -1
- package/templates/docker/src/api/server.ts +27 -0
- package/templates/docker/src/app/api/[...route]/route.ts +2 -2
- package/templates/docker/src/app/globals.css +13 -12
- package/templates/docker/src/app/layout.tsx +9 -2
- package/templates/docker/src/components/ui/alert-dialog.tsx +196 -0
- package/templates/docker/src/components/ui/badge.tsx +48 -0
- package/templates/docker/src/components/ui/button.tsx +64 -0
- package/templates/docker/src/components/ui/card.tsx +92 -0
- package/templates/docker/src/components/ui/collapsible.tsx +33 -0
- package/templates/docker/src/components/ui/command.tsx +184 -0
- package/templates/docker/src/components/ui/dialog.tsx +158 -0
- package/templates/docker/src/components/ui/dropdown-menu.tsx +257 -0
- package/templates/docker/src/components/ui/input.tsx +21 -0
- package/templates/docker/src/components/ui/scroll-area.tsx +58 -0
- package/templates/docker/src/components/ui/select.tsx +190 -0
- package/templates/docker/src/components/ui/separator.tsx +28 -0
- package/templates/docker/src/components/ui/sheet.tsx +143 -0
- package/templates/docker/src/components/ui/sidebar.tsx +726 -0
- package/templates/docker/src/components/ui/skeleton.tsx +13 -0
- package/templates/docker/src/components/ui/table.tsx +116 -0
- package/templates/docker/src/components/ui/tabs.tsx +91 -0
- package/templates/docker/src/components/ui/toggle-group.tsx +83 -0
- package/templates/docker/src/components/ui/toggle.tsx +47 -0
- package/templates/docker/src/components/ui/tooltip.tsx +57 -0
- package/templates/docker/src/hooks/use-mobile.ts +19 -0
- package/templates/docker/src/lib/__tests__/agent-cache.test.ts +2 -0
- package/templates/docker/src/lib/__tests__/agent-dialect.test.ts +17 -0
- package/templates/docker/src/lib/__tests__/agent-health-annotations.test.ts +2 -0
- package/templates/docker/src/lib/__tests__/agent-integration.test.ts +2 -0
- package/templates/docker/src/lib/__tests__/config.test.ts +69 -19
- package/templates/docker/src/lib/__tests__/plugin-aware-validation.test.ts +321 -0
- package/templates/docker/src/lib/__tests__/providers.test.ts +32 -1
- package/templates/docker/src/lib/__tests__/startup-actions.test.ts +9 -0
- package/templates/docker/src/lib/__tests__/startup-first-run.test.ts +429 -0
- package/templates/docker/src/lib/__tests__/startup.test.ts +5 -0
- package/templates/docker/src/lib/agent-query.ts +5 -23
- package/templates/docker/src/lib/agent.ts +32 -112
- package/templates/docker/src/lib/auth/__tests__/migrate.test.ts +5 -3
- package/templates/docker/src/lib/auth/middleware.ts +30 -4
- package/templates/docker/src/lib/auth/migrate.ts +97 -0
- package/templates/docker/src/lib/auth/server.ts +12 -1
- package/templates/docker/src/lib/config.ts +37 -39
- package/templates/docker/src/lib/db/__tests__/connection.test.ts +89 -14
- package/templates/docker/src/lib/db/__tests__/registry-health.test.ts +1 -18
- package/templates/docker/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -19
- package/templates/docker/src/lib/db/__tests__/registry.test.ts +11 -208
- package/templates/docker/src/lib/db/connection.ts +87 -265
- package/templates/docker/src/lib/db/internal.ts +6 -1
- package/templates/docker/src/lib/plugins/__tests__/hooks-integration.test.ts +3 -1
- package/templates/docker/src/lib/plugins/__tests__/hooks.test.ts +2 -2
- package/templates/docker/src/lib/plugins/__tests__/migrate.test.ts +355 -1
- package/templates/docker/src/lib/plugins/__tests__/registry.test.ts +32 -5
- package/templates/docker/src/lib/plugins/__tests__/wiring.test.ts +228 -14
- package/templates/docker/src/lib/plugins/index.ts +4 -1
- package/templates/docker/src/lib/plugins/migrate.ts +103 -0
- package/templates/docker/src/lib/plugins/registry.ts +12 -6
- package/templates/docker/src/lib/plugins/wiring.ts +113 -4
- package/templates/docker/src/lib/providers.ts +6 -1
- package/templates/docker/src/lib/security.ts +24 -0
- package/templates/docker/src/lib/semantic.ts +2 -0
- package/templates/docker/src/lib/sidecar-types.ts +12 -1
- package/templates/docker/src/lib/startup.ts +71 -101
- package/templates/docker/src/lib/tools/__tests__/custom-validation.test.ts +2 -0
- package/templates/docker/src/lib/tools/__tests__/explore-nsjail.test.ts +32 -18
- package/templates/docker/src/lib/tools/__tests__/explore-plugin.test.ts +14 -14
- package/templates/docker/src/lib/tools/__tests__/explore-sidecar.test.ts +5 -3
- package/templates/docker/src/lib/tools/__tests__/python-nsjail.test.ts +515 -0
- package/templates/docker/src/lib/tools/__tests__/python-sandbox.test.ts +397 -0
- package/templates/docker/src/lib/tools/__tests__/python-sidecar.test.ts +365 -0
- package/templates/docker/src/lib/tools/__tests__/python.test.ts +331 -0
- package/templates/docker/src/lib/tools/__tests__/registry-actions.test.ts +1 -13
- package/templates/docker/src/lib/tools/__tests__/registry.test.ts +38 -31
- package/templates/docker/src/lib/tools/__tests__/sql-audit.test.ts +2 -0
- package/templates/docker/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +2 -0
- package/templates/docker/src/lib/tools/__tests__/sql-ratelimit.test.ts +2 -0
- package/templates/docker/src/lib/tools/__tests__/sql.test.ts +5 -308
- package/templates/docker/src/lib/tools/explore-nsjail.ts +17 -12
- package/templates/docker/src/lib/tools/explore-sidecar.ts +25 -0
- package/templates/docker/src/lib/tools/explore.ts +28 -32
- package/templates/docker/src/lib/tools/python-nsjail.ts +396 -0
- package/templates/docker/src/lib/tools/python-sandbox.ts +476 -0
- package/templates/docker/src/lib/tools/python-sidecar.ts +150 -0
- package/templates/docker/src/lib/tools/python.ts +367 -0
- package/templates/docker/src/lib/tools/registry.ts +49 -22
- package/templates/docker/src/lib/tools/sql.ts +88 -88
- package/templates/docker/src/types/vercel-sandbox.d.ts +7 -0
- package/templates/docker/src/ui/components/admin/admin-layout.tsx +77 -8
- package/templates/docker/src/ui/components/admin/admin-sidebar.tsx +25 -17
- package/templates/docker/src/ui/components/admin/change-password-dialog.tsx +128 -0
- package/templates/docker/src/ui/components/admin/entity-detail.tsx +3 -3
- package/templates/docker/src/ui/components/admin/semantic-file-tree.tsx +159 -0
- package/templates/docker/src/ui/components/atlas-chat.tsx +64 -12
- package/templates/docker/src/ui/components/chart/result-chart.tsx +25 -15
- package/templates/docker/src/ui/components/chat/markdown.tsx +88 -42
- package/templates/docker/src/ui/components/chat/python-result-card.tsx +244 -0
- package/templates/docker/src/ui/components/chat/sql-block.tsx +39 -15
- package/templates/docker/src/ui/components/chat/sql-result-card.tsx +6 -1
- package/templates/docker/src/ui/components/chat/tool-part.tsx +12 -3
- package/templates/docker/src/ui/components/chat/typing-indicator.tsx +5 -2
- package/templates/docker/src/ui/components/conversations/conversation-item.tsx +25 -20
- package/templates/docker/src/ui/context.tsx +1 -1
- package/templates/docker/src/ui/hooks/use-conversations.ts +3 -3
- package/templates/docker/src/ui/hooks/use-dark-mode.ts +17 -10
- package/templates/docker/tsconfig.json +2 -2
- package/templates/nextjs-standalone/.env.example +1 -1
- package/templates/nextjs-standalone/bin/__tests__/plugin-cli.test.ts +9 -9
- package/templates/nextjs-standalone/bin/atlas.ts +108 -44
- package/templates/nextjs-standalone/data/demo-semantic/catalog.yml +51 -27
- package/templates/nextjs-standalone/data/demo-semantic/entities/accounts.yml +95 -103
- package/templates/nextjs-standalone/data/demo-semantic/entities/companies.yml +88 -152
- package/templates/nextjs-standalone/data/demo-semantic/entities/people.yml +82 -95
- package/templates/nextjs-standalone/data/demo-semantic/glossary.yml +104 -8
- package/templates/nextjs-standalone/data/demo-semantic/metrics/accounts.yml +62 -23
- package/templates/nextjs-standalone/data/demo-semantic/metrics/companies.yml +52 -78
- package/templates/nextjs-standalone/docs/deploy.md +2 -39
- package/templates/nextjs-standalone/package.json +11 -2
- package/templates/nextjs-standalone/scripts/migrate-auth.ts +25 -0
- package/templates/nextjs-standalone/scripts/seed-demo.ts +94 -0
- package/templates/nextjs-standalone/semantic/catalog.yml +62 -3
- package/templates/nextjs-standalone/semantic/entities/accounts.yml +162 -0
- package/templates/nextjs-standalone/semantic/entities/companies.yml +143 -0
- package/templates/nextjs-standalone/semantic/entities/people.yml +132 -0
- package/templates/nextjs-standalone/semantic/glossary.yml +116 -4
- package/templates/nextjs-standalone/semantic/metrics/accounts.yml +77 -0
- package/templates/nextjs-standalone/semantic/metrics/companies.yml +63 -0
- package/templates/nextjs-standalone/src/api/__tests__/admin.test.ts +7 -7
- package/templates/nextjs-standalone/src/api/__tests__/health-plugin.test.ts +7 -0
- package/templates/nextjs-standalone/src/api/__tests__/health.test.ts +30 -8
- package/templates/nextjs-standalone/src/api/routes/admin.ts +549 -8
- package/templates/nextjs-standalone/src/api/routes/chat.ts +5 -20
- package/templates/nextjs-standalone/src/api/routes/health.ts +39 -27
- package/templates/nextjs-standalone/src/api/routes/openapi.ts +1329 -74
- package/templates/nextjs-standalone/src/api/routes/query.ts +2 -1
- package/templates/nextjs-standalone/src/api/server.ts +27 -0
- package/templates/nextjs-standalone/src/app/api/[...route]/route.ts +2 -2
- package/templates/nextjs-standalone/src/app/globals.css +13 -12
- package/templates/nextjs-standalone/src/app/layout.tsx +9 -2
- package/templates/nextjs-standalone/src/components/ui/alert-dialog.tsx +196 -0
- package/templates/nextjs-standalone/src/components/ui/badge.tsx +48 -0
- package/templates/nextjs-standalone/src/components/ui/button.tsx +64 -0
- package/templates/nextjs-standalone/src/components/ui/card.tsx +92 -0
- package/templates/nextjs-standalone/src/components/ui/collapsible.tsx +33 -0
- package/templates/nextjs-standalone/src/components/ui/command.tsx +184 -0
- package/templates/nextjs-standalone/src/components/ui/dialog.tsx +158 -0
- package/templates/nextjs-standalone/src/components/ui/dropdown-menu.tsx +257 -0
- package/templates/nextjs-standalone/src/components/ui/input.tsx +21 -0
- package/templates/nextjs-standalone/src/components/ui/scroll-area.tsx +58 -0
- package/templates/nextjs-standalone/src/components/ui/select.tsx +190 -0
- package/templates/nextjs-standalone/src/components/ui/separator.tsx +28 -0
- package/templates/nextjs-standalone/src/components/ui/sheet.tsx +143 -0
- package/templates/nextjs-standalone/src/components/ui/sidebar.tsx +726 -0
- package/templates/nextjs-standalone/src/components/ui/skeleton.tsx +13 -0
- package/templates/nextjs-standalone/src/components/ui/table.tsx +116 -0
- package/templates/nextjs-standalone/src/components/ui/tabs.tsx +91 -0
- package/templates/nextjs-standalone/src/components/ui/toggle-group.tsx +83 -0
- package/templates/nextjs-standalone/src/components/ui/toggle.tsx +47 -0
- package/templates/nextjs-standalone/src/components/ui/tooltip.tsx +57 -0
- package/templates/nextjs-standalone/src/hooks/use-mobile.ts +19 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-cache.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-dialect.test.ts +17 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-health-annotations.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-integration.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/__tests__/config.test.ts +69 -19
- package/templates/nextjs-standalone/src/lib/__tests__/plugin-aware-validation.test.ts +321 -0
- package/templates/nextjs-standalone/src/lib/__tests__/providers.test.ts +32 -1
- package/templates/nextjs-standalone/src/lib/__tests__/startup-actions.test.ts +9 -0
- package/templates/nextjs-standalone/src/lib/__tests__/startup-first-run.test.ts +429 -0
- package/templates/nextjs-standalone/src/lib/__tests__/startup.test.ts +5 -0
- package/templates/nextjs-standalone/src/lib/agent-query.ts +5 -23
- package/templates/nextjs-standalone/src/lib/agent.ts +32 -112
- package/templates/nextjs-standalone/src/lib/auth/__tests__/migrate.test.ts +5 -3
- package/templates/nextjs-standalone/src/lib/auth/middleware.ts +30 -4
- package/templates/nextjs-standalone/src/lib/auth/migrate.ts +97 -0
- package/templates/nextjs-standalone/src/lib/auth/server.ts +12 -1
- package/templates/nextjs-standalone/src/lib/config.ts +37 -39
- package/templates/nextjs-standalone/src/lib/db/__tests__/connection.test.ts +89 -14
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry-health.test.ts +1 -18
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -19
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry.test.ts +11 -208
- package/templates/nextjs-standalone/src/lib/db/connection.ts +87 -265
- package/templates/nextjs-standalone/src/lib/db/internal.ts +6 -1
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks-integration.test.ts +3 -1
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks.test.ts +2 -2
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/migrate.test.ts +355 -1
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/registry.test.ts +32 -5
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/wiring.test.ts +228 -14
- package/templates/nextjs-standalone/src/lib/plugins/index.ts +4 -1
- package/templates/nextjs-standalone/src/lib/plugins/migrate.ts +103 -0
- package/templates/nextjs-standalone/src/lib/plugins/registry.ts +12 -6
- package/templates/nextjs-standalone/src/lib/plugins/wiring.ts +113 -4
- package/templates/nextjs-standalone/src/lib/providers.ts +6 -1
- package/templates/nextjs-standalone/src/lib/security.ts +24 -0
- package/templates/nextjs-standalone/src/lib/semantic.ts +2 -0
- package/templates/nextjs-standalone/src/lib/sidecar-types.ts +12 -1
- package/templates/nextjs-standalone/src/lib/startup.ts +71 -101
- package/templates/nextjs-standalone/src/lib/tools/__tests__/custom-validation.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-nsjail.test.ts +32 -18
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-plugin.test.ts +14 -14
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sidecar.test.ts +5 -3
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python-nsjail.test.ts +515 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sandbox.test.ts +397 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sidecar.test.ts +365 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python.test.ts +331 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/registry-actions.test.ts +1 -13
- package/templates/nextjs-standalone/src/lib/tools/__tests__/registry.test.ts +38 -31
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-audit.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-ratelimit.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql.test.ts +5 -308
- package/templates/nextjs-standalone/src/lib/tools/explore-nsjail.ts +17 -12
- package/templates/nextjs-standalone/src/lib/tools/explore-sidecar.ts +25 -0
- package/templates/nextjs-standalone/src/lib/tools/explore.ts +28 -32
- package/templates/nextjs-standalone/src/lib/tools/python-nsjail.ts +396 -0
- package/templates/nextjs-standalone/src/lib/tools/python-sandbox.ts +476 -0
- package/templates/nextjs-standalone/src/lib/tools/python-sidecar.ts +150 -0
- package/templates/nextjs-standalone/src/lib/tools/python.ts +367 -0
- package/templates/nextjs-standalone/src/lib/tools/registry.ts +49 -22
- package/templates/nextjs-standalone/src/lib/tools/sql.ts +88 -88
- package/templates/nextjs-standalone/src/ui/components/admin/admin-layout.tsx +77 -8
- package/templates/nextjs-standalone/src/ui/components/admin/admin-sidebar.tsx +25 -17
- package/templates/nextjs-standalone/src/ui/components/admin/change-password-dialog.tsx +128 -0
- package/templates/nextjs-standalone/src/ui/components/admin/entity-detail.tsx +3 -3
- package/templates/nextjs-standalone/src/ui/components/admin/semantic-file-tree.tsx +159 -0
- package/templates/nextjs-standalone/src/ui/components/atlas-chat.tsx +64 -12
- package/templates/nextjs-standalone/src/ui/components/chart/result-chart.tsx +25 -15
- package/templates/nextjs-standalone/src/ui/components/chat/markdown.tsx +88 -42
- package/templates/nextjs-standalone/src/ui/components/chat/python-result-card.tsx +244 -0
- package/templates/nextjs-standalone/src/ui/components/chat/sql-block.tsx +39 -15
- package/templates/nextjs-standalone/src/ui/components/chat/sql-result-card.tsx +6 -1
- package/templates/nextjs-standalone/src/ui/components/chat/tool-part.tsx +12 -3
- package/templates/nextjs-standalone/src/ui/components/chat/typing-indicator.tsx +5 -2
- package/templates/nextjs-standalone/src/ui/components/conversations/conversation-item.tsx +25 -20
- package/templates/nextjs-standalone/src/ui/context.tsx +1 -1
- package/templates/nextjs-standalone/src/ui/hooks/use-conversations.ts +3 -3
- package/templates/nextjs-standalone/src/ui/hooks/use-dark-mode.ts +17 -10
- package/templates/nextjs-standalone/tsconfig.json +0 -1
- package/templates/nextjs-standalone/vercel.json +4 -1
- package/templates/docker/render.yaml +0 -34
- package/templates/docker/semantic/entities/.gitkeep +0 -0
- package/templates/docker/semantic/metrics/.gitkeep +0 -0
- package/templates/docker/src/lib/db/__tests__/duckdb.test.ts +0 -141
- package/templates/docker/src/lib/db/__tests__/salesforce.test.ts +0 -339
- package/templates/docker/src/lib/db/__tests__/snowflake.test.ts +0 -217
- package/templates/docker/src/lib/db/duckdb.ts +0 -122
- package/templates/docker/src/lib/db/salesforce.ts +0 -342
- package/templates/docker/src/lib/tools/__tests__/salesforce-tool.test.ts +0 -154
- package/templates/docker/src/lib/tools/__tests__/soql-validation.test.ts +0 -303
- package/templates/docker/src/lib/tools/__tests__/sql-duckdb.test.ts +0 -233
- package/templates/docker/src/lib/tools/salesforce.ts +0 -138
- package/templates/docker/src/lib/tools/soql-validation.ts +0 -172
- package/templates/nextjs-standalone/semantic/entities/.gitkeep +0 -0
- package/templates/nextjs-standalone/semantic/metrics/.gitkeep +0 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/duckdb.test.ts +0 -141
- package/templates/nextjs-standalone/src/lib/db/__tests__/salesforce.test.ts +0 -339
- package/templates/nextjs-standalone/src/lib/db/__tests__/snowflake.test.ts +0 -217
- package/templates/nextjs-standalone/src/lib/db/duckdb.ts +0 -122
- package/templates/nextjs-standalone/src/lib/db/salesforce.ts +0 -342
- package/templates/nextjs-standalone/src/lib/tools/__tests__/salesforce-tool.test.ts +0 -154
- package/templates/nextjs-standalone/src/lib/tools/__tests__/soql-validation.test.ts +0 -303
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-duckdb.test.ts +0 -233
- package/templates/nextjs-standalone/src/lib/tools/salesforce.ts +0 -138
- package/templates/nextjs-standalone/src/lib/tools/soql-validation.ts +0 -172
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Snowflake URL parsing, detectDBType integration, and connection behavior.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect, mock, beforeEach } from "bun:test";
|
|
5
|
-
import { resolve } from "path";
|
|
6
|
-
|
|
7
|
-
// Track all SQL statements executed via the mock connection.
|
|
8
|
-
// Reset in each describe block's beforeEach to prevent cross-test pollution.
|
|
9
|
-
let executedStatements: string[] = [];
|
|
10
|
-
|
|
11
|
-
// Flag to make the QUERY_TAG ALTER SESSION fail in specific tests
|
|
12
|
-
let queryTagShouldFail = false;
|
|
13
|
-
|
|
14
|
-
// Mock logger to capture structured log output
|
|
15
|
-
const mockWarn = mock(() => {});
|
|
16
|
-
mock.module("@atlas/api/lib/logger", () => ({
|
|
17
|
-
createLogger: () => ({
|
|
18
|
-
info: mock(() => {}),
|
|
19
|
-
warn: mockWarn,
|
|
20
|
-
error: mock(() => {}),
|
|
21
|
-
debug: mock(() => {}),
|
|
22
|
-
child: () => ({ info: mock(() => {}), warn: mockWarn, error: mock(() => {}), debug: mock(() => {}) }),
|
|
23
|
-
}),
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
// Mock database drivers before importing connection module
|
|
27
|
-
mock.module("pg", () => ({
|
|
28
|
-
Pool: class MockPool {
|
|
29
|
-
async query() { return { rows: [], fields: [] }; }
|
|
30
|
-
async connect() { return { async query() { return { rows: [], fields: [] }; }, release() {} }; }
|
|
31
|
-
async end() {}
|
|
32
|
-
},
|
|
33
|
-
}));
|
|
34
|
-
|
|
35
|
-
mock.module("mysql2/promise", () => ({
|
|
36
|
-
createPool: () => ({
|
|
37
|
-
async getConnection() { return { async execute() { return [[], []]; }, release() {} }; },
|
|
38
|
-
async end() {},
|
|
39
|
-
}),
|
|
40
|
-
}));
|
|
41
|
-
|
|
42
|
-
mock.module("snowflake-sdk", () => ({
|
|
43
|
-
configure: () => {},
|
|
44
|
-
createPool: () => ({
|
|
45
|
-
use: async (fn: (conn: unknown) => Promise<unknown>) => {
|
|
46
|
-
const mockConn = {
|
|
47
|
-
execute: (opts: { sqlText: string; complete: (err: Error | null, stmt: unknown, rows: unknown[]) => void }) => {
|
|
48
|
-
executedStatements.push(opts.sqlText);
|
|
49
|
-
if (queryTagShouldFail && opts.sqlText.includes("QUERY_TAG")) {
|
|
50
|
-
opts.complete(new Error("Insufficient privileges to set QUERY_TAG"), null, []);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
opts.complete(null, { getColumns: () => [] }, []);
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
return fn(mockConn);
|
|
57
|
-
},
|
|
58
|
-
drain: async () => {},
|
|
59
|
-
clear: async () => {},
|
|
60
|
-
}),
|
|
61
|
-
}));
|
|
62
|
-
|
|
63
|
-
// Cache-busting import to get a fresh module instance, avoiding interference
|
|
64
|
-
// from global mock.module("@atlas/api/lib/db/connection") in other test files
|
|
65
|
-
// (e.g. sql.test.ts) that don't export parseSnowflakeURL.
|
|
66
|
-
const connModPath = resolve(__dirname, "../connection.ts");
|
|
67
|
-
const connMod = await import(`${connModPath}?t=${Date.now()}`);
|
|
68
|
-
const parseSnowflakeURL = connMod.parseSnowflakeURL as typeof import("../connection").parseSnowflakeURL;
|
|
69
|
-
const detectDBType = connMod.detectDBType as typeof import("../connection").detectDBType;
|
|
70
|
-
|
|
71
|
-
describe("parseSnowflakeURL", () => {
|
|
72
|
-
it("parses full Snowflake URL with all components", () => {
|
|
73
|
-
const opts = parseSnowflakeURL(
|
|
74
|
-
"snowflake://myuser:mypass@xy12345.us-east-1/mydb/myschema?warehouse=COMPUTE_WH&role=ANALYST"
|
|
75
|
-
);
|
|
76
|
-
expect(opts.account).toBe("xy12345.us-east-1");
|
|
77
|
-
expect(opts.username).toBe("myuser");
|
|
78
|
-
expect(opts.password).toBe("mypass");
|
|
79
|
-
expect(opts.database).toBe("mydb");
|
|
80
|
-
expect(opts.schema).toBe("myschema");
|
|
81
|
-
expect(opts.warehouse).toBe("COMPUTE_WH");
|
|
82
|
-
expect(opts.role).toBe("ANALYST");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("parses URL with database only (no schema)", () => {
|
|
86
|
-
const opts = parseSnowflakeURL("snowflake://user:pass@account123/mydb");
|
|
87
|
-
expect(opts.account).toBe("account123");
|
|
88
|
-
expect(opts.username).toBe("user");
|
|
89
|
-
expect(opts.password).toBe("pass");
|
|
90
|
-
expect(opts.database).toBe("mydb");
|
|
91
|
-
expect(opts.schema).toBeUndefined();
|
|
92
|
-
expect(opts.warehouse).toBeUndefined();
|
|
93
|
-
expect(opts.role).toBeUndefined();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("parses minimal URL (account only)", () => {
|
|
97
|
-
const opts = parseSnowflakeURL("snowflake://user:pass@account123");
|
|
98
|
-
expect(opts.account).toBe("account123");
|
|
99
|
-
expect(opts.username).toBe("user");
|
|
100
|
-
expect(opts.password).toBe("pass");
|
|
101
|
-
expect(opts.database).toBeUndefined();
|
|
102
|
-
expect(opts.schema).toBeUndefined();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("decodes URL-encoded credentials", () => {
|
|
106
|
-
const opts = parseSnowflakeURL("snowflake://my%40user:p%40ss%23word@account123/db");
|
|
107
|
-
expect(opts.username).toBe("my@user");
|
|
108
|
-
expect(opts.password).toBe("p@ss#word");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("handles warehouse query parameter only", () => {
|
|
112
|
-
const opts = parseSnowflakeURL("snowflake://user:pass@account123/db/schema?warehouse=WH");
|
|
113
|
-
expect(opts.warehouse).toBe("WH");
|
|
114
|
-
expect(opts.role).toBeUndefined();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("throws for non-snowflake protocol", () => {
|
|
118
|
-
expect(() => parseSnowflakeURL("postgresql://user:pass@localhost:5432/db")).toThrow(
|
|
119
|
-
"Invalid Snowflake URL"
|
|
120
|
-
);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("accepts empty password (key-pair auth scenario)", () => {
|
|
124
|
-
const opts = parseSnowflakeURL("snowflake://user:@account123/db");
|
|
125
|
-
expect(opts.username).toBe("user");
|
|
126
|
-
expect(opts.password).toBe("");
|
|
127
|
-
expect(opts.database).toBe("db");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("throws for missing username", () => {
|
|
131
|
-
expect(() => parseSnowflakeURL("snowflake://:pass@account123/db")).toThrow(
|
|
132
|
-
"missing username"
|
|
133
|
-
);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("throws for missing account (unparseable URL)", () => {
|
|
137
|
-
// snowflake://user:pass@/db is not a valid URL — the URL constructor throws
|
|
138
|
-
expect(() => parseSnowflakeURL("snowflake://user:pass@/db")).toThrow();
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe("detectDBType — Snowflake", () => {
|
|
143
|
-
it("returns 'snowflake' for snowflake:// URLs", () => {
|
|
144
|
-
expect(detectDBType("snowflake://user:pass@account123/db")).toBe("snowflake");
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("still returns 'postgres' for postgresql:// URLs", () => {
|
|
148
|
-
expect(detectDBType("postgresql://user:pass@localhost:5432/db")).toBe("postgres");
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("still returns 'mysql' for mysql:// URLs", () => {
|
|
152
|
-
expect(detectDBType("mysql://user:pass@localhost:3306/db")).toBe("mysql");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("throws for unsupported URL schemes", () => {
|
|
156
|
-
expect(() => detectDBType("sqlite:///test.db")).toThrow("Unsupported database URL");
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
describe("createSnowflakeDB — defense-in-depth", () => {
|
|
161
|
-
const ConnectionRegistry = connMod.ConnectionRegistry as typeof import("../connection").ConnectionRegistry;
|
|
162
|
-
|
|
163
|
-
beforeEach(() => {
|
|
164
|
-
executedStatements = [];
|
|
165
|
-
queryTagShouldFail = false;
|
|
166
|
-
mockWarn.mockClear();
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("logs a startup warning recommending a SELECT-only role", () => {
|
|
170
|
-
const registry = new ConnectionRegistry();
|
|
171
|
-
registry.register("sf-test", { url: "snowflake://user:pass@account123/db" });
|
|
172
|
-
expect(mockWarn).toHaveBeenCalledWith(
|
|
173
|
-
expect.stringContaining("no session-level read-only mode"),
|
|
174
|
-
);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("sets QUERY_TAG before each query", async () => {
|
|
178
|
-
const registry = new ConnectionRegistry();
|
|
179
|
-
registry.register("sf-tag", { url: "snowflake://user:pass@account123/db" });
|
|
180
|
-
const db = registry.get("sf-tag");
|
|
181
|
-
await db.query("SELECT 1");
|
|
182
|
-
|
|
183
|
-
expect(executedStatements).toContain("ALTER SESSION SET QUERY_TAG = 'atlas:readonly'");
|
|
184
|
-
// QUERY_TAG should come after timeout and before the actual query
|
|
185
|
-
const tagIdx = executedStatements.indexOf("ALTER SESSION SET QUERY_TAG = 'atlas:readonly'");
|
|
186
|
-
const queryIdx = executedStatements.indexOf("SELECT 1");
|
|
187
|
-
expect(tagIdx).toBeLessThan(queryIdx);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("sets statement timeout before QUERY_TAG", async () => {
|
|
191
|
-
const registry = new ConnectionRegistry();
|
|
192
|
-
registry.register("sf-order", { url: "snowflake://user:pass@account123/db" });
|
|
193
|
-
const db = registry.get("sf-order");
|
|
194
|
-
await db.query("SELECT 1", 15000);
|
|
195
|
-
|
|
196
|
-
const timeoutIdx = executedStatements.findIndex((s) => s.includes("STATEMENT_TIMEOUT_IN_SECONDS"));
|
|
197
|
-
const tagIdx = executedStatements.indexOf("ALTER SESSION SET QUERY_TAG = 'atlas:readonly'");
|
|
198
|
-
expect(timeoutIdx).toBeLessThan(tagIdx);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("proceeds with the query when QUERY_TAG fails (best-effort)", async () => {
|
|
202
|
-
queryTagShouldFail = true;
|
|
203
|
-
const registry = new ConnectionRegistry();
|
|
204
|
-
registry.register("sf-tag-fail", { url: "snowflake://user:pass@account123/db" });
|
|
205
|
-
const db = registry.get("sf-tag-fail");
|
|
206
|
-
const result = await db.query("SELECT 42");
|
|
207
|
-
|
|
208
|
-
// The actual query should still execute despite QUERY_TAG failure
|
|
209
|
-
expect(executedStatements).toContain("SELECT 42");
|
|
210
|
-
expect(result).toEqual({ columns: [], rows: [] });
|
|
211
|
-
|
|
212
|
-
// A warning should be logged
|
|
213
|
-
expect(mockWarn).toHaveBeenCalledWith(
|
|
214
|
-
expect.stringContaining("Failed to set QUERY_TAG"),
|
|
215
|
-
);
|
|
216
|
-
});
|
|
217
|
-
});
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DuckDB adapter implementing the DBConnection interface.
|
|
3
|
-
*
|
|
4
|
-
* Uses @duckdb/node-api (the "Neo" API) for in-process analytical queries
|
|
5
|
-
* on CSV, Parquet, and persistent DuckDB database files. DuckDB runs fully
|
|
6
|
-
* in-process — no external server required.
|
|
7
|
-
*
|
|
8
|
-
* Connection URL formats:
|
|
9
|
-
* - `duckdb://path/to/file.duckdb` — persistent database file
|
|
10
|
-
* - `duckdb://:memory:` — in-memory database
|
|
11
|
-
* - `duckdb://` — in-memory (shorthand)
|
|
12
|
-
*
|
|
13
|
-
* Read-only enforcement: the database is opened with `access_mode: 'READ_ONLY'`
|
|
14
|
-
* when the file already exists. For newly-created databases (e.g. during CLI
|
|
15
|
-
* ingestion), the caller is responsible for opening in read-write mode first,
|
|
16
|
-
* then re-opening as read-only for runtime use.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import type { DBConnection, QueryResult } from "./connection";
|
|
20
|
-
import { createLogger } from "@atlas/api/lib/logger";
|
|
21
|
-
|
|
22
|
-
const log = createLogger("duckdb");
|
|
23
|
-
|
|
24
|
-
export interface DuckDBConfig {
|
|
25
|
-
/** Path to the .duckdb file, or ":memory:" for in-memory. */
|
|
26
|
-
path: string;
|
|
27
|
-
/** Open in read-only mode (default: true for runtime). */
|
|
28
|
-
readOnly?: boolean;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Parse a duckdb:// URL into a DuckDBConfig.
|
|
33
|
-
*
|
|
34
|
-
* - `duckdb://` or `duckdb://:memory:` → in-memory
|
|
35
|
-
* - `duckdb:///absolute/path.duckdb` → absolute path
|
|
36
|
-
* - `duckdb://relative/path.duckdb` → relative path
|
|
37
|
-
*/
|
|
38
|
-
export function parseDuckDBUrl(url: string): DuckDBConfig {
|
|
39
|
-
if (!url.startsWith("duckdb://")) {
|
|
40
|
-
throw new Error(`Invalid DuckDB URL: expected duckdb:// scheme, got "${url.slice(0, 20)}..."`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const rest = url.slice("duckdb://".length);
|
|
44
|
-
|
|
45
|
-
if (!rest || rest === ":memory:") {
|
|
46
|
-
return { path: ":memory:" };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// duckdb:///absolute/path → /absolute/path
|
|
50
|
-
// duckdb://relative/path → relative/path
|
|
51
|
-
return { path: rest };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function createDuckDBConnection(config: DuckDBConfig): DBConnection {
|
|
55
|
-
// Lazy-load to avoid requiring @duckdb/node-api when not needed
|
|
56
|
-
let instancePromise: Promise<{ instance: unknown; connection: unknown }> | null = null;
|
|
57
|
-
|
|
58
|
-
async function getConnection() {
|
|
59
|
-
if (!instancePromise) {
|
|
60
|
-
instancePromise = (async () => {
|
|
61
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
62
|
-
const { DuckDBInstance } = require("@duckdb/node-api");
|
|
63
|
-
|
|
64
|
-
const options: Record<string, string> = {};
|
|
65
|
-
if (config.readOnly !== false && config.path !== ":memory:") {
|
|
66
|
-
options.access_mode = "READ_ONLY";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const instance = await DuckDBInstance.create(config.path, options);
|
|
70
|
-
const connection = await instance.connect();
|
|
71
|
-
return { instance, connection };
|
|
72
|
-
})();
|
|
73
|
-
// Allow retry on transient failures — don't cache rejected promises
|
|
74
|
-
instancePromise.catch(() => { instancePromise = null; });
|
|
75
|
-
}
|
|
76
|
-
return instancePromise;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
async query(sql: string, timeoutMs?: number): Promise<QueryResult> {
|
|
81
|
-
const { connection } = await getConnection();
|
|
82
|
-
|
|
83
|
-
const runQuery = async () => {
|
|
84
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
85
|
-
const reader = await (connection as any).runAndReadAll(sql);
|
|
86
|
-
const columns: string[] = reader.columnNames();
|
|
87
|
-
const rowObjects: Record<string, unknown>[] = reader.getRowObjects();
|
|
88
|
-
return { columns, rows: rowObjects };
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
if (timeoutMs && timeoutMs > 0) {
|
|
92
|
-
const timeout = new Promise<never>((_, reject) =>
|
|
93
|
-
setTimeout(() => reject(new Error(
|
|
94
|
-
`DuckDB query timed out after ${timeoutMs}ms`
|
|
95
|
-
)), timeoutMs)
|
|
96
|
-
);
|
|
97
|
-
return Promise.race([runQuery(), timeout]);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return runQuery();
|
|
101
|
-
},
|
|
102
|
-
|
|
103
|
-
async close(): Promise<void> {
|
|
104
|
-
if (instancePromise) {
|
|
105
|
-
try {
|
|
106
|
-
const { connection, instance } = await instancePromise;
|
|
107
|
-
// DuckDB Neo API uses synchronous cleanup methods
|
|
108
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
|
-
(connection as any).disconnectSync();
|
|
110
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
111
|
-
(instance as any).closeSync();
|
|
112
|
-
} catch (err) {
|
|
113
|
-
log.warn(
|
|
114
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
115
|
-
"Failed to close DuckDB connection"
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
instancePromise = null;
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
}
|
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Salesforce DataSource adapter via jsforce.
|
|
3
|
-
*
|
|
4
|
-
* Salesforce uses SOQL, not SQL, so it cannot be a DBConnection (which has
|
|
5
|
-
* `query(sql)`). Instead, it exposes a separate DataSource interface and
|
|
6
|
-
* a separate registry.
|
|
7
|
-
*
|
|
8
|
-
* Connection URL format: salesforce://user:pass@login.salesforce.com?token=TOKEN
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { QueryResult, ConnectionMetadata } from "@atlas/api/lib/db/connection";
|
|
12
|
-
import { createLogger } from "@atlas/api/lib/logger";
|
|
13
|
-
|
|
14
|
-
const log = createLogger("salesforce");
|
|
15
|
-
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Types
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
export interface SObjectInfo {
|
|
21
|
-
name: string;
|
|
22
|
-
label: string;
|
|
23
|
-
queryable: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface SObjectField {
|
|
27
|
-
name: string;
|
|
28
|
-
type: string;
|
|
29
|
-
label: string;
|
|
30
|
-
picklistValues: { value: string; label: string; active: boolean }[];
|
|
31
|
-
referenceTo: string[];
|
|
32
|
-
nillable: boolean;
|
|
33
|
-
length: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface SObjectDescribe {
|
|
37
|
-
name: string;
|
|
38
|
-
label: string;
|
|
39
|
-
fields: SObjectField[];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface SalesforceConfig {
|
|
43
|
-
loginUrl: string;
|
|
44
|
-
username: string;
|
|
45
|
-
password: string;
|
|
46
|
-
securityToken?: string;
|
|
47
|
-
clientId?: string;
|
|
48
|
-
clientSecret?: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface SalesforceDataSource {
|
|
52
|
-
query(soql: string, timeoutMs?: number): Promise<QueryResult>;
|
|
53
|
-
describe(objectName: string): Promise<SObjectDescribe>;
|
|
54
|
-
listObjects(): Promise<SObjectInfo[]>;
|
|
55
|
-
close(): Promise<void>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
// URL parser
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Parse a Salesforce connection URL into SalesforceConfig.
|
|
64
|
-
*
|
|
65
|
-
* Format: salesforce://user:pass@login.salesforce.com?token=TOKEN
|
|
66
|
-
*
|
|
67
|
-
* - hostname → loginUrl (default `login.salesforce.com`)
|
|
68
|
-
* - URL username/password → credentials
|
|
69
|
-
* - Query params: `token` (security token), `clientId`, `clientSecret`
|
|
70
|
-
*/
|
|
71
|
-
export function parseSalesforceURL(url: string): SalesforceConfig {
|
|
72
|
-
const parsed = new URL(url);
|
|
73
|
-
if (parsed.protocol !== "salesforce:") {
|
|
74
|
-
throw new Error(
|
|
75
|
-
`Invalid Salesforce URL: expected salesforce:// scheme, got "${parsed.protocol}"`,
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const username = decodeURIComponent(parsed.username);
|
|
80
|
-
if (!username) {
|
|
81
|
-
throw new Error("Invalid Salesforce URL: missing username.");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const password = decodeURIComponent(parsed.password);
|
|
85
|
-
if (!password) {
|
|
86
|
-
throw new Error("Invalid Salesforce URL: missing password.");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const hostname = parsed.hostname || "login.salesforce.com";
|
|
90
|
-
const loginUrl = `https://${hostname}`;
|
|
91
|
-
|
|
92
|
-
const securityToken = parsed.searchParams.get("token") ?? undefined;
|
|
93
|
-
const clientId = parsed.searchParams.get("clientId") ?? undefined;
|
|
94
|
-
const clientSecret = parsed.searchParams.get("clientSecret") ?? undefined;
|
|
95
|
-
|
|
96
|
-
return { loginUrl, username, password, securityToken, clientId, clientSecret };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
// DataSource factory
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Create a Salesforce DataSource backed by jsforce.
|
|
105
|
-
*/
|
|
106
|
-
export function createSalesforceDataSource(
|
|
107
|
-
config: SalesforceConfig,
|
|
108
|
-
): SalesforceDataSource {
|
|
109
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
110
|
-
let jsforce: any;
|
|
111
|
-
try {
|
|
112
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
113
|
-
jsforce = require("jsforce");
|
|
114
|
-
} catch {
|
|
115
|
-
throw new Error(
|
|
116
|
-
"Salesforce support requires the jsforce package. Install it with: bun add jsforce",
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const connOpts: Record<string, unknown> = {
|
|
121
|
-
loginUrl: config.loginUrl,
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
if (config.clientId && config.clientSecret) {
|
|
125
|
-
connOpts.oauth2 = {
|
|
126
|
-
clientId: config.clientId,
|
|
127
|
-
clientSecret: config.clientSecret,
|
|
128
|
-
loginUrl: config.loginUrl,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
133
|
-
const conn = new jsforce.Connection(connOpts) as any;
|
|
134
|
-
|
|
135
|
-
let loginPromise: Promise<void> | null = null;
|
|
136
|
-
|
|
137
|
-
async function ensureLoggedIn(): Promise<void> {
|
|
138
|
-
if (loginPromise) return loginPromise;
|
|
139
|
-
loginPromise = (async () => {
|
|
140
|
-
const loginPassword = config.securityToken
|
|
141
|
-
? config.password + config.securityToken
|
|
142
|
-
: config.password;
|
|
143
|
-
try {
|
|
144
|
-
await conn.login(config.username, loginPassword);
|
|
145
|
-
} catch (err) {
|
|
146
|
-
loginPromise = null;
|
|
147
|
-
log.error(
|
|
148
|
-
{
|
|
149
|
-
err: err instanceof Error ? err : new Error(String(err)),
|
|
150
|
-
loginUrl: config.loginUrl,
|
|
151
|
-
username: config.username,
|
|
152
|
-
},
|
|
153
|
-
"Salesforce login failed",
|
|
154
|
-
);
|
|
155
|
-
throw err;
|
|
156
|
-
}
|
|
157
|
-
log.info("Salesforce login successful");
|
|
158
|
-
})();
|
|
159
|
-
return loginPromise;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function isSessionExpiredError(err: unknown): boolean {
|
|
163
|
-
if (!(err instanceof Error)) return false;
|
|
164
|
-
const msg = err.message;
|
|
165
|
-
return (
|
|
166
|
-
msg.includes("INVALID_SESSION_ID") ||
|
|
167
|
-
msg.includes("Session expired") ||
|
|
168
|
-
msg.includes("session has expired")
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async function withSessionRetry<T>(fn: () => Promise<T>): Promise<T> {
|
|
173
|
-
try {
|
|
174
|
-
return await fn();
|
|
175
|
-
} catch (err) {
|
|
176
|
-
if (isSessionExpiredError(err)) {
|
|
177
|
-
log.warn("Salesforce session expired — re-authenticating and retrying");
|
|
178
|
-
loginPromise = null;
|
|
179
|
-
await ensureLoggedIn();
|
|
180
|
-
return await fn();
|
|
181
|
-
}
|
|
182
|
-
throw err;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return {
|
|
187
|
-
async query(soql: string, timeoutMs = 30000): Promise<QueryResult> {
|
|
188
|
-
return withSessionRetry(async () => {
|
|
189
|
-
await ensureLoggedIn();
|
|
190
|
-
|
|
191
|
-
let timeoutId: ReturnType<typeof setTimeout>;
|
|
192
|
-
const result = await Promise.race([
|
|
193
|
-
conn.query(soql),
|
|
194
|
-
new Promise<never>((_, reject) => {
|
|
195
|
-
timeoutId = setTimeout(
|
|
196
|
-
() => reject(new Error("Salesforce query timed out")),
|
|
197
|
-
timeoutMs,
|
|
198
|
-
);
|
|
199
|
-
}),
|
|
200
|
-
]).finally(() => clearTimeout(timeoutId!));
|
|
201
|
-
|
|
202
|
-
const records = (result.records ?? []) as Record<string, unknown>[];
|
|
203
|
-
|
|
204
|
-
if (records.length === 0) {
|
|
205
|
-
return { columns: [], rows: [] };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Extract columns from first record, filtering out the `attributes` metadata key
|
|
209
|
-
const columns = Object.keys(records[0]).filter(
|
|
210
|
-
(k) => k !== "attributes",
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
const rows = records.map((record) => {
|
|
214
|
-
const row: Record<string, unknown> = {};
|
|
215
|
-
for (const col of columns) {
|
|
216
|
-
row[col] = record[col];
|
|
217
|
-
}
|
|
218
|
-
return row;
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
return { columns, rows };
|
|
222
|
-
});
|
|
223
|
-
},
|
|
224
|
-
|
|
225
|
-
async describe(objectName: string): Promise<SObjectDescribe> {
|
|
226
|
-
return withSessionRetry(async () => {
|
|
227
|
-
await ensureLoggedIn();
|
|
228
|
-
const desc = await conn.describe(objectName);
|
|
229
|
-
return {
|
|
230
|
-
name: desc.name,
|
|
231
|
-
label: desc.label,
|
|
232
|
-
fields: (desc.fields ?? []).map(
|
|
233
|
-
(f: Record<string, unknown>) => ({
|
|
234
|
-
name: f.name as string,
|
|
235
|
-
type: f.type as string,
|
|
236
|
-
label: f.label as string,
|
|
237
|
-
picklistValues: Array.isArray(f.picklistValues)
|
|
238
|
-
? f.picklistValues.map(
|
|
239
|
-
(pv: Record<string, unknown>) => ({
|
|
240
|
-
value: pv.value as string,
|
|
241
|
-
label: pv.label as string,
|
|
242
|
-
active: pv.active as boolean,
|
|
243
|
-
}),
|
|
244
|
-
)
|
|
245
|
-
: [],
|
|
246
|
-
referenceTo: Array.isArray(f.referenceTo)
|
|
247
|
-
? (f.referenceTo as string[])
|
|
248
|
-
: [],
|
|
249
|
-
nillable: f.nillable as boolean,
|
|
250
|
-
length: (f.length as number) ?? 0,
|
|
251
|
-
}),
|
|
252
|
-
),
|
|
253
|
-
};
|
|
254
|
-
});
|
|
255
|
-
},
|
|
256
|
-
|
|
257
|
-
async listObjects(): Promise<SObjectInfo[]> {
|
|
258
|
-
return withSessionRetry(async () => {
|
|
259
|
-
await ensureLoggedIn();
|
|
260
|
-
const result = await conn.describeGlobal();
|
|
261
|
-
return (result.sobjects ?? [])
|
|
262
|
-
.filter((obj: Record<string, unknown>) => obj.queryable === true)
|
|
263
|
-
.map((obj: Record<string, unknown>) => ({
|
|
264
|
-
name: obj.name as string,
|
|
265
|
-
label: obj.label as string,
|
|
266
|
-
queryable: true,
|
|
267
|
-
}));
|
|
268
|
-
});
|
|
269
|
-
},
|
|
270
|
-
|
|
271
|
-
async close(): Promise<void> {
|
|
272
|
-
if (loginPromise) {
|
|
273
|
-
try {
|
|
274
|
-
await loginPromise;
|
|
275
|
-
await conn.logout();
|
|
276
|
-
} catch (err) {
|
|
277
|
-
log.warn(
|
|
278
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
279
|
-
"Failed to logout from Salesforce",
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
loginPromise = null;
|
|
283
|
-
}
|
|
284
|
-
},
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// ---------------------------------------------------------------------------
|
|
289
|
-
// Source registry (parallel to ConnectionRegistry)
|
|
290
|
-
// ---------------------------------------------------------------------------
|
|
291
|
-
|
|
292
|
-
const _sources = new Map<string, SalesforceDataSource>();
|
|
293
|
-
|
|
294
|
-
export function registerSalesforceSource(
|
|
295
|
-
id: string,
|
|
296
|
-
config: SalesforceConfig,
|
|
297
|
-
): void {
|
|
298
|
-
const existing = _sources.get(id);
|
|
299
|
-
const newSource = createSalesforceDataSource(config);
|
|
300
|
-
_sources.set(id, newSource);
|
|
301
|
-
if (existing) {
|
|
302
|
-
existing.close().catch((err) => {
|
|
303
|
-
log.warn(
|
|
304
|
-
{ err: err instanceof Error ? err.message : String(err), sourceId: id },
|
|
305
|
-
"Failed to close previous Salesforce source during re-registration",
|
|
306
|
-
);
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
export function getSalesforceSource(id: string): SalesforceDataSource {
|
|
312
|
-
const source = _sources.get(id);
|
|
313
|
-
if (!source) {
|
|
314
|
-
throw new Error(`Salesforce source "${id}" is not registered.`);
|
|
315
|
-
}
|
|
316
|
-
return source;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
export function listSalesforceSources(): string[] {
|
|
320
|
-
return Array.from(_sources.keys());
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/** Return ConnectionMetadata for each registered Salesforce source. */
|
|
324
|
-
export function describeSalesforceSources(): ConnectionMetadata[] {
|
|
325
|
-
return Array.from(_sources.keys()).map((id) => ({
|
|
326
|
-
id,
|
|
327
|
-
dbType: "salesforce" as const,
|
|
328
|
-
}));
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/** Test helper — clears all registered sources. */
|
|
332
|
-
export function _resetSalesforceSources(): void {
|
|
333
|
-
for (const [id, source] of _sources.entries()) {
|
|
334
|
-
source.close().catch((err) => {
|
|
335
|
-
log.warn(
|
|
336
|
-
{ err: err instanceof Error ? err.message : String(err), sourceId: id },
|
|
337
|
-
"Failed to close Salesforce source during registry reset",
|
|
338
|
-
);
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
_sources.clear();
|
|
342
|
-
}
|