Skip to main content

haste_repository/pg/
project.rs

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