@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,317 @@
1
+ /**
2
+ * Database Error Classes
3
+ * Custom error types for database operations
4
+ */
5
+
6
+ import type { DatabaseType } from './types';
7
+ import { ApiErrorCode } from '@/lib/api/error-codes';
8
+
9
+ // ============================================================================
10
+ // Base Database Error
11
+ // ============================================================================
12
+
13
+ /**
14
+ * Base error class for all database-related errors
15
+ */
16
+ export class DatabaseError extends Error {
17
+ constructor(
18
+ message: string,
19
+ public readonly provider?: DatabaseType,
20
+ public readonly code?: ApiErrorCode,
21
+ public readonly query?: string
22
+ ) {
23
+ super(message);
24
+ this.name = 'DatabaseError';
25
+ Object.setPrototypeOf(this, DatabaseError.prototype);
26
+ }
27
+
28
+ toJSON() {
29
+ return {
30
+ name: this.name,
31
+ message: this.message,
32
+ provider: this.provider,
33
+ code: this.code,
34
+ // Don't expose full query in production for security
35
+ query: this.query ? this.query.substring(0, 100) + '...' : undefined,
36
+ };
37
+ }
38
+ }
39
+
40
+ // ============================================================================
41
+ // Configuration Errors
42
+ // ============================================================================
43
+
44
+ /**
45
+ * Configuration error - missing or invalid configuration
46
+ */
47
+ export class DatabaseConfigError extends DatabaseError {
48
+ constructor(message: string, provider?: DatabaseType) {
49
+ super(message, provider, ApiErrorCode.CONFIG_ERROR);
50
+ this.name = 'DatabaseConfigError';
51
+ Object.setPrototypeOf(this, DatabaseConfigError.prototype);
52
+ }
53
+ }
54
+
55
+ // ============================================================================
56
+ // Connection Errors
57
+ // ============================================================================
58
+
59
+ /**
60
+ * Connection error - failed to connect to database
61
+ */
62
+ export class ConnectionError extends DatabaseError {
63
+ constructor(
64
+ message: string,
65
+ provider?: DatabaseType,
66
+ public readonly host?: string,
67
+ public readonly port?: number
68
+ ) {
69
+ super(message, provider, ApiErrorCode.CONNECTION_ERROR);
70
+ this.name = 'ConnectionError';
71
+ Object.setPrototypeOf(this, ConnectionError.prototype);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Authentication error - invalid credentials
77
+ */
78
+ export class AuthenticationError extends DatabaseError {
79
+ constructor(message: string, provider?: DatabaseType) {
80
+ super(message, provider, ApiErrorCode.AUTH_ERROR);
81
+ this.name = 'AuthenticationError';
82
+ Object.setPrototypeOf(this, AuthenticationError.prototype);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Pool exhausted error - no available connections in pool
88
+ */
89
+ export class PoolExhaustedError extends DatabaseError {
90
+ constructor(
91
+ message: string,
92
+ provider?: DatabaseType,
93
+ public readonly poolSize?: number
94
+ ) {
95
+ super(message, provider, ApiErrorCode.POOL_EXHAUSTED);
96
+ this.name = 'PoolExhaustedError';
97
+ Object.setPrototypeOf(this, PoolExhaustedError.prototype);
98
+ }
99
+ }
100
+
101
+ // ============================================================================
102
+ // Query Errors
103
+ // ============================================================================
104
+
105
+ /**
106
+ * Query error - SQL syntax or execution error
107
+ */
108
+ export class QueryError extends DatabaseError {
109
+ constructor(
110
+ message: string,
111
+ provider?: DatabaseType,
112
+ query?: string,
113
+ public readonly position?: number,
114
+ public readonly detail?: string
115
+ ) {
116
+ super(message, provider, ApiErrorCode.QUERY_ERROR, query);
117
+ this.name = 'QueryError';
118
+ Object.setPrototypeOf(this, QueryError.prototype);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Timeout error - query or connection timeout
124
+ */
125
+ export class TimeoutError extends DatabaseError {
126
+ constructor(
127
+ message: string,
128
+ provider?: DatabaseType,
129
+ public readonly timeout?: number,
130
+ query?: string
131
+ ) {
132
+ super(message, provider, ApiErrorCode.TIMEOUT_ERROR, query);
133
+ this.name = 'TimeoutError';
134
+ Object.setPrototypeOf(this, TimeoutError.prototype);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Query cancelled error - user-initiated cancellation
140
+ */
141
+ export class QueryCancelledError extends DatabaseError {
142
+ constructor(message: string, provider?: DatabaseType, query?: string) {
143
+ super(message, provider, ApiErrorCode.QUERY_CANCELLED, query);
144
+ this.name = 'QueryCancelledError';
145
+ Object.setPrototypeOf(this, QueryCancelledError.prototype);
146
+ }
147
+ }
148
+
149
+ // ============================================================================
150
+ // Type Guards
151
+ // ============================================================================
152
+
153
+ export function isDatabaseError(error: unknown): error is DatabaseError {
154
+ return error instanceof DatabaseError;
155
+ }
156
+
157
+ export function isConnectionError(error: unknown): error is ConnectionError {
158
+ return error instanceof ConnectionError;
159
+ }
160
+
161
+ export function isQueryError(error: unknown): error is QueryError {
162
+ return error instanceof QueryError;
163
+ }
164
+
165
+ export function isTimeoutError(error: unknown): error is TimeoutError {
166
+ return error instanceof TimeoutError;
167
+ }
168
+
169
+ export function isAuthenticationError(error: unknown): error is AuthenticationError {
170
+ return error instanceof AuthenticationError;
171
+ }
172
+
173
+ export function isQueryCancelledError(error: unknown): error is QueryCancelledError {
174
+ return error instanceof QueryCancelledError;
175
+ }
176
+
177
+ // ============================================================================
178
+ // Error Mapping Utilities
179
+ // ============================================================================
180
+
181
+ /**
182
+ * Check if an error is retryable
183
+ */
184
+ export function isRetryableError(error: unknown): boolean {
185
+ if (!isDatabaseError(error)) {
186
+ // Network errors are typically retryable
187
+ if (error instanceof TypeError && error.message.includes('fetch')) {
188
+ return true;
189
+ }
190
+ return false;
191
+ }
192
+
193
+ // Auth and config errors are not retryable
194
+ if (error instanceof AuthenticationError || error instanceof DatabaseConfigError) {
195
+ return false;
196
+ }
197
+
198
+ // Query syntax errors are not retryable
199
+ if (error instanceof QueryError && error.position !== undefined) {
200
+ return false;
201
+ }
202
+
203
+ // Connection and timeout errors may be retryable
204
+ return true;
205
+ }
206
+
207
+ /**
208
+ * Map native database errors to our error types
209
+ */
210
+ export function mapDatabaseError(
211
+ error: unknown,
212
+ provider: DatabaseType,
213
+ query?: string
214
+ ): DatabaseError {
215
+ if (isDatabaseError(error)) {
216
+ return error;
217
+ }
218
+
219
+ if (!(error instanceof Error)) {
220
+ return new DatabaseError(String(error), provider);
221
+ }
222
+
223
+ const message = error.message.toLowerCase();
224
+
225
+ // Connection errors
226
+ if (
227
+ message.includes('econnrefused') ||
228
+ message.includes('connection refused') ||
229
+ message.includes('connect etimedout') ||
230
+ message.includes('getaddrinfo')
231
+ ) {
232
+ return new ConnectionError(
233
+ `Failed to connect to ${provider} database: ${error.message}`,
234
+ provider
235
+ );
236
+ }
237
+
238
+ // Authentication errors
239
+ if (
240
+ message.includes('password') ||
241
+ message.includes('authentication') ||
242
+ message.includes('access denied') ||
243
+ message.includes('permission denied')
244
+ ) {
245
+ return new AuthenticationError(
246
+ `Authentication failed: ${error.message}`,
247
+ provider
248
+ );
249
+ }
250
+
251
+ // Query cancellation (must check before timeout — 'canceling statement' is cancellation, not timeout)
252
+ if (
253
+ message.includes('canceling statement') ||
254
+ message.includes('query execution was interrupted') ||
255
+ message.includes('query was cancelled') ||
256
+ message.includes('kill query')
257
+ ) {
258
+ return new QueryCancelledError(
259
+ 'Query was cancelled',
260
+ provider,
261
+ query
262
+ );
263
+ }
264
+
265
+ // Timeout errors
266
+ if (
267
+ message.includes('timeout') ||
268
+ message.includes('timed out')
269
+ ) {
270
+ return new TimeoutError(
271
+ `Query timeout: ${error.message}`,
272
+ provider,
273
+ undefined,
274
+ query
275
+ );
276
+ }
277
+
278
+ // Oracle errors
279
+ if (message.includes('ora-01017') || message.includes('invalid username/password')) {
280
+ return new AuthenticationError(`Authentication failed: ${error.message}`, provider);
281
+ }
282
+ if (message.includes('ora-12541') || message.includes('ora-12154') || message.includes('tns:')) {
283
+ return new ConnectionError(`Failed to connect to Oracle: ${error.message}`, provider);
284
+ }
285
+ if (message.includes('ora-00942')) {
286
+ return new QueryError(`Table or view does not exist: ${error.message}`, provider, query);
287
+ }
288
+
289
+ // MSSQL errors
290
+ if (message.includes('login failed')) {
291
+ return new AuthenticationError(`Authentication failed: ${error.message}`, provider);
292
+ }
293
+ if (message.includes('cannot open database')) {
294
+ return new ConnectionError(`Database not found: ${error.message}`, provider);
295
+ }
296
+
297
+ // Query errors (PostgreSQL specific)
298
+ if (message.includes('syntax error') || message.includes('column') || message.includes('relation')) {
299
+ return new QueryError(
300
+ error.message,
301
+ provider,
302
+ query,
303
+ (error as { position?: number }).position
304
+ );
305
+ }
306
+
307
+ // Pool errors
308
+ if (message.includes('pool') || message.includes('too many connections')) {
309
+ return new PoolExhaustedError(
310
+ `Connection pool error: ${error.message}`,
311
+ provider
312
+ );
313
+ }
314
+
315
+ // Generic database error
316
+ return new DatabaseError(error.message, provider, undefined, query);
317
+ }
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Database Provider Factory
3
+ * Creates appropriate provider instance based on connection type
4
+ * Uses dynamic imports to reduce memory footprint - providers are loaded on demand
5
+ */
6
+
7
+ import {
8
+ type DatabaseProvider,
9
+ type DatabaseConnection,
10
+ type ProviderOptions,
11
+ } from './types';
12
+ import { DatabaseConfigError } from './errors';
13
+ import { createSSHTunnel, closeSSHTunnel } from '@/lib/ssh/tunnel';
14
+ import { logger } from '@/lib/logger';
15
+
16
+ // ============================================================================
17
+ // Provider Factory
18
+ // ============================================================================
19
+
20
+ /**
21
+ * Create a database provider based on connection configuration
22
+ * Uses dynamic imports to load providers on-demand, reducing initial memory usage
23
+ *
24
+ * @param connection - Database connection configuration
25
+ * @param options - Optional provider options (pooling, timeout, etc.)
26
+ * @returns Promise<DatabaseProvider> instance
27
+ * @throws DatabaseConfigError if connection type is not supported
28
+ *
29
+ * @example
30
+ * // SQL Database
31
+ * const provider = await createDatabaseProvider({
32
+ * id: '1',
33
+ * name: 'My PostgreSQL',
34
+ * type: 'postgres',
35
+ * host: 'localhost',
36
+ * port: 5432,
37
+ * database: 'mydb',
38
+ * user: 'admin',
39
+ * password: 'secret',
40
+ * createdAt: new Date(),
41
+ * });
42
+ *
43
+ * // MongoDB
44
+ * const mongoProvider = await createDatabaseProvider({
45
+ * id: '2',
46
+ * name: 'My MongoDB',
47
+ * type: 'mongodb',
48
+ * connectionString: 'mongodb://localhost:27017/mydb',
49
+ * createdAt: new Date(),
50
+ * });
51
+ *
52
+ * await provider.connect();
53
+ * const result = await provider.query('SELECT * FROM users');
54
+ * await provider.disconnect();
55
+ */
56
+ export async function createDatabaseProvider(
57
+ connection: DatabaseConnection,
58
+ options: ProviderOptions = {}
59
+ ): Promise<DatabaseProvider> {
60
+ // Sanitize user-controlled values to prevent log injection
61
+ const sanitize = (v: string) => v.replace(/[\r\n]/g, ' ').replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
62
+ console.log(`[DB] Creating ${sanitize(connection.type)} provider for "${sanitize(connection.name || '')}"`);
63
+
64
+ switch (connection.type) {
65
+ // SQL Databases - dynamically imported to reduce memory
66
+ case 'postgres': {
67
+ const { PostgresProvider } = await import('./providers/sql/postgres');
68
+ return new PostgresProvider(connection, options);
69
+ }
70
+
71
+ case 'mysql': {
72
+ const { MySQLProvider } = await import('./providers/sql/mysql');
73
+ return new MySQLProvider(connection, options);
74
+ }
75
+
76
+ case 'sqlite': {
77
+ const { SQLiteProvider } = await import('./providers/sql/sqlite');
78
+ return new SQLiteProvider(connection, options);
79
+ }
80
+
81
+ case 'oracle': {
82
+ const { OracleProvider } = await import('./providers/sql/oracle');
83
+ return new OracleProvider(connection, options);
84
+ }
85
+
86
+ case 'mssql': {
87
+ const { MSSQLProvider } = await import('./providers/sql/mssql');
88
+ return new MSSQLProvider(connection, options);
89
+ }
90
+
91
+ // Document Databases - dynamically imported
92
+ case 'mongodb': {
93
+ const { MongoDBProvider } = await import('./providers/document/mongodb');
94
+ return new MongoDBProvider(connection, options);
95
+ }
96
+
97
+ // Key-Value Stores - dynamically imported
98
+ case 'redis': {
99
+ const { RedisProvider } = await import('./providers/keyvalue/redis');
100
+ return new RedisProvider(connection, options);
101
+ }
102
+
103
+ default:
104
+ throw new DatabaseConfigError(
105
+ `Unknown database type: ${connection.type}. Supported types: postgres, mysql, sqlite, oracle, mssql, mongodb, redis`,
106
+ connection.type
107
+ );
108
+ }
109
+ }
110
+
111
+ // ============================================================================
112
+ // Provider Cache (for connection reuse)
113
+ // ============================================================================
114
+
115
+ interface CachedProvider {
116
+ provider: DatabaseProvider;
117
+ lastUsed: number;
118
+ }
119
+
120
+ const providerCache = new Map<string, CachedProvider>();
121
+
122
+ /** Idle timeout: evict providers unused for 30 minutes */
123
+ const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
124
+ /** Sweep interval: check for idle providers every 5 minutes */
125
+ const SWEEP_INTERVAL_MS = 5 * 60 * 1000;
126
+
127
+ let sweepTimer: ReturnType<typeof setInterval> | null = null;
128
+
129
+ /**
130
+ * Evict providers that have been idle longer than maxIdleMs.
131
+ * Called by the periodic sweep timer, but also exported for direct testing.
132
+ *
133
+ * @returns number of evicted providers
134
+ */
135
+ export async function evictIdleProviders(maxIdleMs: number = IDLE_TIMEOUT_MS): Promise<number> {
136
+ const now = Date.now();
137
+ let evicted = 0;
138
+
139
+ for (const [id, entry] of providerCache) {
140
+ if (now - entry.lastUsed >= maxIdleMs) {
141
+ logger.info(`[DB] Evicting idle provider: ${id} (idle ${Math.round((now - entry.lastUsed) / 60000)}min)`);
142
+ try {
143
+ await entry.provider.disconnect();
144
+ } catch (error) {
145
+ logger.warn(`[DB] Error disconnecting idle provider ${id}`, { connectionId: id, error: String(error) });
146
+ }
147
+ providerCache.delete(id);
148
+ // Also close SSH tunnel
149
+ try {
150
+ await closeSSHTunnel(id);
151
+ } catch { /* ignore */ }
152
+ evicted++;
153
+ }
154
+ }
155
+
156
+ // Stop sweeping if cache is empty
157
+ if (providerCache.size === 0 && sweepTimer) {
158
+ clearInterval(sweepTimer);
159
+ sweepTimer = null;
160
+ }
161
+
162
+ return evicted;
163
+ }
164
+
165
+ function startIdleSweep(): void {
166
+ if (sweepTimer) return;
167
+ sweepTimer = setInterval(() => { evictIdleProviders(); }, SWEEP_INTERVAL_MS);
168
+ // Allow process to exit even if timer is running
169
+ if (sweepTimer && typeof sweepTimer === 'object' && 'unref' in sweepTimer) {
170
+ sweepTimer.unref();
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Get or create a database provider with caching
176
+ * Useful for API routes to reuse connections
177
+ *
178
+ * @param connection - Database connection configuration
179
+ * @param options - Optional provider options
180
+ * @returns Cached or new DatabaseProvider instance
181
+ */
182
+ export async function getOrCreateProvider(
183
+ connection: DatabaseConnection,
184
+ options: ProviderOptions = {}
185
+ ): Promise<DatabaseProvider> {
186
+ const cacheKey = connection.id;
187
+
188
+ // Check cache
189
+ const cached = providerCache.get(cacheKey);
190
+
191
+ if (cached?.provider.isConnected()) {
192
+ cached.lastUsed = Date.now();
193
+ return cached.provider;
194
+ }
195
+
196
+ // If SSH tunnel is configured, create tunnel first and rewrite connection
197
+ let effectiveConnection = connection;
198
+ let tunnel: Awaited<ReturnType<typeof createSSHTunnel>> | null = null;
199
+ if (connection.sshTunnel?.enabled && connection.host && connection.port) {
200
+ tunnel = await createSSHTunnel(
201
+ connection.id,
202
+ connection.sshTunnel,
203
+ connection.host,
204
+ connection.port
205
+ );
206
+ // Rewrite connection to point to local tunnel endpoint
207
+ effectiveConnection = {
208
+ ...connection,
209
+ host: tunnel.localHost,
210
+ port: tunnel.localPort,
211
+ };
212
+ }
213
+
214
+ // Create new provider (async - dynamically loads the provider module)
215
+ const provider = await createDatabaseProvider(effectiveConnection, options);
216
+ try {
217
+ await provider.connect();
218
+ } catch (error) {
219
+ // Clean up SSH tunnel if provider connect fails to prevent FD leak
220
+ if (tunnel) {
221
+ await tunnel.close().catch(() => {});
222
+ }
223
+ throw error;
224
+ }
225
+
226
+ // Cache it
227
+ providerCache.set(cacheKey, { provider, lastUsed: Date.now() });
228
+
229
+ // Start idle sweep if not already running
230
+ startIdleSweep();
231
+
232
+ return provider;
233
+ }
234
+
235
+ /**
236
+ * Remove a provider from cache and disconnect
237
+ */
238
+ export async function removeProvider(connectionId: string): Promise<void> {
239
+ const cached = providerCache.get(connectionId);
240
+
241
+ if (cached) {
242
+ try {
243
+ await cached.provider.disconnect();
244
+ } catch (error) {
245
+ logger.warn(`Error disconnecting provider ${connectionId}`, { connectionId, error: String(error) });
246
+ }
247
+ providerCache.delete(connectionId);
248
+ }
249
+
250
+ // Close SSH tunnel if exists
251
+ try {
252
+ await closeSSHTunnel(connectionId);
253
+ } catch (error) {
254
+ logger.warn(`Error closing SSH tunnel for ${connectionId}`, { connectionId, error: String(error) });
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Clear all cached providers
260
+ */
261
+ export async function clearProviderCache(): Promise<void> {
262
+ // Stop idle sweep
263
+ if (sweepTimer) {
264
+ clearInterval(sweepTimer);
265
+ sweepTimer = null;
266
+ }
267
+
268
+ const disconnectPromises: Promise<void>[] = [];
269
+
270
+ for (const [id, entry] of providerCache) {
271
+ disconnectPromises.push(
272
+ entry.provider.disconnect().catch((error) => {
273
+ console.error(`[DB] Error disconnecting provider ${id}:`, error);
274
+ })
275
+ );
276
+ }
277
+
278
+ await Promise.all(disconnectPromises);
279
+ providerCache.clear();
280
+ }
281
+
282
+ /**
283
+ * Get cache statistics
284
+ */
285
+ export function getProviderCacheStats(): { size: number; connections: string[] } {
286
+ return {
287
+ size: providerCache.size,
288
+ connections: Array.from(providerCache.keys()),
289
+ };
290
+ }
291
+
292
+ // ============================================================================
293
+ // Graceful Shutdown
294
+ // ============================================================================
295
+
296
+ let shutdownRegistered = false;
297
+
298
+ /**
299
+ * Register process signal handlers for graceful shutdown.
300
+ * Safe to call multiple times — handlers are only registered once.
301
+ */
302
+ export function registerShutdownHandlers(): void {
303
+ if (shutdownRegistered) return;
304
+ shutdownRegistered = true;
305
+
306
+ const shutdown = async (signal: string) => {
307
+ logger.info(`[DB] Received ${signal}, closing all database connections...`);
308
+ try {
309
+ await clearProviderCache();
310
+ logger.info('[DB] All database connections closed gracefully');
311
+ } catch (error) {
312
+ logger.error('[DB] Error during graceful shutdown', { error: String(error) });
313
+ }
314
+ process.exit(0);
315
+ };
316
+
317
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
318
+ process.on('SIGINT', () => shutdown('SIGINT'));
319
+ }
320
+
321
+ // Auto-register on server-side (not during tests)
322
+ if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'test') {
323
+ registerShutdownHandlers();
324
+ }