Skip to main content

koprogo_api/application/ports/
user_repository.rs

1use crate::domain::entities::User;
2use async_trait::async_trait;
3use uuid::Uuid;
4
5#[async_trait]
6pub trait UserRepository: Send + Sync {
7    async fn create(&self, user: &User) -> Result<User, String>;
8    async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, String>;
9    async fn find_by_email(&self, email: &str) -> Result<Option<User>, String>;
10    async fn find_all(&self) -> Result<Vec<User>, String>;
11
12    /// Une page d'utilisateurs, filtrée par `recherche` si elle est fournie.
13    ///
14    /// `find_all` existe toujours, mais `GET /users` ne l'appelle plus : elle
15    /// rendait la table entière — **2 399 187 octets en 667 ms** pour 4 120
16    /// lignes, mesuré le 2026-09-18 sur la recette (#953). Le même écran
17    /// après pagination de `/organizations` la veille rendait 6 190 octets en
18    /// 18,8 ms.
19    ///
20    /// `Option<String>` et non `Option<&str>` : mockall ne sait pas exprimer
21    /// la durée de vie ici, et un port n'a pas à se tordre pour un détail de
22    /// doublure. Le coût est une allocation par appel.
23    async fn find_page(
24        &self,
25        recherche: Option<String>,
26        role: Option<String>,
27        limit: i64,
28        offset: i64,
29    ) -> Result<Vec<User>, String>;
30
31    /// Le total correspondant au même filtre, pour que la pagination sache
32    /// combien de pages annoncer.
33    async fn count_matching(
34        &self,
35        recherche: Option<String>,
36        role: Option<String>,
37    ) -> Result<i64, String>;
38    async fn find_by_organization(&self, org_id: Uuid) -> Result<Vec<User>, String>;
39    async fn update(&self, user: &User) -> Result<User, String>;
40    async fn update_password(&self, id: Uuid, password_hash: &str) -> Result<bool, String>;
41    async fn activate(&self, id: Uuid) -> Result<Option<User>, String>;
42    async fn deactivate(&self, id: Uuid) -> Result<Option<User>, String>;
43    async fn delete(&self, id: Uuid) -> Result<bool, String>;
44    async fn count_by_organization(&self, org_id: Uuid) -> Result<i64, String>;
45}
46
47#[cfg(test)]
48pub use tests::MockUserRepo;
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use mockall::mock;
54
55    mock! {
56        pub UserRepo {}
57
58        #[async_trait]
59        impl UserRepository for UserRepo {
60            async fn create(&self, user: &User) -> Result<User, String>;
61            async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, String>;
62            async fn find_by_email(&self, email: &str) -> Result<Option<User>, String>;
63            async fn find_all(&self) -> Result<Vec<User>, String>;
64            async fn find_page(
65                &self,
66                recherche: Option<String>,
67                role: Option<String>,
68                limit: i64,
69                offset: i64,
70            ) -> Result<Vec<User>, String>;
71            async fn count_matching(
72                &self,
73                recherche: Option<String>,
74                role: Option<String>,
75            ) -> Result<i64, String>;
76            async fn find_by_organization(&self, org_id: Uuid) -> Result<Vec<User>, String>;
77            async fn update(&self, user: &User) -> Result<User, String>;
78            async fn update_password(&self, id: Uuid, password_hash: &str) -> Result<bool, String>;
79            async fn activate(&self, id: Uuid) -> Result<Option<User>, String>;
80            async fn deactivate(&self, id: Uuid) -> Result<Option<User>, String>;
81            async fn delete(&self, id: Uuid) -> Result<bool, String>;
82            async fn count_by_organization(&self, org_id: Uuid) -> Result<i64, String>;
83        }
84    }
85}