Skip to main content

fraiseql_cli/schema/
rich_filters.rs

1//! Rich scalar type filter compilation
2//!
3//! This module generates GraphQL WhereInput types, SQL templates, and validation rules
4//! for rich scalar types (EmailAddress, VIN, IBAN, etc.) detected in the schema.
5//!
6//! Flow:
7//! 1. Detect rich scalar types in schema
8//! 2. Look up operators for each type (from fraiseql-core)
9//! 3. Generate GraphQL WhereInput input types
10//! 4. Extract SQL templates from database handlers
11//! 5. Embed validation rules
12//! 6. Add to compiled schema
13
14use fraiseql_core::{
15    filters::{ParameterType, get_operators_for_type},
16    schema::{CompiledSchema, SecurityConfig},
17};
18
19use super::{lookup_data, sql_templates};
20
21/// Rich filter compilation configuration
22#[derive(Debug, Clone)]
23pub struct RichFilterConfig {
24    /// Enable rich filter compilation
25    pub enabled: bool,
26}
27
28impl Default for RichFilterConfig {
29    fn default() -> Self {
30        Self { enabled: true }
31    }
32}
33
34/// Compile rich filters: generate artifacts for rich scalar types
35///
36/// # Errors
37///
38/// Returns an error if serializing the lookup data or injecting filter types
39/// into the schema fails.
40pub fn compile_rich_filters(
41    schema: &mut CompiledSchema,
42    config: &RichFilterConfig,
43) -> anyhow::Result<()> {
44    if !config.enabled {
45        return Ok(());
46    }
47
48    // Build global lookup data (embedded in schema for runtime use)
49    let lookup_data_value = lookup_data::build_lookup_data();
50
51    // Get list of rich scalar type names from config or detect from schema
52    // For now, we'll detect them from operators module
53    let rich_types = get_all_rich_types();
54
55    // For each rich type, generate GraphQL WhereInput
56    for rich_type in rich_types {
57        if let Some(operators) = get_operators_for_type(&rich_type) {
58            // Generate the WhereInput type
59            let where_input = generate_where_input_type(&rich_type, &operators)?;
60
61            // Add to schema
62            schema.input_types.push(where_input);
63        }
64    }
65
66    // Store lookup data in the schema for runtime access
67    // This enables the server to perform lookups without external dependencies
68    if let Some(ref mut security_val) = schema.security {
69        security_val.additional.insert("lookup_data".to_string(), lookup_data_value);
70    } else {
71        let mut sec = SecurityConfig::default();
72        sec.additional.insert("lookup_data".to_string(), lookup_data_value);
73        schema.security = Some(sec);
74    }
75
76    Ok(())
77}
78
79/// Get all rich scalar type names
80fn get_all_rich_types() -> Vec<String> {
81    vec![
82        // Contact/Communication
83        "EmailAddress".to_string(),
84        "PhoneNumber".to_string(),
85        "URL".to_string(),
86        "DomainName".to_string(),
87        "Hostname".to_string(),
88        // Location/Address
89        "PostalCode".to_string(),
90        "Latitude".to_string(),
91        "Longitude".to_string(),
92        "Coordinates".to_string(),
93        "Timezone".to_string(),
94        "LocaleCode".to_string(),
95        "LanguageCode".to_string(),
96        "CountryCode".to_string(),
97        // Financial
98        "IBAN".to_string(),
99        "CUSIP".to_string(),
100        "ISIN".to_string(),
101        "SEDOL".to_string(),
102        "LEI".to_string(),
103        "MIC".to_string(),
104        "CurrencyCode".to_string(),
105        "Money".to_string(),
106        "ExchangeCode".to_string(),
107        "ExchangeRate".to_string(),
108        "StockSymbol".to_string(),
109        // Identifiers & Content
110        "Slug".to_string(),
111        "SemanticVersion".to_string(),
112        "HashSHA256".to_string(),
113        "APIKey".to_string(),
114        // Transportation & Logistics
115        "LicensePlate".to_string(),
116        "VIN".to_string(),
117        "TrackingNumber".to_string(),
118        "ContainerNumber".to_string(),
119        // Network & Geography
120        "IPAddress".to_string(),
121        "IPv4".to_string(),
122        "IPv6".to_string(),
123        "CIDR".to_string(),
124        "Port".to_string(),
125        "AirportCode".to_string(),
126        "PortCode".to_string(),
127        "FlightNumber".to_string(),
128        // Content Types
129        "Markdown".to_string(),
130        "HTML".to_string(),
131        "MimeType".to_string(),
132        "Color".to_string(),
133        "Image".to_string(),
134        "File".to_string(),
135        // Ranges & Measurements
136        "DateRange".to_string(),
137        "Duration".to_string(),
138        "Percentage".to_string(),
139    ]
140}
141
142/// Generate a GraphQL WhereInput type for a rich scalar type
143fn generate_where_input_type(
144    rich_type_name: &str,
145    operators: &[fraiseql_core::filters::OperatorInfo],
146) -> anyhow::Result<fraiseql_core::schema::InputObjectDefinition> {
147    use fraiseql_core::schema::{InputFieldDefinition, InputObjectDefinition};
148
149    let where_input_name = format!("{rich_type_name}WhereInput");
150
151    // Standard operators (always present)
152    let mut fields = vec![
153        InputFieldDefinition {
154            name:             "eq".to_string(),
155            field_type:       "String".to_string(),
156            description:      Some("Equals".to_string()),
157            default_value:    None,
158            deprecation:      None,
159            validation_rules: Vec::new(),
160        },
161        InputFieldDefinition {
162            name:             "neq".to_string(),
163            field_type:       "String".to_string(),
164            description:      Some("Not equals".to_string()),
165            default_value:    None,
166            deprecation:      None,
167            validation_rules: Vec::new(),
168        },
169        InputFieldDefinition {
170            name:             "in".to_string(),
171            field_type:       "[String!]!".to_string(),
172            description:      Some("In list".to_string()),
173            default_value:    None,
174            deprecation:      None,
175            validation_rules: Vec::new(),
176        },
177        InputFieldDefinition {
178            name:             "nin".to_string(),
179            field_type:       "[String!]!".to_string(),
180            description:      Some("Not in list".to_string()),
181            default_value:    None,
182            deprecation:      None,
183            validation_rules: Vec::new(),
184        },
185        InputFieldDefinition {
186            name:             "contains".to_string(),
187            field_type:       "String".to_string(),
188            description:      Some("Contains substring".to_string()),
189            default_value:    None,
190            deprecation:      None,
191            validation_rules: Vec::new(),
192        },
193        InputFieldDefinition {
194            name:             "isnull".to_string(),
195            field_type:       "Boolean".to_string(),
196            description:      Some("Is null".to_string()),
197            default_value:    None,
198            deprecation:      None,
199            validation_rules: Vec::new(),
200        },
201    ];
202
203    // Rich operators
204    let mut operator_names = Vec::new();
205    for op_info in operators {
206        let graphql_type = operator_param_type_to_graphql_string(op_info.parameter_type);
207        operator_names.push(op_info.graphql_name.clone());
208        fields.push(InputFieldDefinition {
209            name:             op_info.graphql_name.clone(),
210            field_type:       graphql_type,
211            description:      Some(op_info.description.clone()),
212            default_value:    None,
213            deprecation:      None,
214            validation_rules: Vec::new(),
215        });
216    }
217
218    // Build SQL template metadata for this rich type
219    let operator_refs: Vec<&str> = operator_names.iter().map(std::string::String::as_str).collect();
220    let sql_metadata = sql_templates::build_sql_templates_metadata(&operator_refs);
221
222    Ok(InputObjectDefinition {
223        name: where_input_name,
224        description: Some(format!("Filter operations for {rich_type_name}")),
225        fields,
226        metadata: Some(sql_metadata),
227    })
228}
229
230/// Convert parameter type to GraphQL type string
231fn operator_param_type_to_graphql_string(param_type: ParameterType) -> String {
232    match param_type {
233        ParameterType::String => "String".to_string(),
234        ParameterType::StringArray => "[String!]!".to_string(),
235        ParameterType::Number => "Float".to_string(),
236        ParameterType::NumberRange => "FloatRange".to_string(),
237        ParameterType::Boolean => "Boolean".to_string(),
238        // Reason: non_exhaustive requires catch-all for cross-crate matches
239        _ => "String".to_string(),
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_rich_types_list() {
249        let types = get_all_rich_types();
250        assert!(types.contains(&"EmailAddress".to_string()));
251        assert!(types.contains(&"VIN".to_string()));
252        assert!(types.contains(&"IBAN".to_string()));
253    }
254
255    #[test]
256    fn test_generate_where_input_name() {
257        let where_input_name = "EmailAddressWhereInput";
258        assert!(where_input_name.ends_with("WhereInput"));
259    }
260}