Skip to main content

fraiseql_core/schema/dependency_graph/
builder.rs

1//! Graph construction: builds the dependency graph from a compiled schema.
2
3use std::collections::{HashMap, HashSet};
4
5use super::graph::SchemaDependencyGraph;
6use crate::schema::{CompiledSchema, FieldType};
7
8impl SchemaDependencyGraph {
9    /// Build a dependency graph from a compiled schema.
10    ///
11    /// This analyzes all types, queries, mutations, and subscriptions to
12    /// build a complete dependency graph.
13    #[must_use]
14    #[allow(clippy::cognitive_complexity)] // Reason: iterates all schema elements (types, queries, mutations, subscriptions) to build a complete dependency graph
15    pub fn build(schema: &CompiledSchema) -> Self {
16        let mut outgoing: HashMap<String, HashSet<String>> = HashMap::new();
17        let mut incoming: HashMap<String, HashSet<String>> = HashMap::new();
18        let mut all_types: HashSet<String> = HashSet::new();
19        let mut root_types: HashSet<String> = HashSet::new();
20
21        // Collect all type names first
22        for type_def in &schema.types {
23            all_types.insert(type_def.name.to_string());
24            outgoing.entry(type_def.name.to_string()).or_default();
25            incoming.entry(type_def.name.to_string()).or_default();
26        }
27
28        for enum_def in &schema.enums {
29            all_types.insert(enum_def.name.clone());
30            outgoing.entry(enum_def.name.clone()).or_default();
31            incoming.entry(enum_def.name.clone()).or_default();
32        }
33
34        for input_def in &schema.input_types {
35            all_types.insert(input_def.name.clone());
36            outgoing.entry(input_def.name.clone()).or_default();
37            incoming.entry(input_def.name.clone()).or_default();
38        }
39
40        for interface_def in &schema.interfaces {
41            all_types.insert(interface_def.name.clone());
42            outgoing.entry(interface_def.name.clone()).or_default();
43            incoming.entry(interface_def.name.clone()).or_default();
44        }
45
46        for union_def in &schema.unions {
47            all_types.insert(union_def.name.clone());
48            outgoing.entry(union_def.name.clone()).or_default();
49            incoming.entry(union_def.name.clone()).or_default();
50        }
51
52        // Add virtual root types for operations
53        if !schema.queries.is_empty() {
54            root_types.insert("Query".to_string());
55            all_types.insert("Query".to_string());
56            outgoing.entry("Query".to_string()).or_default();
57            incoming.entry("Query".to_string()).or_default();
58        }
59        if !schema.mutations.is_empty() {
60            root_types.insert("Mutation".to_string());
61            all_types.insert("Mutation".to_string());
62            outgoing.entry("Mutation".to_string()).or_default();
63            incoming.entry("Mutation".to_string()).or_default();
64        }
65        if !schema.subscriptions.is_empty() {
66            root_types.insert("Subscription".to_string());
67            all_types.insert("Subscription".to_string());
68            outgoing.entry("Subscription".to_string()).or_default();
69            incoming.entry("Subscription".to_string()).or_default();
70        }
71
72        // Build dependencies for object types
73        for type_def in &schema.types {
74            for field in &type_def.fields {
75                if let Some(ref_type) = Self::extract_referenced_type(&field.field_type) {
76                    if all_types.contains(&ref_type) {
77                        outgoing
78                            .entry(type_def.name.to_string())
79                            .or_default()
80                            .insert(ref_type.clone());
81                        incoming
82                            .entry(ref_type.clone())
83                            .or_default()
84                            .insert(type_def.name.to_string());
85                    }
86                }
87            }
88
89            // Track interface implementations
90            for interface_name in &type_def.implements {
91                if all_types.contains(interface_name) {
92                    outgoing
93                        .entry(type_def.name.to_string())
94                        .or_default()
95                        .insert(interface_name.clone());
96                    incoming
97                        .entry(interface_name.clone())
98                        .or_default()
99                        .insert(type_def.name.to_string());
100                }
101            }
102        }
103
104        // Build dependencies for interfaces
105        for interface_def in &schema.interfaces {
106            for field in &interface_def.fields {
107                if let Some(ref_type) = Self::extract_referenced_type(&field.field_type) {
108                    if all_types.contains(&ref_type) {
109                        outgoing
110                            .entry(interface_def.name.clone())
111                            .or_default()
112                            .insert(ref_type.clone());
113                        incoming
114                            .entry(ref_type.clone())
115                            .or_default()
116                            .insert(interface_def.name.clone());
117                    }
118                }
119            }
120        }
121
122        // Build dependencies for unions
123        for union_def in &schema.unions {
124            for member_type in &union_def.member_types {
125                if all_types.contains(member_type) {
126                    outgoing.entry(union_def.name.clone()).or_default().insert(member_type.clone());
127                    incoming.entry(member_type.clone()).or_default().insert(union_def.name.clone());
128                }
129            }
130        }
131
132        // Build dependencies for input types (they can reference other input types)
133        for input_def in &schema.input_types {
134            for field in &input_def.fields {
135                // Parse the field_type string to extract type references
136                let parsed = FieldType::parse(&field.field_type);
137                if let Some(ref_type) = Self::extract_referenced_type(&parsed) {
138                    if all_types.contains(&ref_type) {
139                        outgoing
140                            .entry(input_def.name.clone())
141                            .or_default()
142                            .insert(ref_type.clone());
143                        incoming
144                            .entry(ref_type.clone())
145                            .or_default()
146                            .insert(input_def.name.clone());
147                    }
148                }
149            }
150        }
151
152        // Build dependencies from queries to their return types
153        for query in &schema.queries {
154            let parsed = FieldType::parse(&query.return_type);
155            if let Some(ref_type) = Self::extract_referenced_type(&parsed) {
156                if all_types.contains(&ref_type) {
157                    outgoing.entry("Query".to_string()).or_default().insert(ref_type.clone());
158                    incoming.entry(ref_type.clone()).or_default().insert("Query".to_string());
159                }
160            }
161        }
162
163        // Build dependencies from mutations to their return types
164        for mutation in &schema.mutations {
165            let parsed = FieldType::parse(&mutation.return_type);
166            if let Some(ref_type) = Self::extract_referenced_type(&parsed) {
167                if all_types.contains(&ref_type) {
168                    outgoing.entry("Mutation".to_string()).or_default().insert(ref_type.clone());
169                    incoming.entry(ref_type.clone()).or_default().insert("Mutation".to_string());
170                }
171            }
172        }
173
174        // Build dependencies from subscriptions to their return types
175        for subscription in &schema.subscriptions {
176            let parsed = FieldType::parse(&subscription.return_type);
177            if let Some(ref_type) = Self::extract_referenced_type(&parsed) {
178                if all_types.contains(&ref_type) {
179                    outgoing
180                        .entry("Subscription".to_string())
181                        .or_default()
182                        .insert(ref_type.clone());
183                    incoming
184                        .entry(ref_type.clone())
185                        .or_default()
186                        .insert("Subscription".to_string());
187                }
188            }
189        }
190
191        Self {
192            outgoing,
193            incoming,
194            all_types,
195            root_types,
196        }
197    }
198
199    /// Extract the referenced type name from a `FieldType`, recursively unwrapping lists.
200    pub(super) fn extract_referenced_type(field_type: &FieldType) -> Option<String> {
201        match field_type {
202            FieldType::Object(name)
203            | FieldType::Enum(name)
204            | FieldType::Input(name)
205            | FieldType::Interface(name)
206            | FieldType::Union(name) => Some(name.clone()),
207            FieldType::List(inner) => Self::extract_referenced_type(inner),
208            _ => None, // Scalars don't create dependencies
209        }
210    }
211}