Skip to main content

parse_rust_mongo/
adapter.rs

1//! The MongoDB `StorageAdapter`.
2
3use bson::{doc, Bson, Document};
4use futures::TryStreamExt;
5use mongodb::options::IndexOptions;
6use mongodb::{Client, Database, IndexModel};
7use parse_rust_core::{ErrorCode, ParseError, ParseValue};
8use parse_rust_schema::storage_format::{
9    field_type_to_storage, storage_to_field_type, NON_FIELD_KEYS,
10};
11use parse_rust_storage::{
12    ClassSchema, Comparison, Constraint, QueryOptions, Row, SortDirection, StorageAdapter,
13    WriteResult,
14};
15
16use crate::transform::{
17    mongo_object_to_parse, parse_object_to_mongo_create, value_to_bson_for_query,
18};
19
20/// Where class schemas live. Not configurable: parse-server hardcodes it, and a mixed fleet has
21/// to agree.
22const SCHEMA_COLLECTION: &str = "_SCHEMA";
23
24pub struct MongoAdapter {
25    db: Database,
26}
27
28impl MongoAdapter {
29    pub async fn connect(uri: &str, database: &str) -> Result<Self, ParseError> {
30        let client = Client::with_uri_str(uri).await.map_err(mongo_err)?;
31        Ok(Self {
32            db: client.database(database),
33        })
34    }
35
36    /// Translate a constraint list into a Mongo filter.
37    ///
38    /// Every branch is total over [`Comparison`], so adding a comparison fails to compile here
39    /// rather than silently matching everything.
40    fn build_filter(
41        schema: &ClassSchema,
42        constraints: &[Constraint],
43    ) -> Result<Document, ParseError> {
44        let mut filter = Document::new();
45        for c in constraints {
46            let key = crate::transform::storage_key(schema, &c.field);
47            let value = |v: &ParseValue| value_to_bson_for_query(schema, &c.field, v);
48
49            let entry: Bson = match &c.comparison {
50                Comparison::Equal(v) => value(v)?,
51                Comparison::NotEqual(v) => Bson::Document(doc! { "$ne": value(v)? }),
52                Comparison::GreaterThan(v) => Bson::Document(doc! { "$gt": value(v)? }),
53                Comparison::GreaterThanOrEqual(v) => Bson::Document(doc! { "$gte": value(v)? }),
54                Comparison::LessThan(v) => Bson::Document(doc! { "$lt": value(v)? }),
55                Comparison::LessThanOrEqual(v) => Bson::Document(doc! { "$lte": value(v)? }),
56                Comparison::In(items) => Bson::Document(doc! {
57                    "$in": items.iter().map(value).collect::<Result<Vec<_>, _>>()?
58                }),
59                Comparison::NotIn(items) => Bson::Document(doc! {
60                    "$nin": items.iter().map(value).collect::<Result<Vec<_>, _>>()?
61                }),
62                Comparison::Exists(b) => Bson::Document(doc! { "$exists": *b }),
63            };
64
65            // Several constraints on one field must merge rather than overwrite. Overwriting is
66            // the bug the `tbraun96/parse-rs` query builder shipped, and it silently drops a
67            // constraint, which broadens the result set.
68            merge_constraint(&mut filter, key, entry)?;
69        }
70        Ok(filter)
71    }
72}
73
74/// Merge a new constraint into an existing filter entry for the same field.
75fn merge_constraint(filter: &mut Document, key: String, entry: Bson) -> Result<(), ParseError> {
76    match filter.remove(&key) {
77        None => {
78            filter.insert(key, entry);
79        }
80        Some(existing) => match (existing, entry) {
81            // Two operator documents merge key-wise: `{$gt: 1}` plus `{$lt: 5}` is a range.
82            (Bson::Document(mut a), Bson::Document(b)) => {
83                for (k, v) in b {
84                    a.insert(k, v);
85                }
86                filter.insert(key, Bson::Document(a));
87            }
88            // Anything involving a bare equality cannot merge: Mongo has no way to express
89            // "equals 1 and equals 2", and silently keeping one would drop the other.
90            _ => {
91                return Err(ParseError::invalid_query(format!(
92                    "conflicting constraints on field {key}"
93                )))
94            }
95        },
96    }
97    Ok(())
98}
99
100fn mongo_err(e: mongodb::error::Error) -> ParseError {
101    // Duplicate key is the one storage error with a specific Parse code, because signup depends
102    // on it: `username_1` colliding must become 202, not a generic failure.
103    if let mongodb::error::ErrorKind::Write(mongodb::error::WriteFailure::WriteError(we)) =
104        e.kind.as_ref()
105    {
106        if we.code == 11000 {
107            return ParseError::new(ErrorCode::DuplicateValue, we.message.clone());
108        }
109    }
110    ParseError::new(
111        ErrorCode::InternalServerError,
112        format!("storage error: {e}"),
113    )
114}
115
116impl StorageAdapter for MongoAdapter {
117    async fn all_schemas(&self) -> Result<Vec<ClassSchema>, ParseError> {
118        let mut cursor = self
119            .db
120            .collection::<Document>(SCHEMA_COLLECTION)
121            .find(doc! {})
122            .await
123            .map_err(mongo_err)?;
124
125        let mut out = Vec::new();
126        while let Some(doc) = cursor.try_next().await.map_err(mongo_err)? {
127            let Some(class_name) = doc.get_str("_id").ok() else {
128                continue;
129            };
130            let mut schema = parse_rust_schema::default_schema(class_name);
131            for (key, value) in &doc {
132                if NON_FIELD_KEYS.contains(&key.as_str()) {
133                    continue;
134                }
135                let Bson::String(type_str) = value else {
136                    continue;
137                };
138                // An unrecognised type string is skipped rather than erroring, which is what
139                // upstream's missing default case amounts to. A mixed fleet can contain one.
140                if let Some(ty) = storage_to_field_type(type_str) {
141                    schema.fields.insert(key.clone(), ty);
142                }
143            }
144            out.push(schema);
145        }
146        Ok(out)
147    }
148
149    async fn upsert_schema(&self, schema: &ClassSchema) -> Result<(), ParseError> {
150        let mut set = Document::new();
151        for (name, ty) in &schema.fields {
152            let s = field_type_to_storage(ty);
153            // ACL renders empty and is never stored; writing it would be a phantom column.
154            if s.is_empty() {
155                continue;
156            }
157            set.insert(name.clone(), s);
158        }
159        self.db
160            .collection::<Document>(SCHEMA_COLLECTION)
161            .update_one(doc! { "_id": &schema.class_name }, doc! { "$set": set })
162            .upsert(true)
163            .await
164            .map_err(mongo_err)?;
165        Ok(())
166    }
167
168    async fn create(&self, schema: &ClassSchema, row: &Row) -> Result<WriteResult, ParseError> {
169        let doc = parse_object_to_mongo_create(schema, row)?;
170        let object_id = doc
171            .get_str("_id")
172            .map_err(|_| ParseError::new(ErrorCode::MissingObjectId, "objectId is required"))?
173            .to_string();
174        self.db
175            .collection::<Document>(&schema.class_name)
176            .insert_one(doc)
177            .await
178            .map_err(mongo_err)?;
179        Ok(WriteResult { object_id })
180    }
181
182    async fn find(
183        &self,
184        schema: &ClassSchema,
185        constraints: &[Constraint],
186        options: &QueryOptions,
187    ) -> Result<Vec<Row>, ParseError> {
188        let filter = Self::build_filter(schema, constraints)?;
189        // Bind the collection first: the driver's fluent builder borrows it, so building
190        // directly off a temporary would drop it while still in use.
191        let collection = self.db.collection::<Document>(&schema.class_name);
192        let mut find = collection.find(filter);
193
194        if let Some(limit) = options.limit {
195            // The driver reads 0 as "no limit", which is the opposite of what a caller asking for
196            // zero rows means. Short-circuit instead.
197            if limit == 0 {
198                return Ok(Vec::new());
199            }
200            find = find.limit(limit as i64);
201        }
202        if let Some(skip) = options.skip {
203            find = find.skip(skip as u64);
204        }
205        if !options.order.is_empty() {
206            let mut sort = Document::new();
207            for (key, dir) in &options.order {
208                let dir = match dir {
209                    SortDirection::Ascending => 1,
210                    SortDirection::Descending => -1,
211                };
212                sort.insert(crate::transform::storage_key(schema, key), dir);
213            }
214            find = find.sort(sort);
215        }
216        if let Some(keys) = &options.keys {
217            let mut projection = Document::new();
218            for k in keys {
219                projection.insert(crate::transform::storage_key(schema, k), 1);
220            }
221            // Always projected regardless of `keys`:
222            //  - the permission columns, because ACL filtering happens after the read and
223            //    projecting them away would make every row look public;
224            //  - the timestamps, which Parse returns on every object whether asked for or not.
225            for always in ["_rperm", "_wperm", "_created_at", "_updated_at"] {
226                projection.insert(always, 1);
227            }
228            find = find.projection(projection);
229        }
230
231        let mut cursor = find.await.map_err(mongo_err)?;
232        let mut out = Vec::new();
233        while let Some(doc) = cursor.try_next().await.map_err(mongo_err)? {
234            out.push(mongo_object_to_parse(&doc)?);
235        }
236        Ok(out)
237    }
238
239    async fn count(
240        &self,
241        schema: &ClassSchema,
242        constraints: &[Constraint],
243    ) -> Result<u64, ParseError> {
244        let filter = Self::build_filter(schema, constraints)?;
245        self.db
246            .collection::<Document>(&schema.class_name)
247            .count_documents(filter)
248            .await
249            .map_err(mongo_err)
250    }
251
252    async fn update(
253        &self,
254        schema: &ClassSchema,
255        constraints: &[Constraint],
256        values: &Row,
257    ) -> Result<u64, ParseError> {
258        let filter = Self::build_filter(schema, constraints)?;
259        let set = parse_object_to_mongo_create(schema, values)?;
260        if set.is_empty() {
261            return Ok(0);
262        }
263        let res = self
264            .db
265            .collection::<Document>(&schema.class_name)
266            .update_many(filter, doc! { "$set": set })
267            .await
268            .map_err(mongo_err)?;
269        Ok(res.matched_count)
270    }
271
272    async fn delete(
273        &self,
274        schema: &ClassSchema,
275        constraints: &[Constraint],
276    ) -> Result<u64, ParseError> {
277        let filter = Self::build_filter(schema, constraints)?;
278        let res = self
279            .db
280            .collection::<Document>(&schema.class_name)
281            .delete_many(filter)
282            .await
283            .map_err(mongo_err)?;
284        Ok(res.deleted_count)
285    }
286
287    async fn ensure_unique_index(
288        &self,
289        class_name: &str,
290        fields: &[&str],
291        name: Option<&str>,
292    ) -> Result<(), ParseError> {
293        let mut keys = Document::new();
294        for f in fields {
295            keys.insert(f.to_string(), 1);
296        }
297        // Sparse and background, matching `ensureIndex`'s defaults
298        // (`MongoStorageAdapter.js:797`). A non-sparse unique index would refuse a second row
299        // with the field absent, which is not upstream's behavior.
300        let mut opts = IndexOptions::builder().unique(true).sparse(true).build();
301        opts.name = name.map(str::to_string);
302
303        self.db
304            .collection::<Document>(class_name)
305            .create_index(IndexModel::builder().keys(keys).options(opts).build())
306            .await
307            .map_err(mongo_err)?;
308        Ok(())
309    }
310}