Skip to main content

fraiseql_core/schema/compiled/
schema_lookup.rs

1//! Lookup and indexing methods for [`CompiledSchema`].
2//!
3//! All `find_*` methods, `build_indexes`, `display_name`, and `operation_count`.
4
5use super::{
6    directive::DirectiveDefinition, mutation::MutationDefinition, query::QueryDefinition,
7    schema::CompiledSchema,
8};
9use crate::schema::{
10    config_types::NamingConvention,
11    graphql_type_defs::{
12        EnumDefinition, InputObjectDefinition, InterfaceDefinition, TypeDefinition, UnionDefinition,
13    },
14    subscription_types::SubscriptionDefinition,
15};
16
17impl CompiledSchema {
18    /// Build O(1) lookup indexes for queries, mutations, and subscriptions.
19    ///
20    /// Called automatically by `from_json()`. Must be called manually after any
21    /// direct mutation of `self.queries`, `self.mutations`, or `self.subscriptions`.
22    pub fn build_indexes(&mut self) {
23        let camel = matches!(self.naming_convention, NamingConvention::CamelCase);
24
25        self.query_index = self
26            .queries
27            .iter()
28            .enumerate()
29            .flat_map(|(i, q)| {
30                let mut entries = vec![(q.name.clone(), i)];
31                if camel {
32                    let converted = crate::utils::casing::to_camel_case(&q.name);
33                    if converted != q.name {
34                        entries.push((converted, i));
35                    }
36                }
37                entries
38            })
39            .collect();
40
41        self.mutation_index = self
42            .mutations
43            .iter()
44            .enumerate()
45            .flat_map(|(i, m)| {
46                let mut entries = vec![(m.name.clone(), i)];
47                if camel {
48                    let converted = crate::utils::casing::to_camel_case(&m.name);
49                    if converted != m.name {
50                        entries.push((converted, i));
51                    }
52                }
53                entries
54            })
55            .collect();
56
57        self.subscription_index = self
58            .subscriptions
59            .iter()
60            .enumerate()
61            .flat_map(|(i, s)| {
62                let mut entries = vec![(s.name.clone(), i)];
63                if camel {
64                    let converted = crate::utils::casing::to_camel_case(&s.name);
65                    if converted != s.name {
66                        entries.push((converted, i));
67                    }
68                }
69                entries
70            })
71            .collect();
72    }
73
74    /// Return the display name for an operation, applying the naming convention.
75    ///
76    /// When `naming_convention` is `CamelCase`, converts `snake_case` names to
77    /// `camelCase` (e.g., `create_dns_server` → `createDnsServer`).
78    /// When `Preserve`, returns the name unchanged.
79    #[must_use]
80    pub fn display_name(&self, name: &str) -> String {
81        match self.naming_convention {
82            NamingConvention::CamelCase => crate::utils::casing::to_camel_case(name),
83            NamingConvention::Preserve => name.to_string(),
84        }
85    }
86
87    /// Find a type definition by name.
88    #[must_use]
89    pub fn find_type(&self, name: &str) -> Option<&TypeDefinition> {
90        self.types.iter().find(|t| t.name == name)
91    }
92
93    /// Whether the named type has any policy-gated field
94    /// ([`FieldDefinition::authorize`](crate::schema::FieldDefinition)).
95    ///
96    /// Used by projection paths that do not (yet) run the dynamic field authorizer
97    /// to fail closed conservatively when a gated type could be projected (#423).
98    #[must_use]
99    pub fn type_has_gated_field(&self, type_name: &str) -> bool {
100        self.find_type(type_name).is_some_and(|t| t.fields.iter().any(|f| f.authorize))
101    }
102
103    /// Whether any object type in the schema declares a policy-gated field (#423).
104    ///
105    /// Used by projection paths with a dynamic/unknown result type (Relay `node`,
106    /// federation `_entities`) to fail closed when field-level authorization is in
107    /// use but cannot yet be enforced on that path.
108    #[must_use]
109    pub fn has_any_authorize_field(&self) -> bool {
110        self.types.iter().any(|t| t.fields.iter().any(|f| f.authorize))
111    }
112
113    /// Find an enum definition by name.
114    #[must_use]
115    pub fn find_enum(&self, name: &str) -> Option<&EnumDefinition> {
116        self.enums.iter().find(|e| e.name == name)
117    }
118
119    /// Find an input object definition by name.
120    #[must_use]
121    pub fn find_input_type(&self, name: &str) -> Option<&InputObjectDefinition> {
122        self.input_types.iter().find(|i| i.name == name)
123    }
124
125    /// Find an interface definition by name.
126    #[must_use]
127    pub fn find_interface(&self, name: &str) -> Option<&InterfaceDefinition> {
128        self.interfaces.iter().find(|i| i.name == name)
129    }
130
131    /// Find all types that implement a given interface.
132    #[must_use]
133    pub fn find_implementors(&self, interface_name: &str) -> Vec<&TypeDefinition> {
134        self.types
135            .iter()
136            .filter(|t| t.implements.contains(&interface_name.to_string()))
137            .collect()
138    }
139
140    /// Find a union definition by name.
141    #[must_use]
142    pub fn find_union(&self, name: &str) -> Option<&UnionDefinition> {
143        self.unions.iter().find(|u| u.name == name)
144    }
145
146    /// Find a query definition by name.
147    ///
148    /// Uses the O(1) pre-built index when available; falls back to O(n) linear
149    /// scan for schemas built directly in tests without calling `build_indexes()`.
150    ///
151    /// If the exact name is not found, retries with `to_snake_case(name)` to
152    /// handle camelCase → `snake_case` normalization (e.g. `dnsServers` →
153    /// `dns_servers`). This supports schemas compiled before the SDK camelCase
154    /// migration.
155    #[must_use]
156    pub fn find_query(&self, name: &str) -> Option<&QueryDefinition> {
157        if self.query_index.is_empty() && !self.queries.is_empty() {
158            self.queries.iter().find(|q| q.name == name).or_else(|| {
159                let snake = crate::utils::casing::to_snake_case(name);
160                self.queries.iter().find(|q| q.name == snake)
161            })
162        } else {
163            self.query_index
164                .get(name)
165                .or_else(|| self.query_index.get(&crate::utils::casing::to_snake_case(name)))
166                .map(|&i| &self.queries[i])
167        }
168    }
169
170    /// Find a mutation definition by name.
171    ///
172    /// Uses the O(1) pre-built index when available; falls back to O(n) linear
173    /// scan for schemas built directly in tests without calling `build_indexes()`.
174    ///
175    /// If the exact name is not found, retries with `to_snake_case(name)` to
176    /// handle camelCase → `snake_case` normalization. This supports schemas
177    /// compiled before the SDK camelCase migration.
178    #[must_use]
179    pub fn find_mutation(&self, name: &str) -> Option<&MutationDefinition> {
180        if self.mutation_index.is_empty() && !self.mutations.is_empty() {
181            self.mutations.iter().find(|m| m.name == name).or_else(|| {
182                let snake = crate::utils::casing::to_snake_case(name);
183                self.mutations.iter().find(|m| m.name == snake)
184            })
185        } else {
186            self.mutation_index
187                .get(name)
188                .or_else(|| self.mutation_index.get(&crate::utils::casing::to_snake_case(name)))
189                .map(|&i| &self.mutations[i])
190        }
191    }
192
193    /// Find a subscription definition by name.
194    ///
195    /// Uses the O(1) pre-built index when available; falls back to O(n) linear
196    /// scan for schemas built directly in tests without calling `build_indexes()`.
197    ///
198    /// If the exact name is not found, retries with `to_snake_case(name)` to
199    /// handle camelCase → `snake_case` normalization. This supports schemas
200    /// compiled before the SDK camelCase migration.
201    #[must_use]
202    pub fn find_subscription(&self, name: &str) -> Option<&SubscriptionDefinition> {
203        if self.subscription_index.is_empty() && !self.subscriptions.is_empty() {
204            self.subscriptions.iter().find(|s| s.name == name).or_else(|| {
205                let snake = crate::utils::casing::to_snake_case(name);
206                self.subscriptions.iter().find(|s| s.name == snake)
207            })
208        } else {
209            self.subscription_index
210                .get(name)
211                .or_else(|| self.subscription_index.get(&crate::utils::casing::to_snake_case(name)))
212                .map(|&i| &self.subscriptions[i])
213        }
214    }
215
216    /// Find a custom directive definition by name.
217    #[must_use]
218    pub fn find_directive(&self, name: &str) -> Option<&DirectiveDefinition> {
219        self.directives.iter().find(|d| d.name == name)
220    }
221
222    /// Get total number of operations (queries + mutations + subscriptions).
223    #[must_use]
224    pub const fn operation_count(&self) -> usize {
225        self.queries.len() + self.mutations.len() + self.subscriptions.len()
226    }
227}