koprogo_api/infrastructure/database/repositories/
user_role_repository_impl.rs

1use crate::application::ports::UserRoleRepository;
2use crate::domain::entities::{UserRole, UserRoleAssignment};
3use crate::infrastructure::pool::DbPool;
4use async_trait::async_trait;
5use sqlx::Row;
6use std::collections::HashMap;
7use uuid::Uuid;
8
9pub struct PostgresUserRoleRepository {
10    pool: DbPool,
11}
12
13impl PostgresUserRoleRepository {
14    pub fn new(pool: DbPool) -> Self {
15        Self { pool }
16    }
17
18    fn map_row(row: sqlx::postgres::PgRow) -> Result<UserRoleAssignment, String> {
19        let role: UserRole = row
20            .try_get::<String, _>("role")
21            .map_err(|e| format!("Failed to read role: {}", e))?
22            .parse()
23            .map_err(|e| format!("Invalid role: {}", e))?;
24
25        Ok(UserRoleAssignment {
26            id: row
27                .try_get("id")
28                .map_err(|e| format!("Failed to read id: {}", e))?,
29            user_id: row
30                .try_get("user_id")
31                .map_err(|e| format!("Failed to read user_id: {}", e))?,
32            role,
33            organization_id: row
34                .try_get("organization_id")
35                .map_err(|e| format!("Failed to read organization_id: {}", e))?,
36            is_primary: row
37                .try_get("is_primary")
38                .map_err(|e| format!("Failed to read is_primary: {}", e))?,
39            created_at: row
40                .try_get("created_at")
41                .map_err(|e| format!("Failed to read created_at: {}", e))?,
42            updated_at: row
43                .try_get("updated_at")
44                .map_err(|e| format!("Failed to read updated_at: {}", e))?,
45        })
46    }
47}
48
49#[async_trait]
50impl UserRoleRepository for PostgresUserRoleRepository {
51    async fn create(&self, assignment: &UserRoleAssignment) -> Result<UserRoleAssignment, String> {
52        let row = sqlx::query(
53            r#"
54            INSERT INTO user_roles (id, user_id, role, organization_id, is_primary, created_at, updated_at)
55            VALUES ($1, $2, $3, $4, $5, $6, $7)
56            RETURNING id, user_id, role, organization_id, is_primary, created_at, updated_at
57            "#,
58        )
59        .bind(assignment.id)
60        .bind(assignment.user_id)
61        .bind(assignment.role.to_string())
62        .bind(assignment.organization_id)
63        .bind(assignment.is_primary)
64        .bind(assignment.created_at)
65        .bind(assignment.updated_at)
66        .fetch_one(&self.pool)
67        .await
68        .map_err(|e| format!("Failed to create user role: {}", e))?;
69
70        Self::map_row(row)
71    }
72
73    async fn list_for_user(&self, user_id: Uuid) -> Result<Vec<UserRoleAssignment>, String> {
74        let rows = sqlx::query(
75            r#"
76            SELECT id, user_id, role, organization_id, is_primary, created_at, updated_at
77            FROM user_roles
78            WHERE user_id = $1
79            ORDER BY is_primary DESC, created_at ASC
80            "#,
81        )
82        .bind(user_id)
83        .fetch_all(&self.pool)
84        .await
85        .map_err(|e| format!("Failed to list user roles: {}", e))?;
86
87        rows.into_iter()
88            .map(Self::map_row)
89            .collect::<Result<Vec<_>, _>>()
90    }
91
92    async fn list_for_users(
93        &self,
94        user_ids: &[Uuid],
95    ) -> Result<HashMap<Uuid, Vec<UserRoleAssignment>>, String> {
96        if user_ids.is_empty() {
97            return Ok(HashMap::new());
98        }
99
100        let rows = sqlx::query(
101            r#"
102            SELECT id, user_id, role, organization_id, is_primary, created_at, updated_at
103            FROM user_roles
104            WHERE user_id = ANY($1)
105            ORDER BY user_id, is_primary DESC, created_at ASC
106            "#,
107        )
108        .bind(user_ids)
109        .fetch_all(&self.pool)
110        .await
111        .map_err(|e| format!("Failed to list roles for users: {}", e))?;
112
113        let mut map: HashMap<Uuid, Vec<UserRoleAssignment>> = HashMap::new();
114        for row in rows {
115            let assignment = Self::map_row(row)?;
116            map.entry(assignment.user_id).or_default().push(assignment);
117        }
118        Ok(map)
119    }
120
121    async fn replace_all(
122        &self,
123        user_id: Uuid,
124        assignments: &[UserRoleAssignment],
125    ) -> Result<(), String> {
126        let mut tx = self
127            .pool
128            .begin()
129            .await
130            .map_err(|e| format!("Failed to begin transaction: {}", e))?;
131
132        sqlx::query("DELETE FROM user_roles WHERE user_id = $1")
133            .bind(user_id)
134            .execute(&mut *tx)
135            .await
136            .map_err(|e| format!("Failed to delete old roles: {}", e))?;
137
138        for a in assignments {
139            sqlx::query(
140                r#"
141                INSERT INTO user_roles (id, user_id, role, organization_id, is_primary, created_at, updated_at)
142                VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
143                "#,
144            )
145            .bind(a.id)
146            .bind(user_id)
147            .bind(a.role.to_string())
148            .bind(a.organization_id)
149            .bind(a.is_primary)
150            .execute(&mut *tx)
151            .await
152            .map_err(|e| format!("Failed to insert role: {}", e))?;
153        }
154
155        tx.commit()
156            .await
157            .map_err(|e| format!("Failed to commit replace_all: {}", e))?;
158        Ok(())
159    }
160
161    async fn find_by_id(&self, id: Uuid) -> Result<Option<UserRoleAssignment>, String> {
162        let row = sqlx::query(
163            r#"
164            SELECT id, user_id, role, organization_id, is_primary, created_at, updated_at
165            FROM user_roles
166            WHERE id = $1
167            "#,
168        )
169        .bind(id)
170        .fetch_optional(&self.pool)
171        .await
172        .map_err(|e| format!("Failed to find user role: {}", e))?;
173
174        match row {
175            Some(row) => Ok(Some(Self::map_row(row)?)),
176            None => Ok(None),
177        }
178    }
179
180    async fn set_primary_role(
181        &self,
182        user_id: Uuid,
183        role_id: Uuid,
184    ) -> Result<UserRoleAssignment, String> {
185        let mut tx = self
186            .pool
187            .begin()
188            .await
189            .map_err(|e| format!("Failed to begin transaction: {}", e))?;
190
191        sqlx::query(
192            r#"
193            UPDATE user_roles
194            SET is_primary = false, updated_at = NOW()
195            WHERE user_id = $1
196            "#,
197        )
198        .bind(user_id)
199        .execute(&mut *tx)
200        .await
201        .map_err(|e| format!("Failed to clear primary roles: {}", e))?;
202
203        let row = sqlx::query(
204            r#"
205            UPDATE user_roles
206            SET is_primary = true, updated_at = NOW()
207            WHERE id = $1 AND user_id = $2
208            RETURNING id, user_id, role, organization_id, is_primary, created_at, updated_at
209            "#,
210        )
211        .bind(role_id)
212        .bind(user_id)
213        .fetch_one(&mut *tx)
214        .await
215        .map_err(|e| format!("Failed to set primary role: {}", e))?;
216
217        tx.commit()
218            .await
219            .map_err(|e| format!("Failed to commit transaction: {}", e))?;
220
221        Self::map_row(row)
222    }
223}