@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,174 @@
|
|
|
1
|
+
import { describe, test, expect, mock, beforeEach } from 'bun:test';
|
|
2
|
+
import { createMockRequest, readStreamResponse, parseResponseJSON } from '../../helpers/mock-next';
|
|
3
|
+
|
|
4
|
+
// ─── Mock helpers ───────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
function createMockStream(text = 'mock response') {
|
|
7
|
+
return new ReadableStream({
|
|
8
|
+
start(controller) {
|
|
9
|
+
controller.enqueue(new TextEncoder().encode(text));
|
|
10
|
+
controller.close();
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class MockLLMError extends Error {
|
|
16
|
+
statusCode?: number;
|
|
17
|
+
constructor(msg: string, _provider?: string, code?: number) {
|
|
18
|
+
super(msg);
|
|
19
|
+
this.name = 'LLMError';
|
|
20
|
+
this.statusCode = code;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
class MockLLMConfigError extends MockLLMError {
|
|
24
|
+
constructor(msg: string) { super(msg); this.name = 'LLMConfigError'; }
|
|
25
|
+
}
|
|
26
|
+
class MockLLMAuthError extends MockLLMError {
|
|
27
|
+
constructor(msg: string) { super(msg, undefined, 401); this.name = 'LLMAuthError'; }
|
|
28
|
+
}
|
|
29
|
+
class MockLLMRateLimitError extends MockLLMError {
|
|
30
|
+
constructor(msg: string) { super(msg, undefined, 429); this.name = 'LLMRateLimitError'; }
|
|
31
|
+
}
|
|
32
|
+
class MockLLMSafetyError extends MockLLMError {
|
|
33
|
+
constructor(msg: string) { super(msg, undefined, 400); this.name = 'LLMSafetyError'; }
|
|
34
|
+
}
|
|
35
|
+
class MockLLMStreamError extends MockLLMError {
|
|
36
|
+
constructor(msg: string) { super(msg); this.name = 'LLMStreamError'; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Mock @/lib/llm BEFORE importing the route ─────────────────────────────
|
|
40
|
+
|
|
41
|
+
const mockStream = mock(async () => createMockStream());
|
|
42
|
+
const mockProvider = { stream: mockStream };
|
|
43
|
+
const mockCreateLLMProvider = mock(async () => mockProvider);
|
|
44
|
+
|
|
45
|
+
mock.module('@/lib/llm', () => ({
|
|
46
|
+
createLLMProvider: mockCreateLLMProvider,
|
|
47
|
+
LLMError: MockLLMError,
|
|
48
|
+
LLMConfigError: MockLLMConfigError,
|
|
49
|
+
LLMAuthError: MockLLMAuthError,
|
|
50
|
+
LLMRateLimitError: MockLLMRateLimitError,
|
|
51
|
+
LLMSafetyError: MockLLMSafetyError,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
mock.module('@/lib/llm/types', () => ({
|
|
55
|
+
LLMError: MockLLMError,
|
|
56
|
+
LLMConfigError: MockLLMConfigError,
|
|
57
|
+
LLMAuthError: MockLLMAuthError,
|
|
58
|
+
LLMRateLimitError: MockLLMRateLimitError,
|
|
59
|
+
LLMSafetyError: MockLLMSafetyError,
|
|
60
|
+
LLMStreamError: MockLLMStreamError,
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
// ─── Import route handler AFTER mocking ─────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
const { POST } = await import('@/app/api/ai/autopilot/route');
|
|
66
|
+
|
|
67
|
+
// ─── Tests ──────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
describe('POST /api/ai/autopilot', () => {
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
mockCreateLLMProvider.mockClear();
|
|
72
|
+
mockStream.mockClear();
|
|
73
|
+
mockCreateLLMProvider.mockImplementation(async () => mockProvider);
|
|
74
|
+
mockStream.mockImplementation(async () => createMockStream());
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('returns streaming response with all metrics', async () => {
|
|
78
|
+
const req = createMockRequest('/api/ai/autopilot', {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
body: {
|
|
81
|
+
slowQueries: [{ query: 'SELECT * FROM users', avgTime: 200, calls: 50 }],
|
|
82
|
+
indexStats: [{ name: 'idx_users_email', scans: 100 }],
|
|
83
|
+
tableStats: [{ name: 'users', rows: 10000, size: '5 MB' }],
|
|
84
|
+
performanceMetrics: { cacheHitRatio: 0.95 },
|
|
85
|
+
overview: { version: '15.2', uptime: '2 days' },
|
|
86
|
+
schemaContext: 'users(id, name, email)',
|
|
87
|
+
databaseType: 'postgres',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const res = await POST(req as never);
|
|
92
|
+
expect(res.status).toBe(200);
|
|
93
|
+
expect(res.headers.get('content-type')).toBe('text/plain; charset=utf-8');
|
|
94
|
+
|
|
95
|
+
const text = await readStreamResponse(res);
|
|
96
|
+
expect(text).toBe('mock response');
|
|
97
|
+
expect(mockCreateLLMProvider).toHaveBeenCalledTimes(1);
|
|
98
|
+
expect(mockStream).toHaveBeenCalledTimes(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('returns streaming response with partial data', async () => {
|
|
102
|
+
const req = createMockRequest('/api/ai/autopilot', {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
body: {
|
|
105
|
+
slowQueries: [{ query: 'SELECT 1', avgTime: 10 }],
|
|
106
|
+
databaseType: 'postgres',
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const res = await POST(req as never);
|
|
111
|
+
expect(res.status).toBe(200);
|
|
112
|
+
|
|
113
|
+
const text = await readStreamResponse(res);
|
|
114
|
+
expect(text).toBe('mock response');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('returns streaming response with empty body (no validation)', async () => {
|
|
118
|
+
const req = createMockRequest('/api/ai/autopilot', {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
body: {},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const res = await POST(req as never);
|
|
124
|
+
expect(res.status).toBe(200);
|
|
125
|
+
|
|
126
|
+
const text = await readStreamResponse(res);
|
|
127
|
+
expect(text).toBe('mock response');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('returns 503 on LLMConfigError', async () => {
|
|
131
|
+
mockCreateLLMProvider.mockImplementation(async () => {
|
|
132
|
+
throw new MockLLMConfigError('LLM not configured');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const req = createMockRequest('/api/ai/autopilot', {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
body: {},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const res = await POST(req as never);
|
|
141
|
+
expect(res.status).toBe(503);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('returns 401 on LLMAuthError', async () => {
|
|
145
|
+
mockCreateLLMProvider.mockImplementation(async () => {
|
|
146
|
+
throw new MockLLMAuthError('Invalid API key');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const req = createMockRequest('/api/ai/autopilot', {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
body: {},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const res = await POST(req as never);
|
|
155
|
+
expect(res.status).toBe(401);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('returns 500 on generic error', async () => {
|
|
159
|
+
mockStream.mockImplementation(async () => {
|
|
160
|
+
throw new Error('Something broke');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const req = createMockRequest('/api/ai/autopilot', {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
body: {},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const res = await POST(req as never);
|
|
169
|
+
expect(res.status).toBe(500);
|
|
170
|
+
|
|
171
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
172
|
+
expect(data.error).toBe('Something broke');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { describe, test, expect, mock, beforeEach } from 'bun:test';
|
|
2
|
+
import { createMockRequest, readStreamResponse, parseResponseJSON } from '../../helpers/mock-next';
|
|
3
|
+
|
|
4
|
+
// ─── Mock helpers ───────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
function createMockStream(text = 'mock response') {
|
|
7
|
+
return new ReadableStream({
|
|
8
|
+
start(controller) {
|
|
9
|
+
controller.enqueue(new TextEncoder().encode(text));
|
|
10
|
+
controller.close();
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class MockLLMError extends Error {
|
|
16
|
+
statusCode?: number;
|
|
17
|
+
constructor(msg: string, _provider?: string, code?: number) {
|
|
18
|
+
super(msg);
|
|
19
|
+
this.name = 'LLMError';
|
|
20
|
+
this.statusCode = code;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
class MockLLMConfigError extends MockLLMError {
|
|
24
|
+
constructor(msg: string) { super(msg); this.name = 'LLMConfigError'; }
|
|
25
|
+
}
|
|
26
|
+
class MockLLMAuthError extends MockLLMError {
|
|
27
|
+
constructor(msg: string) { super(msg, undefined, 401); this.name = 'LLMAuthError'; }
|
|
28
|
+
}
|
|
29
|
+
class MockLLMRateLimitError extends MockLLMError {
|
|
30
|
+
constructor(msg: string) { super(msg, undefined, 429); this.name = 'LLMRateLimitError'; }
|
|
31
|
+
}
|
|
32
|
+
class MockLLMSafetyError extends MockLLMError {
|
|
33
|
+
constructor(msg: string) { super(msg, undefined, 400); this.name = 'LLMSafetyError'; }
|
|
34
|
+
}
|
|
35
|
+
class MockLLMStreamError extends MockLLMError {
|
|
36
|
+
constructor(msg: string) { super(msg); this.name = 'LLMStreamError'; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Mock @/lib/llm BEFORE importing the route ─────────────────────────────
|
|
40
|
+
|
|
41
|
+
const mockStream = mock(async () => createMockStream());
|
|
42
|
+
const mockProvider = { stream: mockStream };
|
|
43
|
+
const mockCreateLLMProvider = mock(async () => mockProvider);
|
|
44
|
+
|
|
45
|
+
mock.module('@/lib/llm', () => ({
|
|
46
|
+
createLLMProvider: mockCreateLLMProvider,
|
|
47
|
+
LLMError: MockLLMError,
|
|
48
|
+
LLMConfigError: MockLLMConfigError,
|
|
49
|
+
LLMAuthError: MockLLMAuthError,
|
|
50
|
+
LLMRateLimitError: MockLLMRateLimitError,
|
|
51
|
+
LLMSafetyError: MockLLMSafetyError,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
mock.module('@/lib/llm/types', () => ({
|
|
55
|
+
LLMError: MockLLMError,
|
|
56
|
+
LLMConfigError: MockLLMConfigError,
|
|
57
|
+
LLMAuthError: MockLLMAuthError,
|
|
58
|
+
LLMRateLimitError: MockLLMRateLimitError,
|
|
59
|
+
LLMSafetyError: MockLLMSafetyError,
|
|
60
|
+
LLMStreamError: MockLLMStreamError,
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
// ─── Import route handler AFTER mocking ─────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
const { POST } = await import('@/app/api/ai/chat/route');
|
|
66
|
+
|
|
67
|
+
// ─── Tests ──────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
describe('POST /api/ai/chat', () => {
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
mockCreateLLMProvider.mockClear();
|
|
72
|
+
mockStream.mockClear();
|
|
73
|
+
mockCreateLLMProvider.mockImplementation(async () => mockProvider);
|
|
74
|
+
mockStream.mockImplementation(async () => createMockStream());
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('returns streaming response with prompt', async () => {
|
|
78
|
+
const req = createMockRequest('/api/ai/chat', {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
body: { prompt: 'SELECT * FROM users', databaseType: 'postgres' },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const res = await POST(req as never);
|
|
84
|
+
expect(res.status).toBe(200);
|
|
85
|
+
expect(res.headers.get('content-type')).toBe('text/plain; charset=utf-8');
|
|
86
|
+
|
|
87
|
+
const text = await readStreamResponse(res);
|
|
88
|
+
expect(text).toBe('mock response');
|
|
89
|
+
expect(mockCreateLLMProvider).toHaveBeenCalledTimes(1);
|
|
90
|
+
expect(mockStream).toHaveBeenCalledTimes(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('returns streaming response with conversation history', async () => {
|
|
94
|
+
const req = createMockRequest('/api/ai/chat', {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
body: {
|
|
97
|
+
prompt: 'And now join with orders',
|
|
98
|
+
databaseType: 'postgres',
|
|
99
|
+
conversationHistory: [
|
|
100
|
+
{ role: 'user', content: 'Show me all users' },
|
|
101
|
+
{ role: 'assistant', content: 'SELECT * FROM users;' },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const res = await POST(req as never);
|
|
107
|
+
expect(res.status).toBe(200);
|
|
108
|
+
|
|
109
|
+
const text = await readStreamResponse(res);
|
|
110
|
+
expect(text).toBe('mock response');
|
|
111
|
+
|
|
112
|
+
// Verify conversation history was passed in messages
|
|
113
|
+
const callArgs = (mockStream.mock.calls as unknown[][])[0][0] as { messages: Array<{ role: string; content: string }> };
|
|
114
|
+
expect(callArgs.messages.length).toBe(4); // system + 2 history + current prompt
|
|
115
|
+
expect(callArgs.messages[1].role).toBe('user');
|
|
116
|
+
expect(callArgs.messages[2].role).toBe('assistant');
|
|
117
|
+
expect(callArgs.messages[3].content).toBe('And now join with orders');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('returns streaming response with queryLanguage json (MongoDB)', async () => {
|
|
121
|
+
const req = createMockRequest('/api/ai/chat', {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
body: {
|
|
124
|
+
prompt: 'Find all active users',
|
|
125
|
+
databaseType: 'mongodb',
|
|
126
|
+
queryLanguage: 'json',
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const res = await POST(req as never);
|
|
131
|
+
expect(res.status).toBe(200);
|
|
132
|
+
|
|
133
|
+
const text = await readStreamResponse(res);
|
|
134
|
+
expect(text).toBe('mock response');
|
|
135
|
+
|
|
136
|
+
// System prompt should be MongoDB-specific
|
|
137
|
+
const callArgs = (mockStream.mock.calls as unknown[][])[0][0] as { messages: Array<{ role: string; content: string }> };
|
|
138
|
+
expect(callArgs.messages[0].content).toContain('MongoDB');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('returns streaming response with schemaContext', async () => {
|
|
142
|
+
const req = createMockRequest('/api/ai/chat', {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
body: {
|
|
145
|
+
prompt: 'Get user count',
|
|
146
|
+
schemaContext: 'users(id, name, email)',
|
|
147
|
+
databaseType: 'postgres',
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const res = await POST(req as never);
|
|
152
|
+
expect(res.status).toBe(200);
|
|
153
|
+
|
|
154
|
+
const callArgs = (mockStream.mock.calls as unknown[][])[0][0] as { messages: Array<{ role: string; content: string }> };
|
|
155
|
+
expect(callArgs.messages[0].content).toContain('users(id, name, email)');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('returns 503 on LLMConfigError', async () => {
|
|
159
|
+
mockCreateLLMProvider.mockImplementation(async () => {
|
|
160
|
+
throw new MockLLMConfigError('LLM not configured');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const req = createMockRequest('/api/ai/chat', {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
body: { prompt: 'test' },
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const res = await POST(req as never);
|
|
169
|
+
expect(res.status).toBe(503);
|
|
170
|
+
|
|
171
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
172
|
+
expect(data.error).toContain('LLM not configured');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('returns 401 on LLMAuthError', async () => {
|
|
176
|
+
mockCreateLLMProvider.mockImplementation(async () => {
|
|
177
|
+
throw new MockLLMAuthError('Invalid API key');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const req = createMockRequest('/api/ai/chat', {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
body: { prompt: 'test' },
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const res = await POST(req as never);
|
|
186
|
+
expect(res.status).toBe(401);
|
|
187
|
+
|
|
188
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
189
|
+
expect(data.error).toContain('Invalid API key');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('returns 429 on LLMRateLimitError', async () => {
|
|
193
|
+
mockCreateLLMProvider.mockImplementation(async () => {
|
|
194
|
+
throw new MockLLMRateLimitError('Rate limit exceeded');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const req = createMockRequest('/api/ai/chat', {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
body: { prompt: 'test' },
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const res = await POST(req as never);
|
|
203
|
+
expect(res.status).toBe(429);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('returns 400 on LLMSafetyError', async () => {
|
|
207
|
+
mockStream.mockImplementation(async () => {
|
|
208
|
+
throw new MockLLMSafetyError('Content blocked');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const req = createMockRequest('/api/ai/chat', {
|
|
212
|
+
method: 'POST',
|
|
213
|
+
body: { prompt: 'test' },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const res = await POST(req as never);
|
|
217
|
+
expect(res.status).toBe(400);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('returns custom status code on LLMError with statusCode', async () => {
|
|
221
|
+
mockStream.mockImplementation(async () => {
|
|
222
|
+
throw new MockLLMError('Service unavailable', undefined, 503);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const req = createMockRequest('/api/ai/chat', {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
body: { prompt: 'test' },
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const res = await POST(req as never);
|
|
231
|
+
expect(res.status).toBe(503);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('returns 500 on generic error', async () => {
|
|
235
|
+
mockStream.mockImplementation(async () => {
|
|
236
|
+
throw new Error('Something went wrong');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const req = createMockRequest('/api/ai/chat', {
|
|
240
|
+
method: 'POST',
|
|
241
|
+
body: { prompt: 'test' },
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const res = await POST(req as never);
|
|
245
|
+
expect(res.status).toBe(500);
|
|
246
|
+
|
|
247
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
248
|
+
expect(data.error).toBe('Something went wrong');
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { describe, test, expect, mock, beforeEach } from 'bun:test';
|
|
2
|
+
import { createMockRequest, readStreamResponse, parseResponseJSON } from '../../helpers/mock-next';
|
|
3
|
+
|
|
4
|
+
// ─── Mock helpers ───────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
function createMockStream(text = 'mock response') {
|
|
7
|
+
return new ReadableStream({
|
|
8
|
+
start(controller) {
|
|
9
|
+
controller.enqueue(new TextEncoder().encode(text));
|
|
10
|
+
controller.close();
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class MockLLMError extends Error {
|
|
16
|
+
statusCode?: number;
|
|
17
|
+
constructor(msg: string, _provider?: string, code?: number) {
|
|
18
|
+
super(msg);
|
|
19
|
+
this.name = 'LLMError';
|
|
20
|
+
this.statusCode = code;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
class MockLLMConfigError extends MockLLMError {
|
|
24
|
+
constructor(msg: string) { super(msg); this.name = 'LLMConfigError'; }
|
|
25
|
+
}
|
|
26
|
+
class MockLLMAuthError extends MockLLMError {
|
|
27
|
+
constructor(msg: string) { super(msg, undefined, 401); this.name = 'LLMAuthError'; }
|
|
28
|
+
}
|
|
29
|
+
class MockLLMRateLimitError extends MockLLMError {
|
|
30
|
+
constructor(msg: string) { super(msg, undefined, 429); this.name = 'LLMRateLimitError'; }
|
|
31
|
+
}
|
|
32
|
+
class MockLLMSafetyError extends MockLLMError {
|
|
33
|
+
constructor(msg: string) { super(msg, undefined, 400); this.name = 'LLMSafetyError'; }
|
|
34
|
+
}
|
|
35
|
+
class MockLLMStreamError extends MockLLMError {
|
|
36
|
+
constructor(msg: string) { super(msg); this.name = 'LLMStreamError'; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Mock @/lib/llm BEFORE importing the route ─────────────────────────────
|
|
40
|
+
|
|
41
|
+
const mockStream = mock(async () => createMockStream());
|
|
42
|
+
const mockProvider = { stream: mockStream };
|
|
43
|
+
const mockCreateLLMProvider = mock(async () => mockProvider);
|
|
44
|
+
|
|
45
|
+
mock.module('@/lib/llm', () => ({
|
|
46
|
+
createLLMProvider: mockCreateLLMProvider,
|
|
47
|
+
LLMError: MockLLMError,
|
|
48
|
+
LLMConfigError: MockLLMConfigError,
|
|
49
|
+
LLMAuthError: MockLLMAuthError,
|
|
50
|
+
LLMRateLimitError: MockLLMRateLimitError,
|
|
51
|
+
LLMSafetyError: MockLLMSafetyError,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
mock.module('@/lib/llm/types', () => ({
|
|
55
|
+
LLMError: MockLLMError,
|
|
56
|
+
LLMConfigError: MockLLMConfigError,
|
|
57
|
+
LLMAuthError: MockLLMAuthError,
|
|
58
|
+
LLMRateLimitError: MockLLMRateLimitError,
|
|
59
|
+
LLMSafetyError: MockLLMSafetyError,
|
|
60
|
+
LLMStreamError: MockLLMStreamError,
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
// ─── Import route handler AFTER mocking ─────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
const { POST } = await import('@/app/api/ai/describe-schema/route');
|
|
66
|
+
|
|
67
|
+
// ─── Tests ──────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
describe('POST /api/ai/describe-schema', () => {
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
mockCreateLLMProvider.mockClear();
|
|
72
|
+
mockStream.mockClear();
|
|
73
|
+
mockCreateLLMProvider.mockImplementation(async () => mockProvider);
|
|
74
|
+
mockStream.mockImplementation(async () => createMockStream());
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('returns streaming response with schemaContext', async () => {
|
|
78
|
+
const req = createMockRequest('/api/ai/describe-schema', {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
body: {
|
|
81
|
+
schemaContext: 'users(id, name, email), orders(id, user_id, total)',
|
|
82
|
+
databaseType: 'postgres',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const res = await POST(req as never);
|
|
87
|
+
expect(res.status).toBe(200);
|
|
88
|
+
expect(res.headers.get('content-type')).toBe('text/plain; charset=utf-8');
|
|
89
|
+
|
|
90
|
+
const text = await readStreamResponse(res);
|
|
91
|
+
expect(text).toBe('mock response');
|
|
92
|
+
expect(mockCreateLLMProvider).toHaveBeenCalledTimes(1);
|
|
93
|
+
expect(mockStream).toHaveBeenCalledTimes(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('returns 400 when schemaContext is missing', async () => {
|
|
97
|
+
const req = createMockRequest('/api/ai/describe-schema', {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
body: { databaseType: 'postgres' },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const res = await POST(req as never);
|
|
103
|
+
expect(res.status).toBe(400);
|
|
104
|
+
|
|
105
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
106
|
+
expect(data.error).toContain('required');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('returns streaming response with mode=table', async () => {
|
|
110
|
+
const req = createMockRequest('/api/ai/describe-schema', {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
body: {
|
|
113
|
+
schemaContext: 'users(id INTEGER PK, name VARCHAR, email VARCHAR UNIQUE)',
|
|
114
|
+
databaseType: 'postgres',
|
|
115
|
+
mode: 'table',
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const res = await POST(req as never);
|
|
120
|
+
expect(res.status).toBe(200);
|
|
121
|
+
|
|
122
|
+
const text = await readStreamResponse(res);
|
|
123
|
+
expect(text).toBe('mock response');
|
|
124
|
+
|
|
125
|
+
// Verify the table-specific system prompt was used
|
|
126
|
+
const callArgs = (mockStream.mock.calls as unknown[][])[0][0] as { messages: Array<{ role: string; content: string }> };
|
|
127
|
+
expect(callArgs.messages[0].content).toContain('table schema');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('returns 503 on LLMConfigError', async () => {
|
|
131
|
+
mockCreateLLMProvider.mockImplementation(async () => {
|
|
132
|
+
throw new MockLLMConfigError('AI not configured');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const req = createMockRequest('/api/ai/describe-schema', {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
body: { schemaContext: 'users(id, name)' },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const res = await POST(req as never);
|
|
141
|
+
expect(res.status).toBe(503);
|
|
142
|
+
|
|
143
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
144
|
+
expect(data.error).toContain('AI not configured');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('returns 401 on LLMAuthError', async () => {
|
|
148
|
+
mockCreateLLMProvider.mockImplementation(async () => {
|
|
149
|
+
throw new MockLLMAuthError('Invalid API key');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const req = createMockRequest('/api/ai/describe-schema', {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
body: { schemaContext: 'users(id, name)' },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const res = await POST(req as never);
|
|
158
|
+
expect(res.status).toBe(401);
|
|
159
|
+
|
|
160
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
161
|
+
expect(data.error).toContain('Invalid API key');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('returns 429 on LLMRateLimitError', async () => {
|
|
165
|
+
mockCreateLLMProvider.mockImplementation(async () => {
|
|
166
|
+
throw new MockLLMRateLimitError('Rate limit exceeded');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const req = createMockRequest('/api/ai/describe-schema', {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
body: { schemaContext: 'users(id, name)' },
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const res = await POST(req as never);
|
|
175
|
+
expect(res.status).toBe(429);
|
|
176
|
+
|
|
177
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
178
|
+
expect(data.error).toContain('usage limit');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('returns 400 on LLMSafetyError', async () => {
|
|
182
|
+
mockCreateLLMProvider.mockImplementation(async () => {
|
|
183
|
+
throw new MockLLMSafetyError('Content blocked');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const req = createMockRequest('/api/ai/describe-schema', {
|
|
187
|
+
method: 'POST',
|
|
188
|
+
body: { schemaContext: 'users(id, name)' },
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const res = await POST(req as never);
|
|
192
|
+
expect(res.status).toBe(400);
|
|
193
|
+
|
|
194
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
195
|
+
expect(data.error).toContain('safety');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('returns 500 with error.message for generic Error', async () => {
|
|
199
|
+
mockCreateLLMProvider.mockImplementation(async () => {
|
|
200
|
+
throw new Error('Something went wrong');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const req = createMockRequest('/api/ai/describe-schema', {
|
|
204
|
+
method: 'POST',
|
|
205
|
+
body: { schemaContext: 'users(id, name)' },
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const res = await POST(req as never);
|
|
209
|
+
expect(res.status).toBe(500);
|
|
210
|
+
|
|
211
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
212
|
+
expect(data.error).toBe('Something went wrong');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('returns 500 with Internal server error for non-Error thrown', async () => {
|
|
216
|
+
mockCreateLLMProvider.mockImplementation(async () => {
|
|
217
|
+
throw 'string error';
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const req = createMockRequest('/api/ai/describe-schema', {
|
|
221
|
+
method: 'POST',
|
|
222
|
+
body: { schemaContext: 'users(id, name)' },
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const res = await POST(req as never);
|
|
226
|
+
expect(res.status).toBe(500);
|
|
227
|
+
|
|
228
|
+
const data = await parseResponseJSON<{ error: string }>(res);
|
|
229
|
+
expect(data.error).toBe('Internal server error');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('mode omitted uses database overview prompt (not table prompt)', async () => {
|
|
233
|
+
const req = createMockRequest('/api/ai/describe-schema', {
|
|
234
|
+
method: 'POST',
|
|
235
|
+
body: {
|
|
236
|
+
schemaContext: 'users(id, name), orders(id, user_id)',
|
|
237
|
+
databaseType: 'postgres',
|
|
238
|
+
// mode not specified
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const res = await POST(req as never);
|
|
243
|
+
expect(res.status).toBe(200);
|
|
244
|
+
|
|
245
|
+
const callArgs = (mockStream.mock.calls as unknown[][])[0][0] as { messages: Array<{ role: string; content: string }> };
|
|
246
|
+
expect(callArgs.messages[0].content).toContain('Database Overview');
|
|
247
|
+
expect(callArgs.messages[0].content).not.toContain('table schema');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('databaseType omitted falls back to SQL in prompt', async () => {
|
|
251
|
+
const req = createMockRequest('/api/ai/describe-schema', {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
body: {
|
|
254
|
+
schemaContext: 'users(id, name)',
|
|
255
|
+
mode: 'table',
|
|
256
|
+
// databaseType not specified
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const res = await POST(req as never);
|
|
261
|
+
expect(res.status).toBe(200);
|
|
262
|
+
|
|
263
|
+
const callArgs = (mockStream.mock.calls as unknown[][])[0][0] as { messages: Array<{ role: string; content: string }> };
|
|
264
|
+
expect(callArgs.messages[0].content).toContain('Database type: SQL');
|
|
265
|
+
});
|
|
266
|
+
});
|