@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,127 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { generateTableQuery, generateSelectQuery, shouldRefreshSchema } from '@/lib/query-generators';
3
+ import type { ProviderCapabilities } from '@/lib/db/types';
4
+ import type { ColumnSchema } from '@/lib/types';
5
+
6
+ // ============================================================================
7
+ // Helpers
8
+ // ============================================================================
9
+
10
+ function makeCaps(overrides: Partial<ProviderCapabilities> = {}): ProviderCapabilities {
11
+ return {
12
+ queryLanguage: 'sql',
13
+ supportsExplain: true,
14
+ supportsExternalQueryLimiting: true,
15
+ supportsCreateTable: true,
16
+ supportsMaintenance: true,
17
+ maintenanceOperations: [],
18
+ supportsConnectionString: true,
19
+ defaultPort: 5432,
20
+ schemaRefreshPattern: 'CREATE|ALTER|DROP|TRUNCATE',
21
+ ...overrides,
22
+ };
23
+ }
24
+
25
+ const sampleColumns: ColumnSchema[] = [
26
+ { name: 'id', type: 'integer', nullable: false, isPrimary: true },
27
+ { name: 'name', type: 'varchar(255)', nullable: false, isPrimary: false },
28
+ ];
29
+
30
+ // ============================================================================
31
+ // generateTableQuery
32
+ // ============================================================================
33
+
34
+ describe('generateTableQuery', () => {
35
+ test('SQL (postgres/mysql/sqlite) uses LIMIT 50', () => {
36
+ const result = generateTableQuery('users', makeCaps({ defaultPort: 5432 }));
37
+ expect(result).toBe('SELECT * FROM users LIMIT 50;');
38
+ });
39
+
40
+ test('JSON (MongoDB) generates JSON find query', () => {
41
+ const result = generateTableQuery('users', makeCaps({ queryLanguage: 'json', defaultPort: null }));
42
+ const parsed = JSON.parse(result);
43
+ expect(parsed.collection).toBe('users');
44
+ expect(parsed.operation).toBe('find');
45
+ expect(parsed.options.limit).toBe(50);
46
+ });
47
+
48
+ test('Oracle (port 1521) uses FETCH FIRST 50 ROWS ONLY', () => {
49
+ const result = generateTableQuery('users', makeCaps({ defaultPort: 1521 }));
50
+ expect(result).toContain('FETCH FIRST 50 ROWS ONLY');
51
+ expect(result).toContain('SELECT * FROM users');
52
+ });
53
+
54
+ test('MSSQL (port 1433) uses TOP 50', () => {
55
+ const result = generateTableQuery('users', makeCaps({ defaultPort: 1433 }));
56
+ expect(result).toBe('SELECT TOP 50 * FROM users;');
57
+ });
58
+ });
59
+
60
+ // ============================================================================
61
+ // generateSelectQuery
62
+ // ============================================================================
63
+
64
+ describe('generateSelectQuery', () => {
65
+ test('SQL with columns generates column list and LIMIT 100', () => {
66
+ const result = generateSelectQuery('users', sampleColumns, makeCaps({ defaultPort: 5432 }));
67
+ expect(result).toContain('id');
68
+ expect(result).toContain('name');
69
+ expect(result).toContain('LIMIT 100');
70
+ expect(result).toContain('WHERE 1=1');
71
+ });
72
+
73
+ test('JSON (MongoDB) generates projection', () => {
74
+ const result = generateSelectQuery('users', sampleColumns, makeCaps({ queryLanguage: 'json', defaultPort: null }));
75
+ const parsed = JSON.parse(result);
76
+ expect(parsed.collection).toBe('users');
77
+ expect(parsed.options.projection.id).toBe(1);
78
+ expect(parsed.options.projection.name).toBe(1);
79
+ expect(parsed.options.limit).toBe(100);
80
+ });
81
+
82
+ test('Oracle uses FETCH FIRST 100 ROWS ONLY', () => {
83
+ const result = generateSelectQuery('users', sampleColumns, makeCaps({ defaultPort: 1521 }));
84
+ expect(result).toContain('FETCH FIRST 100 ROWS ONLY');
85
+ expect(result).toContain('id');
86
+ expect(result).toContain('name');
87
+ });
88
+
89
+ test('MSSQL uses TOP 100', () => {
90
+ const result = generateSelectQuery('users', sampleColumns, makeCaps({ defaultPort: 1433 }));
91
+ expect(result).toContain('SELECT TOP 100');
92
+ expect(result).toContain('id');
93
+ expect(result).toContain('name');
94
+ });
95
+ });
96
+
97
+ // ============================================================================
98
+ // shouldRefreshSchema
99
+ // ============================================================================
100
+
101
+ describe('shouldRefreshSchema', () => {
102
+ const pattern = 'CREATE|ALTER|DROP|TRUNCATE';
103
+
104
+ test('CREATE TABLE triggers refresh', () => {
105
+ expect(shouldRefreshSchema('CREATE TABLE users (id INT)', pattern)).toBe(true);
106
+ });
107
+
108
+ test('ALTER TABLE triggers refresh', () => {
109
+ expect(shouldRefreshSchema('ALTER TABLE users ADD COLUMN email TEXT', pattern)).toBe(true);
110
+ });
111
+
112
+ test('DROP TABLE triggers refresh', () => {
113
+ expect(shouldRefreshSchema('DROP TABLE users', pattern)).toBe(true);
114
+ });
115
+
116
+ test('TRUNCATE triggers refresh', () => {
117
+ expect(shouldRefreshSchema('TRUNCATE TABLE users', pattern)).toBe(true);
118
+ });
119
+
120
+ test('SELECT does NOT trigger refresh', () => {
121
+ expect(shouldRefreshSchema('SELECT * FROM users', pattern)).toBe(false);
122
+ });
123
+
124
+ test('INSERT does NOT trigger refresh', () => {
125
+ expect(shouldRefreshSchema('INSERT INTO users VALUES (1)', pattern)).toBe(false);
126
+ });
127
+ });
@@ -0,0 +1,71 @@
1
+ import { describe, test, expect, beforeEach } from 'bun:test';
2
+ import {
3
+ getStorageProviderType,
4
+ isServerStorageEnabled,
5
+ getStorageConfig,
6
+ } from '@/lib/storage/factory';
7
+
8
+ // Clean env before every test to prevent leakage
9
+ beforeEach(() => {
10
+ delete process.env.STORAGE_PROVIDER;
11
+ });
12
+
13
+ describe('storage factory: getStorageProviderType', () => {
14
+ test('returns "local" when STORAGE_PROVIDER not set', () => {
15
+ expect(getStorageProviderType()).toBe('local');
16
+ });
17
+
18
+ test('returns "local" for empty string', () => {
19
+ process.env.STORAGE_PROVIDER = '';
20
+ expect(getStorageProviderType()).toBe('local');
21
+ });
22
+
23
+ test('returns "sqlite" when STORAGE_PROVIDER=sqlite', () => {
24
+ process.env.STORAGE_PROVIDER = 'sqlite';
25
+ expect(getStorageProviderType()).toBe('sqlite');
26
+ });
27
+
28
+ test('returns "postgres" when STORAGE_PROVIDER=postgres', () => {
29
+ process.env.STORAGE_PROVIDER = 'postgres';
30
+ expect(getStorageProviderType()).toBe('postgres');
31
+ });
32
+
33
+ test('returns "local" for unknown values', () => {
34
+ process.env.STORAGE_PROVIDER = 'redis';
35
+ expect(getStorageProviderType()).toBe('local');
36
+ });
37
+
38
+ test('is case-insensitive', () => {
39
+ process.env.STORAGE_PROVIDER = 'SQLite';
40
+ expect(getStorageProviderType()).toBe('sqlite');
41
+ });
42
+ });
43
+
44
+ describe('storage factory: isServerStorageEnabled', () => {
45
+ test('returns false when local', () => {
46
+ expect(isServerStorageEnabled()).toBe(false);
47
+ });
48
+
49
+ test('returns true for sqlite', () => {
50
+ process.env.STORAGE_PROVIDER = 'sqlite';
51
+ expect(isServerStorageEnabled()).toBe(true);
52
+ });
53
+
54
+ test('returns true for postgres', () => {
55
+ process.env.STORAGE_PROVIDER = 'postgres';
56
+ expect(isServerStorageEnabled()).toBe(true);
57
+ });
58
+ });
59
+
60
+ describe('storage factory: getStorageConfig', () => {
61
+ test('returns correct shape for local', () => {
62
+ const config = getStorageConfig();
63
+ expect(config).toEqual({ provider: 'local', serverMode: false });
64
+ });
65
+
66
+ test('returns correct shape for sqlite', () => {
67
+ process.env.STORAGE_PROVIDER = 'sqlite';
68
+ const config = getStorageConfig();
69
+ expect(config).toEqual({ provider: 'sqlite', serverMode: true });
70
+ });
71
+ });
@@ -0,0 +1,114 @@
1
+ import { describe, test, expect, beforeEach } from 'bun:test';
2
+
3
+ if (typeof globalThis.window === 'undefined') {
4
+ // @ts-expect-error — minimal window stub
5
+ globalThis.window = globalThis;
6
+ }
7
+
8
+ import { readJSON, writeJSON, readString, writeString, remove, getKey } from '@/lib/storage/local-storage';
9
+
10
+ describe('local-storage: getKey', () => {
11
+ test('maps known collection names to libredb_ prefix keys', () => {
12
+ expect(getKey('connections')).toBe('libredb_connections');
13
+ expect(getKey('history')).toBe('libredb_history');
14
+ expect(getKey('saved_queries')).toBe('libredb_saved_queries');
15
+ expect(getKey('audit_log')).toBe('libredb_audit_log');
16
+ expect(getKey('masking_config')).toBe('libredb_masking_config');
17
+ expect(getKey('threshold_config')).toBe('libredb_threshold_config');
18
+ });
19
+
20
+ test('falls back to libredb_ prefix for unknown collections', () => {
21
+ expect(getKey('unknown')).toBe('libredb_unknown');
22
+ });
23
+ });
24
+
25
+ describe('local-storage: readJSON / writeJSON', () => {
26
+ beforeEach(() => {
27
+ localStorage.clear();
28
+ });
29
+
30
+ test('writeJSON / readJSON round-trip', () => {
31
+ writeJSON('connections', [{ id: 1 }]);
32
+ expect(readJSON<{ id: number }[]>('connections')).toEqual([{ id: 1 }]);
33
+ });
34
+
35
+ test('readJSON returns null for non-existent key', () => {
36
+ expect(readJSON('nonexistent')).toBeNull();
37
+ });
38
+
39
+ test('readJSON returns null for invalid JSON', () => {
40
+ localStorage.setItem('libredb_connections', 'not-json{{{');
41
+ expect(readJSON('connections')).toBeNull();
42
+ });
43
+ });
44
+
45
+ describe('local-storage: readString / writeString', () => {
46
+ beforeEach(() => {
47
+ localStorage.clear();
48
+ });
49
+
50
+ test('writeString / readString round-trip', () => {
51
+ writeString('active_connection_id', 'conn-42');
52
+ expect(readString('active_connection_id')).toBe('conn-42');
53
+ });
54
+
55
+ test('readString returns null for non-existent key', () => {
56
+ expect(readString('active_connection_id')).toBeNull();
57
+ });
58
+ });
59
+
60
+ describe('local-storage: writeJSON quota handling', () => {
61
+ beforeEach(() => {
62
+ localStorage.clear();
63
+ });
64
+
65
+ test('returns true on success', () => {
66
+ const result = writeJSON('test-key', { data: 'value' });
67
+ expect(result).toBe(true);
68
+ });
69
+
70
+ test('returns false on QuotaExceededError', () => {
71
+ const originalSetItem = localStorage.setItem;
72
+ localStorage.setItem = () => { throw new DOMException('quota exceeded', 'QuotaExceededError'); };
73
+ try {
74
+ const result = writeJSON('test-key', { data: 'value' });
75
+ expect(result).toBe(false);
76
+ } finally {
77
+ localStorage.setItem = originalSetItem;
78
+ }
79
+ });
80
+ });
81
+
82
+ describe('local-storage: writeString quota handling', () => {
83
+ beforeEach(() => {
84
+ localStorage.clear();
85
+ });
86
+
87
+ test('returns true on success', () => {
88
+ const result = writeString('active_connection_id', 'conn-42');
89
+ expect(result).toBe(true);
90
+ });
91
+
92
+ test('returns false on QuotaExceededError', () => {
93
+ const originalSetItem = localStorage.setItem;
94
+ localStorage.setItem = () => { throw new DOMException('quota exceeded', 'QuotaExceededError'); };
95
+ try {
96
+ const result = writeString('active_connection_id', 'conn-42');
97
+ expect(result).toBe(false);
98
+ } finally {
99
+ localStorage.setItem = originalSetItem;
100
+ }
101
+ });
102
+ });
103
+
104
+ describe('local-storage: remove', () => {
105
+ beforeEach(() => {
106
+ localStorage.clear();
107
+ });
108
+
109
+ test('remove deletes the key', () => {
110
+ writeString('active_connection_id', 'conn-42');
111
+ remove('active_connection_id');
112
+ expect(readString('active_connection_id')).toBeNull();
113
+ });
114
+ });
@@ -0,0 +1,312 @@
1
+ import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test';
2
+ import type { ServerStorageProvider } from '@/lib/storage/types';
3
+
4
+ // ── Mock pg ──────────────────────────────────────────────────────────────────
5
+
6
+ /* eslint-disable @typescript-eslint/no-explicit-any */
7
+ const mockQuery = mock(async (..._args: any[]): Promise<any> => ({ rows: [] }));
8
+ const mockRelease = mock(() => {});
9
+ const mockEnd = mock(async () => {});
10
+
11
+ const mockClient = {
12
+ query: mockQuery,
13
+ release: mockRelease,
14
+ };
15
+
16
+ const mockPool: Record<string, any> = {
17
+ query: mockQuery,
18
+ connect: mock(async () => mockClient),
19
+ end: mockEnd,
20
+ };
21
+
22
+ const mockPoolConstructor = mock(() => mockPool);
23
+
24
+ mock.module('pg', () => ({
25
+ Pool: mockPoolConstructor,
26
+ }));
27
+ /* eslint-enable @typescript-eslint/no-explicit-any */
28
+
29
+ import { PostgresStorageProvider } from '@/lib/storage/providers/postgres';
30
+
31
+ describe('PostgresStorageProvider', () => {
32
+ let provider: ServerStorageProvider;
33
+
34
+ beforeEach(() => {
35
+ mockQuery.mockClear();
36
+ mockEnd.mockClear();
37
+ mockRelease.mockClear();
38
+ mockPoolConstructor.mockClear();
39
+ provider = new PostgresStorageProvider('postgresql://localhost:5432/test');
40
+ });
41
+
42
+ afterEach(async () => {
43
+ await provider.close();
44
+ });
45
+
46
+ test('initialize creates table', async () => {
47
+ await provider.initialize();
48
+ expect(mockQuery).toHaveBeenCalledTimes(1);
49
+ const sql = (mockQuery.mock.calls as unknown[][])[0][0] as string;
50
+ expect(sql).toContain('CREATE TABLE IF NOT EXISTS user_storage');
51
+ });
52
+
53
+ test('initialize disables SSL for localhost when no ssl params', async () => {
54
+ const localProvider = new PostgresStorageProvider(
55
+ 'postgresql://localhost:5432/test'
56
+ );
57
+ await localProvider.initialize();
58
+
59
+ const poolConfig = (mockPoolConstructor.mock.calls as unknown[][])[0]?.[0] as {
60
+ ssl?: unknown;
61
+ };
62
+ expect(poolConfig.ssl).toBe(false);
63
+ await localProvider.close();
64
+ });
65
+
66
+ test('initialize disables SSL when sslmode=disable', async () => {
67
+ const localProvider = new PostgresStorageProvider(
68
+ 'postgresql://localhost:5432/test?sslmode=disable'
69
+ );
70
+ await localProvider.initialize();
71
+
72
+ const poolConfig = (mockPoolConstructor.mock.calls as unknown[][])[0]?.[0] as {
73
+ ssl?: unknown;
74
+ };
75
+ expect(poolConfig.ssl).toBe(false);
76
+ await localProvider.close();
77
+ });
78
+
79
+ test('initialize disables SSL for docker local host aliases', async () => {
80
+ const localProvider = new PostgresStorageProvider(
81
+ 'postgresql://host.docker.internal:5432/test'
82
+ );
83
+ await localProvider.initialize();
84
+
85
+ const poolConfig = (mockPoolConstructor.mock.calls as unknown[][])[0]?.[0] as {
86
+ ssl?: unknown;
87
+ };
88
+ expect(poolConfig.ssl).toBe(false);
89
+ await localProvider.close();
90
+ });
91
+
92
+ test('initialize enables SSL when sslmode=require', async () => {
93
+ const cloudProvider = new PostgresStorageProvider(
94
+ 'postgresql://db.example.com:5432/test?sslmode=require'
95
+ );
96
+ await cloudProvider.initialize();
97
+
98
+ const poolConfig = (mockPoolConstructor.mock.calls as unknown[][])[0]?.[0] as {
99
+ ssl?: unknown;
100
+ };
101
+ expect(poolConfig.ssl).toEqual({ rejectUnauthorized: false });
102
+ await cloudProvider.close();
103
+ });
104
+
105
+ test('initialize enables SSL for non-local hosts by default', async () => {
106
+ const cloudProvider = new PostgresStorageProvider(
107
+ 'postgresql://db.internal.example:5432/test'
108
+ );
109
+ await cloudProvider.initialize();
110
+
111
+ const poolConfig = (mockPoolConstructor.mock.calls as unknown[][])[0]?.[0] as {
112
+ ssl?: unknown;
113
+ };
114
+ expect(poolConfig.ssl).toEqual({ rejectUnauthorized: false });
115
+ await cloudProvider.close();
116
+ });
117
+
118
+ test('initialize disables SSL for all loopback 127.x.x.x addresses', async () => {
119
+ const localProvider = new PostgresStorageProvider(
120
+ 'postgresql://127.0.0.42:5432/test'
121
+ );
122
+ await localProvider.initialize();
123
+
124
+ const poolConfig = (mockPoolConstructor.mock.calls as unknown[][])[0]?.[0] as {
125
+ ssl?: unknown;
126
+ };
127
+ expect(poolConfig.ssl).toBe(false);
128
+ await localProvider.close();
129
+ });
130
+
131
+ test('getAllData returns parsed collections', async () => {
132
+ await provider.initialize();
133
+ mockQuery.mockResolvedValueOnce({
134
+ rows: [
135
+ { collection: 'connections', data: JSON.stringify([{ id: 'c1' }]) },
136
+ { collection: 'history', data: JSON.stringify([{ id: 'h1' }]) },
137
+ ],
138
+ });
139
+
140
+ const result = await provider.getAllData('admin@test.com');
141
+ expect(result.connections as unknown).toEqual([{ id: 'c1' }]);
142
+ expect(result.history as unknown).toEqual([{ id: 'h1' }]);
143
+ });
144
+
145
+ test('getCollection returns null when not found', async () => {
146
+ await provider.initialize();
147
+ mockQuery.mockResolvedValueOnce({ rows: [] });
148
+
149
+ const result = await provider.getCollection('admin@test.com', 'connections');
150
+ expect(result).toBeNull();
151
+ });
152
+
153
+ test('getCollection returns parsed data', async () => {
154
+ const data = [{ id: 'c1', name: 'Test' }];
155
+ await provider.initialize();
156
+ mockQuery.mockResolvedValueOnce({
157
+ rows: [{ data: JSON.stringify(data) }],
158
+ });
159
+
160
+ const result = await provider.getCollection('admin@test.com', 'connections');
161
+ expect(result as unknown).toEqual(data);
162
+ });
163
+
164
+ test('setCollection calls INSERT with ON CONFLICT', async () => {
165
+ await provider.initialize();
166
+ mockQuery.mockResolvedValueOnce({ rows: [] });
167
+
168
+ await provider.setCollection('admin@test.com', 'connections', []);
169
+
170
+ const calls = mockQuery.mock.calls as unknown[][];
171
+ const lastCall = calls[calls.length - 1];
172
+ const sql = lastCall[0] as string;
173
+ expect(sql).toContain('INSERT INTO user_storage');
174
+ expect(sql).toContain('ON CONFLICT');
175
+ });
176
+
177
+ test('isHealthy returns true on success', async () => {
178
+ await provider.initialize();
179
+ mockQuery.mockResolvedValueOnce({ rows: [{ ok: 1 }] });
180
+
181
+ expect(await provider.isHealthy()).toBe(true);
182
+ });
183
+
184
+ test('isHealthy returns false on error', async () => {
185
+ await provider.initialize();
186
+ mockQuery.mockRejectedValueOnce(new Error('Connection lost'));
187
+
188
+ expect(await provider.isHealthy()).toBe(false);
189
+ });
190
+
191
+ test('close calls pool.end()', async () => {
192
+ await provider.initialize();
193
+ await provider.close();
194
+ expect(mockEnd).toHaveBeenCalledTimes(1);
195
+ });
196
+
197
+ test('mergeData uses transaction', async () => {
198
+ await provider.initialize();
199
+
200
+ const mockClientQuery = mock(async (): Promise<{ rows: unknown[] }> => ({ rows: [] }));
201
+ const client = {
202
+ query: mockClientQuery,
203
+ release: mock(() => {}),
204
+ };
205
+ mockPool.connect = mock(async () => client);
206
+
207
+ await provider.mergeData('admin@test.com', {
208
+ connections: [{ id: 'c1', name: 'Test', type: 'postgres', createdAt: new Date() } as import('@/lib/types').DatabaseConnection],
209
+ });
210
+
211
+ const queries = (mockClientQuery.mock.calls as unknown[][]).map((c) => c[0] as string);
212
+ expect(queries[0]).toBe('BEGIN');
213
+ expect(queries[queries.length - 1]).toBe('COMMIT');
214
+ });
215
+
216
+ test('mergeData rolls back on error and releases client', async () => {
217
+ await provider.initialize();
218
+
219
+ let callCount = 0;
220
+ const mockClientQuery = mock(async (sql: string): Promise<{ rows: unknown[] }> => {
221
+ callCount++;
222
+ // Fail on the INSERT (3rd call: BEGIN, then INSERT fails)
223
+ if (callCount === 2) throw new Error('Insert failed');
224
+ return { rows: [] };
225
+ });
226
+ const mockClientRelease = mock(() => {});
227
+ const client = {
228
+ query: mockClientQuery,
229
+ release: mockClientRelease,
230
+ };
231
+ mockPool.connect = mock(async () => client);
232
+
233
+ await expect(
234
+ provider.mergeData('admin@test.com', {
235
+ connections: [{ id: 'c1', name: 'Test', type: 'postgres', createdAt: new Date() } as import('@/lib/types').DatabaseConnection],
236
+ })
237
+ ).rejects.toThrow('Insert failed');
238
+
239
+ // ROLLBACK should have been called
240
+ const queries = (mockClientQuery.mock.calls as unknown[][]).map((c) => c[0] as string);
241
+ expect(queries).toContain('ROLLBACK');
242
+ // Client always released (finally block)
243
+ expect(mockClientRelease).toHaveBeenCalledTimes(1);
244
+ });
245
+
246
+ test('mergeData only writes provided collections', async () => {
247
+ await provider.initialize();
248
+
249
+ const mockClientQuery = mock(async (): Promise<{ rows: unknown[] }> => ({ rows: [] }));
250
+ const client = {
251
+ query: mockClientQuery,
252
+ release: mock(() => {}),
253
+ };
254
+ mockPool.connect = mock(async () => client);
255
+
256
+ await provider.mergeData('admin@test.com', {
257
+ connections: [{ id: 'c1', name: 'Test', type: 'postgres', createdAt: new Date() } as import('@/lib/types').DatabaseConnection],
258
+ });
259
+
260
+ const queries = (mockClientQuery.mock.calls as unknown[][]).map((c) => c[0] as string);
261
+ // BEGIN + 1 INSERT + COMMIT = 3 queries
262
+ expect(queries.length).toBe(3);
263
+ expect(queries[0]).toBe('BEGIN');
264
+ expect(queries[1]).toContain('INSERT INTO user_storage');
265
+ expect(queries[2]).toBe('COMMIT');
266
+ });
267
+
268
+ test('getCollection returns null for corrupted JSON', async () => {
269
+ await provider.initialize();
270
+ mockQuery.mockResolvedValueOnce({
271
+ rows: [{ data: 'invalid-json{{{' }],
272
+ });
273
+
274
+ const result = await provider.getCollection('admin@test.com', 'connections');
275
+ expect(result).toBeNull();
276
+ });
277
+
278
+ test('getAllData skips corrupted JSON rows', async () => {
279
+ await provider.initialize();
280
+ mockQuery.mockResolvedValueOnce({
281
+ rows: [
282
+ { collection: 'connections', data: JSON.stringify([{ id: 'c1' }]) },
283
+ { collection: 'history', data: 'corrupted{{{' },
284
+ ],
285
+ });
286
+
287
+ const result = await provider.getAllData('admin@test.com');
288
+ expect(result.connections as unknown).toEqual([{ id: 'c1' }]);
289
+ expect(result.history).toBeUndefined();
290
+ });
291
+
292
+ test('initialize throws when no connection string', async () => {
293
+ const origEnv = process.env.STORAGE_POSTGRES_URL;
294
+ delete process.env.STORAGE_POSTGRES_URL;
295
+ try {
296
+ const noUrlProvider = new PostgresStorageProvider('');
297
+ await expect(noUrlProvider.initialize()).rejects.toThrow('STORAGE_POSTGRES_URL is required');
298
+ } finally {
299
+ if (origEnv !== undefined) process.env.STORAGE_POSTGRES_URL = origEnv;
300
+ }
301
+ });
302
+
303
+ test('close on uninitialized provider does not throw', async () => {
304
+ const freshProvider = new PostgresStorageProvider('postgresql://localhost/test');
305
+ await expect(freshProvider.close()).resolves.toBeUndefined();
306
+ });
307
+
308
+ test('ensurePool throws when not initialized', async () => {
309
+ const freshProvider = new PostgresStorageProvider('postgresql://localhost/test');
310
+ await expect(freshProvider.getAllData('test@test.com')).rejects.toThrow('not initialized');
311
+ });
312
+ });