1use super::{Error, JsonSnafu, SchemaOption, SchemaView};
16use chrono::DateTime;
17use serde::{Deserialize, Serialize};
18use serde_json::json;
19use snafu::ResultExt;
20use sqlx::{Pool, Postgres, QueryBuilder};
21use std::collections::HashMap;
22
23type Result<T> = std::result::Result<T, Error>;
24
25#[derive(Debug, Clone, Deserialize, Default)]
26pub struct ModelListParams {
27 pub page: u64,
28 pub limit: u64,
29 pub order_by: Option<String>,
30 pub keyword: Option<String>,
31 pub filters: Option<String>,
32}
33
34impl ModelListParams {
35 pub fn parse_filters(&self) -> Result<Option<HashMap<String, String>>> {
36 if let Some(filters) = &self.filters {
37 let filters: HashMap<String, String> =
38 serde_json::from_str(filters).context(JsonSnafu)?;
39 Ok(Some(filters))
40 } else {
41 Ok(None)
42 }
43 }
44
45 pub fn push_pagination(&self, qb: &mut QueryBuilder<'_, Postgres>) {
46 if let Some(order_by) = &self.order_by {
47 push_order_by(qb, order_by);
48 }
49 let limit = self.limit.min(200);
50 let offset = (self.page.max(1) - 1) * limit;
51 qb.push(format!(" LIMIT {limit} OFFSET {offset}"));
52 }
53}
54
55pub fn push_order_by(qb: &mut QueryBuilder<'_, Postgres>, order_by: &str) {
57 let (col, dir) = if let Some(col) = order_by.strip_prefix('-') {
58 (col, "DESC")
59 } else {
60 (order_by, "ASC")
61 };
62 if col.chars().all(|c| c.is_alphanumeric() || c == '_') {
63 qb.push(format!(" ORDER BY {col} {dir}"));
64 }
65}
66
67#[allow(async_fn_in_trait)]
68pub trait Model: Send + Sync {
69 type Output: Serialize;
70 fn new() -> Self;
71 async fn schema_view(&self, _pool: &Pool<Postgres>) -> SchemaView;
72 fn keyword(&self) -> String {
73 String::new()
74 }
75 fn push_filter_conditions<'args>(
76 &self,
77 _qb: &mut QueryBuilder<'args, Postgres>,
78 _filters: &HashMap<String, String>,
79 ) -> Result<()> {
80 Ok(())
81 }
82 fn push_conditions<'args>(
83 &self,
84 qb: &mut QueryBuilder<'args, Postgres>,
85 params: &ModelListParams,
86 ) -> Result<()> {
87 qb.push(" WHERE deleted_at IS NULL");
88
89 let col = self.keyword();
90 if !col.is_empty() && col.chars().all(|c| c.is_alphanumeric() || c == '_') {
91 if let Some(keyword) = ¶ms.keyword {
92 qb.push(format!(" AND {col} LIKE "));
93 qb.push_bind(format!("%{keyword}%"));
94 }
95 }
96
97 if let Some(filters) = params.parse_filters()? {
98 if let Some(modified) = filters.get("modified")
99 && let Some((start, end)) = modified.split_once(',')
100 {
101 if let Ok(dt) = DateTime::parse_from_rfc3339(start) {
102 qb.push(" AND modified >= ");
103 qb.push_bind(dt.naive_utc());
104 }
105 if let Ok(dt) = DateTime::parse_from_rfc3339(end) {
106 qb.push(" AND modified <= ");
107 qb.push_bind(dt.naive_utc());
108 }
109 }
110 self.push_filter_conditions(qb, &filters)?;
111 }
112
113 Ok(())
114 }
115 async fn insert(&self, _pool: &Pool<Postgres>, _params: serde_json::Value) -> Result<u64> {
116 Err(Error::NotSupported {
117 name: "insert".to_string(),
118 })
119 }
120 async fn get_by_id(&self, _pool: &Pool<Postgres>, _id: u64) -> Result<Option<Self::Output>> {
121 Err(Error::NotSupported {
122 name: "get_by_id".to_string(),
123 })
124 }
125 async fn delete_by_id(&self, _pool: &Pool<Postgres>, _id: u64) -> Result<()> {
126 Err(Error::NotSupported {
127 name: "delete_by_id".to_string(),
128 })
129 }
130 async fn update_by_id(
131 &self,
132 _pool: &Pool<Postgres>,
133 _id: u64,
134 _params: serde_json::Value,
135 ) -> Result<()> {
136 Err(Error::NotSupported {
137 name: "update_by_id".to_string(),
138 })
139 }
140 async fn count(&self, _pool: &Pool<Postgres>, _params: &ModelListParams) -> Result<i64> {
141 Err(Error::NotSupported {
142 name: "count".to_string(),
143 })
144 }
145 async fn list(
146 &self,
147 _pool: &Pool<Postgres>,
148 _params: &ModelListParams,
149 ) -> Result<Vec<Self::Output>> {
150 Err(Error::NotSupported {
151 name: "list".to_string(),
152 })
153 }
154 async fn list_and_count(
155 &self,
156 pool: &Pool<Postgres>,
157 count: bool,
158 params: &ModelListParams,
159 ) -> Result<serde_json::Value> {
160 if count {
161 let (count, items) =
162 tokio::try_join!(self.count(pool, params), self.list(pool, params))?;
163 Ok(json!({ "count": count, "items": items }))
164 } else {
165 let items = self.list(pool, params).await?;
166 Ok(json!({ "count": -1_i64, "items": items }))
167 }
168 }
169 async fn search_options(
170 &self,
171 _pool: &Pool<Postgres>,
172 _keyword: Option<String>,
173 ) -> Result<Vec<SchemaOption>> {
174 Ok(vec![])
175 }
176}