diff --git a/crates/dbx-core/src/db/mongo_driver.rs b/crates/dbx-core/src/db/mongo_driver.rs index 5f6ecf633f..cf030b60d4 100644 --- a/crates/dbx-core/src/db/mongo_driver.rs +++ b/crates/dbx-core/src/db/mongo_driver.rs @@ -117,7 +117,7 @@ pub async fn find_documents( let filter_doc: Document = match filter { Some(f) if !f.trim().is_empty() => { let json: serde_json::Value = serde_json::from_str(f).map_err(|e| format!("Invalid filter JSON: {e}"))?; - json_object_to_document(&json)? + json_filter_to_document(&json)? } _ => doc! {}, }; @@ -240,7 +240,7 @@ pub async fn update_documents( serde_json::from_str(filter_json).map_err(|e| format!("Invalid filter JSON: {e}"))?; let update_value: serde_json::Value = serde_json::from_str(update_json).map_err(|e| format!("Invalid update JSON: {e}"))?; - let filter = json_object_to_document(&filter_value).map_err(|e| format!("Invalid filter: {e}"))?; + let filter = json_filter_to_document(&filter_value).map_err(|e| format!("Invalid filter: {e}"))?; let update = json_object_to_document(&update_value).map_err(|e| format!("Invalid update: {e}"))?; let col = client.database(database).collection::(collection); let result = if many { @@ -279,7 +279,7 @@ pub async fn delete_documents( ) -> Result { let filter_value: serde_json::Value = serde_json::from_str(filter_json).map_err(|e| format!("Invalid filter JSON: {e}"))?; - let filter = json_object_to_document(&filter_value).map_err(|e| format!("Invalid filter: {e}"))?; + let filter = json_filter_to_document(&filter_value).map_err(|e| format!("Invalid filter: {e}"))?; let col = client.database(database).collection::(collection); let result = if many { col.delete_many(filter).await.map_err(|e| e.to_string())? @@ -320,6 +320,13 @@ pub fn json_object_to_document(value: &serde_json::Value) -> Result Result { + match json_filter_value_to_bson(value, None) { + Bson::Document(doc) => Ok(doc), + other => Err(format!("Expected a JSON object, got {other:?}")), + } +} + fn json_value_to_bson(value: &serde_json::Value) -> Bson { match value { serde_json::Value::Null => Bson::Null, @@ -350,6 +357,94 @@ fn json_value_to_bson(value: &serde_json::Value) -> Bson { } } +fn json_filter_value_to_bson(value: &serde_json::Value, field_name: Option<&str>) -> Bson { + if field_name == Some("_id") { + if let Some(id) = value.as_str() { + return id_equality_bson(id); + } + } + + match value { + serde_json::Value::Array(arr) => { + Bson::Array(arr.iter().map(|item| json_filter_value_to_bson(item, None)).collect()) + } + serde_json::Value::Object(obj) => { + if obj.len() == 1 { + if let Some(serde_json::Value::String(hex)) = obj.get("$oid") { + if let Ok(oid) = ObjectId::parse_str(hex) { + return Bson::ObjectId(oid); + } + } + } + + if field_name == Some("_id") && obj.keys().all(|key| key.starts_with('$')) { + let mut doc = Document::new(); + for (key, item) in obj { + match key.as_str() { + "$eq" => { + if let Some(id) = item.as_str() { + doc.insert("$in", object_id_string_variants(id)); + } else { + doc.insert(key, json_filter_value_to_bson(item, None)); + } + } + "$ne" => { + if let Some(id) = item.as_str() { + doc.insert("$nin", object_id_string_variants(id)); + } else { + doc.insert(key, json_filter_value_to_bson(item, None)); + } + } + "$in" | "$nin" => { + if let Some(items) = item.as_array() { + doc.insert(key, expand_object_id_string_array(items)); + } else { + doc.insert(key, json_filter_value_to_bson(item, None)); + } + } + _ => { + doc.insert(key, json_filter_value_to_bson(item, None)); + } + } + } + return Bson::Document(doc); + } + + let doc: Document = obj.iter().map(|(k, v)| (k.clone(), json_filter_value_to_bson(v, Some(k)))).collect(); + Bson::Document(doc) + } + _ => json_value_to_bson(value), + } +} + +fn id_equality_bson(id: &str) -> Bson { + let variants = object_id_string_variants(id); + if variants.len() == 1 { + variants.into_iter().next().unwrap_or(Bson::String(id.to_string())) + } else { + Bson::Document(doc! { "$in": variants }) + } +} + +fn object_id_string_variants(id: &str) -> Vec { + match ObjectId::parse_str(id) { + Ok(oid) => vec![Bson::ObjectId(oid), Bson::String(id.to_string())], + Err(_) => vec![Bson::String(id.to_string())], + } +} + +fn expand_object_id_string_array(items: &[serde_json::Value]) -> Bson { + let mut values = Vec::new(); + for item in items { + if let Some(id) = item.as_str() { + values.extend(object_id_string_variants(id)); + } else { + values.push(json_filter_value_to_bson(item, None)); + } + } + Bson::Array(values) +} + #[cfg(test)] mod tests { use super::*; @@ -372,4 +467,47 @@ mod tests { assert_eq!(filters.len(), 1); assert!(matches!(filters[0].get("_id"), Some(Bson::String(value)) if value == id)); } + + #[test] + fn json_filter_to_document_matches_object_id_and_string_for_id_hex() { + let id = "507f1f77bcf86cd799439011"; + let filter = serde_json::json!({ "_id": id }); + let doc = json_filter_to_document(&filter).unwrap(); + + let Some(Bson::Document(id_filter)) = doc.get("_id") else { + panic!("expected _id operator document"); + }; + let Some(Bson::Array(values)) = id_filter.get("$in") else { + panic!("expected _id $in variants"); + }; + assert!(matches!(values.first(), Some(Bson::ObjectId(_)))); + assert!(matches!(values.get(1), Some(Bson::String(value)) if value == id)); + } + + #[test] + fn json_filter_to_document_expands_id_operator_variants() { + let id = "507f1f77bcf86cd799439011"; + let filter = serde_json::json!({ "$and": [{ "_id": { "$eq": id } }] }); + let doc = json_filter_to_document(&filter).unwrap(); + + let Some(Bson::Array(and_items)) = doc.get("$and") else { + panic!("expected $and array"); + }; + let Some(Bson::Document(first)) = and_items.first() else { + panic!("expected first $and document"); + }; + let Some(Bson::Document(id_filter)) = first.get("_id") else { + panic!("expected _id operator document"); + }; + assert!(matches!(id_filter.get("$in"), Some(Bson::Array(values)) if values.len() == 2)); + } + + #[test] + fn json_filter_to_document_leaves_non_id_hex_strings_alone() { + let id = "507f1f77bcf86cd799439011"; + let filter = serde_json::json!({ "owner_id": id }); + let doc = json_filter_to_document(&filter).unwrap(); + + assert!(matches!(doc.get("owner_id"), Some(Bson::String(value)) if value == id)); + } }