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 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}