Skip to main content

tibba_model/
model.rs

1// Copyright 2026 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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;
22use std::future::Future;
23
24type Result<T> = std::result::Result<T, Error>;
25
26#[derive(Debug, Clone, Deserialize, Default)]
27pub struct ModelListParams {
28    pub page: u64,
29    pub limit: u64,
30    pub order_by: Option<String>,
31    pub keyword: Option<String>,
32    pub filters: Option<String>,
33}
34
35impl ModelListParams {
36    pub fn parse_filters(&self) -> Result<Option<HashMap<String, String>>> {
37        if let Some(filters) = &self.filters {
38            let filters: HashMap<String, String> =
39                serde_json::from_str(filters).context(JsonSnafu)?;
40            Ok(Some(filters))
41        } else {
42            Ok(None)
43        }
44    }
45
46    pub fn push_pagination(&self, qb: &mut QueryBuilder<'_, Postgres>) {
47        let order_by = self.order_by.as_deref().unwrap_or("id");
48        push_order_by(qb, order_by);
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
55/// Append a validated ORDER BY clause. Column name must be alphanumeric/underscore only.
56pub 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
67pub trait Model: Send + Sync {
68    type Output: Serialize + Send;
69    fn new() -> Self;
70    fn schema_view<'a>(
71        &'a self,
72        pool: &'a Pool<Postgres>,
73    ) -> impl Future<Output = SchemaView> + Send + 'a;
74    fn keyword(&self) -> String {
75        String::new()
76    }
77    fn push_filter_conditions<'args>(
78        &self,
79        _qb: &mut QueryBuilder<'args, Postgres>,
80        _filters: &HashMap<String, String>,
81    ) -> Result<()> {
82        Ok(())
83    }
84    fn push_conditions<'args>(
85        &self,
86        qb: &mut QueryBuilder<'args, Postgres>,
87        params: &ModelListParams,
88    ) -> Result<()> {
89        qb.push(" WHERE deleted_at IS NULL");
90
91        let col = self.keyword();
92        if !col.is_empty() && col.chars().all(|c| c.is_alphanumeric() || c == '_') {
93            if let Some(keyword) = &params.keyword {
94                qb.push(format!(" AND {col} LIKE "));
95                qb.push_bind(format!("%{keyword}%"));
96            }
97        }
98
99        if let Some(filters) = params.parse_filters()? {
100            if let Some(modified) = filters.get("modified")
101                && let Some((start, end)) = modified.split_once(',')
102            {
103                if let Ok(dt) = DateTime::parse_from_rfc3339(start) {
104                    qb.push(" AND modified >= ");
105                    qb.push_bind(dt.naive_utc());
106                }
107                if let Ok(dt) = DateTime::parse_from_rfc3339(end) {
108                    qb.push(" AND modified <= ");
109                    qb.push_bind(dt.naive_utc());
110                }
111            }
112            self.push_filter_conditions(qb, &filters)?;
113        }
114
115        Ok(())
116    }
117    fn insert<'a>(
118        &'a self,
119        _pool: &'a Pool<Postgres>,
120        _params: serde_json::Value,
121    ) -> impl Future<Output = Result<u64>> + Send + 'a {
122        async {
123            Err(Error::NotSupported {
124                name: "insert".to_string(),
125            })
126        }
127    }
128    fn get_by_id<'a>(
129        &'a self,
130        _pool: &'a Pool<Postgres>,
131        _id: u64,
132    ) -> impl Future<Output = Result<Option<Self::Output>>> + Send + 'a {
133        async {
134            Err(Error::NotSupported {
135                name: "get_by_id".to_string(),
136            })
137        }
138    }
139    fn delete_by_id<'a>(
140        &'a self,
141        _pool: &'a Pool<Postgres>,
142        _id: u64,
143    ) -> impl Future<Output = Result<()>> + Send + 'a {
144        async {
145            Err(Error::NotSupported {
146                name: "delete_by_id".to_string(),
147            })
148        }
149    }
150    fn update_by_id<'a>(
151        &'a self,
152        _pool: &'a Pool<Postgres>,
153        _id: u64,
154        _params: serde_json::Value,
155    ) -> impl Future<Output = Result<()>> + Send + 'a {
156        async {
157            Err(Error::NotSupported {
158                name: "update_by_id".to_string(),
159            })
160        }
161    }
162    fn count<'a>(
163        &'a self,
164        _pool: &'a Pool<Postgres>,
165        _params: &'a ModelListParams,
166    ) -> impl Future<Output = Result<i64>> + Send + 'a {
167        async {
168            Err(Error::NotSupported {
169                name: "count".to_string(),
170            })
171        }
172    }
173    fn list<'a>(
174        &'a self,
175        _pool: &'a Pool<Postgres>,
176        _params: &'a ModelListParams,
177    ) -> impl Future<Output = Result<Vec<Self::Output>>> + Send + 'a {
178        async {
179            Err(Error::NotSupported {
180                name: "list".to_string(),
181            })
182        }
183    }
184    fn list_and_count<'a>(
185        &'a self,
186        pool: &'a Pool<Postgres>,
187        count: bool,
188        params: &'a ModelListParams,
189    ) -> impl Future<Output = Result<serde_json::Value>> + Send + 'a {
190        async move {
191            if count {
192                let (n, items) =
193                    tokio::try_join!(self.count(pool, params), self.list(pool, params))?;
194                Ok(json!({ "count": n, "items": items }))
195            } else {
196                let items = self.list(pool, params).await?;
197                Ok(json!({ "count": -1_i64, "items": items }))
198            }
199        }
200    }
201    fn search_options<'a>(
202        &'a self,
203        _pool: &'a Pool<Postgres>,
204        _keyword: Option<String>,
205    ) -> impl Future<Output = Result<Vec<SchemaOption>>> + Send + 'a {
206        async { Ok(vec![]) }
207    }
208}