Skip to main content

haste_repository/pg/models/
membership.rs

1use 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    // Rows are keyed by user, so pointing a Membership at another user would
83    // otherwise insert a row for the new user and leave the old user's row,
84    // and its access to the project, behind. Remove it in the same statement.
85    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    // A user has one membership per project. If the user already belongs to
128    // it through another Membership resource, leave that row alone: no row
129    // comes back and the update is refused.
130    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}