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