shaperail_runtime/db/
filter.rs1use std::collections::HashMap;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct FilterParam {
6 pub field: String,
8 pub value: String,
10}
11
12#[derive(Debug, Clone, Default)]
14pub struct FilterSet {
15 pub filters: Vec<FilterParam>,
16}
17
18impl FilterSet {
19 pub fn from_query_params(params: &HashMap<String, String>, allowed_fields: &[String]) -> Self {
24 let mut filters = Vec::new();
25 for (key, value) in params {
26 if let Some(field) = key
27 .strip_prefix("filter[")
28 .and_then(|s| s.strip_suffix(']'))
29 {
30 if allowed_fields.iter().any(|f| f == field) {
31 filters.push(FilterParam {
32 field: field.to_string(),
33 value: value.clone(),
34 });
35 }
36 }
37 }
38 FilterSet { filters }
39 }
40
41 pub fn is_empty(&self) -> bool {
43 self.filters.is_empty()
44 }
45
46 pub fn apply_to_sql(&self, sql: &mut String, has_where: bool, param_offset: usize) -> usize {
51 let mut offset = param_offset;
52 for (i, filter) in self.filters.iter().enumerate() {
53 if i == 0 && !has_where {
54 sql.push_str(" WHERE ");
55 } else {
56 sql.push_str(" AND ");
57 }
58 sql.push_str(&format!("\"{}\" = ${}", filter.field, offset));
59 offset += 1;
60 }
61 offset
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn parse_filter_params() {
71 let mut params = HashMap::new();
72 params.insert("filter[role]".to_string(), "admin".to_string());
73 params.insert("filter[org_id]".to_string(), "abc-123".to_string());
74 params.insert("other_param".to_string(), "ignored".to_string());
75 params.insert("filter[secret]".to_string(), "blocked".to_string());
76
77 let allowed = vec!["role".to_string(), "org_id".to_string()];
78 let fs = FilterSet::from_query_params(¶ms, &allowed);
79
80 assert_eq!(fs.filters.len(), 2);
81 assert!(fs
82 .filters
83 .iter()
84 .any(|f| f.field == "role" && f.value == "admin"));
85 assert!(fs
86 .filters
87 .iter()
88 .any(|f| f.field == "org_id" && f.value == "abc-123"));
89 }
90
91 #[test]
92 fn filter_disallowed_fields_ignored() {
93 let mut params = HashMap::new();
94 params.insert("filter[secret]".to_string(), "value".to_string());
95
96 let allowed: Vec<String> = vec!["role".to_string()];
97 let fs = FilterSet::from_query_params(¶ms, &allowed);
98
99 assert!(fs.is_empty());
100 }
101
102 #[test]
103 fn apply_to_sql_no_existing_where() {
104 let fs = FilterSet {
105 filters: vec![
106 FilterParam {
107 field: "role".to_string(),
108 value: "admin".to_string(),
109 },
110 FilterParam {
111 field: "org_id".to_string(),
112 value: "abc".to_string(),
113 },
114 ],
115 };
116
117 let mut sql = "SELECT * FROM users".to_string();
118 let offset = fs.apply_to_sql(&mut sql, false, 1);
119
120 assert_eq!(
121 sql,
122 "SELECT * FROM users WHERE \"role\" = $1 AND \"org_id\" = $2"
123 );
124 assert_eq!(offset, 3);
125 }
126
127 #[test]
128 fn apply_to_sql_with_existing_where() {
129 let fs = FilterSet {
130 filters: vec![FilterParam {
131 field: "role".to_string(),
132 value: "admin".to_string(),
133 }],
134 };
135
136 let mut sql = "SELECT * FROM users WHERE \"deleted_at\" IS NULL".to_string();
137 let offset = fs.apply_to_sql(&mut sql, true, 1);
138
139 assert_eq!(
140 sql,
141 "SELECT * FROM users WHERE \"deleted_at\" IS NULL AND \"role\" = $1"
142 );
143 assert_eq!(offset, 2);
144 }
145
146 #[test]
147 fn empty_filter_set() {
148 let fs = FilterSet::default();
149 assert!(fs.is_empty());
150
151 let mut sql = "SELECT * FROM users".to_string();
152 let offset = fs.apply_to_sql(&mut sql, false, 1);
153 assert_eq!(sql, "SELECT * FROM users");
154 assert_eq!(offset, 1);
155 }
156}