fix(elasticsearch): preserve legacy type on delete

This commit is contained in:
t8y2 2026-08-01 13:43:54 +08:00
parent 288cc0687a
commit 78a61334a5
No known key found for this signature in database
9 changed files with 85 additions and 42 deletions

View File

@ -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 = "";

View File

@ -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<number> {
export async function documentDeleteDocument(connectionId: string, database: string, collection: string, id: string, routing?: string, documentType?: string): Promise<number> {
return post("/api/document-store/delete-document", {
connectionId,
database,
collection,
id,
routing,
documentType,
});
}

View File

@ -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<number> {
export async function documentDeleteDocument(connectionId: string, database: string, collection: string, id: string, routing?: string, documentType?: string): Promise<number> {
return invoke("document_delete_document", {
connectionId,
database,
collection,
id,
routing,
documentType,
});
}

View File

@ -88,9 +88,12 @@ pub async fn delete_document(
client: &EasysearchClient,
index: &str,
id: &str,
document_type: Option<&str>,
routing: Option<&str>,
) -> Result<u64, String> {
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<QueryResult, String> {

View File

@ -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<u64, String> {
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<String>
}
}
pub async fn delete_document(client: &EsClient, index: &str, id: &str, routing: Option<&str>) -> Result<u64, String> {
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<u64, String> {
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));
}

View File

@ -539,6 +539,18 @@ pub async fn delete_document_core(
collection: &str,
id: &str,
routing: Option<&str>,
) -> Result<u64, String> {
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<u64, String> {
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;

View File

@ -107,6 +107,7 @@ pub struct DocumentDeleteRequest {
pub collection: String,
pub id: String,
pub routing: Option<String>,
pub document_type: Option<String>,
}
#[derive(Deserialize)]
@ -256,13 +257,14 @@ pub async fn delete_document(
Json(req): Json<DocumentDeleteRequest>,
) -> Result<Json<u64>, 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)?;

View File

@ -147,15 +147,17 @@ pub async fn document_delete_document(
collection: String,
id: String,
routing: Option<String>,
document_type: Option<String>,
) -> Result<u64, String> {
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
}

View File

@ -467,8 +467,16 @@ pub async fn mongo_delete_document(
id: String,
routing: Option<String>,
) -> Result<u64, String> {
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]