@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,185 @@
|
|
|
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 { DatabaseConfigError } from '@/lib/db/errors';
|
|
5
|
+
|
|
6
|
+
// ─── Create mock objects ────────────────────────────────────────────────────
|
|
7
|
+
const mockProvider = createMockProvider();
|
|
8
|
+
const mockCreateDatabaseProvider = mock(async () => mockProvider);
|
|
9
|
+
|
|
10
|
+
// ─── Mock auth + seed resolution BEFORE importing route ─────────────────────
|
|
11
|
+
mock.module('@/lib/auth', () => ({
|
|
12
|
+
getSession: mock(async () => ({ role: 'admin', username: 'admin' })),
|
|
13
|
+
signJWT: mock(async () => 'mock-token'),
|
|
14
|
+
verifyJWT: mock(async () => null),
|
|
15
|
+
login: mock(async () => {}),
|
|
16
|
+
logout: mock(async () => {}),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
mock.module('@/lib/seed/resolve-connection', () => {
|
|
20
|
+
class SeedConnectionError extends Error {
|
|
21
|
+
constructor(message: string, public statusCode: number) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'SeedConnectionError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
resolveConnection: mock(async (body: Record<string, unknown>) => {
|
|
28
|
+
if (!body.connection && !body.connectionId) {
|
|
29
|
+
throw new SeedConnectionError('Either connection or connectionId is required', 400);
|
|
30
|
+
}
|
|
31
|
+
return body.connection;
|
|
32
|
+
}),
|
|
33
|
+
SeedConnectionError,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ─── Mock dependencies BEFORE importing route ───────────────────────────────
|
|
38
|
+
mock.module('@/lib/db/factory', () => ({
|
|
39
|
+
createDatabaseProvider: mockCreateDatabaseProvider,
|
|
40
|
+
getOrCreateProvider: mock(async () => mockProvider),
|
|
41
|
+
removeProvider: mock(async () => {}),
|
|
42
|
+
clearProviderCache: mock(async () => {}),
|
|
43
|
+
getProviderCacheStats: mock(() => ({ size: 0, connections: [] })),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// ─── Import route handler AFTER mocking ─────────────────────────────────────
|
|
47
|
+
const { POST } = await import('@/app/api/db/test-connection/route');
|
|
48
|
+
|
|
49
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
50
|
+
const validConnection = {
|
|
51
|
+
id: 'test-1',
|
|
52
|
+
name: 'Test DB',
|
|
53
|
+
type: 'postgres',
|
|
54
|
+
host: 'localhost',
|
|
55
|
+
port: 5432,
|
|
56
|
+
database: 'testdb',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ─── Tests ──────────────────────────────────────────────────────────────────
|
|
60
|
+
describe('POST /api/db/test-connection', () => {
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
mockCreateDatabaseProvider.mockClear();
|
|
63
|
+
(mockProvider.connect as ReturnType<typeof mock>).mockClear();
|
|
64
|
+
(mockProvider.disconnect as ReturnType<typeof mock>).mockClear();
|
|
65
|
+
(mockProvider.getHealth as ReturnType<typeof mock>).mockClear();
|
|
66
|
+
|
|
67
|
+
// Reset implementations to defaults
|
|
68
|
+
mockCreateDatabaseProvider.mockImplementation(async () => mockProvider);
|
|
69
|
+
(mockProvider.connect as ReturnType<typeof mock>).mockImplementation(async () => {});
|
|
70
|
+
(mockProvider.disconnect as ReturnType<typeof mock>).mockImplementation(async () => {});
|
|
71
|
+
(mockProvider.getHealth as ReturnType<typeof mock>).mockImplementation(async () => ({
|
|
72
|
+
activeConnections: 5,
|
|
73
|
+
databaseSize: '256 MB',
|
|
74
|
+
cacheHitRatio: '99.2%',
|
|
75
|
+
slowQueries: [],
|
|
76
|
+
activeSessions: [],
|
|
77
|
+
}));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('returns success with latency for valid connection', async () => {
|
|
81
|
+
const req = createMockRequest('/api/db/test-connection', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
body: validConnection,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const res = await POST(req as never);
|
|
87
|
+
const data = await parseResponseJSON<{ success: boolean; message: string; latency: number }>(res);
|
|
88
|
+
|
|
89
|
+
expect(res.status).toBe(200);
|
|
90
|
+
expect(data.success).toBe(true);
|
|
91
|
+
expect(data.message).toBe('Connection successful');
|
|
92
|
+
expect(typeof data.latency).toBe('number');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('returns 400 when connection type is missing', async () => {
|
|
96
|
+
const req = createMockRequest('/api/db/test-connection', {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
body: { host: 'localhost' },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const res = await POST(req as never);
|
|
102
|
+
const data = await parseResponseJSON<{ success: boolean; error: string }>(res);
|
|
103
|
+
|
|
104
|
+
expect(res.status).toBe(400);
|
|
105
|
+
expect(data.success).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('returns 400 when body is empty object', async () => {
|
|
109
|
+
const req = createMockRequest('/api/db/test-connection', {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
body: {},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const res = await POST(req as never);
|
|
115
|
+
const data = await parseResponseJSON<{ success: boolean; error: string }>(res);
|
|
116
|
+
|
|
117
|
+
expect(res.status).toBe(400);
|
|
118
|
+
expect(data.success).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('returns 400 when DatabaseConfigError is thrown', async () => {
|
|
122
|
+
mockCreateDatabaseProvider.mockImplementation(async () => {
|
|
123
|
+
throw new DatabaseConfigError('Invalid database configuration');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const req = createMockRequest('/api/db/test-connection', {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
body: validConnection,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const res = await POST(req as never);
|
|
132
|
+
const data = await parseResponseJSON<{ error: string; code: string }>(res);
|
|
133
|
+
|
|
134
|
+
expect(res.status).toBe(400);
|
|
135
|
+
expect(data.error).toBe('Invalid database configuration');
|
|
136
|
+
expect(data.code).toBe('CONFIG_ERROR');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('returns 500 when connection error occurs', async () => {
|
|
140
|
+
(mockProvider.connect as ReturnType<typeof mock>).mockImplementation(async () => {
|
|
141
|
+
throw new Error('ECONNREFUSED');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const req = createMockRequest('/api/db/test-connection', {
|
|
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(500);
|
|
153
|
+
expect(data.error).toBe('ECONNREFUSED');
|
|
154
|
+
expect(data.code).toBe('INTERNAL_ERROR');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('calls connect and disconnect on successful test', async () => {
|
|
158
|
+
const req = createMockRequest('/api/db/test-connection', {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
body: validConnection,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await POST(req as never);
|
|
164
|
+
|
|
165
|
+
expect(mockProvider.connect).toHaveBeenCalledTimes(1);
|
|
166
|
+
expect(mockProvider.getHealth).toHaveBeenCalledTimes(1);
|
|
167
|
+
expect(mockProvider.disconnect).toHaveBeenCalledTimes(1);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('calls disconnect on error', async () => {
|
|
171
|
+
(mockProvider.getHealth as ReturnType<typeof mock>).mockImplementation(async () => {
|
|
172
|
+
throw new Error('Health check failed');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const req = createMockRequest('/api/db/test-connection', {
|
|
176
|
+
method: 'POST',
|
|
177
|
+
body: validConnection,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await POST(req as never);
|
|
181
|
+
|
|
182
|
+
expect(mockProvider.connect).toHaveBeenCalledTimes(1);
|
|
183
|
+
expect(mockProvider.disconnect).toHaveBeenCalledTimes(1);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -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
|
+
DatabaseError,
|
|
7
|
+
DatabaseConfigError,
|
|
8
|
+
ConnectionError,
|
|
9
|
+
TimeoutError,
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
PoolExhaustedError,
|
|
12
|
+
isDatabaseError,
|
|
13
|
+
isConnectionError,
|
|
14
|
+
isQueryError,
|
|
15
|
+
isTimeoutError,
|
|
16
|
+
isAuthenticationError,
|
|
17
|
+
isRetryableError,
|
|
18
|
+
mapDatabaseError,
|
|
19
|
+
} from '@/lib/db/errors';
|
|
20
|
+
|
|
21
|
+
// ─── Create mock provider with transaction methods ──────────────────────────
|
|
22
|
+
const baseMockProvider = createMockProvider();
|
|
23
|
+
|
|
24
|
+
const mockTxProvider = {
|
|
25
|
+
...baseMockProvider,
|
|
26
|
+
beginTransaction: mock(async () => {}),
|
|
27
|
+
commitTransaction: mock(async () => {}),
|
|
28
|
+
rollbackTransaction: mock(async () => {}),
|
|
29
|
+
isInTransaction: mock(() => true),
|
|
30
|
+
queryInTransaction: mock(async () => ({
|
|
31
|
+
rows: [{ id: 1, name: 'Alice' }],
|
|
32
|
+
fields: ['id', 'name'],
|
|
33
|
+
rowCount: 1,
|
|
34
|
+
executionTime: 10,
|
|
35
|
+
})),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Non-transaction provider (no transaction methods)
|
|
39
|
+
const mockNonTxProvider = createMockProvider();
|
|
40
|
+
|
|
41
|
+
const mockGetOrCreateProvider = mock(async () => mockTxProvider as never);
|
|
42
|
+
|
|
43
|
+
// ─── Mock auth + seed resolution BEFORE importing route ─────────────────────
|
|
44
|
+
mock.module('@/lib/auth', () => ({
|
|
45
|
+
getSession: mock(async () => ({ role: 'admin', username: 'admin' })),
|
|
46
|
+
signJWT: mock(async () => 'mock-token'),
|
|
47
|
+
verifyJWT: mock(async () => null),
|
|
48
|
+
login: mock(async () => {}),
|
|
49
|
+
logout: mock(async () => {}),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
mock.module('@/lib/seed/resolve-connection', () => {
|
|
53
|
+
class SeedConnectionError extends Error {
|
|
54
|
+
constructor(message: string, public statusCode: number) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = 'SeedConnectionError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
resolveConnection: mock(async (body: Record<string, unknown>) => {
|
|
61
|
+
if (!body.connection && !body.connectionId) {
|
|
62
|
+
throw new SeedConnectionError('Either connection or connectionId is required', 400);
|
|
63
|
+
}
|
|
64
|
+
return body.connection;
|
|
65
|
+
}),
|
|
66
|
+
SeedConnectionError,
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ─── Mock dependencies BEFORE importing route ───────────────────────────────
|
|
71
|
+
mock.module('@/lib/db', () => ({
|
|
72
|
+
getOrCreateProvider: mockGetOrCreateProvider,
|
|
73
|
+
createDatabaseProvider: mock(async () => mockTxProvider),
|
|
74
|
+
removeProvider: mock(async () => {}),
|
|
75
|
+
clearProviderCache: mock(async () => {}),
|
|
76
|
+
getProviderCacheStats: mock(() => ({ size: 0, connections: [] })),
|
|
77
|
+
QueryError,
|
|
78
|
+
DatabaseError,
|
|
79
|
+
DatabaseConfigError,
|
|
80
|
+
ConnectionError,
|
|
81
|
+
TimeoutError,
|
|
82
|
+
AuthenticationError,
|
|
83
|
+
PoolExhaustedError,
|
|
84
|
+
isDatabaseError,
|
|
85
|
+
isConnectionError,
|
|
86
|
+
isQueryError,
|
|
87
|
+
isTimeoutError,
|
|
88
|
+
isAuthenticationError,
|
|
89
|
+
isRetryableError,
|
|
90
|
+
mapDatabaseError,
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
// ─── Import route handler AFTER mocking ─────────────────────────────────────
|
|
94
|
+
const { POST } = await import('@/app/api/db/transaction/route');
|
|
95
|
+
|
|
96
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
97
|
+
const validConnection = {
|
|
98
|
+
id: 'test-1',
|
|
99
|
+
name: 'Test DB',
|
|
100
|
+
type: 'postgres',
|
|
101
|
+
host: 'localhost',
|
|
102
|
+
port: 5432,
|
|
103
|
+
database: 'testdb',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// ─── Tests ──────────────────────────────────────────────────────────────────
|
|
107
|
+
describe('POST /api/db/transaction', () => {
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
mockGetOrCreateProvider.mockClear();
|
|
110
|
+
mockTxProvider.beginTransaction.mockClear();
|
|
111
|
+
mockTxProvider.commitTransaction.mockClear();
|
|
112
|
+
mockTxProvider.rollbackTransaction.mockClear();
|
|
113
|
+
mockTxProvider.isInTransaction.mockClear();
|
|
114
|
+
mockTxProvider.queryInTransaction.mockClear();
|
|
115
|
+
(mockTxProvider.prepareQuery as ReturnType<typeof mock>).mockClear();
|
|
116
|
+
|
|
117
|
+
// Reset to default implementations
|
|
118
|
+
mockGetOrCreateProvider.mockImplementation(async () => mockTxProvider as never);
|
|
119
|
+
mockTxProvider.beginTransaction.mockImplementation(async () => {});
|
|
120
|
+
mockTxProvider.commitTransaction.mockImplementation(async () => {});
|
|
121
|
+
mockTxProvider.rollbackTransaction.mockImplementation(async () => {});
|
|
122
|
+
mockTxProvider.isInTransaction.mockImplementation(() => true);
|
|
123
|
+
mockTxProvider.queryInTransaction.mockImplementation(async () => ({
|
|
124
|
+
rows: [{ id: 1, name: 'Alice' }],
|
|
125
|
+
fields: ['id', 'name'],
|
|
126
|
+
rowCount: 1,
|
|
127
|
+
executionTime: 10,
|
|
128
|
+
}));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('begin action returns status active', async () => {
|
|
132
|
+
const req = createMockRequest('/api/db/transaction', {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
body: { connection: validConnection, action: 'begin' },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const res = await POST(req as never);
|
|
138
|
+
const data = await parseResponseJSON<{ status: string; message: string }>(res);
|
|
139
|
+
|
|
140
|
+
expect(res.status).toBe(200);
|
|
141
|
+
expect(data.status).toBe('active');
|
|
142
|
+
expect(data.message).toBe('Transaction started');
|
|
143
|
+
expect(mockTxProvider.beginTransaction).toHaveBeenCalledTimes(1);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('commit action returns status committed', async () => {
|
|
147
|
+
const req = createMockRequest('/api/db/transaction', {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
body: { connection: validConnection, action: 'commit' },
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const res = await POST(req as never);
|
|
153
|
+
const data = await parseResponseJSON<{ status: string; message: string }>(res);
|
|
154
|
+
|
|
155
|
+
expect(res.status).toBe(200);
|
|
156
|
+
expect(data.status).toBe('committed');
|
|
157
|
+
expect(data.message).toBe('Transaction committed');
|
|
158
|
+
expect(mockTxProvider.commitTransaction).toHaveBeenCalledTimes(1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('rollback action returns status rolled_back', async () => {
|
|
162
|
+
const req = createMockRequest('/api/db/transaction', {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
body: { connection: validConnection, action: 'rollback' },
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const res = await POST(req as never);
|
|
168
|
+
const data = await parseResponseJSON<{ status: string; message: string }>(res);
|
|
169
|
+
|
|
170
|
+
expect(res.status).toBe(200);
|
|
171
|
+
expect(data.status).toBe('rolled_back');
|
|
172
|
+
expect(data.message).toBe('Transaction rolled back');
|
|
173
|
+
expect(mockTxProvider.rollbackTransaction).toHaveBeenCalledTimes(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('query action with sql returns result with pagination', async () => {
|
|
177
|
+
const req = createMockRequest('/api/db/transaction', {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
body: { connection: validConnection, action: 'query', sql: 'SELECT * FROM users' },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const res = await POST(req as never);
|
|
183
|
+
const data = await parseResponseJSON<{
|
|
184
|
+
rows: unknown[];
|
|
185
|
+
fields: string[];
|
|
186
|
+
rowCount: number;
|
|
187
|
+
inTransaction: boolean;
|
|
188
|
+
pagination: { limit: number; offset: number; hasMore: boolean; totalReturned: number; wasLimited: boolean };
|
|
189
|
+
}>(res);
|
|
190
|
+
|
|
191
|
+
expect(res.status).toBe(200);
|
|
192
|
+
expect(data.inTransaction).toBe(true);
|
|
193
|
+
expect(data.rows).toBeDefined();
|
|
194
|
+
expect(data.fields).toBeDefined();
|
|
195
|
+
expect(data.pagination).toBeDefined();
|
|
196
|
+
expect(data.pagination.wasLimited).toBeDefined();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('query action without sql returns 400', async () => {
|
|
200
|
+
const req = createMockRequest('/api/db/transaction', {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
body: { connection: validConnection, action: 'query' },
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const res = await POST(req as never);
|
|
206
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
207
|
+
|
|
208
|
+
expect(res.status).toBe(400);
|
|
209
|
+
expect(data.error).toContain('SQL query is required');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('status action returns inTransaction boolean', async () => {
|
|
213
|
+
mockTxProvider.isInTransaction.mockImplementation(() => false);
|
|
214
|
+
|
|
215
|
+
const req = createMockRequest('/api/db/transaction', {
|
|
216
|
+
method: 'POST',
|
|
217
|
+
body: { connection: validConnection, action: 'status' },
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const res = await POST(req as never);
|
|
221
|
+
const data = await parseResponseJSON<{ inTransaction: boolean }>(res);
|
|
222
|
+
|
|
223
|
+
expect(res.status).toBe(200);
|
|
224
|
+
expect(data.inTransaction).toBe(false);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('unknown action returns 400', async () => {
|
|
228
|
+
const req = createMockRequest('/api/db/transaction', {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
body: { connection: validConnection, action: 'invalid-action' },
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const res = await POST(req as never);
|
|
234
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
235
|
+
|
|
236
|
+
expect(res.status).toBe(400);
|
|
237
|
+
expect(data.error).toContain('Unknown transaction action');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('missing connection returns 400', async () => {
|
|
241
|
+
const req = createMockRequest('/api/db/transaction', {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
body: { action: 'begin' },
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const res = await POST(req as never);
|
|
247
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
248
|
+
|
|
249
|
+
expect(res.status).toBe(400);
|
|
250
|
+
expect(data.error).toContain('required');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('missing action returns 400', async () => {
|
|
254
|
+
const req = createMockRequest('/api/db/transaction', {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
body: { connection: validConnection },
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const res = await POST(req as never);
|
|
260
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
261
|
+
|
|
262
|
+
expect(res.status).toBe(400);
|
|
263
|
+
expect(data.error).toContain('Connection and action are required');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('provider without transaction support returns 400', async () => {
|
|
267
|
+
mockGetOrCreateProvider.mockImplementation(async () => mockNonTxProvider as never);
|
|
268
|
+
|
|
269
|
+
const req = createMockRequest('/api/db/transaction', {
|
|
270
|
+
method: 'POST',
|
|
271
|
+
body: { connection: validConnection, action: 'begin' },
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const res = await POST(req as never);
|
|
275
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
276
|
+
|
|
277
|
+
expect(res.status).toBe(400);
|
|
278
|
+
expect(data.error).toContain('not supported');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('QueryError returns 400', async () => {
|
|
282
|
+
mockTxProvider.beginTransaction.mockImplementation(async () => {
|
|
283
|
+
throw new QueryError('Syntax error near BEGIN', 'postgres');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const req = createMockRequest('/api/db/transaction', {
|
|
287
|
+
method: 'POST',
|
|
288
|
+
body: { connection: validConnection, action: 'begin' },
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const res = await POST(req as never);
|
|
292
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
293
|
+
|
|
294
|
+
expect(res.status).toBe(400);
|
|
295
|
+
expect(data.error).toContain('Syntax error');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('DatabaseError returns 500', async () => {
|
|
299
|
+
mockTxProvider.beginTransaction.mockImplementation(async () => {
|
|
300
|
+
throw new DatabaseError('Internal database error', 'postgres', 'DATABASE_ERROR');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const req = createMockRequest('/api/db/transaction', {
|
|
304
|
+
method: 'POST',
|
|
305
|
+
body: { connection: validConnection, action: 'begin' },
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const res = await POST(req as never);
|
|
309
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
310
|
+
|
|
311
|
+
expect(res.status).toBe(500);
|
|
312
|
+
expect(data.error).toContain('Internal database error');
|
|
313
|
+
});
|
|
314
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { NextRequest } from 'next/server';
|
|
3
|
+
import { SignJWT } from 'jose';
|
|
4
|
+
import { proxy } from '@/proxy';
|
|
5
|
+
|
|
6
|
+
// ─── JWT helpers ────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const JWT_SECRET = new TextEncoder().encode('test-jwt-secret-for-unit-tests-32ch');
|
|
9
|
+
|
|
10
|
+
async function createToken(role: string, expiresIn = '1h') {
|
|
11
|
+
return await new SignJWT({ role, username: role })
|
|
12
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
13
|
+
.setIssuedAt()
|
|
14
|
+
.setExpirationTime(expiresIn)
|
|
15
|
+
.sign(JWT_SECRET);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createNextRequest(pathname: string, token?: string): NextRequest {
|
|
19
|
+
const url = `http://localhost:3000${pathname}`;
|
|
20
|
+
const headers = new Headers();
|
|
21
|
+
if (token) {
|
|
22
|
+
headers.set('cookie', `auth-token=${token}`);
|
|
23
|
+
}
|
|
24
|
+
return new NextRequest(url, { headers });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isRedirect(response: Response): boolean {
|
|
28
|
+
return response.status === 307 || response.status === 308 || response.status === 302 || response.status === 301;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getRedirectLocation(response: Response): string | null {
|
|
32
|
+
return response.headers.get('location');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Tests ──────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
describe('proxy', () => {
|
|
38
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
39
|
+
// Public routes
|
|
40
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
describe('public routes', () => {
|
|
43
|
+
test('/api/auth/login passes through without redirect', async () => {
|
|
44
|
+
const req = createNextRequest('/api/auth/login');
|
|
45
|
+
const res = await proxy(req);
|
|
46
|
+
|
|
47
|
+
expect(isRedirect(res)).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('/api/db/health passes through without redirect', async () => {
|
|
51
|
+
const req = createNextRequest('/api/db/health');
|
|
52
|
+
const res = await proxy(req);
|
|
53
|
+
|
|
54
|
+
expect(isRedirect(res)).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('/_next/static/chunk.js passes through without redirect', async () => {
|
|
58
|
+
const req = createNextRequest('/_next/static/chunk.js');
|
|
59
|
+
const res = await proxy(req);
|
|
60
|
+
|
|
61
|
+
expect(isRedirect(res)).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('/favicon.ico passes through without redirect', async () => {
|
|
65
|
+
const req = createNextRequest('/favicon.ico');
|
|
66
|
+
const res = await proxy(req);
|
|
67
|
+
|
|
68
|
+
expect(isRedirect(res)).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
73
|
+
// Login page
|
|
74
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
describe('/login page', () => {
|
|
77
|
+
test('allows access without token', async () => {
|
|
78
|
+
const req = createNextRequest('/login');
|
|
79
|
+
const res = await proxy(req);
|
|
80
|
+
|
|
81
|
+
expect(isRedirect(res)).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('redirects to /admin with valid admin token', async () => {
|
|
85
|
+
const token = await createToken('admin');
|
|
86
|
+
const req = createNextRequest('/login', token);
|
|
87
|
+
const res = await proxy(req);
|
|
88
|
+
|
|
89
|
+
expect(isRedirect(res)).toBe(true);
|
|
90
|
+
expect(getRedirectLocation(res)).toContain('/admin');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('redirects to / with valid user token', async () => {
|
|
94
|
+
const token = await createToken('user');
|
|
95
|
+
const req = createNextRequest('/login', token);
|
|
96
|
+
const res = await proxy(req);
|
|
97
|
+
|
|
98
|
+
expect(isRedirect(res)).toBe(true);
|
|
99
|
+
const location = getRedirectLocation(res)!;
|
|
100
|
+
// Should redirect to root, not /admin
|
|
101
|
+
expect(location).toContain('http://localhost:3000');
|
|
102
|
+
expect(location).not.toContain('/admin');
|
|
103
|
+
expect(location).not.toContain('/login');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('allows access with invalid token', async () => {
|
|
107
|
+
const req = createNextRequest('/login', 'invalid-token-garbage');
|
|
108
|
+
const res = await proxy(req);
|
|
109
|
+
|
|
110
|
+
expect(isRedirect(res)).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
115
|
+
// Protected routes
|
|
116
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
describe('protected routes', () => {
|
|
119
|
+
test('redirects to /login without token', async () => {
|
|
120
|
+
const req = createNextRequest('/');
|
|
121
|
+
const res = await proxy(req);
|
|
122
|
+
|
|
123
|
+
expect(isRedirect(res)).toBe(true);
|
|
124
|
+
expect(getRedirectLocation(res)).toContain('/login');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('allows access with valid token', async () => {
|
|
128
|
+
const token = await createToken('user');
|
|
129
|
+
const req = createNextRequest('/', token);
|
|
130
|
+
const res = await proxy(req);
|
|
131
|
+
|
|
132
|
+
expect(isRedirect(res)).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('redirects to /login with expired/invalid token', async () => {
|
|
136
|
+
const req = createNextRequest('/', 'expired-or-invalid-token');
|
|
137
|
+
const res = await proxy(req);
|
|
138
|
+
|
|
139
|
+
expect(isRedirect(res)).toBe(true);
|
|
140
|
+
expect(getRedirectLocation(res)).toContain('/login');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
145
|
+
// RBAC: /admin routes
|
|
146
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
describe('/admin RBAC', () => {
|
|
149
|
+
test('allows admin role to access /admin', async () => {
|
|
150
|
+
const token = await createToken('admin');
|
|
151
|
+
const req = createNextRequest('/admin', token);
|
|
152
|
+
const res = await proxy(req);
|
|
153
|
+
|
|
154
|
+
expect(isRedirect(res)).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('redirects user role from /admin to /', async () => {
|
|
158
|
+
const token = await createToken('user');
|
|
159
|
+
const req = createNextRequest('/admin', token);
|
|
160
|
+
const res = await proxy(req);
|
|
161
|
+
|
|
162
|
+
expect(isRedirect(res)).toBe(true);
|
|
163
|
+
const location = getRedirectLocation(res)!;
|
|
164
|
+
expect(location).toContain('http://localhost:3000');
|
|
165
|
+
expect(location).not.toContain('/admin');
|
|
166
|
+
expect(location).not.toContain('/login');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
171
|
+
// API routes with auth
|
|
172
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
describe('API routes', () => {
|
|
175
|
+
test('/api/db/query with valid token passes through', async () => {
|
|
176
|
+
const token = await createToken('user');
|
|
177
|
+
const req = createNextRequest('/api/db/query', token);
|
|
178
|
+
const res = await proxy(req);
|
|
179
|
+
|
|
180
|
+
expect(isRedirect(res)).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('/api/db/query without token redirects to /login', async () => {
|
|
184
|
+
const req = createNextRequest('/api/db/query');
|
|
185
|
+
const res = await proxy(req);
|
|
186
|
+
|
|
187
|
+
expect(isRedirect(res)).toBe(true);
|
|
188
|
+
expect(getRedirectLocation(res)).toContain('/login');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|