Skip to main content

haste_repository/pg/models/
project.rs

1use crate::{
2    admin::TenantModelAdmin,
3    pg::{PGConnection, StoreError},
4    types::project::{CreateProject, Project, ProjectSearchClaims},
5    utilities::{generate_hostname_id, validate_project_id},
6};
7use haste_fhir_model::r4::generated::terminology::IssueType;
8use haste_fhir_operation_error::OperationOutcomeError;
9use haste_jwt::{ProjectId, TenantId};
10use sqlx::{PgExecutor, QueryBuilder};
11
12async fn create_project<'a, 'e, E>(
13    executor: E,
14    tenant: &'a TenantId,
15    project: CreateProject,
16) -> Result<Project, OperationOutcomeError>
17where
18    E: PgExecutor<'e>,
19{
20    // Short enough to fit beside the longest tenant id (see `validate_project_id`).
21    let id = project
22        .id
23        .unwrap_or_else(|| ProjectId::new(generate_hostname_id(Some(8))));
24
25    validate_project_id(tenant.as_ref(), id.as_ref())?;
26
27    let project = sqlx::query_as::<_, Project>(
28        r"
29            INSERT INTO projects (tenant, id, fhir_version, system_created)
30            VALUES ($1, $2, $3, $4)
31            RETURNING tenant, system_created, id, fhir_version
32        ",
33    )
34    .bind(tenant.as_ref())
35    .bind(id.as_ref())
36    .bind(project.fhir_version)
37    .bind(project.system_created)
38    .fetch_one(executor)
39    .await
40    .map_err(StoreError::SQLXError)?;
41
42    Ok(project)
43}
44
45async fn read_project<'a, 'e, E>(
46    executor: E,
47    tenant: &'a TenantId,
48    id: &'a str,
49) -> Result<Option<Project>, OperationOutcomeError>
50where
51    E: PgExecutor<'e>,
52{
53    let project = sqlx::query_as::<_, Project>(
54        r"
55            SELECT id, tenant, system_created, fhir_version
56            FROM projects
57            WHERE tenant = $1 AND id = $2
58        ",
59    )
60    .bind(tenant.as_ref())
61    .bind(id)
62    .fetch_optional(executor)
63    .await
64    .map_err(StoreError::SQLXError)?;
65
66    Ok(project)
67}
68
69async fn delete_project<'a, 'e, E>(
70    executor: E,
71    tenant: &'a TenantId,
72    id: &'a str,
73) -> Result<(), OperationOutcomeError>
74where
75    E: PgExecutor<'e>,
76{
77    let rows_affected = sqlx::query(
78        r"
79            DELETE FROM projects
80            WHERE tenant = $1 AND id = $2 AND system_created = false
81        ",
82    )
83    .bind(tenant.as_ref())
84    .bind(id)
85    .execute(executor)
86    .await
87    .map_err(|_e| {
88        OperationOutcomeError::error(
89            IssueType::not_found(),
90            format!("Project '{id}' not found or is system created and cannot be deleted."),
91        )
92    })?
93    .rows_affected();
94
95    if rows_affected == 0 {
96        return Err(OperationOutcomeError::error(
97            IssueType::not_found(),
98            format!("Project '{id}' not found or is system created and cannot be deleted."),
99        ));
100    }
101
102    Ok(())
103}
104
105async fn search_project<'a, 'e, E>(
106    executor: E,
107    tenant: &'a TenantId,
108    clauses: &'a ProjectSearchClaims,
109) -> Result<Vec<Project>, OperationOutcomeError>
110where
111    E: PgExecutor<'e>,
112{
113    let mut query_builder: QueryBuilder<sqlx::Postgres> =
114        QueryBuilder::new(r"SELECT tenant, id, fhir_version, system_created FROM projects WHERE ");
115
116    let mut and_clauses = query_builder.separated(" AND ");
117
118    and_clauses
119        .push(" tenant = ")
120        .push_bind_unseparated(tenant.as_ref());
121
122    if let Some(id) = clauses.id.as_ref() {
123        and_clauses
124            .push(" id = ")
125            .push_bind_unseparated(id.as_ref());
126    }
127
128    if let Some(fhir_version) = clauses.fhir_version.as_ref() {
129        and_clauses
130            .push(" fhir_version = ")
131            .push_bind_unseparated(fhir_version);
132    }
133
134    if let Some(system_created) = clauses.system_created.as_ref() {
135        and_clauses
136            .push(" system_created = ")
137            .push_bind_unseparated(system_created);
138    }
139
140    let query = query_builder.build_query_as::<Project>();
141
142    let projects: Vec<Project> = query.fetch_all(executor).await.map_err(StoreError::from)?;
143
144    Ok(projects)
145}
146
147/// Not allowing updates on internal row just reading to confirm it's existance.
148async fn update_project<'a, 'e, E>(
149    executor: E,
150    tenant: &'a TenantId,
151    model: Project,
152) -> Result<Project, OperationOutcomeError>
153where
154    E: PgExecutor<'e>,
155{
156    read_project(executor, tenant, model.id.as_ref())
157        .await?
158        .ok_or_else(|| {
159            OperationOutcomeError::error(
160                IssueType::not_found(),
161                format!("Project '{}' not found.", model.id.as_ref()),
162            )
163        })
164}
165
166impl<Key: AsRef<str> + Send + Sync>
167    TenantModelAdmin<CreateProject, Project, ProjectSearchClaims, Project, Key> for PGConnection
168{
169    async fn create(
170        &self,
171        tenant: &TenantId,
172        new_project: CreateProject,
173    ) -> Result<Project, OperationOutcomeError> {
174        match self {
175            PGConnection::Pool(pool, _) => create_project(pool, tenant, new_project).await,
176            PGConnection::Transaction(tx, _, _) => {
177                let mut tx = tx.lock().await;
178                create_project(&mut **tx, tenant, new_project).await
179            }
180        }
181    }
182
183    async fn read(
184        &self,
185        tenant: &TenantId,
186        id: &Key,
187    ) -> Result<Option<Project>, haste_fhir_operation_error::OperationOutcomeError> {
188        match self {
189            PGConnection::Pool(pool, _) => read_project(pool, tenant, id.as_ref()).await,
190            PGConnection::Transaction(tx, _, _) => {
191                let mut tx = tx.lock().await;
192                read_project(&mut **tx, tenant, id.as_ref()).await
193            }
194        }
195    }
196
197    async fn update(
198        &self,
199        tenant: &TenantId,
200        model: Project,
201    ) -> Result<Project, haste_fhir_operation_error::OperationOutcomeError> {
202        match self {
203            PGConnection::Pool(pool, _) => update_project(pool, tenant, model).await,
204            PGConnection::Transaction(tx, _, _) => {
205                let mut tx = tx.lock().await;
206                update_project(&mut **tx, tenant, model).await
207            }
208        }
209    }
210
211    async fn delete(
212        &self,
213        tenant: &TenantId,
214        id: &Key,
215    ) -> Result<(), haste_fhir_operation_error::OperationOutcomeError> {
216        match self {
217            PGConnection::Pool(pool, _) => delete_project(pool, tenant, id.as_ref()).await,
218            PGConnection::Transaction(tx, _, _) => {
219                let mut tx = tx.lock().await;
220                delete_project(&mut **tx, tenant, id.as_ref()).await
221            }
222        }
223    }
224
225    async fn search(
226        &self,
227        tenant: &TenantId,
228        claims: &ProjectSearchClaims,
229    ) -> Result<Vec<Project>, OperationOutcomeError> {
230        match self {
231            PGConnection::Pool(pool, _) => search_project(pool, tenant, claims).await,
232            PGConnection::Transaction(tx, _, _) => {
233                let mut tx = tx.lock().await;
234                search_project(&mut **tx, tenant, claims).await
235            }
236        }
237    }
238}