feat(mongodb): improve document quick filters

This commit is contained in:
co1dsand authored and GitHub committed 2026-07-17 18:51:33 +08:00
1 parent c417e54698
commit 681da22c33
32 files changed
+1639 -116

No files matched your search

+233 -47
View File
@@ -678,7 +678,7 @@ pub async fn count_documents(
}
}
/// Find MongoDB documents as relaxed Extended JSON for MongoDB transfer paths.
/// Find MongoDB documents in a browser-friendly representation.
#[allow(clippy::too_many_arguments)]
pub async fn find_documents_extended_json(
client: &Client,
@@ -728,7 +728,7 @@ pub async fn find_documents_extended_json(
let mut documents = Vec::new();
while cursor.advance().await.map_err(|e| e.to_string())? {
let doc = cursor.deserialize_current().map_err(|e| e.to_string())?;
documents.push(Bson::Document(doc).into_relaxed_extjson());
documents.push(bson_to_browser_json(&Bson::Document(doc)));
}
Ok(MongoDocumentResult { extended_documents: Some(documents.clone()), documents, raw_documents: None, total })
@@ -1077,7 +1077,7 @@ pub async fn update_document(
) -> Result<u64, String> {
let value: serde_json::Value = serde_json::from_str(doc_json).map_err(|e| format!("Invalid JSON: {e}"))?;
let col = client.database(database).collection::<Document>(collection);
let update_doc = json_object_to_document(&value).map_err(|e| format!("Invalid document: {e}"))?;
let update_doc = json_object_to_document_for_update(&value, None).map_err(|e| format!("Invalid document: {e}"))?;
if is_update_operator_document(&update_doc) {
for filter in document_id_filters(id) {
let result = col.update_one(filter, update_doc.clone()).await.map_err(|e| e.to_string())?;
@@ -1090,7 +1090,7 @@ pub async fn update_document(
for filter in document_id_filters(id) {
let current = col.find_one(filter.clone()).await.map_err(|e| e.to_string())?;
let mut new_doc = json_object_to_document_preserving_existing(&value, current.as_ref())
let mut new_doc = json_object_to_document_for_update(&value, current.as_ref())
.map_err(|e| format!("Invalid document: {e}"))?;
new_doc.remove("_id");
let result = col.replace_one(filter, new_doc.clone()).await.map_err(|e| e.to_string())?;
@@ -1468,6 +1468,20 @@ fn bson_document_field_to_json(key: &str, bson: &Bson) -> serde_json::Value {
bson_to_json(bson)
}
fn bson_to_browser_json(bson: &Bson) -> serde_json::Value {
match bson {
Bson::Int64(value) if !(-9_007_199_254_740_991..=9_007_199_254_740_991).contains(value) => {
serde_json::json!({ "$numberLong": value.to_string() })
}
Bson::ObjectId(oid) => serde_json::json!({ "$oid": oid.to_hex() }),
Bson::Array(values) => serde_json::Value::Array(values.iter().map(bson_to_browser_json).collect()),
Bson::Document(document) => serde_json::Value::Object(
document.iter().map(|(key, value)| (key.clone(), bson_to_browser_json(value))).collect(),
),
_ => bson_to_json(bson),
}
}
/// Convert a `serde_json::Value` (JSON object) to a BSON `Document`,
/// handling MongoDB extended JSON conventions such as `{"$oid":"..."}`.
pub fn json_object_to_document(value: &serde_json::Value) -> Result<Document, String> {
@@ -1504,21 +1518,27 @@ fn json_object_to_document_preserving_existing(
value: &serde_json::Value,
existing: Option<&Document>,
) -> Result<Document, String> {
match (value, existing) {
(serde_json::Value::Object(obj), Some(existing)) => Ok(obj
match value {
serde_json::Value::Object(obj) => obj
.iter()
.map(|(key, value)| {
let bson = existing
.get(key)
.map(|existing_bson| json_value_to_bson_preserving_existing(value, existing_bson))
.unwrap_or_else(|| json_value_to_bson(value));
(key.clone(), bson)
json_value_to_bson_preserving_existing(value, existing.and_then(|document| document.get(key)))
.map(|bson| (key.clone(), bson))
})
.collect()),
.collect(),
_ => json_object_to_document(value),
}
}
/// Parse document-editor input value by value so mixed browser representations
/// retain their BSON types.
fn json_object_to_document_for_update(
value: &serde_json::Value,
existing: Option<&Document>,
) -> Result<Document, String> {
json_object_to_document_preserving_existing(value, existing)
}
pub fn json_filter_to_document(value: &serde_json::Value) -> Result<Document, String> {
match json_filter_value_to_bson(value, None) {
Bson::Document(doc) => Ok(doc),
@@ -1526,36 +1546,49 @@ pub fn json_filter_to_document(value: &serde_json::Value) -> Result<Document, St
}
}
fn json_value_to_bson_preserving_existing(value: &serde_json::Value, existing: &Bson) -> Bson {
if &bson_to_json(existing) == value {
return existing.clone();
fn json_value_to_bson_preserving_existing(value: &serde_json::Value, existing: Option<&Bson>) -> Result<Bson, String> {
if let Some(existing) = existing {
if &bson_to_json(existing) == value {
return Ok(existing.clone());
}
}
match (value, existing) {
(serde_json::Value::Array(values), Bson::Array(existing_values)) => Bson::Array(
match value {
serde_json::Value::Array(values) => {
let existing_values = match existing {
Some(Bson::Array(values)) => Some(values),
_ => None,
};
values
.iter()
.enumerate()
.map(|(index, item)| {
existing_values
.get(index)
.map(|existing_item| json_value_to_bson_preserving_existing(item, existing_item))
.unwrap_or_else(|| json_value_to_bson(item))
json_value_to_bson_preserving_existing(item, existing_values.and_then(|values| values.get(index)))
})
.collect(),
),
(serde_json::Value::Object(obj), Bson::Document(existing_doc)) => Bson::Document(
.collect::<Result<Vec<_>, _>>()
.map(Bson::Array)
}
serde_json::Value::Object(obj) => {
if let Some(value) = parse_extended_json_value(obj)? {
return Ok(value);
}
let existing_document = match existing {
Some(Bson::Document(document)) => Some(document),
_ => None,
};
obj.iter()
.map(|(key, item)| {
let bson = existing_doc
.get(key)
.map(|existing_item| json_value_to_bson_preserving_existing(item, existing_item))
.unwrap_or_else(|| json_value_to_bson(item));
(key.clone(), bson)
json_value_to_bson_preserving_existing(
item,
existing_document.and_then(|document| document.get(key)),
)
.map(|bson| (key.clone(), bson))
})
.collect(),
),
_ => json_value_to_bson(value),
.collect::<Result<Document, _>>()
.map(Bson::Document)
}
_ => Ok(json_value_to_bson(value)),
}
}
@@ -1577,19 +1610,8 @@ fn json_value_to_bson(value: &serde_json::Value) -> Bson {
}
serde_json::Value::Array(arr) => Bson::Array(arr.iter().map(json_value_to_bson).collect()),
serde_json::Value::Object(obj) => {
// Extended JSON: {"$oid":"..."} → BSON ObjectId
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 let Some(value) = parse_extended_json_int64(obj) {
return Bson::Int64(value);
}
if let Some(date) = parse_extended_json_date(obj) {
return Bson::DateTime(date);
}
if let Ok(Some(value)) = parse_extended_json_value(obj) {
return value;
}
let doc: Document = obj.iter().map(|(k, v)| (k.clone(), json_value_to_bson(v))).collect();
Bson::Document(doc)
@@ -1597,6 +1619,40 @@ fn json_value_to_bson(value: &serde_json::Value) -> Bson {
}
}
fn parse_extended_json_value(obj: &serde_json::Map<String, serde_json::Value>) -> Result<Option<Bson>, String> {
let is_wrapper = match obj.len() {
1 => obj.keys().next().is_some_and(|key| {
matches!(
key.as_str(),
"$oid"
| "$date"
| "$numberInt"
| "$numberLong"
| "$numberDouble"
| "$numberDecimal"
| "$binary"
| "$regularExpression"
| "$timestamp"
| "$minKey"
| "$maxKey"
| "$undefined"
| "$symbol"
| "$code"
| "$dbPointer"
| "$uuid"
)
}),
2 => obj.contains_key("$code") && obj.contains_key("$scope"),
_ => false,
};
if !is_wrapper {
return Ok(None);
}
Bson::try_from(serde_json::Value::Object(obj.clone())).map(Some).map_err(|error| error.to_string())
}
fn parse_mongo_shell_date(value: &str) -> Option<DateTime> {
let trimmed = value.trim();
if let Some(inner) = trimmed.strip_prefix("ISODate(").or_else(|| trimmed.strip_prefix("new Date(")) {
@@ -2150,7 +2206,124 @@ mod tests {
}
#[test]
fn bson_to_extended_json_preserves_nested_object_ids() {
fn browser_json_keeps_normal_scalars_readable_and_unsafe_int64_typed() {
let date = DateTime::parse_rfc3339_str("2026-06-10T13:59:31.287Z").unwrap();
let value = bson_to_browser_json(&Bson::Document(doc! {
"int32": Bson::Int32(42), "double": Bson::Double(3.5), "date": Bson::DateTime(date),
"unsafe": Bson::Int64(2_326_645_729_978_441_729),
}));
assert_eq!(value["int32"], serde_json::json!(42));
assert_eq!(value["double"], serde_json::json!(3.5));
assert_eq!(value["date"], serde_json::json!("ISODate(\"2026-06-10T13:59:31.287Z\")"));
assert_eq!(value["unsafe"], serde_json::json!({ "$numberLong": "2326645729978441729" }));
}
#[test]
fn browser_json_round_trips_normal_scalars_and_unsafe_int64_on_edit() {
let date = DateTime::parse_rfc3339_str("2026-06-10T13:59:31.287Z").unwrap();
let existing = doc! {
"name": "before",
"int32": Bson::Int32(42),
"double": Bson::Double(3.5),
"date": Bson::DateTime(date),
"unsafe": Bson::Int64(2_326_645_729_978_441_729),
};
let mut edited = bson_to_browser_json(&Bson::Document(existing.clone()));
edited["name"] = serde_json::json!("after");
let round_tripped = json_object_to_document_for_update(&edited, Some(&existing)).unwrap();
assert_eq!(round_tripped.get_str("name").unwrap(), "after");
assert!(matches!(round_tripped.get("int32"), Some(Bson::Int32(42))));
assert!(matches!(round_tripped.get("double"), Some(Bson::Double(value)) if *value == 3.5));
assert!(matches!(round_tripped.get("date"), Some(Bson::DateTime(value)) if *value == date));
assert!(matches!(round_tripped.get("unsafe"), Some(Bson::Int64(2_326_645_729_978_441_729))));
}
#[test]
fn browser_json_round_trips_mixed_bson_types_when_editing_an_unrelated_field() {
let object_id = ObjectId::parse_str("507f1f77bcf86cd799439011").unwrap();
let date = DateTime::parse_rfc3339_str("2026-06-10T13:59:31.287Z").unwrap();
let existing = doc! {
"_id": Bson::ObjectId(object_id),
"name": "before",
"date": Bson::DateTime(date),
"unsafe": Bson::Int64(2_326_645_729_978_441_729),
};
let mut edited = bson_to_browser_json(&Bson::Document(existing.clone()));
assert_eq!(edited["_id"], serde_json::json!({ "$oid": "507f1f77bcf86cd799439011" }));
assert_eq!(edited["date"], serde_json::json!("ISODate(\"2026-06-10T13:59:31.287Z\")"));
assert_eq!(edited["unsafe"], serde_json::json!({ "$numberLong": "2326645729978441729" }));
edited["name"] = serde_json::json!("after");
let round_tripped = json_object_to_document_for_update(&edited, None).unwrap();
assert_eq!(round_tripped.get_str("name").unwrap(), "after");
assert!(matches!(
round_tripped.get("_id"),
Some(Bson::ObjectId(value)) if *value == object_id
));
assert!(matches!(round_tripped.get("date"), Some(Bson::DateTime(value)) if *value == date));
assert!(matches!(round_tripped.get("unsafe"), Some(Bson::Int64(2_326_645_729_978_441_729))));
}
#[test]
fn document_update_rejects_invalid_extended_json_wrapper() {
let edited = serde_json::json!({
"unsafe": { "$numberLong": "not-an-integer" },
});
assert!(json_object_to_document_for_update(&edited, None).is_err());
}
#[test]
fn document_update_parses_wrapper_when_replacing_an_existing_document_value() {
let object_id = ObjectId::parse_str("507f1f77bcf86cd799439011").unwrap();
let existing = doc! {
"value": { "kind": "plain" },
};
let edited = serde_json::json!({
"value": { "$oid": "507f1f77bcf86cd799439011" },
});
let updated = json_object_to_document_for_update(&edited, Some(&existing)).unwrap();
assert!(matches!(updated.get("value"), Some(Bson::ObjectId(value)) if *value == object_id));
}
#[test]
fn document_update_decodes_uuid_wrapper_per_value() {
let uuid = serde_json::json!({ "$uuid": "00112233-4455-6677-8899-aabbccddeeff" });
let expected = Bson::try_from(uuid.clone()).unwrap();
let edited = serde_json::json!({
"name": "after",
"uuid": uuid,
});
let updated = json_object_to_document_for_update(&edited, None).unwrap();
assert_eq!(updated.get("uuid"), Some(&expected));
}
#[test]
fn browser_json_filters_decode_normal_scalars_and_unsafe_int64() {
let filter = serde_json::json!({
"int32": 42,
"double": 3.5,
"date": "ISODate(\"2026-06-10T13:59:31.287Z\")",
"unsafe": { "$numberLong": "2326645729978441729" },
});
let document = json_filter_to_document(&filter).unwrap();
assert!(matches!(document.get("int32"), Some(Bson::Int64(42))));
assert!(matches!(document.get("double"), Some(Bson::Double(value)) if *value == 3.5));
assert!(
matches!(document.get("date"), Some(Bson::DateTime(value)) if value.timestamp_millis() == 1_781_099_971_287)
);
assert!(matches!(document.get("unsafe"), Some(Bson::Int64(2_326_645_729_978_441_729))));
}
#[test]
fn canonical_extended_json_preserves_nested_object_ids() {
let oid = ObjectId::parse_str("507f1f77bcf86cd799439011").unwrap();
let nested_oid = ObjectId::parse_str("507f191e810c19729de860ea").unwrap();
let value = Bson::Document(doc! {
@@ -2158,7 +2331,7 @@ mod tests {
"owner": { "id": Bson::ObjectId(nested_oid) },
"tags": [Bson::ObjectId(nested_oid)],
})
.into_relaxed_extjson();
.into_canonical_extjson();
assert_eq!(
value,
@@ -2170,6 +2343,19 @@ mod tests {
);
}
#[test]
fn canonical_extended_json_round_trips_unsafe_int64() {
let original = doc! {
"counter": Bson::Int64(2_326_645_729_978_441_729),
};
let extended_json = Bson::Document(original.clone()).into_canonical_extjson();
assert_eq!(extended_json, serde_json::json!({ "counter": { "$numberLong": "2326645729978441729" } }));
let round_tripped = json_object_to_document_extended_json(&extended_json).unwrap();
assert_eq!(round_tripped, original);
}
#[test]
fn index_info_from_model_maps_mongodb_index_metadata() {
let model = IndexModel::builder()
+18 -2
View File
@@ -367,7 +367,12 @@ pub async fn find_documents_core(
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::MongoDb(client) => {
mongo_driver::find_documents(client, database, collection, skip, limit, filter, projection, sort).await
// Document browser responses must retain BSON type metadata so nested filters
// can round-trip ObjectId, Date, and int64 values through Extended JSON.
mongo_driver::find_documents_extended_json(
client, database, collection, skip, limit, filter, projection, sort,
)
.await
}
PoolKind::Elasticsearch(client) => {
let client = client.clone();
@@ -393,12 +398,23 @@ pub async fn find_documents_core(
if let Some(projection) = projection {
params["projection"] = serde_json::json!(projection);
}
client.mongo_find_documents(params).await
match client.mongo_find_documents_extended_json(params.clone()).await {
Ok(result) => Ok(result),
Err(error) if is_unknown_agent_method_error(&error, "find_documents_extended_json") => {
client.mongo_find_documents(params).await
}
Err(error) => Err(error),
}
}
_ => Err("Not a MongoDB/Elasticsearch/vector connection".to_string()),
}
}
fn is_unknown_agent_method_error(error: &str, method: &str) -> bool {
let lower = error.to_ascii_lowercase();
lower.contains(method) && (lower.contains("unknown method") || lower.contains("method not found"))
}
pub async fn insert_document_core(
state: &AppState,
connection_id: &str,