use serde_json::json;
use helios_persistence::core::{IncludeProvider, ResourceStorage, SearchProvider};
use helios_persistence::tenant::{TenantContext, TenantId, TenantPermissions};
use helios_persistence::types::{
IncludeDirective, IncludeType, Pagination, SearchQuery,
};
#[cfg(feature = "sqlite")]
use helios_persistence::backends::sqlite::SqliteBackend;
#[cfg(feature = "sqlite")]
fn create_sqlite_backend() -> SqliteBackend {
let backend = SqliteBackend::in_memory().expect("Failed to create SQLite backend");
backend.init_schema().expect("Failed to initialize schema");
backend
}
fn create_tenant() -> TenantContext {
TenantContext::new(TenantId::new("test-tenant"), TenantPermissions::full_access())
}
#[cfg(feature = "sqlite")]
async fn seed_include_data(backend: &SqliteBackend, tenant: &TenantContext) {
let org = json!({
"resourceType": "Organization",
"id": "org-hospital",
"name": "Test Hospital"
});
backend.create_or_update(tenant, "Organization", "org-hospital", org).await.unwrap();
let patient1 = json!({
"resourceType": "Patient",
"id": "patient-1",
"name": [{"family": "Smith"}],
"managingOrganization": {"reference": "Organization/org-hospital"}
});
let patient2 = json!({
"resourceType": "Patient",
"id": "patient-2",
"name": [{"family": "Jones"}],
"managingOrganization": {"reference": "Organization/org-hospital"}
});
backend.create_or_update(tenant, "Patient", "patient-1", patient1).await.unwrap();
backend.create_or_update(tenant, "Patient", "patient-2", patient2).await.unwrap();
let obs1 = json!({
"resourceType": "Observation",
"status": "final",
"subject": {"reference": "Patient/patient-1"},
"code": {"coding": [{"code": "test"}]}
});
let obs2 = json!({
"resourceType": "Observation",
"status": "final",
"subject": {"reference": "Patient/patient-1"},
"code": {"coding": [{"code": "test2"}]}
});
backend.create(tenant, "Observation", obs1).await.unwrap();
backend.create(tenant, "Observation", obs2).await.unwrap();
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_include_basic() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
seed_include_data(&backend, &tenant).await;
let query = SearchQuery::new("Observation").with_include(IncludeDirective {
include_type: IncludeType::Include,
source_type: "Observation".to_string(),
search_param: "subject".to_string(),
target_type: Some("Patient".to_string()),
iterate: false,
});
let result = backend
.search(&tenant, &query, Pagination::new(100))
.await
.unwrap();
assert!(!result.resources.is_empty());
assert!(!result.included.is_empty());
for resource in &result.included {
assert_eq!(resource.resource_type(), "Patient");
}
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_include_with_target_type() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
seed_include_data(&backend, &tenant).await;
let query = SearchQuery::new("Patient").with_include(IncludeDirective {
include_type: IncludeType::Include,
source_type: "Patient".to_string(),
search_param: "organization".to_string(),
target_type: Some("Organization".to_string()),
iterate: false,
});
let result = backend
.search(&tenant, &query, Pagination::new(100))
.await
.unwrap();
for resource in &result.included {
assert_eq!(resource.resource_type(), "Organization");
}
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_include_iterate() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
seed_include_data(&backend, &tenant).await;
let query = SearchQuery::new("Observation")
.with_include(IncludeDirective {
include_type: IncludeType::Include,
source_type: "Observation".to_string(),
search_param: "subject".to_string(),
target_type: Some("Patient".to_string()),
iterate: false,
})
.with_include(IncludeDirective {
include_type: IncludeType::Include,
source_type: "Patient".to_string(),
search_param: "organization".to_string(),
target_type: Some("Organization".to_string()),
iterate: true, });
let result = backend
.search(&tenant, &query, Pagination::new(100))
.await
.unwrap();
let included_types: std::collections::HashSet<_> =
result.included.iter().map(|r| r.resource_type()).collect();
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_revinclude_basic() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
seed_include_data(&backend, &tenant).await;
let query = SearchQuery::new("Patient").with_include(IncludeDirective {
include_type: IncludeType::Revinclude,
source_type: "Observation".to_string(),
search_param: "subject".to_string(),
target_type: Some("Patient".to_string()),
iterate: false,
});
let result = backend
.search(&tenant, &query, Pagination::new(100))
.await
.unwrap();
assert!(!result.resources.is_empty());
for resource in &result.resources {
assert_eq!(resource.resource_type(), "Patient");
}
for resource in &result.included {
assert_eq!(resource.resource_type(), "Observation");
}
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_revinclude_filtered() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
seed_include_data(&backend, &tenant).await;
let query = SearchQuery::new("Patient")
.with_parameter(helios_persistence::types::SearchParameter {
name: "_id".to_string(),
param_type: helios_persistence::types::SearchParamType::Token,
modifier: None,
values: vec![helios_persistence::types::SearchValue::eq("patient-1")],
chain: vec![],
components: vec![],
})
.with_include(IncludeDirective {
include_type: IncludeType::Revinclude,
source_type: "Observation".to_string(),
search_param: "subject".to_string(),
target_type: Some("Patient".to_string()),
iterate: false,
});
let result = backend
.search(&tenant, &query, Pagination::new(100))
.await
.unwrap();
assert_eq!(result.resources.len(), 1);
for resource in &result.included {
assert_eq!(
resource.content()["subject"]["reference"],
"Patient/patient-1"
);
}
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_include_iterate_recursive_depth() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let grandparent = json!({
"resourceType": "Organization",
"id": "org-grandparent",
"name": "Grandparent Org"
});
backend.create_or_update(&tenant, "Organization", "org-grandparent", grandparent).await.unwrap();
let parent = json!({
"resourceType": "Organization",
"id": "org-parent",
"name": "Parent Org",
"partOf": {"reference": "Organization/org-grandparent"}
});
backend.create_or_update(&tenant, "Organization", "org-parent", parent).await.unwrap();
let child = json!({
"resourceType": "Organization",
"id": "org-child",
"name": "Child Org",
"partOf": {"reference": "Organization/org-parent"}
});
backend.create_or_update(&tenant, "Organization", "org-child", child).await.unwrap();
let patient = json!({
"resourceType": "Patient",
"id": "patient-org-chain",
"managingOrganization": {"reference": "Organization/org-child"}
});
backend.create_or_update(&tenant, "Patient", "patient-org-chain", patient).await.unwrap();
let query = SearchQuery::new("Patient")
.with_parameter(helios_persistence::types::SearchParameter {
name: "_id".to_string(),
param_type: helios_persistence::types::SearchParamType::Token,
modifier: None,
values: vec![helios_persistence::types::SearchValue::eq("patient-org-chain")],
chain: vec![],
components: vec![],
})
.with_include(IncludeDirective {
include_type: IncludeType::Include,
source_type: "Patient".to_string(),
search_param: "organization".to_string(),
target_type: Some("Organization".to_string()),
iterate: false,
})
.with_include(IncludeDirective {
include_type: IncludeType::Include,
source_type: "Organization".to_string(),
search_param: "partof".to_string(),
target_type: Some("Organization".to_string()),
iterate: true, });
let result = backend
.search(&tenant, &query, Pagination::new(100))
.await
.unwrap();
assert_eq!(result.resources.len(), 1);
assert_eq!(result.resources[0].resource_type(), "Patient");
let org_count = result.included.iter()
.filter(|r| r.resource_type() == "Organization")
.count();
assert!(org_count >= 1, "Should include at least the direct organization");
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_include_iterate_cycle_detection() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let org_a = json!({
"resourceType": "Organization",
"id": "org-cycle-a",
"name": "Org A",
"partOf": {"reference": "Organization/org-cycle-c"}
});
let org_b = json!({
"resourceType": "Organization",
"id": "org-cycle-b",
"name": "Org B",
"partOf": {"reference": "Organization/org-cycle-a"}
});
let org_c = json!({
"resourceType": "Organization",
"id": "org-cycle-c",
"name": "Org C",
"partOf": {"reference": "Organization/org-cycle-b"}
});
backend.create_or_update(&tenant, "Organization", "org-cycle-a", org_a.clone()).await.unwrap();
backend.create_or_update(&tenant, "Organization", "org-cycle-b", org_b).await.unwrap();
backend.create_or_update(&tenant, "Organization", "org-cycle-c", org_c).await.unwrap();
backend.create_or_update(&tenant, "Organization", "org-cycle-a", org_a).await.unwrap();
let query = SearchQuery::new("Organization")
.with_parameter(helios_persistence::types::SearchParameter {
name: "_id".to_string(),
param_type: helios_persistence::types::SearchParamType::Token,
modifier: None,
values: vec![helios_persistence::types::SearchValue::eq("org-cycle-a")],
chain: vec![],
components: vec![],
})
.with_include(IncludeDirective {
include_type: IncludeType::Include,
source_type: "Organization".to_string(),
search_param: "partof".to_string(),
target_type: Some("Organization".to_string()),
iterate: true,
});
let result = backend
.search(&tenant, &query, Pagination::new(100))
.await;
match result {
Ok(result) => {
let unique_ids: std::collections::HashSet<_> = result.included.iter()
.map(|r| r.id())
.collect();
assert!(unique_ids.len() <= 3, "Should not have more than 3 orgs (cycle detected)");
}
Err(_) => {
}
}
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_include_iterate_max_depth() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let mut prev_id: Option<String> = None;
for i in 1..=8 {
let id = format!("location-depth-{}", i);
let mut location = json!({
"resourceType": "Location",
"id": id.clone(),
"name": format!("Location {}", i)
});
if let Some(ref parent) = prev_id {
location["partOf"] = json!({"reference": format!("Location/{}", parent)});
}
backend.create_or_update(&tenant, "Location", &id, location).await.unwrap();
prev_id = Some(id);
}
let query = SearchQuery::new("Location")
.with_parameter(helios_persistence::types::SearchParameter {
name: "_id".to_string(),
param_type: helios_persistence::types::SearchParamType::Token,
modifier: None,
values: vec![helios_persistence::types::SearchValue::eq("location-depth-8")],
chain: vec![],
components: vec![],
})
.with_include(IncludeDirective {
include_type: IncludeType::Include,
source_type: "Location".to_string(),
search_param: "partof".to_string(),
target_type: Some("Location".to_string()),
iterate: true,
});
let result = backend
.search(&tenant, &query, Pagination::new(100))
.await
.unwrap();
assert_eq!(result.resources.len(), 1);
let included_locations = result.included.iter()
.filter(|r| r.resource_type() == "Location")
.count();
assert!(
included_locations >= 1,
"Should include at least one parent location"
);
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_include_no_references() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let patient = json!({
"resourceType": "Patient",
"name": [{"family": "NoOrg"}]
});
backend.create(&tenant, "Patient", patient).await.unwrap();
let query = SearchQuery::new("Patient").with_include(IncludeDirective {
include_type: IncludeType::Include,
source_type: "Patient".to_string(),
search_param: "organization".to_string(),
target_type: Some("Organization".to_string()),
iterate: false,
});
let result = backend
.search(&tenant, &query, Pagination::new(100))
.await
.unwrap();
assert!(!result.resources.is_empty());
assert!(result.included.is_empty());
}