Skip to main content

mongo_graphql/helpers/
serialization.rs

1use crate::error::GraphQLError;
2use crate::schema::definition::{CollectionDef, FieldType, RelationKind};
3use async_graphql::dynamic::FieldValue;
4use mongodb::bson::Bson;
5
6/// Convert a MongoDB document to a GraphQL-compatible JSON value.
7pub fn document_to_graphql_value(
8    doc: &mongodb::bson::Document,
9    collection_def: &CollectionDef,
10) -> serde_json::Value {
11    let mut map = serde_json::Map::new();
12
13    for field_def in &collection_def.fields {
14        if let FieldType::Relation(rel) = &field_def.field_type {
15            match rel.kind {
16                RelationKind::OneToMany | RelationKind::OneToOne => {
17                    if let Some(bson) = doc.get(&field_def.name) {
18                        map.insert(field_def.name.clone(), bson_to_json(bson));
19                    }
20                }
21                RelationKind::ManyToMany => {}
22            }
23            continue;
24        }
25
26        let gql_name = field_def.graphql_name();
27        match doc.get(&field_def.name) {
28            Some(bson) => {
29                map.insert(gql_name, bson_to_json(bson));
30            }
31            // Omit missing fields rather than inserting null — async-graphql will
32            // enforce the non-null contract and surface a field error if a required
33            // field is absent from the MongoDB document.
34            None => {}
35        }
36    }
37
38    serde_json::Value::Object(map)
39}
40
41/// Convert a BSON value to its JSON equivalent.
42pub fn bson_to_json(bson: &Bson) -> serde_json::Value {
43    match bson {
44        Bson::ObjectId(oid) => serde_json::Value::String(oid.to_hex()),
45        Bson::String(s) => serde_json::Value::String(s.clone()),
46        Bson::Int32(i) => serde_json::Value::Number((*i).into()),
47        Bson::Int64(i) => serde_json::Value::Number((*i).into()),
48        Bson::Double(f) => serde_json::Number::from_f64(*f)
49            .map(serde_json::Value::Number)
50            .unwrap_or(serde_json::Value::Null),
51        Bson::Boolean(b) => serde_json::Value::Bool(*b),
52        Bson::DateTime(dt) => {
53            let millis = dt.timestamp_millis();
54            let secs = millis / 1000;
55            let nanos = ((millis % 1000) * 1_000_000) as u32;
56            match chrono::DateTime::from_timestamp(secs, nanos) {
57                Some(chrono_dt) => serde_json::Value::String(chrono_dt.to_rfc3339()),
58                None => serde_json::Value::Null,
59            }
60        }
61        Bson::Array(arr) => {
62            serde_json::Value::Array(arr.iter().map(bson_to_json).collect())
63        }
64        Bson::Document(subdoc) => {
65            serde_json::Value::Object(
66                subdoc.iter().map(|(key, value)| (key.clone(), bson_to_json(value))).collect(),
67            )
68        }
69        Bson::Null | Bson::Undefined => serde_json::Value::Null,
70        _ => serde_json::Value::Null,
71    }
72}
73
74/// Map GraphQL input field names back to MongoDB field names.
75/// Relation fields with hex string values are converted to ObjectId.
76/// Fields without an explicit `graphql_name` are returned unchanged.
77/// TODO this function look unnecessary, let's review
78pub fn input_doc_to_mongo(doc: mongodb::bson::Document, collection_def: &CollectionDef) -> mongodb::bson::Document {
79    let mut mapped = mongodb::bson::Document::new();
80    for (key, value) in doc {
81        let field_def = collection_def
82            .fields
83            .iter()
84            .find(|field| field.graphql_name() == key);
85        let mongo_name = field_def
86            .map(|field| field.name.clone())
87            .unwrap_or_else(|| key.clone());
88
89        let mapped_value = match field_def {
90            Some(f) if matches!(f.field_type, FieldType::Relation(_)) => {
91                // Relation fields are extracted and processed by the mutation resolver
92                // (process_nested_one_input / process_nested_many_input).
93                // They no longer arrive here as plain hex strings.
94                continue;
95            }
96            _ => value,
97        };
98        mapped.insert(mongo_name, mapped_value);
99    }
100    mapped
101}
102
103/// Convert a `serde_json::Value` into a dynamic `FieldValue` for resolver responses.
104pub fn json_to_field_value(json: serde_json::Value) -> Result<FieldValue<'static>, GraphQLError> {
105    async_graphql::Value::try_from(json)
106        .map(FieldValue::value)
107        .map_err(|e| GraphQLError::Internal(format!("Value conversion: {}", e)))
108}