haste_repository/pg/models/
project.rs1use 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 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
147async 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}