@useatlas/create 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (515) hide show
  1. package/README.md +231 -0
  2. package/index.ts +829 -0
  3. package/package.json +38 -0
  4. package/templates/docker/.env.example +67 -0
  5. package/templates/docker/Dockerfile +52 -0
  6. package/templates/docker/bin/__tests__/benchmark.test.ts +598 -0
  7. package/templates/docker/bin/__tests__/duckdb-ingest.test.ts +171 -0
  8. package/templates/docker/bin/__tests__/eval.test.ts +434 -0
  9. package/templates/docker/bin/__tests__/matview-partition.test.ts +615 -0
  10. package/templates/docker/bin/__tests__/multi-source.test.ts +113 -0
  11. package/templates/docker/bin/__tests__/plugin-cli.test.ts +322 -0
  12. package/templates/docker/bin/__tests__/profiler-heuristics.test.ts +608 -0
  13. package/templates/docker/bin/__tests__/query.test.ts +240 -0
  14. package/templates/docker/bin/__tests__/schema-drift.test.ts +542 -0
  15. package/templates/docker/bin/__tests__/view-yaml-generation.test.ts +146 -0
  16. package/templates/docker/bin/atlas.ts +5044 -0
  17. package/templates/docker/bin/benchmark.ts +695 -0
  18. package/templates/docker/bin/enrich.ts +559 -0
  19. package/templates/docker/bin/eval.ts +770 -0
  20. package/templates/docker/bin/smoke.ts +438 -0
  21. package/templates/docker/data/.gitkeep +0 -0
  22. package/templates/docker/data/cybersec.sql +1961 -0
  23. package/templates/docker/data/demo-semantic/catalog.yml +40 -0
  24. package/templates/docker/data/demo-semantic/entities/accounts.yml +170 -0
  25. package/templates/docker/data/demo-semantic/entities/companies.yml +207 -0
  26. package/templates/docker/data/demo-semantic/entities/people.yml +145 -0
  27. package/templates/docker/data/demo-semantic/glossary.yml +22 -0
  28. package/templates/docker/data/demo-semantic/metrics/accounts.yml +38 -0
  29. package/templates/docker/data/demo-semantic/metrics/companies.yml +89 -0
  30. package/templates/docker/data/demo.sql +373 -0
  31. package/templates/docker/data/ecommerce.sql +1690 -0
  32. package/templates/docker/data/init-demo-db.sql +8 -0
  33. package/templates/docker/docker-compose.yml +34 -0
  34. package/templates/docker/docs/deploy.md +390 -0
  35. package/templates/docker/eslint.config.mjs +18 -0
  36. package/templates/docker/gitignore +5 -0
  37. package/templates/docker/next.config.ts +9 -0
  38. package/templates/docker/package.json +59 -0
  39. package/templates/docker/postcss.config.mjs +8 -0
  40. package/templates/docker/public/.gitkeep +0 -0
  41. package/templates/docker/public/favicon.svg +4 -0
  42. package/templates/docker/railway.json +13 -0
  43. package/templates/docker/render.yaml +34 -0
  44. package/templates/docker/semantic/catalog.yml +5 -0
  45. package/templates/docker/semantic/entities/.gitkeep +0 -0
  46. package/templates/docker/semantic/glossary.yml +6 -0
  47. package/templates/docker/semantic/metrics/.gitkeep +0 -0
  48. package/templates/docker/sidecar/Dockerfile +28 -0
  49. package/templates/docker/sidecar/railway.json +14 -0
  50. package/templates/docker/sidecar/server.ts +188 -0
  51. package/templates/docker/src/api/__tests__/actions.test.ts +683 -0
  52. package/templates/docker/src/api/__tests__/admin.test.ts +820 -0
  53. package/templates/docker/src/api/__tests__/auth.test.ts +165 -0
  54. package/templates/docker/src/api/__tests__/chat.test.ts +376 -0
  55. package/templates/docker/src/api/__tests__/conversations.test.ts +555 -0
  56. package/templates/docker/src/api/__tests__/cors.test.ts +135 -0
  57. package/templates/docker/src/api/__tests__/health-plugin.test.ts +169 -0
  58. package/templates/docker/src/api/__tests__/health.test.ts +261 -0
  59. package/templates/docker/src/api/__tests__/query.test.ts +891 -0
  60. package/templates/docker/src/api/__tests__/scheduled-tasks.test.ts +601 -0
  61. package/templates/docker/src/api/__tests__/slack.test.ts +847 -0
  62. package/templates/docker/src/api/index.ts +117 -0
  63. package/templates/docker/src/api/routes/actions.ts +274 -0
  64. package/templates/docker/src/api/routes/admin.ts +757 -0
  65. package/templates/docker/src/api/routes/auth.ts +48 -0
  66. package/templates/docker/src/api/routes/chat.ts +465 -0
  67. package/templates/docker/src/api/routes/conversations.ts +266 -0
  68. package/templates/docker/src/api/routes/health.ts +287 -0
  69. package/templates/docker/src/api/routes/openapi.ts +390 -0
  70. package/templates/docker/src/api/routes/query.ts +318 -0
  71. package/templates/docker/src/api/routes/scheduled-tasks.ts +467 -0
  72. package/templates/docker/src/api/routes/slack.ts +611 -0
  73. package/templates/docker/src/api/server.ts +226 -0
  74. package/templates/docker/src/app/api/[...route]/route.ts +33 -0
  75. package/templates/docker/src/app/error.tsx +24 -0
  76. package/templates/docker/src/app/globals.css +126 -0
  77. package/templates/docker/src/app/layout.tsx +19 -0
  78. package/templates/docker/src/app/page.tsx +14 -0
  79. package/templates/docker/src/global.d.ts +1 -0
  80. package/templates/docker/src/lib/__tests__/agent-cache.test.ts +437 -0
  81. package/templates/docker/src/lib/__tests__/agent-dialect.test.ts +114 -0
  82. package/templates/docker/src/lib/__tests__/agent-health-annotations.test.ts +164 -0
  83. package/templates/docker/src/lib/__tests__/agent-integration.test.ts +514 -0
  84. package/templates/docker/src/lib/__tests__/config-actions.test.ts +166 -0
  85. package/templates/docker/src/lib/__tests__/config.test.ts +1063 -0
  86. package/templates/docker/src/lib/__tests__/conversations.test.ts +589 -0
  87. package/templates/docker/src/lib/__tests__/errors.test.ts +256 -0
  88. package/templates/docker/src/lib/__tests__/logger.test.ts +200 -0
  89. package/templates/docker/src/lib/__tests__/providers.test.ts +99 -0
  90. package/templates/docker/src/lib/__tests__/rls.test.ts +435 -0
  91. package/templates/docker/src/lib/__tests__/scheduled-task-types.test.ts +124 -0
  92. package/templates/docker/src/lib/__tests__/scheduled-tasks.test.ts +550 -0
  93. package/templates/docker/src/lib/__tests__/semantic-index.test.ts +547 -0
  94. package/templates/docker/src/lib/__tests__/semantic-multisource.test.ts +544 -0
  95. package/templates/docker/src/lib/__tests__/semantic.test.ts +363 -0
  96. package/templates/docker/src/lib/__tests__/startup-actions.test.ts +452 -0
  97. package/templates/docker/src/lib/__tests__/startup.test.ts +465 -0
  98. package/templates/docker/src/lib/__tests__/tracing.test.ts +28 -0
  99. package/templates/docker/src/lib/action-types.ts +95 -0
  100. package/templates/docker/src/lib/agent-query.ts +178 -0
  101. package/templates/docker/src/lib/agent.ts +505 -0
  102. package/templates/docker/src/lib/api-url.ts +2 -0
  103. package/templates/docker/src/lib/auth/__tests__/audit.test.ts +418 -0
  104. package/templates/docker/src/lib/auth/__tests__/byot-integration.test.ts +222 -0
  105. package/templates/docker/src/lib/auth/__tests__/byot.test.ts +366 -0
  106. package/templates/docker/src/lib/auth/__tests__/detect.test.ts +190 -0
  107. package/templates/docker/src/lib/auth/__tests__/managed.test.ts +173 -0
  108. package/templates/docker/src/lib/auth/__tests__/middleware.test.ts +456 -0
  109. package/templates/docker/src/lib/auth/__tests__/migrate.test.ts +201 -0
  110. package/templates/docker/src/lib/auth/__tests__/permissions.test.ts +225 -0
  111. package/templates/docker/src/lib/auth/__tests__/server.test.ts +34 -0
  112. package/templates/docker/src/lib/auth/__tests__/simple-key.test.ts +176 -0
  113. package/templates/docker/src/lib/auth/__tests__/types.test.ts +44 -0
  114. package/templates/docker/src/lib/auth/audit.ts +89 -0
  115. package/templates/docker/src/lib/auth/byot.ts +158 -0
  116. package/templates/docker/src/lib/auth/client.ts +35 -0
  117. package/templates/docker/src/lib/auth/detect.ts +83 -0
  118. package/templates/docker/src/lib/auth/managed.ts +73 -0
  119. package/templates/docker/src/lib/auth/middleware.ts +208 -0
  120. package/templates/docker/src/lib/auth/migrate.ts +111 -0
  121. package/templates/docker/src/lib/auth/permissions.ts +156 -0
  122. package/templates/docker/src/lib/auth/server.ts +142 -0
  123. package/templates/docker/src/lib/auth/simple-key.ts +92 -0
  124. package/templates/docker/src/lib/auth/types.ts +49 -0
  125. package/templates/docker/src/lib/config.ts +704 -0
  126. package/templates/docker/src/lib/conversation-types.ts +29 -0
  127. package/templates/docker/src/lib/conversations.ts +270 -0
  128. package/templates/docker/src/lib/db/__tests__/connection.test.ts +69 -0
  129. package/templates/docker/src/lib/db/__tests__/duckdb.test.ts +141 -0
  130. package/templates/docker/src/lib/db/__tests__/internal.test.ts +387 -0
  131. package/templates/docker/src/lib/db/__tests__/registry-health.test.ts +207 -0
  132. package/templates/docker/src/lib/db/__tests__/registry-pool-limits.test.ts +156 -0
  133. package/templates/docker/src/lib/db/__tests__/registry.test.ts +595 -0
  134. package/templates/docker/src/lib/db/__tests__/salesforce.test.ts +339 -0
  135. package/templates/docker/src/lib/db/__tests__/snowflake.test.ts +217 -0
  136. package/templates/docker/src/lib/db/__tests__/source-rate-limit.test.ts +130 -0
  137. package/templates/docker/src/lib/db/connection.ts +753 -0
  138. package/templates/docker/src/lib/db/duckdb.ts +122 -0
  139. package/templates/docker/src/lib/db/internal.ts +273 -0
  140. package/templates/docker/src/lib/db/salesforce.ts +342 -0
  141. package/templates/docker/src/lib/db/source-rate-limit.ts +191 -0
  142. package/templates/docker/src/lib/errors.ts +154 -0
  143. package/templates/docker/src/lib/logger.ts +98 -0
  144. package/templates/docker/src/lib/plugins/__tests__/hooks-integration.test.ts +202 -0
  145. package/templates/docker/src/lib/plugins/__tests__/hooks.test.ts +529 -0
  146. package/templates/docker/src/lib/plugins/__tests__/migrate.test.ts +521 -0
  147. package/templates/docker/src/lib/plugins/__tests__/registry.test.ts +346 -0
  148. package/templates/docker/src/lib/plugins/__tests__/tools.test.ts +49 -0
  149. package/templates/docker/src/lib/plugins/__tests__/wiring.test.ts +585 -0
  150. package/templates/docker/src/lib/plugins/hooks.ts +162 -0
  151. package/templates/docker/src/lib/plugins/index.ts +9 -0
  152. package/templates/docker/src/lib/plugins/migrate.ts +309 -0
  153. package/templates/docker/src/lib/plugins/registry.ts +231 -0
  154. package/templates/docker/src/lib/plugins/tools.ts +39 -0
  155. package/templates/docker/src/lib/plugins/wiring.ts +291 -0
  156. package/templates/docker/src/lib/providers.ts +102 -0
  157. package/templates/docker/src/lib/rls.ts +321 -0
  158. package/templates/docker/src/lib/scheduled-task-types.ts +132 -0
  159. package/templates/docker/src/lib/scheduled-tasks.ts +475 -0
  160. package/templates/docker/src/lib/scheduler/__tests__/delivery.test.ts +192 -0
  161. package/templates/docker/src/lib/scheduler/__tests__/engine.test.ts +248 -0
  162. package/templates/docker/src/lib/scheduler/__tests__/format-email.test.ts +96 -0
  163. package/templates/docker/src/lib/scheduler/__tests__/format-slack.test.ts +78 -0
  164. package/templates/docker/src/lib/scheduler/__tests__/format-webhook.test.ts +78 -0
  165. package/templates/docker/src/lib/scheduler/delivery.ts +248 -0
  166. package/templates/docker/src/lib/scheduler/engine.ts +317 -0
  167. package/templates/docker/src/lib/scheduler/executor.ts +73 -0
  168. package/templates/docker/src/lib/scheduler/format-email.ts +109 -0
  169. package/templates/docker/src/lib/scheduler/format-slack.ts +35 -0
  170. package/templates/docker/src/lib/scheduler/format-webhook.ts +37 -0
  171. package/templates/docker/src/lib/scheduler/index.ts +7 -0
  172. package/templates/docker/src/lib/security.ts +11 -0
  173. package/templates/docker/src/lib/semantic-index.ts +503 -0
  174. package/templates/docker/src/lib/semantic.ts +387 -0
  175. package/templates/docker/src/lib/sidecar-types.ts +16 -0
  176. package/templates/docker/src/lib/slack/__tests__/api.test.ts +160 -0
  177. package/templates/docker/src/lib/slack/__tests__/format.test.ts +237 -0
  178. package/templates/docker/src/lib/slack/__tests__/store.test.ts +188 -0
  179. package/templates/docker/src/lib/slack/__tests__/threads.test.ts +112 -0
  180. package/templates/docker/src/lib/slack/__tests__/verify.test.ts +111 -0
  181. package/templates/docker/src/lib/slack/api.ts +102 -0
  182. package/templates/docker/src/lib/slack/format.ts +209 -0
  183. package/templates/docker/src/lib/slack/store.ts +107 -0
  184. package/templates/docker/src/lib/slack/threads.ts +64 -0
  185. package/templates/docker/src/lib/slack/verify.ts +71 -0
  186. package/templates/docker/src/lib/startup.ts +730 -0
  187. package/templates/docker/src/lib/tools/__tests__/action-permissions.test.ts +594 -0
  188. package/templates/docker/src/lib/tools/__tests__/custom-validation.test.ts +238 -0
  189. package/templates/docker/src/lib/tools/__tests__/explore-backend.test.ts +267 -0
  190. package/templates/docker/src/lib/tools/__tests__/explore-nsjail.test.ts +492 -0
  191. package/templates/docker/src/lib/tools/__tests__/explore-plugin.test.ts +374 -0
  192. package/templates/docker/src/lib/tools/__tests__/explore-sdk-compat.test.ts +82 -0
  193. package/templates/docker/src/lib/tools/__tests__/explore-sidecar.test.ts +208 -0
  194. package/templates/docker/src/lib/tools/__tests__/registry-actions.test.ts +144 -0
  195. package/templates/docker/src/lib/tools/__tests__/registry.test.ts +235 -0
  196. package/templates/docker/src/lib/tools/__tests__/salesforce-tool.test.ts +154 -0
  197. package/templates/docker/src/lib/tools/__tests__/soql-validation.test.ts +303 -0
  198. package/templates/docker/src/lib/tools/__tests__/sql-audit.test.ts +225 -0
  199. package/templates/docker/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +98 -0
  200. package/templates/docker/src/lib/tools/__tests__/sql-duckdb.test.ts +233 -0
  201. package/templates/docker/src/lib/tools/__tests__/sql-ratelimit.test.ts +225 -0
  202. package/templates/docker/src/lib/tools/__tests__/sql.test.ts +1012 -0
  203. package/templates/docker/src/lib/tools/actions/__tests__/audit.test.ts +211 -0
  204. package/templates/docker/src/lib/tools/actions/__tests__/email.test.ts +378 -0
  205. package/templates/docker/src/lib/tools/actions/__tests__/handler.test.ts +681 -0
  206. package/templates/docker/src/lib/tools/actions/__tests__/jira.test.ts +427 -0
  207. package/templates/docker/src/lib/tools/actions/audit.ts +47 -0
  208. package/templates/docker/src/lib/tools/actions/email.ts +191 -0
  209. package/templates/docker/src/lib/tools/actions/handler.ts +591 -0
  210. package/templates/docker/src/lib/tools/actions/index.ts +23 -0
  211. package/templates/docker/src/lib/tools/actions/jira.ts +220 -0
  212. package/templates/docker/src/lib/tools/explore-nsjail.ts +343 -0
  213. package/templates/docker/src/lib/tools/explore-sandbox.ts +264 -0
  214. package/templates/docker/src/lib/tools/explore-sidecar.ts +163 -0
  215. package/templates/docker/src/lib/tools/explore.ts +379 -0
  216. package/templates/docker/src/lib/tools/registry.ts +221 -0
  217. package/templates/docker/src/lib/tools/salesforce.ts +138 -0
  218. package/templates/docker/src/lib/tools/soql-validation.ts +172 -0
  219. package/templates/docker/src/lib/tools/sql.ts +680 -0
  220. package/templates/docker/src/lib/tracing.ts +40 -0
  221. package/templates/docker/src/lib/utils.ts +6 -0
  222. package/templates/docker/src/test-setup.ts +38 -0
  223. package/templates/docker/src/types/vercel-sandbox.d.ts +54 -0
  224. package/templates/docker/src/ui/components/actions/action-approval-card.tsx +295 -0
  225. package/templates/docker/src/ui/components/actions/action-status-badge.tsx +50 -0
  226. package/templates/docker/src/ui/components/admin/admin-layout.tsx +26 -0
  227. package/templates/docker/src/ui/components/admin/admin-sidebar.tsx +96 -0
  228. package/templates/docker/src/ui/components/admin/empty-state.tsx +24 -0
  229. package/templates/docker/src/ui/components/admin/entity-detail.tsx +233 -0
  230. package/templates/docker/src/ui/components/admin/entity-list.tsx +96 -0
  231. package/templates/docker/src/ui/components/admin/error-banner.tsx +22 -0
  232. package/templates/docker/src/ui/components/admin/feature-disabled.tsx +44 -0
  233. package/templates/docker/src/ui/components/admin/health-badge.tsx +30 -0
  234. package/templates/docker/src/ui/components/admin/loading-state.tsx +14 -0
  235. package/templates/docker/src/ui/components/admin/stat-card.tsx +32 -0
  236. package/templates/docker/src/ui/components/atlas-chat.tsx +370 -0
  237. package/templates/docker/src/ui/components/chart/chart-detection.ts +261 -0
  238. package/templates/docker/src/ui/components/chart/result-chart.tsx +375 -0
  239. package/templates/docker/src/ui/components/chat/api-key-bar.tsx +66 -0
  240. package/templates/docker/src/ui/components/chat/copy-button.tsx +25 -0
  241. package/templates/docker/src/ui/components/chat/data-table.tsx +102 -0
  242. package/templates/docker/src/ui/components/chat/error-banner.tsx +32 -0
  243. package/templates/docker/src/ui/components/chat/explore-card.tsx +41 -0
  244. package/templates/docker/src/ui/components/chat/loading-card.tsx +10 -0
  245. package/templates/docker/src/ui/components/chat/managed-auth-card.tsx +116 -0
  246. package/templates/docker/src/ui/components/chat/markdown.tsx +72 -0
  247. package/templates/docker/src/ui/components/chat/sql-block.tsx +30 -0
  248. package/templates/docker/src/ui/components/chat/sql-result-card.tsx +144 -0
  249. package/templates/docker/src/ui/components/chat/starter-prompts.ts +6 -0
  250. package/templates/docker/src/ui/components/chat/tool-part.tsx +40 -0
  251. package/templates/docker/src/ui/components/chat/typing-indicator.tsx +19 -0
  252. package/templates/docker/src/ui/components/conversations/conversation-item.tsx +120 -0
  253. package/templates/docker/src/ui/components/conversations/conversation-list.tsx +66 -0
  254. package/templates/docker/src/ui/components/conversations/conversation-sidebar.tsx +78 -0
  255. package/templates/docker/src/ui/components/conversations/delete-confirmation.tsx +27 -0
  256. package/templates/docker/src/ui/context.tsx +78 -0
  257. package/templates/docker/src/ui/hooks/use-admin-fetch.ts +104 -0
  258. package/templates/docker/src/ui/hooks/use-conversations.ts +184 -0
  259. package/templates/docker/src/ui/hooks/use-dark-mode.ts +17 -0
  260. package/templates/docker/src/ui/lib/action-types.ts +63 -0
  261. package/templates/docker/src/ui/lib/helpers.ts +104 -0
  262. package/templates/docker/src/ui/lib/types.ts +145 -0
  263. package/templates/docker/tsconfig.json +41 -0
  264. package/templates/docker/vercel.json +3 -0
  265. package/templates/nextjs-standalone/.env.example +68 -0
  266. package/templates/nextjs-standalone/bin/__tests__/benchmark.test.ts +598 -0
  267. package/templates/nextjs-standalone/bin/__tests__/duckdb-ingest.test.ts +171 -0
  268. package/templates/nextjs-standalone/bin/__tests__/eval.test.ts +434 -0
  269. package/templates/nextjs-standalone/bin/__tests__/matview-partition.test.ts +615 -0
  270. package/templates/nextjs-standalone/bin/__tests__/multi-source.test.ts +113 -0
  271. package/templates/nextjs-standalone/bin/__tests__/plugin-cli.test.ts +322 -0
  272. package/templates/nextjs-standalone/bin/__tests__/profiler-heuristics.test.ts +608 -0
  273. package/templates/nextjs-standalone/bin/__tests__/query.test.ts +240 -0
  274. package/templates/nextjs-standalone/bin/__tests__/schema-drift.test.ts +542 -0
  275. package/templates/nextjs-standalone/bin/__tests__/view-yaml-generation.test.ts +146 -0
  276. package/templates/nextjs-standalone/bin/atlas.ts +5044 -0
  277. package/templates/nextjs-standalone/bin/benchmark.ts +695 -0
  278. package/templates/nextjs-standalone/bin/enrich.ts +559 -0
  279. package/templates/nextjs-standalone/bin/eval.ts +770 -0
  280. package/templates/nextjs-standalone/bin/smoke.ts +438 -0
  281. package/templates/nextjs-standalone/data/.gitkeep +0 -0
  282. package/templates/nextjs-standalone/data/cybersec.sql +1961 -0
  283. package/templates/nextjs-standalone/data/demo-semantic/catalog.yml +40 -0
  284. package/templates/nextjs-standalone/data/demo-semantic/entities/accounts.yml +170 -0
  285. package/templates/nextjs-standalone/data/demo-semantic/entities/companies.yml +207 -0
  286. package/templates/nextjs-standalone/data/demo-semantic/entities/people.yml +145 -0
  287. package/templates/nextjs-standalone/data/demo-semantic/glossary.yml +22 -0
  288. package/templates/nextjs-standalone/data/demo-semantic/metrics/accounts.yml +38 -0
  289. package/templates/nextjs-standalone/data/demo-semantic/metrics/companies.yml +89 -0
  290. package/templates/nextjs-standalone/data/demo.sql +373 -0
  291. package/templates/nextjs-standalone/data/ecommerce.sql +1690 -0
  292. package/templates/nextjs-standalone/data/init-demo-db.sql +8 -0
  293. package/templates/nextjs-standalone/docs/deploy.md +390 -0
  294. package/templates/nextjs-standalone/eslint.config.mjs +18 -0
  295. package/templates/nextjs-standalone/gitignore +5 -0
  296. package/templates/nextjs-standalone/next.config.ts +10 -0
  297. package/templates/nextjs-standalone/package.json +63 -0
  298. package/templates/nextjs-standalone/postcss.config.mjs +8 -0
  299. package/templates/nextjs-standalone/semantic/catalog.yml +5 -0
  300. package/templates/nextjs-standalone/semantic/entities/.gitkeep +0 -0
  301. package/templates/nextjs-standalone/semantic/glossary.yml +6 -0
  302. package/templates/nextjs-standalone/semantic/metrics/.gitkeep +0 -0
  303. package/templates/nextjs-standalone/src/api/__tests__/actions.test.ts +683 -0
  304. package/templates/nextjs-standalone/src/api/__tests__/admin.test.ts +820 -0
  305. package/templates/nextjs-standalone/src/api/__tests__/auth.test.ts +165 -0
  306. package/templates/nextjs-standalone/src/api/__tests__/chat.test.ts +376 -0
  307. package/templates/nextjs-standalone/src/api/__tests__/conversations.test.ts +555 -0
  308. package/templates/nextjs-standalone/src/api/__tests__/cors.test.ts +135 -0
  309. package/templates/nextjs-standalone/src/api/__tests__/health-plugin.test.ts +169 -0
  310. package/templates/nextjs-standalone/src/api/__tests__/health.test.ts +261 -0
  311. package/templates/nextjs-standalone/src/api/__tests__/query.test.ts +891 -0
  312. package/templates/nextjs-standalone/src/api/__tests__/scheduled-tasks.test.ts +601 -0
  313. package/templates/nextjs-standalone/src/api/__tests__/slack.test.ts +847 -0
  314. package/templates/nextjs-standalone/src/api/index.ts +117 -0
  315. package/templates/nextjs-standalone/src/api/routes/actions.ts +274 -0
  316. package/templates/nextjs-standalone/src/api/routes/admin.ts +757 -0
  317. package/templates/nextjs-standalone/src/api/routes/auth.ts +48 -0
  318. package/templates/nextjs-standalone/src/api/routes/chat.ts +465 -0
  319. package/templates/nextjs-standalone/src/api/routes/conversations.ts +266 -0
  320. package/templates/nextjs-standalone/src/api/routes/health.ts +287 -0
  321. package/templates/nextjs-standalone/src/api/routes/openapi.ts +390 -0
  322. package/templates/nextjs-standalone/src/api/routes/query.ts +318 -0
  323. package/templates/nextjs-standalone/src/api/routes/scheduled-tasks.ts +467 -0
  324. package/templates/nextjs-standalone/src/api/routes/slack.ts +611 -0
  325. package/templates/nextjs-standalone/src/api/server.ts +226 -0
  326. package/templates/nextjs-standalone/src/app/api/[...route]/route.ts +33 -0
  327. package/templates/nextjs-standalone/src/app/error.tsx +24 -0
  328. package/templates/nextjs-standalone/src/app/global-error.tsx +68 -0
  329. package/templates/nextjs-standalone/src/app/globals.css +126 -0
  330. package/templates/nextjs-standalone/src/app/layout.tsx +19 -0
  331. package/templates/nextjs-standalone/src/app/page.tsx +14 -0
  332. package/templates/nextjs-standalone/src/lib/__tests__/agent-cache.test.ts +437 -0
  333. package/templates/nextjs-standalone/src/lib/__tests__/agent-dialect.test.ts +114 -0
  334. package/templates/nextjs-standalone/src/lib/__tests__/agent-health-annotations.test.ts +164 -0
  335. package/templates/nextjs-standalone/src/lib/__tests__/agent-integration.test.ts +514 -0
  336. package/templates/nextjs-standalone/src/lib/__tests__/config-actions.test.ts +166 -0
  337. package/templates/nextjs-standalone/src/lib/__tests__/config.test.ts +1063 -0
  338. package/templates/nextjs-standalone/src/lib/__tests__/conversations.test.ts +589 -0
  339. package/templates/nextjs-standalone/src/lib/__tests__/errors.test.ts +256 -0
  340. package/templates/nextjs-standalone/src/lib/__tests__/logger.test.ts +200 -0
  341. package/templates/nextjs-standalone/src/lib/__tests__/providers.test.ts +99 -0
  342. package/templates/nextjs-standalone/src/lib/__tests__/rls.test.ts +435 -0
  343. package/templates/nextjs-standalone/src/lib/__tests__/scheduled-task-types.test.ts +124 -0
  344. package/templates/nextjs-standalone/src/lib/__tests__/scheduled-tasks.test.ts +550 -0
  345. package/templates/nextjs-standalone/src/lib/__tests__/semantic-index.test.ts +547 -0
  346. package/templates/nextjs-standalone/src/lib/__tests__/semantic-multisource.test.ts +544 -0
  347. package/templates/nextjs-standalone/src/lib/__tests__/semantic.test.ts +363 -0
  348. package/templates/nextjs-standalone/src/lib/__tests__/startup-actions.test.ts +452 -0
  349. package/templates/nextjs-standalone/src/lib/__tests__/startup.test.ts +465 -0
  350. package/templates/nextjs-standalone/src/lib/__tests__/tracing.test.ts +28 -0
  351. package/templates/nextjs-standalone/src/lib/action-types.ts +95 -0
  352. package/templates/nextjs-standalone/src/lib/agent-query.ts +178 -0
  353. package/templates/nextjs-standalone/src/lib/agent.ts +505 -0
  354. package/templates/nextjs-standalone/src/lib/api-url.ts +3 -0
  355. package/templates/nextjs-standalone/src/lib/auth/__tests__/audit.test.ts +418 -0
  356. package/templates/nextjs-standalone/src/lib/auth/__tests__/byot-integration.test.ts +222 -0
  357. package/templates/nextjs-standalone/src/lib/auth/__tests__/byot.test.ts +366 -0
  358. package/templates/nextjs-standalone/src/lib/auth/__tests__/detect.test.ts +190 -0
  359. package/templates/nextjs-standalone/src/lib/auth/__tests__/managed.test.ts +173 -0
  360. package/templates/nextjs-standalone/src/lib/auth/__tests__/middleware.test.ts +456 -0
  361. package/templates/nextjs-standalone/src/lib/auth/__tests__/migrate.test.ts +201 -0
  362. package/templates/nextjs-standalone/src/lib/auth/__tests__/permissions.test.ts +225 -0
  363. package/templates/nextjs-standalone/src/lib/auth/__tests__/server.test.ts +34 -0
  364. package/templates/nextjs-standalone/src/lib/auth/__tests__/simple-key.test.ts +176 -0
  365. package/templates/nextjs-standalone/src/lib/auth/__tests__/types.test.ts +44 -0
  366. package/templates/nextjs-standalone/src/lib/auth/audit.ts +89 -0
  367. package/templates/nextjs-standalone/src/lib/auth/byot.ts +158 -0
  368. package/templates/nextjs-standalone/src/lib/auth/client.ts +23 -0
  369. package/templates/nextjs-standalone/src/lib/auth/detect.ts +83 -0
  370. package/templates/nextjs-standalone/src/lib/auth/managed.ts +73 -0
  371. package/templates/nextjs-standalone/src/lib/auth/middleware.ts +208 -0
  372. package/templates/nextjs-standalone/src/lib/auth/migrate.ts +111 -0
  373. package/templates/nextjs-standalone/src/lib/auth/permissions.ts +156 -0
  374. package/templates/nextjs-standalone/src/lib/auth/server.ts +142 -0
  375. package/templates/nextjs-standalone/src/lib/auth/simple-key.ts +92 -0
  376. package/templates/nextjs-standalone/src/lib/auth/types.ts +49 -0
  377. package/templates/nextjs-standalone/src/lib/config.ts +704 -0
  378. package/templates/nextjs-standalone/src/lib/conversation-types.ts +29 -0
  379. package/templates/nextjs-standalone/src/lib/conversations.ts +270 -0
  380. package/templates/nextjs-standalone/src/lib/db/__tests__/connection.test.ts +69 -0
  381. package/templates/nextjs-standalone/src/lib/db/__tests__/duckdb.test.ts +141 -0
  382. package/templates/nextjs-standalone/src/lib/db/__tests__/internal.test.ts +387 -0
  383. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-health.test.ts +207 -0
  384. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-pool-limits.test.ts +156 -0
  385. package/templates/nextjs-standalone/src/lib/db/__tests__/registry.test.ts +595 -0
  386. package/templates/nextjs-standalone/src/lib/db/__tests__/salesforce.test.ts +339 -0
  387. package/templates/nextjs-standalone/src/lib/db/__tests__/snowflake.test.ts +217 -0
  388. package/templates/nextjs-standalone/src/lib/db/__tests__/source-rate-limit.test.ts +130 -0
  389. package/templates/nextjs-standalone/src/lib/db/connection.ts +753 -0
  390. package/templates/nextjs-standalone/src/lib/db/duckdb.ts +122 -0
  391. package/templates/nextjs-standalone/src/lib/db/internal.ts +273 -0
  392. package/templates/nextjs-standalone/src/lib/db/salesforce.ts +342 -0
  393. package/templates/nextjs-standalone/src/lib/db/source-rate-limit.ts +191 -0
  394. package/templates/nextjs-standalone/src/lib/errors.ts +154 -0
  395. package/templates/nextjs-standalone/src/lib/logger.ts +98 -0
  396. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks-integration.test.ts +202 -0
  397. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks.test.ts +529 -0
  398. package/templates/nextjs-standalone/src/lib/plugins/__tests__/migrate.test.ts +521 -0
  399. package/templates/nextjs-standalone/src/lib/plugins/__tests__/registry.test.ts +346 -0
  400. package/templates/nextjs-standalone/src/lib/plugins/__tests__/tools.test.ts +49 -0
  401. package/templates/nextjs-standalone/src/lib/plugins/__tests__/wiring.test.ts +585 -0
  402. package/templates/nextjs-standalone/src/lib/plugins/hooks.ts +162 -0
  403. package/templates/nextjs-standalone/src/lib/plugins/index.ts +9 -0
  404. package/templates/nextjs-standalone/src/lib/plugins/migrate.ts +309 -0
  405. package/templates/nextjs-standalone/src/lib/plugins/registry.ts +231 -0
  406. package/templates/nextjs-standalone/src/lib/plugins/tools.ts +39 -0
  407. package/templates/nextjs-standalone/src/lib/plugins/wiring.ts +291 -0
  408. package/templates/nextjs-standalone/src/lib/providers.ts +102 -0
  409. package/templates/nextjs-standalone/src/lib/rls.ts +321 -0
  410. package/templates/nextjs-standalone/src/lib/scheduled-task-types.ts +132 -0
  411. package/templates/nextjs-standalone/src/lib/scheduled-tasks.ts +475 -0
  412. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/delivery.test.ts +192 -0
  413. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/engine.test.ts +248 -0
  414. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-email.test.ts +96 -0
  415. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-slack.test.ts +78 -0
  416. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-webhook.test.ts +78 -0
  417. package/templates/nextjs-standalone/src/lib/scheduler/delivery.ts +248 -0
  418. package/templates/nextjs-standalone/src/lib/scheduler/engine.ts +317 -0
  419. package/templates/nextjs-standalone/src/lib/scheduler/executor.ts +73 -0
  420. package/templates/nextjs-standalone/src/lib/scheduler/format-email.ts +109 -0
  421. package/templates/nextjs-standalone/src/lib/scheduler/format-slack.ts +35 -0
  422. package/templates/nextjs-standalone/src/lib/scheduler/format-webhook.ts +37 -0
  423. package/templates/nextjs-standalone/src/lib/scheduler/index.ts +7 -0
  424. package/templates/nextjs-standalone/src/lib/security.ts +11 -0
  425. package/templates/nextjs-standalone/src/lib/semantic-index.ts +503 -0
  426. package/templates/nextjs-standalone/src/lib/semantic.ts +387 -0
  427. package/templates/nextjs-standalone/src/lib/sidecar-types.ts +16 -0
  428. package/templates/nextjs-standalone/src/lib/slack/__tests__/api.test.ts +160 -0
  429. package/templates/nextjs-standalone/src/lib/slack/__tests__/format.test.ts +237 -0
  430. package/templates/nextjs-standalone/src/lib/slack/__tests__/store.test.ts +188 -0
  431. package/templates/nextjs-standalone/src/lib/slack/__tests__/threads.test.ts +112 -0
  432. package/templates/nextjs-standalone/src/lib/slack/__tests__/verify.test.ts +111 -0
  433. package/templates/nextjs-standalone/src/lib/slack/api.ts +102 -0
  434. package/templates/nextjs-standalone/src/lib/slack/format.ts +209 -0
  435. package/templates/nextjs-standalone/src/lib/slack/store.ts +107 -0
  436. package/templates/nextjs-standalone/src/lib/slack/threads.ts +64 -0
  437. package/templates/nextjs-standalone/src/lib/slack/verify.ts +71 -0
  438. package/templates/nextjs-standalone/src/lib/startup.ts +730 -0
  439. package/templates/nextjs-standalone/src/lib/tools/__tests__/action-permissions.test.ts +594 -0
  440. package/templates/nextjs-standalone/src/lib/tools/__tests__/custom-validation.test.ts +238 -0
  441. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-backend.test.ts +267 -0
  442. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-nsjail.test.ts +492 -0
  443. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-plugin.test.ts +374 -0
  444. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sdk-compat.test.ts +82 -0
  445. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sidecar.test.ts +208 -0
  446. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry-actions.test.ts +144 -0
  447. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry.test.ts +235 -0
  448. package/templates/nextjs-standalone/src/lib/tools/__tests__/salesforce-tool.test.ts +154 -0
  449. package/templates/nextjs-standalone/src/lib/tools/__tests__/soql-validation.test.ts +303 -0
  450. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-audit.test.ts +225 -0
  451. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +98 -0
  452. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-duckdb.test.ts +233 -0
  453. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-ratelimit.test.ts +225 -0
  454. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql.test.ts +1012 -0
  455. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/audit.test.ts +211 -0
  456. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/email.test.ts +378 -0
  457. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/handler.test.ts +681 -0
  458. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/jira.test.ts +427 -0
  459. package/templates/nextjs-standalone/src/lib/tools/actions/audit.ts +47 -0
  460. package/templates/nextjs-standalone/src/lib/tools/actions/email.ts +191 -0
  461. package/templates/nextjs-standalone/src/lib/tools/actions/handler.ts +591 -0
  462. package/templates/nextjs-standalone/src/lib/tools/actions/index.ts +23 -0
  463. package/templates/nextjs-standalone/src/lib/tools/actions/jira.ts +220 -0
  464. package/templates/nextjs-standalone/src/lib/tools/explore-nsjail.ts +343 -0
  465. package/templates/nextjs-standalone/src/lib/tools/explore-sandbox.ts +264 -0
  466. package/templates/nextjs-standalone/src/lib/tools/explore-sidecar.ts +163 -0
  467. package/templates/nextjs-standalone/src/lib/tools/explore.ts +379 -0
  468. package/templates/nextjs-standalone/src/lib/tools/registry.ts +221 -0
  469. package/templates/nextjs-standalone/src/lib/tools/salesforce.ts +138 -0
  470. package/templates/nextjs-standalone/src/lib/tools/soql-validation.ts +172 -0
  471. package/templates/nextjs-standalone/src/lib/tools/sql.ts +680 -0
  472. package/templates/nextjs-standalone/src/lib/tracing.ts +40 -0
  473. package/templates/nextjs-standalone/src/lib/utils.ts +6 -0
  474. package/templates/nextjs-standalone/src/test-setup.ts +38 -0
  475. package/templates/nextjs-standalone/src/ui/components/actions/action-approval-card.tsx +295 -0
  476. package/templates/nextjs-standalone/src/ui/components/actions/action-status-badge.tsx +50 -0
  477. package/templates/nextjs-standalone/src/ui/components/admin/admin-layout.tsx +26 -0
  478. package/templates/nextjs-standalone/src/ui/components/admin/admin-sidebar.tsx +96 -0
  479. package/templates/nextjs-standalone/src/ui/components/admin/empty-state.tsx +24 -0
  480. package/templates/nextjs-standalone/src/ui/components/admin/entity-detail.tsx +233 -0
  481. package/templates/nextjs-standalone/src/ui/components/admin/entity-list.tsx +96 -0
  482. package/templates/nextjs-standalone/src/ui/components/admin/error-banner.tsx +22 -0
  483. package/templates/nextjs-standalone/src/ui/components/admin/feature-disabled.tsx +44 -0
  484. package/templates/nextjs-standalone/src/ui/components/admin/health-badge.tsx +30 -0
  485. package/templates/nextjs-standalone/src/ui/components/admin/loading-state.tsx +14 -0
  486. package/templates/nextjs-standalone/src/ui/components/admin/stat-card.tsx +32 -0
  487. package/templates/nextjs-standalone/src/ui/components/atlas-chat.tsx +370 -0
  488. package/templates/nextjs-standalone/src/ui/components/chart/chart-detection.ts +261 -0
  489. package/templates/nextjs-standalone/src/ui/components/chart/result-chart.tsx +375 -0
  490. package/templates/nextjs-standalone/src/ui/components/chat/api-key-bar.tsx +66 -0
  491. package/templates/nextjs-standalone/src/ui/components/chat/copy-button.tsx +25 -0
  492. package/templates/nextjs-standalone/src/ui/components/chat/data-table.tsx +102 -0
  493. package/templates/nextjs-standalone/src/ui/components/chat/error-banner.tsx +32 -0
  494. package/templates/nextjs-standalone/src/ui/components/chat/explore-card.tsx +41 -0
  495. package/templates/nextjs-standalone/src/ui/components/chat/loading-card.tsx +10 -0
  496. package/templates/nextjs-standalone/src/ui/components/chat/managed-auth-card.tsx +116 -0
  497. package/templates/nextjs-standalone/src/ui/components/chat/markdown.tsx +72 -0
  498. package/templates/nextjs-standalone/src/ui/components/chat/sql-block.tsx +30 -0
  499. package/templates/nextjs-standalone/src/ui/components/chat/sql-result-card.tsx +144 -0
  500. package/templates/nextjs-standalone/src/ui/components/chat/starter-prompts.ts +6 -0
  501. package/templates/nextjs-standalone/src/ui/components/chat/tool-part.tsx +40 -0
  502. package/templates/nextjs-standalone/src/ui/components/chat/typing-indicator.tsx +19 -0
  503. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-item.tsx +120 -0
  504. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-list.tsx +66 -0
  505. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-sidebar.tsx +78 -0
  506. package/templates/nextjs-standalone/src/ui/components/conversations/delete-confirmation.tsx +27 -0
  507. package/templates/nextjs-standalone/src/ui/context.tsx +78 -0
  508. package/templates/nextjs-standalone/src/ui/hooks/use-admin-fetch.ts +104 -0
  509. package/templates/nextjs-standalone/src/ui/hooks/use-conversations.ts +184 -0
  510. package/templates/nextjs-standalone/src/ui/hooks/use-dark-mode.ts +17 -0
  511. package/templates/nextjs-standalone/src/ui/lib/action-types.ts +63 -0
  512. package/templates/nextjs-standalone/src/ui/lib/helpers.ts +104 -0
  513. package/templates/nextjs-standalone/src/ui/lib/types.ts +145 -0
  514. package/templates/nextjs-standalone/tsconfig.json +32 -0
  515. package/templates/nextjs-standalone/vercel.json +4 -0
@@ -0,0 +1,1012 @@
1
+ import { describe, expect, it, beforeEach, mock } from "bun:test";
2
+
3
+ // Mock getWhitelistedTables before importing the module under test.
4
+ // This avoids hitting the filesystem for semantic layer YAML files.
5
+ // The whitelist includes both unqualified names (e.g. "companies") for default-schema
6
+ // queries and qualified names (e.g. "public.companies", "analytics.companies") for
7
+ // explicit schema-qualified queries. This mirrors real usage where atlas init adds
8
+ // qualified entries for non-public schemas.
9
+ mock.module("@atlas/api/lib/semantic", () => ({
10
+ getWhitelistedTables: () =>
11
+ new Set(["companies", "people", "accounts", "public.companies", "analytics.companies", "orders"]),
12
+ _resetWhitelists: () => {},
13
+ }));
14
+
15
+ // Mock the DB connection — validateSQL doesn't need it, but the module
16
+ // imports it at the top level.
17
+ const mockDBConnection = {
18
+ query: async () => ({ columns: [], rows: [] }),
19
+ close: async () => {},
20
+ };
21
+
22
+ const mockDetectDBType = () => {
23
+ const url = process.env.ATLAS_DATASOURCE_URL ?? "";
24
+ if (url.startsWith("postgresql://") || url.startsWith("postgres://")) {
25
+ return "postgres";
26
+ }
27
+ if (url.startsWith("mysql://") || url.startsWith("mysql2://")) {
28
+ return "mysql";
29
+ }
30
+ if (url.startsWith("clickhouse://")) {
31
+ return "clickhouse";
32
+ }
33
+ if (url.startsWith("snowflake://")) {
34
+ return "snowflake";
35
+ }
36
+ if (url.startsWith("duckdb://")) {
37
+ return "duckdb";
38
+ }
39
+ if (url.startsWith("salesforce://")) {
40
+ return "salesforce";
41
+ }
42
+ throw new Error(`Unsupported database URL: "${url.slice(0, 40)}…".`);
43
+ };
44
+
45
+ mock.module("@atlas/api/lib/db/connection", () => ({
46
+ getDB: () => mockDBConnection,
47
+ connections: {
48
+ get: () => mockDBConnection,
49
+ getDefault: () => mockDBConnection,
50
+ getDBType: () => mockDetectDBType(),
51
+ getValidator: () => undefined,
52
+ list: () => ["default"],
53
+ },
54
+ detectDBType: mockDetectDBType,
55
+ }));
56
+
57
+ // Import after mocks are registered
58
+ const { validateSQL } = await import("@atlas/api/lib/tools/sql");
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Helpers
62
+ // ---------------------------------------------------------------------------
63
+
64
+ function expectValid(sql: string) {
65
+ const result = validateSQL(sql);
66
+ expect(result.valid).toBe(true);
67
+ }
68
+
69
+ function expectInvalid(sql: string, messageSubstring?: string) {
70
+ const result = validateSQL(sql);
71
+ expect(result.valid).toBe(false);
72
+ expect(result.error).toBeDefined();
73
+ if (messageSubstring) {
74
+ expect(result.error!.toLowerCase()).toContain(
75
+ messageSubstring.toLowerCase()
76
+ );
77
+ }
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Tests
82
+ // ---------------------------------------------------------------------------
83
+
84
+ describe("validateSQL", () => {
85
+ // Reset env between tests that tweak ATLAS_TABLE_WHITELIST.
86
+ // Force PostgreSQL mode — existing tests were written for it.
87
+ const origEnv = { ...process.env, ATLAS_DATASOURCE_URL: "postgresql://test:test@localhost:5432/test" };
88
+ beforeEach(() => {
89
+ process.env = { ...origEnv };
90
+ });
91
+
92
+ // ----- Valid SELECT queries ------------------------------------------------
93
+
94
+ describe("valid SELECT queries", () => {
95
+ it("accepts a simple SELECT", () => {
96
+ expectValid("SELECT id, name FROM companies");
97
+ });
98
+
99
+ it("accepts SELECT *", () => {
100
+ expectValid("SELECT * FROM companies");
101
+ });
102
+
103
+ it("accepts SELECT with WHERE", () => {
104
+ expectValid("SELECT id FROM companies WHERE name = 'Acme'");
105
+ });
106
+
107
+ it("accepts SELECT with JOIN", () => {
108
+ expectValid(
109
+ "SELECT c.name, p.name FROM companies c JOIN people p ON c.id = p.company_id"
110
+ );
111
+ });
112
+
113
+ it("accepts LEFT JOIN", () => {
114
+ expectValid(
115
+ "SELECT c.name, a.type FROM companies c LEFT JOIN accounts a ON c.id = a.company_id"
116
+ );
117
+ });
118
+
119
+ it("accepts subqueries", () => {
120
+ expectValid(
121
+ "SELECT * FROM companies WHERE id IN (SELECT company_id FROM people)"
122
+ );
123
+ });
124
+
125
+ it("accepts CTEs (WITH ... AS)", () => {
126
+ expectValid(
127
+ "WITH top AS (SELECT id, name FROM companies LIMIT 10) SELECT * FROM top"
128
+ );
129
+ });
130
+
131
+ it("accepts aggregate functions", () => {
132
+ expectValid(
133
+ "SELECT COUNT(*), SUM(id), AVG(id), MIN(id), MAX(id) FROM companies"
134
+ );
135
+ });
136
+
137
+ it("accepts GROUP BY and HAVING", () => {
138
+ expectValid(
139
+ "SELECT name, COUNT(*) as cnt FROM companies GROUP BY name HAVING COUNT(*) > 1"
140
+ );
141
+ });
142
+
143
+ it("accepts window functions", () => {
144
+ expectValid(
145
+ "SELECT name, ROW_NUMBER() OVER (ORDER BY id) FROM companies"
146
+ );
147
+ });
148
+
149
+ it("accepts CASE expressions", () => {
150
+ expectValid(
151
+ "SELECT CASE WHEN id > 10 THEN 'big' ELSE 'small' END AS size FROM companies"
152
+ );
153
+ });
154
+
155
+ it("accepts ORDER BY and LIMIT", () => {
156
+ expectValid("SELECT * FROM companies ORDER BY name ASC LIMIT 50");
157
+ });
158
+
159
+ it("accepts DISTINCT", () => {
160
+ expectValid("SELECT DISTINCT name FROM companies");
161
+ });
162
+
163
+ it("accepts UNION of SELECTs", () => {
164
+ expectValid(
165
+ "SELECT name FROM companies UNION ALL SELECT name FROM people"
166
+ );
167
+ });
168
+
169
+ it("accepts trailing semicolon (stripped)", () => {
170
+ expectValid("SELECT 1 ;");
171
+ });
172
+
173
+ it("accepts trailing semicolon with whitespace", () => {
174
+ expectValid("SELECT 1 ; ");
175
+ });
176
+
177
+ it("accepts COALESCE", () => {
178
+ expectValid("SELECT COALESCE(name, 'unknown') FROM companies");
179
+ });
180
+
181
+ it("accepts nested CTEs", () => {
182
+ expectValid(`
183
+ WITH a AS (SELECT id FROM companies),
184
+ b AS (SELECT id FROM a)
185
+ SELECT * FROM b
186
+ `);
187
+ });
188
+
189
+ it("accepts semicolons inside string literals", () => {
190
+ // Semicolons in data values are legitimate — the AST parser handles
191
+ // them correctly as part of the string, not as statement separators.
192
+ expectValid("SELECT * FROM companies WHERE name = 'foo;bar'");
193
+ });
194
+ });
195
+
196
+ // ----- Blocked DML/DDL ----------------------------------------------------
197
+
198
+ describe("blocked DML/DDL", () => {
199
+ it("rejects INSERT", () => {
200
+ expectInvalid(
201
+ "INSERT INTO companies (name) VALUES ('Evil')",
202
+ "forbidden"
203
+ );
204
+ });
205
+
206
+ it("rejects UPDATE", () => {
207
+ expectInvalid("UPDATE companies SET name = 'Evil'", "forbidden");
208
+ });
209
+
210
+ it("rejects DELETE", () => {
211
+ expectInvalid("DELETE FROM companies WHERE id = 1", "forbidden");
212
+ });
213
+
214
+ it("rejects DROP TABLE", () => {
215
+ expectInvalid("DROP TABLE companies", "forbidden");
216
+ });
217
+
218
+ it("rejects CREATE TABLE", () => {
219
+ expectInvalid(
220
+ "CREATE TABLE evil (id int)",
221
+ "forbidden"
222
+ );
223
+ });
224
+
225
+ it("rejects ALTER TABLE", () => {
226
+ expectInvalid("ALTER TABLE companies ADD COLUMN evil text", "forbidden");
227
+ });
228
+
229
+ it("rejects TRUNCATE", () => {
230
+ expectInvalid("TRUNCATE companies", "forbidden");
231
+ });
232
+ });
233
+
234
+ // ----- Blocked privilege / admin commands -----------------------------------
235
+
236
+ describe("blocked privilege and admin commands", () => {
237
+ it("rejects GRANT", () => {
238
+ expectInvalid("GRANT ALL ON companies TO evil", "forbidden");
239
+ });
240
+
241
+ it("rejects REVOKE", () => {
242
+ expectInvalid("REVOKE ALL ON companies FROM evil", "forbidden");
243
+ });
244
+
245
+ it("rejects EXEC", () => {
246
+ expectInvalid("EXEC sp_executesql N'DROP TABLE companies'", "forbidden");
247
+ });
248
+
249
+ it("rejects EXECUTE", () => {
250
+ expectInvalid("EXECUTE sp_addrolemember 'db_owner', 'evil'", "forbidden");
251
+ });
252
+
253
+ it("rejects CALL", () => {
254
+ expectInvalid("CALL some_procedure()", "forbidden");
255
+ });
256
+
257
+ it("rejects COPY", () => {
258
+ expectInvalid("COPY companies TO '/tmp/data.csv'", "forbidden");
259
+ });
260
+
261
+ it("rejects LOAD", () => {
262
+ expectInvalid("LOAD DATA INFILE '/tmp/data.csv' INTO TABLE companies", "forbidden");
263
+ });
264
+
265
+ it("rejects VACUUM", () => {
266
+ expectInvalid("VACUUM companies", "forbidden");
267
+ });
268
+
269
+ it("rejects REINDEX", () => {
270
+ expectInvalid("REINDEX TABLE companies", "forbidden");
271
+ });
272
+
273
+ it("rejects OPTIMIZE TABLE", () => {
274
+ expectInvalid("OPTIMIZE TABLE companies", "forbidden");
275
+ });
276
+
277
+ it("rejects INTO OUTFILE", () => {
278
+ expectInvalid(
279
+ "SELECT * INTO OUTFILE '/tmp/dump.csv' FROM companies",
280
+ "forbidden"
281
+ );
282
+ });
283
+ });
284
+
285
+ // ----- Multi-statement injection -------------------------------------------
286
+
287
+ describe("multi-statement injection", () => {
288
+ it("rejects semicolon-separated SELECT + DML", () => {
289
+ // DML caught by regex guard before AST even runs
290
+ expectInvalid("SELECT 1; DROP TABLE companies", "forbidden");
291
+ });
292
+
293
+ it("rejects two SELECT statements", () => {
294
+ expectInvalid("SELECT 1; SELECT 2", "multiple statements");
295
+ });
296
+
297
+ it("rejects two statements with string literals", () => {
298
+ expectInvalid("SELECT '1'; SELECT '2'", "multiple statements");
299
+ });
300
+ });
301
+
302
+ // ----- Comment-based bypass attempts ---------------------------------------
303
+
304
+ describe("comment-based bypass attempts", () => {
305
+ it("rejects DML hidden after block comment", () => {
306
+ expectInvalid(
307
+ "DROP /* harmless */ TABLE companies",
308
+ "forbidden"
309
+ );
310
+ });
311
+
312
+ it("rejects DML with inline comment", () => {
313
+ expectInvalid(
314
+ "DELETE -- just a select\nFROM companies",
315
+ "forbidden"
316
+ );
317
+ });
318
+
319
+ it("allows SELECT with harmless comments", () => {
320
+ // The regex guard passes (no forbidden keywords) and the parser
321
+ // handles inline comments, so this is valid.
322
+ expectValid("SELECT /* this is a comment */ 1");
323
+ });
324
+ });
325
+
326
+ // ----- AST parse failure (reject unparseable) ------------------------------
327
+
328
+ describe("AST parse failure — reject unparseable", () => {
329
+ it("rejects SELECT ... FOR UPDATE (locking clause)", () => {
330
+ // FOR UPDATE acquires row-level locks, violating read-only intent.
331
+ // Caught by the regex guard because "UPDATE" is a forbidden keyword.
332
+ expectInvalid(
333
+ "SELECT * FROM companies FOR UPDATE",
334
+ "forbidden"
335
+ );
336
+ });
337
+
338
+ it("rejects completely unparseable gibberish", () => {
339
+ expectInvalid(
340
+ "XYZZY PLUGH 42",
341
+ "could not be parsed"
342
+ );
343
+ });
344
+
345
+ it("rejects partial SQL that confuses the parser", () => {
346
+ expectInvalid(
347
+ "SELECT FROM WHERE",
348
+ "could not be parsed"
349
+ );
350
+ });
351
+ });
352
+
353
+ // ----- Table whitelist -----------------------------------------------------
354
+
355
+ describe("table whitelist", () => {
356
+ it("allows queries on whitelisted tables", () => {
357
+ expectValid("SELECT * FROM companies");
358
+ });
359
+
360
+ it("allows queries joining whitelisted tables", () => {
361
+ expectValid(
362
+ "SELECT c.name FROM companies c JOIN people p ON c.id = p.company_id"
363
+ );
364
+ });
365
+
366
+ it("rejects queries on non-whitelisted tables", () => {
367
+ expectInvalid(
368
+ "SELECT * FROM secret_data",
369
+ "not in the allowed list"
370
+ );
371
+ });
372
+
373
+ it("rejects when any joined table is not whitelisted", () => {
374
+ expectInvalid(
375
+ "SELECT * FROM companies c JOIN secret_data s ON c.id = s.company_id",
376
+ "not in the allowed list"
377
+ );
378
+ });
379
+
380
+ it("rejects non-whitelisted tables in subqueries", () => {
381
+ expectInvalid(
382
+ "SELECT * FROM companies WHERE id IN (SELECT id FROM secret_data)",
383
+ "not in the allowed list"
384
+ );
385
+ });
386
+
387
+ it("does not reject CTE names as non-whitelisted tables", () => {
388
+ // "my_temp" is not in the whitelist, but it's a CTE name —
389
+ // the validator extracts CTE names and excludes them from the check.
390
+ expectValid(
391
+ "WITH my_temp AS (SELECT id FROM companies) SELECT * FROM my_temp"
392
+ );
393
+ });
394
+
395
+ it("allows all tables when whitelist is disabled", () => {
396
+ process.env.ATLAS_TABLE_WHITELIST = "false";
397
+ expectValid("SELECT * FROM anything_goes");
398
+ });
399
+ });
400
+
401
+ // ----- Schema-qualified table names ----------------------------------------
402
+
403
+ describe("schema-qualified table names", () => {
404
+ it("accepts schema-qualified name when in whitelist", () => {
405
+ expectValid("SELECT * FROM public.companies");
406
+ });
407
+
408
+ it("accepts non-public schema-qualified name when in whitelist", () => {
409
+ expectValid("SELECT * FROM analytics.companies");
410
+ });
411
+
412
+ it("rejects schema-qualified name when schema.table not in whitelist", () => {
413
+ expectInvalid(
414
+ "SELECT * FROM secret.passwords",
415
+ "not in the allowed list"
416
+ );
417
+ });
418
+
419
+ it("accepts joins mixing qualified and unqualified names", () => {
420
+ expectValid(
421
+ "SELECT c.name, o.id FROM public.companies c JOIN orders o ON c.id = o.company_id"
422
+ );
423
+ });
424
+
425
+ it("rejects schema-qualified table when only unqualified name is whitelisted (whitelist bypass)", () => {
426
+ // "companies" (unqualified) is whitelisted, but "secret.companies" is NOT.
427
+ // This must be rejected to prevent cross-schema access.
428
+ expectInvalid(
429
+ "SELECT * FROM secret.companies",
430
+ "not in the allowed list"
431
+ );
432
+ });
433
+
434
+ it("does not show 'null.' prefix in error messages for unqualified tables", () => {
435
+ const result = validateSQL("SELECT * FROM nonexistent_table");
436
+ expect(result.valid).toBe(false);
437
+ expect(result.error).not.toContain("null.");
438
+ expect(result.error).toContain("nonexistent_table");
439
+ });
440
+
441
+ it("accepts case-insensitive schema-qualified names (PUBLIC.Companies)", () => {
442
+ expectValid("SELECT * FROM PUBLIC.Companies");
443
+ });
444
+
445
+ it("accepts case-insensitive schema-qualified names (ANALYTICS.COMPANIES)", () => {
446
+ expectValid("SELECT * FROM ANALYTICS.COMPANIES");
447
+ });
448
+ });
449
+
450
+ // ----- Auto-LIMIT ---------------------------------------------------------
451
+
452
+ describe("auto-LIMIT", () => {
453
+ // Auto-LIMIT is applied in the tool's execute function, not in validateSQL.
454
+ // These tests verify that validateSQL itself doesn't reject queries with
455
+ // or without LIMIT — the limit logic is tested separately.
456
+
457
+ it("accepts queries with explicit LIMIT", () => {
458
+ expectValid("SELECT * FROM companies LIMIT 10");
459
+ });
460
+
461
+ it("accepts queries without LIMIT (auto-appended later)", () => {
462
+ expectValid("SELECT * FROM companies");
463
+ });
464
+ });
465
+
466
+ // ----- Edge cases ----------------------------------------------------------
467
+
468
+ describe("edge cases", () => {
469
+ it("rejects empty string", () => {
470
+ const result = validateSQL("");
471
+ expect(result.valid).toBe(false);
472
+ });
473
+
474
+ it("rejects whitespace-only", () => {
475
+ const result = validateSQL(" \n\t ");
476
+ expect(result.valid).toBe(false);
477
+ });
478
+
479
+ it("accepts extremely long queries without crashing", () => {
480
+ const longQuery = `SELECT ${Array(500).fill("1").join(", ")} FROM companies`;
481
+ expectValid(longQuery);
482
+ });
483
+
484
+ it("handles unicode in string literals", () => {
485
+ expectValid("SELECT * FROM companies WHERE name = '日本語テスト'");
486
+ });
487
+
488
+ it("handles unicode table names that aren't whitelisted", () => {
489
+ expectInvalid('SELECT * FROM "données_secrètes"');
490
+ });
491
+
492
+ it("rejects a query that is just a number", () => {
493
+ const result = validateSQL("42");
494
+ expect(result.valid).toBe(false);
495
+ });
496
+
497
+ it("case-insensitive detection of forbidden keywords", () => {
498
+ expectInvalid("insert INTO companies (name) VALUES ('x')", "forbidden");
499
+ expectInvalid("InSeRt INTO companies (name) VALUES ('x')", "forbidden");
500
+ });
501
+
502
+ it("rejects UPDATE disguised with mixed case", () => {
503
+ expectInvalid("uPdAtE companies SET name = 'x'", "forbidden");
504
+ });
505
+
506
+ it("rejects forbidden keywords inside string literals (known false positive)", () => {
507
+ // The regex guard runs before AST parsing, so it catches "DELETE"
508
+ // even inside a WHERE string value. This is a deliberate conservative
509
+ // choice — security over usability. The agent can work around this by
510
+ // using different filter values or column aliases.
511
+ expectInvalid(
512
+ "SELECT * FROM companies WHERE status = 'DELETE'",
513
+ "forbidden"
514
+ );
515
+ });
516
+ });
517
+
518
+ // ----- Known limitations ---------------------------------------------------
519
+
520
+ describe("known limitations", () => {
521
+ it("does not block dangerous PostgreSQL functions (mitigated by statement_timeout and DB permissions)", () => {
522
+ // Functions like pg_sleep, pg_read_file, pg_terminate_backend pass
523
+ // validation because there is no function blocklist. Mitigation:
524
+ // - pg_sleep: bounded by statement_timeout (default 30s)
525
+ // - pg_read_file/pg_ls_dir: require superuser or explicit GRANT
526
+ // - pg_terminate_backend: requires pg_signal_backend role
527
+ // The DB user should have minimal permissions in production.
528
+ expectValid("SELECT pg_sleep(1) FROM companies");
529
+ });
530
+ });
531
+
532
+ // ----- MySQL-specific validation --------------------------------------------
533
+
534
+ describe("MySQL-specific", () => {
535
+ beforeEach(() => {
536
+ process.env.ATLAS_DATASOURCE_URL = "mysql://test:test@localhost:3306/test";
537
+ });
538
+
539
+ it("rejects HANDLER statement", () => {
540
+ expectInvalid("HANDLER companies OPEN", "forbidden");
541
+ });
542
+
543
+ it("rejects SHOW TABLES", () => {
544
+ expectInvalid("SHOW TABLES", "forbidden");
545
+ });
546
+
547
+ it("rejects SHOW DATABASES", () => {
548
+ expectInvalid("SHOW DATABASES", "forbidden");
549
+ });
550
+
551
+ it("rejects DESCRIBE", () => {
552
+ expectInvalid("DESCRIBE companies", "forbidden");
553
+ });
554
+
555
+ it("rejects EXPLAIN", () => {
556
+ expectInvalid("EXPLAIN SELECT * FROM companies", "forbidden");
557
+ });
558
+
559
+ it("rejects LOAD DATA", () => {
560
+ expectInvalid(
561
+ "LOAD DATA INFILE '/tmp/data.csv' INTO TABLE companies",
562
+ "forbidden"
563
+ );
564
+ });
565
+
566
+ it("rejects LOAD XML", () => {
567
+ expectInvalid(
568
+ "LOAD XML INFILE '/tmp/data.xml' INTO TABLE companies",
569
+ "forbidden"
570
+ );
571
+ });
572
+
573
+ it("rejects USE database", () => {
574
+ expectInvalid("USE other_database", "forbidden");
575
+ });
576
+
577
+ it("rejects mixed-case HANDLER", () => {
578
+ expectInvalid("HaNdLeR companies OPEN", "forbidden");
579
+ });
580
+ });
581
+
582
+ // ----- MySQL parser mode ---------------------------------------------------
583
+
584
+ describe("MySQL parser mode", () => {
585
+ beforeEach(() => {
586
+ process.env.ATLAS_DATASOURCE_URL = "mysql://test:test@localhost:3306/test";
587
+ });
588
+
589
+ it("accepts a simple SELECT in MySQL mode", () => {
590
+ expectValid("SELECT id, name FROM companies");
591
+ });
592
+
593
+ it("accepts SELECT with JOIN in MySQL mode", () => {
594
+ expectValid(
595
+ "SELECT c.name, p.name FROM companies c JOIN people p ON c.id = p.company_id"
596
+ );
597
+ });
598
+
599
+ it("accepts CTEs in MySQL mode", () => {
600
+ expectValid(
601
+ "WITH top AS (SELECT id, name FROM companies LIMIT 10) SELECT * FROM top"
602
+ );
603
+ });
604
+
605
+ it("accepts subqueries in MySQL mode", () => {
606
+ expectValid(
607
+ "SELECT * FROM companies WHERE id IN (SELECT company_id FROM people)"
608
+ );
609
+ });
610
+
611
+ it("rejects INSERT in MySQL mode", () => {
612
+ expectInvalid(
613
+ "INSERT INTO companies (name) VALUES ('Evil')",
614
+ "forbidden"
615
+ );
616
+ });
617
+
618
+ it("rejects non-whitelisted tables in MySQL mode", () => {
619
+ expectInvalid(
620
+ "SELECT * FROM secret_data",
621
+ "not in the allowed list"
622
+ );
623
+ });
624
+
625
+ it("accepts MySQL-specific functions (DATE_FORMAT) in MySQL mode", () => {
626
+ expectValid("SELECT DATE_FORMAT(NOW(), '%Y-%m') as month FROM companies");
627
+ });
628
+
629
+ it("accepts MySQL-specific functions (IFNULL) in MySQL mode", () => {
630
+ expectValid("SELECT IFNULL(name, 'unknown') FROM companies");
631
+ });
632
+
633
+ it("accepts MySQL-specific functions (GROUP_CONCAT) in MySQL mode", () => {
634
+ expectValid("SELECT GROUP_CONCAT(name SEPARATOR ', ') FROM companies");
635
+ });
636
+
637
+ it("accepts backtick-quoted identifiers in MySQL mode", () => {
638
+ expectValid("SELECT `id`, `name` FROM companies");
639
+ });
640
+
641
+ it("does not reject CTE names as non-whitelisted tables in MySQL mode", () => {
642
+ expectValid(
643
+ "WITH my_temp AS (SELECT id FROM companies) SELECT * FROM my_temp"
644
+ );
645
+ });
646
+
647
+ it("accepts UNION of SELECTs in MySQL mode", () => {
648
+ expectValid(
649
+ "SELECT name FROM companies UNION ALL SELECT name FROM people"
650
+ );
651
+ });
652
+ });
653
+
654
+ // ----- Cross-DB guard: MySQL patterns don't fire in PostgreSQL mode ---------
655
+
656
+ describe("cross-database regex guard isolation", () => {
657
+ it("rejects HANDLER in PostgreSQL mode via AST parser, not regex guard", () => {
658
+ process.env.ATLAS_DATASOURCE_URL = "postgresql://test:test@localhost:5432/test";
659
+ const result = validateSQL("HANDLER companies OPEN");
660
+ expect(result.valid).toBe(false);
661
+ expect(result.error).not.toContain("Forbidden SQL operation detected");
662
+ });
663
+
664
+ it("rejects SHOW in PostgreSQL mode via AST parser, not regex guard", () => {
665
+ process.env.ATLAS_DATASOURCE_URL = "postgresql://test:test@localhost:5432/test";
666
+ const result = validateSQL("SHOW TABLES");
667
+ expect(result.valid).toBe(false);
668
+ expect(result.error).not.toContain("Forbidden SQL operation detected");
669
+ });
670
+
671
+ it("rejects DESCRIBE in PostgreSQL mode via AST parser, not regex guard", () => {
672
+ process.env.ATLAS_DATASOURCE_URL = "postgresql://test:test@localhost:5432/test";
673
+ const result = validateSQL("DESCRIBE companies");
674
+ expect(result.valid).toBe(false);
675
+ expect(result.error).not.toContain("Forbidden SQL operation detected");
676
+ });
677
+
678
+ it("rejects USE in PostgreSQL mode via AST parser, not regex guard", () => {
679
+ process.env.ATLAS_DATASOURCE_URL = "postgresql://test:test@localhost:5432/test";
680
+ const result = validateSQL("USE other_database");
681
+ expect(result.valid).toBe(false);
682
+ expect(result.error).not.toContain("Forbidden SQL operation detected");
683
+ });
684
+
685
+ it("still blocks HANDLER in MySQL mode via regex guard", () => {
686
+ process.env.ATLAS_DATASOURCE_URL = "mysql://test:test@localhost:3306/test";
687
+ expectInvalid("HANDLER companies OPEN", "forbidden");
688
+ });
689
+ });
690
+
691
+ // ----- Cross-DB guard: Snowflake patterns don't fire in PostgreSQL mode ---
692
+
693
+ describe("cross-database regex guard isolation — Snowflake patterns", () => {
694
+ it("does not reject GET in PostgreSQL mode via regex guard", () => {
695
+ process.env.ATLAS_DATASOURCE_URL = "postgresql://test:test@localhost:5432/test";
696
+ const result = validateSQL("GET @mystage file:///tmp/");
697
+ expect(result.valid).toBe(false);
698
+ expect(result.error).not.toContain("Forbidden SQL operation detected");
699
+ });
700
+
701
+ it("does not reject LIST in PostgreSQL mode via regex guard", () => {
702
+ process.env.ATLAS_DATASOURCE_URL = "postgresql://test:test@localhost:5432/test";
703
+ const result = validateSQL("LIST @mystage");
704
+ expect(result.valid).toBe(false);
705
+ expect(result.error).not.toContain("Forbidden SQL operation detected");
706
+ });
707
+
708
+ it("does not reject MERGE in MySQL mode via regex guard", () => {
709
+ process.env.ATLAS_DATASOURCE_URL = "mysql://test:test@localhost:3306/test";
710
+ // Minimal MERGE without UPDATE/INSERT keywords to isolate Snowflake-specific regex
711
+ const result = validateSQL("MERGE INTO companies USING src ON companies.id = src.id");
712
+ expect(result.valid).toBe(false);
713
+ expect(result.error).not.toContain("Forbidden SQL operation detected");
714
+ });
715
+
716
+ it("still blocks PUT in Snowflake mode via regex guard", () => {
717
+ process.env.ATLAS_DATASOURCE_URL = "snowflake://user:pass@account123/db/schema";
718
+ expectInvalid("PUT file:///tmp/data.csv @mystage", "forbidden");
719
+ });
720
+ });
721
+
722
+ // ----- Formerly SQLite commands still rejected by AST ----------------------
723
+
724
+ describe("formerly SQLite-specific commands still rejected via AST", () => {
725
+ beforeEach(() => {
726
+ process.env.ATLAS_DATASOURCE_URL = "postgresql://test:test@localhost:5432/test";
727
+ });
728
+
729
+ it("rejects PRAGMA in PostgreSQL mode", () => {
730
+ expectInvalid("PRAGMA table_info(companies)", "could not be parsed");
731
+ });
732
+
733
+ it("rejects ATTACH DATABASE in PostgreSQL mode", () => {
734
+ expectInvalid("ATTACH DATABASE '/tmp/evil.db' AS evil", "could not be parsed");
735
+ });
736
+
737
+ it("rejects DETACH DATABASE in PostgreSQL mode", () => {
738
+ expectInvalid("DETACH DATABASE evil", "could not be parsed");
739
+ });
740
+
741
+ it("rejects PRAGMA in MySQL mode", () => {
742
+ process.env.ATLAS_DATASOURCE_URL = "mysql://test:test@localhost:3306/test";
743
+ expectInvalid("PRAGMA table_info(companies)", "could not be parsed");
744
+ });
745
+ });
746
+
747
+ // ----- ClickHouse-specific validation ----------------------------------------
748
+
749
+ describe("ClickHouse-specific", () => {
750
+ beforeEach(() => {
751
+ process.env.ATLAS_DATASOURCE_URL = "clickhouse://test:test@localhost:8123/default";
752
+ });
753
+
754
+ it("rejects OPTIMIZE statement", () => {
755
+ expectInvalid("OPTIMIZE TABLE companies", "forbidden");
756
+ });
757
+
758
+ it("rejects SYSTEM statement", () => {
759
+ expectInvalid("SYSTEM FLUSH LOGS", "forbidden");
760
+ });
761
+
762
+ it("rejects KILL statement", () => {
763
+ expectInvalid("KILL QUERY WHERE query_id = '123'", "forbidden");
764
+ });
765
+
766
+ it("rejects ATTACH statement", () => {
767
+ expectInvalid("ATTACH TABLE companies", "forbidden");
768
+ });
769
+
770
+ it("rejects DETACH statement", () => {
771
+ expectInvalid("DETACH TABLE companies", "forbidden");
772
+ });
773
+
774
+ it("rejects RENAME statement", () => {
775
+ expectInvalid("RENAME TABLE companies TO old_companies", "forbidden");
776
+ });
777
+
778
+ it("rejects EXCHANGE statement", () => {
779
+ expectInvalid("EXCHANGE TABLES companies AND new_companies", "forbidden");
780
+ });
781
+
782
+ it("rejects SHOW statement", () => {
783
+ expectInvalid("SHOW TABLES", "forbidden");
784
+ });
785
+
786
+ it("rejects DESCRIBE statement", () => {
787
+ expectInvalid("DESCRIBE companies", "forbidden");
788
+ });
789
+
790
+ it("rejects mixed-case OPTIMIZE", () => {
791
+ expectInvalid("OpTiMiZe TABLE companies", "forbidden");
792
+ });
793
+
794
+ it("rejects mixed-case SYSTEM", () => {
795
+ expectInvalid("SyStEm FLUSH LOGS", "forbidden");
796
+ });
797
+
798
+ it("still rejects base forbidden patterns (INSERT, UPDATE, DELETE, DROP)", () => {
799
+ expectInvalid("INSERT INTO companies (name) VALUES ('x')", "forbidden");
800
+ expectInvalid("DROP TABLE companies", "forbidden");
801
+ });
802
+ });
803
+
804
+ // ----- ClickHouse parser mode -------------------------------------------------
805
+
806
+ describe("ClickHouse parser mode", () => {
807
+ beforeEach(() => {
808
+ process.env.ATLAS_DATASOURCE_URL = "clickhouse://test:test@localhost:8123/default";
809
+ });
810
+
811
+ it("accepts a simple SELECT in ClickHouse mode", () => {
812
+ expectValid("SELECT id, name FROM companies");
813
+ });
814
+
815
+ it("accepts SELECT with JOIN in ClickHouse mode", () => {
816
+ expectValid(
817
+ "SELECT c.name, p.name FROM companies c JOIN people p ON c.id = p.company_id"
818
+ );
819
+ });
820
+
821
+ it("accepts CTEs in ClickHouse mode", () => {
822
+ expectValid(
823
+ "WITH top AS (SELECT id, name FROM companies LIMIT 10) SELECT * FROM top"
824
+ );
825
+ });
826
+
827
+ it("accepts subqueries in ClickHouse mode", () => {
828
+ expectValid(
829
+ "SELECT * FROM companies WHERE id IN (SELECT company_id FROM people)"
830
+ );
831
+ });
832
+
833
+ it("rejects INSERT in ClickHouse mode", () => {
834
+ expectInvalid(
835
+ "INSERT INTO companies (name) VALUES ('Evil')",
836
+ "forbidden"
837
+ );
838
+ });
839
+
840
+ it("rejects UPDATE in ClickHouse mode", () => {
841
+ expectInvalid("UPDATE companies SET name = 'Evil'", "forbidden");
842
+ });
843
+
844
+ it("rejects DELETE in ClickHouse mode", () => {
845
+ expectInvalid("DELETE FROM companies WHERE id = 1", "forbidden");
846
+ });
847
+
848
+ it("rejects non-whitelisted tables in ClickHouse mode", () => {
849
+ expectInvalid(
850
+ "SELECT * FROM secret_data",
851
+ "not in the allowed list"
852
+ );
853
+ });
854
+
855
+ it("does not reject CTE names as non-whitelisted tables in ClickHouse mode", () => {
856
+ expectValid(
857
+ "WITH my_temp AS (SELECT id FROM companies) SELECT * FROM my_temp"
858
+ );
859
+ });
860
+
861
+ it("accepts UNION of SELECTs in ClickHouse mode", () => {
862
+ expectValid(
863
+ "SELECT name FROM companies UNION ALL SELECT name FROM people"
864
+ );
865
+ });
866
+ });
867
+
868
+ // ----- Cross-DB guard: ClickHouse patterns don't fire in PostgreSQL mode ------
869
+
870
+ describe("cross-database regex guard isolation (ClickHouse)", () => {
871
+ it("rejects OPTIMIZE in PostgreSQL mode via regex guard (base pattern)", () => {
872
+ process.env.ATLAS_DATASOURCE_URL = "postgresql://test:test@localhost:5432/test";
873
+ expectInvalid("OPTIMIZE TABLE companies", "forbidden");
874
+ });
875
+
876
+ it("rejects SYSTEM in PostgreSQL mode via AST parser, not regex guard", () => {
877
+ process.env.ATLAS_DATASOURCE_URL = "postgresql://test:test@localhost:5432/test";
878
+ const result = validateSQL("SYSTEM FLUSH LOGS");
879
+ expect(result.valid).toBe(false);
880
+ expect(result.error).not.toContain("Forbidden SQL operation detected");
881
+ });
882
+
883
+ it("still blocks OPTIMIZE in ClickHouse mode via regex guard", () => {
884
+ process.env.ATLAS_DATASOURCE_URL = "clickhouse://test:test@localhost:8123/default";
885
+ expectInvalid("OPTIMIZE TABLE companies", "forbidden");
886
+ });
887
+
888
+ it("rejects OPTIMIZE in MySQL mode via regex guard (base pattern)", () => {
889
+ process.env.ATLAS_DATASOURCE_URL = "mysql://test:test@localhost:3306/test";
890
+ expectInvalid("OPTIMIZE TABLE companies", "forbidden");
891
+ });
892
+ });
893
+
894
+ // ----- Snowflake-specific validation -------------------------------------------
895
+
896
+ describe("Snowflake-specific", () => {
897
+ beforeEach(() => {
898
+ process.env.ATLAS_DATASOURCE_URL = "snowflake://user:pass@account123/db/schema";
899
+ });
900
+
901
+ it("rejects PUT stage operation", () => {
902
+ expectInvalid("PUT file:///tmp/data.csv @mystage", "forbidden");
903
+ });
904
+
905
+ it("rejects GET stage operation", () => {
906
+ expectInvalid("GET @mystage file:///tmp/", "forbidden");
907
+ });
908
+
909
+ it("rejects LIST stage operation", () => {
910
+ expectInvalid("LIST @mystage", "forbidden");
911
+ });
912
+
913
+ it("rejects REMOVE stage operation", () => {
914
+ expectInvalid("REMOVE @mystage/file.csv", "forbidden");
915
+ });
916
+
917
+ it("rejects RM stage operation", () => {
918
+ expectInvalid("RM @mystage/file.csv", "forbidden");
919
+ });
920
+
921
+ it("rejects COPY INTO (Snowflake data ingestion)", () => {
922
+ expectInvalid("COPY INTO my_table FROM @my_stage", "forbidden");
923
+ });
924
+
925
+ it("rejects MERGE statement", () => {
926
+ expectInvalid(
927
+ "MERGE INTO companies USING new_data ON companies.id = new_data.id WHEN MATCHED THEN UPDATE SET name = new_data.name",
928
+ "forbidden"
929
+ );
930
+ });
931
+
932
+ it("rejects SHOW TABLES", () => {
933
+ expectInvalid("SHOW TABLES", "forbidden");
934
+ });
935
+
936
+ it("rejects DESCRIBE", () => {
937
+ expectInvalid("DESCRIBE companies", "forbidden");
938
+ });
939
+
940
+ it("rejects USE statement", () => {
941
+ expectInvalid("USE DATABASE mydb", "forbidden");
942
+ });
943
+
944
+ it("accepts valid SELECT in Snowflake mode", () => {
945
+ expectValid("SELECT * FROM companies LIMIT 10");
946
+ });
947
+
948
+ it("accepts SELECT with Snowflake-style identifier quoting", () => {
949
+ expectValid('SELECT "name", "id" FROM companies WHERE "id" > 0');
950
+ });
951
+
952
+ it("accepts CTEs in Snowflake mode", () => {
953
+ expectValid("WITH top AS (SELECT id, name FROM companies LIMIT 10) SELECT * FROM top");
954
+ });
955
+
956
+ it("accepts SELECT with JOIN in Snowflake mode", () => {
957
+ expectValid("SELECT c.name, p.name FROM companies c JOIN people p ON c.id = p.company_id");
958
+ });
959
+
960
+ it("accepts subqueries in Snowflake mode", () => {
961
+ expectValid("SELECT * FROM companies WHERE id IN (SELECT company_id FROM people)");
962
+ });
963
+
964
+ it("accepts window functions in Snowflake mode", () => {
965
+ expectValid("SELECT name, ROW_NUMBER() OVER (ORDER BY id) FROM companies");
966
+ });
967
+
968
+ it("does not reject CTE names as non-whitelisted tables in Snowflake mode", () => {
969
+ expectValid("WITH my_temp AS (SELECT id FROM companies) SELECT * FROM my_temp");
970
+ });
971
+
972
+ it("rejects non-whitelisted tables in Snowflake mode", () => {
973
+ expectInvalid("SELECT * FROM secret_data", "not in the allowed list");
974
+ });
975
+
976
+ it("accepts UNION of SELECTs in Snowflake mode", () => {
977
+ expectValid("SELECT name FROM companies UNION ALL SELECT name FROM people");
978
+ });
979
+
980
+ it("rejects INSERT in Snowflake mode", () => {
981
+ expectInvalid("INSERT INTO companies (name) VALUES ('Evil')", "forbidden");
982
+ });
983
+
984
+ it("rejects EXPLAIN in Snowflake mode", () => {
985
+ expectInvalid("EXPLAIN SELECT * FROM companies", "forbidden");
986
+ });
987
+
988
+ it("rejects mixed-case PUT in Snowflake mode", () => {
989
+ expectInvalid("PuT file:///tmp/data.csv @mystage", "forbidden");
990
+ });
991
+
992
+ it("allows data values containing 'Get' (no false positive after anchoring)", () => {
993
+ expectValid("SELECT id, name FROM companies WHERE name = 'Get Ready'");
994
+ });
995
+
996
+ it("allows column named 'list' (no false positive after anchoring)", () => {
997
+ expectValid("SELECT id FROM companies WHERE list = true");
998
+ });
999
+ });
1000
+
1001
+ describe("Salesforce redirect", () => {
1002
+ beforeEach(() => {
1003
+ process.env.ATLAS_DATASOURCE_URL = "salesforce://user:pass@login.salesforce.com";
1004
+ });
1005
+
1006
+ it("rejects any SQL and directs to querySalesforce tool", () => {
1007
+ const result = validateSQL("SELECT Id FROM Account");
1008
+ expect(result.valid).toBe(false);
1009
+ expect(result.error).toContain("querySalesforce");
1010
+ });
1011
+ });
1012
+ });