@useatlas/create 0.0.2 → 0.0.3
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,172 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SOQL validation — regex + structural checks.
|
|
3
|
-
*
|
|
4
|
-
* SOQL is simpler than SQL, so no AST parser is needed. Validation layers:
|
|
5
|
-
* 0. Empty check
|
|
6
|
-
* 1. Regex mutation guard (INSERT, UPDATE, DELETE, UPSERT, MERGE, UNDELETE)
|
|
7
|
-
* 2. Must start with SELECT, no semicolons
|
|
8
|
-
* 3. Object whitelist — FROM object must be in the allowed set
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const SOQL_FORBIDDEN_PATTERNS = [
|
|
12
|
-
/\b(INSERT)\b/i,
|
|
13
|
-
/\b(UPDATE)\b/i,
|
|
14
|
-
/\b(DELETE)\b/i,
|
|
15
|
-
/\b(UPSERT)\b/i,
|
|
16
|
-
/\b(MERGE)\b/i,
|
|
17
|
-
/\b(UNDELETE)\b/i,
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Strip single-quoted string literals from SOQL so regex guards don't match
|
|
22
|
-
* keywords embedded in user values (e.g. `WHERE Name = 'delete this'`).
|
|
23
|
-
*/
|
|
24
|
-
function stripStringLiterals(soql: string): string {
|
|
25
|
-
return soql.replace(/'[^']*'/g, "''");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Extract top-level object names referenced in FROM clauses.
|
|
30
|
-
*
|
|
31
|
-
* Parent-to-child relationship subqueries — `(SELECT ... FROM Contacts)` inside
|
|
32
|
-
* the SELECT list — use relationship names (plural) that don't appear in the
|
|
33
|
-
* object whitelist. Salesforce enforces object-level security server-side for
|
|
34
|
-
* these, so we skip nested FROM inside parenthesized subqueries.
|
|
35
|
-
*
|
|
36
|
-
* Semi-join / anti-join subqueries in WHERE — `WHERE Id IN (SELECT ... FROM Contact)`
|
|
37
|
-
* — reference real object names and ARE checked.
|
|
38
|
-
*/
|
|
39
|
-
function extractFromObjects(soql: string): string[] {
|
|
40
|
-
const objects: string[] = [];
|
|
41
|
-
|
|
42
|
-
// Step 1: Remove parenthesized subqueries that appear in the SELECT clause
|
|
43
|
-
// (relationship subqueries). We do this by stripping content between the
|
|
44
|
-
// top-level SELECT and the top-level FROM, then extracting FROM objects from
|
|
45
|
-
// the remainder.
|
|
46
|
-
//
|
|
47
|
-
// Strategy: find the top-level FROM position (not inside parens), then only
|
|
48
|
-
// extract FROM objects from that point onward.
|
|
49
|
-
|
|
50
|
-
let depth = 0;
|
|
51
|
-
let topLevelFromIndex = -1;
|
|
52
|
-
|
|
53
|
-
// We need to find the top-level FROM keyword (not inside parentheses)
|
|
54
|
-
const upperSoql = soql.toUpperCase();
|
|
55
|
-
for (let i = 0; i < soql.length; i++) {
|
|
56
|
-
if (soql[i] === "(") {
|
|
57
|
-
depth++;
|
|
58
|
-
} else if (soql[i] === ")") {
|
|
59
|
-
depth--;
|
|
60
|
-
} else if (depth === 0) {
|
|
61
|
-
// Check if this position starts "FROM " at the top level
|
|
62
|
-
if (
|
|
63
|
-
upperSoql.startsWith("FROM", i) &&
|
|
64
|
-
(i === 0 || /\s/.test(soql[i - 1])) &&
|
|
65
|
-
i + 4 < soql.length &&
|
|
66
|
-
/\s/.test(soql[i + 4])
|
|
67
|
-
) {
|
|
68
|
-
topLevelFromIndex = i;
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (topLevelFromIndex === -1) {
|
|
75
|
-
return objects;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Extract the top-level FROM object
|
|
79
|
-
const afterFrom = soql.slice(topLevelFromIndex);
|
|
80
|
-
const topMatch = /\bFROM\s+(\w+)/i.exec(afterFrom);
|
|
81
|
-
if (topMatch) {
|
|
82
|
-
objects.push(topMatch[1]);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Now extract FROM objects in WHERE/HAVING subqueries (semi-joins/anti-joins).
|
|
86
|
-
// These are parenthesized SELECT...FROM blocks that appear AFTER the top-level FROM.
|
|
87
|
-
const whereClause = soql.slice(topLevelFromIndex + (topMatch ? topMatch[0].length : 4));
|
|
88
|
-
// Find FROM inside parenthesized subqueries in WHERE — these are real object references
|
|
89
|
-
const subqueryPattern = /\(\s*SELECT\b[^)]*\bFROM\s+(\w+)/gi;
|
|
90
|
-
let subMatch;
|
|
91
|
-
while ((subMatch = subqueryPattern.exec(whereClause)) !== null) {
|
|
92
|
-
objects.push(subMatch[1]);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return objects;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Validate a SOQL query for safety.
|
|
100
|
-
*
|
|
101
|
-
* @param soql - The SOQL query string.
|
|
102
|
-
* @param allowedObjects - Set of allowed Salesforce object names (case-insensitive).
|
|
103
|
-
* @returns Validation result.
|
|
104
|
-
*/
|
|
105
|
-
export function validateSOQL(
|
|
106
|
-
soql: string,
|
|
107
|
-
allowedObjects: Set<string>,
|
|
108
|
-
): { valid: boolean; error?: string } {
|
|
109
|
-
// 0. Empty check
|
|
110
|
-
const trimmed = soql.trim();
|
|
111
|
-
if (!trimmed) {
|
|
112
|
-
return { valid: false, error: "Empty query" };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Reject semicolons (no statement chaining)
|
|
116
|
-
if (trimmed.includes(";")) {
|
|
117
|
-
return { valid: false, error: "Semicolons are not allowed in SOQL queries" };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// 1. Regex mutation guard — strip string literals first so keywords inside
|
|
121
|
-
// values like `WHERE Name = 'delete this'` don't trigger false positives.
|
|
122
|
-
const stripped = stripStringLiterals(trimmed);
|
|
123
|
-
for (const pattern of SOQL_FORBIDDEN_PATTERNS) {
|
|
124
|
-
if (pattern.test(stripped)) {
|
|
125
|
-
return {
|
|
126
|
-
valid: false,
|
|
127
|
-
error: `Forbidden SOQL operation detected: ${pattern.source}`,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// 2. Must start with SELECT
|
|
133
|
-
if (!/^\s*SELECT\b/i.test(trimmed)) {
|
|
134
|
-
return {
|
|
135
|
-
valid: false,
|
|
136
|
-
error: "Only SELECT queries are allowed in SOQL",
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// 3. Object whitelist
|
|
141
|
-
const objects = extractFromObjects(trimmed);
|
|
142
|
-
if (objects.length === 0) {
|
|
143
|
-
return { valid: false, error: "No FROM clause found in query" };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Build lowercase allowed set for case-insensitive comparison
|
|
147
|
-
const allowedLower = new Set(
|
|
148
|
-
Array.from(allowedObjects).map((o) => o.toLowerCase()),
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
for (const obj of objects) {
|
|
152
|
-
if (!allowedLower.has(obj.toLowerCase())) {
|
|
153
|
-
return {
|
|
154
|
-
valid: false,
|
|
155
|
-
error: `Object "${obj}" is not in the allowed list. Check catalog.yml for available objects.`,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return { valid: true };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Append a LIMIT clause to a SOQL query if one is not already present.
|
|
165
|
-
*/
|
|
166
|
-
export function appendSOQLLimit(soql: string, limit: number): string {
|
|
167
|
-
const trimmed = soql.trim();
|
|
168
|
-
if (/\bLIMIT\b/i.test(trimmed)) {
|
|
169
|
-
return trimmed;
|
|
170
|
-
}
|
|
171
|
-
return `${trimmed} LIMIT ${limit}`;
|
|
172
|
-
}
|
|
File without changes
|
|
File without changes
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the DuckDB adapter (duckdb.ts) and DuckDB URL parsing.
|
|
3
|
-
*
|
|
4
|
-
* Uses a real in-memory DuckDB instance (no mocks) to verify end-to-end
|
|
5
|
-
* query execution, column extraction, and close behavior.
|
|
6
|
-
*/
|
|
7
|
-
import { describe, it, expect, afterEach } from "bun:test";
|
|
8
|
-
import { parseDuckDBUrl, createDuckDBConnection } from "../duckdb";
|
|
9
|
-
import type { DBConnection } from "../connection";
|
|
10
|
-
import * as fs from "fs";
|
|
11
|
-
import * as path from "path";
|
|
12
|
-
import * as os from "os";
|
|
13
|
-
|
|
14
|
-
describe("parseDuckDBUrl", () => {
|
|
15
|
-
it("parses duckdb://:memory: as in-memory", () => {
|
|
16
|
-
const config = parseDuckDBUrl("duckdb://:memory:");
|
|
17
|
-
expect(config.path).toBe(":memory:");
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("parses bare duckdb:// as in-memory", () => {
|
|
21
|
-
const config = parseDuckDBUrl("duckdb://");
|
|
22
|
-
expect(config.path).toBe(":memory:");
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("parses duckdb:///absolute/path.duckdb", () => {
|
|
26
|
-
const config = parseDuckDBUrl("duckdb:///tmp/test.duckdb");
|
|
27
|
-
expect(config.path).toBe("/tmp/test.duckdb");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("parses duckdb://relative/path.duckdb", () => {
|
|
31
|
-
const config = parseDuckDBUrl("duckdb://data/test.duckdb");
|
|
32
|
-
expect(config.path).toBe("data/test.duckdb");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("throws for non-duckdb:// URL", () => {
|
|
36
|
-
expect(() => parseDuckDBUrl("postgresql://localhost/db")).toThrow("Invalid DuckDB URL");
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe("createDuckDBConnection", () => {
|
|
41
|
-
let conn: DBConnection | null = null;
|
|
42
|
-
|
|
43
|
-
afterEach(async () => {
|
|
44
|
-
if (conn) {
|
|
45
|
-
await conn.close();
|
|
46
|
-
conn = null;
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("creates a connection and runs a simple query", async () => {
|
|
51
|
-
conn = createDuckDBConnection({ path: ":memory:", readOnly: false });
|
|
52
|
-
const result = await conn.query("SELECT 42 AS answer");
|
|
53
|
-
expect(result.columns).toEqual(["answer"]);
|
|
54
|
-
expect(result.rows).toHaveLength(1);
|
|
55
|
-
expect(result.rows[0].answer).toBe(42);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("returns correct columns and rows for multi-column queries", async () => {
|
|
59
|
-
conn = createDuckDBConnection({ path: ":memory:", readOnly: false });
|
|
60
|
-
const result = await conn.query("SELECT 1 AS a, 'hello' AS b, true AS c");
|
|
61
|
-
expect(result.columns).toEqual(["a", "b", "c"]);
|
|
62
|
-
expect(result.rows).toHaveLength(1);
|
|
63
|
-
expect(result.rows[0].a).toBe(1);
|
|
64
|
-
expect(result.rows[0].b).toBe("hello");
|
|
65
|
-
expect(result.rows[0].c).toBe(true);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("handles empty result sets", async () => {
|
|
69
|
-
conn = createDuckDBConnection({ path: ":memory:", readOnly: false });
|
|
70
|
-
const result = await conn.query("SELECT 1 AS n WHERE false");
|
|
71
|
-
expect(result.columns).toEqual(["n"]);
|
|
72
|
-
expect(result.rows).toHaveLength(0);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("supports CREATE TABLE and SELECT in read-write mode", async () => {
|
|
76
|
-
conn = createDuckDBConnection({ path: ":memory:", readOnly: false });
|
|
77
|
-
await conn.query("CREATE TABLE test (id INTEGER, name VARCHAR)");
|
|
78
|
-
await conn.query("INSERT INTO test VALUES (1, 'alice'), (2, 'bob')");
|
|
79
|
-
const result = await conn.query("SELECT * FROM test ORDER BY id");
|
|
80
|
-
expect(result.columns).toEqual(["id", "name"]);
|
|
81
|
-
expect(result.rows).toHaveLength(2);
|
|
82
|
-
expect(result.rows[0].name).toBe("alice");
|
|
83
|
-
expect(result.rows[1].name).toBe("bob");
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("supports VALUES clause for inline data", async () => {
|
|
87
|
-
conn = createDuckDBConnection({ path: ":memory:", readOnly: false });
|
|
88
|
-
const result = await conn.query(
|
|
89
|
-
"SELECT * FROM (VALUES (1, 'a'), (2, 'b'), (3, 'c')) AS t(id, letter)"
|
|
90
|
-
);
|
|
91
|
-
expect(result.rows).toHaveLength(3);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("close is idempotent", async () => {
|
|
95
|
-
conn = createDuckDBConnection({ path: ":memory:", readOnly: false });
|
|
96
|
-
await conn.query("SELECT 1");
|
|
97
|
-
await conn.close();
|
|
98
|
-
await conn.close(); // Should not throw
|
|
99
|
-
conn = null; // Prevent afterEach from closing again
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("enforces read-only mode for file-based databases", async () => {
|
|
103
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "atlas-duckdb-ro-"));
|
|
104
|
-
try {
|
|
105
|
-
const dbPath = path.join(tmpDir, "test.duckdb");
|
|
106
|
-
|
|
107
|
-
// Create and populate the database first (read-write)
|
|
108
|
-
const rwConn = createDuckDBConnection({ path: dbPath, readOnly: false });
|
|
109
|
-
await rwConn.query("CREATE TABLE test (id INTEGER)");
|
|
110
|
-
await rwConn.query("INSERT INTO test VALUES (1)");
|
|
111
|
-
await rwConn.close();
|
|
112
|
-
|
|
113
|
-
// Open in read-only mode (default for file-based)
|
|
114
|
-
conn = createDuckDBConnection({ path: dbPath });
|
|
115
|
-
const result = await conn.query("SELECT * FROM test");
|
|
116
|
-
expect(result.rows).toHaveLength(1);
|
|
117
|
-
|
|
118
|
-
// Write operations should fail
|
|
119
|
-
expect(conn.query("INSERT INTO test VALUES (2)")).rejects.toThrow();
|
|
120
|
-
} finally {
|
|
121
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("respects timeoutMs parameter", async () => {
|
|
126
|
-
conn = createDuckDBConnection({ path: ":memory:", readOnly: false });
|
|
127
|
-
// A very short timeout on a query that takes some time
|
|
128
|
-
await expect(
|
|
129
|
-
conn.query("SELECT COUNT(*) FROM generate_series(1, 100000000)", 1)
|
|
130
|
-
).rejects.toThrow("timed out");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("can recover after close by re-initializing", async () => {
|
|
134
|
-
conn = createDuckDBConnection({ path: ":memory:", readOnly: false });
|
|
135
|
-
await conn.query("SELECT 1");
|
|
136
|
-
await conn.close();
|
|
137
|
-
// After close + retry, lazy init should re-create the connection
|
|
138
|
-
const result = await conn.query("SELECT 42 AS answer");
|
|
139
|
-
expect(result.rows[0].answer).toBe(42);
|
|
140
|
-
});
|
|
141
|
-
});
|
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the Salesforce DataSource adapter and registry.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect, beforeEach, mock } from "bun:test";
|
|
5
|
-
|
|
6
|
-
// Mock jsforce before importing the module under test
|
|
7
|
-
const mockLogin = mock(() => Promise.resolve());
|
|
8
|
-
const mockLogout = mock(() => Promise.resolve());
|
|
9
|
-
const mockQuery = mock(() =>
|
|
10
|
-
Promise.resolve({
|
|
11
|
-
records: [
|
|
12
|
-
{ attributes: { type: "Account" }, Id: "001", Name: "Acme" },
|
|
13
|
-
{ attributes: { type: "Account" }, Id: "002", Name: "Widget Co" },
|
|
14
|
-
],
|
|
15
|
-
}),
|
|
16
|
-
);
|
|
17
|
-
const mockDescribe = mock(() =>
|
|
18
|
-
Promise.resolve({
|
|
19
|
-
name: "Account",
|
|
20
|
-
label: "Account",
|
|
21
|
-
fields: [
|
|
22
|
-
{
|
|
23
|
-
name: "Id",
|
|
24
|
-
type: "id",
|
|
25
|
-
label: "Account ID",
|
|
26
|
-
picklistValues: [],
|
|
27
|
-
referenceTo: [],
|
|
28
|
-
nillable: false,
|
|
29
|
-
length: 18,
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
name: "Name",
|
|
33
|
-
type: "string",
|
|
34
|
-
label: "Account Name",
|
|
35
|
-
picklistValues: [],
|
|
36
|
-
referenceTo: [],
|
|
37
|
-
nillable: false,
|
|
38
|
-
length: 255,
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
name: "Industry",
|
|
42
|
-
type: "picklist",
|
|
43
|
-
label: "Industry",
|
|
44
|
-
picklistValues: [
|
|
45
|
-
{ value: "Technology", label: "Technology", active: true },
|
|
46
|
-
{ value: "Finance", label: "Finance", active: true },
|
|
47
|
-
],
|
|
48
|
-
referenceTo: [],
|
|
49
|
-
nillable: true,
|
|
50
|
-
length: 255,
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
}),
|
|
54
|
-
);
|
|
55
|
-
const mockDescribeGlobal = mock(() =>
|
|
56
|
-
Promise.resolve({
|
|
57
|
-
sobjects: [
|
|
58
|
-
{ name: "Account", label: "Account", queryable: true },
|
|
59
|
-
{ name: "Contact", label: "Contact", queryable: true },
|
|
60
|
-
{ name: "ApexLog", label: "Apex Log", queryable: false },
|
|
61
|
-
],
|
|
62
|
-
}),
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
mock.module("jsforce", () => ({
|
|
66
|
-
Connection: class MockConnection {
|
|
67
|
-
login = mockLogin;
|
|
68
|
-
logout = mockLogout;
|
|
69
|
-
query = mockQuery;
|
|
70
|
-
describe = mockDescribe;
|
|
71
|
-
describeGlobal = mockDescribeGlobal;
|
|
72
|
-
},
|
|
73
|
-
}));
|
|
74
|
-
|
|
75
|
-
const {
|
|
76
|
-
parseSalesforceURL,
|
|
77
|
-
createSalesforceDataSource,
|
|
78
|
-
registerSalesforceSource,
|
|
79
|
-
getSalesforceSource,
|
|
80
|
-
listSalesforceSources,
|
|
81
|
-
describeSalesforceSources,
|
|
82
|
-
_resetSalesforceSources,
|
|
83
|
-
} = await import("@atlas/api/lib/db/salesforce");
|
|
84
|
-
|
|
85
|
-
describe("parseSalesforceURL", () => {
|
|
86
|
-
it("parses a full URL", () => {
|
|
87
|
-
const config = parseSalesforceURL(
|
|
88
|
-
"salesforce://user%40example.com:pass123@login.salesforce.com?token=SECTOKEN",
|
|
89
|
-
);
|
|
90
|
-
expect(config.loginUrl).toBe("https://login.salesforce.com");
|
|
91
|
-
expect(config.username).toBe("user@example.com");
|
|
92
|
-
expect(config.password).toBe("pass123");
|
|
93
|
-
expect(config.securityToken).toBe("SECTOKEN");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("parses minimal URL with defaults", () => {
|
|
97
|
-
const config = parseSalesforceURL("salesforce://admin:secret@localhost");
|
|
98
|
-
expect(config.loginUrl).toBe("https://localhost");
|
|
99
|
-
expect(config.username).toBe("admin");
|
|
100
|
-
expect(config.password).toBe("secret");
|
|
101
|
-
expect(config.securityToken).toBeUndefined();
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("parses sandbox URL (test.salesforce.com)", () => {
|
|
105
|
-
const config = parseSalesforceURL(
|
|
106
|
-
"salesforce://user:pass@test.salesforce.com",
|
|
107
|
-
);
|
|
108
|
-
expect(config.loginUrl).toBe("https://test.salesforce.com");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("parses OAuth params", () => {
|
|
112
|
-
const config = parseSalesforceURL(
|
|
113
|
-
"salesforce://user:pass@login.salesforce.com?clientId=CID&clientSecret=CSEC",
|
|
114
|
-
);
|
|
115
|
-
expect(config.clientId).toBe("CID");
|
|
116
|
-
expect(config.clientSecret).toBe("CSEC");
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("throws for non-salesforce scheme", () => {
|
|
120
|
-
expect(() => parseSalesforceURL("postgresql://user:pass@localhost")).toThrow(
|
|
121
|
-
"expected salesforce://",
|
|
122
|
-
);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("throws for missing username", () => {
|
|
126
|
-
expect(() =>
|
|
127
|
-
parseSalesforceURL("salesforce://:pass@login.salesforce.com"),
|
|
128
|
-
).toThrow("missing username");
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("throws for missing password", () => {
|
|
132
|
-
expect(() =>
|
|
133
|
-
parseSalesforceURL("salesforce://user@login.salesforce.com"),
|
|
134
|
-
).toThrow("missing password");
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
describe("createSalesforceDataSource", () => {
|
|
139
|
-
beforeEach(() => {
|
|
140
|
-
mockLogin.mockClear();
|
|
141
|
-
mockLogout.mockClear();
|
|
142
|
-
mockQuery.mockClear();
|
|
143
|
-
mockDescribe.mockClear();
|
|
144
|
-
mockDescribeGlobal.mockClear();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("query returns columns and rows without attributes key", async () => {
|
|
148
|
-
const source = createSalesforceDataSource({
|
|
149
|
-
loginUrl: "https://login.salesforce.com",
|
|
150
|
-
username: "user",
|
|
151
|
-
password: "pass",
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const result = await source.query("SELECT Id, Name FROM Account");
|
|
155
|
-
expect(result.columns).toEqual(["Id", "Name"]);
|
|
156
|
-
expect(result.rows).toEqual([
|
|
157
|
-
{ Id: "001", Name: "Acme" },
|
|
158
|
-
{ Id: "002", Name: "Widget Co" },
|
|
159
|
-
]);
|
|
160
|
-
expect(mockLogin).toHaveBeenCalledTimes(1);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("query returns empty result for no records", async () => {
|
|
164
|
-
mockQuery.mockImplementationOnce(() =>
|
|
165
|
-
Promise.resolve({ records: [] }),
|
|
166
|
-
);
|
|
167
|
-
const source = createSalesforceDataSource({
|
|
168
|
-
loginUrl: "https://login.salesforce.com",
|
|
169
|
-
username: "user",
|
|
170
|
-
password: "pass",
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
const result = await source.query("SELECT Id FROM Account WHERE Id = 'none'");
|
|
174
|
-
expect(result.columns).toEqual([]);
|
|
175
|
-
expect(result.rows).toEqual([]);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("describe returns mapped fields", async () => {
|
|
179
|
-
const source = createSalesforceDataSource({
|
|
180
|
-
loginUrl: "https://login.salesforce.com",
|
|
181
|
-
username: "user",
|
|
182
|
-
password: "pass",
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
const desc = await source.describe("Account");
|
|
186
|
-
expect(desc.name).toBe("Account");
|
|
187
|
-
expect(desc.fields).toHaveLength(3);
|
|
188
|
-
expect(desc.fields[0].name).toBe("Id");
|
|
189
|
-
expect(desc.fields[2].picklistValues).toHaveLength(2);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("listObjects filters to queryable only", async () => {
|
|
193
|
-
const source = createSalesforceDataSource({
|
|
194
|
-
loginUrl: "https://login.salesforce.com",
|
|
195
|
-
username: "user",
|
|
196
|
-
password: "pass",
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
const objects = await source.listObjects();
|
|
200
|
-
expect(objects).toHaveLength(2);
|
|
201
|
-
expect(objects.map((o) => o.name)).toEqual(["Account", "Contact"]);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("close calls logout", async () => {
|
|
205
|
-
const source = createSalesforceDataSource({
|
|
206
|
-
loginUrl: "https://login.salesforce.com",
|
|
207
|
-
username: "user",
|
|
208
|
-
password: "pass",
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// Force login first
|
|
212
|
-
await source.listObjects();
|
|
213
|
-
await source.close();
|
|
214
|
-
expect(mockLogout).toHaveBeenCalledTimes(1);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it("appends security token to password on login", async () => {
|
|
218
|
-
const source = createSalesforceDataSource({
|
|
219
|
-
loginUrl: "https://login.salesforce.com",
|
|
220
|
-
username: "user",
|
|
221
|
-
password: "pass",
|
|
222
|
-
securityToken: "TOKEN123",
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
await source.listObjects();
|
|
226
|
-
expect(mockLogin).toHaveBeenCalledWith("user", "passTOKEN123");
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it("serializes concurrent login attempts (no duplicate logins)", async () => {
|
|
230
|
-
// Make login take some time so concurrent calls overlap
|
|
231
|
-
mockLogin.mockImplementation(
|
|
232
|
-
() => new Promise((resolve) => setTimeout(resolve, 50)),
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
const source = createSalesforceDataSource({
|
|
236
|
-
loginUrl: "https://login.salesforce.com",
|
|
237
|
-
username: "user",
|
|
238
|
-
password: "pass",
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
// Fire three concurrent queries — all need login
|
|
242
|
-
await Promise.all([
|
|
243
|
-
source.query("SELECT Id FROM Account"),
|
|
244
|
-
source.listObjects(),
|
|
245
|
-
source.describe("Account"),
|
|
246
|
-
]);
|
|
247
|
-
|
|
248
|
-
// Login should have been called exactly once despite three concurrent callers
|
|
249
|
-
expect(mockLogin).toHaveBeenCalledTimes(1);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it("close without prior login is a no-op", async () => {
|
|
253
|
-
const source = createSalesforceDataSource({
|
|
254
|
-
loginUrl: "https://login.salesforce.com",
|
|
255
|
-
username: "user",
|
|
256
|
-
password: "pass",
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// Close without ever calling login — should not throw or call logout
|
|
260
|
-
await source.close();
|
|
261
|
-
expect(mockLogout).not.toHaveBeenCalled();
|
|
262
|
-
expect(mockLogin).not.toHaveBeenCalled();
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
describe("Salesforce source registry", () => {
|
|
267
|
-
beforeEach(() => {
|
|
268
|
-
_resetSalesforceSources();
|
|
269
|
-
mockLogin.mockClear();
|
|
270
|
-
mockLogout.mockClear();
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it("register and get a source", () => {
|
|
274
|
-
registerSalesforceSource("sf1", {
|
|
275
|
-
loginUrl: "https://login.salesforce.com",
|
|
276
|
-
username: "user",
|
|
277
|
-
password: "pass",
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
const source = getSalesforceSource("sf1");
|
|
281
|
-
expect(source).toBeDefined();
|
|
282
|
-
expect(source.query).toBeDefined();
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it("throws for unregistered source", () => {
|
|
286
|
-
expect(() => getSalesforceSource("nonexistent")).toThrow(
|
|
287
|
-
'not registered',
|
|
288
|
-
);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it("lists registered sources", () => {
|
|
292
|
-
registerSalesforceSource("sf1", {
|
|
293
|
-
loginUrl: "https://login.salesforce.com",
|
|
294
|
-
username: "user",
|
|
295
|
-
password: "pass",
|
|
296
|
-
});
|
|
297
|
-
registerSalesforceSource("sf2", {
|
|
298
|
-
loginUrl: "https://test.salesforce.com",
|
|
299
|
-
username: "admin",
|
|
300
|
-
password: "secret",
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
expect(listSalesforceSources()).toEqual(["sf1", "sf2"]);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it("reset clears all sources", () => {
|
|
307
|
-
registerSalesforceSource("sf1", {
|
|
308
|
-
loginUrl: "https://login.salesforce.com",
|
|
309
|
-
username: "user",
|
|
310
|
-
password: "pass",
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
_resetSalesforceSources();
|
|
314
|
-
expect(listSalesforceSources()).toEqual([]);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it("describeSalesforceSources returns metadata for registered sources", () => {
|
|
318
|
-
registerSalesforceSource("sf1", {
|
|
319
|
-
loginUrl: "https://login.salesforce.com",
|
|
320
|
-
username: "user",
|
|
321
|
-
password: "pass",
|
|
322
|
-
});
|
|
323
|
-
registerSalesforceSource("sf2", {
|
|
324
|
-
loginUrl: "https://test.salesforce.com",
|
|
325
|
-
username: "admin",
|
|
326
|
-
password: "secret",
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
const meta = describeSalesforceSources();
|
|
330
|
-
expect(meta).toEqual([
|
|
331
|
-
{ id: "sf1", dbType: "salesforce" },
|
|
332
|
-
{ id: "sf2", dbType: "salesforce" },
|
|
333
|
-
]);
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
it("describeSalesforceSources returns empty array when no sources registered", () => {
|
|
337
|
-
expect(describeSalesforceSources()).toEqual([]);
|
|
338
|
-
});
|
|
339
|
-
});
|