Skip to main content

karbon_framework/db/
repository.rs

1use std::future::Future;
2
3use chrono::{DateTime, Utc};
4
5use super::{Db, DbPool, DbRow, placeholder};
6use super::{DeleteBuilder, UpdateBuilder};
7use crate::error::AppResult;
8
9/// Valeur dynamique pour les clauses WHERE.
10///
11/// Grâce aux implémentations `From`, on peut écrire simplement :
12/// ```ignore
13/// User::find_where(pool, &[("email", "foo@bar.com".into())]).await?;
14/// User::find_all_where(pool, &[("active", 1_i64.into())]).await?;
15/// ```
16#[derive(Debug, Clone)]
17pub enum WhereValue {
18    Int(i64),
19    Float(f64),
20    String(String),
21    Bool(bool),
22    DateTime(DateTime<Utc>),
23}
24
25impl From<i64> for WhereValue {
26    fn from(v: i64) -> Self { WhereValue::Int(v) }
27}
28
29impl From<i32> for WhereValue {
30    fn from(v: i32) -> Self { WhereValue::Int(v as i64) }
31}
32
33impl From<f64> for WhereValue {
34    fn from(v: f64) -> Self { WhereValue::Float(v) }
35}
36
37impl From<f32> for WhereValue {
38    fn from(v: f32) -> Self { WhereValue::Float(v as f64) }
39}
40
41impl From<&str> for WhereValue {
42    fn from(v: &str) -> Self { WhereValue::String(v.to_string()) }
43}
44
45impl From<String> for WhereValue {
46    fn from(v: String) -> Self { WhereValue::String(v) }
47}
48
49impl From<bool> for WhereValue {
50    fn from(v: bool) -> Self { WhereValue::Bool(v) }
51}
52
53impl From<DateTime<Utc>> for WhereValue {
54    fn from(v: DateTime<Utc>) -> Self { WhereValue::DateTime(v) }
55}
56
57/// Trait générique pour les opérations CRUD de base.
58///
59/// Fournit automatiquement `find_by_id`, `count`, `delete`, `exists`,
60/// `find_all` et optionnellement `find_by_slug` à tout type qui l'implémente.
61///
62/// Supporte le soft delete via `SOFT_DELETE = true` : les entités ne sont pas
63/// supprimées mais marquées avec `deleted_at`. Les requêtes filtrent
64/// automatiquement les entités supprimées.
65///
66/// ```ignore
67/// impl CrudRepository for Post {
68///     const TABLE: &'static str = "posts";
69///     const ENTITY_NAME: &'static str = "Article";
70///     const HAS_SLUG: bool = true;
71///     const SOFT_DELETE: bool = true;
72/// }
73/// ```
74pub trait CrudRepository: Sized + Send + Unpin + for<'r> sqlx::FromRow<'r, DbRow> {
75    const TABLE: &'static str;
76    const ENTITY_NAME: &'static str;
77    const HAS_SLUG: bool = false;
78
79    /// Active le soft delete. Si `true`, `delete()` fait un UPDATE SET deleted_at = NOW()
80    /// au lieu d'un DELETE, et toutes les requêtes ajoutent `WHERE deleted_at IS NULL`.
81    const SOFT_DELETE: bool = false;
82
83    // ─── Helpers internes ───
84
85    /// Retourne le filtre soft delete si activé
86    fn soft_filter() -> &'static str {
87        if Self::SOFT_DELETE { " AND deleted_at IS NULL" } else { "" }
88    }
89
90    fn soft_where() -> &'static str {
91        if Self::SOFT_DELETE { " WHERE deleted_at IS NULL" } else { "" }
92    }
93
94    // ─── Par ID ───
95
96    fn find_by_id(
97        pool: &DbPool,
98        id: i64,
99    ) -> impl Future<Output = AppResult<Option<Self>>> + Send {
100        async move {
101            let query = format!(
102                "SELECT * FROM {} WHERE id = {}{}",
103                Self::TABLE, placeholder(1), Self::soft_filter()
104            );
105            let result = sqlx::query_as::<Db, Self>(&query)
106                .bind(id)
107                .fetch_optional(pool)
108                .await?;
109            Ok(result)
110        }
111    }
112
113    // ─── Par slug ───
114
115    fn find_by_slug(
116        pool: &DbPool,
117        slug: &str,
118    ) -> impl Future<Output = AppResult<Option<Self>>> + Send {
119        async move {
120            if !Self::HAS_SLUG {
121                return Err(crate::error::AppError::Internal(
122                    format!("find_by_slug called on {} which does not have HAS_SLUG = true", Self::TABLE)
123                ));
124            }
125            let query = format!(
126                "SELECT * FROM {} WHERE slug = {}{}",
127                Self::TABLE, placeholder(1), Self::soft_filter()
128            );
129            let result = sqlx::query_as::<Db, Self>(&query)
130                .bind(slug)
131                .fetch_optional(pool)
132                .await?;
133            Ok(result)
134        }
135    }
136
137    // ─── Comptage ───
138
139    fn count(pool: &DbPool) -> impl Future<Output = AppResult<i64>> + Send {
140        async move {
141            let query = format!("SELECT COUNT(*) FROM {}{}", Self::TABLE, Self::soft_where());
142            let (count,): (i64,) = sqlx::query_as(&query).fetch_one(pool).await?;
143            Ok(count)
144        }
145    }
146
147    fn exists(pool: &DbPool, id: i64) -> impl Future<Output = AppResult<bool>> + Send {
148        async move {
149            let query = format!(
150                "SELECT COUNT(*) FROM {} WHERE id = {}{}",
151                Self::TABLE, placeholder(1), Self::soft_filter()
152            );
153            let (count,): (i64,) = sqlx::query_as(&query).bind(id).fetch_one(pool).await?;
154            Ok(count > 0)
155        }
156    }
157
158    // ─── Suppression ───
159
160    /// Supprime une entité (soft delete si SOFT_DELETE = true, sinon DELETE réel).
161    fn delete(pool: &DbPool, id: i64) -> impl Future<Output = AppResult<u64>> + Send {
162        async move {
163            if Self::SOFT_DELETE {
164                UpdateBuilder::table(Self::TABLE)
165                    .set_raw("deleted_at", "NOW()")
166                    .where_eq("id", id)
167                    .execute(pool)
168                    .await
169            } else {
170                DeleteBuilder::from(Self::TABLE)
171                    .where_eq("id", id)
172                    .execute(pool)
173                    .await
174            }
175        }
176    }
177
178    /// Suppression définitive (ignore SOFT_DELETE).
179    fn force_delete(pool: &DbPool, id: i64) -> impl Future<Output = AppResult<u64>> + Send {
180        async move {
181            DeleteBuilder::from(Self::TABLE)
182                .where_eq("id", id)
183                .execute(pool)
184                .await
185        }
186    }
187
188    /// Restaure une entité soft-deleted.
189    fn restore(pool: &DbPool, id: i64) -> impl Future<Output = AppResult<u64>> + Send {
190        async move {
191            if !Self::SOFT_DELETE {
192                return Err(crate::error::AppError::Internal(
193                    format!("restore called on {} which does not have SOFT_DELETE = true", Self::TABLE)
194                ));
195            }
196            UpdateBuilder::table(Self::TABLE)
197                .set_raw("deleted_at", "NULL")
198                .where_eq("id", id)
199                .execute(pool)
200                .await
201        }
202    }
203
204    // ─── Listes ───
205
206    /// ```ignore
207    /// let users = User::find_all(pool, None).await?;
208    /// let categories = Category::find_all(pool, Some(("name", "ASC"))).await?;
209    /// ```
210    fn find_all(
211        pool: &DbPool,
212        order: Option<(&str, &str)>,
213    ) -> impl Future<Output = AppResult<Vec<Self>>> + Send {
214        let mut query = format!("SELECT * FROM {}{}", Self::TABLE, Self::soft_where());
215        if let Some((col, dir)) = order {
216            query.push_str(&format!(" ORDER BY {} {}", col, dir));
217        }
218        async move {
219            let items = sqlx::query_as::<Db, Self>(&query).fetch_all(pool).await?;
220            Ok(items)
221        }
222    }
223
224    /// Récupère toutes les entités y compris les soft-deleted.
225    fn find_all_with_trashed(
226        pool: &DbPool,
227        order: Option<(&str, &str)>,
228    ) -> impl Future<Output = AppResult<Vec<Self>>> + Send {
229        let mut query = format!("SELECT * FROM {}", Self::TABLE);
230        if let Some((col, dir)) = order {
231            query.push_str(&format!(" ORDER BY {} {}", col, dir));
232        }
233        async move {
234            let items = sqlx::query_as::<Db, Self>(&query).fetch_all(pool).await?;
235            Ok(items)
236        }
237    }
238
239    // ─── Recherche dynamique ───
240
241    /// ```ignore
242    /// let user = User::find_where(pool, &[("email", "foo@bar.com".into())]).await?;
243    /// ```
244    fn find_where(
245        pool: &DbPool,
246        conditions: &[(&str, WhereValue)],
247    ) -> impl Future<Output = AppResult<Option<Self>>> + Send {
248        let (sql, values) = build_where_query(Self::TABLE, conditions, None, Self::SOFT_DELETE);
249        async move {
250            let mut query = sqlx::query_as::<Db, Self>(&sql);
251            for value in &values {
252                query = bind_where_value_as(query, value);
253            }
254            Ok(query.fetch_optional(pool).await?)
255        }
256    }
257
258    /// ```ignore
259    /// let comments = Comment::find_all_where(pool, &[("status", "approved".into())], None).await?;
260    /// ```
261    fn find_all_where(
262        pool: &DbPool,
263        conditions: &[(&str, WhereValue)],
264        order: Option<(&str, &str)>,
265    ) -> impl Future<Output = AppResult<Vec<Self>>> + Send {
266        let (sql, values) = build_where_query(Self::TABLE, conditions, order, Self::SOFT_DELETE);
267        async move {
268            let mut query = sqlx::query_as::<Db, Self>(&sql);
269            for value in &values {
270                query = bind_where_value_as(query, value);
271            }
272            Ok(query.fetch_all(pool).await?)
273        }
274    }
275}
276
277fn build_where_query(table: &str, conditions: &[(&str, WhereValue)], order: Option<(&str, &str)>, soft_delete: bool) -> (String, Vec<WhereValue>) {
278    let mut clauses: Vec<String> = conditions.iter().enumerate()
279        .map(|(i, (col, _))| format!("{} = {}", col, placeholder(i + 1)))
280        .collect();
281    if soft_delete {
282        clauses.push("deleted_at IS NULL".to_string());
283    }
284    let values: Vec<WhereValue> = conditions.iter().map(|(_, v)| v.clone()).collect();
285    let mut sql = if clauses.is_empty() {
286        format!("SELECT * FROM {}", table)
287    } else {
288        format!("SELECT * FROM {} WHERE {}", table, clauses.join(" AND "))
289    };
290    if let Some((col, dir)) = order {
291        sql.push_str(&format!(" ORDER BY {} {}", col, dir));
292    }
293    (sql, values)
294}
295
296fn bind_where_value_as<'q, T>(
297    query: sqlx::query::QueryAs<'q, Db, T, <Db as sqlx::Database>::Arguments<'q>>,
298    value: &'q WhereValue,
299) -> sqlx::query::QueryAs<'q, Db, T, <Db as sqlx::Database>::Arguments<'q>>
300where
301    T: Send + Unpin + for<'r> sqlx::FromRow<'r, DbRow>,
302{
303    match value {
304        WhereValue::Int(v) => query.bind(*v),
305        WhereValue::Float(v) => query.bind(*v),
306        WhereValue::String(v) => query.bind(v.as_str()),
307        WhereValue::Bool(v) => query.bind(*v),
308        WhereValue::DateTime(v) => query.bind(*v),
309    }
310}