Skip to main content

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}