@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.
- package/.claude/settings.local.json +127 -0
- package/.cursorrules +426 -0
- package/.devin/wiki.json +143 -0
- package/.dockerignore +80 -0
- package/.env.example +159 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +49 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +57 -0
- package/.github/workflows/ci.yml +185 -0
- package/.github/workflows/codeql.yml +57 -0
- package/.github/workflows/docker-build-push.yml +118 -0
- package/.github/workflows/helm-release.yml +113 -0
- package/CLAUDE.md +265 -0
- package/CODE_OF_CONDUCT.md +124 -0
- package/CONTRIBUTING.md +154 -0
- package/Dockerfile +73 -0
- package/LICENSE +21 -0
- package/README.md +614 -0
- package/SECURITY.md +107 -0
- package/artifacthub-repo.yml +4 -0
- package/bun.lock +1714 -0
- package/bunfig.toml +3 -0
- package/charts/libredb-studio/.helmignore +11 -0
- package/charts/libredb-studio/Chart.lock +6 -0
- package/charts/libredb-studio/Chart.yaml +50 -0
- package/charts/libredb-studio/README.md +206 -0
- package/charts/libredb-studio/templates/NOTES.txt +59 -0
- package/charts/libredb-studio/templates/_helpers.tpl +135 -0
- package/charts/libredb-studio/templates/configmap.yaml +37 -0
- package/charts/libredb-studio/templates/deployment.yaml +184 -0
- package/charts/libredb-studio/templates/hpa.yaml +32 -0
- package/charts/libredb-studio/templates/ingress.yaml +41 -0
- package/charts/libredb-studio/templates/networkpolicy.yaml +50 -0
- package/charts/libredb-studio/templates/pdb.yaml +18 -0
- package/charts/libredb-studio/templates/pvc.yaml +23 -0
- package/charts/libredb-studio/templates/secret.yaml +30 -0
- package/charts/libredb-studio/templates/seed-configmap.yaml +11 -0
- package/charts/libredb-studio/templates/service.yaml +22 -0
- package/charts/libredb-studio/templates/serviceaccount.yaml +13 -0
- package/charts/libredb-studio/values.schema.json +246 -0
- package/charts/libredb-studio/values.yaml +286 -0
- package/components.json +22 -0
- package/conductor/code_styleguides/typescript.md +43 -0
- package/conductor/product-guidelines.md +43 -0
- package/conductor/product.md +3 -0
- package/conductor/setup_state.json +1 -0
- package/conductor/tech-stack.md +39 -0
- package/conductor/tracks/enhance_postgres_monitoring_20251227/metadata.json +8 -0
- package/conductor/tracks/enhance_postgres_monitoring_20251227/plan.md +44 -0
- package/conductor/tracks/enhance_postgres_monitoring_20251227/spec.md +31 -0
- package/conductor/tracks.md +8 -0
- package/conductor/workflow.md +333 -0
- package/database-compose.yml +55 -0
- package/docker/postgres-init/01-extensions.sql +10 -0
- package/docker/postgres-init/02-sample-data.sql +585 -0
- package/docker/postgres.yml +68 -0
- package/docker-compose.yml +38 -0
- package/docs/AI_PLAN.md +74 -0
- package/docs/API_DOCS.md +875 -0
- package/docs/ARCHITECTURE.md +218 -0
- package/docs/DATABASE_PROVIDERS.md +358 -0
- package/docs/FEATURES.md +116 -0
- package/docs/HELM_CHART.md +252 -0
- package/docs/LOGIN_PAGE.md +178 -0
- package/docs/MONACO_EDITOR_PERFORMANCE.md +315 -0
- package/docs/OIDC_ARCH.md +681 -0
- package/docs/OIDC_SETUP.md +322 -0
- package/docs/POSTGRES_METRICS.md +516 -0
- package/docs/QUERY_OPTIMIZATION.md +370 -0
- package/docs/SEED_CONNECTIONS.md +468 -0
- package/docs/SQL_ALIAS_COMPLETION.md +190 -0
- package/docs/STORAGE_ARCHITECTURE.md +565 -0
- package/docs/STORAGE_QUICK_SETUP.md +419 -0
- package/docs/TECHNICAL_PLAN.md +36 -0
- package/docs/THEMING.md +345 -0
- package/docs/adding-a-new-database-provider.md +642 -0
- package/docs/backlogs/000-PLATFORM_DATA_SYNC_DATABASE.md +360 -0
- package/docs/backlogs/001-INLINE_DATA_EDITING.md +118 -0
- package/docs/backlogs/002-DATA_IMPORT.md +215 -0
- package/docs/backlogs/003-QUERY_TIME_MACHINE.md +183 -0
- package/docs/backlogs/004-AI_DATA_STORYTELLER.md +292 -0
- package/docs/backlogs/005-QUERY_PLAYGROUND.md +352 -0
- package/docs/backlogs/006-DATA_MASKING.md +418 -0
- package/docs/enterprise-features.md +718 -0
- package/docs/kubernetes-helm-chart-artifacthub-plan.md +803 -0
- package/docs/medium-koyeb-article-en.md +215 -0
- package/docs/plans/test-plans.md +445 -0
- package/docs/releases/RELEASE.V0.3.0.md +22 -0
- package/docs/releases/RELEASE.V0.4.0.md +154 -0
- package/docs/releases/RELEASE.V0.5.0.md +252 -0
- package/docs/releases/RELEASE_v0.5.6.md +145 -0
- package/docs/releases/RELEASE_v0.6.1.md +303 -0
- package/docs/releases/RELEASE_v0.6.7.md +292 -0
- package/docs/releases/RELEASE_v0.7.0.md +332 -0
- package/docs/releases/RELEASE_v0.8.0.md +521 -0
- package/docs/sampledb/titanic.sql +1379 -0
- package/docs/superpowers/plans/2026-03-25-seed-connections.md +1362 -0
- package/docs/superpowers/specs/2026-03-25-seed-connections-design.md +590 -0
- package/e2e/admin-dashboard.spec.ts +64 -0
- package/e2e/connection-management.spec.ts +58 -0
- package/e2e/export.spec.ts +34 -0
- package/e2e/login.spec.ts +85 -0
- package/e2e/query-execution.spec.ts +35 -0
- package/e2e/tab-management.spec.ts +64 -0
- package/eslint.config.mjs +28 -0
- package/fly.toml +43 -0
- package/next.config.ts +32 -0
- package/package.json +130 -0
- package/playwright.config.ts +34 -0
- package/postcss.config.mjs +7 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/logo.svg +32 -0
- package/public/next.svg +1 -0
- package/public/screenshots/code-generator.png +0 -0
- package/public/screenshots/connection-modal.png +0 -0
- package/public/screenshots/data-profiler.png +0 -0
- package/public/screenshots/erd-diagram.png +0 -0
- package/public/screenshots/hero-editor.png +0 -0
- package/public/screenshots/nl2sql.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/render.yaml +58 -0
- package/scripts/merge-lcov.mjs +239 -0
- package/sonar-project.properties +16 -0
- package/src/app/admin/error.tsx +46 -0
- package/src/app/admin/page.tsx +10 -0
- package/src/app/api/admin/audit/route.ts +52 -0
- package/src/app/api/admin/fleet-health/route.ts +81 -0
- package/src/app/api/ai/autopilot/route.ts +105 -0
- package/src/app/api/ai/chat/route.ts +132 -0
- package/src/app/api/ai/describe-schema/route.ts +52 -0
- package/src/app/api/ai/explain/route.ts +86 -0
- package/src/app/api/ai/impact/route.ts +97 -0
- package/src/app/api/ai/index-advisor/route.ts +98 -0
- package/src/app/api/ai/nl2sql/route.ts +87 -0
- package/src/app/api/ai/query-safety/route.ts +87 -0
- package/src/app/api/auth/login/route.ts +62 -0
- package/src/app/api/auth/logout/route.ts +25 -0
- package/src/app/api/auth/me/route.ts +10 -0
- package/src/app/api/auth/oidc/callback/route.ts +82 -0
- package/src/app/api/auth/oidc/login/route.ts +43 -0
- package/src/app/api/connections/managed/route.ts +35 -0
- package/src/app/api/db/cancel/route.ts +42 -0
- package/src/app/api/db/disconnect/route.ts +28 -0
- package/src/app/api/db/health/route.ts +49 -0
- package/src/app/api/db/maintenance/route.ts +72 -0
- package/src/app/api/db/monitoring/route.ts +62 -0
- package/src/app/api/db/multi-query/route.ts +116 -0
- package/src/app/api/db/pool-stats/route.ts +37 -0
- package/src/app/api/db/profile/route.ts +144 -0
- package/src/app/api/db/provider-meta/route.ts +49 -0
- package/src/app/api/db/query/route.ts +50 -0
- package/src/app/api/db/schema/route.ts +47 -0
- package/src/app/api/db/schema-snapshot/route.ts +42 -0
- package/src/app/api/db/test-connection/route.ts +55 -0
- package/src/app/api/db/transaction/route.ts +111 -0
- package/src/app/api/storage/[collection]/route.ts +67 -0
- package/src/app/api/storage/config/route.ts +17 -0
- package/src/app/api/storage/migrate/route.ts +45 -0
- package/src/app/api/storage/route.ts +32 -0
- package/src/app/error.tsx +49 -0
- package/src/app/global-error.tsx +55 -0
- package/src/app/globals.css +146 -0
- package/src/app/icon.svg +42 -0
- package/src/app/layout.tsx +34 -0
- package/src/app/login/login-form.tsx +301 -0
- package/src/app/login/page.tsx +11 -0
- package/src/app/monitoring/page.tsx +8 -0
- package/src/app/not-found.tsx +29 -0
- package/src/app/page.tsx +5 -0
- package/src/components/AIAutopilotPanel.tsx +238 -0
- package/src/components/CodeGenerator.tsx +271 -0
- package/src/components/CommandPalette.tsx +227 -0
- package/src/components/ConnectionModal.tsx +759 -0
- package/src/components/CreateTableModal.tsx +281 -0
- package/src/components/DataCharts.tsx +962 -0
- package/src/components/DataImportModal.tsx +582 -0
- package/src/components/DataProfiler.tsx +335 -0
- package/src/components/DatabaseDocs.tsx +251 -0
- package/src/components/MaskingSettings.tsx +414 -0
- package/src/components/MobileNav.tsx +50 -0
- package/src/components/NL2SQLPanel.tsx +281 -0
- package/src/components/PivotTable.tsx +257 -0
- package/src/components/QueryEditor.tsx +760 -0
- package/src/components/QueryHistory.tsx +344 -0
- package/src/components/QuerySafetyDialog.tsx +290 -0
- package/src/components/ResultsGrid.tsx +644 -0
- package/src/components/SaveQueryModal.tsx +104 -0
- package/src/components/SavedQueries.tsx +128 -0
- package/src/components/SchemaDiagram.tsx +473 -0
- package/src/components/SchemaDiff.tsx +473 -0
- package/src/components/SnapshotTimeline.tsx +116 -0
- package/src/components/Studio.tsx +639 -0
- package/src/components/TestDataGenerator.tsx +261 -0
- package/src/components/VisualExplain.tsx +820 -0
- package/src/components/admin/AdminDashboard.tsx +163 -0
- package/src/components/admin/tabs/AuditTab.tsx +531 -0
- package/src/components/admin/tabs/MonitoringEmbed.tsx +11 -0
- package/src/components/admin/tabs/OperationsTab.tsx +646 -0
- package/src/components/admin/tabs/OverviewTab.tsx +1328 -0
- package/src/components/admin/tabs/SecurityTab.tsx +284 -0
- package/src/components/community-section.tsx +92 -0
- package/src/components/icons/db-icons.tsx +84 -0
- package/src/components/libredb-logo.tsx +61 -0
- package/src/components/monitoring/MonitoringDashboard.tsx +345 -0
- package/src/components/monitoring/tabs/MetricChart.tsx +82 -0
- package/src/components/monitoring/tabs/OverviewTab.tsx +263 -0
- package/src/components/monitoring/tabs/PerformanceTab.tsx +254 -0
- package/src/components/monitoring/tabs/PoolTab.tsx +174 -0
- package/src/components/monitoring/tabs/QueriesTab.tsx +287 -0
- package/src/components/monitoring/tabs/SessionsTab.tsx +316 -0
- package/src/components/monitoring/tabs/StorageTab.tsx +335 -0
- package/src/components/monitoring/tabs/TablesTab.tsx +300 -0
- package/src/components/results-grid/ResultCard.tsx +111 -0
- package/src/components/results-grid/RowDetailSheet.tsx +178 -0
- package/src/components/results-grid/StatsBar.tsx +201 -0
- package/src/components/results-grid/index.ts +1 -0
- package/src/components/results-grid/utils.ts +23 -0
- package/src/components/schema-explorer/ColumnList.tsx +53 -0
- package/src/components/schema-explorer/SchemaExplorer.tsx +182 -0
- package/src/components/schema-explorer/TableItem.tsx +210 -0
- package/src/components/schema-explorer/index.ts +1 -0
- package/src/components/sidebar/ConnectionItem.tsx +105 -0
- package/src/components/sidebar/ConnectionsList.tsx +62 -0
- package/src/components/sidebar/Sidebar.tsx +130 -0
- package/src/components/sidebar/index.ts +2 -0
- package/src/components/studio/BottomPanel.tsx +286 -0
- package/src/components/studio/QueryToolbar.tsx +180 -0
- package/src/components/studio/StudioDesktopHeader.tsx +114 -0
- package/src/components/studio/StudioMobileHeader.tsx +340 -0
- package/src/components/studio/StudioTabBar.tsx +82 -0
- package/src/components/studio/index.ts +5 -0
- package/src/components/ui/accordion.tsx +66 -0
- package/src/components/ui/alert-dialog.tsx +157 -0
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/aspect-ratio.tsx +11 -0
- package/src/components/ui/avatar.tsx +53 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/breadcrumb.tsx +109 -0
- package/src/components/ui/button-group.tsx +83 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/calendar.tsx +216 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/carousel.tsx +241 -0
- package/src/components/ui/chart.tsx +357 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/command.tsx +184 -0
- package/src/components/ui/context-menu.tsx +252 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/drawer.tsx +135 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +248 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/hover-card.tsx +44 -0
- package/src/components/ui/input-group.tsx +170 -0
- package/src/components/ui/input-otp.tsx +77 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/item.tsx +193 -0
- package/src/components/ui/kbd.tsx +28 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/menubar.tsx +276 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/pagination.tsx +127 -0
- package/src/components/ui/popover.tsx +48 -0
- package/src/components/ui/progress.tsx +31 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/resizable.tsx +56 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +187 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +139 -0
- package/src/components/ui/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +63 -0
- package/src/components/ui/sonner.tsx +40 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +83 -0
- package/src/components/ui/toggle.tsx +47 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/exports/components.ts +15 -0
- package/src/exports/index.ts +4 -0
- package/src/exports/providers.ts +4 -0
- package/src/exports/types.ts +26 -0
- package/src/hooks/use-ai-chat.ts +182 -0
- package/src/hooks/use-all-connections.ts +66 -0
- package/src/hooks/use-api-call.ts +71 -0
- package/src/hooks/use-auth.ts +51 -0
- package/src/hooks/use-connection-form.ts +349 -0
- package/src/hooks/use-connection-manager.ts +169 -0
- package/src/hooks/use-connection-payload.ts +15 -0
- package/src/hooks/use-inline-editing.ts +109 -0
- package/src/hooks/use-mobile.ts +20 -0
- package/src/hooks/use-monitoring-data.ts +270 -0
- package/src/hooks/use-provider-metadata.ts +62 -0
- package/src/hooks/use-query-execution.ts +478 -0
- package/src/hooks/use-storage-sync.ts +259 -0
- package/src/hooks/use-tab-manager.ts +231 -0
- package/src/hooks/use-toast.ts +20 -0
- package/src/hooks/use-transaction-control.ts +64 -0
- package/src/lib/api/error-codes.ts +30 -0
- package/src/lib/api/errors.ts +236 -0
- package/src/lib/api/with-error-handler.ts +41 -0
- package/src/lib/audit.ts +105 -0
- package/src/lib/auth.ts +87 -0
- package/src/lib/connection-string-parser.ts +172 -0
- package/src/lib/data-masking.ts +385 -0
- package/src/lib/db/base-provider.ts +325 -0
- package/src/lib/db/errors.ts +317 -0
- package/src/lib/db/factory.ts +324 -0
- package/src/lib/db/index.ts +123 -0
- package/src/lib/db/providers/document/index.ts +6 -0
- package/src/lib/db/providers/document/mongodb.ts +992 -0
- package/src/lib/db/providers/keyvalue/redis.ts +554 -0
- package/src/lib/db/providers/sql/index.ts +11 -0
- package/src/lib/db/providers/sql/mssql.ts +1065 -0
- package/src/lib/db/providers/sql/mysql.ts +978 -0
- package/src/lib/db/providers/sql/oracle.ts +1044 -0
- package/src/lib/db/providers/sql/postgres.ts +1179 -0
- package/src/lib/db/providers/sql/sql-base.ts +174 -0
- package/src/lib/db/providers/sql/sqlite.ts +721 -0
- package/src/lib/db/types.ts +437 -0
- package/src/lib/db/utils/pool-manager.ts +287 -0
- package/src/lib/db/utils/query-limiter.ts +239 -0
- package/src/lib/db-ui-config.ts +86 -0
- package/src/lib/editor/mongodb-completions.ts +172 -0
- package/src/lib/editor/sql-completions.ts +280 -0
- package/src/lib/llm/base-provider.ts +117 -0
- package/src/lib/llm/factory.ts +102 -0
- package/src/lib/llm/index.ts +90 -0
- package/src/lib/llm/providers/custom.ts +181 -0
- package/src/lib/llm/providers/gemini.ts +126 -0
- package/src/lib/llm/providers/ollama.ts +154 -0
- package/src/lib/llm/providers/openai.ts +146 -0
- package/src/lib/llm/types.ts +173 -0
- package/src/lib/llm/utils/config.ts +187 -0
- package/src/lib/llm/utils/retry.ts +119 -0
- package/src/lib/llm/utils/streaming.ts +202 -0
- package/src/lib/logger.ts +127 -0
- package/src/lib/monitoring-thresholds.ts +44 -0
- package/src/lib/oidc.ts +262 -0
- package/src/lib/query-generators.ts +61 -0
- package/src/lib/schema-diff/diff-engine.ts +273 -0
- package/src/lib/schema-diff/migration-generator.ts +208 -0
- package/src/lib/schema-diff/types.ts +55 -0
- package/src/lib/seed/config-loader.ts +79 -0
- package/src/lib/seed/connection-filter.ts +49 -0
- package/src/lib/seed/credential-resolver.ts +62 -0
- package/src/lib/seed/index.ts +40 -0
- package/src/lib/seed/resolve-connection.ts +57 -0
- package/src/lib/seed/types.ts +69 -0
- package/src/lib/sql/alias-extractor.ts +267 -0
- package/src/lib/sql/index.ts +8 -0
- package/src/lib/sql/statement-splitter.ts +167 -0
- package/src/lib/sql/types.ts +40 -0
- package/src/lib/ssh/tunnel.ts +142 -0
- package/src/lib/storage/factory.ts +84 -0
- package/src/lib/storage/index.ts +14 -0
- package/src/lib/storage/local-storage.ts +99 -0
- package/src/lib/storage/providers/postgres.ts +225 -0
- package/src/lib/storage/providers/sqlite.ts +153 -0
- package/src/lib/storage/storage-facade.ts +272 -0
- package/src/lib/storage/types.ts +75 -0
- package/src/lib/time-series-buffer.ts +58 -0
- package/src/lib/types.ts +173 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +104 -0
- package/src/types/db-drivers.d.ts +23 -0
- package/src/types/html2canvas.d.ts +9 -0
- package/tests/api/admin/audit.test.ts +178 -0
- package/tests/api/admin/fleet-health.test.ts +183 -0
- package/tests/api/ai/autopilot.test.ts +174 -0
- package/tests/api/ai/chat.test.ts +250 -0
- package/tests/api/ai/describe-schema.test.ts +266 -0
- package/tests/api/ai/explain.test.ts +199 -0
- package/tests/api/ai/impact.test.ts +168 -0
- package/tests/api/ai/index-advisor.test.ts +171 -0
- package/tests/api/ai/nl2sql.test.ts +202 -0
- package/tests/api/ai/query-safety.test.ts +196 -0
- package/tests/api/auth/login.test.ts +170 -0
- package/tests/api/auth/logout.test.ts +140 -0
- package/tests/api/auth/me.test.ts +73 -0
- package/tests/api/auth/oidc-callback.test.ts +215 -0
- package/tests/api/auth/oidc-login.test.ts +127 -0
- package/tests/api/db/cancel.test.ts +198 -0
- package/tests/api/db/disconnect.test.ts +124 -0
- package/tests/api/db/health.test.ts +222 -0
- package/tests/api/db/maintenance.test.ts +263 -0
- package/tests/api/db/monitoring.test.ts +221 -0
- package/tests/api/db/multi-query.test.ts +316 -0
- package/tests/api/db/pool-stats.test.ts +135 -0
- package/tests/api/db/profile.test.ts +330 -0
- package/tests/api/db/provider-meta.test.ts +193 -0
- package/tests/api/db/query.test.ts +314 -0
- package/tests/api/db/schema-snapshot.test.ts +170 -0
- package/tests/api/db/schema.test.ts +191 -0
- package/tests/api/db/test-connection.test.ts +185 -0
- package/tests/api/db/transaction.test.ts +314 -0
- package/tests/api/proxy.test.ts +191 -0
- package/tests/api/seed/managed-route.test.ts +113 -0
- package/tests/api/storage/config.test.ts +42 -0
- package/tests/api/storage/storage-routes.test.ts +309 -0
- package/tests/components/AIAutopilotPanel.test.tsx +756 -0
- package/tests/components/AdminPage.test.tsx +33 -0
- package/tests/components/CodeGenerator.test.tsx +182 -0
- package/tests/components/CommandPalette.test.tsx +428 -0
- package/tests/components/CommunitySection.test.tsx +91 -0
- package/tests/components/ConnectionModal.mobile.test.tsx +284 -0
- package/tests/components/ConnectionModal.test.tsx +570 -0
- package/tests/components/CreateTableModal.test.tsx +383 -0
- package/tests/components/DataCharts.test.tsx +739 -0
- package/tests/components/DataImportModal.test.tsx +751 -0
- package/tests/components/DataProfiler.test.tsx +589 -0
- package/tests/components/DatabaseDocs.test.tsx +353 -0
- package/tests/components/LoginPage.test.tsx +163 -0
- package/tests/components/LoginPageOIDC.test.tsx +92 -0
- package/tests/components/MaskingSettings.test.tsx +498 -0
- package/tests/components/MobileNav.test.tsx +30 -0
- package/tests/components/MonitoringPage.test.tsx +32 -0
- package/tests/components/NL2SQLPanel.test.tsx +621 -0
- package/tests/components/Page.test.tsx +33 -0
- package/tests/components/PivotTable.test.tsx +350 -0
- package/tests/components/QueryEditor.test.tsx +1730 -0
- package/tests/components/QueryHistory.test.tsx +572 -0
- package/tests/components/QuerySafetyDialog.test.tsx +586 -0
- package/tests/components/ResultsGrid.test.tsx +804 -0
- package/tests/components/RootLayout.test.tsx +83 -0
- package/tests/components/SaveQueryModal.test.tsx +25 -0
- package/tests/components/SavedQueries.test.tsx +43 -0
- package/tests/components/SchemaDiagram.test.tsx +1034 -0
- package/tests/components/SchemaDiff.test.tsx +906 -0
- package/tests/components/SnapshotTimeline.test.tsx +174 -0
- package/tests/components/Studio.test.tsx +1030 -0
- package/tests/components/TestDataGenerator.test.tsx +291 -0
- package/tests/components/VisualExplain.test.tsx +704 -0
- package/tests/components/admin/AdminDashboard.test.tsx +205 -0
- package/tests/components/admin/AuditTab.test.tsx +220 -0
- package/tests/components/admin/MonitoringEmbed.test.tsx +58 -0
- package/tests/components/admin/OperationsTab.test.tsx +975 -0
- package/tests/components/admin/OverviewTab.test.tsx +254 -0
- package/tests/components/admin/SecurityTab.test.tsx +467 -0
- package/tests/components/monitoring/MetricChart.test.tsx +111 -0
- package/tests/components/monitoring/MonitoringDashboard.test.tsx +259 -0
- package/tests/components/monitoring/OverviewTab.test.tsx +78 -0
- package/tests/components/monitoring/PerformanceTab.test.tsx +87 -0
- package/tests/components/monitoring/PoolTab.test.tsx +42 -0
- package/tests/components/monitoring/QueriesTab.test.tsx +80 -0
- package/tests/components/monitoring/SessionsTab.test.tsx +154 -0
- package/tests/components/monitoring/StorageTab.test.tsx +127 -0
- package/tests/components/monitoring/TablesTab.test.tsx +153 -0
- package/tests/components/results-grid/ResultCard.test.tsx +105 -0
- package/tests/components/results-grid/RowDetailSheet.test.tsx +308 -0
- package/tests/components/results-grid/StatsBar.test.tsx +162 -0
- package/tests/components/schema-explorer/ColumnList.test.tsx +151 -0
- package/tests/components/schema-explorer/SchemaExplorer.test.tsx +461 -0
- package/tests/components/schema-explorer/TableItem.test.tsx +415 -0
- package/tests/components/sidebar/ConnectionItem.test.tsx +201 -0
- package/tests/components/sidebar/ConnectionsList.test.tsx +176 -0
- package/tests/components/sidebar/Sidebar.test.tsx +187 -0
- package/tests/components/studio/BottomPanel.test.tsx +383 -0
- package/tests/components/studio/QueryToolbar.test.tsx +321 -0
- package/tests/components/studio/StudioDesktopHeader.test.tsx +377 -0
- package/tests/components/studio/StudioMobileHeader.test.tsx +198 -0
- package/tests/components/studio/StudioTabBar.test.tsx +331 -0
- package/tests/fixtures/connections.ts +96 -0
- package/tests/fixtures/masking-configs.ts +86 -0
- package/tests/fixtures/query-results.ts +71 -0
- package/tests/fixtures/schemas.ts +64 -0
- package/tests/fixtures/seed-connections/invalid-config.yaml +7 -0
- package/tests/fixtures/seed-connections/minimal-config.yaml +8 -0
- package/tests/fixtures/seed-connections/mixed-credentials.yaml +23 -0
- package/tests/fixtures/seed-connections/multi-role-config.yaml +30 -0
- package/tests/fixtures/seed-connections/valid-config.json +15 -0
- package/tests/fixtures/seed-connections/valid-config.yaml +51 -0
- package/tests/helpers/mock-fetch.ts +59 -0
- package/tests/helpers/mock-monaco.ts +112 -0
- package/tests/helpers/mock-navigation.ts +28 -0
- package/tests/helpers/mock-next.ts +80 -0
- package/tests/helpers/mock-provider.ts +133 -0
- package/tests/helpers/mock-sonner.ts +29 -0
- package/tests/helpers/render-with-providers.tsx +19 -0
- package/tests/hooks/use-ai-chat.test.ts +600 -0
- package/tests/hooks/use-auth.test.ts +371 -0
- package/tests/hooks/use-connection-form.test.ts +743 -0
- package/tests/hooks/use-connection-manager.test.ts +466 -0
- package/tests/hooks/use-inline-editing.test.ts +321 -0
- package/tests/hooks/use-mobile.test.ts +177 -0
- package/tests/hooks/use-monitoring-data.test.ts +819 -0
- package/tests/hooks/use-provider-metadata.test.ts +228 -0
- package/tests/hooks/use-query-execution.test.ts +1212 -0
- package/tests/hooks/use-tab-manager.test.ts +756 -0
- package/tests/hooks/use-toast.test.ts +74 -0
- package/tests/hooks/use-transaction-control.test.ts +211 -0
- package/tests/integration/db/mongodb-provider.test.ts +698 -0
- package/tests/integration/db/mssql-provider.test.ts +840 -0
- package/tests/integration/db/mysql-provider.test.ts +872 -0
- package/tests/integration/db/oracle-provider.test.ts +843 -0
- package/tests/integration/db/postgres-provider.test.ts +1382 -0
- package/tests/integration/db/redis-provider.test.ts +526 -0
- package/tests/integration/db/sqlite-provider.test.ts +480 -0
- package/tests/integration/seed/seed-pipeline.test.ts +102 -0
- package/tests/isolated/factory-singleton.test.ts +150 -0
- package/tests/isolated/use-storage-sync.test.ts +389 -0
- package/tests/run-components.sh +196 -0
- package/tests/setup-dom.ts +58 -0
- package/tests/setup.ts +40 -0
- package/tests/unit/api-errors.test.ts +210 -0
- package/tests/unit/code-generator-functions.test.ts +271 -0
- package/tests/unit/components/column-list.test.tsx +190 -0
- package/tests/unit/components/data-import-modal.test.tsx +441 -0
- package/tests/unit/components/studio-mobile-header.test.tsx +327 -0
- package/tests/unit/data-charts-functions.test.ts +496 -0
- package/tests/unit/data-import-functions.test.ts +320 -0
- package/tests/unit/data-import-utils.test.ts +125 -0
- package/tests/unit/db/base-provider.test.ts +517 -0
- package/tests/unit/db/errors.test.ts +403 -0
- package/tests/unit/db/factory.test.ts +436 -0
- package/tests/unit/db/pool-manager.test.ts +440 -0
- package/tests/unit/db/query-limiter.test.ts +387 -0
- package/tests/unit/db/sql-base.test.ts +438 -0
- package/tests/unit/lib/api/error-codes.test.ts +39 -0
- package/tests/unit/lib/audit.test.ts +326 -0
- package/tests/unit/lib/auth.test.ts +146 -0
- package/tests/unit/lib/connection-string-parser.test.ts +424 -0
- package/tests/unit/lib/data-masking.test.ts +583 -0
- package/tests/unit/lib/db-icons.test.tsx +41 -0
- package/tests/unit/lib/monitoring-thresholds.test.ts +133 -0
- package/tests/unit/lib/oidc.test.ts +509 -0
- package/tests/unit/lib/query-generators.test.ts +127 -0
- package/tests/unit/lib/storage/factory.test.ts +71 -0
- package/tests/unit/lib/storage/local-storage.test.ts +114 -0
- package/tests/unit/lib/storage/providers/postgres.test.ts +312 -0
- package/tests/unit/lib/storage/providers/sqlite.test.ts +232 -0
- package/tests/unit/lib/storage/storage-facade-extended.test.ts +331 -0
- package/tests/unit/lib/storage/storage-facade.test.ts +184 -0
- package/tests/unit/lib/storage.test.ts +317 -0
- package/tests/unit/lib/time-series-buffer.test.ts +212 -0
- package/tests/unit/lib/utils.test.ts +24 -0
- package/tests/unit/llm/base-provider.test.ts +238 -0
- package/tests/unit/llm/config.test.ts +262 -0
- package/tests/unit/llm/custom-provider.test.ts +281 -0
- package/tests/unit/llm/gemini-provider.test.ts +248 -0
- package/tests/unit/llm/llm-factory.test.ts +155 -0
- package/tests/unit/llm/ollama-provider.test.ts +288 -0
- package/tests/unit/llm/openai-provider.test.ts +324 -0
- package/tests/unit/llm/retry.test.ts +180 -0
- package/tests/unit/llm/streaming.test.ts +355 -0
- package/tests/unit/logger.test.ts +198 -0
- package/tests/unit/mongodb-completions.test.ts +516 -0
- package/tests/unit/pivot-table-functions.test.ts +76 -0
- package/tests/unit/query-cancelled-error.test.ts +81 -0
- package/tests/unit/schema-diff/diff-engine.test.ts +367 -0
- package/tests/unit/schema-diff/migration-generator.test.ts +513 -0
- package/tests/unit/seed/config-loader.test.ts +73 -0
- package/tests/unit/seed/connection-filter.test.ts +91 -0
- package/tests/unit/seed/credential-resolver.test.ts +85 -0
- package/tests/unit/seed/index.test.ts +72 -0
- package/tests/unit/seed/resolve-connection.test.ts +74 -0
- package/tests/unit/seed/types.test.ts +129 -0
- package/tests/unit/sql/alias-extractor.test.ts +444 -0
- package/tests/unit/sql/statement-splitter.test.ts +348 -0
- package/tests/unit/sql-completions.test.ts +463 -0
- package/tests/unit/ssh-tunnel.test.ts +465 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { describe, test, expect, mock, beforeEach } from 'bun:test';
|
|
2
|
+
import { createMockRequest, parseResponseJSON } from '../../helpers/mock-next';
|
|
3
|
+
import { createMockProvider } from '../../helpers/mock-provider';
|
|
4
|
+
import {
|
|
5
|
+
QueryError,
|
|
6
|
+
TimeoutError,
|
|
7
|
+
DatabaseError,
|
|
8
|
+
DatabaseConfigError,
|
|
9
|
+
ConnectionError,
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
PoolExhaustedError,
|
|
12
|
+
QueryCancelledError,
|
|
13
|
+
isDatabaseError,
|
|
14
|
+
isConnectionError,
|
|
15
|
+
isQueryError,
|
|
16
|
+
isTimeoutError,
|
|
17
|
+
isAuthenticationError,
|
|
18
|
+
isRetryableError,
|
|
19
|
+
mapDatabaseError,
|
|
20
|
+
} from '@/lib/db/errors';
|
|
21
|
+
|
|
22
|
+
// ─── Mock provider ──────────────────────────────────────────────────────────
|
|
23
|
+
const mockProvider = createMockProvider();
|
|
24
|
+
const mockGetOrCreateProvider = mock(async () => mockProvider);
|
|
25
|
+
|
|
26
|
+
// ─── Mock auth + seed resolution BEFORE importing the route ─────────────────
|
|
27
|
+
mock.module('@/lib/auth', () => ({
|
|
28
|
+
getSession: mock(async () => ({ role: 'admin', username: 'admin' })),
|
|
29
|
+
signJWT: mock(async () => 'mock-token'),
|
|
30
|
+
verifyJWT: mock(async () => null),
|
|
31
|
+
login: mock(async () => {}),
|
|
32
|
+
logout: mock(async () => {}),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
mock.module('@/lib/seed/resolve-connection', () => {
|
|
36
|
+
class SeedConnectionError extends Error {
|
|
37
|
+
constructor(message: string, public statusCode: number) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'SeedConnectionError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
resolveConnection: mock(async (body: Record<string, unknown>) => {
|
|
44
|
+
if (!body.connection && !body.connectionId) {
|
|
45
|
+
throw new SeedConnectionError('Either connection or connectionId is required', 400);
|
|
46
|
+
}
|
|
47
|
+
return body.connection;
|
|
48
|
+
}),
|
|
49
|
+
SeedConnectionError,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ─── Mock @/lib/db BEFORE importing the route ───────────────────────────────
|
|
54
|
+
mock.module('@/lib/db', () => ({
|
|
55
|
+
getOrCreateProvider: mockGetOrCreateProvider,
|
|
56
|
+
createDatabaseProvider: mock(),
|
|
57
|
+
removeProvider: mock(),
|
|
58
|
+
clearProviderCache: mock(),
|
|
59
|
+
getProviderCacheStats: mock(),
|
|
60
|
+
QueryError,
|
|
61
|
+
TimeoutError,
|
|
62
|
+
DatabaseError,
|
|
63
|
+
DatabaseConfigError,
|
|
64
|
+
ConnectionError,
|
|
65
|
+
AuthenticationError,
|
|
66
|
+
PoolExhaustedError,
|
|
67
|
+
QueryCancelledError,
|
|
68
|
+
isDatabaseError,
|
|
69
|
+
isConnectionError,
|
|
70
|
+
isQueryError,
|
|
71
|
+
isTimeoutError,
|
|
72
|
+
isAuthenticationError,
|
|
73
|
+
isRetryableError,
|
|
74
|
+
mapDatabaseError,
|
|
75
|
+
BaseDatabaseProvider: class {},
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
// ─── Import route handler AFTER mocking ─────────────────────────────────────
|
|
79
|
+
const { POST } = await import('@/app/api/db/query/route');
|
|
80
|
+
|
|
81
|
+
// ─── Fixtures ───────────────────────────────────────────────────────────────
|
|
82
|
+
const validConnection = {
|
|
83
|
+
id: 'test-1',
|
|
84
|
+
name: 'Test DB',
|
|
85
|
+
type: 'postgres',
|
|
86
|
+
host: 'localhost',
|
|
87
|
+
port: 5432,
|
|
88
|
+
database: 'testdb',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// ─── Tests ──────────────────────────────────────────────────────────────────
|
|
92
|
+
describe('POST /api/db/query', () => {
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
mockGetOrCreateProvider.mockClear();
|
|
95
|
+
(mockProvider.query as ReturnType<typeof mock>).mockClear();
|
|
96
|
+
(mockProvider.prepareQuery as ReturnType<typeof mock>).mockClear();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('returns 200 with rows and pagination for valid query', async () => {
|
|
100
|
+
const req = createMockRequest('/api/db/query', {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
body: { connection: validConnection, sql: 'SELECT * FROM users' },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const res = await POST(req as never);
|
|
106
|
+
const data = await parseResponseJSON<{
|
|
107
|
+
rows: unknown[];
|
|
108
|
+
fields: string[];
|
|
109
|
+
pagination: { limit: number; offset: number; hasMore: boolean; totalReturned: number; wasLimited: boolean };
|
|
110
|
+
}>(res);
|
|
111
|
+
|
|
112
|
+
expect(res.status).toBe(200);
|
|
113
|
+
expect(data.rows).toBeDefined();
|
|
114
|
+
expect(data.fields).toBeDefined();
|
|
115
|
+
expect(data.pagination).toBeDefined();
|
|
116
|
+
expect(data.pagination.limit).toBe(50);
|
|
117
|
+
expect(data.pagination.offset).toBe(0);
|
|
118
|
+
expect(data.pagination.wasLimited).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('returns 400 when connection is missing', async () => {
|
|
122
|
+
const req = createMockRequest('/api/db/query', {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
body: { sql: 'SELECT 1' },
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const res = await POST(req as never);
|
|
128
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
129
|
+
|
|
130
|
+
expect(res.status).toBe(400);
|
|
131
|
+
expect(data.error).toContain('required');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('returns 400 when sql is missing', async () => {
|
|
135
|
+
const req = createMockRequest('/api/db/query', {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
body: { connection: validConnection },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const res = await POST(req as never);
|
|
141
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
142
|
+
|
|
143
|
+
expect(res.status).toBe(400);
|
|
144
|
+
expect(data.error).toContain('required');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('returns 400 for QueryError', async () => {
|
|
148
|
+
(mockProvider.query as ReturnType<typeof mock>).mockRejectedValueOnce(
|
|
149
|
+
new QueryError('syntax error at or near "FORM"')
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const req = createMockRequest('/api/db/query', {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
body: { connection: validConnection, sql: 'SELECT * FORM users' },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const res = await POST(req as never);
|
|
158
|
+
const data = await parseResponseJSON<{ error: string; code: string }>(res);
|
|
159
|
+
|
|
160
|
+
expect(res.status).toBe(400);
|
|
161
|
+
expect(data.error).toContain('syntax error');
|
|
162
|
+
expect(data.code).toBe('QUERY_ERROR');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('returns 408 for TimeoutError', async () => {
|
|
166
|
+
(mockProvider.query as ReturnType<typeof mock>).mockRejectedValueOnce(
|
|
167
|
+
new TimeoutError('Query timed out after 30000ms')
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const req = createMockRequest('/api/db/query', {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
body: { connection: validConnection, sql: 'SELECT pg_sleep(60)' },
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const res = await POST(req as never);
|
|
176
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
177
|
+
|
|
178
|
+
expect(res.status).toBe(408);
|
|
179
|
+
expect(data.error).toContain('timed out');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('returns 500 for DatabaseError', async () => {
|
|
183
|
+
const dbError = new DatabaseError('Internal database failure', 'postgres', 'INTERNAL_ERROR');
|
|
184
|
+
(mockProvider.query as ReturnType<typeof mock>).mockRejectedValueOnce(dbError);
|
|
185
|
+
|
|
186
|
+
const req = createMockRequest('/api/db/query', {
|
|
187
|
+
method: 'POST',
|
|
188
|
+
body: { connection: validConnection, sql: 'SELECT 1' },
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const res = await POST(req as never);
|
|
192
|
+
const data = await parseResponseJSON<{ error: string; code: string }>(res);
|
|
193
|
+
|
|
194
|
+
expect(res.status).toBe(500);
|
|
195
|
+
expect(data.error).toBe('Internal database failure');
|
|
196
|
+
expect(data.code).toBe('INTERNAL_ERROR');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('returns 499 for cancelled query', async () => {
|
|
200
|
+
(mockProvider.query as ReturnType<typeof mock>).mockRejectedValueOnce(
|
|
201
|
+
new QueryCancelledError('Query was cancelled')
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const req = createMockRequest('/api/db/query', {
|
|
205
|
+
method: 'POST',
|
|
206
|
+
body: { connection: validConnection, sql: 'SELECT * FROM large_table' },
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const res = await POST(req as never);
|
|
210
|
+
const data = await parseResponseJSON<{ error: string; code: string }>(res);
|
|
211
|
+
|
|
212
|
+
expect(res.status).toBe(499);
|
|
213
|
+
expect(data.code).toBe('QUERY_CANCELLED');
|
|
214
|
+
expect(data.error).toContain('cancelled');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('returns 500 for generic error', async () => {
|
|
218
|
+
(mockProvider.query as ReturnType<typeof mock>).mockRejectedValueOnce(
|
|
219
|
+
new Error('Something unexpected happened')
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const req = createMockRequest('/api/db/query', {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
body: { connection: validConnection, sql: 'SELECT 1' },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const res = await POST(req as never);
|
|
228
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
229
|
+
|
|
230
|
+
expect(res.status).toBe(500);
|
|
231
|
+
expect(data.error).toBe('Something unexpected happened');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('calls prepareQuery with sql and options', async () => {
|
|
235
|
+
const req = createMockRequest('/api/db/query', {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
body: {
|
|
238
|
+
connection: validConnection,
|
|
239
|
+
sql: 'SELECT * FROM users',
|
|
240
|
+
options: { limit: 100 },
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await POST(req as never);
|
|
245
|
+
|
|
246
|
+
expect(mockProvider.prepareQuery).toHaveBeenCalledTimes(1);
|
|
247
|
+
expect(mockProvider.prepareQuery).toHaveBeenCalledWith('SELECT * FROM users', { limit: 100 });
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('pagination hasMore is true when rows.length equals limit', async () => {
|
|
251
|
+
const fiftyRows = Array.from({ length: 50 }, (_, i) => ({ id: i + 1 }));
|
|
252
|
+
(mockProvider.query as ReturnType<typeof mock>).mockResolvedValueOnce({
|
|
253
|
+
rows: fiftyRows,
|
|
254
|
+
fields: ['id'],
|
|
255
|
+
rowCount: 50,
|
|
256
|
+
executionTime: 10,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const req = createMockRequest('/api/db/query', {
|
|
260
|
+
method: 'POST',
|
|
261
|
+
body: { connection: validConnection, sql: 'SELECT * FROM users' },
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const res = await POST(req as never);
|
|
265
|
+
const data = await parseResponseJSON<{
|
|
266
|
+
pagination: { hasMore: boolean; totalReturned: number };
|
|
267
|
+
}>(res);
|
|
268
|
+
|
|
269
|
+
expect(res.status).toBe(200);
|
|
270
|
+
expect(data.pagination.hasMore).toBe(true);
|
|
271
|
+
expect(data.pagination.totalReturned).toBe(50);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('pagination hasMore is false when rows.length less than limit', async () => {
|
|
275
|
+
const threeRows = [{ id: 1 }, { id: 2 }, { id: 3 }];
|
|
276
|
+
(mockProvider.query as ReturnType<typeof mock>).mockResolvedValueOnce({
|
|
277
|
+
rows: threeRows,
|
|
278
|
+
fields: ['id'],
|
|
279
|
+
rowCount: 3,
|
|
280
|
+
executionTime: 5,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const req = createMockRequest('/api/db/query', {
|
|
284
|
+
method: 'POST',
|
|
285
|
+
body: { connection: validConnection, sql: 'SELECT * FROM users' },
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const res = await POST(req as never);
|
|
289
|
+
const data = await parseResponseJSON<{
|
|
290
|
+
pagination: { hasMore: boolean; totalReturned: number };
|
|
291
|
+
}>(res);
|
|
292
|
+
|
|
293
|
+
expect(res.status).toBe(200);
|
|
294
|
+
expect(data.pagination.hasMore).toBe(false);
|
|
295
|
+
expect(data.pagination.totalReturned).toBe(3);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('returns 499 for interrupted query execution', async () => {
|
|
299
|
+
(mockProvider.query as ReturnType<typeof mock>).mockRejectedValueOnce(
|
|
300
|
+
new QueryCancelledError('Query execution was interrupted')
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const req = createMockRequest('/api/db/query', {
|
|
304
|
+
method: 'POST',
|
|
305
|
+
body: { connection: validConnection, sql: 'SELECT * FROM users' },
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const res = await POST(req as never);
|
|
309
|
+
const data = await parseResponseJSON<{ error: string; code: string }>(res);
|
|
310
|
+
|
|
311
|
+
expect(res.status).toBe(499);
|
|
312
|
+
expect(data.code).toBe('QUERY_CANCELLED');
|
|
313
|
+
});
|
|
314
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, test, expect, mock, beforeEach } from 'bun:test';
|
|
2
|
+
import { createMockRequest, parseResponseJSON } from '../../helpers/mock-next';
|
|
3
|
+
import { createMockProvider } from '../../helpers/mock-provider';
|
|
4
|
+
|
|
5
|
+
// ─── Mock provider ──────────────────────────────────────────────────────────
|
|
6
|
+
const mockProvider = createMockProvider();
|
|
7
|
+
const mockCreateDatabaseProvider = mock(async () => mockProvider);
|
|
8
|
+
|
|
9
|
+
// ─── Mock auth + seed resolution BEFORE importing the route ─────────────────
|
|
10
|
+
mock.module('@/lib/auth', () => ({
|
|
11
|
+
getSession: mock(async () => ({ role: 'admin', username: 'admin' })),
|
|
12
|
+
signJWT: mock(async () => 'mock-token'),
|
|
13
|
+
verifyJWT: mock(async () => null),
|
|
14
|
+
login: mock(async () => {}),
|
|
15
|
+
logout: mock(async () => {}),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
mock.module('@/lib/seed/resolve-connection', () => {
|
|
19
|
+
class SeedConnectionError extends Error {
|
|
20
|
+
constructor(message: string, public statusCode: number) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'SeedConnectionError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
resolveConnection: mock(async (body: Record<string, unknown>) => {
|
|
27
|
+
if (!body.connection && !body.connectionId) {
|
|
28
|
+
throw new SeedConnectionError('Either connection or connectionId is required', 400);
|
|
29
|
+
}
|
|
30
|
+
return body.connection;
|
|
31
|
+
}),
|
|
32
|
+
SeedConnectionError,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ─── Mock @/lib/db/factory BEFORE importing the route ───────────────────────
|
|
37
|
+
mock.module('@/lib/db/factory', () => ({
|
|
38
|
+
getOrCreateProvider: mock(async () => mockProvider),
|
|
39
|
+
createDatabaseProvider: mockCreateDatabaseProvider,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
// ─── Import route handler AFTER mocking ─────────────────────────────────────
|
|
43
|
+
const { POST } = await import('@/app/api/db/schema-snapshot/route');
|
|
44
|
+
|
|
45
|
+
// ─── Fixtures ───────────────────────────────────────────────────────────────
|
|
46
|
+
const validConnection = {
|
|
47
|
+
id: 'test-1',
|
|
48
|
+
name: 'Test DB',
|
|
49
|
+
type: 'postgres',
|
|
50
|
+
host: 'localhost',
|
|
51
|
+
port: 5432,
|
|
52
|
+
database: 'testdb',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// ─── Tests ──────────────────────────────────────────────────────────────────
|
|
56
|
+
describe('POST /api/db/schema-snapshot', () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
mockCreateDatabaseProvider.mockClear();
|
|
59
|
+
mockCreateDatabaseProvider.mockImplementation(async () => mockProvider);
|
|
60
|
+
(mockProvider.connect as ReturnType<typeof mock>).mockClear();
|
|
61
|
+
(mockProvider.disconnect as ReturnType<typeof mock>).mockClear();
|
|
62
|
+
(mockProvider.getSchema as ReturnType<typeof mock>).mockClear();
|
|
63
|
+
// Reset implementations
|
|
64
|
+
(mockProvider.connect as ReturnType<typeof mock>).mockImplementation(async () => {});
|
|
65
|
+
(mockProvider.disconnect as ReturnType<typeof mock>).mockImplementation(async () => {});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('returns schema with metadata for valid connection', async () => {
|
|
69
|
+
const req = createMockRequest('/api/db/schema-snapshot', {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
body: { connection: validConnection },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const res = await POST(req as never);
|
|
75
|
+
const data = await parseResponseJSON<{
|
|
76
|
+
schema: unknown[];
|
|
77
|
+
connectionId: string;
|
|
78
|
+
connectionName: string;
|
|
79
|
+
databaseType: string;
|
|
80
|
+
timestamp: string;
|
|
81
|
+
}>(res);
|
|
82
|
+
|
|
83
|
+
expect(res.status).toBe(200);
|
|
84
|
+
expect(data.schema).toBeArray();
|
|
85
|
+
expect(data.connectionId).toBe('test-1');
|
|
86
|
+
expect(data.connectionName).toBe('Test DB');
|
|
87
|
+
expect(data.databaseType).toBe('postgres');
|
|
88
|
+
expect(data.timestamp).toBeDefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('returns 400 when connection is missing', async () => {
|
|
92
|
+
const req = createMockRequest('/api/db/schema-snapshot', {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
body: {},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const res = await POST(req as never);
|
|
98
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
99
|
+
|
|
100
|
+
expect(res.status).toBe(400);
|
|
101
|
+
expect(data.error).toContain('required');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('returns 500 when connect() fails', async () => {
|
|
105
|
+
(mockProvider.connect as ReturnType<typeof mock>).mockRejectedValueOnce(
|
|
106
|
+
new Error('Connection refused')
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const req = createMockRequest('/api/db/schema-snapshot', {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
body: { connection: validConnection },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const res = await POST(req as never);
|
|
115
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
116
|
+
|
|
117
|
+
expect(res.status).toBe(500);
|
|
118
|
+
expect(data.error).toBe('Connection refused');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('returns 500 when getSchema() fails and disconnect is still called', async () => {
|
|
122
|
+
(mockProvider.getSchema as ReturnType<typeof mock>).mockRejectedValueOnce(
|
|
123
|
+
new Error('Schema fetch failed')
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const req = createMockRequest('/api/db/schema-snapshot', {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
body: { connection: validConnection },
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const res = await POST(req as never);
|
|
132
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
133
|
+
|
|
134
|
+
expect(res.status).toBe(500);
|
|
135
|
+
expect(data.error).toBe('Schema fetch failed');
|
|
136
|
+
// disconnect should be called in the error handler
|
|
137
|
+
expect(mockProvider.disconnect).toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('calls connect() and disconnect() on success', async () => {
|
|
141
|
+
const req = createMockRequest('/api/db/schema-snapshot', {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
body: { connection: validConnection },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await POST(req as never);
|
|
147
|
+
|
|
148
|
+
expect(mockProvider.connect).toHaveBeenCalledTimes(1);
|
|
149
|
+
expect(mockProvider.disconnect).toHaveBeenCalledTimes(1);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('calls disconnect() in error handler when connect succeeds but getSchema fails', async () => {
|
|
153
|
+
(mockProvider.connect as ReturnType<typeof mock>).mockImplementation(async () => {});
|
|
154
|
+
(mockProvider.getSchema as ReturnType<typeof mock>).mockRejectedValueOnce(
|
|
155
|
+
new Error('Schema error')
|
|
156
|
+
);
|
|
157
|
+
(mockProvider.disconnect as ReturnType<typeof mock>).mockImplementation(async () => {});
|
|
158
|
+
|
|
159
|
+
const req = createMockRequest('/api/db/schema-snapshot', {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
body: { connection: validConnection },
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const res = await POST(req as never);
|
|
165
|
+
|
|
166
|
+
expect(res.status).toBe(500);
|
|
167
|
+
// disconnect should have been called in the catch block
|
|
168
|
+
expect(mockProvider.disconnect).toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, test, expect, mock, beforeEach } from 'bun:test';
|
|
2
|
+
import { createMockRequest, parseResponseJSON } from '../../helpers/mock-next';
|
|
3
|
+
import { createMockProvider } from '../../helpers/mock-provider';
|
|
4
|
+
import { mockSchema } from '../../fixtures/schemas';
|
|
5
|
+
import {
|
|
6
|
+
QueryError,
|
|
7
|
+
TimeoutError,
|
|
8
|
+
DatabaseError,
|
|
9
|
+
DatabaseConfigError,
|
|
10
|
+
ConnectionError,
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
PoolExhaustedError,
|
|
13
|
+
isDatabaseError,
|
|
14
|
+
isConnectionError,
|
|
15
|
+
isQueryError,
|
|
16
|
+
isTimeoutError,
|
|
17
|
+
isAuthenticationError,
|
|
18
|
+
isRetryableError,
|
|
19
|
+
mapDatabaseError,
|
|
20
|
+
} from '@/lib/db/errors';
|
|
21
|
+
|
|
22
|
+
// ─── Mock provider ──────────────────────────────────────────────────────────
|
|
23
|
+
const mockProvider = createMockProvider();
|
|
24
|
+
const mockGetOrCreateProvider = mock(async () => mockProvider);
|
|
25
|
+
|
|
26
|
+
// ─── Mock auth + seed resolution BEFORE importing the route ─────────────────
|
|
27
|
+
mock.module('@/lib/auth', () => ({
|
|
28
|
+
getSession: mock(async () => ({ role: 'admin', username: 'admin' })),
|
|
29
|
+
signJWT: mock(async () => 'mock-token'),
|
|
30
|
+
verifyJWT: mock(async () => null),
|
|
31
|
+
login: mock(async () => {}),
|
|
32
|
+
logout: mock(async () => {}),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
mock.module('@/lib/seed/resolve-connection', () => {
|
|
36
|
+
class SeedConnectionError extends Error {
|
|
37
|
+
constructor(message: string, public statusCode: number) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'SeedConnectionError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
resolveConnection: mock(async (body: Record<string, unknown>) => {
|
|
44
|
+
if (!body.connection && !body.connectionId) {
|
|
45
|
+
throw new SeedConnectionError('Either connection or connectionId is required', 400);
|
|
46
|
+
}
|
|
47
|
+
return body.connection;
|
|
48
|
+
}),
|
|
49
|
+
SeedConnectionError,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ─── Mock @/lib/db BEFORE importing the route ───────────────────────────────
|
|
54
|
+
mock.module('@/lib/db', () => ({
|
|
55
|
+
getOrCreateProvider: mockGetOrCreateProvider,
|
|
56
|
+
createDatabaseProvider: mock(),
|
|
57
|
+
removeProvider: mock(),
|
|
58
|
+
clearProviderCache: mock(),
|
|
59
|
+
getProviderCacheStats: mock(),
|
|
60
|
+
QueryError,
|
|
61
|
+
TimeoutError,
|
|
62
|
+
DatabaseError,
|
|
63
|
+
DatabaseConfigError,
|
|
64
|
+
ConnectionError,
|
|
65
|
+
AuthenticationError,
|
|
66
|
+
PoolExhaustedError,
|
|
67
|
+
isDatabaseError,
|
|
68
|
+
isConnectionError,
|
|
69
|
+
isQueryError,
|
|
70
|
+
isTimeoutError,
|
|
71
|
+
isAuthenticationError,
|
|
72
|
+
isRetryableError,
|
|
73
|
+
mapDatabaseError,
|
|
74
|
+
BaseDatabaseProvider: class {},
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
// ─── Import route handler AFTER mocking ─────────────────────────────────────
|
|
78
|
+
const { POST } = await import('@/app/api/db/schema/route');
|
|
79
|
+
|
|
80
|
+
// ─── Fixtures ───────────────────────────────────────────────────────────────
|
|
81
|
+
const validConnection = {
|
|
82
|
+
id: 'test-1',
|
|
83
|
+
name: 'Test DB',
|
|
84
|
+
type: 'postgres',
|
|
85
|
+
host: 'localhost',
|
|
86
|
+
port: 5432,
|
|
87
|
+
database: 'testdb',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ─── Tests ──────────────────────────────────────────────────────────────────
|
|
91
|
+
describe('POST /api/db/schema', () => {
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
mockGetOrCreateProvider.mockClear();
|
|
94
|
+
(mockProvider.getSchema as ReturnType<typeof mock>).mockClear();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('returns 200 with schema array for valid connection', async () => {
|
|
98
|
+
const req = createMockRequest('/api/db/schema', {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
body: validConnection,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const res = await POST(req as never);
|
|
104
|
+
const data = await parseResponseJSON<typeof mockSchema>(res);
|
|
105
|
+
|
|
106
|
+
expect(res.status).toBe(200);
|
|
107
|
+
expect(Array.isArray(data)).toBe(true);
|
|
108
|
+
expect(data.length).toBe(mockSchema.length);
|
|
109
|
+
expect(data[0].name).toBe('users');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('returns 400 for empty request body', async () => {
|
|
113
|
+
const req = new Request('http://localhost:3000/api/db/schema', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'content-type': 'application/json' },
|
|
116
|
+
body: '',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const res = await POST(req as never);
|
|
120
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
121
|
+
|
|
122
|
+
expect(res.status).toBe(400);
|
|
123
|
+
expect(data.error).toContain('Empty request body');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('returns 400 when connection has no type field', async () => {
|
|
127
|
+
const req = createMockRequest('/api/db/schema', {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
body: { host: 'localhost', database: 'testdb' },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const res = await POST(req as never);
|
|
133
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
134
|
+
|
|
135
|
+
expect(res.status).toBe(400);
|
|
136
|
+
expect(data.error).toContain('required');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('returns 503 for ConnectionError', async () => {
|
|
140
|
+
mockGetOrCreateProvider.mockRejectedValueOnce(
|
|
141
|
+
new ConnectionError('Connection refused')
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const req = createMockRequest('/api/db/schema', {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
body: validConnection,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const res = await POST(req as never);
|
|
150
|
+
const data = await parseResponseJSON<{ error: string; code: string }>(res);
|
|
151
|
+
|
|
152
|
+
expect(res.status).toBe(503);
|
|
153
|
+
expect(data.error).toContain('Connection refused');
|
|
154
|
+
expect(data.code).toBe('CONNECTION_ERROR');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('returns 500 for DatabaseError', async () => {
|
|
158
|
+
const dbError = new DatabaseError('Database internal error', 'postgres', 'INTERNAL_ERROR');
|
|
159
|
+
mockGetOrCreateProvider.mockResolvedValueOnce(mockProvider);
|
|
160
|
+
(mockProvider.getSchema as ReturnType<typeof mock>).mockRejectedValueOnce(dbError);
|
|
161
|
+
|
|
162
|
+
const req = createMockRequest('/api/db/schema', {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
body: validConnection,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const res = await POST(req as never);
|
|
168
|
+
const data = await parseResponseJSON<{ error: string; code: string }>(res);
|
|
169
|
+
|
|
170
|
+
expect(res.status).toBe(500);
|
|
171
|
+
expect(data.error).toBe('Database internal error');
|
|
172
|
+
expect(data.code).toBe('INTERNAL_ERROR');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('returns 500 for generic error', async () => {
|
|
176
|
+
(mockProvider.getSchema as ReturnType<typeof mock>).mockRejectedValueOnce(
|
|
177
|
+
new Error('Unexpected failure')
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const req = createMockRequest('/api/db/schema', {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
body: validConnection,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const res = await POST(req as never);
|
|
186
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
187
|
+
|
|
188
|
+
expect(res.status).toBe(500);
|
|
189
|
+
expect(data.error).toBe('Unexpected failure');
|
|
190
|
+
});
|
|
191
|
+
});
|