Skip to main content

parse_rust_rest/
write.rs

1//! The write body: decoding it, and lowering it onto the two write paths.
2//!
3//! This is the 0.1.0 gap. `Op` was decoded correctly and the write path never called the decoder,
4//! so `{"__op":"Increment","amount":1}` was stored as a literal object with an `__op` key. The
5//! body is a map of [`parse_rust_core::FieldWrite`] end to end for that reason: a field is either
6//! a value or an operation, and "the write path forgot about operations" becomes a missing match
7//! arm rather than silence.
8//!
9//! The two paths are genuinely different and upstream treats them so. A create flattens each
10//! operation to the value it would produce against an absent field
11//! (`flattenUpdateOperatorsForCreate`, `DatabaseController.js:323-365`); an update lowers each to
12//! a storage operation.
13
14use indexmap::IndexMap;
15use parse_rust_core::op::OpPath;
16use parse_rust_core::{
17    classify_field, ErrorCode, FieldWrite, Op, ParseError, ParseMap, ParseValue,
18};
19use parse_rust_storage::{Update, UpdateValue};
20use serde_json::Value as Json;
21
22/// A decoded write body: ordered, because upstream field order is wire-visible.
23pub type WriteBody = IndexMap<String, FieldWrite>;
24
25/// Decode a JSON request body into fields and operations.
26///
27/// `path` decides one wire-visible error message: a non-numeric `Increment.amount` reports
28/// `objects to add must be an array` on create, a copy-paste bug at
29/// `DatabaseController.js:326-330`, and `incrementing must provide a number` on update.
30pub fn decode_write_body(value: &Json, path: OpPath) -> Result<WriteBody, ParseError> {
31    let Json::Object(map) = value else {
32        return Err(ParseError::invalid_json("body must be an object"));
33    };
34    let mut out = WriteBody::new();
35    for (key, value) in map {
36        out.insert(key.clone(), classify_field(value.clone(), path)?);
37    }
38    Ok(out)
39}
40
41/// The body as plain values, for the checks that run against the raw REST body.
42///
43/// `validateRequiredColumns` tests `object[column].__op == 'Delete'` on an undecoded body
44/// (`SchemaController.js:1340-1342`), so a `Delete` has to be visible as its envelope rather than
45/// as a decoded operation. Every other operation is represented by its `__op` name alone, which
46/// is enough for a truthiness test and carries nothing that could be mistaken for a value.
47pub fn as_plain_body(body: &WriteBody) -> ParseMap {
48    let mut out = ParseMap::new();
49    for (key, write) in body {
50        let value = match write {
51            FieldWrite::Value(v) => v.clone(),
52            FieldWrite::Op(op) => {
53                let mut envelope = ParseMap::new();
54                envelope.insert(
55                    "__op".to_string(),
56                    ParseValue::String(op.name().to_string()),
57                );
58                ParseValue::Object(envelope)
59            }
60        };
61        out.insert(key.clone(), value);
62    }
63    out
64}
65
66/// Flatten a write body onto the create path.
67///
68/// Two results are counter-intuitive and both are upstream's: `Remove` yields an **empty array**
69/// rather than removing anything, and `Delete` removes the key entirely.
70pub fn flatten_for_create(body: &WriteBody) -> Result<ParseMap, ParseError> {
71    let mut out = ParseMap::new();
72    for (key, write) in body {
73        match write {
74            FieldWrite::Value(value) => {
75                out.insert(key.clone(), value.clone());
76            }
77            FieldWrite::Op(op) => {
78                if let Some(value) = op.flatten_for_create()? {
79                    out.insert(key.clone(), value);
80                }
81            }
82        }
83    }
84    Ok(out)
85}
86
87/// Lower a write body onto the update path.
88///
89/// Every non-operation field is a `Set`. Relation operations must already have been stripped by
90/// [`crate::relations::collect_relation_updates`]; one reaching here is refused rather than
91/// written into a column that does not exist.
92pub fn lower_update(body: &WriteBody) -> Result<Update, ParseError> {
93    let mut out = Update::new();
94    for (key, write) in body {
95        let value = match write {
96            FieldWrite::Value(value) => UpdateValue::Set(value.clone()),
97            FieldWrite::Op(op) => match op {
98                Op::Increment(amount) => UpdateValue::Increment(*amount),
99                Op::SetOnInsert(value) => UpdateValue::SetOnInsert(value.clone()),
100                Op::Add(objects) => UpdateValue::Add(objects.clone()),
101                Op::AddUnique(objects) => UpdateValue::AddUnique(objects.clone()),
102                Op::Remove(objects) => UpdateValue::Remove(objects.clone()),
103                Op::Delete => UpdateValue::Unset,
104                Op::AddRelation(_) | Op::RemoveRelation(_) | Op::Batch(_) => {
105                    return Err(ParseError::new(
106                        ErrorCode::CommandUnavailable,
107                        format!("The {} operator is not supported yet.", op.name()),
108                    ))
109                }
110            },
111        };
112        out.insert(key.clone(), value);
113    }
114    Ok(out)
115}
116
117/// `allowCustomObjectId`, on the create path only (`RestWrite.js:50-65`).
118///
119/// **This runs on the client's body, before any server-generated identity is folded in.** Signup
120/// pre-generates an objectId so it can build the user's private ACL, so checking after that point
121/// would refuse every signup. That is why this is a separate function called by each create route
122/// rather than a guard inside the write pipeline.
123///
124/// At the default of `false`, `objectId` and `id` are both refused with `INVALID_KEY_NAME`. `id`
125/// is there because the JavaScript SDK uses it internally and a body carrying one is a sign the
126/// caller serialized a `Parse.Object` rather than its attributes.
127///
128/// At `true`, the only check is that a present `objectId` is not falsy, which upstream reports as
129/// `MISSING_OBJECT_ID`. Note the asymmetry: `hasOwnProperty` decides whether to check and JS
130/// truthiness decides the outcome, so `{"objectId": ""}` is an error while an absent key is fine.
131pub fn enforce_object_id_policy(
132    body: &WriteBody,
133    allow_custom_object_id: bool,
134) -> Result<(), ParseError> {
135    let present = |key: &str| match body.get(key) {
136        Some(FieldWrite::Value(value)) => Some(value),
137        // An op in either of these positions is not a string objectId under any setting, and
138        // upstream's truthiness test on the raw `{"__op":...}` object says truthy.
139        Some(FieldWrite::Op(_)) => Some(&ParseValue::Bool(true)),
140        None => None,
141    };
142
143    if allow_custom_object_id {
144        if let Some(value) = present("objectId") {
145            if !parse_rust_core::is_js_truthy(value) {
146                return Err(ParseError::new(
147                    ErrorCode::MissingObjectId,
148                    "objectId must not be empty, null or undefined",
149                ));
150            }
151        }
152        return Ok(());
153    }
154
155    for key in ["objectId", "id"] {
156        if present(key).is_some_and(parse_rust_core::is_js_truthy) {
157            return Err(ParseError::new(
158                ErrorCode::InvalidKeyName,
159                format!("{key} is an invalid field name."),
160            ));
161        }
162    }
163    Ok(())
164}
165
166/// The keys whose post-write value the response echoes back.
167///
168/// Exactly the five operations in `_sanitizeDatabaseResult`'s allow-list
169/// (`DatabaseController.js:2129-2157`): `Add`, `AddUnique`, `Remove`, `Increment` and
170/// `SetOnInsert`. Nothing else, so a plain set and a `Delete` both produce `{updatedAt}` and
171/// nothing more.
172pub fn echoed_keys(body: &WriteBody) -> Vec<String> {
173    body.iter()
174        .filter_map(|(key, write)| match write {
175            FieldWrite::Op(op) if op.echoes_result() => Some(key.clone()),
176            _ => None,
177        })
178        .collect()
179}
180
181/// Build the response body for a write, from the keys the request asked to echo and the row the
182/// adapter returned.
183pub fn echo_response(body: &WriteBody, row: Option<&ParseMap>) -> ParseMap {
184    let mut out = ParseMap::new();
185    let Some(row) = row else { return out };
186    for key in echoed_keys(body) {
187        if let Some(value) = row.get(&key) {
188            out.insert(key, value.clone());
189        }
190    }
191    out
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    fn body(json: &str, path: OpPath) -> WriteBody {
199        decode_write_body(&serde_json::from_str(json).expect("test literal"), path).expect("decode")
200    }
201
202    #[test]
203    fn an_op_is_decoded_rather_than_stored_as_an_object() {
204        let b = body(
205            r#"{"views":{"__op":"Increment","amount":2}}"#,
206            OpPath::Update,
207        );
208        assert!(matches!(b.get("views"), Some(FieldWrite::Op(Op::Increment(a))) if *a == 2.0));
209        let update = lower_update(&b).expect("lower");
210        assert!(matches!(update.get("views"), Some(UpdateValue::Increment(a)) if *a == 2.0));
211    }
212
213    #[test]
214    fn create_flattens_every_op_the_way_upstream_does() {
215        let b = body(
216            r#"{
217                "views":{"__op":"Increment","amount":3},
218                "tags":{"__op":"Add","objects":["a"]},
219                "unique":{"__op":"AddUnique","objects":["b"]},
220                "gone":{"__op":"Remove","objects":["c"]},
221                "dropped":{"__op":"Delete"},
222                "plain":"x"
223            }"#,
224            OpPath::Create,
225        );
226        let row = flatten_for_create(&b).expect("flatten");
227        assert!(matches!(row.get("views"), Some(ParseValue::Number(n)) if *n == 3.0));
228        assert!(matches!(row.get("tags"), Some(ParseValue::Array(a)) if a.len() == 1));
229        assert!(matches!(row.get("unique"), Some(ParseValue::Array(a)) if a.len() == 1));
230        assert!(
231            matches!(row.get("gone"), Some(ParseValue::Array(a)) if a.is_empty()),
232            "Remove yields an empty array rather than removing anything"
233        );
234        assert!(row.get("dropped").is_none());
235        assert!(row.get("plain").is_some());
236    }
237
238    #[test]
239    fn only_the_five_result_bearing_ops_echo_back() {
240        let b = body(
241            r#"{
242                "views":{"__op":"Increment","amount":1},
243                "tags":{"__op":"Add","objects":["a"]},
244                "unique":{"__op":"AddUnique","objects":["b"]},
245                "gone":{"__op":"Remove","objects":["c"]},
246                "dropped":{"__op":"Delete"},
247                "plain":"x"
248            }"#,
249            OpPath::Update,
250        );
251        assert_eq!(echoed_keys(&b), vec!["views", "tags", "unique", "gone"]);
252
253        let mut row = ParseMap::new();
254        row.insert("views".into(), ParseValue::Number(4.0));
255        row.insert("plain".into(), ParseValue::String("x".into()));
256        let echoed = echo_response(&b, Some(&row));
257        assert!(matches!(echoed.get("views"), Some(ParseValue::Number(n)) if *n == 4.0));
258        assert!(
259            echoed.get("plain").is_none(),
260            "a plain set tells the client nothing it did not already know"
261        );
262    }
263
264    #[test]
265    fn the_increment_message_differs_by_path() {
266        let bad = serde_json::from_str(r#"{"n":{"__op":"Increment","amount":"x"}}"#)
267            .expect("test literal");
268        assert_eq!(
269            decode_write_body(&bad, OpPath::Create).unwrap_err().message,
270            "objects to add must be an array"
271        );
272        assert_eq!(
273            decode_write_body(&bad, OpPath::Update).unwrap_err().message,
274            "incrementing must provide a number"
275        );
276    }
277
278    #[test]
279    fn a_relation_op_reaching_the_update_lowering_is_refused() {
280        let b = body(
281            r#"{"users":{"__op":"AddRelation","objects":[]}}"#,
282            OpPath::Update,
283        );
284        let e = lower_update(&b).unwrap_err();
285        assert_eq!(e.code, ErrorCode::CommandUnavailable);
286    }
287
288    #[test]
289    fn a_delete_survives_as_its_envelope_in_the_plain_view() {
290        let b = body(r#"{"ACL":{"__op":"Delete"},"n":1}"#, OpPath::Update);
291        let plain = as_plain_body(&b);
292        match plain.get("ACL") {
293            Some(ParseValue::Object(map)) => {
294                assert!(matches!(map.get("__op"), Some(ParseValue::String(s)) if s == "Delete"))
295            }
296            other => panic!("expected an op envelope, got {other:?}"),
297        }
298        assert!(matches!(plain.get("n"), Some(ParseValue::Number(_))));
299    }
300
301    #[test]
302    fn the_default_refuses_a_client_supplied_object_id_and_id() {
303        for key in ["objectId", "id"] {
304            let b = body(
305                &format!(r#"{{"{key}":"chosen","title":"a"}}"#),
306                OpPath::Create,
307            );
308            let e = enforce_object_id_policy(&b, false).unwrap_err();
309            assert_eq!(e.code, ErrorCode::InvalidKeyName);
310            assert_eq!(e.message, format!("{key} is an invalid field name."));
311        }
312        // A body carrying neither is the ordinary case and passes.
313        let plain = body(r#"{"title":"a"}"#, OpPath::Create);
314        assert!(enforce_object_id_policy(&plain, false).is_ok());
315    }
316
317    /// JS truthiness, not `is_some`. Upstream tests `if (data.objectId)`, so an empty string is a
318    /// body without one as far as this check is concerned.
319    #[test]
320    fn a_falsy_object_id_is_not_a_custom_one() {
321        let b = body(r#"{"objectId":""}"#, OpPath::Create);
322        assert!(enforce_object_id_policy(&b, false).is_ok());
323    }
324
325    /// With the option on, the check inverts: any objectId is allowed and only a falsy one is
326    /// refused, under a different code.
327    #[test]
328    fn allowing_custom_ids_refuses_only_an_empty_one() {
329        let chosen = body(r#"{"objectId":"chosen"}"#, OpPath::Create);
330        assert!(enforce_object_id_policy(&chosen, true).is_ok());
331
332        for literal in [r#"{"objectId":""}"#, r#"{"objectId":null}"#] {
333            let b = body(literal, OpPath::Create);
334            let e = enforce_object_id_policy(&b, true).unwrap_err();
335            assert_eq!(e.code, ErrorCode::MissingObjectId);
336            assert_eq!(e.message, "objectId must not be empty, null or undefined");
337        }
338
339        // An absent key is fine under either setting: `hasOwnProperty` gates the check.
340        let absent = body(r#"{"title":"a"}"#, OpPath::Create);
341        assert!(enforce_object_id_policy(&absent, true).is_ok());
342    }
343}