haste_repository/pg/models/
membership.rs1use crate::{
2 admin::ProjectModelAdmin,
3 pg::{PGConnection, StoreError},
4 types::membership::{CreateMembership, Membership, MembershipSearchClaims},
5};
6use haste_fhir_model::r4::generated::terminology::IssueType;
7use haste_fhir_operation_error::OperationOutcomeError;
8use haste_jwt::{ProjectId, TenantId};
9use sqlx::{PgExecutor, QueryBuilder};
10
11async fn create_membership<'a, 'e, E>(
12 executor: E,
13 tenant: &'a TenantId,
14 project: &'a ProjectId,
15 membership: CreateMembership,
16) -> Result<Membership, OperationOutcomeError>
17where
18 E: PgExecutor<'e>,
19{
20 let mut query_builder = QueryBuilder::new(
21 r"
22 INSERT INTO memberships(tenant, project, user_id, role, resource_id) VALUES (
23 ",
24 );
25
26 let mut seperator = query_builder.separated(", ");
27
28 seperator
29 .push_bind(tenant.as_ref())
30 .push_bind(project.as_ref())
31 .push_bind(&membership.user_id)
32 .push_bind(membership.role)
33 .push_bind(&membership.resource_id);
34
35 query_builder.push(r") RETURNING tenant, project, user_id, role, resource_id");
36
37 let query = query_builder.build_query_as::<Membership>();
38
39 let membership = query
40 .fetch_one(executor)
41 .await
42 .map_err(StoreError::SQLXError)?;
43
44 Ok(membership)
45}
46
47async fn read_membership<'a, 'e, E>(
48 executor: E,
49 tenant: &'a TenantId,
50 project: &'a ProjectId,
51 user_id: &'a str,
52) -> Result<Option<Membership>, OperationOutcomeError>
53where
54 E: PgExecutor<'e>,
55{
56 let membership = sqlx::query_as::<_, Membership>(
57 r"
58 SELECT tenant, project, user_id, role, resource_id
59 FROM memberships
60 WHERE tenant = $1 AND project = $2 AND user_id = $3
61 ",
62 )
63 .bind(tenant.as_ref())
64 .bind(project.as_ref())
65 .bind(user_id)
66 .fetch_optional(executor)
67 .await
68 .map_err(StoreError::SQLXError)?;
69
70 Ok(membership)
71}
72
73async fn update_membership<'a, 'e, E>(
74 executor: E,
75 tenant: &'a TenantId,
76 project: &'a ProjectId,
77 model: Membership,
78) -> Result<Membership, OperationOutcomeError>
79where
80 E: PgExecutor<'e>,
81{
82 let mut query_builder = QueryBuilder::new(
86 r"
87 WITH replaced AS (
88 DELETE FROM memberships
89 WHERE tenant = ",
90 );
91 query_builder
92 .push_bind(tenant.as_ref())
93 .push(" AND project = ")
94 .push_bind(project.as_ref())
95 .push(" AND resource_id = ")
96 .push_bind(&model.resource_id)
97 .push(" AND user_id <> ")
98 .push_bind(&model.user_id)
99 .push(
100 r"
101 )
102 INSERT INTO memberships(tenant, project, user_id, role, resource_id) VALUES (
103 ",
104 );
105
106 let mut seperator = query_builder.separated(", ");
107
108 seperator
109 .push_bind(tenant.as_ref())
110 .push_bind(project.as_ref())
111 .push_bind(&model.user_id)
112 .push_bind(model.role.clone())
113 .push_bind(&model.resource_id);
114
115 query_builder.push(r") ON CONFLICT (tenant, project, user_id) DO UPDATE SET ");
116
117 let mut set_statements = query_builder.separated(", ");
118
119 set_statements
120 .push(" role = ")
121 .push_bind_unseparated(model.role);
122
123 set_statements
124 .push(" resource_id = ")
125 .push_bind_unseparated(&model.resource_id);
126
127 query_builder.push(" WHERE memberships.resource_id = EXCLUDED.resource_id");
131 query_builder.push(r" RETURNING tenant, project, user_id, role, resource_id");
132
133 let query = query_builder.build_query_as::<Membership>();
134
135 query
136 .fetch_optional(executor)
137 .await
138 .map_err(StoreError::SQLXError)?
139 .ok_or_else(|| {
140 OperationOutcomeError::error(
141 IssueType::conflict(),
142 format!(
143 "User '{}' already has a membership in this project.",
144 model.user_id
145 ),
146 )
147 })
148}
149
150async fn delete_membership<'a, 'e, E>(
151 executor: E,
152 tenant: &'a TenantId,
153 project: &'a ProjectId,
154 user_id: &'a str,
155) -> Result<(), OperationOutcomeError>
156where
157 E: PgExecutor<'e>,
158{
159 sqlx::query(
160 r"
161 DELETE FROM memberships
162 WHERE tenant = $1 AND project = $2 AND user_id = $3
163 ",
164 )
165 .bind(tenant.as_ref())
166 .bind(project.as_ref())
167 .bind(user_id)
168 .execute(executor)
169 .await
170 .map_err(StoreError::SQLXError)?;
171
172 Ok(())
173}
174
175async fn search_memberships<'a, 'e, E>(
176 executor: E,
177 tenant: &'a TenantId,
178 project: &'a ProjectId,
179 clauses: &'a MembershipSearchClaims,
180) -> Result<Vec<Membership>, OperationOutcomeError>
181where
182 E: PgExecutor<'e>,
183{
184 let mut query_builder: QueryBuilder<sqlx::Postgres> = QueryBuilder::new(
185 r"SELECT user_id, tenant, project, role, resource_id FROM memberships WHERE ",
186 );
187
188 let mut seperator = query_builder.separated(" AND ");
189 seperator
190 .push(" tenant = ")
191 .push_bind_unseparated(tenant.as_ref())
192 .push(" project = ")
193 .push_bind_unseparated(project.as_ref());
194
195 if let Some(user_id) = clauses.user_id.as_ref() {
196 seperator
197 .push(" user_id = ")
198 .push_bind_unseparated(user_id.as_ref());
199 }
200
201 if let Some(role) = clauses.role.as_ref() {
202 seperator.push(" role = ").push_bind_unseparated(role);
203 }
204
205 let query = query_builder.build_query_as::<Membership>();
206
207 let memberships: Vec<Membership> = query.fetch_all(executor).await.map_err(StoreError::from)?;
208
209 Ok(memberships)
210}
211
212impl<Key: AsRef<str> + Send + Sync>
213 ProjectModelAdmin<CreateMembership, Membership, MembershipSearchClaims, Membership, Key>
214 for PGConnection
215{
216 async fn create(
217 &self,
218 tenant: &TenantId,
219 project: &ProjectId,
220 new_membership: CreateMembership,
221 ) -> Result<Membership, OperationOutcomeError> {
222 match self {
223 PGConnection::Pool(pool, _) => {
224 create_membership(pool, tenant, project, new_membership).await
225 }
226 PGConnection::Transaction(tx, _, _) => {
227 let mut tx = tx.lock().await;
228 create_membership(&mut **tx, tenant, project, new_membership).await
229 }
230 }
231 }
232
233 async fn read(
234 &self,
235 tenant: &TenantId,
236 project: &ProjectId,
237 id: &Key,
238 ) -> Result<Option<Membership>, OperationOutcomeError> {
239 match self {
240 PGConnection::Pool(pool, _) => {
241 read_membership(pool, tenant, project, id.as_ref()).await
242 }
243 PGConnection::Transaction(tx, _, _) => {
244 let mut tx = tx.lock().await;
245 read_membership(&mut **tx, tenant, project, id.as_ref()).await
246 }
247 }
248 }
249
250 async fn update(
251 &self,
252 tenant: &TenantId,
253 project: &ProjectId,
254 model: Membership,
255 ) -> Result<Membership, OperationOutcomeError> {
256 match self {
257 PGConnection::Pool(pool, _) => update_membership(pool, tenant, project, model).await,
258 PGConnection::Transaction(tx, _, _) => {
259 let mut tx = tx.lock().await;
260 update_membership(&mut **tx, tenant, project, model).await
261 }
262 }
263 }
264
265 async fn delete(
266 &self,
267 tenant: &TenantId,
268 project: &ProjectId,
269 id: &Key,
270 ) -> Result<(), OperationOutcomeError> {
271 match self {
272 PGConnection::Pool(pool, _) => {
273 delete_membership(pool, tenant, project, id.as_ref()).await
274 }
275 PGConnection::Transaction(tx, _, _) => {
276 let mut tx = tx.lock().await;
277 delete_membership(&mut **tx, tenant, project, id.as_ref()).await
278 }
279 }
280 }
281
282 async fn search(
283 &self,
284 tenant: &TenantId,
285 project: &ProjectId,
286 clauses: &MembershipSearchClaims,
287 ) -> Result<Vec<Membership>, OperationOutcomeError> {
288 match self {
289 PGConnection::Pool(pool, _) => search_memberships(pool, tenant, project, clauses).await,
290 PGConnection::Transaction(tx, _, _) => {
291 let mut tx = tx.lock().await;
292 search_memberships(&mut **tx, tenant, project, clauses).await
293 }
294 }
295 }
296}