fraiseql_core/cache/cascade_metadata.rs
1//! Cascade metadata for mapping mutations to entity types.
2//!
3//! This module builds a mapping from mutation names to the entity types they modify,
4//! extracted from the compiled schema. This enables tracking which entities are affected
5//! by each mutation, critical for entity-level cache invalidation.
6//!
7//! # Architecture
8//!
9//! ```text
10//! Compiled Schema
11//! ┌──────────────────────────────────┐
12//! │ mutations: │
13//! │ - createUser: { return: User } │
14//! │ - updatePost: { return: Post } │
15//! └──────────┬───────────────────────┘
16//! │
17//! ↓ build_from_schema()
18//! ┌──────────────────────────────────┐
19//! │ CascadeMetadata: │
20//! │ "createUser" → "User" │
21//! │ "updatePost" → "Post" │
22//! └──────────────────────────────────┘
23//! ```
24//!
25//! # Examples
26//!
27//! ```ignore
28//! use fraiseql_core::cache::cascade_metadata::CascadeMetadata;
29//! use fraiseql_core::schema::CompiledSchema;
30//!
31//! let schema = CompiledSchema::from_file("schema.json")?;
32//! let metadata = CascadeMetadata::from_schema(&schema);
33//!
34//! assert_eq!(metadata.get_entity_type("createUser"), Some("User"));
35//! assert_eq!(metadata.get_entity_type("updatePost"), Some("Post"));
36//! ```
37
38use std::collections::HashMap;
39
40#[cfg(test)]
41use crate::schema::CompiledSchema;
42
43/// Maps mutation names to the entity types they modify.
44///
45/// Built from compiled schema, this metadata enables determining which entities
46/// are affected by each mutation operation.
47#[derive(Debug, Clone)]
48pub struct CascadeMetadata {
49 /// Mutation name → Entity type mapping
50 ///
51 /// Example: { "createUser": "User", "updatePost": "Post" }
52 mutation_entity_map: HashMap<String, String>,
53
54 /// Entity type → List of mutations affecting it
55 /// Useful for reverse lookups (which mutations affect "User"?)
56 entity_mutations_map: HashMap<String, Vec<String>>,
57}
58
59impl CascadeMetadata {
60 /// Create empty cascade metadata.
61 ///
62 /// Useful when building metadata programmatically or in tests.
63 #[must_use]
64 pub fn new() -> Self {
65 Self {
66 mutation_entity_map: HashMap::new(),
67 entity_mutations_map: HashMap::new(),
68 }
69 }
70
71 /// Add a mutation-to-entity mapping.
72 ///
73 /// # Arguments
74 ///
75 /// * `mutation_name` - Name of the mutation (e.g., "createUser")
76 /// * `entity_type` - Type of entity it modifies (e.g., "User")
77 pub fn add_mutation(&mut self, mutation_name: &str, entity_type: &str) {
78 let mutation_name = mutation_name.to_string();
79 let entity_type = entity_type.to_string();
80
81 self.mutation_entity_map.insert(mutation_name.clone(), entity_type.clone());
82
83 self.entity_mutations_map
84 .entry(entity_type)
85 .or_insert_with(Vec::new)
86 .push(mutation_name);
87 }
88
89 /// Get the entity type modified by a mutation.
90 ///
91 /// # Arguments
92 ///
93 /// * `mutation_name` - Name of the mutation
94 ///
95 /// # Returns
96 ///
97 /// - `Some(&str)` - Entity type if mutation is known
98 /// - `None` - If mutation is not in schema
99 ///
100 /// # Examples
101 ///
102 /// ```ignore
103 /// let entity = metadata.get_entity_type("createUser");
104 /// assert_eq!(entity, Some("User"));
105 /// ```
106 #[must_use]
107 pub fn get_entity_type(&self, mutation_name: &str) -> Option<&str> {
108 self.mutation_entity_map.get(mutation_name).map(|s| s.as_str())
109 }
110
111 /// Get all mutations affecting a specific entity type.
112 ///
113 /// Useful for finding all caches that might be affected by changes to an entity type.
114 ///
115 /// # Arguments
116 ///
117 /// * `entity_type` - Type of entity to query
118 ///
119 /// # Returns
120 ///
121 /// List of mutation names affecting this entity, or empty list if none
122 #[must_use]
123 pub fn get_mutations_for_entity(&self, entity_type: &str) -> Vec<String> {
124 self.entity_mutations_map.get(entity_type).cloned().unwrap_or_default()
125 }
126
127 /// Get total number of mutation-entity mappings.
128 #[must_use]
129 pub fn count(&self) -> usize {
130 self.mutation_entity_map.len()
131 }
132
133 /// Check if metadata contains a mutation.
134 #[must_use]
135 pub fn contains_mutation(&self, mutation_name: &str) -> bool {
136 self.mutation_entity_map.contains_key(mutation_name)
137 }
138
139 #[cfg(test)]
140 /// Build metadata from a compiled schema (for testing).
141 ///
142 /// In production, this would be called during server initialization
143 /// to extract all mutations and their return types from the compiled schema.
144 pub fn from_schema(_schema: &CompiledSchema) -> Self {
145 // In a real implementation, this would:
146 // 1. Extract mutations from schema.mutations()
147 // 2. For each mutation, extract its return_type
148 // 3. Map return_type to entity name
149 //
150 // For now, return empty - tests will build metadata manually
151 Self::new()
152 }
153}
154
155impl Default for CascadeMetadata {
156 fn default() -> Self {
157 Self::new()
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn test_build_from_mutations() {
167 let mut metadata = CascadeMetadata::new();
168 metadata.add_mutation("createUser", "User");
169 metadata.add_mutation("updateUser", "User");
170 metadata.add_mutation("deleteUser", "User");
171
172 assert_eq!(metadata.count(), 3);
173 }
174
175 #[test]
176 fn test_map_mutation_to_entity_type() {
177 let mut metadata = CascadeMetadata::new();
178 metadata.add_mutation("createUser", "User");
179 metadata.add_mutation("createPost", "Post");
180
181 assert_eq!(metadata.get_entity_type("createUser"), Some("User"));
182 assert_eq!(metadata.get_entity_type("createPost"), Some("Post"));
183 }
184
185 #[test]
186 fn test_handle_unknown_mutation() {
187 let metadata = CascadeMetadata::new();
188 assert_eq!(metadata.get_entity_type("unknownMutation"), None);
189 }
190
191 #[test]
192 fn test_multiple_mutations_same_entity() {
193 let mut metadata = CascadeMetadata::new();
194 metadata.add_mutation("createUser", "User");
195 metadata.add_mutation("updateUser", "User");
196 metadata.add_mutation("deleteUser", "User");
197
198 let mutations = metadata.get_mutations_for_entity("User");
199 assert_eq!(mutations.len(), 3);
200 assert!(mutations.contains(&"createUser".to_string()));
201 assert!(mutations.contains(&"updateUser".to_string()));
202 assert!(mutations.contains(&"deleteUser".to_string()));
203 }
204
205 #[test]
206 fn test_contains_mutation() {
207 let mut metadata = CascadeMetadata::new();
208 metadata.add_mutation("createUser", "User");
209
210 assert!(metadata.contains_mutation("createUser"));
211 assert!(!metadata.contains_mutation("unknownMutation"));
212 }
213}