1use serde::{Deserialize, Serialize};
2
3#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
5#[serde(rename_all = "lowercase")]
6pub enum FilterOperator {
7 Eq,
9 Ne,
11 Gt,
13 Lt,
15 Gte,
17 Lte,
19 Like,
21 ILike,
23 In,
25 NotIn,
27 IsNull,
29 IsNotNull,
31 Between,
33 Contains,
35}
36
37#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
39#[serde(untagged)]
40pub enum FilterValue {
41 String(String),
43 Int(i64),
45 Float(f64),
47 Bool(bool),
49 Array(Vec<FilterValue>),
51 Null,
53}
54
55impl FilterValue {
56 pub fn to_sql_string(&self) -> String {
58 match self {
59 FilterValue::String(s) => format!("'{}'", s.replace('\'', "''")),
60 FilterValue::Int(i) => i.to_string(),
61 FilterValue::Float(f) => f.to_string(),
62 FilterValue::Bool(b) => if *b { "TRUE" } else { "FALSE" }.to_string(),
63 FilterValue::Array(arr) => {
64 let items: Vec<String> = arr.iter().map(|v| v.to_sql_string()).collect();
65 format!("({})", items.join(", "))
66 }
67 FilterValue::Null => "NULL".to_string(),
68 }
69 }
70}
71
72#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
74pub struct Filter {
75 pub field: String,
77 pub operator: FilterOperator,
79 pub value: FilterValue,
81}
82
83impl Filter {
84 pub fn new(field: impl Into<String>, operator: FilterOperator, value: FilterValue) -> Self {
86 Self {
87 field: field.into(),
88 operator,
89 value,
90 }
91 }
92
93 pub fn to_sql_where(&self) -> String {
95 match &self.operator {
96 FilterOperator::Eq => format!("{} = {}", self.field, self.value.to_sql_string()),
97 FilterOperator::Ne => format!("{} != {}", self.field, self.value.to_sql_string()),
98 FilterOperator::Gt => format!("{} > {}", self.field, self.value.to_sql_string()),
99 FilterOperator::Lt => format!("{} < {}", self.field, self.value.to_sql_string()),
100 FilterOperator::Gte => format!("{} >= {}", self.field, self.value.to_sql_string()),
101 FilterOperator::Lte => format!("{} <= {}", self.field, self.value.to_sql_string()),
102 FilterOperator::Like => format!("{} LIKE {}", self.field, self.value.to_sql_string()),
103 FilterOperator::ILike => format!("{} ILIKE {}", self.field, self.value.to_sql_string()),
104 FilterOperator::In => format!("{} IN {}", self.field, self.value.to_sql_string()),
105 FilterOperator::NotIn => format!("{} NOT IN {}", self.field, self.value.to_sql_string()),
106 FilterOperator::IsNull => format!("{} IS NULL", self.field),
107 FilterOperator::IsNotNull => format!("{} IS NOT NULL", self.field),
108 FilterOperator::Between => {
109 if let FilterValue::Array(arr) = &self.value {
110 if arr.len() == 2 {
111 return format!(
112 "{} BETWEEN {} AND {}",
113 self.field,
114 arr[0].to_sql_string(),
115 arr[1].to_sql_string()
116 );
117 }
118 }
119 format!("{} = {}", self.field, self.value.to_sql_string())
120 }
121 FilterOperator::Contains => {
122 format!("{} @> {}", self.field, self.value.to_sql_string())
123 }
124 }
125 }
126
127 pub fn to_surrealql_where(&self) -> String {
129 match &self.operator {
130 FilterOperator::Eq => format!("{} = {}", self.field, self.value.to_sql_string()),
131 FilterOperator::Ne => format!("{} != {}", self.field, self.value.to_sql_string()),
132 FilterOperator::Gt => format!("{} > {}", self.field, self.value.to_sql_string()),
133 FilterOperator::Lt => format!("{} < {}", self.field, self.value.to_sql_string()),
134 FilterOperator::Gte => format!("{} >= {}", self.field, self.value.to_sql_string()),
135 FilterOperator::Lte => format!("{} <= {}", self.field, self.value.to_sql_string()),
136 FilterOperator::Like | FilterOperator::ILike => {
137 format!("{} ~ {}", self.field, self.value.to_sql_string())
138 }
139 FilterOperator::In => format!("{} INSIDE {}", self.field, self.value.to_sql_string()),
140 FilterOperator::NotIn => format!("{} NOT INSIDE {}", self.field, self.value.to_sql_string()),
141 FilterOperator::IsNull => format!("{} IS NULL", self.field),
142 FilterOperator::IsNotNull => format!("{} IS NOT NULL", self.field),
143 FilterOperator::Between => {
144 if let FilterValue::Array(arr) = &self.value {
145 if arr.len() == 2 {
146 return format!(
147 "{} >= {} AND {} <= {}",
148 self.field,
149 arr[0].to_sql_string(),
150 self.field,
151 arr[1].to_sql_string()
152 );
153 }
154 }
155 format!("{} = {}", self.field, self.value.to_sql_string())
156 }
157 FilterOperator::Contains => {
158 format!("{} CONTAINS {}", self.field, self.value.to_sql_string())
159 }
160 }
161 }
162}
163
164#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
166pub struct SearchParams {
167 pub query: String,
169 pub fields: Vec<String>,
171 #[serde(default)]
173 pub case_sensitive: bool,
174 #[serde(default)]
176 pub exact_match: bool,
177}
178
179impl SearchParams {
180 pub fn new(query: impl Into<String>, fields: Vec<String>) -> Self {
182 Self {
183 query: query.into(),
184 fields,
185 case_sensitive: false,
186 exact_match: false,
187 }
188 }
189
190 pub fn with_case_sensitive(mut self, sensitive: bool) -> Self {
192 self.case_sensitive = sensitive;
193 self
194 }
195
196 pub fn with_exact_match(mut self, exact: bool) -> Self {
198 self.exact_match = exact;
199 self
200 }
201
202 pub fn to_sql_where(&self) -> String {
204 let pattern = if self.exact_match {
205 format!("'{}'", self.query.replace('\'', "''"))
206 } else {
207 format!("'%{}%'", self.query.replace('\'', "''"))
208 };
209
210 let operator = if self.case_sensitive {
211 "LIKE"
212 } else {
213 "ILIKE" };
215
216 let conditions: Vec<String> = self
217 .fields
218 .iter()
219 .map(|field| {
220 if self.case_sensitive || operator == "ILIKE" {
221 format!("{} {} {}", field, operator, pattern)
222 } else {
223 format!("LOWER({}) LIKE LOWER({})", field, pattern)
224 }
225 })
226 .collect();
227
228 format!("({})", conditions.join(" OR "))
229 }
230}
231
232#[derive(Clone, Debug, Serialize, Deserialize)]
234pub struct PaginationParams {
235 pub page: u32,
237 pub per_page: u32,
239 pub sort_by: Option<String>,
241 pub sort_direction: Option<SortDirection>,
243 #[serde(default)]
245 pub filters: Vec<Filter>,
246 pub search: Option<SearchParams>,
248}
249
250#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
252#[serde(rename_all = "lowercase")]
253pub enum SortDirection {
254 Asc,
255 Desc,
256}
257
258impl Default for PaginationParams {
259 fn default() -> Self {
260 Self {
261 page: 1,
262 per_page: 20,
263 sort_by: None,
264 sort_direction: None,
265 filters: Vec::new(),
266 search: None,
267 }
268 }
269}
270
271impl PaginationParams {
272 pub fn new(page: u32, per_page: u32) -> Self {
274 Self {
275 page: page.max(1),
276 per_page: per_page.max(1).min(100),
277 sort_by: None,
278 sort_direction: None,
279 filters: Vec::new(),
280 search: None,
281 }
282 }
283
284 pub fn with_sort(mut self, field: impl Into<String>) -> Self {
286 self.sort_by = Some(field.into());
287 self
288 }
289
290 pub fn with_direction(mut self, direction: SortDirection) -> Self {
292 self.sort_direction = Some(direction);
293 self
294 }
295
296 pub fn with_filter(mut self, filter: Filter) -> Self {
298 self.filters.push(filter);
299 self
300 }
301
302 pub fn with_filters(mut self, filters: Vec<Filter>) -> Self {
304 self.filters.extend(filters);
305 self
306 }
307
308 pub fn with_search(mut self, search: SearchParams) -> Self {
310 self.search = Some(search);
311 self
312 }
313
314 pub fn offset(&self) -> u32 {
316 (self.page - 1) * self.per_page
317 }
318
319 pub fn limit(&self) -> u32 {
321 self.per_page
322 }
323
324 pub fn to_sql_where(&self) -> Option<String> {
326 let mut conditions = Vec::new();
327
328 for filter in &self.filters {
330 conditions.push(filter.to_sql_where());
331 }
332
333 if let Some(ref search) = self.search {
335 conditions.push(search.to_sql_where());
336 }
337
338 if conditions.is_empty() {
339 None
340 } else {
341 Some(conditions.join(" AND "))
342 }
343 }
344
345 pub fn to_surrealql_where(&self) -> Option<String> {
347 let mut conditions = Vec::new();
348
349 for filter in &self.filters {
351 conditions.push(filter.to_surrealql_where());
352 }
353
354 if let Some(ref search) = self.search {
356 let search_conditions: Vec<String> = search
357 .fields
358 .iter()
359 .map(|field| {
360 let pattern = if search.exact_match {
361 format!("'{}'", search.query.replace('\'', "''"))
362 } else {
363 format!("'%{}%'", search.query.replace('\'', "''"))
364 };
365 format!("{} ~ {}", field, pattern)
366 })
367 .collect();
368 conditions.push(format!("({})", search_conditions.join(" OR ")));
369 }
370
371 if conditions.is_empty() {
372 None
373 } else {
374 Some(conditions.join(" AND "))
375 }
376 }
377}
378
379#[derive(Serialize, Deserialize, Debug)]
381pub struct PaginatorResponse<T> {
382 pub data: Vec<T>,
384 pub meta: PaginatorResponseMeta,
386}
387
388#[derive(Serialize, Deserialize, Debug)]
390pub struct PaginatorResponseMeta {
391 pub page: u32,
393 pub per_page: u32,
395 pub total: u32,
397 pub total_pages: u32,
399 pub has_next: bool,
401 pub has_prev: bool,
403}
404
405impl PaginatorResponseMeta {
406 pub fn new(page: u32, per_page: u32, total: u32) -> Self {
408 let total_pages = (total as f32 / per_page as f32).ceil() as u32;
409 Self {
410 page,
411 per_page,
412 total,
413 total_pages,
414 has_next: page < total_pages,
415 has_prev: page > 1,
416 }
417 }
418}