1use deno_core::cppgc::GcCell;
2use deno_core::error::ModuleLoaderError;
3
4pub mod pool;
5
6use deno_core::{
7 Extension, GarbageCollected, ModuleLoadOptions, ModuleLoadReferrer, ModuleLoader, ModuleType,
8 OpState, PollEventLoopOptions, op2, resolve_import, serde_json, v8,
9};
10use deno_core::error::AnyError;
12use haste_fhir_client::FHIRClient;
13use haste_fhir_client::url::ParsedParameters;
14use haste_fhir_model::r4::generated::resources::{Bundle, Parameters, Resource, ResourceType};
15use haste_fhir_operation_error::OperationOutcomeError;
16use json_patch::Patch;
17use std::cell::RefCell;
18use std::collections::HashMap;
19use std::rc::Rc;
20use std::sync::{Arc, LazyLock};
21use std::time::Duration;
22use tokio::sync::Mutex;
23
24use deno_ast::{MediaType, ModuleSpecifier};
25use deno_ast::{ParseParams, SourceMapOption};
26use deno_core::ModuleLoadResponse;
27use deno_error::JsErrorBox;
28
29pub use pool::DenoPool;
30
31use crate::structs::PluginCodeType;
32
33const EXECUTION_TIMEOUT: Duration = Duration::from_secs(10);
43
44const MAX_HEAP_BYTES: usize = 128 * 1024 * 1024;
49
50const TRANSPILE_CACHE_CAPACITY: usize = 256;
57
58type TranspileCacheKey = (PluginCodeType, String);
59
60static TRANSPILE_CACHE: LazyLock<
61 std::sync::Mutex<HashMap<TranspileCacheKey, (ModuleType, String)>>,
62> = LazyLock::new(|| std::sync::Mutex::new(HashMap::new()));
63
64impl From<PluginCodeType> for MediaType {
65 fn from(value: PluginCodeType) -> Self {
66 match value {
67 PluginCodeType::JavaScript => MediaType::JavaScript,
68 PluginCodeType::TypeScript => MediaType::TypeScript,
69 }
70 }
71}
72
73static RUNTIME_SNAPSHOT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/RUNJS_SNAPSHOT.bin"));
75
76fn transpile_code_to_js(
77 module_specifier: &ModuleSpecifier,
78 media_type: MediaType,
79 code: &str,
80) -> Result<(ModuleType, String), JsErrorBox> {
81 let (module_type, should_transpile) = match media_type {
82 MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs => (ModuleType::JavaScript, false),
83 MediaType::Jsx
84 | MediaType::TypeScript
85 | MediaType::Mts
86 | MediaType::Cts
87 | MediaType::Dts
88 | MediaType::Dmts
89 | MediaType::Dcts
90 | MediaType::Tsx => (ModuleType::JavaScript, true),
91 MediaType::Json => (ModuleType::Json, false),
92 _ => {
93 return Err(JsErrorBox::generic(format!(
94 "Unknown extension {media_type:?}",
95 )));
96 }
97 };
98 if should_transpile {
99 let parsed = deno_ast::parse_module(ParseParams {
100 specifier: module_specifier.clone(),
101 text: code.into(),
102 media_type,
103 capture_tokens: false,
104 scope_analysis: false,
105 maybe_syntax: None,
106 })
107 .map_err(JsErrorBox::from_err)?;
108 let res = parsed
109 .transpile(
110 &deno_ast::TranspileOptions {
111 imports_not_used_as_values: deno_ast::ImportsNotUsedAsValues::Remove,
112 decorators: deno_ast::DecoratorsTranspileOption::Ecma,
113 ..Default::default()
114 },
115 &deno_ast::TranspileModuleOptions { module_kind: None },
116 &deno_ast::EmitOptions {
117 source_map: SourceMapOption::Separate,
118 inline_sources: true,
119 ..Default::default()
120 },
121 )
122 .map_err(JsErrorBox::from_err)?;
123 let res = res.into_source();
124 Ok((module_type, res.text))
127 } else {
128 Ok((module_type, code.to_string()))
129 }
130}
131
132struct TsModuleLoader;
135impl ModuleLoader for TsModuleLoader {
136 fn resolve(
137 &self,
138 specifier: &str,
139 referrer: &str,
140 _kind: deno_core::ResolutionKind,
141 ) -> Result<deno_core::ModuleSpecifier, ModuleLoaderError> {
142 resolve_import(specifier, referrer).map_err(JsErrorBox::from_err)
143 }
144
145 fn load(
146 &self,
147 _module_specifier: &ModuleSpecifier,
148 _maybe_referrer: Option<&ModuleLoadReferrer>,
149 _options: ModuleLoadOptions,
150 ) -> ModuleLoadResponse {
151 ModuleLoadResponse::Sync(Err(JsErrorBox::generic(
152 "Imports are not supported in custom operation scripts.",
153 )))
154 }
155}
156
157fn cached_transpile_to_js(
160 plugin_code_type: PluginCodeType,
161 code: &str,
162) -> Result<(ModuleType, String), JsErrorBox> {
163 let key = (plugin_code_type, code.to_string());
164
165 if let Some(cached) = TRANSPILE_CACHE
166 .lock()
167 .unwrap_or_else(std::sync::PoisonError::into_inner)
168 .get(&key)
169 {
170 return Ok(cached.clone());
171 }
172
173 let user_module_specifier = ModuleSpecifier::parse("memo://user.ts").unwrap();
174 let result = transpile_code_to_js(&user_module_specifier, plugin_code_type.into(), code)?;
175
176 let mut cache = TRANSPILE_CACHE
177 .lock()
178 .unwrap_or_else(std::sync::PoisonError::into_inner);
179 if cache.len() >= TRANSPILE_CACHE_CAPACITY {
180 cache.clear();
181 }
182 cache.insert(key, result.clone());
183
184 Ok(result)
185}
186
187struct ExecutionWatchdog {
197 stop_tx: Option<std::sync::mpsc::Sender<()>>,
198 thread: Option<std::thread::JoinHandle<()>>,
199}
200
201impl ExecutionWatchdog {
202 fn start(isolate_handle: v8::IsolateHandle, timeout: Duration) -> Self {
203 let (stop_tx, stop_rx) = std::sync::mpsc::channel::<()>();
204
205 let thread = std::thread::spawn(move || {
206 if matches!(
207 stop_rx.recv_timeout(timeout),
208 Err(std::sync::mpsc::RecvTimeoutError::Timeout)
209 ) {
210 isolate_handle.terminate_execution();
211 }
212 });
213
214 Self {
215 stop_tx: Some(stop_tx),
216 thread: Some(thread),
217 }
218 }
219}
220
221impl Drop for ExecutionWatchdog {
222 fn drop(&mut self) {
223 if let Some(stop_tx) = self.stop_tx.take() {
224 let _ = stop_tx.send(());
225 }
226 if let Some(thread) = self.thread.take() {
227 let _ = thread.join();
228 }
229 }
230}
231
232struct JSRuntimeState<CTX, Client: FHIRClient<CTX, OperationOutcomeError>> {
233 fhir_client: Arc<Client>,
234 ctx: CTX,
235 return_value: Option<serde_json::Value>,
236 input: Rc<serde_json::Value>,
237}
238
239#[repr(C)]
240pub struct InteropObject {
241 value: GcCell<f64>,
242}
243
244unsafe impl GarbageCollected for InteropObject {
245 fn trace(&self, _visitor: &mut v8::cppgc::Visitor) {}
246 fn get_name(&self) -> &'static std::ffi::CStr {
247 c"InteropObject"
248 }
249}
250
251#[op2]
252impl InteropObject {
253 #[constructor]
254 #[cppgc]
255 fn new(value: f64) -> InteropObject {
256 InteropObject {
257 value: GcCell::new(value),
258 }
259 }
260
261 #[getter]
262 fn value(&self, isolate: &v8::Isolate) -> f64 {
263 *self.value.get(isolate)
264 }
265
266 #[setter]
267 fn value(&self, isolate: &mut v8::Isolate, value: f64) {
268 self.value.set(isolate, value);
269 }
270
271 #[static_method]
277 #[cppgc]
278 fn create(value: f64) -> InteropObject {
279 InteropObject {
280 value: GcCell::new(value),
281 }
282 }
283}
284
285fn runtime_state<CTX: Clone + 'static, Client: FHIRClient<CTX, OperationOutcomeError> + 'static>(
288 state: &Rc<RefCell<OpState>>,
289) -> Arc<Mutex<JSRuntimeState<CTX, Client>>> {
290 state
291 .borrow()
292 .borrow::<Arc<Mutex<JSRuntimeState<CTX, Client>>>>()
293 .clone()
294}
295
296fn parse_resource_type(resource_type: String) -> Result<ResourceType, JsErrorBox> {
297 ResourceType::try_from(resource_type)
298 .map_err(|_| JsErrorBox::type_error("Invalid resource type"))
299}
300
301fn parse_query(query: &str) -> Result<ParsedParameters, JsErrorBox> {
304 ParsedParameters::try_from(query)
305 .map_err(|error| JsErrorBox::type_error(format!("Invalid search parameters: {error}")))
306}
307
308fn parse_body<T: serde::de::DeserializeOwned>(
309 value: serde_json::Value,
310 what: &str,
311) -> Result<T, JsErrorBox> {
312 serde_json::from_value(value)
313 .map_err(|error| JsErrorBox::type_error(format!("Invalid {what}: {error}")))
314}
315
316fn to_json<T: serde::Serialize>(value: &T) -> Result<serde_json::Value, JsErrorBox> {
317 serde_json::to_value(value).map_err(|_| JsErrorBox::type_error("Failed to serialize response"))
318}
319
320fn map_fhir_error(operation: &'static str) -> impl FnOnce(OperationOutcomeError) -> JsErrorBox {
325 move |error| {
326 tracing::error!(error = ?error, operation, "FHIR operation failed in custom operation script");
327 JsErrorBox::type_error(error.to_string())
328 }
329}
330
331#[op2]
332#[serde]
333pub async fn fhir_capabilities<
334 CTX: Clone + 'static,
335 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
336>(
337 state: Rc<RefCell<OpState>>,
338) -> Result<serde_json::Value, JsErrorBox> {
339 let app_state = runtime_state::<CTX, Client>(&state);
340 let app_state = app_state.lock().await;
341
342 let capabilities = app_state
343 .fhir_client
344 .capabilities(app_state.ctx.clone())
345 .await
346 .map_err(map_fhir_error("capabilities"))?;
347
348 to_json(&capabilities)
349}
350
351#[op2]
352#[serde]
353pub async fn fhir_search_type<
354 CTX: Clone + 'static,
355 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
356>(
357 state: Rc<RefCell<OpState>>,
358 #[string] resource_type: String,
359 #[string] query: String,
360) -> Result<serde_json::Value, JsErrorBox> {
361 let resource_type = parse_resource_type(resource_type)?;
362 let query = parse_query(&query)?;
363 let app_state = runtime_state::<CTX, Client>(&state);
364 let app_state = app_state.lock().await;
365
366 let bundle = app_state
367 .fhir_client
368 .search_type(app_state.ctx.clone(), resource_type, query)
369 .await
370 .map_err(map_fhir_error("searchType"))?;
371
372 to_json(&bundle)
373}
374
375#[op2]
376#[serde]
377pub async fn fhir_search_system<
378 CTX: Clone + 'static,
379 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
380>(
381 state: Rc<RefCell<OpState>>,
382 #[string] query: String,
383) -> Result<serde_json::Value, JsErrorBox> {
384 let query = parse_query(&query)?;
385 let app_state = runtime_state::<CTX, Client>(&state);
386 let app_state = app_state.lock().await;
387
388 let bundle = app_state
389 .fhir_client
390 .search_system(app_state.ctx.clone(), query)
391 .await
392 .map_err(map_fhir_error("searchSystem"))?;
393
394 to_json(&bundle)
395}
396
397#[op2]
398#[serde]
399pub async fn fhir_create<
400 CTX: Clone + 'static,
401 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
402>(
403 state: Rc<RefCell<OpState>>,
404 #[string] resource_type: String,
405 #[serde] resource: serde_json::Value,
406) -> Result<serde_json::Value, JsErrorBox> {
407 let resource_type = parse_resource_type(resource_type)?;
408 let resource: Resource = parse_body(resource, "resource")?;
409 let app_state = runtime_state::<CTX, Client>(&state);
410 let app_state = app_state.lock().await;
411
412 let created = app_state
413 .fhir_client
414 .create(app_state.ctx.clone(), resource_type, resource)
415 .await
416 .map_err(map_fhir_error("create"))?;
417
418 to_json(&created)
419}
420
421#[op2]
422#[serde]
423pub async fn fhir_update<
424 CTX: Clone + 'static,
425 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
426>(
427 state: Rc<RefCell<OpState>>,
428 #[string] resource_type: String,
429 #[string] id: String,
430 #[serde] resource: serde_json::Value,
431) -> Result<serde_json::Value, JsErrorBox> {
432 let resource_type = parse_resource_type(resource_type)?;
433 let resource: Resource = parse_body(resource, "resource")?;
434 let app_state = runtime_state::<CTX, Client>(&state);
435 let app_state = app_state.lock().await;
436
437 let updated = app_state
438 .fhir_client
439 .update(app_state.ctx.clone(), resource_type, id, resource)
440 .await
441 .map_err(map_fhir_error("update"))?;
442
443 to_json(&updated)
444}
445
446#[op2]
447#[serde]
448pub async fn fhir_conditional_update<
449 CTX: Clone + 'static,
450 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
451>(
452 state: Rc<RefCell<OpState>>,
453 #[string] resource_type: String,
454 #[string] query: String,
455 #[serde] resource: serde_json::Value,
456) -> Result<serde_json::Value, JsErrorBox> {
457 let resource_type = parse_resource_type(resource_type)?;
458 let query = parse_query(&query)?;
459 let resource: Resource = parse_body(resource, "resource")?;
460 let app_state = runtime_state::<CTX, Client>(&state);
461 let app_state = app_state.lock().await;
462
463 let updated = app_state
464 .fhir_client
465 .conditional_update(app_state.ctx.clone(), resource_type, query, resource)
466 .await
467 .map_err(map_fhir_error("conditionalUpdate"))?;
468
469 to_json(&updated)
470}
471
472#[op2]
473#[serde]
474pub async fn fhir_patch<
475 CTX: Clone + 'static,
476 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
477>(
478 state: Rc<RefCell<OpState>>,
479 #[string] resource_type: String,
480 #[string] id: String,
481 #[serde] patch: serde_json::Value,
482) -> Result<serde_json::Value, JsErrorBox> {
483 let resource_type = parse_resource_type(resource_type)?;
484 let patch: Patch = parse_body(patch, "patch")?;
485 let app_state = runtime_state::<CTX, Client>(&state);
486 let app_state = app_state.lock().await;
487
488 let patched = app_state
489 .fhir_client
490 .patch(app_state.ctx.clone(), resource_type, id, patch)
491 .await
492 .map_err(map_fhir_error("patch"))?;
493
494 to_json(&patched)
495}
496
497#[op2]
498#[serde]
499pub async fn fhir_read<
500 CTX: Clone + 'static,
501 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
502>(
503 state: Rc<RefCell<OpState>>,
504 #[string] resource_type: String,
505 #[string] id: String,
506) -> Result<serde_json::Value, JsErrorBox> {
507 let resource_type = parse_resource_type(resource_type)?;
508 let app_state = runtime_state::<CTX, Client>(&state);
509 let app_state = app_state.lock().await;
510
511 let resource = app_state
512 .fhir_client
513 .read(app_state.ctx.clone(), resource_type, id)
514 .await
515 .map_err(map_fhir_error("read"))?;
516
517 match resource {
518 Some(resource) => to_json(&resource),
519 None => Ok(serde_json::Value::Null),
520 }
521}
522
523#[op2]
524#[serde]
525pub async fn fhir_vread<
526 CTX: Clone + 'static,
527 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
528>(
529 state: Rc<RefCell<OpState>>,
530 #[string] resource_type: String,
531 #[string] id: String,
532 #[string] version_id: String,
533) -> Result<serde_json::Value, JsErrorBox> {
534 let resource_type = parse_resource_type(resource_type)?;
535 let app_state = runtime_state::<CTX, Client>(&state);
536 let app_state = app_state.lock().await;
537
538 let resource = app_state
539 .fhir_client
540 .vread(app_state.ctx.clone(), resource_type, id, version_id)
541 .await
542 .map_err(map_fhir_error("vread"))?;
543
544 match resource {
545 Some(resource) => to_json(&resource),
546 None => Ok(serde_json::Value::Null),
547 }
548}
549
550#[op2]
551pub async fn fhir_delete_instance<
552 CTX: Clone + 'static,
553 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
554>(
555 state: Rc<RefCell<OpState>>,
556 #[string] resource_type: String,
557 #[string] id: String,
558) -> Result<(), JsErrorBox> {
559 let resource_type = parse_resource_type(resource_type)?;
560 let app_state = runtime_state::<CTX, Client>(&state);
561 let app_state = app_state.lock().await;
562
563 app_state
564 .fhir_client
565 .delete_instance(app_state.ctx.clone(), resource_type, id)
566 .await
567 .map_err(map_fhir_error("deleteInstance"))
568}
569
570#[op2]
571pub async fn fhir_delete_type<
572 CTX: Clone + 'static,
573 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
574>(
575 state: Rc<RefCell<OpState>>,
576 #[string] resource_type: String,
577 #[string] query: String,
578) -> Result<(), JsErrorBox> {
579 let resource_type = parse_resource_type(resource_type)?;
580 let query = parse_query(&query)?;
581 let app_state = runtime_state::<CTX, Client>(&state);
582 let app_state = app_state.lock().await;
583
584 app_state
585 .fhir_client
586 .delete_type(app_state.ctx.clone(), resource_type, query)
587 .await
588 .map_err(map_fhir_error("deleteType"))
589}
590
591#[op2]
592pub async fn fhir_delete_system<
593 CTX: Clone + 'static,
594 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
595>(
596 state: Rc<RefCell<OpState>>,
597 #[string] query: String,
598) -> Result<(), JsErrorBox> {
599 let query = parse_query(&query)?;
600 let app_state = runtime_state::<CTX, Client>(&state);
601 let app_state = app_state.lock().await;
602
603 app_state
604 .fhir_client
605 .delete_system(app_state.ctx.clone(), query)
606 .await
607 .map_err(map_fhir_error("deleteSystem"))
608}
609
610#[op2]
611#[serde]
612pub async fn fhir_history_instance<
613 CTX: Clone + 'static,
614 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
615>(
616 state: Rc<RefCell<OpState>>,
617 #[string] resource_type: String,
618 #[string] id: String,
619 #[string] query: String,
620) -> Result<serde_json::Value, JsErrorBox> {
621 let resource_type = parse_resource_type(resource_type)?;
622 let query = parse_query(&query)?;
623 let app_state = runtime_state::<CTX, Client>(&state);
624 let app_state = app_state.lock().await;
625
626 let bundle = app_state
627 .fhir_client
628 .history_instance(app_state.ctx.clone(), resource_type, id, query)
629 .await
630 .map_err(map_fhir_error("historyInstance"))?;
631
632 to_json(&bundle)
633}
634
635#[op2]
636#[serde]
637pub async fn fhir_history_type<
638 CTX: Clone + 'static,
639 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
640>(
641 state: Rc<RefCell<OpState>>,
642 #[string] resource_type: String,
643 #[string] query: String,
644) -> Result<serde_json::Value, JsErrorBox> {
645 let resource_type = parse_resource_type(resource_type)?;
646 let query = parse_query(&query)?;
647 let app_state = runtime_state::<CTX, Client>(&state);
648 let app_state = app_state.lock().await;
649
650 let bundle = app_state
651 .fhir_client
652 .history_type(app_state.ctx.clone(), resource_type, query)
653 .await
654 .map_err(map_fhir_error("historyType"))?;
655
656 to_json(&bundle)
657}
658
659#[op2]
660#[serde]
661pub async fn fhir_history_system<
662 CTX: Clone + 'static,
663 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
664>(
665 state: Rc<RefCell<OpState>>,
666 #[string] query: String,
667) -> Result<serde_json::Value, JsErrorBox> {
668 let query = parse_query(&query)?;
669 let app_state = runtime_state::<CTX, Client>(&state);
670 let app_state = app_state.lock().await;
671
672 let bundle = app_state
673 .fhir_client
674 .history_system(app_state.ctx.clone(), query)
675 .await
676 .map_err(map_fhir_error("historySystem"))?;
677
678 to_json(&bundle)
679}
680
681#[op2]
682#[serde]
683pub async fn fhir_invoke_instance<
684 CTX: Clone + 'static,
685 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
686>(
687 state: Rc<RefCell<OpState>>,
688 #[string] resource_type: String,
689 #[string] id: String,
690 #[string] operation: String,
691 #[serde] parameters: serde_json::Value,
692) -> Result<serde_json::Value, JsErrorBox> {
693 let resource_type = parse_resource_type(resource_type)?;
694 let parameters: Parameters = parse_body(parameters, "parameters")?;
695 let app_state = runtime_state::<CTX, Client>(&state);
696 let app_state = app_state.lock().await;
697
698 let result = app_state
699 .fhir_client
700 .invoke_instance(
701 app_state.ctx.clone(),
702 resource_type,
703 id,
704 operation,
705 parameters,
706 )
707 .await
708 .map_err(map_fhir_error("invokeInstance"))?;
709
710 to_json(&result)
711}
712
713#[op2]
714#[serde]
715pub async fn fhir_invoke_type<
716 CTX: Clone + 'static,
717 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
718>(
719 state: Rc<RefCell<OpState>>,
720 #[string] resource_type: String,
721 #[string] operation: String,
722 #[serde] parameters: serde_json::Value,
723) -> Result<serde_json::Value, JsErrorBox> {
724 let resource_type = parse_resource_type(resource_type)?;
725 let parameters: Parameters = parse_body(parameters, "parameters")?;
726 let app_state = runtime_state::<CTX, Client>(&state);
727 let app_state = app_state.lock().await;
728
729 let result = app_state
730 .fhir_client
731 .invoke_type(app_state.ctx.clone(), resource_type, operation, parameters)
732 .await
733 .map_err(map_fhir_error("invokeType"))?;
734
735 to_json(&result)
736}
737
738#[op2]
739#[serde]
740pub async fn fhir_invoke_system<
741 CTX: Clone + 'static,
742 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
743>(
744 state: Rc<RefCell<OpState>>,
745 #[string] operation: String,
746 #[serde] parameters: serde_json::Value,
747) -> Result<serde_json::Value, JsErrorBox> {
748 let parameters: Parameters = parse_body(parameters, "parameters")?;
749 let app_state = runtime_state::<CTX, Client>(&state);
750 let app_state = app_state.lock().await;
751
752 let result = app_state
753 .fhir_client
754 .invoke_system(app_state.ctx.clone(), operation, parameters)
755 .await
756 .map_err(map_fhir_error("invokeSystem"))?;
757
758 to_json(&result)
759}
760
761#[op2]
762#[serde]
763pub async fn fhir_transaction<
764 CTX: Clone + 'static,
765 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
766>(
767 state: Rc<RefCell<OpState>>,
768 #[serde] bundle: serde_json::Value,
769) -> Result<serde_json::Value, JsErrorBox> {
770 let bundle: Bundle = parse_body(bundle, "bundle")?;
771 let app_state = runtime_state::<CTX, Client>(&state);
772 let app_state = app_state.lock().await;
773
774 let result = app_state
775 .fhir_client
776 .transaction(app_state.ctx.clone(), bundle)
777 .await
778 .map_err(map_fhir_error("transaction"))?;
779
780 to_json(&result)
781}
782
783#[op2]
784#[serde]
785pub async fn fhir_batch<
786 CTX: Clone + 'static,
787 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
788>(
789 state: Rc<RefCell<OpState>>,
790 #[serde] bundle: serde_json::Value,
791) -> Result<serde_json::Value, JsErrorBox> {
792 let bundle: Bundle = parse_body(bundle, "bundle")?;
793 let app_state = runtime_state::<CTX, Client>(&state);
794 let app_state = app_state.lock().await;
795
796 let result = app_state
797 .fhir_client
798 .batch(app_state.ctx.clone(), bundle)
799 .await
800 .map_err(map_fhir_error("batch"))?;
801
802 to_json(&result)
803}
804
805#[op2]
806#[serde]
807pub async fn set_return_value<
808 CTX: Clone + 'static,
809 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
810>(
811 state: Rc<RefCell<OpState>>,
812 #[serde] value: serde_json::Value,
813) -> Result<(), JsErrorBox> {
814 let app_state = runtime_state::<CTX, Client>(&state);
815 let mut app_state = app_state.lock().await;
816
817 app_state.return_value = Some(value);
818 Ok(())
819}
820
821#[op2]
822#[serde]
823pub async fn get_input_value<
824 CTX: Clone + 'static,
825 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
826>(
827 state: Rc<RefCell<OpState>>,
828) -> Result<serde_json::Value, JsErrorBox> {
829 let app_state = runtime_state::<CTX, Client>(&state);
830 let app_state = app_state.lock().await;
831
832 Ok(serde_json::json!({
833 "request": app_state.input.clone(),
834 }))
835}
836
837fn build_deno_runtime<
840 CTX: Clone + 'static,
841 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
842>() -> deno_core::JsRuntime {
843 let runjs = Extension {
844 name: "runjs",
845 ops: std::borrow::Cow::Owned(vec![
846 fhir_capabilities::<CTX, Client>(),
847 fhir_search_type::<CTX, Client>(),
848 fhir_search_system::<CTX, Client>(),
849 fhir_create::<CTX, Client>(),
850 fhir_update::<CTX, Client>(),
851 fhir_conditional_update::<CTX, Client>(),
852 fhir_patch::<CTX, Client>(),
853 fhir_read::<CTX, Client>(),
854 fhir_vread::<CTX, Client>(),
855 fhir_delete_instance::<CTX, Client>(),
856 fhir_delete_type::<CTX, Client>(),
857 fhir_delete_system::<CTX, Client>(),
858 fhir_history_instance::<CTX, Client>(),
859 fhir_history_type::<CTX, Client>(),
860 fhir_history_system::<CTX, Client>(),
861 fhir_invoke_instance::<CTX, Client>(),
862 fhir_invoke_type::<CTX, Client>(),
863 fhir_invoke_system::<CTX, Client>(),
864 fhir_transaction::<CTX, Client>(),
865 fhir_batch::<CTX, Client>(),
866 set_return_value::<CTX, Client>(),
867 get_input_value::<CTX, Client>(),
868 ]),
869 ..Default::default()
870 };
871
872 let create_params = v8::Isolate::create_params().heap_limits(0, MAX_HEAP_BYTES);
873
874 deno_core::JsRuntime::new(deno_core::RuntimeOptions {
875 module_loader: Some(Rc::new(TsModuleLoader)),
876 extensions: vec![runjs],
877 startup_snapshot: Some(RUNTIME_SNAPSHOT),
878 create_params: Some(create_params),
879 ..Default::default()
880 })
881}
882
883async fn run_code<
884 CTX: Clone + 'static,
885 Client: FHIRClient<CTX, OperationOutcomeError> + 'static,
886>(
887 mut deno_runtime: deno_core::JsRuntime,
888 ctx: CTX,
889 client: Arc<Client>,
890 media_type: PluginCodeType,
891 code: &str,
892 input: serde_json::Value,
893 timeout: Duration,
894) -> Result<Option<serde_json::Value>, AnyError> {
895 let isolate_handle = deno_runtime.v8_isolate().thread_safe_handle();
896
897 {
898 let terminate_handle = isolate_handle.clone();
899 deno_runtime.add_near_heap_limit_callback(move |current_limit, _initial_limit| {
900 terminate_handle.terminate_execution();
903 current_limit * 2
904 });
905 }
906
907 let _watchdog = ExecutionWatchdog::start(isolate_handle, timeout);
910
911 let js_runtime_state = Arc::new(Mutex::new(JSRuntimeState {
912 fhir_client: client,
913 ctx,
914 return_value: None,
915 input: Rc::new(input),
916 }));
917
918 {
919 let op_state = deno_runtime.op_state();
920 let mut op_state = op_state.borrow_mut();
921 op_state.put(js_runtime_state.clone());
922 }
923
924 let user_module_specifier = ModuleSpecifier::parse("memo://user.ts").unwrap();
925
926 let (_module_type, js_code) = cached_transpile_to_js(media_type, code)?;
927
928 let user_mod_id = deno_runtime
929 .load_side_es_module_from_code(&user_module_specifier, js_code)
930 .await?;
931
932 let main_mod_id = deno_runtime
933 .load_main_es_module_from_code(
934 &ModuleSpecifier::parse("memo://main.ts").unwrap(),
935 "import userFunction from 'memo://user.ts'; _internal_.setReturnValue(await userFunction(await _internal_.getInputValue()));"
936 .to_string(),
937 )
938 .await?;
939
940 let user_module_load = deno_runtime.mod_evaluate(user_mod_id);
942 let main_module_load = deno_runtime.mod_evaluate(main_mod_id);
943
944 deno_runtime
945 .run_event_loop(PollEventLoopOptions::default())
946 .await?;
947
948 user_module_load.await?;
949 main_module_load.await?;
950 {
953 let op_state = deno_runtime.op_state();
954 let mut op_state = op_state.borrow_mut();
955
956 op_state.take::<Arc<Mutex<JSRuntimeState<CTX, Client>>>>();
957 }
958
959 let owned_runetime_state = Arc::try_unwrap(js_runtime_state)
960 .map_err(|_| deno_error::JsErrorBox::type_error("Failed to unwrap JSRuntimeState"))?
961 .into_inner();
962
963 Ok(owned_runetime_state.return_value)
964}
965
966#[cfg(test)]
967pub(crate) mod tests {
968 use super::*;
969 use deno_core::serde_json::json;
970 use haste_fhir_client::request::{FHIRRequest, FHIRResponse};
971 use haste_fhir_client::url::ParsedParameters;
972 use haste_fhir_model::r4::generated::resources::{
973 Bundle, CapabilityStatement, Parameters, Resource,
974 };
975 use haste_fhir_model::r4::generated::terminology::IssueType;
976 use json_patch::Patch;
977 use std::time::Instant;
978
979 #[derive(Clone)]
980 pub(crate) struct TestCtx;
981
982 pub(crate) struct MockClient;
986
987 impl FHIRClient<TestCtx, OperationOutcomeError> for MockClient {
988 async fn request(
989 &self,
990 _ctx: TestCtx,
991 _request: FHIRRequest,
992 ) -> Result<FHIRResponse, OperationOutcomeError> {
993 unimplemented!("not used by these tests")
994 }
995
996 async fn capabilities(
997 &self,
998 _ctx: TestCtx,
999 ) -> Result<CapabilityStatement, OperationOutcomeError> {
1000 Ok(CapabilityStatement::default())
1001 }
1002
1003 async fn search_system(
1004 &self,
1005 _ctx: TestCtx,
1006 _parameters: ParsedParameters,
1007 ) -> Result<Bundle, OperationOutcomeError> {
1008 unimplemented!("not used by these tests")
1009 }
1010
1011 async fn search_type(
1012 &self,
1013 _ctx: TestCtx,
1014 _resource_type: ResourceType,
1015 _parameters: ParsedParameters,
1016 ) -> Result<Bundle, OperationOutcomeError> {
1017 Ok(Bundle::default())
1018 }
1019
1020 async fn create(
1021 &self,
1022 _ctx: TestCtx,
1023 _resource_type: ResourceType,
1024 resource: Resource,
1025 ) -> Result<Resource, OperationOutcomeError> {
1026 Ok(resource)
1027 }
1028
1029 async fn update(
1030 &self,
1031 _ctx: TestCtx,
1032 _resource_type: ResourceType,
1033 _id: String,
1034 _resource: Resource,
1035 ) -> Result<Resource, OperationOutcomeError> {
1036 unimplemented!("not used by these tests")
1037 }
1038
1039 async fn conditional_update(
1040 &self,
1041 _ctx: TestCtx,
1042 _resource_type: ResourceType,
1043 _parameters: ParsedParameters,
1044 _resource: Resource,
1045 ) -> Result<Resource, OperationOutcomeError> {
1046 unimplemented!("not used by these tests")
1047 }
1048
1049 async fn patch(
1050 &self,
1051 _ctx: TestCtx,
1052 _resource_type: ResourceType,
1053 _id: String,
1054 _patches: Patch,
1055 ) -> Result<Resource, OperationOutcomeError> {
1056 unimplemented!("not used by these tests")
1057 }
1058
1059 async fn read(
1060 &self,
1061 _ctx: TestCtx,
1062 _resource_type: ResourceType,
1063 _id: String,
1064 ) -> Result<Option<Resource>, OperationOutcomeError> {
1065 Ok(None)
1066 }
1067
1068 async fn vread(
1069 &self,
1070 _ctx: TestCtx,
1071 _resource_type: ResourceType,
1072 _id: String,
1073 _version_id: String,
1074 ) -> Result<Option<Resource>, OperationOutcomeError> {
1075 unimplemented!("not used by these tests")
1076 }
1077
1078 async fn delete_instance(
1079 &self,
1080 _ctx: TestCtx,
1081 _resource_type: ResourceType,
1082 _id: String,
1083 ) -> Result<(), OperationOutcomeError> {
1084 Ok(())
1085 }
1086
1087 async fn delete_type(
1088 &self,
1089 _ctx: TestCtx,
1090 _resource_type: ResourceType,
1091 _parameters: ParsedParameters,
1092 ) -> Result<(), OperationOutcomeError> {
1093 unimplemented!("not used by these tests")
1094 }
1095
1096 async fn delete_system(
1097 &self,
1098 _ctx: TestCtx,
1099 _parameters: ParsedParameters,
1100 ) -> Result<(), OperationOutcomeError> {
1101 unimplemented!("not used by these tests")
1102 }
1103
1104 async fn history_system(
1105 &self,
1106 _ctx: TestCtx,
1107 _parameters: ParsedParameters,
1108 ) -> Result<Bundle, OperationOutcomeError> {
1109 unimplemented!("not used by these tests")
1110 }
1111
1112 async fn history_type(
1113 &self,
1114 _ctx: TestCtx,
1115 _resource_type: ResourceType,
1116 _parameters: ParsedParameters,
1117 ) -> Result<Bundle, OperationOutcomeError> {
1118 unimplemented!("not used by these tests")
1119 }
1120
1121 async fn history_instance(
1122 &self,
1123 _ctx: TestCtx,
1124 _resource_type: ResourceType,
1125 _id: String,
1126 _parameters: ParsedParameters,
1127 ) -> Result<Bundle, OperationOutcomeError> {
1128 unimplemented!("not used by these tests")
1129 }
1130
1131 async fn invoke_instance(
1132 &self,
1133 _ctx: TestCtx,
1134 _resource_type: ResourceType,
1135 _id: String,
1136 _operation: String,
1137 _parameters: Parameters,
1138 ) -> Result<Resource, OperationOutcomeError> {
1139 unimplemented!("not used by these tests")
1140 }
1141
1142 async fn invoke_type(
1143 &self,
1144 _ctx: TestCtx,
1145 _resource_type: ResourceType,
1146 _operation: String,
1147 _parameters: Parameters,
1148 ) -> Result<Resource, OperationOutcomeError> {
1149 unimplemented!("not used by these tests")
1150 }
1151
1152 async fn invoke_system(
1153 &self,
1154 _ctx: TestCtx,
1155 operation: String,
1156 _parameters: Parameters,
1157 ) -> Result<Resource, OperationOutcomeError> {
1158 serde_json::from_value(json!({ "resourceType": "Parameters", "id": operation }))
1159 .map_err(|error| {
1160 OperationOutcomeError::error(IssueType::invalid(), error.to_string())
1161 })
1162 }
1163
1164 async fn transaction(
1165 &self,
1166 _ctx: TestCtx,
1167 _bundle: Bundle,
1168 ) -> Result<Bundle, OperationOutcomeError> {
1169 unimplemented!("not used by these tests")
1170 }
1171
1172 async fn batch(
1173 &self,
1174 _ctx: TestCtx,
1175 _bundle: Bundle,
1176 ) -> Result<Bundle, OperationOutcomeError> {
1177 unimplemented!("not used by these tests")
1178 }
1179 }
1180
1181 #[tokio::test]
1182 async fn happy_path_javascript_returns_value() {
1183 let deno_runtime = build_deno_runtime::<TestCtx, MockClient>();
1184 let result = run_code(
1185 deno_runtime,
1186 TestCtx,
1187 Arc::new(MockClient),
1188 PluginCodeType::JavaScript,
1189 "export default async function () { return { answer: 42 }; }",
1190 json!({}),
1191 Duration::from_secs(5),
1192 )
1193 .await
1194 .expect("script should run successfully");
1195
1196 assert_eq!(result, Some(json!({ "answer": 42 })));
1197 }
1198
1199 #[tokio::test]
1200 async fn happy_path_typescript_returns_value() {
1201 let deno_runtime = build_deno_runtime::<TestCtx, MockClient>();
1202 let result = run_code(
1203 deno_runtime,
1204 TestCtx,
1205 Arc::new(MockClient),
1206 PluginCodeType::TypeScript,
1207 "export default async function (): Promise<{ answer: number }> { return { answer: 42 }; }",
1208 json!({}),
1209 Duration::from_secs(5),
1210 )
1211 .await
1212 .expect("script should run successfully");
1213
1214 assert_eq!(result, Some(json!({ "answer": 42 })));
1215 }
1216
1217 #[tokio::test]
1218 async fn fhir_create_op_round_trips_through_the_client() {
1219 let deno_runtime = build_deno_runtime::<TestCtx, MockClient>();
1220 let result = run_code(
1221 deno_runtime,
1222 TestCtx,
1223 Arc::new(MockClient),
1224 PluginCodeType::JavaScript,
1225 "export default async function () { return await fhir.create('Patient', { resourceType: 'Patient', id: 'example' }); }",
1226 json!({}),
1227 Duration::from_secs(5),
1228 )
1229 .await
1230 .expect("script should call fhir.create");
1231
1232 assert_eq!(
1233 result,
1234 Some(json!({ "resourceType": "Patient", "id": "example" }))
1235 );
1236 }
1237
1238 #[tokio::test]
1239 async fn fhir_search_type_op_returns_a_bundle() {
1240 let deno_runtime = build_deno_runtime::<TestCtx, MockClient>();
1241 let result = run_code(
1242 deno_runtime,
1243 TestCtx,
1244 Arc::new(MockClient),
1245 PluginCodeType::JavaScript,
1246 "export default async function () { return await fhir.searchType('Patient', { name: 'Smith', _count: 10 }); }",
1247 json!({}),
1248 Duration::from_secs(5),
1249 )
1250 .await
1251 .expect("script should call fhir.searchType");
1252
1253 assert_eq!(result, Some(json!({ "resourceType": "Bundle" })));
1254 }
1255
1256 #[tokio::test]
1257 async fn fhir_delete_instance_op_resolves() {
1258 let deno_runtime = build_deno_runtime::<TestCtx, MockClient>();
1259 let result = run_code(
1260 deno_runtime,
1261 TestCtx,
1262 Arc::new(MockClient),
1263 PluginCodeType::JavaScript,
1264 "export default async function () { await fhir.deleteInstance('Patient', 'example'); return { deleted: true }; }",
1265 json!({}),
1266 Duration::from_secs(5),
1267 )
1268 .await
1269 .expect("script should call fhir.deleteInstance");
1270
1271 assert_eq!(result, Some(json!({ "deleted": true })));
1272 }
1273
1274 #[tokio::test]
1275 async fn fhir_invoke_system_op_defaults_missing_parameters() {
1276 let deno_runtime = build_deno_runtime::<TestCtx, MockClient>();
1277 let result = run_code(
1278 deno_runtime,
1279 TestCtx,
1280 Arc::new(MockClient),
1281 PluginCodeType::JavaScript,
1282 "export default async function () { return await fhir.invokeSystem('everything'); }",
1283 json!({}),
1284 Duration::from_secs(5),
1285 )
1286 .await
1287 .expect("script should call fhir.invokeSystem");
1288
1289 assert_eq!(
1290 result,
1291 Some(json!({ "resourceType": "Parameters", "id": "everything" }))
1292 );
1293 }
1294
1295 #[tokio::test]
1296 async fn fhir_capabilities_op_returns_the_capability_statement() {
1297 let deno_runtime = build_deno_runtime::<TestCtx, MockClient>();
1298 let result = run_code(
1299 deno_runtime,
1300 TestCtx,
1301 Arc::new(MockClient),
1302 PluginCodeType::JavaScript,
1303 "export default async function () { return await fhir.capabilities(); }",
1304 json!({}),
1305 Duration::from_secs(5),
1306 )
1307 .await
1308 .expect("script should call fhir.capabilities");
1309
1310 assert_eq!(
1311 result,
1312 Some(json!({ "resourceType": "CapabilityStatement" }))
1313 );
1314 }
1315
1316 #[tokio::test]
1317 async fn imports_are_rejected() {
1318 let deno_runtime = build_deno_runtime::<TestCtx, MockClient>();
1322 let result = run_code(
1323 deno_runtime,
1324 TestCtx,
1325 Arc::new(MockClient),
1326 PluginCodeType::JavaScript,
1327 "import secret from 'file:///etc/passwd'; export default async function () { return secret; }",
1328 json!({}),
1329 Duration::from_secs(5),
1330 )
1331 .await;
1332
1333 assert!(
1334 result.is_err(),
1335 "importing an arbitrary file must never succeed"
1336 );
1337 }
1338
1339 #[tokio::test]
1340 async fn runaway_script_is_terminated_within_timeout() {
1341 let timeout = Duration::from_millis(200);
1342 let started = Instant::now();
1343
1344 let deno_runtime = build_deno_runtime::<TestCtx, MockClient>();
1345 let result = run_code(
1346 deno_runtime,
1347 TestCtx,
1348 Arc::new(MockClient),
1349 PluginCodeType::JavaScript,
1350 "export default async function () { while (true) {} }",
1351 json!({}),
1352 timeout,
1353 )
1354 .await;
1355
1356 assert!(result.is_err(), "runaway script must be terminated");
1357 assert!(
1358 started.elapsed() < timeout * 5,
1359 "termination took far longer than the configured timeout: {:?}",
1360 started.elapsed()
1361 );
1362 }
1363}