Skip to main content

tibba_model/
model.rs

1// Copyright 2025 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;
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
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
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) = &params.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}