1use bson::{Bson, Document};
13use parse_rust_core::{ParseDate, ParseError, ParseMap, ParseValue};
14use parse_rust_storage::ClassSchema;
15
16pub fn storage_key(schema: &ClassSchema, field: &str) -> String {
20 match field {
21 "objectId" => return "_id".into(),
22 "createdAt" => return "_created_at".into(),
23 "updatedAt" => return "_updated_at".into(),
24 "sessionToken" => return "_session_token".into(),
25 "lastUsed" => return "_last_used".into(),
26 "timesUsed" => return "times_used".into(),
27 _ => {}
28 }
29 if schema.is_pointer_field(field) {
30 format!("_p_{field}")
31 } else {
32 field.to_string()
33 }
34}
35
36const INTERNAL_COLUMNS: [&str; 6] = [
51 "_rperm",
52 "_wperm",
53 "_hashed_password",
54 "_perishable_token",
55 "_email_verify_token",
56 "_failed_login_count",
57];
58
59fn untransform_key(field: &str) -> Option<String> {
61 match field {
62 "_id" => Some("objectId".into()),
63 "_created_at" => Some("createdAt".into()),
64 "_updated_at" => Some("updatedAt".into()),
65 "_session_token" => Some("sessionToken".into()),
66 "_last_used" => Some("lastUsed".into()),
67 "times_used" => Some("timesUsed".into()),
68 _ => {
69 if let Some(stripped) = field.strip_prefix("_p_") {
70 return Some(stripped.to_string());
71 }
72 if INTERNAL_COLUMNS.contains(&field) || field.starts_with("_auth_data_") {
73 return Some(field.to_string());
74 }
75 None
76 }
77 }
78}
79
80fn to_bson_number(n: f64) -> Bson {
91 if n.fract() == 0.0 && n >= i32::MIN as f64 && n <= i32::MAX as f64 {
92 Bson::Int32(n as i32)
95 } else {
96 Bson::Double(n)
97 }
98}
99
100fn interior_value_to_bson(value: &ParseValue) -> Result<Bson, ParseError> {
111 Ok(match value {
112 ParseValue::Date(d) => date_to_bson(d),
113 ParseValue::Bytes(b) => Bson::Binary(bson::Binary {
114 subtype: bson::spec::BinarySubtype::Generic,
115 bytes: b.clone(),
116 }),
117 ParseValue::Pointer {
118 class_name,
119 object_id,
120 } => {
121 let mut d = Document::new();
122 d.insert("__type", "Pointer");
123 d.insert("className", class_name.clone());
124 d.insert("objectId", object_id.clone());
125 Bson::Document(d)
126 }
127 other => plain_value_to_bson(other)?,
128 })
129}
130
131fn date_to_bson(d: &ParseDate) -> Bson {
132 Bson::DateTime(bson::DateTime::from_millis(d.timestamp_millis()))
133}
134
135fn plain_value_to_bson(value: &ParseValue) -> Result<Bson, ParseError> {
137 Ok(match value {
138 ParseValue::Null => Bson::Null,
139 ParseValue::Bool(b) => Bson::Boolean(*b),
140 ParseValue::Number(n) => to_bson_number(*n),
141 ParseValue::String(s) => Bson::String(s.clone()),
142 ParseValue::Array(items) => Bson::Array(
143 items
144 .iter()
145 .map(interior_value_to_bson)
146 .collect::<Result<Vec<_>, _>>()?,
147 ),
148 ParseValue::Object(map) => {
149 let mut d = Document::new();
150 for (k, v) in map {
151 d.insert(k.clone(), interior_value_to_bson(v)?);
152 }
153 Bson::Document(d)
154 }
155 ParseValue::Date(d) => date_to_bson(d),
156 ParseValue::Bytes(b) => Bson::Binary(bson::Binary {
157 subtype: bson::spec::BinarySubtype::Generic,
158 bytes: b.clone(),
159 }),
160 ParseValue::GeoPoint {
161 latitude,
162 longitude,
163 } => {
164 Bson::Array(vec![Bson::Double(*longitude), Bson::Double(*latitude)])
166 }
167 ParseValue::Pointer { .. } => {
168 return Err(ParseError::incorrect_type(
169 "a top-level Pointer is lowered by key, not by value".to_string(),
170 ))
171 }
172 ParseValue::Polygon(coords) => Bson::Document({
173 let mut d = Document::new();
174 d.insert("type", "Polygon");
175 d.insert(
176 "coordinates",
177 Bson::Array(vec![Bson::Array(
178 coords
179 .iter()
180 .map(|(lat, lng)| Bson::Array(vec![Bson::Double(*lng), Bson::Double(*lat)]))
183 .collect(),
184 )]),
185 );
186 d
187 }),
188 ParseValue::File { name, .. } => Bson::String(name.clone()),
189 ParseValue::Relation { .. } => {
190 return Err(ParseError::incorrect_type(
191 "Relation fields are not stored on the object".to_string(),
192 ))
193 }
194 })
195}
196
197pub fn parse_object_to_mongo_create(
205 schema: &ClassSchema,
206 object: &ParseMap,
207) -> Result<Document, ParseError> {
208 let mut out = Document::new();
209
210 for (key, value) in object {
211 if matches!(value, ParseValue::Relation { .. }) {
212 continue;
213 }
214 if key == "ACL" {
215 return Err(ParseError::invalid_json(
216 "ACL must be lowered through parse_acl_to_columns, not as a field".to_string(),
217 ));
218 }
219
220 let mongo_key = storage_key(schema, key);
221
222 if schema.is_pointer_field(key) {
224 match value {
225 ParseValue::Pointer {
226 class_name,
227 object_id,
228 } => {
229 out.insert(mongo_key, Bson::String(format!("{class_name}${object_id}")));
230 continue;
231 }
232 ParseValue::Null => {
233 out.insert(mongo_key, Bson::Null);
234 continue;
235 }
236 _ => {
237 return Err(ParseError::incorrect_type(format!(
238 "schema mismatch for {}.{key}; expected Pointer but got a non-pointer",
239 schema.class_name
240 )))
241 }
242 }
243 }
244
245 out.insert(mongo_key, plain_value_to_bson(value)?);
246 }
247
248 Ok(out)
249}
250
251pub fn mongo_object_to_parse(doc: &Document) -> Result<ParseMap, ParseError> {
258 let mut out = ParseMap::new();
259
260 for (key, value) in doc {
261 if key == "_acl" {
268 continue;
269 }
270
271 let parse_key = match untransform_key(key) {
272 Some(k) => k,
273 None if key.starts_with('_') && key != "__type" => {
274 return Err(ParseError::invalid_query(format!(
275 "bad key in untransform: {key}"
276 )))
277 }
278 None => key.clone(),
279 };
280
281 if let Some(stripped) = key.strip_prefix("_p_") {
283 match value {
284 Bson::String(s) => {
285 let (class_name, object_id) = s.split_once('$').ok_or_else(|| {
286 ParseError::incorrect_type(format!(
287 "pointer field {stripped} is malformed: {s}"
288 ))
289 })?;
290 out.insert(
291 stripped.to_string(),
292 ParseValue::Pointer {
293 class_name: class_name.to_string(),
294 object_id: object_id.to_string(),
295 },
296 );
297 }
298 Bson::Null => {
299 out.insert(stripped.to_string(), ParseValue::Null);
300 }
301 _ => {
302 return Err(ParseError::incorrect_type(format!(
303 "pointer field {stripped} is not a string"
304 )))
305 }
306 }
307 continue;
308 }
309
310 out.insert(parse_key, bson_to_parse_value(value)?);
311 }
312
313 Ok(out)
314}
315
316fn bson_to_parse_value(value: &Bson) -> Result<ParseValue, ParseError> {
319 Ok(match value {
320 Bson::Null => ParseValue::Null,
321 Bson::Boolean(b) => ParseValue::Bool(*b),
322 Bson::Int32(n) => ParseValue::Number(*n as f64),
325 Bson::Int64(n) => ParseValue::Number(*n as f64),
326 Bson::Double(n) => ParseValue::Number(*n),
327 Bson::String(s) => ParseValue::String(s.clone()),
328 Bson::DateTime(dt) => ParseValue::Date(ParseDate::parse_iso(
329 &dt.try_to_rfc3339_string()
330 .map_err(|e| ParseError::invalid_json(format!("undecodable stored date: {e}")))?,
331 )?),
332 Bson::Binary(b) => ParseValue::Bytes(b.bytes.clone()),
333 Bson::Array(items) => ParseValue::Array(
334 items
335 .iter()
336 .map(bson_to_parse_value)
337 .collect::<Result<Vec<_>, _>>()?,
338 ),
339 Bson::Document(d) => {
340 let mut map = ParseMap::new();
341 for (k, v) in d {
342 map.insert(k.clone(), bson_to_parse_value(v)?);
343 }
344 ParseValue::Object(map)
345 }
346 other => {
347 return Err(ParseError::incorrect_type(format!(
348 "unsupported BSON type in stored document: {other:?}"
349 )))
350 }
351 })
352}
353
354pub fn value_to_bson_for_query(
360 schema: &ClassSchema,
361 field: &str,
362 value: &ParseValue,
363) -> Result<Bson, ParseError> {
364 if schema.is_pointer_field(field) {
365 if let ParseValue::Pointer {
366 class_name,
367 object_id,
368 } = value
369 {
370 return Ok(Bson::String(format!("{class_name}${object_id}")));
371 }
372 }
373 plain_value_to_bson(value)
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use parse_rust_storage::FieldType;
380
381 fn post_schema() -> ClassSchema {
382 ClassSchema::new("Post")
383 .with_field("title", FieldType::String)
384 .with_field("views", FieldType::Number)
385 .with_field(
386 "author",
387 FieldType::Pointer {
388 target_class: "_User".into(),
389 },
390 )
391 }
392
393 fn map(pairs: Vec<(&str, ParseValue)>) -> ParseMap {
394 let mut m = ParseMap::new();
395 for (k, v) in pairs {
396 m.insert(k.to_string(), v);
397 }
398 m
399 }
400
401 #[test]
402 fn renamed_keys_round_trip() {
403 let s = post_schema();
404 assert_eq!(storage_key(&s, "objectId"), "_id");
405 assert_eq!(storage_key(&s, "createdAt"), "_created_at");
406 assert_eq!(storage_key(&s, "updatedAt"), "_updated_at");
407 assert_eq!(storage_key(&s, "title"), "title");
408 assert_eq!(storage_key(&s, "author"), "_p_author");
409
410 assert_eq!(untransform_key("_id").as_deref(), Some("objectId"));
411 assert_eq!(untransform_key("_created_at").as_deref(), Some("createdAt"));
412 assert_eq!(untransform_key("_p_author").as_deref(), Some("author"));
413 assert_eq!(untransform_key("title"), None);
414 }
415
416 #[test]
418 fn integral_numbers_in_i32_range_store_as_int32() {
419 assert_eq!(to_bson_number(0.0), Bson::Int32(0));
420 assert_eq!(to_bson_number(42.0), Bson::Int32(42));
421 assert_eq!(to_bson_number(-42.0), Bson::Int32(-42));
422 assert_eq!(to_bson_number(i32::MAX as f64), Bson::Int32(i32::MAX));
423 assert_eq!(to_bson_number(i32::MIN as f64), Bson::Int32(i32::MIN));
424 }
425
426 #[test]
427 fn everything_else_stores_as_double() {
428 assert_eq!(to_bson_number(1.5), Bson::Double(1.5));
429 assert_eq!(
431 to_bson_number(i32::MAX as f64 + 1.0),
432 Bson::Double(i32::MAX as f64 + 1.0)
433 );
434 assert_eq!(to_bson_number(1e20), Bson::Double(1e20));
435 }
436
437 #[test]
438 fn a_pointer_field_collapses_to_class_dollar_id() {
439 let doc = parse_object_to_mongo_create(
440 &post_schema(),
441 &map(vec![(
442 "author",
443 ParseValue::Pointer {
444 class_name: "_User".into(),
445 object_id: "abc123".into(),
446 },
447 )]),
448 )
449 .expect("transform");
450 assert_eq!(doc.get_str("_p_author").expect("_p_author"), "_User$abc123");
451 assert!(
452 !doc.contains_key("author"),
453 "must not also store the raw key"
454 );
455 }
456
457 #[test]
460 fn a_nested_pointer_keeps_its_type_envelope() {
461 let doc = parse_object_to_mongo_create(
462 &post_schema(),
463 &map(vec![(
464 "tags",
465 ParseValue::Array(vec![ParseValue::Pointer {
466 class_name: "Tag".into(),
467 object_id: "t1".into(),
468 }]),
469 )]),
470 )
471 .expect("transform");
472 let arr = doc.get_array("tags").expect("tags");
473 let nested = arr[0].as_document().expect("document");
474 assert_eq!(nested.get_str("__type").expect("__type"), "Pointer");
475 assert_eq!(nested.get_str("className").expect("className"), "Tag");
476 }
477
478 #[test]
479 fn relation_values_are_dropped_not_stored() {
480 let doc = parse_object_to_mongo_create(
481 &post_schema(),
482 &map(vec![
483 ("title", ParseValue::String("x".into())),
484 (
485 "comments",
486 ParseValue::Relation {
487 class_name: "Comment".into(),
488 },
489 ),
490 ]),
491 )
492 .expect("transform");
493 assert!(doc.contains_key("title"));
494 assert!(
495 !doc.contains_key("comments"),
496 "a Relation lives in a join table, not on the object"
497 );
498 }
499
500 #[test]
501 fn read_back_restores_keys_and_pointers() {
502 let mut doc = Document::new();
503 doc.insert("_id", "objid1");
504 doc.insert("title", "hello");
505 doc.insert("views", Bson::Int32(7));
506 doc.insert("_p_author", "_User$abc123");
507 doc.insert(
508 "_created_at",
509 Bson::DateTime(bson::DateTime::from_millis(1_700_000_000_000)),
510 );
511
512 let parsed = mongo_object_to_parse(&doc).expect("untransform");
513 assert!(matches!(parsed.get("objectId"), Some(ParseValue::String(s)) if s == "objid1"));
514 assert!(matches!(parsed.get("views"), Some(ParseValue::Number(n)) if *n == 7.0));
515 assert!(matches!(
516 parsed.get("author"),
517 Some(ParseValue::Pointer { class_name, object_id })
518 if class_name == "_User" && object_id == "abc123"
519 ));
520 assert!(matches!(parsed.get("createdAt"), Some(ParseValue::Date(_))));
521 }
522
523 #[test]
526 fn permission_columns_survive_for_the_acl_rebuild() {
527 let mut doc = Document::new();
528 doc.insert("title", "x");
529 doc.insert("_rperm", Bson::Array(vec![Bson::String("*".into())]));
530 doc.insert("_wperm", Bson::Array(vec![]));
531 doc.insert("_acl", Document::new());
532
533 let parsed = mongo_object_to_parse(&doc).expect("untransform");
534 assert!(parsed.get("_rperm").is_some(), "raise_acl needs this");
535 assert!(parsed.get("_wperm").is_some(), "raise_acl needs this");
536 assert!(
537 parsed.get("_acl").is_none(),
538 "the legacy mirror is write-only and is dropped on read"
539 );
540 }
541
542 #[test]
543 fn internal_columns_survive_under_their_own_names() {
544 let mut doc = Document::new();
547 doc.insert("_hashed_password", "$2b$10$abc");
548 doc.insert("_session_token", "r:tok");
549 let parsed = mongo_object_to_parse(&doc).expect("untransform");
550 assert!(parsed.get("_hashed_password").is_some());
551 assert!(
552 parsed.get("password").is_none(),
553 "the hash must never be raised under a user-facing name"
554 );
555 assert!(parsed.get("sessionToken").is_some());
557 }
558
559 #[test]
560 fn an_unknown_underscore_key_is_refused_rather_than_passed_through() {
561 let mut doc = Document::new();
562 doc.insert("_mystery", "x"); let err = mongo_object_to_parse(&doc).unwrap_err();
564 assert!(err.message.contains("bad key in untransform"));
565 }
566
567 #[test]
568 fn int64_and_int32_both_raise_to_one_number_type() {
569 let mut doc = Document::new();
570 doc.insert("a", Bson::Int32(1));
571 doc.insert("b", Bson::Int64(2));
572 doc.insert("c", Bson::Double(3.5));
573 let parsed = mongo_object_to_parse(&doc).expect("untransform");
574 for (k, expected) in [("a", 1.0), ("b", 2.0), ("c", 3.5)] {
575 assert!(matches!(parsed.get(k), Some(ParseValue::Number(n)) if *n == expected));
576 }
577 }
578}