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