From 78a61334a5913440ddb217879296383f7064ae3b Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sat, 1 Aug 2026 13:43:54 +0800 Subject: [PATCH] fix(elasticsearch): preserve legacy type on delete --- .../components/document/DocumentBrowser.vue | 17 +++-- apps/desktop/src/lib/backend/http.ts | 3 +- apps/desktop/src/lib/backend/tauri.ts | 3 +- crates/dbx-core/src/db/easysearch_driver.rs | 5 +- .../dbx-core/src/db/elasticsearch_driver.rs | 63 ++++++++++--------- crates/dbx-core/src/document_ops.rs | 16 ++++- crates/dbx-web/src/routes/document_store.rs | 4 +- src-tauri/src/commands/document_cmd.rs | 4 +- src-tauri/src/commands/mongo_cmd.rs | 12 +++- 9 files changed, 85 insertions(+), 42 deletions(-) diff --git a/apps/desktop/src/components/document/DocumentBrowser.vue b/apps/desktop/src/components/document/DocumentBrowser.vue index 3ac500762..4e38d65d0 100644 --- a/apps/desktop/src/components/document/DocumentBrowser.vue +++ b/apps/desktop/src/components/document/DocumentBrowser.vue @@ -594,16 +594,21 @@ function documentRoutingFromDocument(doc: JsonRecord | undefined): string | unde return normalizeDocumentStoreRouting(doc?._routing); } +function documentTypeFromDocument(doc: JsonRecord | undefined): string | undefined { + const documentType = doc?._type; + return typeof documentType === "string" && documentType.trim() ? documentType.trim() : undefined; +} + function documentRoutingFromGridRow(row: MongoInputValue[] | undefined, columns: string[]): string | undefined { const routingColIdx = columns.indexOf("_routing"); return routingColIdx >= 0 ? normalizeDocumentStoreRouting(row?.[routingColIdx]) : undefined; } -function documentStoreWriteApis() { +function documentStoreWriteApis(documentType?: string) { return { insert: (docJson: string, routing?: string) => api.documentInsertDocument(props.connectionId, props.database, props.collection, docJson, routing), update: (id: string, docJson: string, routing?: string) => api.documentUpdateDocument(props.connectionId, props.database, props.collection, id, docJson, routing), - delete: (id: string, routing?: string) => api.documentDeleteDocument(props.connectionId, props.database, props.collection, id, routing), + delete: (id: string, routing?: string) => api.documentDeleteDocument(props.connectionId, props.database, props.collection, id, routing, documentType), }; } @@ -648,8 +653,9 @@ async function gridSave(changes: DocumentGridChanges) { if (id == null) continue; const document = documents.value[rowIdx]; const routing = isEs ? documentRoutingFromDocument(document) : undefined; + const documentType = isEs ? documentTypeFromDocument(document) : undefined; const documentId = isEs ? id : (document?._id ?? id); - await api.documentDeleteDocument(props.connectionId, props.database, props.collection, isEs ? String(documentId) : serializeMongoDocumentId(documentId), routing); + await api.documentDeleteDocument(props.connectionId, props.database, props.collection, isEs ? String(documentId) : serializeMongoDocumentId(documentId), routing, documentType); } for (const newRow of changes.newRows) { @@ -1336,10 +1342,10 @@ async function saveDoc() { const doc = buildDocumentFromEditor(); if (!doc) return; - const apis = documentStoreWriteApis(); const kind = documentStoreProvider.value.kind; if (isNew.value) { + const apis = documentStoreWriteApis(); const explicitId = kind === "elasticsearch" ? documentIdFromGridValue(documentStoreValueForGrid(doc._id, "elasticsearch")) : null; await insertDocumentStoreDocumentCore({ kind, @@ -1362,6 +1368,7 @@ async function saveDoc() { return; } const currentRouting = documentRoutingFromDocument(current); + const apis = documentStoreWriteApis(kind === "elasticsearch" ? documentTypeFromDocument(current) : undefined); const write = resolveWriteIdentityFromEditor(doc, currentId, currentRouting); if (!write) { error.value = t("mongo.jsonIdRequired"); @@ -1400,7 +1407,7 @@ async function applyDeleteDoc(idx: number) { if (!id) return; error.value = ""; try { - await api.documentDeleteDocument(props.connectionId, props.database, props.collection, serializeDocumentStoreId(id, documentStoreProvider.value.kind), documentRoutingFromDocument(doc)); + await api.documentDeleteDocument(props.connectionId, props.database, props.collection, serializeDocumentStoreId(id, documentStoreProvider.value.kind), documentRoutingFromDocument(doc), documentStoreProvider.value.kind === "elasticsearch" ? documentTypeFromDocument(doc) : undefined); if (selectedIdx.value === idx) { selectedIdx.value = null; editJson.value = ""; diff --git a/apps/desktop/src/lib/backend/http.ts b/apps/desktop/src/lib/backend/http.ts index 21638726a..be2a94d9b 100644 --- a/apps/desktop/src/lib/backend/http.ts +++ b/apps/desktop/src/lib/backend/http.ts @@ -3130,13 +3130,14 @@ export async function mongoDeleteDocument(connectionId: string, database: string return documentDeleteDocument(connectionId, database, collection, id, routing); } -export async function documentDeleteDocument(connectionId: string, database: string, collection: string, id: string, routing?: string): Promise { +export async function documentDeleteDocument(connectionId: string, database: string, collection: string, id: string, routing?: string, documentType?: string): Promise { return post("/api/document-store/delete-document", { connectionId, database, collection, id, routing, + documentType, }); } diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts index 0f74de832..fbce989c9 100644 --- a/apps/desktop/src/lib/backend/tauri.ts +++ b/apps/desktop/src/lib/backend/tauri.ts @@ -3072,13 +3072,14 @@ export async function mongoDeleteDocument(connectionId: string, database: string return documentDeleteDocument(connectionId, database, collection, id, routing); } -export async function documentDeleteDocument(connectionId: string, database: string, collection: string, id: string, routing?: string): Promise { +export async function documentDeleteDocument(connectionId: string, database: string, collection: string, id: string, routing?: string, documentType?: string): Promise { return invoke("document_delete_document", { connectionId, database, collection, id, routing, + documentType, }); } diff --git a/crates/dbx-core/src/db/easysearch_driver.rs b/crates/dbx-core/src/db/easysearch_driver.rs index 59236ce97..a8a94a721 100644 --- a/crates/dbx-core/src/db/easysearch_driver.rs +++ b/crates/dbx-core/src/db/easysearch_driver.rs @@ -88,9 +88,12 @@ pub async fn delete_document( client: &EasysearchClient, index: &str, id: &str, + document_type: Option<&str>, routing: Option<&str>, ) -> Result { - elasticsearch_driver::delete_document(&client.inner, index, id, routing).await.map_err(easysearch_error) + elasticsearch_driver::delete_document(&client.inner, index, id, document_type, routing) + .await + .map_err(easysearch_error) } pub async fn execute_rest_query(client: &EasysearchClient, input: &str) -> Result { diff --git a/crates/dbx-core/src/db/elasticsearch_driver.rs b/crates/dbx-core/src/db/elasticsearch_driver.rs index 48693f73f..ef8fd7f42 100644 --- a/crates/dbx-core/src/db/elasticsearch_driver.rs +++ b/crates/dbx-core/src/db/elasticsearch_driver.rs @@ -319,17 +319,7 @@ fn elasticsearch_query_value(value: &str) -> String { utf8_percent_encode(value, ELASTICSEARCH_QUERY_VALUE_ENCODE_SET).to_string() } -fn elasticsearch_document_path(index: &str, id: &str, routing: Option<&str>) -> String { - let base = format!("/{}/_doc/{}", elasticsearch_path_segment(index), elasticsearch_path_segment(id)); - elasticsearch_path_with_routing_refresh(base, routing) -} - -fn elasticsearch_update_document_path( - index: &str, - id: &str, - document_type: Option<&str>, - routing: Option<&str>, -) -> String { +fn elasticsearch_document_path(index: &str, id: &str, document_type: Option<&str>, routing: Option<&str>) -> String { let document_type = document_type.map(str::trim).filter(|value| !value.is_empty()).unwrap_or("_doc"); let base = format!( "/{}/{}/{}", @@ -926,7 +916,7 @@ pub async fn update_document( ) -> Result { let (doc, routing, document_type) = elasticsearch_update_document_body_and_metadata(doc_json, routing)?; - let path = elasticsearch_update_document_path(index, id, document_type.as_deref(), routing.as_deref()); + let path = elasticsearch_document_path(index, id, document_type.as_deref(), routing.as_deref()); let resp = client.put(&path).json(&doc).send().await.map_err(|e| format!("Elasticsearch request failed: {e}"))?; if !client.response_status(&resp).is_success() { @@ -981,8 +971,14 @@ fn elasticsearch_routing_from_value(value: &serde_json::Value) -> Option } } -pub async fn delete_document(client: &EsClient, index: &str, id: &str, routing: Option<&str>) -> Result { - let path = elasticsearch_document_path(index, id, routing); +pub async fn delete_document( + client: &EsClient, + index: &str, + id: &str, + document_type: Option<&str>, + routing: Option<&str>, +) -> Result { + let path = elasticsearch_document_path(index, id, document_type, routing); let resp = client.delete(&path).send().await.map_err(|e| format!("Elasticsearch request failed: {e}"))?; if !client.response_status(&resp).is_success() { @@ -2334,25 +2330,16 @@ mod tests { } #[test] - fn builds_elasticsearch_document_path_with_routing() { + fn builds_elasticsearch_document_path_with_type_and_routing() { assert_eq!( - super::elasticsearch_document_path("orders/2026", "a%b/c", Some("tenant/a&b")), - "/orders%2F2026/_doc/a%25b%2Fc?routing=tenant%2Fa%26b&refresh=true" - ); - assert_eq!(super::elasticsearch_document_path("orders", "1", None), "/orders/_doc/1?refresh=true"); - } - - #[test] - fn builds_legacy_elasticsearch_update_path_from_document_type() { - assert_eq!( - super::elasticsearch_update_document_path("orders/2026", "a%b/c", Some("legacy/order"), Some("tenant/a&b")), + super::elasticsearch_document_path("orders/2026", "a%b/c", Some("legacy/order"), Some("tenant/a&b")), "/orders%2F2026/legacy%2Forder/a%25b%2Fc?routing=tenant%2Fa%26b&refresh=true" ); assert_eq!( - super::elasticsearch_update_document_path("orders", "1", Some("_doc"), None), + super::elasticsearch_document_path("orders", "1", Some("_doc"), None), "/orders/_doc/1?refresh=true" ); - assert_eq!(super::elasticsearch_update_document_path("orders", "1", None, None), "/orders/_doc/1?refresh=true"); + assert_eq!(super::elasticsearch_document_path("orders", "1", None, None), "/orders/_doc/1?refresh=true"); } #[test] @@ -3335,6 +3322,26 @@ mod tests { server.await.unwrap(); } + #[tokio::test] + async fn delete_document_uses_legacy_type_path() { + use tokio::io::AsyncWriteExt; + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let server = tokio::spawn(async move { + let (mut socket, _) = listener.accept().await.unwrap(); + let request = read_http_request(&mut socket).await; + assert!(request.starts_with("DELETE /orders/order/abc?routing=tenant-1&refresh=true ")); + let response = + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}"; + socket.write_all(response.as_bytes()).await.unwrap(); + }); + + let client = EsClient::new(&format!("http://{addr}"), None, None, false, Duration::from_secs(1)); + super::delete_document(&client, "orders", "abc", Some("order"), Some("tenant-1")).await.unwrap(); + server.await.unwrap(); + } + #[tokio::test] async fn execute_rest_search_preserves_full_json_response() { use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -3437,7 +3444,7 @@ mod tests { let name_index = sql_result.columns.iter().position(|column| column == "name").unwrap(); assert_eq!(sql_result.rows[0][name_index], json!("Notebook Pro")); - super::delete_document(&client, &index, &id, None).await.unwrap(); + super::delete_document(&client, &index, &id, None, None).await.unwrap(); let delete = super::execute_rest_query(&client, &format!("DELETE /{index}")).await.unwrap(); assert_eq!(delete.rows[0][0], json!(200)); } diff --git a/crates/dbx-core/src/document_ops.rs b/crates/dbx-core/src/document_ops.rs index 0451ff974..a4b15dba8 100644 --- a/crates/dbx-core/src/document_ops.rs +++ b/crates/dbx-core/src/document_ops.rs @@ -539,6 +539,18 @@ pub async fn delete_document_core( collection: &str, id: &str, routing: Option<&str>, +) -> Result { + delete_document_core_with_type(state, connection_id, database, collection, id, routing, None).await +} + +pub async fn delete_document_core_with_type( + state: &AppState, + connection_id: &str, + database: &str, + collection: &str, + id: &str, + routing: Option<&str>, + document_type: Option<&str>, ) -> Result { ensure_document_pool(state, connection_id).await?; let connections = state.connections.read().await; @@ -549,12 +561,12 @@ pub async fn delete_document_core( drop(connections); // Elasticsearch requires the same custom routing value for writes // as was used to index the document. - elasticsearch_driver::delete_document(&client, collection, id, routing).await + elasticsearch_driver::delete_document(&client, collection, id, document_type, routing).await } PoolKind::Easysearch(client) => { let client = client.clone(); drop(connections); - easysearch_driver::delete_document(&client, collection, id, routing).await + easysearch_driver::delete_document(&client, collection, id, document_type, routing).await } PoolKind::Agent(client) => { let mut client = client.lock().await; diff --git a/crates/dbx-web/src/routes/document_store.rs b/crates/dbx-web/src/routes/document_store.rs index 5108fe45a..71bba4053 100644 --- a/crates/dbx-web/src/routes/document_store.rs +++ b/crates/dbx-web/src/routes/document_store.rs @@ -107,6 +107,7 @@ pub struct DocumentDeleteRequest { pub collection: String, pub id: String, pub routing: Option, + pub document_type: Option, } #[derive(Deserialize)] @@ -256,13 +257,14 @@ pub async fn delete_document( Json(req): Json, ) -> Result, AppError> { ensure_writable(&state.app, &req.connection_id, "Delete").await?; - let result = dbx_core::document_ops::delete_document_core( + let result = dbx_core::document_ops::delete_document_core_with_type( &state.app, &req.connection_id, &req.database, &req.collection, &req.id, req.routing.as_deref(), + req.document_type.as_deref(), ) .await .map_err(AppError::from)?; diff --git a/src-tauri/src/commands/document_cmd.rs b/src-tauri/src/commands/document_cmd.rs index 84fac16c9..f0aa71a6e 100644 --- a/src-tauri/src/commands/document_cmd.rs +++ b/src-tauri/src/commands/document_cmd.rs @@ -147,15 +147,17 @@ pub async fn document_delete_document( collection: String, id: String, routing: Option, + document_type: Option, ) -> Result { ensure_connection_writable(&state, &connection_id, "Delete").await?; - dbx_core::document_ops::delete_document_core( + dbx_core::document_ops::delete_document_core_with_type( &state, &connection_id, &database, &collection, &id, routing.as_deref(), + document_type.as_deref(), ) .await } diff --git a/src-tauri/src/commands/mongo_cmd.rs b/src-tauri/src/commands/mongo_cmd.rs index 61fb3a591..61e28a8f4 100644 --- a/src-tauri/src/commands/mongo_cmd.rs +++ b/src-tauri/src/commands/mongo_cmd.rs @@ -467,8 +467,16 @@ pub async fn mongo_delete_document( id: String, routing: Option, ) -> Result { - crate::commands::document_cmd::document_delete_document(state, connection_id, database, collection, id, routing) - .await + crate::commands::document_cmd::document_delete_document( + state, + connection_id, + database, + collection, + id, + routing, + None, + ) + .await } #[tauri::command]