1use crate::config::{SearchConfig, SecretProviderConfig, ServerConfig};
2use crate::fhir_client::{FHIRServerClient, ServerClientConfig};
3use haste_fhir_model::r4::generated::terminology::IssueType;
4use haste_fhir_operation_error::{OperationOutcomeError, derive::OperationOutcomeError};
5use haste_fhir_search::elastic_search::search_parameter_resolver::ElasticSearchParameterResolver;
6use haste_fhir_search::{
7 SearchEngine,
8 elastic_search::{ElasticSearchEngine, create_es_client},
9 pg_search::{
10 PgSearchEngine, create_pg_search_pool, search_parameter_resolver::PgSearchParameterResolver,
11 },
12};
13use haste_fhir_terminology::{FHIRTerminology, client::FHIRCanonicalTerminology};
14use haste_fhirpath::FPEngine;
15use haste_repository::{Repository, pg::PGConnection};
16use sqlx::{Pool, Postgres};
17use sqlx_postgres::PgPoolOptions;
18use std::{env::VarError, sync::Arc};
19use tokio::sync::OnceCell;
20use tracing::info;
21
22static POOL: OnceCell<Pool<Postgres>> = OnceCell::const_new();
24pub async fn get_pool(config: &ServerConfig) -> &'static Pool<Postgres> {
25 match &config.repo {
26 crate::config::RepoConfig::Postgres(pg_config) => {
27 POOL.get_or_init(async || {
28 info!("Connecting to postgres database");
29
30 PgPoolOptions::new()
31 .max_connections(pg_config.max_connections)
32 .connect(&pg_config.database_url)
33 .await
34 .expect("Failed to create database connection pool")
35 })
36 .await
37 }
38 }
39}
40
41#[derive(OperationOutcomeError, Debug)]
42pub enum ConfigError {
43 #[error(code = "invalid", diagnostic = "Invalid environment!")]
44 DotEnv(#[from] dotenvy::Error),
45 #[error(code = "invalid", diagnostic = "Invalid session!")]
46 Session(#[from] tower_sessions::session::Error),
47 #[error(code = "invalid", diagnostic = "Database error")]
48 Database(#[from] sqlx::Error),
49 #[error(code = "invalid", diagnostic = "Environment variable not set {arg0}")]
50 EnvironmentVariable(#[from] VarError),
51 #[error(code = "invalid", diagnostic = "Failed to render template.")]
52 TemplateRender,
53}
54
55#[derive(OperationOutcomeError, Debug)]
56pub enum CustomOpError {
57 #[error(code = "invalid", diagnostic = "FHIRPath error")]
58 FHIRPath(#[from] haste_fhirpath::FHIRPathError),
59 #[error(code = "invalid", diagnostic = "Failed to deserialize resource")]
60 Deserialize(#[from] serde_json::Error),
61 #[error(code = "invalid", diagnostic = "Internal server error")]
62 InternalServerError,
63}
64
65pub struct ServerState<
66 Repo: Repository + Send + Sync + 'static,
67 Search: SearchEngine + Send + Sync + 'static,
68 Terminology: FHIRTerminology + Send + Sync + 'static,
69> {
70 pub terminology: Arc<Terminology>,
71 pub search: Arc<Search>,
72 pub repo: Arc<Repo>,
73 pub rate_limit: Arc<dyn haste_rate_limit::RateLimit>,
74 pub fhir_client: Arc<FHIRServerClient<Repo, Search, Terminology>>,
75 pub secret_provider: Arc<dyn haste_encryption::SecretsProvider + Send + Sync>,
76 pub config: Arc<crate::config::ServerConfig>,
77}
78
79impl<
80 Repo: Repository + Send + Sync + 'static,
81 Search: SearchEngine + Send + Sync + 'static,
82 Terminology: FHIRTerminology + Send + Sync + 'static,
83> ServerState<Repo, Search, Terminology>
84{
85 pub async fn transaction(&self) -> Result<Self, OperationOutcomeError> {
86 self.repo.transaction(true).await.map(|tx_repo| {
87 let tx_repo = Arc::new(tx_repo);
88 ServerState {
89 terminology: self.terminology.clone(),
90 search: self.search.clone(),
91 repo: tx_repo.clone(),
92 rate_limit: self.rate_limit.clone(),
93 secret_provider: self.secret_provider.clone(),
94 fhir_client: Arc::new(FHIRServerClient::new(ServerClientConfig::new(
95 tx_repo,
96 self.search.clone(),
97 self.terminology.clone(),
98 self.config.clone(),
99 self.fhir_client.deno_pool(),
100 ))),
101 config: self.config.clone(),
102 }
103 })
104 }
105
106 pub async fn commit(self) -> Result<(), OperationOutcomeError> {
107 let repo = self.repo.clone();
108 drop(self);
109
110 Arc::try_unwrap(repo)
111 .map_err(|_e| {
112 OperationOutcomeError::fatal(
113 IssueType::exception(),
114 "Failed to unwrap transaction client".to_string(),
115 )
116 })?
117 .commit()
118 .await?;
119
120 Ok(())
121 }
122}
123
124#[derive(Clone)]
128pub enum SearchEngineBackend {
129 Elasticsearch(ElasticSearchEngine<ElasticSearchParameterResolver<PGConnection>>),
130 Postgres(PgSearchEngine<PgSearchParameterResolver<PGConnection>>),
131}
132
133impl SearchEngine for SearchEngineBackend {
134 async fn search(
135 &self,
136 fhir_version: &haste_repository::types::SupportedFHIRVersions,
137 tenant: &haste_jwt::TenantId,
138 project: &haste_jwt::ProjectId,
139 search_request: &haste_fhir_client::request::SearchRequest,
140 options: Option<haste_fhir_search::SearchOptions>,
141 ) -> Result<haste_fhir_search::SearchReturn, OperationOutcomeError> {
142 match self {
143 SearchEngineBackend::Elasticsearch(e) => {
144 e.search(fhir_version, tenant, project, search_request, options)
145 .await
146 }
147 SearchEngineBackend::Postgres(e) => {
148 e.search(fhir_version, tenant, project, search_request, options)
149 .await
150 }
151 }
152 }
153
154 async fn index(
155 &self,
156 fhir_version: haste_repository::types::SupportedFHIRVersions,
157 resource: Vec<haste_fhir_search::IndexResource>,
158 ) -> Result<haste_fhir_search::IndexOutcome, OperationOutcomeError> {
159 match self {
160 SearchEngineBackend::Elasticsearch(e) => e.index(fhir_version, resource).await,
161 SearchEngineBackend::Postgres(e) => e.index(fhir_version, resource).await,
162 }
163 }
164
165 async fn migrate(
166 &self,
167 fhir_version: &haste_repository::types::SupportedFHIRVersions,
168 ) -> Result<(), OperationOutcomeError> {
169 match self {
170 SearchEngineBackend::Elasticsearch(e) => e.migrate(fhir_version).await,
171 SearchEngineBackend::Postgres(e) => e.migrate(fhir_version).await,
172 }
173 }
174}
175
176async fn create_search_engine(
177 config: &crate::config::ServerConfig,
178 repo: Arc<PGConnection>,
179) -> Result<Arc<SearchEngineBackend>, OperationOutcomeError> {
180 match &config.search {
181 SearchConfig::Elasticsearch(elasticsearch_config) => {
182 let es_client = create_es_client(
183 &elasticsearch_config.url,
184 elasticsearch_config.username.clone(),
185 elasticsearch_config.password.clone(),
186 )?;
187 let engine = ElasticSearchEngine::new(
188 Arc::new(ElasticSearchParameterResolver::new(es_client.clone(), repo)),
189 Arc::new(FPEngine::new()),
190 es_client,
191 elasticsearch_config.prune_removed_search_parameters,
192 );
193 Ok(Arc::new(SearchEngineBackend::Elasticsearch(engine)))
194 }
195 SearchConfig::Postgres(pg_config) => {
196 let search_pool =
197 create_pg_search_pool(&pg_config.database_url, pg_config.max_connections).await?;
198
199 let resolver = PgSearchParameterResolver::new(search_pool.clone(), repo);
200
201 let engine =
202 PgSearchEngine::new(Arc::new(resolver), Arc::new(FPEngine::new()), search_pool);
203 Ok(Arc::new(SearchEngineBackend::Postgres(engine)))
204 }
205 }
206}
207
208pub async fn create_services(
209 config: Arc<crate::config::ServerConfig>,
210) -> Result<
211 Arc<ServerState<PGConnection, SearchEngineBackend, FHIRCanonicalTerminology>>,
212 OperationOutcomeError,
213> {
214 let pool = Arc::new(PGConnection::pool(get_pool(config.as_ref()).await.clone()));
215
216 let terminology = Arc::new(FHIRCanonicalTerminology::new());
217
218 let search_engine = create_search_engine(config.as_ref(), pool.clone()).await?;
219
220 let deno_pool = Arc::new(
224 haste_operation_executor::providers::deno_embedded::pool::DenoPool::new(
225 config.operations.deno_pool_threads,
226 )
227 .expect("Failed to create DenoPool"),
228 );
229
230 let fhir_client = Arc::new(FHIRServerClient::new(
231 ServerClientConfig::new(
232 pool.clone(),
233 search_engine.clone(),
234 terminology.clone(),
235 config.clone(),
236 deno_pool,
237 )
238 .with_mutate_artifacts(config.allow_artifact_mutations)
239 .with_audit_repo(if config.monitoring.audit_enabled {
240 Some(pool.clone())
241 } else {
242 None
243 }),
244 ));
245
246 let shared_state = Arc::new(ServerState {
247 config: config.clone(),
248 rate_limit: pool.clone(),
249 repo: pool,
250 terminology,
251 search: search_engine,
252 fhir_client,
253 secret_provider: match &config.security.encryption {
254 SecretProviderConfig::Environment { prefix } => haste_encryption::get_secrets_provider(
255 haste_encryption::SecretsProviderKind::Environment {
256 prefix: prefix.clone(),
257 },
258 ),
259 _ => {
260 return Err(OperationOutcomeError::fatal(
261 IssueType::exception(),
262 "Only environment encryption is supported for now.".to_string(),
263 ));
264 }
265 },
266 });
267
268 Ok(shared_state)
269}