fraiseql_core/cache/cascade_response_parser.rs
1//! Parser for GraphQL Cascade responses to extract entity invalidation data.
2//!
3//! This module parses GraphQL mutation responses following the GraphQL Cascade specification,
4//! extracting all affected entities (updated and deleted) to enable entity-level cache
5//! invalidation.
6//!
7//! # Architecture
8//!
9//! ```text
10//! GraphQL Mutation Response
11//! ┌──────────────────────────────────┐
12//! │ { │
13//! │ "createPost": { │
14//! │ "post": { ... }, │
15//! │ "cascade": { │
16//! │ "updated": [ │
17//! │ { │
18//! │ "__typename": "User", │
19//! │ "id": "uuid-123", │
20//! │ ... │
21//! │ } │
22//! │ ], │
23//! │ "deleted": [ ... ] │
24//! │ } │
25//! │ } │
26//! │ } │
27//! └──────────────────────────────────┘
28//! │
29//! ↓ parse_cascade_response()
30//! ┌──────────────────────────────────┐
31//! │ CascadeEntities: │
32//! │ updated: [ │
33//! │ EntityKey("User", "uuid-123") │
34//! │ ] │
35//! │ deleted: [] │
36//! └──────────────────────────────────┘
37//! ```
38//!
39//! # Examples
40//!
41//! ```rust
42//! use fraiseql_core::cache::CascadeResponseParser;
43//! use serde_json::json;
44//! # use fraiseql_core::error::Result;
45//! # fn example() -> Result<()> {
46//!
47//! let parser = CascadeResponseParser::new();
48//!
49//! let response = json!({
50//! "createPost": {
51//! "cascade": {
52//! "updated": [
53//! { "__typename": "User", "id": "550e8400-e29b-41d4-a716-446655440000" }
54//! ]
55//! }
56//! }
57//! });
58//!
59//! let entities = parser.parse_cascade_response(&response)?;
60//! assert_eq!(entities.updated.len(), 1);
61//! assert_eq!(entities.updated[0].entity_type, "User");
62//! # Ok(())
63//! # }
64//! ```
65
66use serde_json::Value;
67
68use super::entity_key::EntityKey;
69use crate::error::{FraiseQLError, Result};
70
71/// Cascade entities extracted from a GraphQL mutation response.
72///
73/// Represents all entities affected by a mutation (both updated and deleted),
74/// used to determine which caches need invalidation.
75#[derive(Debug, Clone, Eq, PartialEq)]
76pub struct CascadeEntities {
77 /// Updated entities - entries that were modified or created
78 pub updated: Vec<EntityKey>,
79
80 /// Deleted entities - entries that were removed
81 pub deleted: Vec<EntityKey>,
82}
83
84impl CascadeEntities {
85 /// Create new cascade entities with separate updated and deleted lists.
86 #[must_use]
87 pub const fn new(updated: Vec<EntityKey>, deleted: Vec<EntityKey>) -> Self {
88 Self { updated, deleted }
89 }
90
91 /// Get all affected entities (both updated and deleted).
92 #[must_use]
93 pub fn all_affected(&self) -> Vec<EntityKey> {
94 let mut all = self.updated.clone();
95 all.extend(self.deleted.clone());
96 all
97 }
98
99 /// Check if cascade has any affected entities.
100 #[must_use]
101 pub const fn has_changes(&self) -> bool {
102 !self.updated.is_empty() || !self.deleted.is_empty()
103 }
104}
105
106/// Parser for GraphQL Cascade responses following the Cascade specification v1.1.
107///
108/// Extracts all affected entities from mutation responses to enable
109/// entity-level cache invalidation.
110#[derive(Debug, Clone)]
111pub struct CascadeResponseParser;
112
113impl CascadeResponseParser {
114 /// Create new cascade response parser.
115 #[must_use]
116 pub const fn new() -> Self {
117 Self
118 }
119
120 /// Parse cascade data from a GraphQL mutation response.
121 ///
122 /// # Arguments
123 ///
124 /// * `response` - The full GraphQL response containing cascade field
125 ///
126 /// # Returns
127 ///
128 /// `CascadeEntities` with all updated and deleted entities
129 ///
130 /// # Examples
131 ///
132 /// ```rust
133 /// use fraiseql_core::cache::CascadeResponseParser;
134 /// use serde_json::json;
135 /// # use fraiseql_core::error::Result;
136 /// # fn example() -> Result<()> {
137 /// let parser = CascadeResponseParser::new();
138 /// let response = json!({
139 /// "createPost": {
140 /// "cascade": {
141 /// "updated": [
142 /// { "__typename": "User", "id": "uuid-123" }
143 /// ]
144 /// }
145 /// }
146 /// });
147 ///
148 /// let entities = parser.parse_cascade_response(&response)?;
149 /// assert_eq!(entities.updated.len(), 1);
150 /// # Ok(())
151 /// # }
152 /// ```
153 ///
154 /// # Errors
155 ///
156 /// Returns [`FraiseQLError::Validation`] if the cascade field is present but
157 /// malformed (e.g., `updated` or `deleted` is not an array, or an entity is
158 /// missing `__typename` / `id`).
159 pub fn parse_cascade_response(&self, response: &Value) -> Result<CascadeEntities> {
160 // Find cascade field in response
161 let cascade = self.find_cascade_field(response)?;
162
163 if cascade.is_null() {
164 // No cascade data - return empty
165 return Ok(CascadeEntities {
166 updated: Vec::new(),
167 deleted: Vec::new(),
168 });
169 }
170
171 // Extract updated entities
172 let updated = self.extract_entities_list(&cascade, "updated")?;
173
174 // Extract deleted entities
175 let deleted = self.extract_entities_list(&cascade, "deleted")?;
176
177 Ok(CascadeEntities { updated, deleted })
178 }
179
180 /// Find cascade field in nested response structure.
181 ///
182 /// Cascade field can be at various depths:
183 /// - response.mutation { cascade { ... } }
184 /// - response.data.mutation { cascade { ... } }
185 /// - etc.
186 fn find_cascade_field(&self, response: &Value) -> Result<Value> {
187 // Try direct cascade field
188 if let Some(cascade) = response.get("cascade") {
189 return Ok(cascade.clone());
190 }
191
192 // Try nested in data
193 if let Some(data) = response.get("data") {
194 if let Some(cascade) = data.get("cascade") {
195 return Ok(cascade.clone());
196 }
197
198 // Try deeper nesting (mutation result contains cascade)
199 for (_key, value) in data.as_object().unwrap_or(&serde_json::Map::default()) {
200 if let Some(cascade) = value.get("cascade") {
201 return Ok(cascade.clone());
202 }
203 }
204 }
205
206 // Try top-level mutation response
207 for (_key, value) in response.as_object().unwrap_or(&serde_json::Map::default()) {
208 if let Some(cascade) = value.get("cascade") {
209 return Ok(cascade.clone());
210 }
211 }
212
213 // No cascade field found - return null (valid case: no side effects)
214 Ok(Value::Null)
215 }
216
217 /// Extract list of entities from cascade.updated or cascade.deleted.
218 fn extract_entities_list(&self, cascade: &Value, field_name: &str) -> Result<Vec<EntityKey>> {
219 let entities_array = match cascade.get(field_name) {
220 Some(Value::Array(arr)) => arr,
221 Some(Value::Null) | None => return Ok(Vec::new()),
222 Some(val) => {
223 return Err(FraiseQLError::Validation {
224 message: format!(
225 "cascade.{} should be array, got {}",
226 field_name,
227 match val {
228 Value::Object(_) => "object",
229 Value::String(_) => "string",
230 Value::Number(_) => "number",
231 Value::Bool(_) => "boolean",
232 Value::Null => "null",
233 Value::Array(_) => "unknown",
234 }
235 ),
236 path: Some(format!("cascade.{}", field_name)),
237 });
238 },
239 };
240
241 let mut entities = Vec::new();
242
243 for entity_obj in entities_array {
244 let entity = self.parse_cascade_entity(entity_obj)?;
245 entities.push(entity);
246 }
247
248 Ok(entities)
249 }
250
251 /// Parse a single entity from cascade.updated or cascade.deleted.
252 ///
253 /// Expects object with `__typename` and `id` fields.
254 fn parse_cascade_entity(&self, entity_obj: &Value) -> Result<EntityKey> {
255 let obj = entity_obj.as_object().ok_or_else(|| FraiseQLError::Validation {
256 message: "Cascade entity should be object".to_string(),
257 path: Some("cascade.updated[*]".to_string()),
258 })?;
259
260 // Extract __typename
261 let type_name = obj.get("__typename").and_then(Value::as_str).ok_or_else(|| {
262 FraiseQLError::Validation {
263 message: "Cascade entity missing __typename field".to_string(),
264 path: Some("cascade.updated[*].__typename".to_string()),
265 }
266 })?;
267
268 // Extract id
269 let entity_id =
270 obj.get("id").and_then(Value::as_str).ok_or_else(|| FraiseQLError::Validation {
271 message: "Cascade entity missing id field".to_string(),
272 path: Some("cascade.updated[*].id".to_string()),
273 })?;
274
275 EntityKey::new(type_name, entity_id)
276 }
277}
278
279impl Default for CascadeResponseParser {
280 fn default() -> Self {
281 Self::new()
282 }
283}