parse_rust_mongo/adapter.rs
1//! The MongoDB `StorageAdapter`.
2
3use bson::{doc, Bson, Document};
4use futures::TryStreamExt;
5use mongodb::options::{IndexOptions, ReturnDocument};
6use mongodb::{Client, Database, IndexModel};
7use parse_rust_core::{
8 ClassLevelPermissions, ErrorCode, ParseError, ParseMap, ParseValue, DUPLICATE_VALUE_MESSAGE,
9};
10use parse_rust_schema::storage_format::{
11 field_type_to_storage, storage_to_field_type, NON_FIELD_KEYS,
12};
13use parse_rust_storage::{
14 join_table_name, AddFieldOutcome, ClassSchema, FieldType, Query, QueryOptions, Row,
15 SchemaIndex, SortDirection, StorageAdapter, Update, WriteResult,
16};
17
18use crate::transform::{
19 bson_document_to_parse_map, index_key_to_bson, mongo_object_to_parse,
20 parse_map_to_bson_document, parse_object_to_mongo_create, storage_key, transform_update,
21 transform_where,
22};
23
24/// Where class schemas live. Not configurable: parse-server hardcodes it, and a mixed fleet has
25/// to agree.
26const SCHEMA_COLLECTION: &str = "_SCHEMA";
27
28/// The three `_metadata` sub-keys parse-server writes.
29///
30/// **This list is closed.** A fourth key invented by parse-rust would travel into a database
31/// parse-server also reads, and `_metadata` is the one place a stray key is read back rather than
32/// rejected.
33const METADATA_CLASS_PERMISSIONS: &str = "_metadata.class_permissions";
34const METADATA_INDEXES: &str = "_metadata.indexes";
35const METADATA_FIELDS_OPTIONS: &str = "_metadata.fields_options";
36
37/// `emptyCLPS`, the merge base used when `class_permissions` **is** present
38/// (`MongoSchemaCollection.js:67-76`).
39///
40/// Note that this is not `defaultCLPS`. The two differ in more than their values: `defaultCLPS`
41/// carries an `ACL` key that this one does not, so a class whose CLP block is absent and a class
42/// whose CLP block sets only `find` do not read back as the same document.
43const EMPTY_CLPS_KEYS: [&str; 8] = [
44 "find",
45 "count",
46 "get",
47 "create",
48 "update",
49 "delete",
50 "addField",
51 "protectedFields",
52];
53
54pub struct MongoAdapter {
55 db: Database,
56}
57
58impl MongoAdapter {
59 pub async fn connect(uri: &str, database: &str) -> Result<Self, ParseError> {
60 let client = Client::with_uri_str(uri).await.map_err(mongo_err)?;
61 Ok(Self {
62 db: client.database(database),
63 })
64 }
65
66 /// Read the `_SCHEMA` document for one class.
67 async fn schema_document(&self, class_name: &str) -> Result<Option<Document>, ParseError> {
68 self.db
69 .collection::<Document>(SCHEMA_COLLECTION)
70 .find_one(doc! { "_id": class_name })
71 .await
72 .map_err(mongo_err)
73 }
74
75 /// Why a reservation matched nothing.
76 ///
77 /// Two conditions in the filter can refuse it and they need different answers, so the write
78 /// stays one operation and only the *explanation* costs a read. The read cannot change the
79 /// decision: it already happened.
80 ///
81 /// One place, deliberately: the driver reports "the upsert matched nothing" and "the upsert
82 /// collided on `_id`" differently, and both mean the same thing here, that somebody else
83 /// reserved the field first. Classifying in two places is how the two stop agreeing.
84 async fn reservation_refused(
85 &self,
86 class_name: &str,
87 field_name: &str,
88 requested: &FieldType,
89 ) -> Result<AddFieldOutcome, ParseError> {
90 if matches!(requested, FieldType::GeoPoint) {
91 let already = self
92 .schema_document(class_name)
93 .await?
94 .map(|d| {
95 d.iter()
96 .any(|(key, value)| key != field_name && value.as_str() == Some("geopoint"))
97 })
98 .unwrap_or(false);
99 if already {
100 // The adapter's own message, not `SchemaController`'s. Upstream raises this one
101 // from the Mongo schema collection (`MongoSchemaCollection.js:234`), and a client
102 // adding a second GeoPoint by an ordinary write sees it rather than the
103 // `validateObject` string. Measured against parse-server 9.10.1-alpha.6.
104 return Err(ParseError::incorrect_type(
105 "MongoDB only supports one GeoPoint field in a class.".to_string(),
106 ));
107 }
108 }
109 self.classify_existing_field(class_name, field_name, requested)
110 .await
111 }
112
113 /// Decide the outcome of a field reservation by reading back what is actually stored.
114 async fn classify_existing_field(
115 &self,
116 class_name: &str,
117 field_name: &str,
118 requested: &FieldType,
119 ) -> Result<AddFieldOutcome, ParseError> {
120 let stored = self
121 .schema_document(class_name)
122 .await?
123 .and_then(|d| d.get_str(field_name).ok().map(str::to_string));
124
125 match stored.as_deref().and_then(storage_to_field_type) {
126 Some(existing) if &existing == requested => Ok(AddFieldOutcome::AlreadyPresentSameType),
127 Some(existing) => Ok(AddFieldOutcome::Conflict { existing }),
128 // Two cases fall here and neither can produce a `FieldType` to report:
129 //
130 // - the stored type string is one parse-server itself cannot read, because
131 // `mongoFieldToParseSchemaField` is a `switch` with no default case
132 // (`MongoSchemaCollection.js:4-39`) and the field's parsed entry becomes
133 // `undefined`;
134 // - the field is absent on re-read, meaning it was deleted between the failed upsert
135 // and this read.
136 //
137 // Upstream reaches the same place by a different route: `enforceFieldExists` swallows
138 // the failure, reloads, and `ensureFields` then finds no usable expected type and
139 // throws `INVALID_JSON` `Could not add field <name>` (`SchemaController.js:1205-1217`).
140 // Reporting that error is closer to upstream than inventing a `FieldType` to put in a
141 // `Conflict`, and it is loud rather than silent, which is what a caller needs when the
142 // reservation did not stick.
143 None => Err(ParseError::invalid_json(format!(
144 "Could not add field {field_name}"
145 ))),
146 }
147 }
148}
149
150/// Is this the duplicate-key error, whatever shape the driver wrapped it in?
151fn is_duplicate_key(e: &mongodb::error::Error) -> bool {
152 match e.kind.as_ref() {
153 mongodb::error::ErrorKind::Write(mongodb::error::WriteFailure::WriteError(we)) => {
154 we.code == 11000
155 }
156 mongodb::error::ErrorKind::Command(ce) => ce.code == 11000,
157 _ => false,
158 }
159}
160
161/// Dropping a collection that is not there is not a failure.
162///
163/// The modern driver returns success, but upstream still guards for the old `ns not found`
164/// (`MongoStorageAdapter.js:473-479`) and a class with no rows yet has no collection at all.
165fn is_namespace_not_found(e: &mongodb::error::Error) -> bool {
166 if let mongodb::error::ErrorKind::Command(ce) = e.kind.as_ref() {
167 return ce.code == 26;
168 }
169 false
170}
171
172/// Is this an infrastructure failure rather than a query-level one?
173///
174/// `isTransientError` (`MongoStorageAdapter.js:35-57`) names four driver error classes plus the
175/// `TransientTransactionError` label, and `handleError` turns them into a `Parse.Error` whose
176/// message is the fixed `Database error` (`MongoStorageAdapter.js:291-293`). The Rust driver
177/// splits the same territory differently, so this matches on the closest kinds rather than on
178/// upstream's error-name strings, which do not exist here.
179fn is_transient(e: &mongodb::error::Error) -> bool {
180 if e.contains_label(mongodb::error::TRANSIENT_TRANSACTION_ERROR) {
181 return true;
182 }
183 matches!(
184 e.kind.as_ref(),
185 mongodb::error::ErrorKind::ServerSelection { .. }
186 | mongodb::error::ErrorKind::ConnectionPoolCleared { .. }
187 | mongodb::error::ErrorKind::Io(_)
188 )
189}
190
191/// The server's own `errmsg`, which is where the index name lives.
192///
193/// Never put this on the wire. It reads
194/// `E11000 duplicate key error collection: <db>.<collection> index: <index> dup key: { <field>: <value> }`,
195/// so it names the database and the value that collided.
196fn driver_message(e: &mongodb::error::Error) -> Option<&str> {
197 match e.kind.as_ref() {
198 mongodb::error::ErrorKind::Write(mongodb::error::WriteFailure::WriteError(we)) => {
199 Some(we.message.as_str())
200 }
201 mongodb::error::ErrorKind::Command(ce) => Some(ce.message.as_str()),
202 _ => None,
203 }
204}
205
206/// The `_metadata.fields_options.<field>` path one field's options are addressed by.
207///
208/// A field name cannot contain a dot (`fieldNameIsValid`), so this cannot accidentally address
209/// something nested.
210fn field_options_path(field_name: &str) -> String {
211 format!("{METADATA_FIELDS_OPTIONS}.{field_name}")
212}
213
214/// The field columns a class schema writes: field name to `_SCHEMA` type string.
215fn schema_fields_document(schema: &ClassSchema) -> Document {
216 let mut out = Document::new();
217 for (name, ty) in &schema.fields {
218 let s = field_type_to_storage(ty);
219 // ACL renders empty and is never stored; writing it would be a phantom column.
220 if s.is_empty() {
221 continue;
222 }
223 out.insert(name.clone(), s);
224 }
225 out
226}
227
228/// The `_metadata` sub-document, **unprefixed**, and empty when the schema carries no metadata.
229///
230/// Kept separate from the field columns because the two writes need it in two different shapes and
231/// the shapes are not interchangeable. `upsert_schema` needs dotted paths, so that `$set`ting one
232/// metadata key leaves the others alone; `insert_schema` needs a real nested document, because a
233/// key containing a dot inside an inserted document is stored **literally**, as a top-level column
234/// named `_metadata.class_permissions`, which no reader on either server will ever look at.
235///
236/// Assembling one document for both is what this did for an hour, and the class it created had a
237/// CLP no reader could see, which reads exactly like a class with no CLP: default-open.
238fn schema_metadata_document(schema: &ClassSchema) -> Result<Document, ParseError> {
239 let mut out = Document::new();
240 if let Some(clp) = &schema.clp {
241 out.insert("class_permissions", parse_map_to_bson_document(clp.raw())?);
242 }
243 if let Some(indexes) = &schema.indexes {
244 out.insert("indexes", parse_map_to_bson_document(indexes)?);
245 }
246 if let Some(field_options) = &schema.field_options {
247 out.insert("fields_options", parse_map_to_bson_document(field_options)?);
248 }
249 Ok(out)
250}
251
252/// The index name out of a duplicate-key `errmsg`: the token after `index:`.
253fn index_name(message: &str) -> Option<&str> {
254 let (_, rest) = message.split_once(" index: ")?;
255 rest.split_whitespace().next()
256}
257
258/// Which field collided, or `None` when the index name does not say.
259///
260/// **Deliberately narrow.** Upstream reads only two index-name shapes: the auto-generated
261/// `<field>_1` (`MongoStorageAdapter.js:582`, which also accepts the legacy `<db>.$<field>_1`
262/// spelling) and the authData form `_auth_data_<provider>_id`
263/// (`MongoStorageAdapter.js:588`), tried only when the first found nothing. A custom index name
264/// yields no `duplicated_field` at all, which is why index names are contract: a differently
265/// named unique index on `username` changes the error a client sees. Parsing any index name here
266/// would hand `RestWrite`'s consumers a field upstream never gives them, and the two servers would
267/// then disagree about which error a client sees.
268///
269/// One difference from upstream's regex, and it is upstream's bug rather than a choice here.
270/// `/index:[\sa-zA-Z0-9_\-\.]+\$?([a-zA-Z_-]+)_1/` backtracks: against a modern MongoDB message
271/// (`index: username_1`) the greedy leading class eats all but the last letter, so the capture is
272/// `e`, not `username`. It captures correctly only against the legacy `index: <db>.$username_1`
273/// spelling, where `$` is outside the class and stops the greed. Upstream therefore reaches 202
274/// and 203 through the fallback queries at `RestWrite.js:1718-1755` rather than through
275/// `duplicated_field`, and the wire result is the same either way. This returns the whole field
276/// name, which is what the fast path was written to produce.
277fn duplicated_field(message: &str) -> Option<String> {
278 let name = index_name(message)?;
279 if let Some(prefix) = name.strip_suffix("_1") {
280 // The legacy spelling qualifies the index with the namespace: `<db>.$<field>_1`.
281 let field = prefix.rsplit(['$', '.']).next().unwrap_or(prefix);
282 // Upstream's capture class is `[a-zA-Z_-]`, so a field name containing a digit is not
283 // recoverable there and is not recoverable here either.
284 let recoverable = !field.is_empty()
285 && field
286 .chars()
287 .all(|c| c.is_ascii_alphabetic() || c == '_' || c == '-');
288 if recoverable {
289 return Some(field.to_string());
290 }
291 }
292 if name.starts_with("_auth_data_") && name.ends_with("_id") {
293 return Some(name.to_string());
294 }
295 None
296}
297
298fn mongo_err(e: mongodb::error::Error) -> ParseError {
299 // Duplicate key is the one storage error with a specific Parse code, because signup depends
300 // on it: `username_1` colliding must become 202, not a generic failure. The message is fixed
301 // and which field collided travels out of band, because the driver's text names the database
302 // and the colliding value (`MongoStorageAdapter.js:574-597`).
303 if is_duplicate_key(&e) {
304 let err = ParseError::new(ErrorCode::DuplicateValue, DUPLICATE_VALUE_MESSAGE);
305 return match driver_message(&e).and_then(duplicated_field) {
306 Some(field) => err.with_duplicated_field(field),
307 None => err,
308 };
309 }
310 if is_transient(&e) {
311 return ParseError::new(ErrorCode::InternalServerError, "Database error");
312 }
313 // Everything else is upstream's bare rethrow: a driver error that is not a `Parse.Error`, so
314 // the client gets the generic 500 body and this text reaches the log only.
315 ParseError::internal(format!("storage error: {e}"))
316}
317
318/// `{ ...emptyCLPS, ...stored }` (`MongoSchemaCollection.js:100`).
319///
320/// The merge is what makes an unspecified operation read back as `{}`, deny-all, rather than as
321/// absent, which is unrestricted. Handing the merged block to `ClassLevelPermissions::from_map`
322/// means `raw()` is the document that would be written back, so a round trip through parse-rust
323/// produces the block parse-server produces rather than the narrower one that was stored.
324fn merge_over_empty_clps(stored: &Document) -> Result<ParseMap, ParseError> {
325 let mut out = ParseMap::new();
326 for key in EMPTY_CLPS_KEYS {
327 out.insert(key.to_string(), ParseValue::Object(ParseMap::new()));
328 }
329 // `IndexMap::insert` keeps an existing key's position, which is what the JavaScript spread
330 // does, so the merged block has `emptyCLPS` order followed by any keys only the stored block
331 // carries.
332 for (key, value) in stored {
333 out.insert(key.clone(), crate::transform::bson_to_parse_value(value)?);
334 }
335 Ok(out)
336}
337
338impl StorageAdapter for MongoAdapter {
339 async fn all_schemas(&self) -> Result<Vec<ClassSchema>, ParseError> {
340 let mut cursor = self
341 .db
342 .collection::<Document>(SCHEMA_COLLECTION)
343 .find(doc! {})
344 .await
345 .map_err(mongo_err)?;
346
347 let mut out = Vec::new();
348 while let Some(doc) = cursor.try_next().await.map_err(mongo_err)? {
349 let Some(class_name) = doc.get_str("_id").ok() else {
350 continue;
351 };
352 let mut schema = parse_rust_schema::default_schema(class_name);
353 for (key, value) in &doc {
354 if NON_FIELD_KEYS.contains(&key.as_str()) {
355 continue;
356 }
357 let Bson::String(type_str) = value else {
358 continue;
359 };
360 // An unrecognised type string is skipped rather than erroring, which is what
361 // upstream's missing default case amounts to. A mixed fleet can contain one.
362 if let Some(ty) = storage_to_field_type(type_str) {
363 schema.fields.insert(key.clone(), ty);
364 }
365 }
366
367 if let Ok(metadata) = doc.get_document("_metadata") {
368 // **Only when the key is present.** Absent stays `None`, and `None` is not
369 // "public": upstream reads an absent block back as `defaultCLPS`, a fully open
370 // document including an `ACL` key that the present-but-partial case never carries
371 // (`MongoSchemaCollection.js:95-101`). Materializing that default into the struct
372 // would make an absent block indistinguishable from an explicitly-public one, and
373 // the next write-back would then store a block parse-server never had, turning an
374 // unset CLP into a set one for every node reading the same database.
375 if let Ok(class_permissions) = metadata.get_document("class_permissions") {
376 schema.clp = Some(ClassLevelPermissions::from_map(merge_over_empty_clps(
377 class_permissions,
378 )?));
379 }
380 // Round-tripped verbatim and never interpreted.
381 if let Ok(indexes) = metadata.get_document("indexes") {
382 schema.indexes = Some(bson_document_to_parse_map(indexes)?);
383 }
384 if let Ok(fields_options) = metadata.get_document("fields_options") {
385 schema.field_options = Some(bson_document_to_parse_map(fields_options)?);
386 }
387 }
388
389 out.push(schema);
390 }
391 Ok(out)
392 }
393
394 async fn insert_schema(&self, schema: &ClassSchema) -> Result<(), ParseError> {
395 let mut document = schema_fields_document(schema);
396 document.insert("_id", &schema.class_name);
397 // Nested, and omitted entirely when there is nothing in it, matching
398 // `mongoSchemaFromFieldsAndClassNameAndCLP`'s `delete mongoObject._metadata`
399 // (`MongoStorageAdapter.js:135-138`). An empty `_metadata` is a document parse-server
400 // never writes.
401 let metadata = schema_metadata_document(schema)?;
402 if !metadata.is_empty() {
403 document.insert("_metadata", metadata);
404 }
405 match self
406 .db
407 .collection::<Document>(SCHEMA_COLLECTION)
408 .insert_one(document)
409 .await
410 {
411 Ok(_) => Ok(()),
412 // `insertSchema`'s own catch, message included (`MongoSchemaCollection.js:188-190`).
413 // The caller re-labels it; this layer reports what the database said.
414 Err(e) if is_duplicate_key(&e) => Err(ParseError::new(
415 ErrorCode::DuplicateValue,
416 "Class already exists.",
417 )),
418 Err(e) => Err(mongo_err(e)),
419 }
420 }
421
422 async fn upsert_schema(&self, schema: &ClassSchema) -> Result<(), ParseError> {
423 let mut set = schema_fields_document(schema);
424 // Dotted paths, so that writing one metadata key leaves the other two alone. An ordinary
425 // field-adding save arrives with `clp: None` simply because nothing loaded one, and
426 // replacing `_metadata` wholesale from that would delete the class's permissions.
427 for (key, value) in schema_metadata_document(schema)? {
428 let path = match key.as_str() {
429 "class_permissions" => METADATA_CLASS_PERMISSIONS,
430 "indexes" => METADATA_INDEXES,
431 _ => METADATA_FIELDS_OPTIONS,
432 };
433 set.insert(path, value);
434 }
435
436 if set.is_empty() {
437 // Mongo rejects an empty update document, and `$set`ting `_id` to work around that
438 // would try to modify an immutable field. A class with nothing storable still has to
439 // have a row, so insert the bare one and treat an existing row as success.
440 let mut bare = Document::new();
441 bare.insert("_id", &schema.class_name);
442 return match self
443 .db
444 .collection::<Document>(SCHEMA_COLLECTION)
445 .insert_one(bare)
446 .await
447 {
448 Ok(_) => Ok(()),
449 Err(e) if is_duplicate_key(&e) => Ok(()),
450 Err(e) => Err(mongo_err(e)),
451 };
452 }
453
454 self.db
455 .collection::<Document>(SCHEMA_COLLECTION)
456 .update_one(doc! { "_id": &schema.class_name }, doc! { "$set": set })
457 .upsert(true)
458 .await
459 .map_err(mongo_err)?;
460 Ok(())
461 }
462
463 async fn reserve_field(
464 &self,
465 class_name: &str,
466 field_name: &str,
467 field_type: &FieldType,
468 options: Option<&ParseMap>,
469 ) -> Result<AddFieldOutcome, ParseError> {
470 let type_string = field_type_to_storage(field_type);
471 if type_string.is_empty() {
472 // `ACL` has no `_SCHEMA` string; it is injected on read. Reserving it would write an
473 // empty type string, which parse-server reads as a phantom field of unknown type.
474 return Err(ParseError::invalid_json(format!(
475 "Could not add field {field_name}"
476 )));
477 }
478
479 // The conditional upsert (`MongoSchemaCollection.js:249-281`, reaching
480 // `upsertSchema` at `:201-203`). The `$exists: false` guard is the whole mechanism: a
481 // writer that loses the race fails the condition rather than overwriting the winner's
482 // type, so the type a row is validated against cannot change under it.
483 let mut filter = Document::new();
484 filter.insert("_id", class_name);
485 filter.insert(field_name, doc! { "$exists": false });
486
487 let mut set = Document::new();
488 set.insert(field_name, &type_string);
489 // In the same `$set`, under the same guard (`MongoSchemaCollection.js:251-269`). A field
490 // reserved without its options would be a field whose options a concurrent writer can win.
491 if let Some(options) = options.filter(|o| !o.is_empty()) {
492 set.insert(
493 field_options_path(field_name),
494 parse_map_to_bson_document(options)?,
495 );
496 }
497
498 // **A class may hold only one GeoPoint field** (`MongoSchemaCollection.js:224-237`).
499 // Upstream reads the schema, checks it for an existing GeoPoint, then upserts, which is
500 // the read-decide-write split that two concurrent writers adding two *different* GeoPoint
501 // fields both survive. This was previously left to `parse-rust-schema` on the reasoning
502 // that it is a schema rule rather than a storage one; that is the wrong axis, because it
503 // is a predicate over stored state paired with a mutation of that state.
504 //
505 // It cannot go in the upsert filter. An upsert builds its insert document from the
506 // filter's equality terms, and `$expr` is not one, so Mongo refuses the whole operation and
507 // an ordinary first GeoPoint 500s. So the guarded form runs without `upsert`, and the class
508 // row is created by an explicit insert instead.
509 //
510 // **The insert is what closes the window, not a prior read.** Checking "does the class
511 // exist?" and then upserting is the same read-decide-write split this guard exists to
512 // remove: two writers can both observe absence, the first inserts GeoPoint `a`, and the
513 // second's plain `{b: {$exists: false}}` filter then adds `b` happily. `insert_one` is
514 // atomic on `_id`, so exactly one writer creates the row and the loser is told so by a
515 // duplicate-key error, at which point the guarded update is the right thing to retry.
516 //
517 // `$objectToArray` is what makes the condition expressible without a prior read. `_id` is
518 // a string and `_metadata` a subdocument, so neither can equal the type string.
519 if matches!(field_type, FieldType::GeoPoint) {
520 let mut guarded = filter.clone();
521 guarded.insert(
522 "$expr",
523 doc! { "$eq": [ { "$size": { "$filter": {
524 "input": { "$objectToArray": "$$ROOT" },
525 "cond": { "$eq": ["$$this.v", &type_string] },
526 } } }, 0 ] },
527 );
528 let guarded_update = || async {
529 self.db
530 .collection::<Document>(SCHEMA_COLLECTION)
531 .update_one(guarded.clone(), doc! { "$set": set.clone() })
532 .await
533 .map_err(mongo_err)
534 };
535
536 if guarded_update().await?.modified_count > 0 {
537 return Ok(AddFieldOutcome::Added);
538 }
539
540 // Matched nothing, so either the class row is absent or the guard refused. Try to
541 // create it. **Nested, not dotted**: a dotted key is a path inside `$set` and a
542 // literal key inside an inserted document, and writing `_metadata.fields_options.x`
543 // here would create a top-level column whose name contains dots.
544 let mut new_row = Document::new();
545 new_row.insert("_id", class_name);
546 new_row.insert(field_name, &type_string);
547 if let Some(options) = options.filter(|o| !o.is_empty()) {
548 new_row.insert(
549 "_metadata",
550 doc! { "fields_options": doc! {
551 field_name: parse_map_to_bson_document(options)?,
552 } },
553 );
554 }
555 return match self
556 .db
557 .collection::<Document>(SCHEMA_COLLECTION)
558 .insert_one(new_row)
559 .await
560 {
561 Ok(_) => Ok(AddFieldOutcome::Added),
562 // Somebody else created the class between the update and the insert. The row now
563 // exists, so the guarded update is meaningful again.
564 Err(e) if is_duplicate_key(&e) => {
565 if guarded_update().await?.modified_count > 0 {
566 return Ok(AddFieldOutcome::Added);
567 }
568 self.reservation_refused(class_name, field_name, field_type)
569 .await
570 }
571 Err(e) => Err(mongo_err(e)),
572 };
573 }
574
575 let result = self
576 .db
577 .collection::<Document>(SCHEMA_COLLECTION)
578 .update_one(filter, doc! { "$set": set })
579 .upsert(true)
580 .await;
581
582 match result {
583 Ok(r) if r.upserted_id.is_some() || r.modified_count > 0 => Ok(AddFieldOutcome::Added),
584 // Matched nothing and did not error. The class document exists and the field does not,
585 // yet nothing changed, which a concurrent write can produce. Same classification as a
586 // collision.
587 Ok(_) => {
588 self.reservation_refused(class_name, field_name, field_type)
589 .await
590 }
591 // The filter matched nothing because the field already exists, so the upsert tried to
592 // insert a second document with the same `_id` and collided.
593 Err(e) if is_duplicate_key(&e) => {
594 self.reservation_refused(class_name, field_name, field_type)
595 .await
596 }
597 Err(e) => Err(mongo_err(e)),
598 }
599 }
600
601 async fn set_field_options(
602 &self,
603 class_name: &str,
604 field_name: &str,
605 options: &ParseMap,
606 ) -> Result<(), ParseError> {
607 // One path, one field, **under a `{field: {$exists: true}}` guard**
608 // (`MongoSchemaCollection.js:284-297`). Without it a field deleted between the caller's
609 // read and this write leaves options behind for a column that no longer exists, which
610 // parse-server reads back as a `fields_options` entry with no field.
611 //
612 // No upsert, unlike upstream. Upstream passes `upsert: true`, and because `$exists` is not
613 // an equality Mongo cannot derive the field from the filter, so the insert it attempts
614 // collides on `_id` and surfaces a raw duplicate-key error for what is really a lost race.
615 // Matching nothing and writing nothing is the same outcome without an error this layer
616 // would then have to sanitize. Recorded under the deliberate differences in `CHANGELOG.md`.
617 let mut filter = Document::new();
618 filter.insert("_id", class_name);
619 filter.insert(field_name, doc! { "$exists": true });
620 self.db
621 .collection::<Document>(SCHEMA_COLLECTION)
622 .update_one(
623 filter,
624 doc! { "$set": { field_options_path(field_name): parse_map_to_bson_document(options)? } },
625 )
626 .await
627 .map_err(mongo_err)?;
628 Ok(())
629 }
630
631 async fn set_indexes(&self, class_name: &str, indexes: &ParseMap) -> Result<(), ParseError> {
632 // No upsert, matching `updateSchema` (`MongoStorageAdapter.js:404-408`). On a class being
633 // created this matches nothing and the indexes travel in the insert instead.
634 self.db
635 .collection::<Document>(SCHEMA_COLLECTION)
636 .update_one(
637 doc! { "_id": class_name },
638 doc! { "$set": { METADATA_INDEXES: parse_map_to_bson_document(indexes)? } },
639 )
640 .await
641 .map_err(mongo_err)?;
642 Ok(())
643 }
644
645 async fn set_class_permissions(
646 &self,
647 class_name: &str,
648 clp: Option<&ClassLevelPermissions>,
649 ) -> Result<(), ParseError> {
650 // `$set` on the one path when there is a block, `$unset` when there is not
651 // (`MongoStorageAdapter.js:337-345` does the `$set` half). Removing the key is not the
652 // same as storing an empty block: the key's absence is what makes a class read back as
653 // `defaultCLPS`.
654 let update = match clp {
655 Some(clp) => {
656 doc! { "$set": { METADATA_CLASS_PERMISSIONS: parse_map_to_bson_document(clp.raw())? } }
657 }
658 None => doc! { "$unset": { METADATA_CLASS_PERMISSIONS: "" } },
659 };
660 // No upsert, matching `updateSchema`: setting permissions on a class that does not exist
661 // must not conjure one.
662 self.db
663 .collection::<Document>(SCHEMA_COLLECTION)
664 .update_one(doc! { "_id": class_name }, update)
665 .await
666 .map_err(mongo_err)?;
667 Ok(())
668 }
669
670 async fn delete_class(&self, schema: &ClassSchema) -> Result<(), ParseError> {
671 // Collected before the first await so the iterator does not borrow across it.
672 let joins: Vec<String> = schema
673 .relation_fields()
674 .map(|(field, _)| join_table_name(&schema.class_name, field))
675 .collect();
676
677 if let Err(e) = self
678 .db
679 .collection::<Document>(&schema.class_name)
680 .drop()
681 .await
682 {
683 if !is_namespace_not_found(&e) {
684 return Err(mongo_err(e));
685 }
686 }
687
688 self.db
689 .collection::<Document>(SCHEMA_COLLECTION)
690 .delete_one(doc! { "_id": &schema.class_name })
691 .await
692 .map_err(mongo_err)?;
693
694 // Every join collection belonging to the class goes with it
695 // (`DatabaseController.js:1631-1638`). Note that the join collections have no `_SCHEMA`
696 // row, so there is nothing else to remove for them.
697 for join in joins {
698 if let Err(e) = self.db.collection::<Document>(&join).drop().await {
699 if !is_namespace_not_found(&e) {
700 return Err(mongo_err(e));
701 }
702 }
703 }
704 Ok(())
705 }
706
707 async fn delete_fields(
708 &self,
709 schema: &ClassSchema,
710 fields: &[String],
711 ) -> Result<(), ParseError> {
712 let mut column_unset = Document::new();
713 let mut existence: Vec<Bson> = Vec::new();
714 let mut schema_unset = Document::new();
715
716 for name in fields {
717 schema_unset.insert(name.clone(), Bson::Null);
718 schema_unset.insert(format!("{METADATA_FIELDS_OPTIONS}.{name}"), Bson::Null);
719
720 // A Relation has no column, so there is nothing to unset on the rows. Upstream issues
721 // the unset anyway and says so in a comment (`MongoStorageAdapter.js:495-499`); the
722 // difference is unobservable, because a stray column of that name is overwritten by
723 // the synthesized Relation on every read.
724 let column = match schema.field(name) {
725 Some(FieldType::Relation { .. }) => continue,
726 Some(FieldType::Pointer { .. }) => format!("_p_{name}"),
727 _ => name.clone(),
728 };
729 existence.push(Bson::Document(doc! { &column: { "$exists": true } }));
730 column_unset.insert(column, Bson::Null);
731 }
732
733 if !column_unset.is_empty() {
734 self.db
735 .collection::<Document>(&schema.class_name)
736 .update_many(doc! { "$or": existence }, doc! { "$unset": column_unset })
737 .await
738 .map_err(mongo_err)?;
739 }
740
741 if !schema_unset.is_empty() {
742 // **Deliberately does not touch join collections** (`MongoStorageAdapter.js:495-501`).
743 // Dropping a Relation field leaves its memberships in place, and a class recreated
744 // with the same field name inherits them. A client can observe that.
745 self.db
746 .collection::<Document>(SCHEMA_COLLECTION)
747 .update_one(
748 doc! { "_id": &schema.class_name },
749 doc! { "$unset": schema_unset },
750 )
751 .await
752 .map_err(mongo_err)?;
753 }
754 Ok(())
755 }
756
757 async fn create(&self, schema: &ClassSchema, row: &Row) -> Result<WriteResult, ParseError> {
758 let doc = parse_object_to_mongo_create(schema, row)?;
759 let object_id = doc
760 .get_str("_id")
761 .map_err(|_| ParseError::new(ErrorCode::MissingObjectId, "objectId is required"))?
762 .to_string();
763 self.db
764 .collection::<Document>(&schema.class_name)
765 .insert_one(doc)
766 .await
767 .map_err(mongo_err)?;
768 Ok(WriteResult { object_id })
769 }
770
771 async fn upsert_one(
772 &self,
773 schema: &ClassSchema,
774 query: &Query,
775 row: &Row,
776 ) -> Result<(), ParseError> {
777 // Upstream hands the same document to `transformUpdate`, which lifts plain values onto
778 // `$set` (`DatabaseController.js:794-806` calling `upsertOneObject`). A join membership is
779 // exactly `{relatedId, owningId}`, so adding a user to a role twice is one row.
780 //
781 // Nothing here writes to `_SCHEMA`. Join collections have no schema document upstream, and
782 // creating one would add a class every parse-server node reading the database would see.
783 let filter = transform_where(schema, query)?;
784 let set = parse_object_to_mongo_create(schema, row)?;
785 if set.is_empty() {
786 return Err(ParseError::invalid_json(
787 "upsert requires at least one value".to_string(),
788 ));
789 }
790 self.db
791 .collection::<Document>(&schema.class_name)
792 .update_one(filter, doc! { "$set": set })
793 .upsert(true)
794 .await
795 .map_err(mongo_err)?;
796 Ok(())
797 }
798
799 async fn find(
800 &self,
801 schema: &ClassSchema,
802 query: &Query,
803 options: &QueryOptions,
804 ) -> Result<Vec<Row>, ParseError> {
805 let filter = transform_where(schema, query)?;
806 // Bind the collection first: the driver's fluent builder borrows it, so building
807 // directly off a temporary would drop it while still in use.
808 let collection = self.db.collection::<Document>(&schema.class_name);
809 let mut find = collection.find(filter);
810
811 if options.case_insensitive {
812 // `{caseInsensitive: true}` resolves to this collation
813 // (`MongoStorageAdapter.js:801-803`, `MongoCollection.js:134-136`). Applied to the
814 // find rather than approximated in the filter, because strength 2 normalizes as well
815 // as folding case, which no regex over the stored value can reproduce.
816 find = find.collation(
817 mongodb::options::Collation::builder()
818 .locale("en_US".to_string())
819 .strength(mongodb::options::CollationStrength::Secondary)
820 .build(),
821 );
822 }
823
824 if let Some(limit) = options.limit {
825 // The driver reads 0 as "no limit", which is the opposite of what a caller asking for
826 // zero rows means. Short-circuit instead.
827 if limit == 0 {
828 return Ok(Vec::new());
829 }
830 find = find.limit(limit as i64);
831 }
832 if let Some(skip) = options.skip {
833 find = find.skip(skip as u64);
834 }
835 if !options.order.is_empty() {
836 let mut sort = Document::new();
837 for (key, dir) in &options.order {
838 let dir = match dir {
839 SortDirection::Ascending => 1,
840 SortDirection::Descending => -1,
841 };
842 sort.insert(storage_key(schema, key), dir);
843 }
844 find = find.sort(sort);
845 }
846 if let Some(keys) = &options.keys {
847 let mut projection = Document::new();
848 for k in keys {
849 projection.insert(storage_key(schema, k), 1);
850 }
851 // Always projected regardless of `keys`:
852 // - the permission columns, because ACL filtering happens after the read and
853 // projecting them away would make every row look public;
854 // - the timestamps, which Parse returns on every object whether asked for or not.
855 for always in ["_rperm", "_wperm", "_created_at", "_updated_at"] {
856 projection.insert(always, 1);
857 }
858 find = find.projection(projection);
859 }
860
861 let mut cursor = find.await.map_err(mongo_err)?;
862 let mut out = Vec::new();
863 while let Some(doc) = cursor.try_next().await.map_err(mongo_err)? {
864 out.push(mongo_object_to_parse(schema, &doc)?);
865 }
866 Ok(out)
867 }
868
869 async fn count(&self, schema: &ClassSchema, query: &Query) -> Result<u64, ParseError> {
870 let filter = transform_where(schema, query)?;
871 self.db
872 .collection::<Document>(&schema.class_name)
873 .count_documents(filter)
874 .await
875 .map_err(mongo_err)
876 }
877
878 async fn update(
879 &self,
880 schema: &ClassSchema,
881 query: &Query,
882 update: &Update,
883 ) -> Result<u64, ParseError> {
884 let filter = transform_where(schema, query)?;
885 let compiled = transform_update(schema, update)?;
886 // An update carrying nothing storable, a lone Relation for instance, compiles to an empty
887 // document, and Mongo rejects that. Nothing matched because nothing was asked for.
888 if compiled.is_empty() {
889 return Ok(0);
890 }
891 let res = self
892 .db
893 .collection::<Document>(&schema.class_name)
894 .update_many(filter, compiled)
895 .await
896 .map_err(mongo_err)?;
897 Ok(res.matched_count)
898 }
899
900 async fn update_one_returning(
901 &self,
902 schema: &ClassSchema,
903 query: &Query,
904 update: &Update,
905 ) -> Result<Option<Row>, ParseError> {
906 let filter = transform_where(schema, query)?;
907 let compiled = transform_update(schema, update)?;
908 if compiled.is_empty() {
909 return Ok(None);
910 }
911 // `returnDocument: 'after'` (`MongoStorageAdapter.js:660-665`). The post-image is what
912 // `_sanitizeDatabaseResult` reads an op's resulting value off, so the *before* image would
913 // report the old value and look like the op silently did nothing.
914 let collection = self.db.collection::<Document>(&schema.class_name);
915 let found = collection
916 .find_one_and_update(filter, compiled)
917 .return_document(ReturnDocument::After)
918 .await
919 .map_err(mongo_err)?;
920 found
921 .as_ref()
922 .map(|doc| mongo_object_to_parse(schema, doc))
923 .transpose()
924 }
925
926 async fn delete(&self, schema: &ClassSchema, query: &Query) -> Result<u64, ParseError> {
927 let filter = transform_where(schema, query)?;
928 let res = self
929 .db
930 .collection::<Document>(&schema.class_name)
931 .delete_many(filter)
932 .await
933 .map_err(mongo_err)?;
934 Ok(res.deleted_count)
935 }
936
937 async fn ensure_index(
938 &self,
939 class_name: &str,
940 fields: &[&str],
941 name: Option<&str>,
942 unique: bool,
943 case_insensitive: bool,
944 ) -> Result<(), ParseError> {
945 let mut keys = Document::new();
946 for f in fields {
947 keys.insert(f.to_string(), 1);
948 }
949 // Sparse and background, matching `ensureIndex`'s defaults
950 // (`MongoStorageAdapter.js:797`). A non-sparse unique index would refuse a second row
951 // with the field absent, which is not upstream's behavior.
952 let mut opts = IndexOptions::builder().unique(unique).sparse(true).build();
953 opts.name = name.map(str::to_string);
954 if case_insensitive {
955 // `caseInsensitiveCollation` (`MongoCollection.js:134-136`). Strength 2 ignores case
956 // and normalizes equivalent Unicode forms; it does **not** ignore diacritics, so
957 // `Café` and `Cafe` stay distinct. Normalization is the part a regex cannot reproduce.
958 opts.collation = Some(
959 mongodb::options::Collation::builder()
960 .locale("en_US".to_string())
961 .strength(mongodb::options::CollationStrength::Secondary)
962 .build(),
963 );
964 }
965
966 self.db
967 .collection::<Document>(class_name)
968 .create_index(IndexModel::builder().keys(keys).options(opts).build())
969 .await
970 .map_err(mongo_err)?;
971 Ok(())
972 }
973
974 async fn create_indexes(
975 &self,
976 class_name: &str,
977 indexes: &[SchemaIndex],
978 ) -> Result<(), ParseError> {
979 if indexes.is_empty() {
980 return Ok(());
981 }
982 let mut models = Vec::with_capacity(indexes.len());
983 for index in indexes {
984 let mut keys = Document::new();
985 for (field, direction) in &index.keys {
986 // Passed through rather than coerced to `1`. `-1` is a descending key and
987 // `"text"`, `"hashed"` and `"2dsphere"` are index types, and all four are what a
988 // parse-server node reading `_metadata.indexes` will expect to find built.
989 keys.insert(
990 field.clone(),
991 index_key_to_bson(&index.name, field, direction)?,
992 );
993 }
994 let mut opts = IndexOptions::default();
995 // Named by the caller, never auto-generated. The name is the key `_metadata.indexes`
996 // is stored under and the handle `dropIndex` needs later.
997 opts.name = Some(index.name.clone());
998 models.push(IndexModel::builder().keys(keys).options(opts).build());
999 }
1000 self.db
1001 .collection::<Document>(class_name)
1002 .create_indexes(models)
1003 .await
1004 .map_err(mongo_err)?;
1005 Ok(())
1006 }
1007
1008 async fn drop_index(&self, class_name: &str, name: &str) -> Result<(), ParseError> {
1009 self.db
1010 .collection::<Document>(class_name)
1011 .drop_index(name)
1012 .await
1013 .map_err(mongo_err)?;
1014 Ok(())
1015 }
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020 use super::*;
1021
1022 fn clp_doc() -> Document {
1023 doc! { "find": { "*": true }, "protectedFields": { "*": ["email"] } }
1024 }
1025
1026 /// The message the server actually sends, so the parser is tested against the real shape
1027 /// rather than against a convenient one.
1028 fn e11000(namespace: &str, index: &str, field: &str, value: &str) -> String {
1029 format!(
1030 "E11000 duplicate key error collection: {namespace} index: {index} dup key: {{ {field}: \"{value}\" }}"
1031 )
1032 }
1033
1034 #[test]
1035 fn the_auto_generated_index_name_yields_its_field() {
1036 for (index, field) in [
1037 ("username_1", "username"),
1038 ("email_1", "email"),
1039 ("name_1", "name"),
1040 ] {
1041 let message = e11000("appdb._User", index, field, "alice");
1042 assert_eq!(duplicated_field(&message).as_deref(), Some(field));
1043 }
1044 }
1045
1046 /// The pre-4.2 spelling qualifies the index with the namespace.
1047 #[test]
1048 fn the_legacy_namespace_qualified_index_name_yields_its_field() {
1049 let message =
1050 "E11000 duplicate key error index: appdb.$username_1 dup key: { : \"alice\" }";
1051 assert_eq!(duplicated_field(message).as_deref(), Some("username"));
1052 }
1053
1054 /// `_throwIfAuthDataDuplicate` reads this prefix, so the whole index name is the field.
1055 #[test]
1056 fn the_auth_data_index_name_is_carried_whole() {
1057 let message = e11000(
1058 "appdb._User",
1059 "_auth_data_facebook_id",
1060 "authData.facebook.id",
1061 "7",
1062 );
1063 assert_eq!(
1064 duplicated_field(&message).as_deref(),
1065 Some("_auth_data_facebook_id")
1066 );
1067 }
1068
1069 /// A name upstream cannot read must not be read here either. Recovering a field from
1070 /// `case_insensitive_username` would produce a `duplicated_field` upstream never produces,
1071 /// and the two servers would then answer a collision differently.
1072 #[test]
1073 fn an_index_name_outside_the_two_shapes_yields_nothing() {
1074 for index in [
1075 "case_insensitive_username",
1076 "username_2",
1077 "field1_1", // upstream's capture class excludes digits
1078 "_id_",
1079 ] {
1080 let message = e11000("appdb._User", index, "username", "alice");
1081 assert_eq!(duplicated_field(&message), None, "{index}");
1082 }
1083 assert_eq!(duplicated_field("some unrelated driver text"), None);
1084 }
1085
1086 /// The field name comes out; the database name and the colliding value do not.
1087 #[test]
1088 fn nothing_but_the_field_name_survives_the_parse() {
1089 let message = e11000("secret_prod_db._User", "username_1", "username", "alice");
1090 let field = duplicated_field(&message).expect("field");
1091 assert!(!field.contains("secret_prod_db"));
1092 assert!(!field.contains("alice"));
1093 }
1094
1095 /// The merge that makes an unspecified operation read back as deny-all rather than as absent.
1096 #[test]
1097 fn a_present_clp_block_merges_over_empty_clps() {
1098 let merged = merge_over_empty_clps(&clp_doc()).expect("merge");
1099
1100 // Every operation is present, and the ones the stored block did not mention are `{}`.
1101 for key in EMPTY_CLPS_KEYS {
1102 assert!(merged.contains_key(key), "{key} must be present");
1103 }
1104 assert!(
1105 matches!(merged.get("update"), Some(ParseValue::Object(m)) if m.is_empty()),
1106 "an unmentioned operation reads back as deny-all, not as absent"
1107 );
1108 assert!(
1109 matches!(merged.get("find"), Some(ParseValue::Object(m)) if m.contains_key("*")),
1110 "the stored value wins over the empty base"
1111 );
1112 // `defaultCLPS` carries an `ACL` key; `emptyCLPS` does not, and merging must not add one.
1113 assert!(
1114 !merged.contains_key("ACL"),
1115 "the present-but-partial case never carries an ACL key"
1116 );
1117 }
1118
1119 #[test]
1120 fn the_merged_block_keeps_empty_clps_key_order() {
1121 let merged =
1122 merge_over_empty_clps(&doc! { "delete": { "*": true }, "later": {} }).expect("merge");
1123 let keys: Vec<&str> = merged.keys().map(String::as_str).collect();
1124 assert_eq!(&keys[..8], &EMPTY_CLPS_KEYS);
1125 assert_eq!(
1126 keys[8], "later",
1127 "a key only the stored block has goes last"
1128 );
1129 }
1130
1131 /// The CLP block is what gets written back, so it has to survive a full round trip through
1132 /// BSON without losing a key parse-rust does not model.
1133 #[test]
1134 fn a_clp_block_round_trips_through_bson() {
1135 let mut stored = clp_doc();
1136 stored.insert("someFutureKey", doc! { "x": 1 });
1137 let merged = merge_over_empty_clps(&stored).expect("merge");
1138 let clp = ClassLevelPermissions::from_map(merged);
1139 let written = parse_map_to_bson_document(clp.raw()).expect("lower");
1140
1141 assert_eq!(
1142 written.get_document("someFutureKey").expect("kept"),
1143 &doc! { "x": 1 }
1144 );
1145 assert!(written.get_document("update").expect("update").is_empty());
1146 }
1147}