fraiseql_core/cache/
cascade_response_parser.rs1use serde_json::Value;
63
64use super::entity_key::EntityKey;
65use crate::error::{FraiseQLError, Result};
66
67#[derive(Debug, Clone, Eq, PartialEq)]
72pub struct CascadeEntities {
73 pub updated: Vec<EntityKey>,
75
76 pub deleted: Vec<EntityKey>,
78}
79
80impl CascadeEntities {
81 pub fn new(updated: Vec<EntityKey>, deleted: Vec<EntityKey>) -> Self {
83 Self { updated, deleted }
84 }
85
86 #[must_use]
88 pub fn all_affected(&self) -> Vec<EntityKey> {
89 let mut all = self.updated.clone();
90 all.extend(self.deleted.clone());
91 all
92 }
93
94 #[must_use]
96 pub fn has_changes(&self) -> bool {
97 !self.updated.is_empty() || !self.deleted.is_empty()
98 }
99}
100
101#[derive(Debug, Clone)]
106pub struct CascadeResponseParser;
107
108impl CascadeResponseParser {
109 #[must_use]
111 pub fn new() -> Self {
112 Self
113 }
114
115 pub fn parse_cascade_response(&self, response: &Value) -> Result<CascadeEntities> {
141 let cascade = self.find_cascade_field(response)?;
143
144 if cascade.is_null() {
145 return Ok(CascadeEntities {
147 updated: Vec::new(),
148 deleted: Vec::new(),
149 });
150 }
151
152 let updated = self.extract_entities_list(&cascade, "updated")?;
154
155 let deleted = self.extract_entities_list(&cascade, "deleted")?;
157
158 Ok(CascadeEntities { updated, deleted })
159 }
160
161 fn find_cascade_field(&self, response: &Value) -> Result<Value> {
168 if let Some(cascade) = response.get("cascade") {
170 return Ok(cascade.clone());
171 }
172
173 if let Some(data) = response.get("data") {
175 if let Some(cascade) = data.get("cascade") {
176 return Ok(cascade.clone());
177 }
178
179 for (_key, value) in data.as_object().unwrap_or(&Default::default()).iter() {
181 if let Some(cascade) = value.get("cascade") {
182 return Ok(cascade.clone());
183 }
184 }
185 }
186
187 for (_key, value) in response.as_object().unwrap_or(&Default::default()).iter() {
189 if let Some(cascade) = value.get("cascade") {
190 return Ok(cascade.clone());
191 }
192 }
193
194 Ok(Value::Null)
196 }
197
198 fn extract_entities_list(&self, cascade: &Value, field_name: &str) -> Result<Vec<EntityKey>> {
200 let entities_array = match cascade.get(field_name) {
201 Some(Value::Array(arr)) => arr,
202 Some(Value::Null) | None => return Ok(Vec::new()),
203 Some(val) => {
204 return Err(FraiseQLError::Validation {
205 message: format!(
206 "cascade.{} should be array, got {}",
207 field_name,
208 match val {
209 Value::Object(_) => "object",
210 Value::String(_) => "string",
211 Value::Number(_) => "number",
212 Value::Bool(_) => "boolean",
213 Value::Null => "null",
214 _ => "unknown",
215 }
216 ),
217 path: Some(format!("cascade.{}", field_name)),
218 });
219 },
220 };
221
222 let mut entities = Vec::new();
223
224 for entity_obj in entities_array.iter() {
225 let entity = self.parse_cascade_entity(entity_obj)?;
226 entities.push(entity);
227 }
228
229 Ok(entities)
230 }
231
232 fn parse_cascade_entity(&self, entity_obj: &Value) -> Result<EntityKey> {
236 let obj = entity_obj.as_object().ok_or_else(|| FraiseQLError::Validation {
237 message: "Cascade entity should be object".to_string(),
238 path: Some("cascade.updated[*]".to_string()),
239 })?;
240
241 let type_name = obj.get("__typename").and_then(Value::as_str).ok_or_else(|| {
243 FraiseQLError::Validation {
244 message: "Cascade entity missing __typename field".to_string(),
245 path: Some("cascade.updated[*].__typename".to_string()),
246 }
247 })?;
248
249 let entity_id =
251 obj.get("id").and_then(Value::as_str).ok_or_else(|| FraiseQLError::Validation {
252 message: "Cascade entity missing id field".to_string(),
253 path: Some("cascade.updated[*].id".to_string()),
254 })?;
255
256 EntityKey::new(type_name, entity_id)
257 }
258}
259
260impl Default for CascadeResponseParser {
261 fn default() -> Self {
262 Self::new()
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use serde_json::json;
269
270 use super::*;
271
272 #[test]
273 fn test_parse_simple_cascade_response() {
274 let parser = CascadeResponseParser::new();
275 let response = json!({
276 "createPost": {
277 "cascade": {
278 "updated": [
279 {
280 "__typename": "User",
281 "id": "550e8400-e29b-41d4-a716-446655440000",
282 "postCount": 5
283 }
284 ]
285 }
286 }
287 });
288
289 let entities = parser.parse_cascade_response(&response).unwrap();
290 assert_eq!(entities.updated.len(), 1);
291 assert_eq!(entities.updated[0].entity_type, "User");
292 assert_eq!(entities.updated[0].entity_id, "550e8400-e29b-41d4-a716-446655440000");
293 assert_eq!(entities.deleted.len(), 0);
294 }
295
296 #[test]
297 fn test_parse_multiple_updated_entities() {
298 let parser = CascadeResponseParser::new();
299 let response = json!({
300 "updateUser": {
301 "cascade": {
302 "updated": [
303 { "__typename": "User", "id": "uuid-1" },
304 { "__typename": "Post", "id": "uuid-2" },
305 { "__typename": "Notification", "id": "uuid-3" }
306 ]
307 }
308 }
309 });
310
311 let entities = parser.parse_cascade_response(&response).unwrap();
312 assert_eq!(entities.updated.len(), 3);
313 assert_eq!(entities.updated[0].entity_type, "User");
314 assert_eq!(entities.updated[1].entity_type, "Post");
315 assert_eq!(entities.updated[2].entity_type, "Notification");
316 }
317
318 #[test]
319 fn test_parse_deleted_entities() {
320 let parser = CascadeResponseParser::new();
321 let response = json!({
322 "deletePost": {
323 "cascade": {
324 "deleted": [
325 { "__typename": "Post", "id": "post-uuid" },
326 { "__typename": "Comment", "id": "comment-uuid" }
327 ]
328 }
329 }
330 });
331
332 let entities = parser.parse_cascade_response(&response).unwrap();
333 assert_eq!(entities.updated.len(), 0);
334 assert_eq!(entities.deleted.len(), 2);
335 assert_eq!(entities.deleted[0].entity_type, "Post");
336 assert_eq!(entities.deleted[1].entity_type, "Comment");
337 }
338
339 #[test]
340 fn test_parse_both_updated_and_deleted() {
341 let parser = CascadeResponseParser::new();
342 let response = json!({
343 "mutation": {
344 "cascade": {
345 "updated": [{ "__typename": "User", "id": "u-1" }],
346 "deleted": [{ "__typename": "Session", "id": "s-1" }]
347 }
348 }
349 });
350
351 let entities = parser.parse_cascade_response(&response).unwrap();
352 assert_eq!(entities.updated.len(), 1);
353 assert_eq!(entities.deleted.len(), 1);
354 assert_eq!(entities.all_affected().len(), 2);
355 }
356
357 #[test]
358 fn test_parse_empty_cascade() {
359 let parser = CascadeResponseParser::new();
360 let response = json!({
361 "mutation": {
362 "cascade": {
363 "updated": [],
364 "deleted": []
365 }
366 }
367 });
368
369 let entities = parser.parse_cascade_response(&response).unwrap();
370 assert!(!entities.has_changes());
371 assert_eq!(entities.all_affected().len(), 0);
372 }
373
374 #[test]
375 fn test_parse_no_cascade_field() {
376 let parser = CascadeResponseParser::new();
377 let response = json!({
378 "createPost": {
379 "post": { "id": "post-1", "title": "Hello" }
380 }
381 });
382
383 let entities = parser.parse_cascade_response(&response).unwrap();
384 assert!(!entities.has_changes());
385 }
386
387 #[test]
388 fn test_parse_nested_in_data_field() {
389 let parser = CascadeResponseParser::new();
390 let response = json!({
391 "data": {
392 "createPost": {
393 "cascade": {
394 "updated": [{ "__typename": "User", "id": "uuid-1" }]
395 }
396 }
397 }
398 });
399
400 let entities = parser.parse_cascade_response(&response).unwrap();
401 assert_eq!(entities.updated.len(), 1);
402 }
403
404 #[test]
405 fn test_parse_missing_typename() {
406 let parser = CascadeResponseParser::new();
407 let response = json!({
408 "mutation": {
409 "cascade": {
410 "updated": [{ "id": "uuid-1" }]
411 }
412 }
413 });
414
415 let result = parser.parse_cascade_response(&response);
416 assert!(result.is_err());
417 }
418
419 #[test]
420 fn test_parse_missing_id() {
421 let parser = CascadeResponseParser::new();
422 let response = json!({
423 "mutation": {
424 "cascade": {
425 "updated": [{ "__typename": "User" }]
426 }
427 }
428 });
429
430 let result = parser.parse_cascade_response(&response);
431 assert!(result.is_err());
432 }
433
434 #[test]
435 fn test_cascade_entities_all_affected() {
436 let updated = vec![
437 EntityKey::new("User", "u-1").unwrap(),
438 EntityKey::new("User", "u-2").unwrap(),
439 ];
440 let deleted = vec![EntityKey::new("Post", "p-1").unwrap()];
441
442 let cascade = CascadeEntities::new(updated, deleted);
443 let all = cascade.all_affected();
444 assert_eq!(all.len(), 3);
445 }
446}