feat(mongodb): improve sidebar index management
This commit is contained in:
1 parent
22055e081f
commit
8e7b74d0ee
56 files changed
+3558
-246
No files matched your search
@@ -3,13 +3,16 @@ use std::sync::Arc;
|
||||
|
||||
use axum::extract::State;
|
||||
use axum::Json;
|
||||
use dbx_core::connection::AppState;
|
||||
use dbx_core::models::connection::{ConnectionConfig, ConnectionTestResult, DatabaseConnectionInfo};
|
||||
use dbx_core::connection::{AppState, PoolKind};
|
||||
use dbx_core::models::connection::{ConnectionConfig, ConnectionTestResult, DatabaseConnectionInfo, DatabaseType};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::state::WebState;
|
||||
|
||||
const MONGO_LEGACY_DRIVER_PROFILE: &str = "mongodb-legacy";
|
||||
const MONGO_LEGACY_DRIVER_LABEL: &str = "MongoDB (Legacy)";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ConnectRequest {
|
||||
@@ -69,6 +72,86 @@ fn is_connection_info_capability_unsupported(error: &str) -> bool {
|
||||
&& (error.contains("unsupported") || error.contains("unknown method") || error.contains("method not found"))
|
||||
}
|
||||
|
||||
fn mark_mongo_legacy_driver(config: &mut ConnectionConfig) -> bool {
|
||||
if config.db_type != DatabaseType::MongoDb {
|
||||
return false;
|
||||
}
|
||||
let changed = config.driver_profile.as_deref() != Some(MONGO_LEGACY_DRIVER_PROFILE)
|
||||
|| config.driver_label.as_deref() != Some(MONGO_LEGACY_DRIVER_LABEL);
|
||||
config.driver_profile = Some(MONGO_LEGACY_DRIVER_PROFILE.to_string());
|
||||
config.driver_label = Some(MONGO_LEGACY_DRIVER_LABEL.to_string());
|
||||
changed
|
||||
}
|
||||
|
||||
fn mongo_fallback_config_matches(current: &ConnectionConfig, expected: &ConnectionConfig) -> bool {
|
||||
let mut current = current.clone();
|
||||
let mut expected = expected.clone();
|
||||
current.note.clear();
|
||||
current.database_info = None;
|
||||
expected.note.clear();
|
||||
expected.database_info = None;
|
||||
if current == expected {
|
||||
return true;
|
||||
}
|
||||
if current.driver_profile.as_deref() != Some(MONGO_LEGACY_DRIVER_PROFILE)
|
||||
|| current.driver_label.as_deref() != Some(MONGO_LEGACY_DRIVER_LABEL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
current.driver_profile = expected.driver_profile.clone();
|
||||
current.driver_label = expected.driver_label.clone();
|
||||
current == expected
|
||||
}
|
||||
|
||||
async fn apply_mongo_legacy_driver_profile(state: &WebState, config: &ConnectionConfig) -> Result<(), AppError> {
|
||||
if config.db_type != DatabaseType::MongoDb {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Draft and one-time connections have no durable profile to update.
|
||||
let persisted = if config.one_time {
|
||||
true
|
||||
} else {
|
||||
state
|
||||
.app
|
||||
.storage
|
||||
.save_connection_driver_profile(
|
||||
config,
|
||||
Some(MONGO_LEGACY_DRIVER_PROFILE.to_string()),
|
||||
Some(MONGO_LEGACY_DRIVER_LABEL.to_string()),
|
||||
)
|
||||
.await
|
||||
.map_err(AppError::from)?
|
||||
};
|
||||
if !persisted {
|
||||
return Ok(());
|
||||
}
|
||||
let mut runtime_configs = state.app.configs.write().await;
|
||||
if let Some(current) =
|
||||
runtime_configs.get_mut(&config.id).filter(|current| mongo_fallback_config_matches(current, config))
|
||||
{
|
||||
mark_mongo_legacy_driver(current);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The core connector can choose the Legacy Agent after a native MongoDB
|
||||
/// handshake failure. Keep Web's runtime and saved profile aligned so the UI
|
||||
/// does not expose native-only collection rename for that session.
|
||||
async fn sync_mongo_legacy_driver_fallback(state: &WebState, config: &ConnectionConfig) -> Result<(), AppError> {
|
||||
if config.db_type != DatabaseType::MongoDb {
|
||||
return Ok(());
|
||||
}
|
||||
let uses_legacy_agent = {
|
||||
let connections = state.app.connections.read().await;
|
||||
matches!(connections.get(&config.id), Some(PoolKind::Agent(_)))
|
||||
};
|
||||
if !uses_legacy_agent {
|
||||
return Ok(());
|
||||
}
|
||||
apply_mongo_legacy_driver_profile(state, config).await
|
||||
}
|
||||
|
||||
async fn run_temporary_connection_test(
|
||||
app: &Arc<AppState>,
|
||||
config: ConnectionConfig,
|
||||
@@ -143,6 +226,11 @@ pub async fn connect_db(
|
||||
app.configs.write().await.insert(connection_id.clone(), config.clone());
|
||||
|
||||
app.get_or_create_pool_for_connection_attempt(&connection_id, None, attempt).await.map_err(AppError::from)?;
|
||||
if let Err(error) = sync_mongo_legacy_driver_fallback(&state, &config).await {
|
||||
app.remove_connection_pools_detached(&connection_id).await;
|
||||
app.reset_connection_transport_for_config(&connection_id, &config).await;
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
Ok(Json(connection_id))
|
||||
}
|
||||
@@ -391,10 +479,10 @@ async fn remove_connection_pools_for_connection_ids(state: &WebState, connection
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
connect_db, connection_final_proxy_port, disconnect_db, load_connections, mcp_add_connection,
|
||||
mcp_remove_connection, save_connection_database_info, save_connections, test_connection,
|
||||
test_connection_with_info, ConnectRequest, DisconnectRequest, McpAddConnectionRequest,
|
||||
McpRemoveConnectionRequest, SaveConnectionDatabaseInfoRequest, SaveConnectionsRequest,
|
||||
apply_mongo_legacy_driver_profile, connect_db, connection_final_proxy_port, disconnect_db, load_connections,
|
||||
mark_mongo_legacy_driver, mcp_add_connection, mcp_remove_connection, save_connection_database_info,
|
||||
save_connections, test_connection, test_connection_with_info, ConnectRequest, DisconnectRequest,
|
||||
McpAddConnectionRequest, McpRemoveConnectionRequest, SaveConnectionDatabaseInfoRequest, SaveConnectionsRequest,
|
||||
};
|
||||
use crate::state::WebState;
|
||||
use axum::extract::State;
|
||||
@@ -481,6 +569,70 @@ mod tests {
|
||||
config
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mongo_legacy_marker_updates_only_mongodb_profiles() {
|
||||
let mut mongo = sqlite_config("mongo", "");
|
||||
mongo.db_type = DatabaseType::MongoDb;
|
||||
mongo.driver_profile = Some("legacy".to_string());
|
||||
assert!(mark_mongo_legacy_driver(&mut mongo));
|
||||
assert_eq!(mongo.driver_profile.as_deref(), Some("mongodb-legacy"));
|
||||
assert_eq!(mongo.driver_label.as_deref(), Some("MongoDB (Legacy)"));
|
||||
assert!(!mark_mongo_legacy_driver(&mut mongo));
|
||||
|
||||
let mut sqlite = sqlite_config("sqlite", ":memory:");
|
||||
assert!(!mark_mongo_legacy_driver(&mut sqlite));
|
||||
assert_eq!(sqlite.driver_profile, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mongo_legacy_profile_sync_preserves_unrelated_saved_connections() {
|
||||
let (state, dir) = test_web_state().await;
|
||||
let mut mongo = sqlite_config("mongo", "");
|
||||
mongo.db_type = DatabaseType::MongoDb;
|
||||
let other = sqlite_config("other", ":memory:");
|
||||
state.app.storage.save_connections(&[mongo.clone(), other.clone()]).await.unwrap();
|
||||
let mut current = mongo.clone();
|
||||
current.note = "Updated while connecting".to_string();
|
||||
state.app.configs.write().await.insert(current.id.clone(), current);
|
||||
|
||||
apply_mongo_legacy_driver_profile(&state, &mongo).await.unwrap();
|
||||
|
||||
let runtime = state.app.configs.read().await.get(&mongo.id).cloned().unwrap();
|
||||
assert_eq!(runtime.note, "Updated while connecting");
|
||||
assert_eq!(runtime.driver_profile.as_deref(), Some("mongodb-legacy"));
|
||||
assert_eq!(runtime.driver_label.as_deref(), Some("MongoDB (Legacy)"));
|
||||
let saved = state.app.storage.load_connections().await.unwrap();
|
||||
assert_eq!(saved.len(), 2);
|
||||
assert_eq!(
|
||||
saved.iter().find(|config| config.id == mongo.id).and_then(|config| config.driver_profile.as_deref()),
|
||||
Some("mongodb-legacy")
|
||||
);
|
||||
assert!(saved.iter().any(|config| config.id == other.id));
|
||||
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mongo_legacy_profile_sync_does_not_overwrite_a_replacement_connection() {
|
||||
let (state, dir) = test_web_state().await;
|
||||
let mut original = sqlite_config("mongo", "");
|
||||
original.db_type = DatabaseType::MongoDb;
|
||||
state.app.storage.save_connections(std::slice::from_ref(&original)).await.unwrap();
|
||||
|
||||
let mut replacement = original.clone();
|
||||
replacement.host = "replacement.example.com".to_string();
|
||||
replacement.name = "Replacement MongoDB".to_string();
|
||||
state.app.storage.save_connections(std::slice::from_ref(&replacement)).await.unwrap();
|
||||
state.app.configs.write().await.insert(replacement.id.clone(), replacement.clone());
|
||||
|
||||
apply_mongo_legacy_driver_profile(&state, &original).await.unwrap();
|
||||
|
||||
assert_eq!(state.app.configs.read().await.get(&replacement.id), Some(&replacement));
|
||||
assert_eq!(state.app.storage.load_connections().await.unwrap(), vec![replacement]);
|
||||
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
async fn test_web_state() -> (Arc<WebState>, std::path::PathBuf) {
|
||||
let dir = std::env::temp_dir().join(format!("dbx-web-test-{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
|
||||
@@ -336,8 +336,11 @@ pub async fn create_database(
|
||||
|
||||
pub async fn drop_database(
|
||||
State(state): State<Arc<WebState>>,
|
||||
headers: HeaderMap,
|
||||
Json(req): Json<MongoCollectionRequest>,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
super::mcp_policy::ensure_dangerous_write(&state, &headers, &req.connection_id, &req.database, "Drop database")
|
||||
.await?;
|
||||
ensure_writable(&state.app, &req.connection_id, "Drop database").await?;
|
||||
dbx_core::mongo_ops::mongo_drop_database_core(&state.app, &req.connection_id, &req.database)
|
||||
.await
|
||||
@@ -796,9 +799,13 @@ async fn ensure_find_one_write_policy(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ensure_find_one_write_policy;
|
||||
use super::{drop_database, ensure_find_one_write_policy, MongoCollectionRequest};
|
||||
use crate::state::WebState;
|
||||
use axum::http::{HeaderMap, HeaderValue};
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::{HeaderMap, HeaderValue},
|
||||
Json,
|
||||
};
|
||||
use dbx_core::connection::AppState;
|
||||
use dbx_core::models::connection::ConnectionConfig;
|
||||
use dbx_core::storage::{McpGlobalPolicy, Storage};
|
||||
@@ -901,4 +908,33 @@ mod tests {
|
||||
drop(state);
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn drop_database_requires_mcp_dangerous_write_approval() {
|
||||
let (state, dir) = test_web_state().await;
|
||||
let connection = mongo_config(false);
|
||||
state.app.storage.save_connections(std::slice::from_ref(&connection)).await.unwrap();
|
||||
state
|
||||
.app
|
||||
.storage
|
||||
.save_mcp_global_policy(&McpGlobalPolicy {
|
||||
read_only: false,
|
||||
allow_dangerous_sql: false,
|
||||
allowed_connection_ids: Some(vec![connection.id.clone()]),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let error = drop_database(
|
||||
State(state.clone()),
|
||||
mcp_headers(),
|
||||
Json(MongoCollectionRequest { connection_id: connection.id, database: "app".to_string() }),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(error.message.starts_with("SQL_BLOCKED:"), "{}", error.message);
|
||||
|
||||
drop(state);
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user