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