1use indexmap::IndexMap;
20use parse_rust_core::{ParseMap, ParseValue};
21
22pub type PointersByClass = IndexMap<String, Vec<String>>;
27
28pub fn collect_pointers(results: &[ParseMap], path: &[String]) -> PointersByClass {
30 let mut out: PointersByClass = IndexMap::new();
31 for row in results {
32 collect_from_value(&ParseValue::Object(row.clone()), path, &mut out);
33 }
34 out
35}
36
37fn collect_from_value(value: &ParseValue, path: &[String], out: &mut PointersByClass) {
38 if let ParseValue::Array(items) = value {
42 for item in items {
43 collect_from_value(item, path, out);
44 }
45 return;
46 }
47 match (path.split_first(), value) {
48 (
49 None,
50 ParseValue::Pointer {
51 class_name,
52 object_id,
53 },
54 ) => {
55 let ids = out.entry(class_name.clone()).or_default();
56 if !ids.contains(object_id) {
57 ids.push(object_id.clone());
58 }
59 }
60 (None, _) => {}
61 (Some((head, rest)), ParseValue::Object(map)) => {
62 if let Some(next) = map.get(head) {
63 collect_from_value(next, rest, out);
64 }
65 }
66 _ => {}
67 }
68}
69
70pub fn graft(results: &mut [ParseMap], path: &[String], fetched: &IndexMap<String, ParseMap>) {
77 for row in results.iter_mut() {
78 graft_into_map(row, path, fetched);
79 }
80}
81
82fn graft_into_map(map: &mut ParseMap, path: &[String], fetched: &IndexMap<String, ParseMap>) {
83 let Some((head, rest)) = path.split_first() else {
84 return;
85 };
86 let Some(current) = map.shift_remove(head) else {
87 return;
88 };
89 if let Some(value) = graft_value(current, rest, fetched) {
91 map.insert(head.clone(), value);
92 }
93 }
97
98fn graft_value(
99 value: ParseValue,
100 path: &[String],
101 fetched: &IndexMap<String, ParseMap>,
102) -> Option<ParseValue> {
103 if let ParseValue::Array(items) = value {
106 return Some(ParseValue::Array(
107 items
108 .into_iter()
109 .filter_map(|item| graft_value(item, path, fetched))
110 .collect(),
111 ));
112 }
113 match (path.split_first(), value) {
114 (None, ParseValue::Pointer { object_id, .. }) => fetched
115 .get(&object_id)
116 .map(|row| ParseValue::Object(row.clone())),
117 (None, other) => Some(other),
118 (Some((head, rest)), ParseValue::Object(mut map)) => {
119 if let Some(inner) = map.shift_remove(head) {
120 if let Some(replaced) = graft_value(inner, rest, fetched) {
121 map.insert(head.clone(), replaced);
122 }
123 }
124 Some(ParseValue::Object(map))
125 }
126 (Some(_), other) => Some(other),
127 }
128}
129
130pub fn shape_included(row: &mut ParseMap, class_name: &str, is_master: bool) {
136 row.insert(
137 "__type".to_string(),
138 ParseValue::String("Object".to_string()),
139 );
140 row.insert(
141 "className".to_string(),
142 ParseValue::String(class_name.to_string()),
143 );
144 if class_name == "_User" && !is_master {
145 row.shift_remove("sessionToken");
146 row.shift_remove("authData");
147 }
148}
149
150pub fn keys_for_path(keys: &[String], path: &[String]) -> Option<Vec<String>> {
155 let mut out: Vec<String> = Vec::new();
156 for key in keys {
157 let parts: Vec<&str> = key.split('.').collect();
158 if !path
159 .iter()
160 .enumerate()
161 .all(|(i, p)| parts.get(i).is_some_and(|k| k == p))
162 {
163 continue;
164 }
165 if let Some(next) = parts.get(path.len()) {
166 let next = (*next).to_string();
167 if !out.contains(&next) {
168 out.push(next);
169 }
170 }
171 }
172 (!out.is_empty()).then_some(out)
173}
174
175pub fn exclude_keys_for_path(exclude_keys: &[String], path: &[String]) -> Option<Vec<String>> {
181 let mut out: Vec<String> = Vec::new();
182 for key in exclude_keys {
183 let parts: Vec<&str> = key.split('.').collect();
184 if !path
185 .iter()
186 .enumerate()
187 .all(|(i, p)| parts.get(i).is_some_and(|k| k == p))
188 {
189 continue;
190 }
191 if path.len() == parts.len().saturating_sub(1) {
192 if let Some(next) = parts.get(path.len()) {
193 let next = (*next).to_string();
194 if !out.contains(&next) {
195 out.push(next);
196 }
197 }
198 }
199 }
200 (!out.is_empty()).then_some(out)
201}
202
203pub fn paths_forced_by_projection(keys: &[String], exclude_keys: &[String]) -> Vec<String> {
208 keys.iter()
209 .chain(exclude_keys.iter())
210 .filter(|k| k.contains('.'))
211 .filter_map(|k| k.rsplit_once('.').map(|(head, _)| head.to_string()))
212 .collect()
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 fn pointer(class: &str, id: &str) -> ParseValue {
220 ParseValue::Pointer {
221 class_name: class.to_string(),
222 object_id: id.to_string(),
223 }
224 }
225
226 fn row(pairs: Vec<(&str, ParseValue)>) -> ParseMap {
227 let mut m = ParseMap::new();
228 for (k, v) in pairs {
229 m.insert(k.to_string(), v);
230 }
231 m
232 }
233
234 #[test]
235 fn pointers_group_by_class_and_dedupe() {
236 let results = vec![
237 row(vec![("author", pointer("_User", "u1"))]),
238 row(vec![("author", pointer("_User", "u1"))]),
239 row(vec![("author", pointer("Robot", "r1"))]),
240 ];
241 let found = collect_pointers(&results, &["author".to_string()]);
242 assert_eq!(found.get("_User"), Some(&vec!["u1".to_string()]));
243 assert_eq!(found.get("Robot"), Some(&vec!["r1".to_string()]));
244 }
245
246 #[test]
247 fn pointers_inside_arrays_are_found() {
248 let results = vec![row(vec![(
249 "editors",
250 ParseValue::Array(vec![pointer("_User", "u1"), pointer("_User", "u2")]),
251 )])];
252 let found = collect_pointers(&results, &["editors".to_string()]);
253 assert_eq!(
254 found.get("_User"),
255 Some(&vec!["u1".to_string(), "u2".to_string()])
256 );
257 }
258
259 #[test]
260 fn a_nested_path_reaches_through_an_expanded_parent() {
261 let results = vec![row(vec![(
262 "author",
263 ParseValue::Object(row(vec![("company", pointer("Company", "c1"))])),
264 )])];
265 let found = collect_pointers(&results, &["author".to_string(), "company".to_string()]);
266 assert_eq!(found.get("Company"), Some(&vec!["c1".to_string()]));
267 }
268
269 #[test]
270 fn a_resolved_pointer_is_replaced_and_an_unresolved_one_disappears() {
271 let mut results = vec![
272 row(vec![
273 ("objectId", ParseValue::String("p1".into())),
274 ("author", pointer("_User", "u1")),
275 ]),
276 row(vec![
277 ("objectId", ParseValue::String("p2".into())),
278 ("author", pointer("_User", "hidden")),
279 ]),
280 ];
281 let mut fetched = IndexMap::new();
282 fetched.insert(
283 "u1".to_string(),
284 row(vec![("objectId", ParseValue::String("u1".into()))]),
285 );
286 graft(&mut results, &["author".to_string()], &fetched);
287 assert!(matches!(
288 results[0].get("author"),
289 Some(ParseValue::Object(_))
290 ));
291 assert!(
292 results[1].get("author").is_none(),
293 "a pointer the caller cannot read is dropped, not left as a pointer"
294 );
295 }
296
297 #[test]
298 fn an_unresolved_pointer_inside_an_array_is_filtered_out() {
299 let mut results = vec![row(vec![(
300 "editors",
301 ParseValue::Array(vec![pointer("_User", "u1"), pointer("_User", "hidden")]),
302 )])];
303 let mut fetched = IndexMap::new();
304 fetched.insert(
305 "u1".to_string(),
306 row(vec![("objectId", ParseValue::String("u1".into()))]),
307 );
308 graft(&mut results, &["editors".to_string()], &fetched);
309 match results[0].get("editors") {
310 Some(ParseValue::Array(items)) => assert_eq!(items.len(), 1),
311 other => panic!("expected an array, got {other:?}"),
312 }
313 }
314
315 #[test]
316 fn an_included_user_loses_its_session_token_for_a_non_master_caller() {
317 let mut r = row(vec![
318 ("sessionToken", ParseValue::String("r:t".into())),
319 ("authData", ParseValue::Object(ParseMap::new())),
320 ]);
321 shape_included(&mut r, "_User", false);
322 assert!(r.get("sessionToken").is_none());
323 assert!(r.get("authData").is_none());
324 assert!(matches!(r.get("__type"), Some(ParseValue::String(s)) if s == "Object"));
325 assert!(matches!(r.get("className"), Some(ParseValue::String(s)) if s == "_User"));
326
327 let mut r = row(vec![("sessionToken", ParseValue::String("r:t".into()))]);
328 shape_included(&mut r, "_User", true);
329 assert!(r.get("sessionToken").is_some());
330 }
331
332 #[test]
333 fn projections_rewrite_per_path() {
334 let keys = vec!["author.name".to_string(), "title".to_string()];
335 assert_eq!(
336 keys_for_path(&keys, &["author".to_string()]),
337 Some(vec!["name".to_string()])
338 );
339 assert_eq!(keys_for_path(&keys, &["other".to_string()]), None);
340
341 let excludes = vec!["author.company.name".to_string()];
343 assert_eq!(
344 exclude_keys_for_path(&excludes, &["author".to_string()]),
345 None
346 );
347 assert_eq!(
348 exclude_keys_for_path(&excludes, &["author".to_string(), "company".to_string()]),
349 Some(vec!["name".to_string()])
350 );
351 }
352
353 #[test]
354 fn a_dotted_projection_forces_its_parent_include() {
355 assert_eq!(
356 paths_forced_by_projection(&["a.b.c".to_string(), "d".to_string()], &[]),
357 vec!["a.b".to_string()]
358 );
359 assert_eq!(
360 paths_forced_by_projection(&[], &["x.y".to_string()]),
361 vec!["x".to_string()]
362 );
363 }
364}