feat(mongodb): 支持聚合查询、批量写入和 MCP 集成
- 桌面端:解析 db.<集合>.aggregate([...]) shell 语法,新增 mongoAggregateDocuments
接口(Tauri + HTTP);mongo 入口统一补 ensureConnected;ObjectId(...) 自动
转换为 {"\$oid":"..."} 扩展 JSON 表示。
- dbx-core:新增 aggregate_documents、insert/update/delete_documents bulk
操作;扩展 JSON 解析识别 \$oid;schema list_tables_core 补 MongoDB/Elasticsearch
分支返回 COLLECTION/INDEX。
- dbx-web / Tauri 命令 / MCP bridge:注册 4 个新 mongo 路由
(aggregate/insert/update/delete documents);MCP bridge 新增 mongo HTTP 入口
和 lazy 连接复用。
- node-core (MCP 后端):mongodb 走专属 executeQuery / listTables /
describeTable;read-only 默认 + DBX_MCP_ALLOW_WRITES /
DBX_MCP_ALLOW_DANGEROUS_SQL 两级开关;aggregate 含 \$out/\$merge 走同样开关;
describeTable 抽样 20 条文档推断字段类型,_id 自动标记主键。
- mcp-server:mongodb 跳过 SQL safety evaluator(mongo shell 与 SQL 语法
不兼容),改由 backend 做命令级 gating;附注释。
- AiAssistant 同时 emit replaceSql + executeSql 让编辑器和结果保持一致;
App.vue ensureQueryTab 在无上下文时回退到连接默认库;aiMessageRender
识别 mongodb / mongo 代码块。
- Review 修复:合并两份重复的 evaluateMongo*Safety 实现(同时修复 web-backend
用字符串比较导致空 filter 检查可被空格绕过的 bug);ObjectId 正则兼容
单/双引号;mongo_ops 错误文案 "MCP" 改为 "bulk";补 aggregate 解析测试用例。
测试:pnpm test 911 通过;vue-tsc 通过;cargo check -p dbx-core 通过。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
d57660eba9
commit
c56bcb8e2d
22 files changed
+1492
-10
No files matched your search
@@ -39,6 +39,52 @@ struct DescribeTableRequest {
|
||||
table: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MongoFindDocumentsRequest {
|
||||
connection_name: String,
|
||||
database: Option<String>,
|
||||
collection: String,
|
||||
skip: Option<u64>,
|
||||
limit: Option<i64>,
|
||||
filter: Option<String>,
|
||||
sort: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MongoAggregateDocumentsRequest {
|
||||
connection_name: String,
|
||||
database: Option<String>,
|
||||
collection: String,
|
||||
pipeline_json: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MongoInsertDocumentsRequest {
|
||||
connection_name: String,
|
||||
database: Option<String>,
|
||||
collection: String,
|
||||
docs_json: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MongoUpdateDocumentsRequest {
|
||||
connection_name: String,
|
||||
database: Option<String>,
|
||||
collection: String,
|
||||
filter_json: String,
|
||||
update_json: String,
|
||||
many: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MongoDeleteDocumentsRequest {
|
||||
connection_name: String,
|
||||
database: Option<String>,
|
||||
collection: String,
|
||||
filter_json: String,
|
||||
many: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct McpOpenTableEvent {
|
||||
pub connection_id: String,
|
||||
@@ -92,6 +138,18 @@ pub fn start(app_handle: AppHandle, state: Arc<AppState>) {
|
||||
handle_list_tables_data(&st, body, &mut stream).await;
|
||||
} else if first_line.starts_with("POST /data/describe-table") {
|
||||
handle_describe_table_data(&st, body, &mut stream).await;
|
||||
} else if first_line.starts_with("POST /data/mongo/list-collections") {
|
||||
handle_mongo_list_collections_data(&st, body, &mut stream).await;
|
||||
} else if first_line.starts_with("POST /data/mongo/find-documents") {
|
||||
handle_mongo_find_documents_data(&st, body, &mut stream).await;
|
||||
} else if first_line.starts_with("POST /data/mongo/aggregate-documents") {
|
||||
handle_mongo_aggregate_documents_data(&st, body, &mut stream).await;
|
||||
} else if first_line.starts_with("POST /data/mongo/insert-documents") {
|
||||
handle_mongo_insert_documents_data(&st, body, &mut stream).await;
|
||||
} else if first_line.starts_with("POST /data/mongo/update-documents") {
|
||||
handle_mongo_update_documents_data(&st, body, &mut stream).await;
|
||||
} else if first_line.starts_with("POST /data/mongo/delete-documents") {
|
||||
handle_mongo_delete_documents_data(&st, body, &mut stream).await;
|
||||
} else if first_line.starts_with("POST /data/execute-query") {
|
||||
handle_execute_query_data(&st, body, &mut stream).await;
|
||||
} else if first_line.starts_with("POST /execute-query") {
|
||||
@@ -148,6 +206,30 @@ async fn resolve_connection(
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
async fn resolve_mongo_pool_key(
|
||||
state: &Arc<AppState>,
|
||||
connection_name: &str,
|
||||
database: Option<String>,
|
||||
stream: &mut tokio::net::TcpStream,
|
||||
) -> Option<(String, String)> {
|
||||
let config = match resolve_connection(state, connection_name).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
respond_error(stream, "404 Not Found", &e).await;
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let database = database.unwrap_or_else(|| config.database.clone().unwrap_or_default());
|
||||
let pool_key = match state.get_or_create_pool(&config.id, Some(&database)).await {
|
||||
Ok(key) => key,
|
||||
Err(e) => {
|
||||
respond_error(stream, "500 Internal Server Error", &e).await;
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some((pool_key, database))
|
||||
}
|
||||
|
||||
async fn handle_open_table(app: &AppHandle, state: &Arc<AppState>, body: &str, stream: &mut tokio::net::TcpStream) {
|
||||
let req: OpenTableRequest = match serde_json::from_str(body) {
|
||||
Ok(r) => r,
|
||||
@@ -251,6 +333,154 @@ async fn handle_describe_table_data(state: &Arc<AppState>, body: &str, stream: &
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_mongo_list_collections_data(state: &Arc<AppState>, body: &str, stream: &mut tokio::net::TcpStream) {
|
||||
let req: ListTablesRequest = match serde_json::from_str(body) {
|
||||
Ok(r) => r,
|
||||
Err(_) => {
|
||||
respond_error(stream, "400 Bad Request", "Invalid JSON").await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some((pool_key, database)) = resolve_mongo_pool_key(state, &req.connection_name, req.database, stream).await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
match dbx_core::mongo_ops::mongo_list_collections_core(state, &pool_key, &database).await {
|
||||
Ok(collections) => respond_json(stream, &collections).await,
|
||||
Err(e) => respond_error(stream, "500 Internal Server Error", &e).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_mongo_find_documents_data(state: &Arc<AppState>, body: &str, stream: &mut tokio::net::TcpStream) {
|
||||
let req: MongoFindDocumentsRequest = match serde_json::from_str(body) {
|
||||
Ok(r) => r,
|
||||
Err(_) => {
|
||||
respond_error(stream, "400 Bad Request", "Invalid JSON").await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some((pool_key, database)) = resolve_mongo_pool_key(state, &req.connection_name, req.database, stream).await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
match dbx_core::mongo_ops::mongo_find_documents_core(
|
||||
state,
|
||||
&pool_key,
|
||||
&database,
|
||||
&req.collection,
|
||||
req.skip.unwrap_or(0),
|
||||
req.limit.unwrap_or(100),
|
||||
req.filter.as_deref(),
|
||||
req.sort.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => respond_json(stream, &result).await,
|
||||
Err(e) => respond_error(stream, "500 Internal Server Error", &e).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_mongo_aggregate_documents_data(state: &Arc<AppState>, body: &str, stream: &mut tokio::net::TcpStream) {
|
||||
let req: MongoAggregateDocumentsRequest = match serde_json::from_str(body) {
|
||||
Ok(r) => r,
|
||||
Err(_) => {
|
||||
respond_error(stream, "400 Bad Request", "Invalid JSON").await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some((pool_key, database)) = resolve_mongo_pool_key(state, &req.connection_name, req.database, stream).await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
match dbx_core::mongo_ops::mongo_aggregate_documents_core(
|
||||
state,
|
||||
&pool_key,
|
||||
&database,
|
||||
&req.collection,
|
||||
&req.pipeline_json,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => respond_json(stream, &result).await,
|
||||
Err(e) => respond_error(stream, "500 Internal Server Error", &e).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_mongo_insert_documents_data(state: &Arc<AppState>, body: &str, stream: &mut tokio::net::TcpStream) {
|
||||
let req: MongoInsertDocumentsRequest = match serde_json::from_str(body) {
|
||||
Ok(r) => r,
|
||||
Err(_) => {
|
||||
respond_error(stream, "400 Bad Request", "Invalid JSON").await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some((pool_key, database)) = resolve_mongo_pool_key(state, &req.connection_name, req.database, stream).await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
match dbx_core::mongo_ops::mongo_insert_documents_core(state, &pool_key, &database, &req.collection, &req.docs_json)
|
||||
.await
|
||||
{
|
||||
Ok(inserted) => respond_json(stream, &serde_json::json!({ "affected_rows": inserted })).await,
|
||||
Err(e) => respond_error(stream, "500 Internal Server Error", &e).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_mongo_update_documents_data(state: &Arc<AppState>, body: &str, stream: &mut tokio::net::TcpStream) {
|
||||
let req: MongoUpdateDocumentsRequest = match serde_json::from_str(body) {
|
||||
Ok(r) => r,
|
||||
Err(_) => {
|
||||
respond_error(stream, "400 Bad Request", "Invalid JSON").await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some((pool_key, database)) = resolve_mongo_pool_key(state, &req.connection_name, req.database, stream).await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
match dbx_core::mongo_ops::mongo_update_documents_core(
|
||||
state,
|
||||
&pool_key,
|
||||
&database,
|
||||
&req.collection,
|
||||
&req.filter_json,
|
||||
&req.update_json,
|
||||
req.many,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(modified) => respond_json(stream, &serde_json::json!({ "affected_rows": modified })).await,
|
||||
Err(e) => respond_error(stream, "500 Internal Server Error", &e).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_mongo_delete_documents_data(state: &Arc<AppState>, body: &str, stream: &mut tokio::net::TcpStream) {
|
||||
let req: MongoDeleteDocumentsRequest = match serde_json::from_str(body) {
|
||||
Ok(r) => r,
|
||||
Err(_) => {
|
||||
respond_error(stream, "400 Bad Request", "Invalid JSON").await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some((pool_key, database)) = resolve_mongo_pool_key(state, &req.connection_name, req.database, stream).await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
match dbx_core::mongo_ops::mongo_delete_documents_core(
|
||||
state,
|
||||
&pool_key,
|
||||
&database,
|
||||
&req.collection,
|
||||
&req.filter_json,
|
||||
req.many,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(deleted) => respond_json(stream, &serde_json::json!({ "affected_rows": deleted })).await,
|
||||
Err(e) => respond_error(stream, "500 Internal Server Error", &e).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_execute_query_data(state: &Arc<AppState>, body: &str, stream: &mut tokio::net::TcpStream) {
|
||||
let req: ExecuteQueryRequest = match serde_json::from_str(body) {
|
||||
Ok(r) => r,
|
||||
|
||||
@@ -45,6 +45,18 @@ pub async fn mongo_find_documents(
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mongo_aggregate_documents(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
database: String,
|
||||
collection: String,
|
||||
pipeline_json: String,
|
||||
) -> Result<MongoDocumentResult, String> {
|
||||
dbx_core::mongo_ops::mongo_aggregate_documents_core(&state, &connection_id, &database, &collection, &pipeline_json)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mongo_insert_document(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
|
||||
Reference in new issue
Block a user