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//! ```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}