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