1use 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#[derive(Debug, Clone)]
26pub struct RichFilterConfig {
27 pub enabled: bool,
29 #[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
44pub fn compile_rich_filters(
46 schema: &mut CompiledSchema,
47 config: &RichFilterConfig,
48) -> anyhow::Result<()> {
49 if !config.enabled {
50 return Ok(());
51 }
52
53 let lookup_data_value = lookup_data::build_lookup_data();
55
56 let rich_types = get_all_rich_types();
59
60 for rich_type in rich_types {
62 if let Some(operators) = get_operators_for_type(&rich_type) {
63 let where_input = generate_where_input_type(&rich_type, &operators)?;
65
66 schema.input_types.push(where_input);
68 }
69 }
70
71 if let Some(ref mut security_val) = schema.security {
74 if let Some(obj) = security_val.as_object_mut() {
76 obj.insert("lookup_data".to_string(), lookup_data_value);
77 }
78 } else {
79 schema.security = Some(json!({
81 "lookup_data": lookup_data_value
82 }));
83 }
84
85 Ok(())
86}
87
88fn get_all_rich_types() -> Vec<String> {
90 vec![
91 "EmailAddress".to_string(),
93 "PhoneNumber".to_string(),
94 "URL".to_string(),
95 "DomainName".to_string(),
96 "Hostname".to_string(),
97 "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 "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 "Slug".to_string(),
120 "SemanticVersion".to_string(),
121 "HashSHA256".to_string(),
122 "APIKey".to_string(),
123 "LicensePlate".to_string(),
125 "VIN".to_string(),
126 "TrackingNumber".to_string(),
127 "ContainerNumber".to_string(),
128 "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 "Markdown".to_string(),
139 "HTML".to_string(),
140 "MimeType".to_string(),
141 "Color".to_string(),
142 "Image".to_string(),
143 "File".to_string(),
144 "DateRange".to_string(),
146 "Duration".to_string(),
147 "Percentage".to_string(),
148 ]
149}
150
151fn 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 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 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 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
239fn 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}