1use fraiseql_core::{
15 filters::{ParameterType, get_operators_for_type},
16 schema::{CompiledSchema, SecurityConfig},
17};
18
19use super::{lookup_data, sql_templates};
20
21#[derive(Debug, Clone)]
23pub struct RichFilterConfig {
24 pub enabled: bool,
26}
27
28impl Default for RichFilterConfig {
29 fn default() -> Self {
30 Self { enabled: true }
31 }
32}
33
34pub fn compile_rich_filters(
41 schema: &mut CompiledSchema,
42 config: &RichFilterConfig,
43) -> anyhow::Result<()> {
44 if !config.enabled {
45 return Ok(());
46 }
47
48 let lookup_data_value = lookup_data::build_lookup_data();
50
51 let rich_types = get_all_rich_types();
54
55 for rich_type in rich_types {
57 if let Some(operators) = get_operators_for_type(&rich_type) {
58 let where_input = generate_where_input_type(&rich_type, &operators)?;
60
61 schema.input_types.push(where_input);
63 }
64 }
65
66 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
79fn get_all_rich_types() -> Vec<String> {
81 vec![
82 "EmailAddress".to_string(),
84 "PhoneNumber".to_string(),
85 "URL".to_string(),
86 "DomainName".to_string(),
87 "Hostname".to_string(),
88 "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 "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 "Slug".to_string(),
111 "SemanticVersion".to_string(),
112 "HashSHA256".to_string(),
113 "APIKey".to_string(),
114 "LicensePlate".to_string(),
116 "VIN".to_string(),
117 "TrackingNumber".to_string(),
118 "ContainerNumber".to_string(),
119 "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 "Markdown".to_string(),
130 "HTML".to_string(),
131 "MimeType".to_string(),
132 "Color".to_string(),
133 "Image".to_string(),
134 "File".to_string(),
135 "DateRange".to_string(),
137 "Duration".to_string(),
138 "Percentage".to_string(),
139 ]
140}
141
142fn 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 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 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 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
230fn 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 _ => "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}