@libredb/studio 0.9.7

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 (572) hide show
  1. package/.claude/settings.local.json +127 -0
  2. package/.cursorrules +426 -0
  3. package/.devin/wiki.json +143 -0
  4. package/.dockerignore +80 -0
  5. package/.env.example +159 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +49 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
  8. package/.github/PULL_REQUEST_TEMPLATE.md +57 -0
  9. package/.github/workflows/ci.yml +185 -0
  10. package/.github/workflows/codeql.yml +57 -0
  11. package/.github/workflows/docker-build-push.yml +118 -0
  12. package/.github/workflows/helm-release.yml +113 -0
  13. package/CLAUDE.md +265 -0
  14. package/CODE_OF_CONDUCT.md +124 -0
  15. package/CONTRIBUTING.md +154 -0
  16. package/Dockerfile +73 -0
  17. package/LICENSE +21 -0
  18. package/README.md +614 -0
  19. package/SECURITY.md +107 -0
  20. package/artifacthub-repo.yml +4 -0
  21. package/bun.lock +1714 -0
  22. package/bunfig.toml +3 -0
  23. package/charts/libredb-studio/.helmignore +11 -0
  24. package/charts/libredb-studio/Chart.lock +6 -0
  25. package/charts/libredb-studio/Chart.yaml +50 -0
  26. package/charts/libredb-studio/README.md +206 -0
  27. package/charts/libredb-studio/templates/NOTES.txt +59 -0
  28. package/charts/libredb-studio/templates/_helpers.tpl +135 -0
  29. package/charts/libredb-studio/templates/configmap.yaml +37 -0
  30. package/charts/libredb-studio/templates/deployment.yaml +184 -0
  31. package/charts/libredb-studio/templates/hpa.yaml +32 -0
  32. package/charts/libredb-studio/templates/ingress.yaml +41 -0
  33. package/charts/libredb-studio/templates/networkpolicy.yaml +50 -0
  34. package/charts/libredb-studio/templates/pdb.yaml +18 -0
  35. package/charts/libredb-studio/templates/pvc.yaml +23 -0
  36. package/charts/libredb-studio/templates/secret.yaml +30 -0
  37. package/charts/libredb-studio/templates/seed-configmap.yaml +11 -0
  38. package/charts/libredb-studio/templates/service.yaml +22 -0
  39. package/charts/libredb-studio/templates/serviceaccount.yaml +13 -0
  40. package/charts/libredb-studio/values.schema.json +246 -0
  41. package/charts/libredb-studio/values.yaml +286 -0
  42. package/components.json +22 -0
  43. package/conductor/code_styleguides/typescript.md +43 -0
  44. package/conductor/product-guidelines.md +43 -0
  45. package/conductor/product.md +3 -0
  46. package/conductor/setup_state.json +1 -0
  47. package/conductor/tech-stack.md +39 -0
  48. package/conductor/tracks/enhance_postgres_monitoring_20251227/metadata.json +8 -0
  49. package/conductor/tracks/enhance_postgres_monitoring_20251227/plan.md +44 -0
  50. package/conductor/tracks/enhance_postgres_monitoring_20251227/spec.md +31 -0
  51. package/conductor/tracks.md +8 -0
  52. package/conductor/workflow.md +333 -0
  53. package/database-compose.yml +55 -0
  54. package/docker/postgres-init/01-extensions.sql +10 -0
  55. package/docker/postgres-init/02-sample-data.sql +585 -0
  56. package/docker/postgres.yml +68 -0
  57. package/docker-compose.yml +38 -0
  58. package/docs/AI_PLAN.md +74 -0
  59. package/docs/API_DOCS.md +875 -0
  60. package/docs/ARCHITECTURE.md +218 -0
  61. package/docs/DATABASE_PROVIDERS.md +358 -0
  62. package/docs/FEATURES.md +116 -0
  63. package/docs/HELM_CHART.md +252 -0
  64. package/docs/LOGIN_PAGE.md +178 -0
  65. package/docs/MONACO_EDITOR_PERFORMANCE.md +315 -0
  66. package/docs/OIDC_ARCH.md +681 -0
  67. package/docs/OIDC_SETUP.md +322 -0
  68. package/docs/POSTGRES_METRICS.md +516 -0
  69. package/docs/QUERY_OPTIMIZATION.md +370 -0
  70. package/docs/SEED_CONNECTIONS.md +468 -0
  71. package/docs/SQL_ALIAS_COMPLETION.md +190 -0
  72. package/docs/STORAGE_ARCHITECTURE.md +565 -0
  73. package/docs/STORAGE_QUICK_SETUP.md +419 -0
  74. package/docs/TECHNICAL_PLAN.md +36 -0
  75. package/docs/THEMING.md +345 -0
  76. package/docs/adding-a-new-database-provider.md +642 -0
  77. package/docs/backlogs/000-PLATFORM_DATA_SYNC_DATABASE.md +360 -0
  78. package/docs/backlogs/001-INLINE_DATA_EDITING.md +118 -0
  79. package/docs/backlogs/002-DATA_IMPORT.md +215 -0
  80. package/docs/backlogs/003-QUERY_TIME_MACHINE.md +183 -0
  81. package/docs/backlogs/004-AI_DATA_STORYTELLER.md +292 -0
  82. package/docs/backlogs/005-QUERY_PLAYGROUND.md +352 -0
  83. package/docs/backlogs/006-DATA_MASKING.md +418 -0
  84. package/docs/enterprise-features.md +718 -0
  85. package/docs/kubernetes-helm-chart-artifacthub-plan.md +803 -0
  86. package/docs/medium-koyeb-article-en.md +215 -0
  87. package/docs/plans/test-plans.md +445 -0
  88. package/docs/releases/RELEASE.V0.3.0.md +22 -0
  89. package/docs/releases/RELEASE.V0.4.0.md +154 -0
  90. package/docs/releases/RELEASE.V0.5.0.md +252 -0
  91. package/docs/releases/RELEASE_v0.5.6.md +145 -0
  92. package/docs/releases/RELEASE_v0.6.1.md +303 -0
  93. package/docs/releases/RELEASE_v0.6.7.md +292 -0
  94. package/docs/releases/RELEASE_v0.7.0.md +332 -0
  95. package/docs/releases/RELEASE_v0.8.0.md +521 -0
  96. package/docs/sampledb/titanic.sql +1379 -0
  97. package/docs/superpowers/plans/2026-03-25-seed-connections.md +1362 -0
  98. package/docs/superpowers/specs/2026-03-25-seed-connections-design.md +590 -0
  99. package/e2e/admin-dashboard.spec.ts +64 -0
  100. package/e2e/connection-management.spec.ts +58 -0
  101. package/e2e/export.spec.ts +34 -0
  102. package/e2e/login.spec.ts +85 -0
  103. package/e2e/query-execution.spec.ts +35 -0
  104. package/e2e/tab-management.spec.ts +64 -0
  105. package/eslint.config.mjs +28 -0
  106. package/fly.toml +43 -0
  107. package/next.config.ts +32 -0
  108. package/package.json +130 -0
  109. package/playwright.config.ts +34 -0
  110. package/postcss.config.mjs +7 -0
  111. package/public/favicon-32x32.png +0 -0
  112. package/public/favicon.ico +0 -0
  113. package/public/file.svg +1 -0
  114. package/public/globe.svg +1 -0
  115. package/public/logo.svg +32 -0
  116. package/public/next.svg +1 -0
  117. package/public/screenshots/code-generator.png +0 -0
  118. package/public/screenshots/connection-modal.png +0 -0
  119. package/public/screenshots/data-profiler.png +0 -0
  120. package/public/screenshots/erd-diagram.png +0 -0
  121. package/public/screenshots/hero-editor.png +0 -0
  122. package/public/screenshots/nl2sql.png +0 -0
  123. package/public/vercel.svg +1 -0
  124. package/public/window.svg +1 -0
  125. package/render.yaml +58 -0
  126. package/scripts/merge-lcov.mjs +239 -0
  127. package/sonar-project.properties +16 -0
  128. package/src/app/admin/error.tsx +46 -0
  129. package/src/app/admin/page.tsx +10 -0
  130. package/src/app/api/admin/audit/route.ts +52 -0
  131. package/src/app/api/admin/fleet-health/route.ts +81 -0
  132. package/src/app/api/ai/autopilot/route.ts +105 -0
  133. package/src/app/api/ai/chat/route.ts +132 -0
  134. package/src/app/api/ai/describe-schema/route.ts +52 -0
  135. package/src/app/api/ai/explain/route.ts +86 -0
  136. package/src/app/api/ai/impact/route.ts +97 -0
  137. package/src/app/api/ai/index-advisor/route.ts +98 -0
  138. package/src/app/api/ai/nl2sql/route.ts +87 -0
  139. package/src/app/api/ai/query-safety/route.ts +87 -0
  140. package/src/app/api/auth/login/route.ts +62 -0
  141. package/src/app/api/auth/logout/route.ts +25 -0
  142. package/src/app/api/auth/me/route.ts +10 -0
  143. package/src/app/api/auth/oidc/callback/route.ts +82 -0
  144. package/src/app/api/auth/oidc/login/route.ts +43 -0
  145. package/src/app/api/connections/managed/route.ts +35 -0
  146. package/src/app/api/db/cancel/route.ts +42 -0
  147. package/src/app/api/db/disconnect/route.ts +28 -0
  148. package/src/app/api/db/health/route.ts +49 -0
  149. package/src/app/api/db/maintenance/route.ts +72 -0
  150. package/src/app/api/db/monitoring/route.ts +62 -0
  151. package/src/app/api/db/multi-query/route.ts +116 -0
  152. package/src/app/api/db/pool-stats/route.ts +37 -0
  153. package/src/app/api/db/profile/route.ts +144 -0
  154. package/src/app/api/db/provider-meta/route.ts +49 -0
  155. package/src/app/api/db/query/route.ts +50 -0
  156. package/src/app/api/db/schema/route.ts +47 -0
  157. package/src/app/api/db/schema-snapshot/route.ts +42 -0
  158. package/src/app/api/db/test-connection/route.ts +55 -0
  159. package/src/app/api/db/transaction/route.ts +111 -0
  160. package/src/app/api/storage/[collection]/route.ts +67 -0
  161. package/src/app/api/storage/config/route.ts +17 -0
  162. package/src/app/api/storage/migrate/route.ts +45 -0
  163. package/src/app/api/storage/route.ts +32 -0
  164. package/src/app/error.tsx +49 -0
  165. package/src/app/global-error.tsx +55 -0
  166. package/src/app/globals.css +146 -0
  167. package/src/app/icon.svg +42 -0
  168. package/src/app/layout.tsx +34 -0
  169. package/src/app/login/login-form.tsx +301 -0
  170. package/src/app/login/page.tsx +11 -0
  171. package/src/app/monitoring/page.tsx +8 -0
  172. package/src/app/not-found.tsx +29 -0
  173. package/src/app/page.tsx +5 -0
  174. package/src/components/AIAutopilotPanel.tsx +238 -0
  175. package/src/components/CodeGenerator.tsx +271 -0
  176. package/src/components/CommandPalette.tsx +227 -0
  177. package/src/components/ConnectionModal.tsx +759 -0
  178. package/src/components/CreateTableModal.tsx +281 -0
  179. package/src/components/DataCharts.tsx +962 -0
  180. package/src/components/DataImportModal.tsx +582 -0
  181. package/src/components/DataProfiler.tsx +335 -0
  182. package/src/components/DatabaseDocs.tsx +251 -0
  183. package/src/components/MaskingSettings.tsx +414 -0
  184. package/src/components/MobileNav.tsx +50 -0
  185. package/src/components/NL2SQLPanel.tsx +281 -0
  186. package/src/components/PivotTable.tsx +257 -0
  187. package/src/components/QueryEditor.tsx +760 -0
  188. package/src/components/QueryHistory.tsx +344 -0
  189. package/src/components/QuerySafetyDialog.tsx +290 -0
  190. package/src/components/ResultsGrid.tsx +644 -0
  191. package/src/components/SaveQueryModal.tsx +104 -0
  192. package/src/components/SavedQueries.tsx +128 -0
  193. package/src/components/SchemaDiagram.tsx +473 -0
  194. package/src/components/SchemaDiff.tsx +473 -0
  195. package/src/components/SnapshotTimeline.tsx +116 -0
  196. package/src/components/Studio.tsx +639 -0
  197. package/src/components/TestDataGenerator.tsx +261 -0
  198. package/src/components/VisualExplain.tsx +820 -0
  199. package/src/components/admin/AdminDashboard.tsx +163 -0
  200. package/src/components/admin/tabs/AuditTab.tsx +531 -0
  201. package/src/components/admin/tabs/MonitoringEmbed.tsx +11 -0
  202. package/src/components/admin/tabs/OperationsTab.tsx +646 -0
  203. package/src/components/admin/tabs/OverviewTab.tsx +1328 -0
  204. package/src/components/admin/tabs/SecurityTab.tsx +284 -0
  205. package/src/components/community-section.tsx +92 -0
  206. package/src/components/icons/db-icons.tsx +84 -0
  207. package/src/components/libredb-logo.tsx +61 -0
  208. package/src/components/monitoring/MonitoringDashboard.tsx +345 -0
  209. package/src/components/monitoring/tabs/MetricChart.tsx +82 -0
  210. package/src/components/monitoring/tabs/OverviewTab.tsx +263 -0
  211. package/src/components/monitoring/tabs/PerformanceTab.tsx +254 -0
  212. package/src/components/monitoring/tabs/PoolTab.tsx +174 -0
  213. package/src/components/monitoring/tabs/QueriesTab.tsx +287 -0
  214. package/src/components/monitoring/tabs/SessionsTab.tsx +316 -0
  215. package/src/components/monitoring/tabs/StorageTab.tsx +335 -0
  216. package/src/components/monitoring/tabs/TablesTab.tsx +300 -0
  217. package/src/components/results-grid/ResultCard.tsx +111 -0
  218. package/src/components/results-grid/RowDetailSheet.tsx +178 -0
  219. package/src/components/results-grid/StatsBar.tsx +201 -0
  220. package/src/components/results-grid/index.ts +1 -0
  221. package/src/components/results-grid/utils.ts +23 -0
  222. package/src/components/schema-explorer/ColumnList.tsx +53 -0
  223. package/src/components/schema-explorer/SchemaExplorer.tsx +182 -0
  224. package/src/components/schema-explorer/TableItem.tsx +210 -0
  225. package/src/components/schema-explorer/index.ts +1 -0
  226. package/src/components/sidebar/ConnectionItem.tsx +105 -0
  227. package/src/components/sidebar/ConnectionsList.tsx +62 -0
  228. package/src/components/sidebar/Sidebar.tsx +130 -0
  229. package/src/components/sidebar/index.ts +2 -0
  230. package/src/components/studio/BottomPanel.tsx +286 -0
  231. package/src/components/studio/QueryToolbar.tsx +180 -0
  232. package/src/components/studio/StudioDesktopHeader.tsx +114 -0
  233. package/src/components/studio/StudioMobileHeader.tsx +340 -0
  234. package/src/components/studio/StudioTabBar.tsx +82 -0
  235. package/src/components/studio/index.ts +5 -0
  236. package/src/components/ui/accordion.tsx +66 -0
  237. package/src/components/ui/alert-dialog.tsx +157 -0
  238. package/src/components/ui/alert.tsx +66 -0
  239. package/src/components/ui/aspect-ratio.tsx +11 -0
  240. package/src/components/ui/avatar.tsx +53 -0
  241. package/src/components/ui/badge.tsx +46 -0
  242. package/src/components/ui/breadcrumb.tsx +109 -0
  243. package/src/components/ui/button-group.tsx +83 -0
  244. package/src/components/ui/button.tsx +60 -0
  245. package/src/components/ui/calendar.tsx +216 -0
  246. package/src/components/ui/card.tsx +92 -0
  247. package/src/components/ui/carousel.tsx +241 -0
  248. package/src/components/ui/chart.tsx +357 -0
  249. package/src/components/ui/checkbox.tsx +32 -0
  250. package/src/components/ui/collapsible.tsx +33 -0
  251. package/src/components/ui/command.tsx +184 -0
  252. package/src/components/ui/context-menu.tsx +252 -0
  253. package/src/components/ui/dialog.tsx +143 -0
  254. package/src/components/ui/drawer.tsx +135 -0
  255. package/src/components/ui/dropdown-menu.tsx +257 -0
  256. package/src/components/ui/empty.tsx +104 -0
  257. package/src/components/ui/field.tsx +248 -0
  258. package/src/components/ui/form.tsx +167 -0
  259. package/src/components/ui/hover-card.tsx +44 -0
  260. package/src/components/ui/input-group.tsx +170 -0
  261. package/src/components/ui/input-otp.tsx +77 -0
  262. package/src/components/ui/input.tsx +21 -0
  263. package/src/components/ui/item.tsx +193 -0
  264. package/src/components/ui/kbd.tsx +28 -0
  265. package/src/components/ui/label.tsx +24 -0
  266. package/src/components/ui/menubar.tsx +276 -0
  267. package/src/components/ui/navigation-menu.tsx +168 -0
  268. package/src/components/ui/pagination.tsx +127 -0
  269. package/src/components/ui/popover.tsx +48 -0
  270. package/src/components/ui/progress.tsx +31 -0
  271. package/src/components/ui/radio-group.tsx +45 -0
  272. package/src/components/ui/resizable.tsx +56 -0
  273. package/src/components/ui/scroll-area.tsx +58 -0
  274. package/src/components/ui/select.tsx +187 -0
  275. package/src/components/ui/separator.tsx +28 -0
  276. package/src/components/ui/sheet.tsx +139 -0
  277. package/src/components/ui/sidebar.tsx +726 -0
  278. package/src/components/ui/skeleton.tsx +13 -0
  279. package/src/components/ui/slider.tsx +63 -0
  280. package/src/components/ui/sonner.tsx +40 -0
  281. package/src/components/ui/spinner.tsx +16 -0
  282. package/src/components/ui/switch.tsx +31 -0
  283. package/src/components/ui/table.tsx +116 -0
  284. package/src/components/ui/tabs.tsx +66 -0
  285. package/src/components/ui/textarea.tsx +18 -0
  286. package/src/components/ui/toggle-group.tsx +83 -0
  287. package/src/components/ui/toggle.tsx +47 -0
  288. package/src/components/ui/tooltip.tsx +61 -0
  289. package/src/exports/components.ts +15 -0
  290. package/src/exports/index.ts +4 -0
  291. package/src/exports/providers.ts +4 -0
  292. package/src/exports/types.ts +26 -0
  293. package/src/hooks/use-ai-chat.ts +182 -0
  294. package/src/hooks/use-all-connections.ts +66 -0
  295. package/src/hooks/use-api-call.ts +71 -0
  296. package/src/hooks/use-auth.ts +51 -0
  297. package/src/hooks/use-connection-form.ts +349 -0
  298. package/src/hooks/use-connection-manager.ts +169 -0
  299. package/src/hooks/use-connection-payload.ts +15 -0
  300. package/src/hooks/use-inline-editing.ts +109 -0
  301. package/src/hooks/use-mobile.ts +20 -0
  302. package/src/hooks/use-monitoring-data.ts +270 -0
  303. package/src/hooks/use-provider-metadata.ts +62 -0
  304. package/src/hooks/use-query-execution.ts +478 -0
  305. package/src/hooks/use-storage-sync.ts +259 -0
  306. package/src/hooks/use-tab-manager.ts +231 -0
  307. package/src/hooks/use-toast.ts +20 -0
  308. package/src/hooks/use-transaction-control.ts +64 -0
  309. package/src/lib/api/error-codes.ts +30 -0
  310. package/src/lib/api/errors.ts +236 -0
  311. package/src/lib/api/with-error-handler.ts +41 -0
  312. package/src/lib/audit.ts +105 -0
  313. package/src/lib/auth.ts +87 -0
  314. package/src/lib/connection-string-parser.ts +172 -0
  315. package/src/lib/data-masking.ts +385 -0
  316. package/src/lib/db/base-provider.ts +325 -0
  317. package/src/lib/db/errors.ts +317 -0
  318. package/src/lib/db/factory.ts +324 -0
  319. package/src/lib/db/index.ts +123 -0
  320. package/src/lib/db/providers/document/index.ts +6 -0
  321. package/src/lib/db/providers/document/mongodb.ts +992 -0
  322. package/src/lib/db/providers/keyvalue/redis.ts +554 -0
  323. package/src/lib/db/providers/sql/index.ts +11 -0
  324. package/src/lib/db/providers/sql/mssql.ts +1065 -0
  325. package/src/lib/db/providers/sql/mysql.ts +978 -0
  326. package/src/lib/db/providers/sql/oracle.ts +1044 -0
  327. package/src/lib/db/providers/sql/postgres.ts +1179 -0
  328. package/src/lib/db/providers/sql/sql-base.ts +174 -0
  329. package/src/lib/db/providers/sql/sqlite.ts +721 -0
  330. package/src/lib/db/types.ts +437 -0
  331. package/src/lib/db/utils/pool-manager.ts +287 -0
  332. package/src/lib/db/utils/query-limiter.ts +239 -0
  333. package/src/lib/db-ui-config.ts +86 -0
  334. package/src/lib/editor/mongodb-completions.ts +172 -0
  335. package/src/lib/editor/sql-completions.ts +280 -0
  336. package/src/lib/llm/base-provider.ts +117 -0
  337. package/src/lib/llm/factory.ts +102 -0
  338. package/src/lib/llm/index.ts +90 -0
  339. package/src/lib/llm/providers/custom.ts +181 -0
  340. package/src/lib/llm/providers/gemini.ts +126 -0
  341. package/src/lib/llm/providers/ollama.ts +154 -0
  342. package/src/lib/llm/providers/openai.ts +146 -0
  343. package/src/lib/llm/types.ts +173 -0
  344. package/src/lib/llm/utils/config.ts +187 -0
  345. package/src/lib/llm/utils/retry.ts +119 -0
  346. package/src/lib/llm/utils/streaming.ts +202 -0
  347. package/src/lib/logger.ts +127 -0
  348. package/src/lib/monitoring-thresholds.ts +44 -0
  349. package/src/lib/oidc.ts +262 -0
  350. package/src/lib/query-generators.ts +61 -0
  351. package/src/lib/schema-diff/diff-engine.ts +273 -0
  352. package/src/lib/schema-diff/migration-generator.ts +208 -0
  353. package/src/lib/schema-diff/types.ts +55 -0
  354. package/src/lib/seed/config-loader.ts +79 -0
  355. package/src/lib/seed/connection-filter.ts +49 -0
  356. package/src/lib/seed/credential-resolver.ts +62 -0
  357. package/src/lib/seed/index.ts +40 -0
  358. package/src/lib/seed/resolve-connection.ts +57 -0
  359. package/src/lib/seed/types.ts +69 -0
  360. package/src/lib/sql/alias-extractor.ts +267 -0
  361. package/src/lib/sql/index.ts +8 -0
  362. package/src/lib/sql/statement-splitter.ts +167 -0
  363. package/src/lib/sql/types.ts +40 -0
  364. package/src/lib/ssh/tunnel.ts +142 -0
  365. package/src/lib/storage/factory.ts +84 -0
  366. package/src/lib/storage/index.ts +14 -0
  367. package/src/lib/storage/local-storage.ts +99 -0
  368. package/src/lib/storage/providers/postgres.ts +225 -0
  369. package/src/lib/storage/providers/sqlite.ts +153 -0
  370. package/src/lib/storage/storage-facade.ts +272 -0
  371. package/src/lib/storage/types.ts +75 -0
  372. package/src/lib/time-series-buffer.ts +58 -0
  373. package/src/lib/types.ts +173 -0
  374. package/src/lib/utils.ts +6 -0
  375. package/src/proxy.ts +104 -0
  376. package/src/types/db-drivers.d.ts +23 -0
  377. package/src/types/html2canvas.d.ts +9 -0
  378. package/tests/api/admin/audit.test.ts +178 -0
  379. package/tests/api/admin/fleet-health.test.ts +183 -0
  380. package/tests/api/ai/autopilot.test.ts +174 -0
  381. package/tests/api/ai/chat.test.ts +250 -0
  382. package/tests/api/ai/describe-schema.test.ts +266 -0
  383. package/tests/api/ai/explain.test.ts +199 -0
  384. package/tests/api/ai/impact.test.ts +168 -0
  385. package/tests/api/ai/index-advisor.test.ts +171 -0
  386. package/tests/api/ai/nl2sql.test.ts +202 -0
  387. package/tests/api/ai/query-safety.test.ts +196 -0
  388. package/tests/api/auth/login.test.ts +170 -0
  389. package/tests/api/auth/logout.test.ts +140 -0
  390. package/tests/api/auth/me.test.ts +73 -0
  391. package/tests/api/auth/oidc-callback.test.ts +215 -0
  392. package/tests/api/auth/oidc-login.test.ts +127 -0
  393. package/tests/api/db/cancel.test.ts +198 -0
  394. package/tests/api/db/disconnect.test.ts +124 -0
  395. package/tests/api/db/health.test.ts +222 -0
  396. package/tests/api/db/maintenance.test.ts +263 -0
  397. package/tests/api/db/monitoring.test.ts +221 -0
  398. package/tests/api/db/multi-query.test.ts +316 -0
  399. package/tests/api/db/pool-stats.test.ts +135 -0
  400. package/tests/api/db/profile.test.ts +330 -0
  401. package/tests/api/db/provider-meta.test.ts +193 -0
  402. package/tests/api/db/query.test.ts +314 -0
  403. package/tests/api/db/schema-snapshot.test.ts +170 -0
  404. package/tests/api/db/schema.test.ts +191 -0
  405. package/tests/api/db/test-connection.test.ts +185 -0
  406. package/tests/api/db/transaction.test.ts +314 -0
  407. package/tests/api/proxy.test.ts +191 -0
  408. package/tests/api/seed/managed-route.test.ts +113 -0
  409. package/tests/api/storage/config.test.ts +42 -0
  410. package/tests/api/storage/storage-routes.test.ts +309 -0
  411. package/tests/components/AIAutopilotPanel.test.tsx +756 -0
  412. package/tests/components/AdminPage.test.tsx +33 -0
  413. package/tests/components/CodeGenerator.test.tsx +182 -0
  414. package/tests/components/CommandPalette.test.tsx +428 -0
  415. package/tests/components/CommunitySection.test.tsx +91 -0
  416. package/tests/components/ConnectionModal.mobile.test.tsx +284 -0
  417. package/tests/components/ConnectionModal.test.tsx +570 -0
  418. package/tests/components/CreateTableModal.test.tsx +383 -0
  419. package/tests/components/DataCharts.test.tsx +739 -0
  420. package/tests/components/DataImportModal.test.tsx +751 -0
  421. package/tests/components/DataProfiler.test.tsx +589 -0
  422. package/tests/components/DatabaseDocs.test.tsx +353 -0
  423. package/tests/components/LoginPage.test.tsx +163 -0
  424. package/tests/components/LoginPageOIDC.test.tsx +92 -0
  425. package/tests/components/MaskingSettings.test.tsx +498 -0
  426. package/tests/components/MobileNav.test.tsx +30 -0
  427. package/tests/components/MonitoringPage.test.tsx +32 -0
  428. package/tests/components/NL2SQLPanel.test.tsx +621 -0
  429. package/tests/components/Page.test.tsx +33 -0
  430. package/tests/components/PivotTable.test.tsx +350 -0
  431. package/tests/components/QueryEditor.test.tsx +1730 -0
  432. package/tests/components/QueryHistory.test.tsx +572 -0
  433. package/tests/components/QuerySafetyDialog.test.tsx +586 -0
  434. package/tests/components/ResultsGrid.test.tsx +804 -0
  435. package/tests/components/RootLayout.test.tsx +83 -0
  436. package/tests/components/SaveQueryModal.test.tsx +25 -0
  437. package/tests/components/SavedQueries.test.tsx +43 -0
  438. package/tests/components/SchemaDiagram.test.tsx +1034 -0
  439. package/tests/components/SchemaDiff.test.tsx +906 -0
  440. package/tests/components/SnapshotTimeline.test.tsx +174 -0
  441. package/tests/components/Studio.test.tsx +1030 -0
  442. package/tests/components/TestDataGenerator.test.tsx +291 -0
  443. package/tests/components/VisualExplain.test.tsx +704 -0
  444. package/tests/components/admin/AdminDashboard.test.tsx +205 -0
  445. package/tests/components/admin/AuditTab.test.tsx +220 -0
  446. package/tests/components/admin/MonitoringEmbed.test.tsx +58 -0
  447. package/tests/components/admin/OperationsTab.test.tsx +975 -0
  448. package/tests/components/admin/OverviewTab.test.tsx +254 -0
  449. package/tests/components/admin/SecurityTab.test.tsx +467 -0
  450. package/tests/components/monitoring/MetricChart.test.tsx +111 -0
  451. package/tests/components/monitoring/MonitoringDashboard.test.tsx +259 -0
  452. package/tests/components/monitoring/OverviewTab.test.tsx +78 -0
  453. package/tests/components/monitoring/PerformanceTab.test.tsx +87 -0
  454. package/tests/components/monitoring/PoolTab.test.tsx +42 -0
  455. package/tests/components/monitoring/QueriesTab.test.tsx +80 -0
  456. package/tests/components/monitoring/SessionsTab.test.tsx +154 -0
  457. package/tests/components/monitoring/StorageTab.test.tsx +127 -0
  458. package/tests/components/monitoring/TablesTab.test.tsx +153 -0
  459. package/tests/components/results-grid/ResultCard.test.tsx +105 -0
  460. package/tests/components/results-grid/RowDetailSheet.test.tsx +308 -0
  461. package/tests/components/results-grid/StatsBar.test.tsx +162 -0
  462. package/tests/components/schema-explorer/ColumnList.test.tsx +151 -0
  463. package/tests/components/schema-explorer/SchemaExplorer.test.tsx +461 -0
  464. package/tests/components/schema-explorer/TableItem.test.tsx +415 -0
  465. package/tests/components/sidebar/ConnectionItem.test.tsx +201 -0
  466. package/tests/components/sidebar/ConnectionsList.test.tsx +176 -0
  467. package/tests/components/sidebar/Sidebar.test.tsx +187 -0
  468. package/tests/components/studio/BottomPanel.test.tsx +383 -0
  469. package/tests/components/studio/QueryToolbar.test.tsx +321 -0
  470. package/tests/components/studio/StudioDesktopHeader.test.tsx +377 -0
  471. package/tests/components/studio/StudioMobileHeader.test.tsx +198 -0
  472. package/tests/components/studio/StudioTabBar.test.tsx +331 -0
  473. package/tests/fixtures/connections.ts +96 -0
  474. package/tests/fixtures/masking-configs.ts +86 -0
  475. package/tests/fixtures/query-results.ts +71 -0
  476. package/tests/fixtures/schemas.ts +64 -0
  477. package/tests/fixtures/seed-connections/invalid-config.yaml +7 -0
  478. package/tests/fixtures/seed-connections/minimal-config.yaml +8 -0
  479. package/tests/fixtures/seed-connections/mixed-credentials.yaml +23 -0
  480. package/tests/fixtures/seed-connections/multi-role-config.yaml +30 -0
  481. package/tests/fixtures/seed-connections/valid-config.json +15 -0
  482. package/tests/fixtures/seed-connections/valid-config.yaml +51 -0
  483. package/tests/helpers/mock-fetch.ts +59 -0
  484. package/tests/helpers/mock-monaco.ts +112 -0
  485. package/tests/helpers/mock-navigation.ts +28 -0
  486. package/tests/helpers/mock-next.ts +80 -0
  487. package/tests/helpers/mock-provider.ts +133 -0
  488. package/tests/helpers/mock-sonner.ts +29 -0
  489. package/tests/helpers/render-with-providers.tsx +19 -0
  490. package/tests/hooks/use-ai-chat.test.ts +600 -0
  491. package/tests/hooks/use-auth.test.ts +371 -0
  492. package/tests/hooks/use-connection-form.test.ts +743 -0
  493. package/tests/hooks/use-connection-manager.test.ts +466 -0
  494. package/tests/hooks/use-inline-editing.test.ts +321 -0
  495. package/tests/hooks/use-mobile.test.ts +177 -0
  496. package/tests/hooks/use-monitoring-data.test.ts +819 -0
  497. package/tests/hooks/use-provider-metadata.test.ts +228 -0
  498. package/tests/hooks/use-query-execution.test.ts +1212 -0
  499. package/tests/hooks/use-tab-manager.test.ts +756 -0
  500. package/tests/hooks/use-toast.test.ts +74 -0
  501. package/tests/hooks/use-transaction-control.test.ts +211 -0
  502. package/tests/integration/db/mongodb-provider.test.ts +698 -0
  503. package/tests/integration/db/mssql-provider.test.ts +840 -0
  504. package/tests/integration/db/mysql-provider.test.ts +872 -0
  505. package/tests/integration/db/oracle-provider.test.ts +843 -0
  506. package/tests/integration/db/postgres-provider.test.ts +1382 -0
  507. package/tests/integration/db/redis-provider.test.ts +526 -0
  508. package/tests/integration/db/sqlite-provider.test.ts +480 -0
  509. package/tests/integration/seed/seed-pipeline.test.ts +102 -0
  510. package/tests/isolated/factory-singleton.test.ts +150 -0
  511. package/tests/isolated/use-storage-sync.test.ts +389 -0
  512. package/tests/run-components.sh +196 -0
  513. package/tests/setup-dom.ts +58 -0
  514. package/tests/setup.ts +40 -0
  515. package/tests/unit/api-errors.test.ts +210 -0
  516. package/tests/unit/code-generator-functions.test.ts +271 -0
  517. package/tests/unit/components/column-list.test.tsx +190 -0
  518. package/tests/unit/components/data-import-modal.test.tsx +441 -0
  519. package/tests/unit/components/studio-mobile-header.test.tsx +327 -0
  520. package/tests/unit/data-charts-functions.test.ts +496 -0
  521. package/tests/unit/data-import-functions.test.ts +320 -0
  522. package/tests/unit/data-import-utils.test.ts +125 -0
  523. package/tests/unit/db/base-provider.test.ts +517 -0
  524. package/tests/unit/db/errors.test.ts +403 -0
  525. package/tests/unit/db/factory.test.ts +436 -0
  526. package/tests/unit/db/pool-manager.test.ts +440 -0
  527. package/tests/unit/db/query-limiter.test.ts +387 -0
  528. package/tests/unit/db/sql-base.test.ts +438 -0
  529. package/tests/unit/lib/api/error-codes.test.ts +39 -0
  530. package/tests/unit/lib/audit.test.ts +326 -0
  531. package/tests/unit/lib/auth.test.ts +146 -0
  532. package/tests/unit/lib/connection-string-parser.test.ts +424 -0
  533. package/tests/unit/lib/data-masking.test.ts +583 -0
  534. package/tests/unit/lib/db-icons.test.tsx +41 -0
  535. package/tests/unit/lib/monitoring-thresholds.test.ts +133 -0
  536. package/tests/unit/lib/oidc.test.ts +509 -0
  537. package/tests/unit/lib/query-generators.test.ts +127 -0
  538. package/tests/unit/lib/storage/factory.test.ts +71 -0
  539. package/tests/unit/lib/storage/local-storage.test.ts +114 -0
  540. package/tests/unit/lib/storage/providers/postgres.test.ts +312 -0
  541. package/tests/unit/lib/storage/providers/sqlite.test.ts +232 -0
  542. package/tests/unit/lib/storage/storage-facade-extended.test.ts +331 -0
  543. package/tests/unit/lib/storage/storage-facade.test.ts +184 -0
  544. package/tests/unit/lib/storage.test.ts +317 -0
  545. package/tests/unit/lib/time-series-buffer.test.ts +212 -0
  546. package/tests/unit/lib/utils.test.ts +24 -0
  547. package/tests/unit/llm/base-provider.test.ts +238 -0
  548. package/tests/unit/llm/config.test.ts +262 -0
  549. package/tests/unit/llm/custom-provider.test.ts +281 -0
  550. package/tests/unit/llm/gemini-provider.test.ts +248 -0
  551. package/tests/unit/llm/llm-factory.test.ts +155 -0
  552. package/tests/unit/llm/ollama-provider.test.ts +288 -0
  553. package/tests/unit/llm/openai-provider.test.ts +324 -0
  554. package/tests/unit/llm/retry.test.ts +180 -0
  555. package/tests/unit/llm/streaming.test.ts +355 -0
  556. package/tests/unit/logger.test.ts +198 -0
  557. package/tests/unit/mongodb-completions.test.ts +516 -0
  558. package/tests/unit/pivot-table-functions.test.ts +76 -0
  559. package/tests/unit/query-cancelled-error.test.ts +81 -0
  560. package/tests/unit/schema-diff/diff-engine.test.ts +367 -0
  561. package/tests/unit/schema-diff/migration-generator.test.ts +513 -0
  562. package/tests/unit/seed/config-loader.test.ts +73 -0
  563. package/tests/unit/seed/connection-filter.test.ts +91 -0
  564. package/tests/unit/seed/credential-resolver.test.ts +85 -0
  565. package/tests/unit/seed/index.test.ts +72 -0
  566. package/tests/unit/seed/resolve-connection.test.ts +74 -0
  567. package/tests/unit/seed/types.test.ts +129 -0
  568. package/tests/unit/sql/alias-extractor.test.ts +444 -0
  569. package/tests/unit/sql/statement-splitter.test.ts +348 -0
  570. package/tests/unit/sql-completions.test.ts +463 -0
  571. package/tests/unit/ssh-tunnel.test.ts +465 -0
  572. package/tsconfig.json +42 -0
@@ -0,0 +1,516 @@
1
+ import '../setup';
2
+ import { describe, test, expect } from 'bun:test';
3
+ import type * as Monaco from 'monaco-editor';
4
+ import { registerMongoDBCompletionProvider } from '@/lib/editor/mongodb-completions';
5
+ import type { SchemaCompletionCache } from '@/lib/editor/sql-completions';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Mock Monaco
9
+ // ---------------------------------------------------------------------------
10
+
11
+ function createMockMonaco() {
12
+ let registeredProvider: {
13
+ triggerCharacters: string[];
14
+ provideCompletionItems: (
15
+ model: Monaco.editor.ITextModel,
16
+ position: Monaco.Position
17
+ ) => { suggestions: Monaco.languages.CompletionItem[] };
18
+ } | null = null;
19
+
20
+ const mockMonaco = {
21
+ languages: {
22
+ registerCompletionItemProvider: (_lang: string, provider: typeof registeredProvider) => {
23
+ registeredProvider = provider;
24
+ return { dispose: () => { registeredProvider = null; } };
25
+ },
26
+ CompletionItemKind: {
27
+ Keyword: 17,
28
+ Enum: 14,
29
+ Class: 5,
30
+ Field: 3,
31
+ Snippet: 27,
32
+ },
33
+ CompletionItemInsertTextRule: {
34
+ InsertAsSnippet: 4,
35
+ },
36
+ },
37
+ _getProvider: () => registeredProvider,
38
+ };
39
+
40
+ return mockMonaco as unknown as typeof Monaco & { _getProvider: () => typeof registeredProvider };
41
+ }
42
+
43
+ function createMockModel(lineContent: string, fullText?: string) {
44
+ return {
45
+ getWordUntilPosition: (position: { column: number }) => {
46
+ const before = lineContent.substring(0, position.column - 1);
47
+ const match = before.match(/(\w[$\w]*)$/);
48
+ return {
49
+ word: match ? match[1] : '',
50
+ startColumn: match ? position.column - match[1].length : position.column,
51
+ endColumn: position.column,
52
+ };
53
+ },
54
+ getLineContent: () => lineContent,
55
+ getValue: () => fullText ?? lineContent,
56
+ } as unknown as Monaco.editor.ITextModel;
57
+ }
58
+
59
+ function createPosition(line: number, col: number) {
60
+ return { lineNumber: line, column: col } as Monaco.Position;
61
+ }
62
+
63
+ function createSchemaCache(): SchemaCompletionCache {
64
+ const columnMap = new Map();
65
+ columnMap.set('users', [
66
+ { label: '_id', labelLower: '_id', type: 'ObjectId', isPrimary: true, tableName: 'users' },
67
+ { label: 'name', labelLower: 'name', type: 'string', isPrimary: false, tableName: 'users' },
68
+ { label: 'email', labelLower: 'email', type: 'string', isPrimary: false, tableName: 'users' },
69
+ ]);
70
+ columnMap.set('orders', [
71
+ { label: '_id', labelLower: '_id', type: 'ObjectId', isPrimary: true, tableName: 'orders' },
72
+ { label: 'total', labelLower: 'total', type: 'number', isPrimary: false, tableName: 'orders' },
73
+ ]);
74
+
75
+ const allColumns = new Map();
76
+ for (const [, cols] of columnMap) {
77
+ for (const col of cols) {
78
+ allColumns.set(col.label, col);
79
+ }
80
+ }
81
+
82
+ return {
83
+ tableItems: [
84
+ { label: 'users', labelLower: 'users', rowCount: 100, columnNames: '_id, name, email' },
85
+ { label: 'orders', labelLower: 'orders', rowCount: 500, columnNames: '_id, total' },
86
+ ],
87
+ columnMap,
88
+ allColumns,
89
+ };
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Registration
94
+ // ---------------------------------------------------------------------------
95
+
96
+ describe('registerMongoDBCompletionProvider', () => {
97
+ test('registers provider for json language and returns disposable', () => {
98
+ const monaco = createMockMonaco();
99
+ const cache = createSchemaCache();
100
+ const disposable = registerMongoDBCompletionProvider(monaco, cache);
101
+ expect(disposable).toBeDefined();
102
+ expect(typeof disposable.dispose).toBe('function');
103
+ expect(monaco._getProvider()).not.toBeNull();
104
+ });
105
+
106
+ test('dispose removes the provider', () => {
107
+ const monaco = createMockMonaco();
108
+ const cache = createSchemaCache();
109
+ const disposable = registerMongoDBCompletionProvider(monaco, cache);
110
+ disposable.dispose();
111
+ expect(monaco._getProvider()).toBeNull();
112
+ });
113
+
114
+ test('sets trigger characters to quote, dollar, colon', () => {
115
+ const monaco = createMockMonaco();
116
+ const cache = createSchemaCache();
117
+ registerMongoDBCompletionProvider(monaco, cache);
118
+ const provider = monaco._getProvider()!;
119
+ expect(provider.triggerCharacters).toEqual(['"', '$', ':']);
120
+ });
121
+ });
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // MQL Operators (after "$")
125
+ // ---------------------------------------------------------------------------
126
+
127
+ describe('MQL operator completions', () => {
128
+ test('suggests MQL operators when text contains $', () => {
129
+ const monaco = createMockMonaco();
130
+ const cache = createSchemaCache();
131
+ registerMongoDBCompletionProvider(monaco, cache);
132
+ const provider = monaco._getProvider()!;
133
+
134
+ const model = createMockModel(' "$', '{ "filter": { "$" } }');
135
+ const position = createPosition(1, 5);
136
+ const result = provider.provideCompletionItems(model, position);
137
+
138
+ const labels = result.suggestions.map(s => s.label);
139
+ expect(labels).toContain('$match');
140
+ expect(labels).toContain('$eq');
141
+ expect(labels).toContain('$gt');
142
+ expect(labels).toContain('$sum');
143
+ expect(labels).toContain('$set');
144
+ });
145
+
146
+ test('MQL operators have Keyword kind and correct detail', () => {
147
+ const monaco = createMockMonaco();
148
+ const cache = createSchemaCache();
149
+ registerMongoDBCompletionProvider(monaco, cache);
150
+ const provider = monaco._getProvider()!;
151
+
152
+ const model = createMockModel(' "$match', '{ "$match": {} }');
153
+ const position = createPosition(1, 10);
154
+ const result = provider.provideCompletionItems(model, position);
155
+
156
+ const matchSugg = result.suggestions.find(s => s.label === '$match');
157
+ expect(matchSugg).toBeDefined();
158
+ expect(matchSugg!.kind).toBe(17); // Keyword
159
+ expect((matchSugg as unknown as { detail: string }).detail).toBe('MQL Operator');
160
+ });
161
+
162
+ test('word starting with $ triggers MQL operators', () => {
163
+ const monaco = createMockMonaco();
164
+ const cache = createSchemaCache();
165
+ registerMongoDBCompletionProvider(monaco, cache);
166
+ const provider = monaco._getProvider()!;
167
+
168
+ // getWordUntilPosition returns "$gr" — starts with $
169
+ const model = {
170
+ getWordUntilPosition: () => ({ word: '$gr', startColumn: 3, endColumn: 6 }),
171
+ getLineContent: () => ' $gr',
172
+ getValue: () => ' $gr',
173
+ } as unknown as Monaco.editor.ITextModel;
174
+
175
+ const position = createPosition(1, 6);
176
+ const result = provider.provideCompletionItems(model, position);
177
+
178
+ const labels = result.suggestions.map(s => s.label);
179
+ expect(labels).toContain('$group');
180
+ expect(labels).toContain('$gt');
181
+ expect(labels).toContain('$gte');
182
+ });
183
+ });
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Operation names (after "operation": ")
187
+ // ---------------------------------------------------------------------------
188
+
189
+ describe('Operation name completions', () => {
190
+ test('suggests operations after "operation": "', () => {
191
+ const monaco = createMockMonaco();
192
+ const cache = createSchemaCache();
193
+ registerMongoDBCompletionProvider(monaco, cache);
194
+ const provider = monaco._getProvider()!;
195
+
196
+ const model = createMockModel(' "operation": "fi', '{ "operation": "fi" }');
197
+ const position = createPosition(1, 19);
198
+ const result = provider.provideCompletionItems(model, position);
199
+
200
+ const labels = result.suggestions.map(s => s.label);
201
+ expect(labels).toContain('find');
202
+ expect(labels).toContain('findOne');
203
+ expect(labels).toContain('aggregate');
204
+ expect(labels).toContain('insertOne');
205
+ expect(labels).toContain('deleteMany');
206
+ });
207
+
208
+ test('operation suggestions have Enum kind', () => {
209
+ const monaco = createMockMonaco();
210
+ const cache = createSchemaCache();
211
+ registerMongoDBCompletionProvider(monaco, cache);
212
+ const provider = monaco._getProvider()!;
213
+
214
+ const model = createMockModel(' "operation": "', '{ "operation": "" }');
215
+ const position = createPosition(1, 17);
216
+ const result = provider.provideCompletionItems(model, position);
217
+
218
+ const findSugg = result.suggestions.find(s => s.label === 'find');
219
+ expect(findSugg).toBeDefined();
220
+ expect(findSugg!.kind).toBe(14); // Enum
221
+ expect((findSugg as unknown as { detail: string }).detail).toBe('MongoDB Operation');
222
+ });
223
+
224
+ test('does not suggest operations without "operation" key context', () => {
225
+ const monaco = createMockMonaco();
226
+ const cache = createSchemaCache();
227
+ registerMongoDBCompletionProvider(monaco, cache);
228
+ const provider = monaco._getProvider()!;
229
+
230
+ const model = createMockModel(' "filter": "', '{ "filter": "" }');
231
+ const position = createPosition(1, 14);
232
+ const result = provider.provideCompletionItems(model, position);
233
+
234
+ const labels = result.suggestions.map(s => s.label);
235
+ expect(labels).not.toContain('find');
236
+ expect(labels).not.toContain('aggregate');
237
+ });
238
+ });
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // Collection names (after "collection": ")
242
+ // ---------------------------------------------------------------------------
243
+
244
+ describe('Collection name completions', () => {
245
+ test('suggests collections after "collection": "', () => {
246
+ const monaco = createMockMonaco();
247
+ const cache = createSchemaCache();
248
+ registerMongoDBCompletionProvider(monaco, cache);
249
+ const provider = monaco._getProvider()!;
250
+
251
+ const model = createMockModel(' "collection": "', '{ "collection": "" }');
252
+ const position = createPosition(1, 18);
253
+ const result = provider.provideCompletionItems(model, position);
254
+
255
+ const labels = result.suggestions.map(s => s.label);
256
+ expect(labels).toContain('users');
257
+ expect(labels).toContain('orders');
258
+ });
259
+
260
+ test('collection suggestions show doc count and have Class kind', () => {
261
+ const monaco = createMockMonaco();
262
+ const cache = createSchemaCache();
263
+ registerMongoDBCompletionProvider(monaco, cache);
264
+ const provider = monaco._getProvider()!;
265
+
266
+ const model = createMockModel(' "collection": "', '{ "collection": "" }');
267
+ const position = createPosition(1, 18);
268
+ const result = provider.provideCompletionItems(model, position);
269
+
270
+ const usersSugg = result.suggestions.find(s => s.label === 'users');
271
+ expect(usersSugg).toBeDefined();
272
+ expect(usersSugg!.kind).toBe(5); // Class
273
+ expect((usersSugg as unknown as { detail: string }).detail).toBe('Collection (100 docs)');
274
+ });
275
+
276
+ test('does not suggest collections outside "collection" key context', () => {
277
+ const monaco = createMockMonaco();
278
+ const cache = createSchemaCache();
279
+ registerMongoDBCompletionProvider(monaco, cache);
280
+ const provider = monaco._getProvider()!;
281
+
282
+ const model = createMockModel(' "filter": "', '{ "filter": "" }');
283
+ const position = createPosition(1, 14);
284
+ const result = provider.provideCompletionItems(model, position);
285
+
286
+ const kinds = result.suggestions.map(s => s.kind);
287
+ // No Class items (collections)
288
+ expect(kinds).not.toContain(5);
289
+ });
290
+ });
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // Field name completions
294
+ // ---------------------------------------------------------------------------
295
+
296
+ describe('Field name completions', () => {
297
+ test('suggests field names inside quoted string context', () => {
298
+ const monaco = createMockMonaco();
299
+ const cache = createSchemaCache();
300
+ registerMongoDBCompletionProvider(monaco, cache);
301
+ const provider = monaco._getProvider()!;
302
+
303
+ // Inside filter: { "na
304
+ const model = createMockModel(' "na', '{ "filter": { "na" } }');
305
+ const position = createPosition(1, 8);
306
+ const result = provider.provideCompletionItems(model, position);
307
+
308
+ const labels = result.suggestions.map(s => s.label);
309
+ expect(labels).toContain('_id');
310
+ expect(labels).toContain('name');
311
+ expect(labels).toContain('email');
312
+ expect(labels).toContain('total');
313
+ });
314
+
315
+ test('field suggestions have Field kind and show type', () => {
316
+ const monaco = createMockMonaco();
317
+ const cache = createSchemaCache();
318
+ registerMongoDBCompletionProvider(monaco, cache);
319
+ const provider = monaco._getProvider()!;
320
+
321
+ const model = createMockModel(' "na', '{ "filter": { "na" } }');
322
+ const position = createPosition(1, 8);
323
+ const result = provider.provideCompletionItems(model, position);
324
+
325
+ const nameSugg = result.suggestions.find(s => s.label === 'name');
326
+ expect(nameSugg).toBeDefined();
327
+ expect(nameSugg!.kind).toBe(3); // Field
328
+ expect((nameSugg as unknown as { detail: string }).detail).toBe('Field (string)');
329
+ });
330
+
331
+ test('does not suggest fields in "collection": context', () => {
332
+ const monaco = createMockMonaco();
333
+ const cache = createSchemaCache();
334
+ registerMongoDBCompletionProvider(monaco, cache);
335
+ const provider = monaco._getProvider()!;
336
+
337
+ const model = createMockModel(' "collection": "us', '{ "collection": "us" }');
338
+ const position = createPosition(1, 20);
339
+ const result = provider.provideCompletionItems(model, position);
340
+
341
+ const fieldSuggs = result.suggestions.filter(s => s.kind === 3);
342
+ expect(fieldSuggs.length).toBe(0);
343
+ });
344
+
345
+ test('does not suggest fields in "operation": context', () => {
346
+ const monaco = createMockMonaco();
347
+ const cache = createSchemaCache();
348
+ registerMongoDBCompletionProvider(monaco, cache);
349
+ const provider = monaco._getProvider()!;
350
+
351
+ const model = createMockModel(' "operation": "fi', '{ "operation": "fi" }');
352
+ const position = createPosition(1, 19);
353
+ const result = provider.provideCompletionItems(model, position);
354
+
355
+ const fieldSuggs = result.suggestions.filter(s => s.kind === 3);
356
+ expect(fieldSuggs.length).toBe(0);
357
+ });
358
+ });
359
+
360
+ // ---------------------------------------------------------------------------
361
+ // Snippet completions
362
+ // ---------------------------------------------------------------------------
363
+
364
+ describe('Snippet completions', () => {
365
+ test('suggests snippets when editor is nearly empty', () => {
366
+ const monaco = createMockMonaco();
367
+ const cache = createSchemaCache();
368
+ registerMongoDBCompletionProvider(monaco, cache);
369
+ const provider = monaco._getProvider()!;
370
+
371
+ const model = createMockModel('', '');
372
+ const position = createPosition(1, 1);
373
+ const result = provider.provideCompletionItems(model, position);
374
+
375
+ const snippetSuggs = result.suggestions.filter(s => s.kind === 27);
376
+ expect(snippetSuggs.length).toBe(7);
377
+
378
+ const labels = snippetSuggs.map(s => s.label);
379
+ expect(labels).toContain('find');
380
+ expect(labels).toContain('findOne');
381
+ expect(labels).toContain('aggregate');
382
+ expect(labels).toContain('count');
383
+ expect(labels).toContain('insertOne');
384
+ expect(labels).toContain('updateOne');
385
+ expect(labels).toContain('deleteMany');
386
+ });
387
+
388
+ test('suggests snippets when cursor at line start', () => {
389
+ const monaco = createMockMonaco();
390
+ const cache = createSchemaCache();
391
+ registerMongoDBCompletionProvider(monaco, cache);
392
+ const provider = monaco._getProvider()!;
393
+
394
+ // textBefore is whitespace-only → matches /^\s*$/
395
+ const model = createMockModel(' ', '{ "collection": "users" }\n ');
396
+ const position = createPosition(1, 3);
397
+ const result = provider.provideCompletionItems(model, position);
398
+
399
+ const snippetSuggs = result.suggestions.filter(s => s.kind === 27);
400
+ expect(snippetSuggs.length).toBe(7);
401
+ });
402
+
403
+ test('snippets have InsertAsSnippet rule and correct detail', () => {
404
+ const monaco = createMockMonaco();
405
+ const cache = createSchemaCache();
406
+ registerMongoDBCompletionProvider(monaco, cache);
407
+ const provider = monaco._getProvider()!;
408
+
409
+ const model = createMockModel('', '');
410
+ const position = createPosition(1, 1);
411
+ const result = provider.provideCompletionItems(model, position);
412
+
413
+ const findSnippet = result.suggestions.find(s => s.label === 'find' && s.kind === 27);
414
+ expect(findSnippet).toBeDefined();
415
+ expect(findSnippet!.insertTextRules).toBe(4); // InsertAsSnippet
416
+ expect((findSnippet as unknown as { detail: string }).detail).toBe('Find documents');
417
+ });
418
+
419
+ test('snippets contain template placeholders', () => {
420
+ const monaco = createMockMonaco();
421
+ const cache = createSchemaCache();
422
+ registerMongoDBCompletionProvider(monaco, cache);
423
+ const provider = monaco._getProvider()!;
424
+
425
+ const model = createMockModel('', '');
426
+ const position = createPosition(1, 1);
427
+ const result = provider.provideCompletionItems(model, position);
428
+
429
+ const snippetSuggs = result.suggestions.filter(s => s.kind === 27);
430
+ for (const snippet of snippetSuggs) {
431
+ expect((snippet.insertText as string)).toContain('${1:collection}');
432
+ }
433
+ });
434
+
435
+ test('does not suggest snippets when editor has substantial content', () => {
436
+ const monaco = createMockMonaco();
437
+ const cache = createSchemaCache();
438
+ registerMongoDBCompletionProvider(monaco, cache);
439
+ const provider = monaco._getProvider()!;
440
+
441
+ // fullText > 5 chars and textBefore is not whitespace-only
442
+ const model = createMockModel(' "filter": {', '{ "collection": "users", "operation": "find", "filter": { } }');
443
+ const position = createPosition(1, 14);
444
+ const result = provider.provideCompletionItems(model, position);
445
+
446
+ const snippetSuggs = result.suggestions.filter(s => s.kind === 27);
447
+ expect(snippetSuggs.length).toBe(0);
448
+ });
449
+ });
450
+
451
+ // ---------------------------------------------------------------------------
452
+ // Sort order
453
+ // ---------------------------------------------------------------------------
454
+
455
+ describe('Sort order', () => {
456
+ test('operators and operations sort before fields', () => {
457
+ const monaco = createMockMonaco();
458
+ const cache = createSchemaCache();
459
+ registerMongoDBCompletionProvider(monaco, cache);
460
+ const provider = monaco._getProvider()!;
461
+
462
+ // Context that triggers both operators and fields: "$
463
+ const model = createMockModel(' "$', '{ "filter": { "$" } }');
464
+ const position = createPosition(1, 7);
465
+ const result = provider.provideCompletionItems(model, position);
466
+
467
+ const operatorSugg = result.suggestions.find(s => s.label === '$match');
468
+ const fieldSugg = result.suggestions.find(s => s.label === 'name');
469
+ expect(operatorSugg?.sortText?.startsWith('0')).toBe(true);
470
+ expect(fieldSugg?.sortText?.startsWith('2')).toBe(true);
471
+ });
472
+ });
473
+
474
+ // ---------------------------------------------------------------------------
475
+ // Empty schema cache
476
+ // ---------------------------------------------------------------------------
477
+
478
+ describe('Empty schema cache', () => {
479
+ test('works with empty schema cache', () => {
480
+ const monaco = createMockMonaco();
481
+ const emptyCache: SchemaCompletionCache = {
482
+ tableItems: [],
483
+ columnMap: new Map(),
484
+ allColumns: new Map(),
485
+ };
486
+ registerMongoDBCompletionProvider(monaco, emptyCache);
487
+ const provider = monaco._getProvider()!;
488
+
489
+ // Trigger $ operators — should still work
490
+ const model = createMockModel(' "$', '{ "$" }');
491
+ const position = createPosition(1, 5);
492
+ const result = provider.provideCompletionItems(model, position);
493
+
494
+ const labels = result.suggestions.map(s => s.label);
495
+ expect(labels).toContain('$match');
496
+ expect(labels).toContain('$group');
497
+ });
498
+
499
+ test('collection context with empty cache returns no collections', () => {
500
+ const monaco = createMockMonaco();
501
+ const emptyCache: SchemaCompletionCache = {
502
+ tableItems: [],
503
+ columnMap: new Map(),
504
+ allColumns: new Map(),
505
+ };
506
+ registerMongoDBCompletionProvider(monaco, emptyCache);
507
+ const provider = monaco._getProvider()!;
508
+
509
+ const model = createMockModel(' "collection": "', '{ "collection": "" }');
510
+ const position = createPosition(1, 18);
511
+ const result = provider.provideCompletionItems(model, position);
512
+
513
+ const classSuggs = result.suggestions.filter(s => s.kind === 5);
514
+ expect(classSuggs.length).toBe(0);
515
+ });
516
+ });
@@ -0,0 +1,76 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { aggregate } from '@/components/PivotTable';
3
+
4
+ describe('aggregate', () => {
5
+ test('count returns length of values array', () => {
6
+ expect(aggregate([1, 2, 3], 'count')).toBe('3');
7
+ });
8
+
9
+ test('count with mixed types', () => {
10
+ expect(aggregate([1, 'a', null, undefined], 'count')).toBe('4');
11
+ });
12
+
13
+ test('count with empty array', () => {
14
+ expect(aggregate([], 'count')).toBe('0');
15
+ });
16
+
17
+ test('sum of numbers', () => {
18
+ expect(aggregate([10, 20, 30], 'sum')).toBe('60.00');
19
+ });
20
+
21
+ test('sum with non-numeric values filtered', () => {
22
+ expect(aggregate([10, 'abc', 20], 'sum')).toBe('30.00');
23
+ });
24
+
25
+ test('sum with all non-numeric returns 0', () => {
26
+ expect(aggregate(['abc', 'def'], 'sum')).toBe('0');
27
+ });
28
+
29
+ test('sum with empty array returns 0', () => {
30
+ expect(aggregate([], 'sum')).toBe('0');
31
+ });
32
+
33
+ test('avg of numbers', () => {
34
+ expect(aggregate([10, 20, 30], 'avg')).toBe('20.00');
35
+ });
36
+
37
+ test('avg with non-numeric filtered', () => {
38
+ expect(aggregate([10, 'abc', 30], 'avg')).toBe('20.00');
39
+ });
40
+
41
+ test('avg with all non-numeric returns 0', () => {
42
+ expect(aggregate(['abc'], 'avg')).toBe('0');
43
+ });
44
+
45
+ test('min of numbers', () => {
46
+ expect(aggregate([30, 10, 20], 'min')).toBe('10');
47
+ });
48
+
49
+ test('min with negatives', () => {
50
+ expect(aggregate([-5, 0, 5], 'min')).toBe('-5');
51
+ });
52
+
53
+ test('min with all non-numeric returns dash', () => {
54
+ expect(aggregate(['abc'], 'min')).toBe('-');
55
+ });
56
+
57
+ test('max of numbers', () => {
58
+ expect(aggregate([30, 10, 20], 'max')).toBe('30');
59
+ });
60
+
61
+ test('max with negatives', () => {
62
+ expect(aggregate([-5, 0, 5], 'max')).toBe('5');
63
+ });
64
+
65
+ test('max with all non-numeric returns dash', () => {
66
+ expect(aggregate(['abc'], 'max')).toBe('-');
67
+ });
68
+
69
+ test('sum with decimal values', () => {
70
+ expect(aggregate([1.5, 2.5], 'sum')).toBe('4.00');
71
+ });
72
+
73
+ test('avg of single value', () => {
74
+ expect(aggregate([42], 'avg')).toBe('42.00');
75
+ });
76
+ });
@@ -0,0 +1,81 @@
1
+ import '../setup';
2
+ import { describe, test, expect } from 'bun:test';
3
+ import {
4
+ DatabaseError,
5
+ QueryCancelledError,
6
+ isQueryCancelledError,
7
+ mapDatabaseError,
8
+ TimeoutError,
9
+ } from '@/lib/db/errors';
10
+
11
+ describe('QueryCancelledError', () => {
12
+ test('has correct name, code, provider, and query', () => {
13
+ const err = new QueryCancelledError('Query was cancelled', 'postgres', 'SELECT 1');
14
+ expect(err.name).toBe('QueryCancelledError');
15
+ expect(err.code).toBe('QUERY_CANCELLED');
16
+ expect(err.provider).toBe('postgres');
17
+ expect(err.query).toBe('SELECT 1');
18
+ expect(err.message).toBe('Query was cancelled');
19
+ expect(err).toBeInstanceOf(Error);
20
+ });
21
+
22
+ test('is an instance of DatabaseError', () => {
23
+ const err = new QueryCancelledError('cancelled', 'mysql');
24
+ expect(err).toBeInstanceOf(DatabaseError);
25
+ });
26
+ });
27
+
28
+ describe('isQueryCancelledError', () => {
29
+ test('returns true for QueryCancelledError', () => {
30
+ const err = new QueryCancelledError('cancelled', 'postgres');
31
+ expect(isQueryCancelledError(err)).toBe(true);
32
+ });
33
+
34
+ test('returns false for a generic Error', () => {
35
+ expect(isQueryCancelledError(new Error('not cancelled'))).toBe(false);
36
+ });
37
+
38
+ test('returns false for a non-Error', () => {
39
+ expect(isQueryCancelledError('string')).toBe(false);
40
+ expect(isQueryCancelledError(null)).toBe(false);
41
+ expect(isQueryCancelledError(undefined)).toBe(false);
42
+ });
43
+ });
44
+
45
+ describe('mapDatabaseError — cancellation patterns', () => {
46
+ test('returns QueryCancelledError for "canceling statement" message', () => {
47
+ const native = new Error('ERROR: canceling statement due to user request');
48
+ const mapped = mapDatabaseError(native, 'postgres', 'SELECT pg_sleep(100)');
49
+ expect(mapped).toBeInstanceOf(QueryCancelledError);
50
+ expect(mapped.message).toBe('Query was cancelled');
51
+ expect(mapped.provider).toBe('postgres');
52
+ });
53
+
54
+ test('returns QueryCancelledError for "Query execution was interrupted" message', () => {
55
+ const native = new Error('Query execution was interrupted');
56
+ const mapped = mapDatabaseError(native, 'mysql', 'SELECT SLEEP(100)');
57
+ expect(mapped).toBeInstanceOf(QueryCancelledError);
58
+ expect(mapped.message).toBe('Query was cancelled');
59
+ });
60
+
61
+ test('returns QueryCancelledError for "query was cancelled" message', () => {
62
+ const native = new Error('The query was cancelled by the user');
63
+ const mapped = mapDatabaseError(native, 'postgres');
64
+ expect(mapped).toBeInstanceOf(QueryCancelledError);
65
+ });
66
+
67
+ test('returns TimeoutError for "timeout" (not cancelled)', () => {
68
+ const native = new Error('Connection timeout expired');
69
+ const mapped = mapDatabaseError(native, 'postgres');
70
+ expect(mapped).toBeInstanceOf(TimeoutError);
71
+ expect(mapped).not.toBeInstanceOf(QueryCancelledError);
72
+ expect(mapped.message).toContain('timeout');
73
+ });
74
+
75
+ test('returns TimeoutError for "timed out" (not cancelled)', () => {
76
+ const native = new Error('Query timed out after 30s');
77
+ const mapped = mapDatabaseError(native, 'mysql');
78
+ expect(mapped).toBeInstanceOf(TimeoutError);
79
+ expect(mapped).not.toBeInstanceOf(QueryCancelledError);
80
+ });
81
+ });