1use std::{collections::HashSet, sync::Arc};
2
3use crate::{config::ServerConfig, fhir_client::ServerCTX, services::create_services};
4use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
5use haste_artifacts::ARTIFACT_RESOURCES;
6use haste_fhir_client::{
7 FHIRClient,
8 request::{FHIRSearchTypeRequest, SearchRequest},
9};
10use haste_fhir_model::r4::generated::{
11 resources::{
12 Bundle, BundleEntry, BundleEntryRequest, OperationDefinition, Resource, ResourceType,
13 SearchParameter, StructureDefinition,
14 },
15 terminology::{BundleType, HttpVerb, IssueType},
16 types::{Coding, FHIRCode, FHIRUri, Meta},
17};
18use haste_fhir_operation_error::OperationOutcomeError;
19use haste_fhir_search::{MAX_COUNT, SearchEngine, SearchOptions};
20use haste_jwt::{ProjectId, TenantId};
21use haste_repository::{Repository, fhir::CachePolicy, types::SupportedFHIRVersions};
22use sha1::{Digest, Sha1};
23
24fn generate_sha256_hash(value: &Resource) -> String {
25 let json = serde_json::to_string(value).expect("failed to serialize value.");
26 let mut sha_hasher = Sha1::new();
27 sha_hasher.update(json.as_bytes());
28 let sha1 = sha_hasher.finalize();
29
30 URL_SAFE_NO_PAD.encode(sha1)
31}
32
33static HASH_TAG_SYSTEM: &str = "https://haste.health/fhir/CodeSystem/hash";
34
35fn _add_hash_tag(meta: &mut Option<Box<Meta>>, sha_hash: String) {
36 let hash_tag = Coding {
37 system: Some(Box::new(FHIRUri {
38 value: Some(HASH_TAG_SYSTEM.to_string()),
39 ..Default::default()
40 })),
41 code: Some(Box::new(FHIRCode {
42 value: Some(sha_hash),
43 ..Default::default()
44 })),
45 ..Default::default()
46 };
47
48 let meta = if let Some(meta) = meta {
49 meta
50 } else {
51 *meta = Some(Box::new(Meta::default()));
52 meta.as_mut().unwrap()
53 };
54
55 match &mut meta.tag {
56 Some(tags) => tags.push(hash_tag),
57 None => meta.tag = Some(vec![hash_tag]),
58 }
59}
60
61fn add_hash_tag(resource: &mut Resource, sha_hash: String) {
62 match resource {
63 Resource::StructureDefinition(structure_definition) => {
64 _add_hash_tag(&mut structure_definition.meta, sha_hash)
65 }
66 Resource::CodeSystem(code_system) => _add_hash_tag(&mut code_system.meta, sha_hash),
67 Resource::ValueSet(value_set) => _add_hash_tag(&mut value_set.meta, sha_hash),
68 Resource::SearchParameter(search_parameter) => {
69 _add_hash_tag(&mut search_parameter.meta, sha_hash)
70 }
71 _ => {}
72 }
73}
74
75fn get_id(resource: &Resource) -> String {
76 match resource {
77 Resource::StructureDefinition(structure_definition) => {
78 structure_definition.id.clone().unwrap_or_default()
79 }
80 Resource::CodeSystem(code_system) => code_system.id.clone().unwrap_or_default(),
81 Resource::ValueSet(value_set) => value_set.id.clone().unwrap_or_default(),
82 Resource::SearchParameter(search_parameter) => {
83 search_parameter.id.clone().unwrap_or_default()
84 }
85 _ => todo!(
86 "Unsupported resource type '{}'",
87 resource.resource_type().as_ref()
88 ),
89 }
90}
91
92pub fn get_resource_type(resource: &Resource) -> ResourceType {
93 match resource {
94 Resource::StructureDefinition(_) => ResourceType::StructureDefinition,
95 Resource::CodeSystem(_) => ResourceType::CodeSystem,
96 Resource::ValueSet(_) => ResourceType::ValueSet,
97 Resource::SearchParameter(_) => ResourceType::SearchParameter,
98 _ => todo!(
99 "Unsupported resource type '{}'",
100 resource.resource_type().as_ref()
101 ),
102 }
103}
104
105pub async fn reset_artifacts(config: Arc<ServerConfig>) -> Result<(), OperationOutcomeError> {
107 let services = create_services(config.clone()).await?;
108
109 let transaction = services.transaction().await?;
110
111 {
112 let ctx = Arc::new(ServerCTX::system(
113 TenantId::System,
114 ProjectId::System,
115 transaction.fhir_client.clone(),
116 transaction.rate_limit.clone(),
117 ));
118
119 tracing::info!("Deleting existing CodeSystems");
120 ctx.client
121 .delete_type(
122 ctx.clone(),
123 ResourceType::CodeSystem,
124 (vec![] as Vec<(String, Vec<String>)>).into(),
125 )
126 .await?;
127 tracing::info!("Deleting existing ValueSets");
128 ctx.client
129 .delete_type(
130 ctx.clone(),
131 ResourceType::ValueSet,
132 (vec![] as Vec<(String, Vec<String>)>).into(),
133 )
134 .await?;
135 tracing::info!("Deleting existing StructureDefinitions");
136 ctx.client
137 .delete_type(
138 ctx.clone(),
139 ResourceType::StructureDefinition,
140 (vec![] as Vec<(String, Vec<String>)>).into(),
141 )
142 .await?;
143 tracing::info!("Deleting existing SearchParameters");
144 ctx.client
145 .delete_type(
146 ctx.clone(),
147 ResourceType::SearchParameter,
148 (vec![] as Vec<(String, Vec<String>)>).into(),
149 )
150 .await?;
151 _load_artifacts(ctx.clone()).await?;
152 }
153
154 transaction.commit().await?;
155
156 Ok(())
157}
158
159async fn _load_artifacts<Client: FHIRClient<Arc<ServerCTX<Client>>, OperationOutcomeError>>(
161 ctx: Arc<ServerCTX<Client>>,
162) -> Result<(), OperationOutcomeError> {
163 let mut hashes = HashSet::new();
164 let mut artifact_transaction_bundle_entries: Vec<BundleEntry> = vec![];
165 let mut entry_meta: Vec<(ResourceType, String, String)> = vec![];
166
167 for resource in ARTIFACT_RESOURCES.iter() {
168 let sha_hash = generate_sha256_hash(resource);
169 hashes.insert(sha_hash);
170
171 match &resource {
172 Resource::SearchParameter(_)
173 | Resource::CodeSystem(_)
174 | Resource::ValueSet(_)
175 | Resource::StructureDefinition(_) => {
176 let mut resource = resource.clone();
177 let resource_type = get_resource_type(&resource);
178 let resource_type_str = resource_type.as_ref();
179 let id = get_id(&resource);
180 let sha_hash = generate_sha256_hash(&resource);
181
182 add_hash_tag(&mut resource, sha_hash.clone());
183
184 entry_meta.push((resource_type.clone(), id.clone(), sha_hash.clone()));
185
186 artifact_transaction_bundle_entries.push(BundleEntry {
187 resource: Some(Box::new(resource.clone())),
188 request: Some(BundleEntryRequest {
189 method: HttpVerb::put(),
190 url: Box::new(
191 format!(
192 "{resource_type_str}?_id={id}&_tag:not={HASH_TAG_SYSTEM}|{sha_hash}"
193 )
194 .into(),
195 ),
196 ..Default::default()
197 }),
198
199 ..Default::default()
200 });
201 }
202 _ => {
203 }
205 }
206 }
207
208 let batch_response = ctx
209 .client
210 .batch(
211 ctx.clone(),
212 Bundle {
213 type_: BundleType::batch(),
214 entry: Some(artifact_transaction_bundle_entries),
215 ..Default::default()
216 },
217 )
218 .await?;
219
220 let mut total_loaded = 0;
221 for ((resource_type, id, sha_hash), entry) in
222 entry_meta.iter().zip(batch_response.entry.iter().flatten())
223 {
224 match entry.response.as_ref().and_then(|r| r.outcome.as_deref()) {
225 Some(Resource::OperationOutcome(outcome)) if !outcome.issue.is_empty() => {
226 let issue = &outcome.issue[0];
227 let diagnostic = issue
228 .diagnostics
229 .as_deref()
230 .and_then(|d| d.value.as_deref())
231 .unwrap_or("unknown");
232
233 if issue.code == IssueType::invalid() {
234 tracing::error!("{:#?}", outcome);
235 panic!("INVALID");
236 } else if issue.code == IssueType::conflict() {
237 } else {
239 tracing::error!(
240 "Failed to update '{}' with id '{}'. Issue code: '{:?}', diagnostic: '{}'",
241 resource_type.as_ref(),
242 id,
243 issue.code,
244 diagnostic
245 );
246 }
247 }
248 _ => {
249 total_loaded += 1;
250 tracing::info!(
251 "Updated '{}' with id '{}' and sha '{}'",
252 resource_type.as_ref(),
253 entry
254 .resource
255 .as_deref()
256 .and_then(|r| r.id().as_deref())
257 .unwrap_or("unknown"),
258 sha_hash.as_str()
259 );
260 }
261 }
262 }
263
264 tracing::info!(
265 "Loaded a total of '{}' artifacts with unique hashes '{}'",
266 total_loaded,
267 hashes.len()
268 );
269
270 Ok(())
271}
272
273pub async fn load_artifacts(config: Arc<ServerConfig>) -> Result<(), OperationOutcomeError> {
274 let services = create_services(config.clone()).await?;
275
276 let ctx = Arc::new(ServerCTX::system(
277 TenantId::System,
278 ProjectId::System,
279 services.fhir_client.clone(),
280 services.rate_limit.clone(),
281 ));
282
283 _load_artifacts(ctx.clone()).await
284}
285
286pub async fn search_all<Repo: Repository, Search: SearchEngine>(
289 repo: &Repo,
290 search_engine: &Search,
291 tenant: &TenantId,
292 project: &ProjectId,
293 resource_type: ResourceType,
294 parameters: &[(&str, &[&str])],
295) -> Result<Vec<Resource>, OperationOutcomeError> {
296 let parameters: Vec<(String, Vec<String>)> = parameters
297 .iter()
298 .map(|(name, values)| {
299 (
300 (*name).to_string(),
301 values.iter().map(|value| (*value).to_string()).collect(),
302 )
303 })
304 .collect();
305 let results = search_engine
306 .search(
307 &SupportedFHIRVersions::R4,
308 tenant,
309 project,
310 &SearchRequest::Type(FHIRSearchTypeRequest {
311 resource_type,
312 parameters: parameters.into(),
313 }),
314 Some(SearchOptions {
315 count_limit: Some(MAX_COUNT),
316 any_of: None,
317 }),
318 )
319 .await?;
320 let version_ids: Vec<_> = results.entries.iter().map(|e| &e.version_id).collect();
321
322 repo.read_by_version_ids(tenant, project, &version_ids, CachePolicy::NoCache)
323 .await
324}
325
326pub async fn get_all_sds<Repo: Repository, Search: SearchEngine>(
328 kinds: &[&str],
329 repo: &Repo,
330 search_engine: &Search,
331) -> Result<Vec<StructureDefinition>, OperationOutcomeError> {
332 let sds = search_all(
333 repo,
334 search_engine,
335 &TenantId::System,
336 &ProjectId::System,
337 ResourceType::StructureDefinition,
338 &[
339 ("kind", kinds),
340 ("abstract", &["false"]),
341 ("derivation", &["specialization"]),
342 ],
343 )
344 .await?;
345
346 Ok(sds
347 .into_iter()
348 .filter_map(|r| match r {
349 Resource::StructureDefinition(sd) => Some(sd),
350 _ => None,
351 })
352 .collect())
353}
354
355pub async fn get_all_sps<Repo: Repository, Search: SearchEngine>(
357 repo: &Repo,
358 search_engine: &Search,
359) -> Result<Vec<SearchParameter>, OperationOutcomeError> {
360 let sps = search_all(
361 repo,
362 search_engine,
363 &TenantId::System,
364 &ProjectId::System,
365 ResourceType::SearchParameter,
366 &[],
367 )
368 .await?;
369
370 Ok(sps
371 .into_iter()
372 .filter_map(|r| match r {
373 Resource::SearchParameter(sp) => Some(sp),
374 _ => None,
375 })
376 .collect())
377}
378
379pub async fn get_all_operation_definitions<
382 Client: FHIRClient<Arc<ServerCTX<Client>>, OperationOutcomeError> + 'static,
383>(
384 ctx: Arc<ServerCTX<Client>>,
385) -> Result<Vec<OperationDefinition>, OperationOutcomeError> {
386 let operations = ctx
387 .client
388 .search_type(
389 ctx.clone(),
390 ResourceType::OperationDefinition,
391 (vec![] as Vec<(String, Vec<String>)>).into(),
392 )
393 .await?
394 .entry
395 .unwrap_or_default()
396 .into_iter()
397 .filter_map(|e| e.resource)
398 .filter_map(|r| match *r {
399 Resource::OperationDefinition(operation) => Some(operation),
400 _ => None,
401 })
402 .collect();
403
404 Ok(operations)
405}