@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,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the querySalesforce agent tool.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect, beforeEach, mock } from "bun:test";
|
|
5
|
-
|
|
6
|
-
// Mock Salesforce registry
|
|
7
|
-
const mockQuery = mock(() =>
|
|
8
|
-
Promise.resolve({
|
|
9
|
-
columns: ["Id", "Name"],
|
|
10
|
-
rows: [
|
|
11
|
-
{ Id: "001", Name: "Acme" },
|
|
12
|
-
{ Id: "002", Name: "Widget Co" },
|
|
13
|
-
],
|
|
14
|
-
}),
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
const mockSources = new Map<string, { query: typeof mockQuery; close: () => Promise<void> }>();
|
|
18
|
-
|
|
19
|
-
mock.module("@atlas/api/lib/db/salesforce", () => ({
|
|
20
|
-
getSalesforceSource: (id: string) => {
|
|
21
|
-
const source = mockSources.get(id);
|
|
22
|
-
if (!source) throw new Error(`Salesforce source "${id}" is not registered.`);
|
|
23
|
-
return source;
|
|
24
|
-
},
|
|
25
|
-
listSalesforceSources: () => Array.from(mockSources.keys()),
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
// Mock semantic layer
|
|
29
|
-
mock.module("@atlas/api/lib/semantic", () => ({
|
|
30
|
-
getWhitelistedTables: () => new Set(["account", "contact", "opportunity"]),
|
|
31
|
-
_resetWhitelists: () => {},
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
// Mock audit log (no-op)
|
|
35
|
-
mock.module("@atlas/api/lib/auth/audit", () => ({
|
|
36
|
-
logQueryAudit: () => {},
|
|
37
|
-
}));
|
|
38
|
-
|
|
39
|
-
const { querySalesforce } = await import("@atlas/api/lib/tools/salesforce");
|
|
40
|
-
|
|
41
|
-
// Helper to call the tool's execute function
|
|
42
|
-
async function executeTool(params: {
|
|
43
|
-
soql: string;
|
|
44
|
-
explanation: string;
|
|
45
|
-
connectionId?: string;
|
|
46
|
-
}) {
|
|
47
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
48
|
-
return (querySalesforce as any).execute(params);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe("querySalesforce tool", () => {
|
|
52
|
-
beforeEach(() => {
|
|
53
|
-
mockQuery.mockClear();
|
|
54
|
-
mockQuery.mockImplementation(() =>
|
|
55
|
-
Promise.resolve({
|
|
56
|
-
columns: ["Id", "Name"],
|
|
57
|
-
rows: [
|
|
58
|
-
{ Id: "001", Name: "Acme" },
|
|
59
|
-
{ Id: "002", Name: "Widget Co" },
|
|
60
|
-
],
|
|
61
|
-
}),
|
|
62
|
-
);
|
|
63
|
-
mockSources.clear();
|
|
64
|
-
mockSources.set("default", {
|
|
65
|
-
query: mockQuery,
|
|
66
|
-
close: async () => {},
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("executes a valid query and returns results", async () => {
|
|
71
|
-
const result = await executeTool({
|
|
72
|
-
soql: "SELECT Id, Name FROM Account",
|
|
73
|
-
explanation: "Get all accounts",
|
|
74
|
-
});
|
|
75
|
-
expect(result.success).toBe(true);
|
|
76
|
-
expect(result.columns).toEqual(["Id", "Name"]);
|
|
77
|
-
expect(result.rows).toHaveLength(2);
|
|
78
|
-
expect(result.row_count).toBe(2);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("rejects invalid SOQL (mutation)", async () => {
|
|
82
|
-
const result = await executeTool({
|
|
83
|
-
soql: "DELETE FROM Account",
|
|
84
|
-
explanation: "Delete accounts",
|
|
85
|
-
});
|
|
86
|
-
expect(result.success).toBe(false);
|
|
87
|
-
expect(result.error).toContain("Forbidden");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("rejects queries against non-whitelisted objects", async () => {
|
|
91
|
-
const result = await executeTool({
|
|
92
|
-
soql: "SELECT Id FROM CustomObject__c",
|
|
93
|
-
explanation: "Get custom objects",
|
|
94
|
-
});
|
|
95
|
-
expect(result.success).toBe(false);
|
|
96
|
-
expect(result.error).toContain("not in the allowed list");
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("returns error for unregistered connection", async () => {
|
|
100
|
-
const result = await executeTool({
|
|
101
|
-
soql: "SELECT Id FROM Account",
|
|
102
|
-
explanation: "test",
|
|
103
|
-
connectionId: "nonexistent",
|
|
104
|
-
});
|
|
105
|
-
expect(result.success).toBe(false);
|
|
106
|
-
expect(result.error).toContain("not registered");
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("appends LIMIT when not present", async () => {
|
|
110
|
-
await executeTool({
|
|
111
|
-
soql: "SELECT Id FROM Account",
|
|
112
|
-
explanation: "test",
|
|
113
|
-
});
|
|
114
|
-
// The mockQuery should have been called with a SOQL that includes LIMIT
|
|
115
|
-
const calledWith = (mockQuery.mock.calls as unknown as string[][])[0][0];
|
|
116
|
-
expect(calledWith).toContain("LIMIT");
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("does not double-append LIMIT", async () => {
|
|
120
|
-
await executeTool({
|
|
121
|
-
soql: "SELECT Id FROM Account LIMIT 5",
|
|
122
|
-
explanation: "test",
|
|
123
|
-
});
|
|
124
|
-
const calledWith = (mockQuery.mock.calls as unknown as string[][])[0][0];
|
|
125
|
-
// Should not have two LIMIT clauses
|
|
126
|
-
const limitCount = (calledWith.match(/LIMIT/gi) ?? []).length;
|
|
127
|
-
expect(limitCount).toBe(1);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("scrubs sensitive error messages", async () => {
|
|
131
|
-
mockQuery.mockImplementationOnce(() =>
|
|
132
|
-
Promise.reject(new Error("INVALID_SESSION_ID: Session expired")),
|
|
133
|
-
);
|
|
134
|
-
const result = await executeTool({
|
|
135
|
-
soql: "SELECT Id FROM Account",
|
|
136
|
-
explanation: "test",
|
|
137
|
-
});
|
|
138
|
-
expect(result.success).toBe(false);
|
|
139
|
-
expect(result.error).toContain("check server logs");
|
|
140
|
-
expect(result.error).not.toContain("INVALID_SESSION_ID");
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("surfaces non-sensitive errors to the agent", async () => {
|
|
144
|
-
mockQuery.mockImplementationOnce(() =>
|
|
145
|
-
Promise.reject(new Error("INVALID_FIELD: No such column 'Foo'")),
|
|
146
|
-
);
|
|
147
|
-
const result = await executeTool({
|
|
148
|
-
soql: "SELECT Foo FROM Account",
|
|
149
|
-
explanation: "test",
|
|
150
|
-
});
|
|
151
|
-
expect(result.success).toBe(false);
|
|
152
|
-
expect(result.error).toContain("INVALID_FIELD");
|
|
153
|
-
});
|
|
154
|
-
});
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { validateSOQL, appendSOQLLimit } from "../soql-validation";
|
|
3
|
-
|
|
4
|
-
const ALLOWED = new Set(["Account", "Contact", "Opportunity", "Lead"]);
|
|
5
|
-
|
|
6
|
-
describe("validateSOQL", () => {
|
|
7
|
-
describe("Layer 0: Empty check", () => {
|
|
8
|
-
it("rejects empty string", () => {
|
|
9
|
-
const result = validateSOQL("", ALLOWED);
|
|
10
|
-
expect(result.valid).toBe(false);
|
|
11
|
-
expect(result.error).toContain("Empty");
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("rejects whitespace-only", () => {
|
|
15
|
-
const result = validateSOQL(" \n\t ", ALLOWED);
|
|
16
|
-
expect(result.valid).toBe(false);
|
|
17
|
-
expect(result.error).toContain("Empty");
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
describe("Layer 1: Mutation guard", () => {
|
|
22
|
-
for (const keyword of ["INSERT", "UPDATE", "DELETE", "UPSERT", "MERGE", "UNDELETE"]) {
|
|
23
|
-
it(`rejects ${keyword}`, () => {
|
|
24
|
-
const result = validateSOQL(`${keyword} INTO Account`, ALLOWED);
|
|
25
|
-
expect(result.valid).toBe(false);
|
|
26
|
-
expect(result.error).toContain("Forbidden");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it(`rejects ${keyword.toLowerCase()}`, () => {
|
|
30
|
-
const result = validateSOQL(`${keyword.toLowerCase()} into account`, ALLOWED);
|
|
31
|
-
expect(result.valid).toBe(false);
|
|
32
|
-
expect(result.error).toContain("Forbidden");
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe("Layer 2: SELECT-only", () => {
|
|
38
|
-
it("accepts SELECT query", () => {
|
|
39
|
-
const result = validateSOQL("SELECT Id FROM Account", ALLOWED);
|
|
40
|
-
expect(result.valid).toBe(true);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("rejects non-SELECT query", () => {
|
|
44
|
-
const result = validateSOQL("DESCRIBE Account", ALLOWED);
|
|
45
|
-
expect(result.valid).toBe(false);
|
|
46
|
-
expect(result.error).toContain("Only SELECT");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("rejects semicolons", () => {
|
|
50
|
-
const result = validateSOQL("SELECT Id FROM Account;", ALLOWED);
|
|
51
|
-
expect(result.valid).toBe(false);
|
|
52
|
-
expect(result.error).toContain("Semicolons");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("rejects multiple statements", () => {
|
|
56
|
-
const result = validateSOQL("SELECT Id FROM Account; SELECT Id FROM Contact", ALLOWED);
|
|
57
|
-
expect(result.valid).toBe(false);
|
|
58
|
-
expect(result.error).toContain("Semicolons");
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe("Layer 3: Object whitelist", () => {
|
|
63
|
-
it("allows whitelisted objects", () => {
|
|
64
|
-
const result = validateSOQL("SELECT Id, Name FROM Account", ALLOWED);
|
|
65
|
-
expect(result.valid).toBe(true);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("rejects non-whitelisted objects", () => {
|
|
69
|
-
const result = validateSOQL("SELECT Id FROM CustomObject__c", ALLOWED);
|
|
70
|
-
expect(result.valid).toBe(false);
|
|
71
|
-
expect(result.error).toContain("not in the allowed list");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("checks subquery objects", () => {
|
|
75
|
-
const result = validateSOQL(
|
|
76
|
-
"SELECT Id FROM Account WHERE Id IN (SELECT AccountId FROM CustomObject__c)",
|
|
77
|
-
ALLOWED,
|
|
78
|
-
);
|
|
79
|
-
expect(result.valid).toBe(false);
|
|
80
|
-
expect(result.error).toContain("CustomObject__c");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("allows subquery with whitelisted objects", () => {
|
|
84
|
-
const result = validateSOQL(
|
|
85
|
-
"SELECT Id FROM Account WHERE Id IN (SELECT AccountId FROM Contact)",
|
|
86
|
-
ALLOWED,
|
|
87
|
-
);
|
|
88
|
-
expect(result.valid).toBe(true);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("is case-insensitive", () => {
|
|
92
|
-
const result = validateSOQL("SELECT Id FROM account", ALLOWED);
|
|
93
|
-
expect(result.valid).toBe(true);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("rejects queries with no FROM clause", () => {
|
|
97
|
-
const result = validateSOQL("SELECT 1", ALLOWED);
|
|
98
|
-
expect(result.valid).toBe(false);
|
|
99
|
-
expect(result.error).toContain("No FROM");
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe("Relationship subquery whitelist bypass (parent-to-child)", () => {
|
|
104
|
-
it("accepts parent-to-child relationship subquery with plural relationship name", () => {
|
|
105
|
-
// "Contacts" is the relationship name (plural), not in the whitelist.
|
|
106
|
-
// Only "Contact" (singular) is whitelisted. This should pass because
|
|
107
|
-
// relationship subqueries in SELECT are not whitelist-checked.
|
|
108
|
-
const result = validateSOQL(
|
|
109
|
-
"SELECT Id, Name, (SELECT LastName FROM Contacts) FROM Account",
|
|
110
|
-
ALLOWED,
|
|
111
|
-
);
|
|
112
|
-
expect(result.valid).toBe(true);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("accepts multiple relationship subqueries in SELECT", () => {
|
|
116
|
-
const result = validateSOQL(
|
|
117
|
-
"SELECT Id, (SELECT LastName FROM Contacts), (SELECT Amount FROM Opportunities) FROM Account",
|
|
118
|
-
ALLOWED,
|
|
119
|
-
);
|
|
120
|
-
expect(result.valid).toBe(true);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("accepts relationship subquery with unknown relationship name", () => {
|
|
124
|
-
// Custom relationship names like "Cases" won't be in the whitelist
|
|
125
|
-
const result = validateSOQL(
|
|
126
|
-
"SELECT Id, (SELECT Subject FROM Cases) FROM Account",
|
|
127
|
-
ALLOWED,
|
|
128
|
-
);
|
|
129
|
-
expect(result.valid).toBe(true);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("still rejects non-whitelisted objects in WHERE semi-join subqueries", () => {
|
|
133
|
-
// Semi-join subqueries in WHERE reference real object names — must be checked
|
|
134
|
-
const result = validateSOQL(
|
|
135
|
-
"SELECT Id FROM Account WHERE Id IN (SELECT AccountId FROM CustomObject__c)",
|
|
136
|
-
ALLOWED,
|
|
137
|
-
);
|
|
138
|
-
expect(result.valid).toBe(false);
|
|
139
|
-
expect(result.error).toContain("CustomObject__c");
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("allows whitelisted objects in WHERE semi-join subqueries", () => {
|
|
143
|
-
const result = validateSOQL(
|
|
144
|
-
"SELECT Id FROM Account WHERE Id IN (SELECT AccountId FROM Contact)",
|
|
145
|
-
ALLOWED,
|
|
146
|
-
);
|
|
147
|
-
expect(result.valid).toBe(true);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("accepts relationship subquery AND valid WHERE subquery together", () => {
|
|
151
|
-
const result = validateSOQL(
|
|
152
|
-
"SELECT Id, (SELECT LastName FROM Contacts) FROM Account WHERE Id IN (SELECT AccountId FROM Opportunity)",
|
|
153
|
-
ALLOWED,
|
|
154
|
-
);
|
|
155
|
-
expect(result.valid).toBe(true);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("rejects relationship subquery with invalid WHERE subquery", () => {
|
|
159
|
-
const result = validateSOQL(
|
|
160
|
-
"SELECT Id, (SELECT LastName FROM Contacts) FROM Account WHERE Id IN (SELECT AccountId FROM Forbidden__c)",
|
|
161
|
-
ALLOWED,
|
|
162
|
-
);
|
|
163
|
-
expect(result.valid).toBe(false);
|
|
164
|
-
expect(result.error).toContain("Forbidden__c");
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("still checks top-level FROM object", () => {
|
|
168
|
-
const result = validateSOQL(
|
|
169
|
-
"SELECT Id, (SELECT LastName FROM Contacts) FROM NotAllowed__c",
|
|
170
|
-
ALLOWED,
|
|
171
|
-
);
|
|
172
|
-
expect(result.valid).toBe(false);
|
|
173
|
-
expect(result.error).toContain("NotAllowed__c");
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
describe("String literal false positives in mutation guard", () => {
|
|
178
|
-
it("allows 'delete' inside a string literal", () => {
|
|
179
|
-
const result = validateSOQL(
|
|
180
|
-
"SELECT Id FROM Account WHERE Name = 'delete this'",
|
|
181
|
-
ALLOWED,
|
|
182
|
-
);
|
|
183
|
-
expect(result.valid).toBe(true);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("allows 'update' inside a string literal", () => {
|
|
187
|
-
const result = validateSOQL(
|
|
188
|
-
"SELECT Id FROM Account WHERE Description = 'please update record'",
|
|
189
|
-
ALLOWED,
|
|
190
|
-
);
|
|
191
|
-
expect(result.valid).toBe(true);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("allows 'insert' inside a string literal", () => {
|
|
195
|
-
const result = validateSOQL(
|
|
196
|
-
"SELECT Id FROM Contact WHERE Name = 'insert coin'",
|
|
197
|
-
ALLOWED,
|
|
198
|
-
);
|
|
199
|
-
expect(result.valid).toBe(true);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("allows 'merge' inside a string literal", () => {
|
|
203
|
-
const result = validateSOQL(
|
|
204
|
-
"SELECT Id FROM Lead WHERE Status = 'merge pending'",
|
|
205
|
-
ALLOWED,
|
|
206
|
-
);
|
|
207
|
-
expect(result.valid).toBe(true);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("allows 'upsert' inside a string literal", () => {
|
|
211
|
-
const result = validateSOQL(
|
|
212
|
-
"SELECT Id FROM Account WHERE Name = 'upsert test'",
|
|
213
|
-
ALLOWED,
|
|
214
|
-
);
|
|
215
|
-
expect(result.valid).toBe(true);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it("allows LIKE pattern with forbidden keyword", () => {
|
|
219
|
-
const result = validateSOQL(
|
|
220
|
-
"SELECT Id FROM Account WHERE Name LIKE '%delete%'",
|
|
221
|
-
ALLOWED,
|
|
222
|
-
);
|
|
223
|
-
expect(result.valid).toBe(true);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("still rejects actual DELETE statements", () => {
|
|
227
|
-
const result = validateSOQL("DELETE FROM Account", ALLOWED);
|
|
228
|
-
expect(result.valid).toBe(false);
|
|
229
|
-
expect(result.error).toContain("Forbidden");
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it("still rejects forbidden keyword outside string literal even with strings present", () => {
|
|
233
|
-
// The keyword DELETE appears outside the string
|
|
234
|
-
const result = validateSOQL(
|
|
235
|
-
"DELETE FROM Account WHERE Name = 'safe string'",
|
|
236
|
-
ALLOWED,
|
|
237
|
-
);
|
|
238
|
-
expect(result.valid).toBe(false);
|
|
239
|
-
expect(result.error).toContain("Forbidden");
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it("handles multiple string literals with forbidden keywords", () => {
|
|
243
|
-
const result = validateSOQL(
|
|
244
|
-
"SELECT Id FROM Account WHERE Name = 'delete' AND Type = 'update this'",
|
|
245
|
-
ALLOWED,
|
|
246
|
-
);
|
|
247
|
-
expect(result.valid).toBe(true);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it("handles empty string literals", () => {
|
|
251
|
-
const result = validateSOQL(
|
|
252
|
-
"SELECT Id FROM Account WHERE Name = ''",
|
|
253
|
-
ALLOWED,
|
|
254
|
-
);
|
|
255
|
-
expect(result.valid).toBe(true);
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
describe("valid queries", () => {
|
|
260
|
-
it("accepts basic query", () => {
|
|
261
|
-
const result = validateSOQL("SELECT Id, Name FROM Account LIMIT 10", ALLOWED);
|
|
262
|
-
expect(result.valid).toBe(true);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it("accepts query with WHERE clause", () => {
|
|
266
|
-
const result = validateSOQL(
|
|
267
|
-
"SELECT Id, Name FROM Account WHERE Name = 'Test'",
|
|
268
|
-
ALLOWED,
|
|
269
|
-
);
|
|
270
|
-
expect(result.valid).toBe(true);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it("accepts query with aggregate functions", () => {
|
|
274
|
-
const result = validateSOQL(
|
|
275
|
-
"SELECT COUNT(Id) FROM Opportunity GROUP BY StageName",
|
|
276
|
-
ALLOWED,
|
|
277
|
-
);
|
|
278
|
-
expect(result.valid).toBe(true);
|
|
279
|
-
});
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
describe("appendSOQLLimit", () => {
|
|
284
|
-
it("appends LIMIT when not present", () => {
|
|
285
|
-
const result = appendSOQLLimit("SELECT Id FROM Account", 100);
|
|
286
|
-
expect(result).toBe("SELECT Id FROM Account LIMIT 100");
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("does not append LIMIT when already present", () => {
|
|
290
|
-
const result = appendSOQLLimit("SELECT Id FROM Account LIMIT 50", 100);
|
|
291
|
-
expect(result).toBe("SELECT Id FROM Account LIMIT 50");
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it("is case-insensitive for existing LIMIT", () => {
|
|
295
|
-
const result = appendSOQLLimit("SELECT Id FROM Account limit 50", 100);
|
|
296
|
-
expect(result).toBe("SELECT Id FROM Account limit 50");
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it("trims whitespace", () => {
|
|
300
|
-
const result = appendSOQLLimit(" SELECT Id FROM Account ", 100);
|
|
301
|
-
expect(result).toBe("SELECT Id FROM Account LIMIT 100");
|
|
302
|
-
});
|
|
303
|
-
});
|
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SQL validation tests specific to DuckDB queries.
|
|
3
|
-
*
|
|
4
|
-
* Verifies that:
|
|
5
|
-
* - Standard SELECT queries pass validation in DuckDB mode
|
|
6
|
-
* - DuckDB-specific forbidden operations (PRAGMA, ATTACH, etc.) are blocked
|
|
7
|
-
* - Common DuckDB query patterns work with the PostgreSQL parser mode
|
|
8
|
-
*/
|
|
9
|
-
import { describe, expect, it, beforeEach, afterEach, mock } from "bun:test";
|
|
10
|
-
|
|
11
|
-
// Mock semantic layer
|
|
12
|
-
mock.module("@atlas/api/lib/semantic", () => ({
|
|
13
|
-
getWhitelistedTables: () =>
|
|
14
|
-
new Set(["sales", "customers", "products", "orders"]),
|
|
15
|
-
_resetWhitelists: () => {},
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
// Mock the DB connection
|
|
19
|
-
const mockDBConnection = {
|
|
20
|
-
query: async () => ({ columns: [], rows: [] }),
|
|
21
|
-
close: async () => {},
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const mockDetectDBType = () => {
|
|
25
|
-
const url = process.env.ATLAS_DATASOURCE_URL ?? "";
|
|
26
|
-
if (url.startsWith("duckdb://")) return "duckdb";
|
|
27
|
-
if (url.startsWith("postgresql://")) return "postgres";
|
|
28
|
-
throw new Error(`Unsupported: ${url}`);
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
mock.module("@atlas/api/lib/db/connection", () => ({
|
|
32
|
-
getDB: () => mockDBConnection,
|
|
33
|
-
connections: {
|
|
34
|
-
get: () => mockDBConnection,
|
|
35
|
-
getDefault: () => mockDBConnection,
|
|
36
|
-
getDBType: () => mockDetectDBType(),
|
|
37
|
-
getValidator: () => undefined,
|
|
38
|
-
list: () => ["default"],
|
|
39
|
-
},
|
|
40
|
-
detectDBType: mockDetectDBType,
|
|
41
|
-
}));
|
|
42
|
-
|
|
43
|
-
const { validateSQL } = await import("@atlas/api/lib/tools/sql");
|
|
44
|
-
|
|
45
|
-
const origEnv = { ...process.env };
|
|
46
|
-
|
|
47
|
-
describe("validateSQL — DuckDB mode", () => {
|
|
48
|
-
beforeEach(() => {
|
|
49
|
-
process.env.ATLAS_DATASOURCE_URL = "duckdb://:memory:";
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
afterEach(() => {
|
|
53
|
-
if (origEnv.ATLAS_DATASOURCE_URL === undefined) {
|
|
54
|
-
delete process.env.ATLAS_DATASOURCE_URL;
|
|
55
|
-
} else {
|
|
56
|
-
process.env.ATLAS_DATASOURCE_URL = origEnv.ATLAS_DATASOURCE_URL;
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// --- Valid queries ---
|
|
61
|
-
|
|
62
|
-
it("allows simple SELECT", () => {
|
|
63
|
-
const result = validateSQL("SELECT * FROM sales");
|
|
64
|
-
expect(result.valid).toBe(true);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("allows SELECT with aggregate functions", () => {
|
|
68
|
-
const result = validateSQL(
|
|
69
|
-
"SELECT COUNT(*), SUM(amount) FROM sales GROUP BY product_id"
|
|
70
|
-
);
|
|
71
|
-
expect(result.valid).toBe(true);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("allows SELECT with CTE", () => {
|
|
75
|
-
const result = validateSQL(
|
|
76
|
-
"WITH totals AS (SELECT customer_id, SUM(amount) AS total FROM sales GROUP BY customer_id) SELECT * FROM totals"
|
|
77
|
-
);
|
|
78
|
-
expect(result.valid).toBe(true);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("allows SELECT with window function", () => {
|
|
82
|
-
const result = validateSQL(
|
|
83
|
-
"SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY amount DESC) AS rn FROM sales"
|
|
84
|
-
);
|
|
85
|
-
expect(result.valid).toBe(true);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("allows SELECT with JOIN", () => {
|
|
89
|
-
const result = validateSQL(
|
|
90
|
-
"SELECT s.*, c.name FROM sales s JOIN customers c ON s.customer_id = c.id"
|
|
91
|
-
);
|
|
92
|
-
expect(result.valid).toBe(true);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("allows CAST expressions", () => {
|
|
96
|
-
const result = validateSQL(
|
|
97
|
-
"SELECT CAST(amount AS VARCHAR) FROM sales"
|
|
98
|
-
);
|
|
99
|
-
expect(result.valid).toBe(true);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("allows COALESCE and CASE", () => {
|
|
103
|
-
const result = validateSQL(
|
|
104
|
-
"SELECT COALESCE(name, 'unknown'), CASE WHEN amount > 100 THEN 'high' ELSE 'low' END FROM sales"
|
|
105
|
-
);
|
|
106
|
-
expect(result.valid).toBe(true);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// --- Blocked operations ---
|
|
110
|
-
|
|
111
|
-
it("blocks PRAGMA", () => {
|
|
112
|
-
const result = validateSQL("PRAGMA database_list");
|
|
113
|
-
expect(result.valid).toBe(false);
|
|
114
|
-
expect(result.error).toContain("Forbidden");
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("blocks ATTACH", () => {
|
|
118
|
-
const result = validateSQL("ATTACH '/tmp/other.duckdb' AS other");
|
|
119
|
-
expect(result.valid).toBe(false);
|
|
120
|
-
expect(result.error).toContain("Forbidden");
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("blocks DETACH", () => {
|
|
124
|
-
const result = validateSQL("DETACH other");
|
|
125
|
-
expect(result.valid).toBe(false);
|
|
126
|
-
expect(result.error).toContain("Forbidden");
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("blocks INSTALL", () => {
|
|
130
|
-
const result = validateSQL("INSTALL httpfs");
|
|
131
|
-
expect(result.valid).toBe(false);
|
|
132
|
-
expect(result.error).toContain("Forbidden");
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("blocks EXPORT", () => {
|
|
136
|
-
const result = validateSQL("EXPORT DATABASE '/tmp/backup'");
|
|
137
|
-
expect(result.valid).toBe(false);
|
|
138
|
-
expect(result.error).toContain("Forbidden");
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("blocks IMPORT", () => {
|
|
142
|
-
const result = validateSQL("IMPORT DATABASE '/tmp/backup'");
|
|
143
|
-
expect(result.valid).toBe(false);
|
|
144
|
-
expect(result.error).toContain("Forbidden");
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("blocks CHECKPOINT", () => {
|
|
148
|
-
const result = validateSQL("CHECKPOINT");
|
|
149
|
-
expect(result.valid).toBe(false);
|
|
150
|
-
expect(result.error).toContain("Forbidden");
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("blocks INSERT", () => {
|
|
154
|
-
const result = validateSQL("INSERT INTO sales VALUES (1, 100)");
|
|
155
|
-
expect(result.valid).toBe(false);
|
|
156
|
-
expect(result.error).toContain("Forbidden");
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("blocks CREATE TABLE", () => {
|
|
160
|
-
const result = validateSQL("CREATE TABLE foo (id INT)");
|
|
161
|
-
expect(result.valid).toBe(false);
|
|
162
|
-
expect(result.error).toContain("Forbidden");
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("blocks DROP TABLE", () => {
|
|
166
|
-
const result = validateSQL("DROP TABLE sales");
|
|
167
|
-
expect(result.valid).toBe(false);
|
|
168
|
-
expect(result.error).toContain("Forbidden");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("blocks DESCRIBE", () => {
|
|
172
|
-
const result = validateSQL("DESCRIBE sales");
|
|
173
|
-
expect(result.valid).toBe(false);
|
|
174
|
-
expect(result.error).toContain("Forbidden");
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("blocks SHOW", () => {
|
|
178
|
-
const result = validateSQL("SHOW TABLES");
|
|
179
|
-
expect(result.valid).toBe(false);
|
|
180
|
-
expect(result.error).toContain("Forbidden");
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// --- Table whitelist ---
|
|
184
|
-
|
|
185
|
-
it("rejects queries on non-whitelisted tables", () => {
|
|
186
|
-
const result = validateSQL("SELECT * FROM secret_data");
|
|
187
|
-
expect(result.valid).toBe(false);
|
|
188
|
-
expect(result.error).toContain("not in the allowed list");
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("allows queries on whitelisted tables", () => {
|
|
192
|
-
const result = validateSQL("SELECT * FROM customers");
|
|
193
|
-
expect(result.valid).toBe(true);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// --- File-reading function blocks ---
|
|
197
|
-
|
|
198
|
-
it("blocks read_csv_auto", () => {
|
|
199
|
-
const result = validateSQL("SELECT * FROM read_csv_auto('/etc/passwd')");
|
|
200
|
-
expect(result.valid).toBe(false);
|
|
201
|
-
expect(result.error).toContain("Forbidden");
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("blocks read_parquet", () => {
|
|
205
|
-
const result = validateSQL("SELECT * FROM read_parquet('/data/secret.parquet')");
|
|
206
|
-
expect(result.valid).toBe(false);
|
|
207
|
-
expect(result.error).toContain("Forbidden");
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("blocks read_json_auto", () => {
|
|
211
|
-
const result = validateSQL("SELECT * FROM read_json_auto('/tmp/data.json')");
|
|
212
|
-
expect(result.valid).toBe(false);
|
|
213
|
-
expect(result.error).toContain("Forbidden");
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it("blocks SET", () => {
|
|
217
|
-
const result = validateSQL("SET memory_limit='100GB'");
|
|
218
|
-
expect(result.valid).toBe(false);
|
|
219
|
-
expect(result.error).toContain("Forbidden");
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("blocks COPY ... TO (via base patterns)", () => {
|
|
223
|
-
const result = validateSQL("COPY sales TO '/tmp/exfil.csv'");
|
|
224
|
-
expect(result.valid).toBe(false);
|
|
225
|
-
expect(result.error).toContain("Forbidden");
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("blocks LOAD (via base patterns)", () => {
|
|
229
|
-
const result = validateSQL("LOAD httpfs");
|
|
230
|
-
expect(result.valid).toBe(false);
|
|
231
|
-
expect(result.error).toContain("Forbidden");
|
|
232
|
-
});
|
|
233
|
-
});
|