diff --git a/Cargo.lock b/Cargo.lock index e77508f01..39d003297 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3583,16 +3583,19 @@ name = "helios-ui" version = "0.2.1" dependencies = [ "askama", + "async-trait", "axum", "axum-embed", "axum-htmx", "chrono", "fluent-syntax 0.11.1", "fluent-templates", + "helios-auth", "helios-fhir", "helios-observability", "helios-persistence", "http-body-util", + "reqwest", "rust-embed", "serde", "serde_json", diff --git a/crates/auth/src/outbound.rs b/crates/auth/src/outbound.rs index 3197edf37..9f8b2b48d 100644 --- a/crates/auth/src/outbound.rs +++ b/crates/auth/src/outbound.rs @@ -16,6 +16,12 @@ //! assertion in the SMART Backend Services style) is planned and is the //! reason the trait carries an `audience` argument — that lets the future //! signer scope tokens per receiver. The current impls ignore it. +//! +//! Consumers that need this today: the web UI's server-side self-call +//! (`crates/hfs/src/main.rs`, mounting `helios_ui`) currently uses the static +//! bearer, so an auth-enabled deployment must provision `HFS_OUTBOUND_BEARER_TOKEN` +//! with a valid token or the UI's conformance pages degrade. See the +//! `TODO(service-token)` there. use std::sync::Arc; diff --git a/crates/fhir/src/compartment/loader.rs b/crates/fhir/src/compartment/loader.rs new file mode 100644 index 000000000..2f9ad306a --- /dev/null +++ b/crates/fhir/src/compartment/loader.rs @@ -0,0 +1,178 @@ +//! CompartmentDefinition loader. +//! +//! Reads the FHIR spec `CompartmentDefinition` resources from the data +//! directory, as stored-shape JSON ready to seed into storage. This is the +//! compartment analogue of +//! [`SearchParameterLoader::load_spec_resources`](crate::search::loader::SearchParameterLoader::load_spec_resources): +//! the server seeds these into primary storage at startup so +//! `GET /CompartmentDefinition` and the web UI read the same definitions the +//! codegen'd membership table (`crate::get_compartment_params`) is built from. + +use std::path::Path; + +use serde_json::Value; + +use crate::FhirVersion; +use crate::search::errors::LoaderError; + +/// Loader for CompartmentDefinition spec resources. +pub struct CompartmentDefinitionLoader { + fhir_version: FhirVersion, +} + +impl CompartmentDefinitionLoader { + /// Creates a new loader for the specified FHIR version. + pub fn new(fhir_version: FhirVersion) -> Self { + Self { fhir_version } + } + + /// Returns the FHIR version. + pub fn version(&self) -> FhirVersion { + self.fhir_version + } + + /// Returns the spec bundle filename for the configured FHIR version. + /// + /// Mirrors the `compartment-definitions-{version}.json` bundles shipped + /// under the data directory, alongside `search-parameters-{version}.json`. + #[allow(unreachable_patterns)] + pub fn spec_filename(&self) -> &'static str { + match self.fhir_version { + #[cfg(feature = "R4")] + FhirVersion::R4 => "compartment-definitions-r4.json", + #[cfg(feature = "R4B")] + FhirVersion::R4B => "compartment-definitions-r4b.json", + #[cfg(feature = "R5")] + FhirVersion::R5 => "compartment-definitions-r5.json", + #[cfg(feature = "R6")] + FhirVersion::R6 => "compartment-definitions-r6.json", + _ => "compartment-definitions-r4.json", + } + } + + /// Returns the raw CompartmentDefinition resources from the spec bundle, + /// as stored-shape JSON. + /// + /// The seeding companion the server hands to storage: each entry carries + /// its bundle id (`patient`, `encounter`, …), so re-seeding is idempotent + /// via `create`'s `AlreadyExists`. + pub fn load_spec_resources(&self, data_dir: &Path) -> Result, LoaderError> { + let path = data_dir.join(self.spec_filename()); + let content = + std::fs::read_to_string(&path).map_err(|e| LoaderError::ConfigLoadFailed { + path: path.display().to_string(), + message: e.to_string(), + })?; + let json: Value = + serde_json::from_str(&content).map_err(|e| LoaderError::ConfigLoadFailed { + path: path.display().to_string(), + message: format!("Invalid JSON: {}", e), + })?; + + let mut resources = Vec::new(); + if let Some(entries) = json.get("entry").and_then(|e| e.as_array()) { + for entry in entries { + if let Some(resource) = entry.get("resource") + && resource.get("resourceType").and_then(|t| t.as_str()) + == Some("CompartmentDefinition") + { + resources.push(resource.clone()); + } + } + } + Ok(resources) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + /// Repo-root `data/` directory, from this crate's manifest dir. + fn workspace_data_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../data") + } + + #[cfg(feature = "R4")] + #[test] + fn loads_r4_compartment_definitions() { + let loader = CompartmentDefinitionLoader::new(FhirVersion::R4); + let resources = loader.load_spec_resources(&workspace_data_dir()).unwrap(); + // Device, Encounter, Patient, Practitioner, RelatedPerson (questionnaire + // has no code and is excluded from the bundle). + assert_eq!(resources.len(), 5); + assert!( + resources + .iter() + .all(|r| r["resourceType"] == "CompartmentDefinition") + ); + assert!(resources.iter().any(|r| r["code"] == "Patient")); + assert!( + resources + .iter() + .all(|r| r.get("id").and_then(|i| i.as_str()).is_some()) + ); + } + + #[test] + fn missing_dir_errors() { + let loader = CompartmentDefinitionLoader::new(FhirVersion::default_enabled()); + assert!( + loader + .load_spec_resources(Path::new("/nonexistent/data/dir")) + .is_err() + ); + } + + /// The shipped `data/compartment-definitions-*.json` bundles must say + /// exactly what the codegen'd `get_compartment_params` table says — every + /// `(compartment, resource)` slot, every version. This is the guard against + /// the seeded copies drifting from `crates/fhir-gen/resources/`. + fn assert_parity(version: FhirVersion) { + let loader = CompartmentDefinitionLoader::new(version); + let defs = loader.load_spec_resources(&workspace_data_dir()).unwrap(); + assert!(!defs.is_empty(), "{version:?} bundle is empty"); + for def in &defs { + let code = def["code"].as_str().unwrap(); + for resource in def["resource"].as_array().unwrap() { + let rtype = resource["code"].as_str().unwrap(); + let params: Vec<&str> = resource + .get("param") + .and_then(|p| p.as_array()) + .map(|a| a.iter().filter_map(|v| v.as_str()).collect()) + .unwrap_or_default(); + let runtime = crate::get_compartment_params(version, code, rtype); + assert_eq!( + runtime, + params.as_slice(), + "{version:?} compartment {code} / {rtype}" + ); + } + } + } + + #[cfg(feature = "R4")] + #[test] + fn r4_bundle_matches_codegen_table() { + assert_parity(FhirVersion::R4); + } + + #[cfg(feature = "R4B")] + #[test] + fn r4b_bundle_matches_codegen_table() { + assert_parity(FhirVersion::R4B); + } + + #[cfg(feature = "R5")] + #[test] + fn r5_bundle_matches_codegen_table() { + assert_parity(FhirVersion::R5); + } + + #[cfg(feature = "R6")] + #[test] + fn r6_bundle_matches_codegen_table() { + assert_parity(FhirVersion::R6); + } +} diff --git a/crates/fhir/src/compartment/mod.rs b/crates/fhir/src/compartment/mod.rs new file mode 100644 index 000000000..8492a1a29 --- /dev/null +++ b/crates/fhir/src/compartment/mod.rs @@ -0,0 +1,15 @@ +//! FHIR CompartmentDefinition spec data. +//! +//! Companion to [`crate::search`]: where SearchParameters drive search +//! resolution, CompartmentDefinitions describe the compartment membership the +//! REST compartment-search handler enforces (via the codegen'd +//! [`crate::get_compartment_params`] table). This module only *loads* the spec +//! resources so the server can seed them into storage — the source of truth the +//! `GET /CompartmentDefinition` route and the web UI read from. Membership +//! itself stays codegen-driven. +//! +//! - [`loader`] – [`CompartmentDefinitionLoader`] for reading the spec bundle. + +pub mod loader; + +pub use loader::CompartmentDefinitionLoader; diff --git a/crates/fhir/src/lib.rs b/crates/fhir/src/lib.rs index e093c242b..a7a25debe 100644 --- a/crates/fhir/src/lib.rs +++ b/crates/fhir/src/lib.rs @@ -1431,6 +1431,7 @@ pub mod r5; #[cfg(feature = "R6")] pub mod r6; +pub mod compartment; pub mod compartment_expressions; pub mod parameters; pub mod search; diff --git a/crates/fhir/src/search/registry.rs b/crates/fhir/src/search/registry.rs index b0fc71e0e..11ae2712c 100644 --- a/crates/fhir/src/search/registry.rs +++ b/crates/fhir/src/search/registry.rs @@ -241,6 +241,12 @@ impl SearchParameterDefinition { /// unregistering, or re-statusing a *shadowed* parameter therefore never /// changes which definition answers a query, and removing the winner /// re-points the slot at the next candidate instead of leaving a hole (#239). +/// +/// `Clone` is cheap: both indexes hold `Arc`, so a +/// clone copies the HashMap structure and bumps Arc refcounts — the definitions +/// themselves are shared. The per-tenant registry container relies on this to +/// materialize a tenant's registry as `base.clone()` plus that tenant's overlay. +#[derive(Clone)] pub struct SearchParameterRegistry { /// Parameters indexed by (resource_type, param_code). Each slot holds the /// candidates in descending precedence order; the first entry wins. diff --git a/crates/hfs/Cargo.toml b/crates/hfs/Cargo.toml index 3c7191424..5c7f9e393 100644 --- a/crates/hfs/Cargo.toml +++ b/crates/hfs/Cargo.toml @@ -17,11 +17,13 @@ path = "src/main.rs" [features] default = ["R4", "sqlite", "ui"] -# FHIR version features -R4 = ["helios-fhir/R4", "helios-rest/R4", "helios-audit/R4"] -R4B = ["helios-fhir/R4B", "helios-rest/R4B", "helios-audit/R4B"] -R5 = ["helios-fhir/R5", "helios-rest/R5", "helios-audit/R5"] -R6 = ["helios-fhir/R6", "helios-rest/R6", "helios-audit/R6"] +# FHIR version features. `helios-ui?/…` keeps the UI's viewers (search +# parameters, compartments) scoped to the same versions as the server when +# the optional `ui` feature is on. +R4 = ["helios-fhir/R4", "helios-rest/R4", "helios-audit/R4", "helios-ui?/R4"] +R4B = ["helios-fhir/R4B", "helios-rest/R4B", "helios-audit/R4B", "helios-ui?/R4B"] +R5 = ["helios-fhir/R5", "helios-rest/R5", "helios-audit/R5", "helios-ui?/R5"] +R6 = ["helios-fhir/R6", "helios-rest/R6", "helios-audit/R6", "helios-ui?/R6"] # Build options skip-r6-download = [] @@ -58,7 +60,7 @@ helios-sof = { path = "../sof", version = "0.2.1", default-features = false } helios-auth = { path = "../auth", version = "0.2.1" } helios-audit = { path = "../audit", version = "0.2.1", default-features = false } helios-subscriptions = { path = "../subscriptions", version = "0.2.1", optional = true, default-features = false } -helios-ui = { path = "../ui", version = "0.2.1", optional = true } +helios-ui = { path = "../ui", version = "0.2.1", optional = true, default-features = false } helios-observability = { path = "../observability", version = "0.2.1" } # Export job controller diff --git a/crates/hfs/src/main.rs b/crates/hfs/src/main.rs index 58816c49d..e262d94c9 100644 --- a/crates/hfs/src/main.rs +++ b/crates/hfs/src/main.rs @@ -46,7 +46,7 @@ use helios_rest::OperationsBundle; use helios_rest::create_app_with_auth_bulk_settings_and_ops; use helios_persistence::core::PurgableStorage; -use helios_persistence::search::{ReindexOperation, SearchParameterExtractor}; +use helios_persistence::search::ReindexOperation; #[cfg(feature = "sqlite")] use helios_persistence::backends::sqlite::{SqliteBackend, SqliteBackendConfig}; @@ -510,7 +510,7 @@ async fn start_mongodb( backend.init_schema().await?; let backend = Arc::new(backend); - seed_search_parameters(&*backend, &config).await; + seed_conformance_resources(&*backend, &config).await; spawn_mongodb_search_param_refresh(backend.clone(), &config); let serve_audit_state = audit_state.clone(); @@ -534,7 +534,7 @@ async fn start_mongodb( let ops = standalone_ops( backend.clone(), - backend.search_extractor().clone(), + backend.tenant_registries().clone(), audit_state.as_ref(), ); let app = create_app_with_auth_bulk_settings_and_ops( @@ -575,7 +575,32 @@ async fn serve( ui_tenants: Option>, ) -> anyhow::Result<()> { #[cfg(all(feature = "ui", not(feature = "headless")))] - let app = helios_ui::mount(app, env!("CARGO_PKG_VERSION"), ui_tenants.clone()); + let app = { + // The UI reads SearchParameter/CompartmentDefinition from the server's + // own FHIR API over HTTP. It calls itself on the loopback address, with + // the configured outbound service token (HFS_OUTBOUND_BEARER_TOKEN) when + // set, or no credentials when auth is disabled. + // + // TODO(service-token): when auth is enabled, this relies on an operator + // provisioning a valid, non-expiring bearer via HFS_OUTBOUND_BEARER_TOKEN; + // without one the self-call is rejected and the conformance pages degrade + // to a warning. The follow-up is to mint a short-lived, auto-refreshed + // `system/SearchParameter.rs system/CompartmentDefinition.rs` token via + // the planned `JwtAssertionOutboundAuthProvider` (SMART Backend Services + // client_credentials + private_key_jwt; see crates/auth/src/outbound.rs) + // configured from HFS_UI_* client credentials. + let self_base_url = format!("http://127.0.0.1:{}", config.port); + let outbound_auth = AuthConfig::from_env().outbound_provider(); + helios_ui::mount( + app, + env!("CARGO_PKG_VERSION"), + config.data_dir.clone(), + ui_tenants.clone(), + self_base_url, + outbound_auth, + config.default_fhir_version, + ) + }; #[cfg(not(all(feature = "ui", not(feature = "headless"))))] let _ = &ui_tenants; @@ -854,28 +879,72 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -/// Seeds storage with the spec SearchParameters under the default tenant -/// (#235), making storage the source of truth the registry caches over. A -/// failed seed logs and boots anyway: the in-memory registry still resolves -/// searches; only API discovery of the spec parameters is degraded. -async fn seed_search_parameters(backend: &S, config: &ServerConfig) +/// Seeds storage with the spec SearchParameters (#235) and CompartmentDefinitions +/// (#237/#238), making primary storage the source of truth the FHIR routes and +/// web UI read. Seeds every provisioned tenant — auto-provisioning the default +/// tenant first — so `GET /SearchParameter` and `GET /CompartmentDefinition` are +/// populated for each valid tenant. A failed seed logs and boots anyway: the +/// in-memory registry still resolves searches; only API discovery is degraded. +async fn seed_conformance_resources(backend: &S, config: &ServerConfig) where S: helios_persistence::core::ResourceStorage, { + if !config.seed_conformance { + return; + } + let data_dir = config .data_dir .clone() .unwrap_or_else(|| std::path::PathBuf::from("./data")); - if let Err(e) = helios_persistence::search::seed_spec_search_parameters( - backend, - config.default_fhir_version, - &data_dir, - &config.default_tenant, - ) - .await - { - tracing::warn!("SearchParameter seeding failed: {e}"); + + for tenant_id in provisioned_tenants(backend, config).await { + helios_persistence::search::seed_tenant_conformance( + backend, + config.default_fhir_version, + &data_dir, + &tenant_id, + ) + .await; + } +} + +/// The set of tenants to seed: the auto-provisioned default tenant plus every +/// registered tenant. Tenants are provisioned-only, so this is the complete set +/// of valid tenants. Falls back to just the default tenant when the backend has +/// no tenant registry (e.g. a minimal deployment). +async fn provisioned_tenants(backend: &S, config: &ServerConfig) -> Vec +where + S: helios_persistence::core::ResourceStorage, +{ + let default = config.default_tenant.clone(); + if !backend.supports_tenant_registry() { + return vec![default]; + } + + // Auto-provision the default tenant so it is a valid, enumerable tenant + // (single-tenant and unauthenticated deployments read it). + match backend.get_tenant(&default).await { + Ok(Some(_)) => {} + Ok(None) => { + if let Err(e) = backend.register_tenant(&default, None).await { + tracing::warn!(tenant = %default, "Auto-provisioning default tenant failed: {e}"); + } + } + Err(e) => tracing::warn!(tenant = %default, "Checking default tenant failed: {e}"), + } + + let mut ids: Vec = match backend.list_tenants().await { + Ok(records) => records.into_iter().map(|r| r.id).collect(), + Err(e) => { + tracing::warn!("Listing tenants for seeding failed: {e}"); + Vec::new() + } + }; + if !ids.iter().any(|id| id == &default) { + ids.push(default); } + ids } /// Spawns the periodic registry refresh from storage for the SQLite backend @@ -966,7 +1035,7 @@ async fn start_sqlite( ) -> anyhow::Result<()> { let serve_audit_state = audit_state.clone(); let backend = Arc::new(create_sqlite_backend(&config)?); - seed_search_parameters(&*backend, &config).await; + seed_conformance_resources(&*backend, &config).await; spawn_sqlite_search_param_refresh(backend.clone(), &config); // Second handle to the same backend for the web UI's tenant-maintenance // read/write path (the FHIR app keeps its own). Cheap: the SQLite backend @@ -980,7 +1049,7 @@ async fn start_sqlite( let submit_bundle = build_bulk_submit(&config, backend.clone()).await?; let ops = standalone_ops( backend.clone(), - backend.search_extractor().clone(), + backend.tenant_registries().clone(), audit_state.as_ref(), ); let app = create_app_with_auth_bulk_settings_and_ops( @@ -1163,7 +1232,7 @@ fn wire_reindex( /// (SQLite, PostgreSQL, MongoDB), where resources and search index share a home. fn standalone_ops( backend: Arc, - extractor: Arc, + registries: Arc, audit_state: Option<&Arc>, ) -> OperationsBundle where @@ -1172,7 +1241,7 @@ where OperationsBundle { purge: Some(backend.clone() as Arc), reindex: Some(wire_reindex( - ReindexOperation::new(backend, extractor), + ReindexOperation::new(backend, registries), audit_state, )), } @@ -1192,13 +1261,13 @@ fn composite_ops( composite: Arc, source: Arc, targets: Vec>, - extractor: Arc, + registries: Arc, audit_state: Option<&Arc>, ) -> OperationsBundle { OperationsBundle { purge: Some(composite as Arc), reindex: Some(wire_reindex( - ReindexOperation::with_parts(source, targets, extractor), + ReindexOperation::with_parts(source, targets, registries), audit_state, )), } @@ -1498,7 +1567,7 @@ async fn start_sqlite_elasticsearch( let sqlite = Arc::new(sqlite); info!("SQLite search indexing disabled (offloaded to Elasticsearch)"); // Seed/refresh on the primary; the ES backend shares its registry Arc. - seed_search_parameters(&*sqlite, &config).await; + seed_conformance_resources(&*sqlite, &config).await; spawn_sqlite_search_param_refresh(sqlite.clone(), &config); // Build Elasticsearch configuration from server config @@ -1543,7 +1612,7 @@ async fn start_sqlite_elasticsearch( // Create ES backend sharing SQLite's search parameter registry let es = Arc::new(ElasticsearchBackend::with_shared_registry( es_config, - sqlite.search_registry().clone(), + sqlite.tenant_registries().clone(), )?); // Build composite configuration @@ -1607,7 +1676,7 @@ async fn start_sqlite_elasticsearch( composite.clone(), sqlite.clone(), vec![sqlite.clone(), es.clone()], - sqlite.search_extractor().clone(), + sqlite.tenant_registries().clone(), audit_state.as_ref(), ); let app = create_app_with_auth_bulk_settings_and_ops( @@ -1665,7 +1734,7 @@ async fn start_postgres( backend.init_schema().await?; let backend = Arc::new(backend); - seed_search_parameters(&*backend, &config).await; + seed_conformance_resources(&*backend, &config).await; spawn_postgres_search_param_refresh(backend.clone(), &config); let serve_audit_state = audit_state.clone(); @@ -1676,7 +1745,7 @@ async fn start_postgres( let submit_bundle = build_bulk_submit(&config, backend.clone()).await?; let ops = standalone_ops( backend.clone(), - backend.search_extractor().clone(), + backend.tenant_registries().clone(), audit_state.as_ref(), ); let app = create_app_with_auth_bulk_settings_and_ops( @@ -1749,7 +1818,7 @@ async fn start_postgres_elasticsearch( let pg = Arc::new(backend); info!("PostgreSQL search indexing disabled (offloaded to Elasticsearch)"); // Seed/refresh on the primary; the ES backend shares its registry Arc. - seed_search_parameters(&*pg, &config).await; + seed_conformance_resources(&*pg, &config).await; spawn_postgres_search_param_refresh(pg.clone(), &config); // Build Elasticsearch configuration from server config @@ -1794,7 +1863,7 @@ async fn start_postgres_elasticsearch( // Create ES backend sharing PostgreSQL's search parameter registry let es = Arc::new(ElasticsearchBackend::with_shared_registry( es_config, - pg.search_registry().clone(), + pg.tenant_registries().clone(), )?); // Build composite configuration @@ -1854,7 +1923,7 @@ async fn start_postgres_elasticsearch( composite.clone(), pg.clone(), vec![pg.clone(), es.clone()], - pg.search_extractor().clone(), + pg.tenant_registries().clone(), audit_state.as_ref(), ); let app = create_app_with_auth_bulk_settings_and_ops( @@ -1919,7 +1988,7 @@ async fn start_mongodb_elasticsearch( let mongo = Arc::new(backend); info!("MongoDB search indexing disabled (offloaded to Elasticsearch)"); // Seed/refresh on the primary; the ES backend shares its registry Arc. - seed_search_parameters(&*mongo, &config).await; + seed_conformance_resources(&*mongo, &config).await; spawn_mongodb_search_param_refresh(mongo.clone(), &config); // Build Elasticsearch configuration from server config @@ -1964,7 +2033,7 @@ async fn start_mongodb_elasticsearch( // Create ES backend sharing MongoDB's search parameter registry let es = Arc::new(ElasticsearchBackend::with_shared_registry( es_config, - mongo.search_registry().clone(), + mongo.tenant_registries().clone(), )?); // Build composite configuration @@ -2035,7 +2104,7 @@ async fn start_mongodb_elasticsearch( composite.clone(), mongo.clone(), vec![mongo.clone(), es.clone()], - mongo.search_extractor().clone(), + mongo.tenant_registries().clone(), audit_state.as_ref(), ); let app = create_app_with_auth_bulk_settings_and_ops( @@ -2190,13 +2259,15 @@ async fn start_s3( fn build_search_registry( fhir_version: helios_fhir::FhirVersion, data_dir: Option<&std::path::Path>, -) -> std::sync::Arc> { - use helios_persistence::search::{SearchParameterLoader, SearchParameterRegistry}; +) -> std::sync::Arc { + use helios_persistence::search::{SearchParameterLoader, TenantSearchRegistries}; - let registry = std::sync::Arc::new(parking_lot::RwLock::new(SearchParameterRegistry::new())); + // S3 stores no SearchParameter resources of its own, so tenants have no + // stored overlay — every tenant sees the shared base (embedded + spec). + let registries = std::sync::Arc::new(TenantSearchRegistries::base_only()); let loader = SearchParameterLoader::new(fhir_version); { - let mut reg = registry.write(); + let mut reg = registries.base().write(); if let Ok(params) = loader.load_embedded() { for p in params { let _ = reg.register(p); @@ -2209,7 +2280,7 @@ fn build_search_registry( } } } - registry + registries } /// Starts the server with S3 + Elasticsearch composite backend. @@ -2379,7 +2450,7 @@ async fn start_s3_elasticsearch( composite.clone(), s3.clone(), vec![es.clone()], - es.search_extractor().clone(), + es.tenant_registries().clone(), audit_state.as_ref(), ); @@ -2505,9 +2576,10 @@ mod tests { #[test] fn test_build_search_registry_returns_registry() { use helios_fhir::FhirVersion; - let registry = build_search_registry(FhirVersion::R4, None); - // Registry should be a valid Arc> and not panic when read. - let _guard = registry.read(); + let registries = build_search_registry(FhirVersion::R4, None); + // The container's base should be populated and every tenant resolves. + assert!(!registries.base().read().is_empty()); + let _guard = registries.for_tenant("default"); } #[cfg(feature = "mongodb")] diff --git a/crates/persistence/src/backends/elasticsearch/backend.rs b/crates/persistence/src/backends/elasticsearch/backend.rs index b6835d3c1..9afbb9ce0 100644 --- a/crates/persistence/src/backends/elasticsearch/backend.rs +++ b/crates/persistence/src/backends/elasticsearch/backend.rs @@ -17,7 +17,10 @@ use helios_fhir::FhirVersion; use crate::core::{Backend, BackendCapability, BackendKind}; use crate::error::{BackendError, StorageResult}; -use crate::search::{SearchParameterExtractor, SearchParameterLoader, SearchParameterRegistry}; +use crate::search::{ + SearchParameterExtractor, SearchParameterLoader, SearchParameterRegistry, + TenantSearchRegistries, +}; /// Authentication configuration for Elasticsearch. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -133,17 +136,16 @@ pub struct ElasticsearchBackend { client: Elasticsearch, /// Configuration. config: ElasticsearchConfig, - /// Search parameter registry (shared with primary for consistency). - search_registry: Arc>, - /// Search parameter extractor. - search_extractor: Arc, + /// Per-tenant search parameter registries (a shared base plus per-tenant + /// overlays). Shared with the primary backend for consistency. + registries: Arc, } impl Debug for ElasticsearchBackend { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ElasticsearchBackend") .field("config", &self.config) - .field("search_registry_len", &self.search_registry.read().len()) + .field("base_registry_len", &self.registries.base().read().len()) .finish_non_exhaustive() } } @@ -153,11 +155,12 @@ impl ElasticsearchBackend { pub fn new(config: ElasticsearchConfig) -> StorageResult { let client = Self::build_client(&config)?; - // Initialize search parameter registry - let search_registry = Arc::new(RwLock::new(SearchParameterRegistry::new())); + // Standalone ES has no store of its own, so tenants have no overlay: + // every tenant sees the shared base (embedded params only). + let registries = Arc::new(TenantSearchRegistries::base_only()); { let loader = SearchParameterLoader::new(config.fhir_version); - let mut registry = search_registry.write(); + let mut registry = registries.base().write(); // Load embedded fallback params match loader.load_embedded() { @@ -177,31 +180,29 @@ impl ElasticsearchBackend { registry.resource_types().len() ); } - let search_extractor = Arc::new(SearchParameterExtractor::new(search_registry.clone())); Ok(Self { client, config, - search_registry, - search_extractor, + registries, }) } - /// Creates a new backend with a shared search parameter registry. + /// Creates a new backend with a shared per-tenant registry container. /// - /// Use this when the ES backend should share its registry with a primary backend. + /// Use this when the ES backend should share its registries with a primary + /// backend (composite deployments): the container's loader points at the + /// primary's storage, so ES resolves per-tenant overlays without its own DB. pub fn with_shared_registry( config: ElasticsearchConfig, - search_registry: Arc>, + registries: Arc, ) -> StorageResult { let client = Self::build_client(&config)?; - let search_extractor = Arc::new(SearchParameterExtractor::new(search_registry.clone())); Ok(Self { client, config, - search_registry, - search_extractor, + registries, }) } @@ -260,19 +261,25 @@ impl ElasticsearchBackend { &self.config } - /// Returns the search parameter registry. + /// Returns the per-tenant search parameter registries (shared base + tenant + /// overlays). + pub fn tenant_registries(&self) -> &Arc { + &self.registries + } + + /// Returns the shared base registry (embedded/spec/custom, tenant-agnostic). #[allow(dead_code)] - pub(crate) fn search_registry(&self) -> &Arc> { - &self.search_registry + pub(crate) fn base_registry(&self) -> &Arc> { + self.registries.base() } - /// Returns the search parameter extractor. + /// Builds an extractor over the given tenant's registry. /// /// Public because the `s3`+Elasticsearch composite has no SQL primary to /// borrow an extractor from: S3 stores resources but maintains no search /// index, so Elasticsearch's extractor is the only one in that deployment. - pub fn search_extractor(&self) -> &Arc { - &self.search_extractor + pub fn tenant_extractor(&self, tenant_id: &str) -> SearchParameterExtractor { + SearchParameterExtractor::new(self.registries.for_tenant(tenant_id)) } /// Returns the index name for a tenant and resource type. @@ -455,13 +462,13 @@ impl SearchCapabilityProvider for ElasticsearchBackend { resource_type: &str, ) -> Option { let params = { - let registry = self.search_registry.read(); + let registry = self.registries.base().read(); registry.get_active_params(resource_type) }; if params.is_empty() { let common_params = { - let registry = self.search_registry.read(); + let registry = self.registries.base().read(); registry.get_active_params("Resource") }; if common_params.is_empty() { @@ -483,7 +490,7 @@ impl SearchCapabilityProvider for ElasticsearchBackend { // Add common Resource-level parameters let common_params = { - let registry = self.search_registry.read(); + let registry = self.registries.base().read(); registry.get_active_params("Resource") }; for param in &common_params { @@ -644,47 +651,51 @@ mod tests { } #[test] - fn test_with_shared_registry_reuses_arc() { + fn test_with_shared_registry_reuses_container() { let config = ElasticsearchConfig::default(); - let shared_registry = Arc::new(RwLock::new(SearchParameterRegistry::new())); + let shared = Arc::new(TenantSearchRegistries::base_only()); - let backend = - ElasticsearchBackend::with_shared_registry(config, shared_registry.clone()).unwrap(); + let backend = ElasticsearchBackend::with_shared_registry(config, shared.clone()).unwrap(); - assert!(Arc::ptr_eq(backend.search_registry(), &shared_registry)); + assert!(Arc::ptr_eq(backend.tenant_registries(), &shared)); } #[test] - fn test_with_shared_registry_reflects_runtime_updates() { + fn test_with_shared_registry_reflects_base_updates() { let config = ElasticsearchConfig::default(); - let shared_registry = Arc::new(RwLock::new(SearchParameterRegistry::new())); - let backend = - ElasticsearchBackend::with_shared_registry(config, shared_registry.clone()).unwrap(); + let shared = Arc::new(TenantSearchRegistries::base_only()); + let backend = ElasticsearchBackend::with_shared_registry(config, shared.clone()).unwrap(); let loader = SearchParameterLoader::new(FhirVersion::default()); let definition = loader .parse_resource(&json!({ "resourceType": "SearchParameter", - "id": "mongo-shared-param", - "url": "http://example.org/fhir/SearchParameter/mongo-shared-param", - "name": "MongoSharedParam", + "id": "es-shared-param", + "url": "http://example.org/fhir/SearchParameter/es-shared-param", + "name": "EsSharedParam", "status": "active", - "code": "mongo-shared-code", + "code": "es-shared-code", "base": ["Patient"], "type": "token", "expression": "Patient.identifier" })) .expect("parse shared SearchParameter definition"); - shared_registry + // A param added to the shared base is visible to the ES backend through + // the shared container (the first per-tenant build clones the base). + shared + .base() .write() .register(definition) .expect("register shared SearchParameter"); - let registry = backend.search_registry().read(); + let registry = backend.tenant_registries().for_tenant("default"); assert!( - registry.get_param("Patient", "mongo-shared-code").is_some(), - "shared registry updates should be visible to Elasticsearch backend" + registry + .read() + .get_param("Patient", "es-shared-code") + .is_some(), + "shared base updates should be visible to the Elasticsearch backend" ); } } diff --git a/crates/persistence/src/backends/elasticsearch/search_impl.rs b/crates/persistence/src/backends/elasticsearch/search_impl.rs index 94fac0a10..874072e40 100644 --- a/crates/persistence/src/backends/elasticsearch/search_impl.rs +++ b/crates/persistence/src/backends/elasticsearch/search_impl.rs @@ -333,8 +333,10 @@ impl SearchProvider for ElasticsearchBackend { fn search_param_registry( &self, - ) -> &std::sync::Arc> { - self.search_registry() + tenant: &crate::tenant::TenantContext, + ) -> std::sync::Arc> { + self.tenant_registries() + .for_tenant(tenant.tenant_id().as_str()) } fn supports_contained_search(&self) -> bool { diff --git a/crates/persistence/src/backends/elasticsearch/storage.rs b/crates/persistence/src/backends/elasticsearch/storage.rs index e5a6061d2..8bc58cf8f 100644 --- a/crates/persistence/src/backends/elasticsearch/storage.rs +++ b/crates/persistence/src/backends/elasticsearch/storage.rs @@ -416,7 +416,7 @@ impl ElasticsearchBackend { .await?; } - for contained in self.search_extractor().extract_contained(resource) { + for contained in self.tenant_extractor(tenant_id).extract_contained(resource) { let doc = build_es_contained_document( tenant_id, container_type, @@ -488,7 +488,7 @@ impl ResourceStorage for ElasticsearchBackend { // Extract search parameters let extracted_values = self - .search_extractor() + .tenant_extractor(tenant_id) .extract(&resource, resource_type) .unwrap_or_default(); @@ -606,7 +606,7 @@ impl ResourceStorage for ElasticsearchBackend { // Extract search parameters let extracted_values = self - .search_extractor() + .tenant_extractor(tenant_id) .extract(&resource, resource_type) .unwrap_or_default(); @@ -749,7 +749,7 @@ impl ResourceStorage for ElasticsearchBackend { } let extracted_values = self - .search_extractor() + .tenant_extractor(tenant_id) .extract(&resource, resource_type) .unwrap_or_default(); @@ -1067,7 +1067,7 @@ impl ReindexTarget for ElasticsearchBackend { let fhir_version = resource.fhir_version(); let extracted_values = self - .search_extractor() + .tenant_extractor(tenant_id) .extract(content, resource_type) .map_err(|e| internal_error(format!("Search parameter extraction failed: {e}")))?; diff --git a/crates/persistence/src/backends/mongodb/backend.rs b/crates/persistence/src/backends/mongodb/backend.rs index 649481639..4d31a6bb4 100644 --- a/crates/persistence/src/backends/mongodb/backend.rs +++ b/crates/persistence/src/backends/mongodb/backend.rs @@ -15,7 +15,16 @@ use helios_fhir::FhirVersion; use crate::core::{Backend, BackendCapability, BackendKind}; use crate::error::{BackendError, StorageError, StorageResult}; -use crate::search::{SearchParameterExtractor, SearchParameterLoader, SearchParameterRegistry}; +use crate::search::{ + SearchParameterDefinition, SearchParameterExtractor, SearchParameterLoader, + SearchParameterRegistry, TenantSearchRegistries, +}; + +/// Sync in-memory cache of each tenant's stored active SearchParameter +/// definitions. MongoDB queries are async but the registry loader must be sync, +/// so async paths populate this map and the loader reads it. +type StoredByTenant = + Arc>>>; use super::schema; @@ -76,17 +85,17 @@ pub struct MongoBackend { /// `Arc` so the in-DB SOF runner can share the same pooled client (it is /// constructed from `&self` but outlives the borrow). client: Arc>, - /// Search parameter registry (in-memory cache of active parameters). - search_registry: Arc>, - /// Extractor for deriving searchable values from resources. - search_extractor: Arc, + /// Per-tenant search parameter registries (shared base + per-tenant overlay). + registries: Arc, + /// Sync cache of each tenant's stored params, read by the registry loader. + stored_by_tenant: StoredByTenant, } impl Debug for MongoBackend { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("MongoBackend") .field("config", &self.config) - .field("search_registry_len", &self.search_registry.read().len()) + .field("base_registry_len", &self.registries.base().read().len()) .finish_non_exhaustive() } } @@ -163,15 +172,25 @@ impl MongoBackend { pub fn new(config: MongoBackendConfig) -> StorageResult { Self::validate_connection_string(&config.connection_string)?; - let search_registry = Arc::new(RwLock::new(SearchParameterRegistry::new())); - Self::initialize_search_registry(&search_registry, &config); - let search_extractor = Arc::new(SearchParameterExtractor::new(search_registry.clone())); + let stored_by_tenant: StoredByTenant = + Arc::new(RwLock::new(std::collections::HashMap::new())); + let loader_cache = stored_by_tenant.clone(); + let registries = Arc::new(TenantSearchRegistries::new(Arc::new( + move |tenant_id: &str| { + loader_cache + .read() + .get(tenant_id) + .cloned() + .unwrap_or_default() + }, + ))); + Self::initialize_search_registry(registries.base(), &config); Ok(Self { config, client: Arc::new(OnceCell::new()), - search_registry, - search_extractor, + registries, + stored_by_tenant, }) } @@ -346,27 +365,16 @@ impl MongoBackend { pub async fn init_schema(&self) -> StorageResult<()> { let db = self.get_database().await?; schema::initialize_schema_async(&db).await?; - - // Restore stored (POSTed) SearchParameters into the registry, as the - // SQLite and Postgres backends do; previously they only re-entered - // the registry through the write hooks, i.e. never after a restart. - let stored_count = self.refresh_stored_search_parameters().await?; - if stored_count > 0 { - tracing::info!( - "Loaded {} stored SearchParameters from database", - stored_count - ); - } + // Populate the per-tenant stored-param cache so the registries can build + // each tenant's overlay lazily. + self.reload_stored_cache().await?; Ok(()) } - /// Rebuilds the registry's `Stored` parameters from what the database - /// currently holds, returning how many are registered afterwards. - /// - /// TTL-cache refresh (#235): documents are fetched and parsed before the - /// (sync) write lock is taken, and on any read error the registry is left - /// untouched — stale-serve rather than losing search resolution. - pub async fn refresh_stored_search_parameters(&self) -> StorageResult { + /// Reloads every tenant's stored active SearchParameters into the sync + /// `stored_by_tenant` cache (grouped by tenant), then drops the cached + /// per-tenant registries so they rebuild against the fresh overlay. + pub(crate) async fn reload_stored_cache(&self) -> StorageResult { use crate::search::registry::{SearchParameterSource, SearchParameterStatus}; use mongodb::bson::{Document, doc}; @@ -384,7 +392,9 @@ impl MongoBackend { })?; let loader = SearchParameterLoader::new(self.config.fhir_version); - let mut definitions = Vec::new(); + let mut by_tenant: std::collections::HashMap> = + std::collections::HashMap::new(); + let mut count = 0; while cursor.advance().await.map_err(|e| { crate::error::StorageError::Backend(BackendError::Internal { backend_name: "mongodb".to_string(), @@ -399,6 +409,10 @@ impl MongoBackend { continue; } }; + let tenant_id = document + .get_str("tenant_id") + .unwrap_or("default") + .to_string(); let Ok(payload) = document.get_document("data") else { tracing::warn!("Stored SearchParameter document has no data payload"); continue; @@ -410,31 +424,26 @@ impl MongoBackend { continue; } }; - match loader.parse_resource(&json) { - Ok(mut def) => { - if def.status == SearchParameterStatus::Active { - def.source = SearchParameterSource::Stored; - definitions.push(def); - } - } - Err(e) => { - tracing::warn!("Failed to parse stored SearchParameter: {}", e); + if let Ok(mut def) = loader.parse_resource(&json) { + if def.status == SearchParameterStatus::Active { + def.source = SearchParameterSource::Stored; + by_tenant.entry(tenant_id).or_default().push(def); + count += 1; } } } - let mut registry = self.search_registry.write(); - registry.unregister_source(SearchParameterSource::Stored); - let mut count = 0; - for def in definitions { - match registry.register(def) { - Ok(()) => count += 1, - Err(e) => tracing::warn!("Stored SearchParameter not registered: {}", e), - } - } + *self.stored_by_tenant.write() = by_tenant; + self.registries.invalidate_all(); Ok(count) } + /// TTL-cache refresh (#235): reload the stored-param cache and drop the + /// cached per-tenant registries. + pub async fn refresh_stored_search_parameters(&self) -> StorageResult { + self.reload_stored_cache().await + } + /// Creates a MongoDB client from backend configuration. /// Returns the shared MongoDB client for this backend. pub(crate) async fn get_client(&self) -> StorageResult { @@ -462,14 +471,24 @@ impl MongoBackend { &self.config } - /// Returns a reference to the search parameter registry. - pub fn search_registry(&self) -> &Arc> { - &self.search_registry + /// The per-tenant registry container (shared with a co-located ES backend). + pub fn tenant_registries(&self) -> &Arc { + &self.registries + } + + /// The shared base registry (embedded + spec + custom), tenant-independent. + pub(crate) fn base_registry(&self) -> &Arc> { + self.registries.base() + } + + /// The registry for a tenant (base + that tenant's stored overlay). + pub(crate) fn tenant_registry(&self, tenant_id: &str) -> Arc> { + self.registries.for_tenant(tenant_id) } - /// Returns a reference to the search parameter extractor. - pub fn search_extractor(&self) -> &Arc { - &self.search_extractor + /// A value extractor over a tenant's registry. + pub(crate) fn tenant_extractor(&self, tenant_id: &str) -> SearchParameterExtractor { + SearchParameterExtractor::new(self.tenant_registry(tenant_id)) } /// Returns whether search indexing is offloaded to a secondary backend. @@ -659,11 +678,11 @@ impl SearchCapabilityProvider for MongoBackend { resource_type: &str, ) -> Option { let params = { - let registry = self.search_registry.read(); + let registry = self.base_registry().read(); registry.get_active_params(resource_type) }; let common_params = { - let registry = self.search_registry.read(); + let registry = self.base_registry().read(); registry.get_active_params("Resource") }; if params.is_empty() && common_params.is_empty() { diff --git a/crates/persistence/src/backends/mongodb/search_impl.rs b/crates/persistence/src/backends/mongodb/search_impl.rs index e82b51116..950a155f5 100644 --- a/crates/persistence/src/backends/mongodb/search_impl.rs +++ b/crates/persistence/src/backends/mongodb/search_impl.rs @@ -324,8 +324,9 @@ impl SearchProvider for MongoBackend { fn search_param_registry( &self, - ) -> &std::sync::Arc> { - self.search_registry() + tenant: &crate::tenant::TenantContext, + ) -> std::sync::Arc> { + self.tenant_registry(tenant.tenant_id().as_str()) } fn supports_contained_search(&self) -> bool { @@ -1370,7 +1371,7 @@ impl MongoBackend { return Ok(Vec::new()); } - let search_params = self.build_search_parameters(resource_type, &parsed_params); + let search_params = self.build_search_parameters(tenant, resource_type, &parsed_params); let query = SearchQuery { resource_type: resource_type.to_string(), @@ -1385,10 +1386,12 @@ impl MongoBackend { fn build_search_parameters( &self, + tenant: &TenantContext, resource_type: &str, params: &[(String, String)], ) -> Vec { - let registry = self.search_registry().read(); + let registry_arc = self.tenant_registry(tenant.tenant_id().as_str()); + let registry = registry_arc.read(); params .iter() diff --git a/crates/persistence/src/backends/mongodb/storage.rs b/crates/persistence/src/backends/mongodb/storage.rs index 3620c9d2f..c4d418a92 100644 --- a/crates/persistence/src/backends/mongodb/storage.rs +++ b/crates/persistence/src/backends/mongodb/storage.rs @@ -25,7 +25,6 @@ use crate::error::{ use crate::search::converters::IndexValue; use crate::search::extractor::ExtractedValue; use crate::search::reindex::{ReindexSource, ReindexTarget, ResourcePage}; -use crate::search::{SearchParameterLoader, SearchParameterStatus}; use crate::tenant::TenantContext; use crate::types::{CursorValue, Page, PageCursor, PageInfo, StoredResource}; @@ -39,11 +38,15 @@ fn internal_error(message: String) -> StorageError { }) } +/// A SearchParameter mutation staged during a bundle transaction. The variant +/// records *that* a change occurred; the post-commit step only needs to know a +/// tenant's SearchParameter overlay changed (to invalidate its cached registry), +/// so no payload is carried. #[derive(Debug, Clone)] enum PendingSearchParameterChange { - Create(Value), - Update { old: Value, new: Value }, - Delete(Value), + Create, + Update, + Delete, } fn serialization_error(message: String) -> StorageError { @@ -668,8 +671,16 @@ impl ResourceStorage for MongoBackend { self.index_resource(&db, tenant_id, resource_type, &id, &resource, &mut session) .await?; - if resource_type == "SearchParameter" { - self.handle_search_parameter_create(&resource)?; + // An active SearchParameter write changes a tenant's overlay: refresh + // the stored-param cache (which the per-tenant loader reads) and drop + // the cached registries. Draft copies (the seeded spec set) never + // overlay, so skip them — avoids an O(n²) reload storm during seeding. + if resource_type == "SearchParameter" + && crate::search::search_parameter_create_affects_overlay(&resource) + { + if let Err(e) = self.reload_stored_cache().await { + tracing::warn!("SearchParameter cache reload failed: {e}"); + } } commit_best_effort_multi_write_session(&mut session, transaction_active, "create").await?; @@ -938,8 +949,12 @@ impl ResourceStorage for MongoBackend { self.index_resource(&db, tenant_id, resource_type, id, &resource, &mut session) .await?; + // A SearchParameter update may change a tenant's overlay (status flips, + // expression edits): refresh the stored-param cache and drop registries. if resource_type == "SearchParameter" { - self.handle_search_parameter_update(current.content(), &resource)?; + if let Err(e) = self.reload_stored_cache().await { + tracing::warn!("SearchParameter cache reload failed: {e}"); + } } commit_best_effort_multi_write_session(&mut session, transaction_active, "update").await?; @@ -1013,7 +1028,6 @@ impl ResourceStorage for MongoBackend { .get_document("data") .map_err(|e| internal_error(format!("Missing resource payload: {}", e)))? .clone(); - let resource_value = document_to_value(&payload)?; let fhir_version = existing_doc .get_str("fhir_version") .unwrap_or("4.0") @@ -1094,8 +1108,12 @@ impl ResourceStorage for MongoBackend { self.delete_search_index(&db, tenant_id, resource_type, id, &mut session) .await?; + // A SearchParameter delete may remove a tenant's overlay entry: refresh + // the stored-param cache and drop registries. if resource_type == "SearchParameter" { - self.handle_search_parameter_delete(&resource_value)?; + if let Err(e) = self.reload_stored_cache().await { + tracing::warn!("SearchParameter cache reload failed: {e}"); + } } commit_best_effort_multi_write_session(&mut session, transaction_active, "delete").await?; @@ -1583,7 +1601,10 @@ impl MongoBackend { self.delete_search_index(db, tenant_id, resource_type, resource_id, session) .await?; - let mut index_docs = match self.search_extractor().extract(resource, resource_type) { + let mut index_docs = match self + .tenant_extractor(tenant_id) + .extract(resource, resource_type) + { Ok(values) => values .iter() .filter_map(|value| { @@ -1610,7 +1631,7 @@ impl MongoBackend { // share the container's (resource_type, resource_id) — so the earlier // delete-by-(type,id) cleans them too — but are flagged `is_contained` // and carry the contained resource's type and local id. - for contained in self.search_extractor().extract_contained(resource) { + for contained in self.tenant_extractor(tenant_id).extract_contained(resource) { for value in &contained.values { if let Some(d) = self.build_contained_index_document( tenant_id, @@ -1841,83 +1862,6 @@ impl MongoBackend { docs } - - fn handle_search_parameter_create(&self, resource: &Value) -> StorageResult<()> { - let loader = SearchParameterLoader::new(self.config().fhir_version); - - match loader.parse_resource(resource) { - Ok(def) => { - if def.status == SearchParameterStatus::Active { - let mut registry = self.search_registry().write(); - if let Err(e) = registry.register(def) { - tracing::debug!("SearchParameter registration skipped: {}", e); - } - } - } - Err(e) => { - tracing::warn!("Failed to parse SearchParameter for registry update: {}", e); - } - } - - Ok(()) - } - - fn handle_search_parameter_update( - &self, - old_resource: &Value, - new_resource: &Value, - ) -> StorageResult<()> { - let loader = SearchParameterLoader::new(self.config().fhir_version); - - let old_def = loader.parse_resource(old_resource).ok(); - let new_def = loader.parse_resource(new_resource).ok(); - - match (old_def, new_def) { - (Some(old), Some(new)) => { - let mut registry = self.search_registry().write(); - - if old.url != new.url { - let _ = registry.unregister(&old.url); - if new.status == SearchParameterStatus::Active { - let _ = registry.register(new); - } - } else if old.status != new.status { - if let Err(e) = registry.update_status(&new.url, new.status) { - tracing::debug!("SearchParameter status update skipped: {}", e); - } - } else { - let _ = registry.unregister(&old.url); - if new.status == SearchParameterStatus::Active { - let _ = registry.register(new); - } - } - } - (None, Some(new)) => { - if new.status == SearchParameterStatus::Active { - let mut registry = self.search_registry().write(); - let _ = registry.register(new); - } - } - (Some(old), None) => { - let mut registry = self.search_registry().write(); - let _ = registry.unregister(&old.url); - } - (None, None) => {} - } - - Ok(()) - } - - fn handle_search_parameter_delete(&self, resource: &Value) -> StorageResult<()> { - if let Some(url) = resource.get("url").and_then(|v| v.as_str()) { - let mut registry = self.search_registry().write(); - if let Err(e) = registry.unregister(url) { - tracing::debug!("SearchParameter unregistration skipped: {}", e); - } - } - - Ok(()) - } } #[async_trait] @@ -2428,24 +2372,12 @@ impl BundleProvider for MongoBackend { reason: format!("Commit failed: {}", e), })?; - for change in pending_search_parameter_changes { - let result = match change { - PendingSearchParameterChange::Create(resource) => { - self.handle_search_parameter_create(&resource) - } - PendingSearchParameterChange::Update { old, new } => { - self.handle_search_parameter_update(&old, &new) - } - PendingSearchParameterChange::Delete(resource) => { - self.handle_search_parameter_delete(&resource) - } - }; - - if let Err(e) = result { - tracing::warn!( - "Transaction committed but failed to apply SearchParameter registry update: {}", - e - ); + // Any SearchParameter change in this transaction alters a tenant's + // overlay — refresh the stored-param cache and drop the cached + // registries so the next access reflects the committed writes. + if !pending_search_parameter_changes.is_empty() { + if let Err(e) = self.reload_stored_cache().await { + tracing::warn!("SearchParameter cache reload failed: {e}"); } } @@ -2784,8 +2716,7 @@ impl MongoBackend { .await?; if resource_type == "SearchParameter" { - pending_search_parameter_changes - .push(PendingSearchParameterChange::Create(resource.clone())); + pending_search_parameter_changes.push(PendingSearchParameterChange::Create); } Ok(StoredResource::from_storage( @@ -2933,10 +2864,7 @@ impl MongoBackend { .await?; if resource_type == "SearchParameter" { - pending_search_parameter_changes.push(PendingSearchParameterChange::Update { - old: current.content().clone(), - new: resource.clone(), - }); + pending_search_parameter_changes.push(PendingSearchParameterChange::Update); } Ok(StoredResource::from_storage( @@ -3007,7 +2935,6 @@ impl MongoBackend { )) })? .clone(); - let resource_value = document_to_value(&payload)?; let fhir_version = existing_doc .get_str("fhir_version") .unwrap_or("4.0") @@ -3077,8 +3004,7 @@ impl MongoBackend { .await?; if resource_type == "SearchParameter" { - pending_search_parameter_changes - .push(PendingSearchParameterChange::Delete(resource_value)); + pending_search_parameter_changes.push(PendingSearchParameterChange::Delete); } Ok(()) @@ -3237,7 +3163,10 @@ impl MongoBackend { ) .await?; - let index_docs = match self.search_extractor().extract(resource, resource_type) { + let index_docs = match self + .tenant_extractor(tenant_id) + .extract(resource, resource_type) + { Ok(values) => values .iter() .filter_map(|value| { @@ -3697,7 +3626,7 @@ impl ReindexTarget for MongoBackend { .await?; let values = self - .search_extractor() + .tenant_extractor(tenant.tenant_id().as_str()) .extract(resource.content(), resource.resource_type()) .map_err(|e| internal_error(format!("Search parameter extraction failed: {e}")))?; diff --git a/crates/persistence/src/backends/postgres/backend.rs b/crates/persistence/src/backends/postgres/backend.rs index 8564af862..249f7f9f2 100644 --- a/crates/persistence/src/backends/postgres/backend.rs +++ b/crates/persistence/src/backends/postgres/backend.rs @@ -1,5 +1,6 @@ //! PostgreSQL backend implementation. +use std::collections::HashMap; use std::fmt::Debug; use std::path::PathBuf; use std::sync::Arc; @@ -14,23 +15,32 @@ use helios_fhir::FhirVersion; use crate::core::{Backend, BackendCapability, BackendKind}; use crate::error::{BackendError, StorageResult}; -use crate::search::{SearchParameterExtractor, SearchParameterLoader, SearchParameterRegistry}; +use crate::search::{ + SearchParameterDefinition, SearchParameterExtractor, SearchParameterLoader, + SearchParameterRegistry, TenantSearchRegistries, +}; + +/// Sync in-memory cache of each tenant's stored (POSTed) active SearchParameter +/// definitions, keyed by tenant id. Postgres queries are async but the per-tenant +/// registry loader must be sync, so async paths (startup, TTL refresh, and +/// SearchParameter writes) populate this map and the loader reads it. +type StoredByTenant = Arc>>>; /// PostgreSQL backend for FHIR resource storage. pub struct PostgresBackend { pool: Pool, config: PostgresConfig, - /// Search parameter registry (in-memory cache of active parameters). - search_registry: Arc>, - /// Extractor for deriving searchable values from resources. - search_extractor: Arc, + /// Per-tenant search parameter registries (shared base + per-tenant overlay). + registries: Arc, + /// Sync cache of each tenant's stored params, read by the registry loader. + stored_by_tenant: StoredByTenant, } impl Debug for PostgresBackend { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("PostgresBackend") .field("config", &self.config) - .field("search_registry_len", &self.search_registry.read().len()) + .field("base_registry_len", &self.registries.base().read().len()) .finish_non_exhaustive() } } @@ -232,16 +242,28 @@ impl PostgresBackend { })?; drop(client); - // Initialize the search parameter registry - let search_registry = Arc::new(RwLock::new(SearchParameterRegistry::new())); - Self::initialize_search_registry(&search_registry, &config); - let search_extractor = Arc::new(SearchParameterExtractor::new(search_registry.clone())); + // Initialize the per-tenant search parameter registries. Base params + // (embedded + spec + custom) go into the shared base; each tenant's + // stored overlay is read from `stored_by_tenant`, populated by + // `reload_stored_cache` at startup / refresh / SearchParameter writes. + let stored_by_tenant: StoredByTenant = Arc::new(RwLock::new(HashMap::new())); + let loader_cache = stored_by_tenant.clone(); + let registries = Arc::new(TenantSearchRegistries::new(Arc::new( + move |tenant_id: &str| { + loader_cache + .read() + .get(tenant_id) + .cloned() + .unwrap_or_default() + }, + ))); + Self::initialize_search_registry(registries.base(), &config); Ok(Self { pool, config, - search_registry, - search_extractor, + registries, + stored_by_tenant, }) } @@ -480,39 +502,23 @@ impl PostgresBackend { pub async fn init_schema(&self) -> StorageResult<()> { let client = self.get_client().await?; super::schema::initialize_schema(&client).await?; - - // Load stored SearchParameters from database - let stored_count = self.load_stored_search_parameters().await?; - if stored_count > 0 { - let registry = self.search_registry.read(); - tracing::info!( - "Loaded {} stored SearchParameters from database (total now: {})", - stored_count, - registry.len() - ); - } - + // Populate the per-tenant stored-param cache so the registries can build + // each tenant's overlay lazily. + self.reload_stored_cache().await?; Ok(()) } - /// Loads SearchParameter resources stored in the database into the registry. - async fn load_stored_search_parameters(&self) -> StorageResult { - self.refresh_stored_search_parameters().await - } - - /// Rebuilds the registry's `Stored` parameters from what the database - /// currently holds, returning how many are registered afterwards. - /// - /// TTL-cache refresh (#235): rows are fetched and parsed before the - /// (sync) write lock is taken, and on any read error the registry is left - /// untouched — stale-serve rather than losing search resolution. - pub async fn refresh_stored_search_parameters(&self) -> StorageResult { + /// Reloads every tenant's stored active SearchParameters into the sync + /// `stored_by_tenant` cache (grouped by tenant), then drops the cached + /// per-tenant registries so they rebuild against the fresh overlay. + pub(crate) async fn reload_stored_cache(&self) -> StorageResult { use crate::search::registry::{SearchParameterSource, SearchParameterStatus}; let client = self.get_client().await?; let rows = client .query( - "SELECT data FROM resources WHERE resource_type = 'SearchParameter' AND is_deleted = FALSE", + "SELECT tenant_id, data FROM resources \ + WHERE resource_type = 'SearchParameter' AND is_deleted = FALSE", &[], ) .await @@ -525,34 +531,30 @@ impl PostgresBackend { })?; let loader = SearchParameterLoader::new(self.config.fhir_version); - let mut definitions = Vec::new(); + let mut by_tenant: HashMap> = HashMap::new(); + let mut count = 0; for row in rows { - let data: serde_json::Value = row.get(0); - match loader.parse_resource(&data) { - Ok(mut def) => { - if def.status == SearchParameterStatus::Active { - def.source = SearchParameterSource::Stored; - definitions.push(def); - } - } - Err(e) => { - tracing::warn!("Failed to parse stored SearchParameter: {}", e); + let tenant_id: String = row.get(0); + let data: serde_json::Value = row.get(1); + if let Ok(mut def) = loader.parse_resource(&data) { + if def.status == SearchParameterStatus::Active { + def.source = SearchParameterSource::Stored; + by_tenant.entry(tenant_id).or_default().push(def); + count += 1; } } } - - let mut registry = self.search_registry.write(); - registry.unregister_source(SearchParameterSource::Stored); - let mut count = 0; - for def in definitions { - match registry.register(def) { - Ok(()) => count += 1, - Err(e) => tracing::warn!("Stored SearchParameter not registered: {}", e), - } - } + *self.stored_by_tenant.write() = by_tenant; + self.registries.invalidate_all(); Ok(count) } + /// TTL-cache refresh (#235): reload the stored-param cache from storage and + /// drop the cached per-tenant registries. Returns the stored-param count. + pub async fn refresh_stored_search_parameters(&self) -> StorageResult { + self.reload_stored_cache().await + } + /// Get a client from the pool. pub(crate) async fn get_client(&self) -> StorageResult { use deadpool_postgres::{PoolError, TimeoutType}; @@ -575,10 +577,24 @@ impl PostgresBackend { }) } - /// Get the search parameter registry. - #[allow(dead_code)] - pub(crate) fn get_search_registry(&self) -> Arc> { - Arc::clone(&self.search_registry) + /// The per-tenant registry container (shared with a co-located ES backend). + pub fn tenant_registries(&self) -> &Arc { + &self.registries + } + + /// The shared base registry (embedded + spec + custom), tenant-independent. + pub(crate) fn base_registry(&self) -> &Arc> { + self.registries.base() + } + + /// The registry for a tenant (base + that tenant's stored overlay). + pub(crate) fn tenant_registry(&self, tenant_id: &str) -> Arc> { + self.registries.for_tenant(tenant_id) + } + + /// A value extractor over a tenant's registry. + pub(crate) fn tenant_extractor(&self, tenant_id: &str) -> SearchParameterExtractor { + SearchParameterExtractor::new(self.tenant_registry(tenant_id)) } /// Returns the backend configuration. @@ -593,16 +609,6 @@ impl PostgresBackend { self.pool.clone() } - /// Returns a reference to the search parameter registry. - pub fn search_registry(&self) -> &Arc> { - &self.search_registry - } - - /// Returns a reference to the search parameter extractor. - pub fn search_extractor(&self) -> &Arc { - &self.search_extractor - } - /// Returns whether search indexing is offloaded to a secondary backend. pub fn is_search_offloaded(&self) -> bool { self.config.search_offloaded @@ -768,13 +774,13 @@ impl SearchCapabilityProvider for PostgresBackend { resource_type: &str, ) -> Option { let params = { - let registry = self.search_registry.read(); + let registry = self.base_registry().read(); registry.get_active_params(resource_type) }; if params.is_empty() { let common_params = { - let registry = self.search_registry.read(); + let registry = self.base_registry().read(); registry.get_active_params("Resource") }; if common_params.is_empty() { @@ -795,7 +801,7 @@ impl SearchCapabilityProvider for PostgresBackend { } let common_params = { - let registry = self.search_registry.read(); + let registry = self.base_registry().read(); registry.get_active_params("Resource") }; for param in &common_params { diff --git a/crates/persistence/src/backends/postgres/search_impl.rs b/crates/persistence/src/backends/postgres/search_impl.rs index 2069f0a41..2bed222f3 100644 --- a/crates/persistence/src/backends/postgres/search_impl.rs +++ b/crates/persistence/src/backends/postgres/search_impl.rs @@ -347,8 +347,9 @@ impl SearchProvider for PostgresBackend { fn search_param_registry( &self, - ) -> &std::sync::Arc> { - self.search_registry() + tenant: &crate::tenant::TenantContext, + ) -> std::sync::Arc> { + self.tenant_registry(tenant.tenant_id().as_str()) } fn supports_contained_search(&self) -> bool { @@ -626,7 +627,7 @@ impl ChainedSearchProvider for PostgresBackend { // The builder produces a `r.id IN (... nested SELECTs ...)` fragment // that handles arbitrary chain depth (was previously stubbed for >2 // segments). - let builder = ChainQueryBuilder::new(tenant_id, base_type, self.search_registry().clone()) + let builder = ChainQueryBuilder::new(tenant_id, base_type, self.tenant_registry(tenant_id)) .with_param_offset(1); let parsed = builder .parse_chain(chain) @@ -678,7 +679,7 @@ impl ChainedSearchProvider for PostgresBackend { // Use the registry-driven builder so we handle nested `_has` chains // and any param type (was previously single-level only with hardcoded // token-or-string-or-empty fallback). - let builder = ChainQueryBuilder::new(tenant_id, base_type, self.search_registry().clone()) + let builder = ChainQueryBuilder::new(tenant_id, base_type, self.tenant_registry(tenant_id)) .with_param_offset(1); let fragment = builder.build_reverse_chain_sql(reverse_chain)?; diff --git a/crates/persistence/src/backends/postgres/storage.rs b/crates/persistence/src/backends/postgres/storage.rs index d8727c2b5..2e5286197 100644 --- a/crates/persistence/src/backends/postgres/storage.rs +++ b/crates/persistence/src/backends/postgres/storage.rs @@ -18,8 +18,6 @@ use crate::core::{ }; use crate::error::TransactionError; use crate::error::{BackendError, ConcurrencyError, ResourceError, StorageError, StorageResult}; -use crate::search::loader::SearchParameterLoader; -use crate::search::registry::SearchParameterStatus; use crate::search::reindex::{ReindexSource, ReindexTarget, ResourcePage}; use crate::tenant::TenantContext; use crate::types::Pagination; @@ -145,9 +143,16 @@ impl ResourceStorage for PostgresBackend { self.index_resource(&client, tenant_id, resource_type, &id, &resource) .await?; - // Handle SearchParameter resources specially - update registry - if resource_type == "SearchParameter" { - self.handle_search_parameter_create(&resource)?; + // An *active* SearchParameter write changes a tenant's overlay: reload + // the stored cache and drop the per-tenant registries so they rebuild. + // Draft copies (the seeded spec set) never overlay, so skip them and + // avoid an O(n²) reload storm during bulk seeding. + if resource_type == "SearchParameter" + && crate::search::search_parameter_create_affects_overlay(&resource) + { + if let Err(e) = self.reload_stored_cache().await { + tracing::warn!("SearchParameter cache reload failed: {e}"); + } } // Return the stored resource with updated metadata @@ -342,9 +347,11 @@ impl ResourceStorage for PostgresBackend { self.index_resource(&client, tenant_id, resource_type, id, &resource) .await?; - // Handle SearchParameter resources specially - update registry + // A SearchParameter write invalidates the tenant overlays. if resource_type == "SearchParameter" { - self.handle_search_parameter_update(current.content(), &resource)?; + if let Err(e) = self.reload_stored_cache().await { + tracing::warn!("SearchParameter cache reload failed: {e}"); + } } Ok(StoredResource::from_storage( @@ -432,9 +439,11 @@ impl ResourceStorage for PostgresBackend { .map_err(|e| internal_error(format!("Failed to delete search index: {}", e)))?; } - // Handle SearchParameter resources specially - update registry + // A SearchParameter delete invalidates the tenant overlays. if resource_type == "SearchParameter" { - self.handle_search_parameter_delete(&data)?; + if let Err(e) = self.reload_stored_cache().await { + tracing::warn!("SearchParameter cache reload failed: {e}"); + } } Ok(()) @@ -836,7 +845,10 @@ impl PostgresBackend { .map_err(|e| internal_error(format!("Failed to clear search index: {}", e)))?; // Extract values using the registry-driven extractor - match self.search_extractor().extract(resource, resource_type) { + match self + .tenant_extractor(tenant_id) + .extract(resource, resource_type) + { Ok(values) => { let mut count = 0; for value in values { @@ -901,7 +913,7 @@ impl PostgresBackend { ) -> StorageResult { let mut count = 0; let container = (container_type, container_id); - for contained in self.search_extractor().extract_contained(resource) { + for contained in self.tenant_extractor(tenant_id).extract_contained(resource) { for value in &contained.values { PostgresSearchIndexWriter::write_contained_entry( client, @@ -1056,114 +1068,6 @@ impl PostgresBackend { } } -// ============================================================================ -// SearchParameter Resource Handling -// ============================================================================ - -impl PostgresBackend { - /// Handle creation of a SearchParameter resource. - /// - /// If the SearchParameter has status=active, it will be registered in the - /// search parameter registry, making it available for searches on new resources. - /// Existing resources will NOT be indexed for this parameter until $reindex is run. - fn handle_search_parameter_create(&self, resource: &Value) -> StorageResult<()> { - let loader = SearchParameterLoader::new(self.config().fhir_version); - - match loader.parse_resource(resource) { - Ok(def) => { - // Only register if status is active - if def.status == SearchParameterStatus::Active { - let mut registry = self.search_registry().write(); - // Ignore duplicate URL errors - the param may already be embedded - if let Err(e) = registry.register(def) { - tracing::debug!("SearchParameter registration skipped: {}", e); - } - } - } - Err(e) => { - // Log but don't fail - the resource is still stored - tracing::warn!("Failed to parse SearchParameter for registry: {}", e); - } - } - - Ok(()) - } - - /// Handle update of a SearchParameter resource. - /// - /// Updates the registry based on status changes: - /// - active -> retired: Parameter disabled for searches - /// - retired -> active: Parameter re-enabled for searches - /// - Any other change: Updates the registry entry - fn handle_search_parameter_update( - &self, - old_resource: &Value, - new_resource: &Value, - ) -> StorageResult<()> { - let loader = SearchParameterLoader::new(self.config().fhir_version); - - let old_def = loader.parse_resource(old_resource).ok(); - let new_def = loader.parse_resource(new_resource).ok(); - - match (old_def, new_def) { - (Some(old), Some(new)) => { - let mut registry = self.search_registry().write(); - - // If URL changed, unregister old and register new - if old.url != new.url { - let _ = registry.unregister(&old.url); - if new.status == SearchParameterStatus::Active { - let _ = registry.register(new); - } - } else if old.status != new.status { - // Status change - update in registry - if let Err(e) = registry.update_status(&new.url, new.status) { - tracing::debug!("SearchParameter status update skipped: {}", e); - } - } else { - // Other changes - re-register (unregister then register) - let _ = registry.unregister(&old.url); - if new.status == SearchParameterStatus::Active { - let _ = registry.register(new); - } - } - } - (None, Some(new)) => { - // Old wasn't valid, try to register new - if new.status == SearchParameterStatus::Active { - let mut registry = self.search_registry().write(); - let _ = registry.register(new); - } - } - (Some(old), None) => { - // New isn't valid, unregister old - let mut registry = self.search_registry().write(); - let _ = registry.unregister(&old.url); - } - (None, None) => { - // Neither valid - nothing to do - } - } - - Ok(()) - } - - /// Handle deletion of a SearchParameter resource. - /// - /// Removes the parameter from the registry. Search index entries for this - /// parameter are NOT automatically cleaned up (use $reindex for that). - fn handle_search_parameter_delete(&self, resource: &Value) -> StorageResult<()> { - if let Some(url) = resource.get("url").and_then(|v| v.as_str()) { - let mut registry = self.search_registry().write(); - if let Err(e) = registry.unregister(url) { - tracing::debug!("SearchParameter unregistration skipped: {}", e); - } - } - - Ok(()) - } -} - // ============================================================================ // VersionedStorage Implementation // ============================================================================ @@ -2393,7 +2297,7 @@ impl PostgresBackend { } // Build SearchParameter objects by looking up types from the registry - let search_params = self.build_search_parameters(resource_type, &parsed_params)?; + let search_params = self.build_search_parameters(tenant, resource_type, &parsed_params)?; // Build a SearchQuery let query = SearchQuery { @@ -2412,10 +2316,12 @@ impl PostgresBackend { /// Builds SearchParameter objects from parsed (name, value) pairs. fn build_search_parameters( &self, + tenant: &TenantContext, resource_type: &str, params: &[(String, String)], ) -> StorageResult> { - let registry = self.search_registry().read(); + let registry_arc = self.tenant_registry(tenant.tenant_id().as_str()); + let registry = registry_arc.read(); let mut search_params = Vec::with_capacity(params.len()); for (name, value) in params { @@ -3154,9 +3060,9 @@ impl ReindexTarget for PostgresBackend { let resource_id = resource.id(); let content = resource.content(); - // Use the dynamic extraction + // Use the dynamic extraction over the tenant's registry let values = self - .search_extractor() + .tenant_extractor(tenant_id) .extract(content, resource_type) .map_err(|e| internal_error(format!("Search parameter extraction failed: {}", e)))?; diff --git a/crates/persistence/src/backends/postgres/transaction.rs b/crates/persistence/src/backends/postgres/transaction.rs index 56813ebe8..9c36f6c72 100644 --- a/crates/persistence/src/backends/postgres/transaction.rs +++ b/crates/persistence/src/backends/postgres/transaction.rs @@ -563,7 +563,7 @@ impl TransactionProvider for PostgresBackend { PostgresTransaction::new( client, tenant.clone(), - self.search_extractor().clone(), + std::sync::Arc::new(self.tenant_extractor(tenant.tenant_id().as_str())), self.is_search_offloaded(), ) .await diff --git a/crates/persistence/src/backends/s3/storage.rs b/crates/persistence/src/backends/s3/storage.rs index 44247fbe0..9a650f950 100644 --- a/crates/persistence/src/backends/s3/storage.rs +++ b/crates/persistence/src/backends/s3/storage.rs @@ -1207,20 +1207,17 @@ impl SearchProvider for S3Backend { fn search_param_registry( &self, - ) -> &std::sync::Arc> { + _tenant: &crate::tenant::TenantContext, + ) -> std::sync::Arc> { // S3 standalone does not implement search; an empty registry is // required only to satisfy the trait. In real deployments S3 is // composed with a search backend (e.g., Elasticsearch) and the // composite forwards to that backend's registry. use std::sync::OnceLock; - static EMPTY: OnceLock< - std::sync::Arc>, - > = OnceLock::new(); - EMPTY.get_or_init(|| { - std::sync::Arc::new(parking_lot::RwLock::new( - crate::search::SearchParameterRegistry::new(), - )) - }) + static EMPTY: OnceLock = OnceLock::new(); + EMPTY + .get_or_init(crate::search::TenantSearchRegistries::base_only) + .for_tenant("") } } diff --git a/crates/persistence/src/backends/sqlite/backend.rs b/crates/persistence/src/backends/sqlite/backend.rs index afc26c2c0..772dcb665 100644 --- a/crates/persistence/src/backends/sqlite/backend.rs +++ b/crates/persistence/src/backends/sqlite/backend.rs @@ -15,10 +15,59 @@ use helios_fhir::FhirVersion; use crate::core::{Backend, BackendCapability, BackendKind}; use crate::error::{BackendError, StorageResult}; -use crate::search::{SearchParameterExtractor, SearchParameterLoader, SearchParameterRegistry}; +use crate::search::{ + SearchParameterDefinition, SearchParameterExtractor, SearchParameterLoader, + SearchParameterRegistry, TenantSearchRegistries, +}; use super::schema; +/// Reads a tenant's stored (POSTed) active SearchParameter definitions from the +/// database. Used as the [`TenantSearchRegistries`] loader closure — it captures +/// only the pool + FHIR version, never the backend, so an Elasticsearch backend +/// sharing the container resolves per-tenant params through this same query. +fn load_tenant_stored_params( + pool: &Pool, + fhir_version: FhirVersion, + tenant_id: &str, +) -> Vec { + use crate::search::registry::{SearchParameterSource, SearchParameterStatus}; + + let Ok(conn) = pool.get() else { + tracing::warn!("SearchParameter loader: could not get connection"); + return Vec::new(); + }; + let Ok(mut stmt) = conn.prepare( + "SELECT data FROM resources WHERE resource_type = 'SearchParameter' \ + AND tenant_id = ?1 AND is_deleted = 0", + ) else { + tracing::warn!("SearchParameter loader: prepare failed"); + return Vec::new(); + }; + let rows = match stmt.query_map([tenant_id], |row| row.get::<_, Vec>(0)) { + Ok(rows) => rows, + Err(e) => { + tracing::warn!("SearchParameter loader: query failed: {e}"); + return Vec::new(); + } + }; + let loader = SearchParameterLoader::new(fhir_version); + let mut defs = Vec::new(); + for row in rows { + let Ok(data) = row else { continue }; + let Ok(json) = serde_json::from_slice::(&data) else { + continue; + }; + if let Ok(mut def) = loader.parse_resource(&json) { + if def.status == SearchParameterStatus::Active { + def.source = SearchParameterSource::Stored; + defs.push(def); + } + } + } + defs +} + /// Counter for generating unique in-memory database names. static MEMORY_DB_COUNTER: AtomicU64 = AtomicU64::new(0); @@ -27,10 +76,9 @@ pub struct SqliteBackend { pool: Pool, config: SqliteBackendConfig, is_memory: bool, - /// Search parameter registry (in-memory cache of active parameters). - search_registry: Arc>, - /// Extractor for deriving searchable values from resources. - search_extractor: Arc, + /// Per-tenant search parameter registries: a shared base (embedded + spec + + /// custom) plus each tenant's stored (POSTed) overlay, built lazily. + registries: Arc, } impl Debug for SqliteBackend { @@ -38,7 +86,7 @@ impl Debug for SqliteBackend { f.debug_struct("SqliteBackend") .field("config", &self.config) .field("is_memory", &self.is_memory) - .field("search_registry_len", &self.search_registry.read().len()) + .field("base_registry_len", &self.registries.base().read().len()) .finish_non_exhaustive() } } @@ -201,11 +249,19 @@ impl SqliteBackend { }) })?; - // Initialize the search parameter registry - let search_registry = Arc::new(RwLock::new(SearchParameterRegistry::new())); + // Initialize the per-tenant search parameter registries. The loader + // reads a tenant's stored params from this pool; base params (embedded + + // spec + custom) are registered into the shared base below. + let loader_pool = pool.clone(); + let loader_version = config.fhir_version; + let registries = Arc::new(TenantSearchRegistries::new(Arc::new( + move |tenant_id: &str| { + load_tenant_stored_params(&loader_pool, loader_version, tenant_id) + }, + ))); { let loader = SearchParameterLoader::new(config.fhir_version); - let mut registry = search_registry.write(); + let mut registry = registries.base().write(); // Track counts and sources for summary let mut fallback_count = 0; @@ -295,14 +351,12 @@ impl SqliteBackend { resource_type_count ); } - let search_extractor = Arc::new(SearchParameterExtractor::new(search_registry.clone())); let backend = Self { pool, config, is_memory, - search_registry, - search_extractor, + registries, }; // Configure the connection @@ -318,107 +372,21 @@ impl SqliteBackend { pub fn init_schema(&self) -> StorageResult<()> { let conn = self.get_connection()?; schema::initialize_schema(&conn)?; - - // Load stored (POSTed) SearchParameters from database - let stored_count = self.load_stored_search_parameters()?; - if stored_count > 0 { - let registry = self.search_registry.read(); - tracing::info!( - "Loaded {} stored SearchParameters from database (total now: {})", - stored_count, - registry.len() - ); - } - + // Per-tenant registries build lazily on first access (each tenant's + // stored params are read from storage then), so nothing to eagerly load. Ok(()) } - /// Loads SearchParameter resources stored in the database into the registry. + /// Drops every cached per-tenant registry so each tenant's stored-param + /// overlay is re-read from storage on next access. /// - /// This is called during schema initialization to restore any custom - /// SearchParameters that were POSTed to the server. - fn load_stored_search_parameters(&self) -> StorageResult { - self.refresh_stored_search_parameters() - } - - /// Rebuilds the registry's `Stored` parameters from what the database - /// currently holds, returning how many are registered afterwards. - /// - /// This is the TTL-cache refresh (#235): storage is the source of truth, - /// so a SearchParameter POSTed to a cluster-mate appears here on the next - /// pass. All rows are read and parsed **before** the write lock is taken - /// (the lock is sync and shared with request paths), and on any read - /// error the registry is left untouched — a node serves its stale cache - /// rather than losing search resolution. + /// This is the TTL-cache refresh (#235): storage is the source of truth, so + /// a SearchParameter POSTed to a cluster-mate becomes visible on the next + /// pass. Returns the number of cached tenant registries that were dropped. pub fn refresh_stored_search_parameters(&self) -> StorageResult { - use crate::search::registry::{SearchParameterSource, SearchParameterStatus}; - - let conn = self.get_connection()?; - let mut stmt = conn - .prepare( - "SELECT data FROM resources WHERE resource_type = 'SearchParameter' AND is_deleted = 0", - ) - .map_err(|e| { - crate::error::StorageError::Backend(BackendError::Internal { - backend_name: "sqlite".to_string(), - message: format!("Failed to prepare SearchParameter query: {}", e), - source: None, - }) - })?; - - let rows = stmt - .query_map([], |row| row.get::<_, Vec>(0)) - .map_err(|e| { - crate::error::StorageError::Backend(BackendError::Internal { - backend_name: "sqlite".to_string(), - message: format!("Failed to query SearchParameters: {}", e), - source: None, - }) - })?; - - let loader = SearchParameterLoader::new(self.config.fhir_version); - let mut definitions = Vec::new(); - for row in rows { - let data = match row { - Ok(data) => data, - Err(e) => { - tracing::warn!("Failed to read SearchParameter row: {}", e); - continue; - } - }; - let json: serde_json::Value = match serde_json::from_slice(&data) { - Ok(json) => json, - Err(e) => { - tracing::warn!("Failed to parse SearchParameter JSON: {}", e); - continue; - } - }; - match loader.parse_resource(&json) { - Ok(mut def) => { - // Only register active parameters - if def.status == SearchParameterStatus::Active { - def.source = SearchParameterSource::Stored; - definitions.push(def); - } - } - Err(e) => { - tracing::warn!("Failed to parse stored SearchParameter: {}", e); - } - } - } - - // Synchronous rebuild under the existing lock; the Arc identity must - // be preserved (extractor/ES/chain builders hold clones). - let mut registry = self.search_registry.write(); - registry.unregister_source(SearchParameterSource::Stored); - let mut count = 0; - for def in definitions { - match registry.register(def) { - Ok(()) => count += 1, - Err(e) => tracing::warn!("Stored SearchParameter not registered: {}", e), - } - } - Ok(count) + let n = self.registries.cached_tenant_count(); + self.registries.invalidate_all(); + Ok(n) } /// Returns a clone of the connection pool (cheap — pool is `Arc`-backed internally). @@ -438,9 +406,25 @@ impl SqliteBackend { }) } - /// Get the search parameter registry. - pub(crate) fn get_search_registry(&self) -> Arc> { - Arc::clone(&self.search_registry) + /// The per-tenant registry container (shared with a co-located ES backend). + pub fn tenant_registries(&self) -> &Arc { + &self.registries + } + + /// The shared base registry (embedded + spec + custom), tenant-independent. + pub(crate) fn base_registry(&self) -> &Arc> { + self.registries.base() + } + + /// The registry for a tenant (base + that tenant's stored overlay). + pub(crate) fn tenant_registry(&self, tenant_id: &str) -> Arc> { + self.registries.for_tenant(tenant_id) + } + + /// A value extractor over a tenant's registry, for indexing that tenant's + /// resources. + pub(crate) fn tenant_extractor(&self, tenant_id: &str) -> SearchParameterExtractor { + SearchParameterExtractor::new(self.tenant_registry(tenant_id)) } /// Configure connection settings. @@ -493,16 +477,6 @@ impl SqliteBackend { &self.config } - /// Returns a reference to the search parameter registry. - pub fn search_registry(&self) -> &Arc> { - &self.search_registry - } - - /// Returns a reference to the search parameter extractor. - pub fn search_extractor(&self) -> &Arc { - &self.search_extractor - } - /// Returns whether search indexing is offloaded to a secondary backend. pub fn is_search_offloaded(&self) -> bool { self.config.search_offloaded @@ -655,14 +629,14 @@ impl SearchCapabilityProvider for SqliteBackend { ) -> Option { // Get active parameters for this resource type from the registry let params = { - let registry = self.search_registry.read(); + let registry = self.base_registry().read(); registry.get_active_params(resource_type) }; if params.is_empty() { // Also check if there are Resource-level params let common_params = { - let registry = self.search_registry.read(); + let registry = self.base_registry().read(); registry.get_active_params("Resource") }; if common_params.is_empty() { @@ -690,7 +664,7 @@ impl SearchCapabilityProvider for SqliteBackend { // Add common Resource-level parameters let common_params = { - let registry = self.search_registry.read(); + let registry = self.base_registry().read(); registry.get_active_params("Resource") }; for param in &common_params { diff --git a/crates/persistence/src/backends/sqlite/search_impl.rs b/crates/persistence/src/backends/sqlite/search_impl.rs index b13263ec6..dfccdc388 100644 --- a/crates/persistence/src/backends/sqlite/search_impl.rs +++ b/crates/persistence/src/backends/sqlite/search_impl.rs @@ -396,8 +396,9 @@ impl SearchProvider for SqliteBackend { fn search_param_registry( &self, - ) -> &std::sync::Arc> { - self.search_registry() + tenant: &crate::tenant::TenantContext, + ) -> std::sync::Arc> { + self.tenant_registry(tenant.tenant_id().as_str()) } fn supports_contained_search(&self) -> bool { @@ -733,7 +734,7 @@ impl ChainedSearchProvider for SqliteBackend { } // Create the chain query builder with registry access - let builder = ChainQueryBuilder::new(tenant_id, base_type, self.get_search_registry()) + let builder = ChainQueryBuilder::new(tenant_id, base_type, self.tenant_registry(tenant_id)) .with_param_offset(2); // After ?1 (tenant) and ?2 (resource_type) // Parse the chain @@ -807,7 +808,7 @@ impl ChainedSearchProvider for SqliteBackend { let tenant_id = tenant.tenant_id().as_str(); // Create the chain query builder with registry access - let builder = ChainQueryBuilder::new(tenant_id, base_type, self.get_search_registry()) + let builder = ChainQueryBuilder::new(tenant_id, base_type, self.tenant_registry(tenant_id)) .with_param_offset(2); // After ?1 (tenant) and ?2 (resource_type) // Build the SQL fragment for reverse chain diff --git a/crates/persistence/src/backends/sqlite/storage.rs b/crates/persistence/src/backends/sqlite/storage.rs index 3440ef264..62cfef44e 100644 --- a/crates/persistence/src/backends/sqlite/storage.rs +++ b/crates/persistence/src/backends/sqlite/storage.rs @@ -20,8 +20,6 @@ use crate::core::{ use crate::error::TransactionError; use crate::error::{BackendError, ConcurrencyError, ResourceError, StorageError, StorageResult}; use crate::search::extractor::ExtractedValue; -use crate::search::loader::SearchParameterLoader; -use crate::search::registry::SearchParameterStatus; use crate::search::reindex::{ReindexSource, ReindexTarget, ResourcePage}; use crate::tenant::TenantContext; use crate::types::Pagination; @@ -145,9 +143,14 @@ impl ResourceStorage for SqliteBackend { // Index the resource for search self.index_resource(&conn, tenant_id, resource_type, &id, &resource)?; - // Handle SearchParameter resources specially - update registry - if resource_type == "SearchParameter" { - self.handle_search_parameter_create(&resource)?; + // An *active* SearchParameter write changes this tenant's overlay — drop + // its cached registry so the next access rebuilds from storage. Draft + // copies (e.g. the seeded spec set) never overlay, so skip them and + // avoid an O(n²) rebuild storm during bulk seeding. + if resource_type == "SearchParameter" + && crate::search::search_parameter_create_affects_overlay(&resource) + { + self.tenant_registries().invalidate(tenant_id); } // Return the stored resource with updated metadata @@ -365,9 +368,9 @@ impl ResourceStorage for SqliteBackend { self.delete_search_index(&conn, tenant_id, resource_type, id)?; self.index_resource(&conn, tenant_id, resource_type, id, &resource)?; - // Handle SearchParameter resources specially - update registry + // A SearchParameter write invalidates this tenant's cached registry. if resource_type == "SearchParameter" { - self.handle_search_parameter_update(current.content(), &resource)?; + self.tenant_registries().invalidate(tenant_id); } Ok(StoredResource::from_storage( @@ -445,11 +448,9 @@ impl ResourceStorage for SqliteBackend { .map_err(|e| internal_error(format!("Failed to delete search index: {}", e)))?; } - // Handle SearchParameter resources specially - update registry + // A SearchParameter delete invalidates this tenant's cached registry. if resource_type == "SearchParameter" { - if let Ok(resource_json) = serde_json::from_slice::(&data) { - self.handle_search_parameter_delete(&resource_json)?; - } + self.tenant_registries().invalidate(tenant_id); } Ok(()) @@ -984,9 +985,9 @@ impl SqliteBackend { resource_id: &str, resource: &Value, ) -> StorageResult { - // Extract values using the registry-driven extractor + // Extract values using the tenant's registry-driven extractor let values = self - .search_extractor() + .tenant_extractor(tenant_id) .extract(resource, resource_type) .map_err(|e| internal_error(format!("Search parameter extraction failed: {}", e)))?; @@ -1065,7 +1066,7 @@ impl SqliteBackend { ) -> StorageResult { let mut count = 0; let container = (container_type, container_id); - for contained in self.search_extractor().extract_contained(resource) { + for contained in self.tenant_extractor(tenant_id).extract_contained(resource) { for value in &contained.values { self.write_contained_index_entry( conn, @@ -1303,111 +1304,6 @@ impl SqliteBackend { } } -// SearchParameter Resource Handling -impl SqliteBackend { - /// Handle creation of a SearchParameter resource. - /// - /// If the SearchParameter has status=active, it will be registered in the - /// search parameter registry, making it available for searches on new resources. - /// Existing resources will NOT be indexed for this parameter until $reindex is run. - fn handle_search_parameter_create(&self, resource: &Value) -> StorageResult<()> { - let loader = SearchParameterLoader::new(FhirVersion::default_enabled()); - - match loader.parse_resource(resource) { - Ok(def) => { - // Only register if status is active - if def.status == SearchParameterStatus::Active { - let mut registry = self.search_registry().write(); - // Ignore duplicate URL errors - the param may already be embedded - if let Err(e) = registry.register(def) { - tracing::debug!("SearchParameter registration skipped: {}", e); - } - } - } - Err(e) => { - // Log but don't fail - the resource is still stored - tracing::warn!("Failed to parse SearchParameter for registry: {}", e); - } - } - - Ok(()) - } - - /// Handle update of a SearchParameter resource. - /// - /// Updates the registry based on status changes: - /// - active -> retired: Parameter disabled for searches - /// - retired -> active: Parameter re-enabled for searches - /// - Any other change: Updates the registry entry - fn handle_search_parameter_update( - &self, - old_resource: &Value, - new_resource: &Value, - ) -> StorageResult<()> { - let loader = SearchParameterLoader::new(FhirVersion::default_enabled()); - - let old_def = loader.parse_resource(old_resource).ok(); - let new_def = loader.parse_resource(new_resource).ok(); - - match (old_def, new_def) { - (Some(old), Some(new)) => { - let mut registry = self.search_registry().write(); - - // If URL changed, unregister old and register new - if old.url != new.url { - let _ = registry.unregister(&old.url); - if new.status == SearchParameterStatus::Active { - let _ = registry.register(new); - } - } else if old.status != new.status { - // Status change - update in registry - if let Err(e) = registry.update_status(&new.url, new.status) { - tracing::debug!("SearchParameter status update skipped: {}", e); - } - } else { - // Other changes - re-register (unregister then register) - let _ = registry.unregister(&old.url); - if new.status == SearchParameterStatus::Active { - let _ = registry.register(new); - } - } - } - (None, Some(new)) => { - // Old wasn't valid, try to register new - if new.status == SearchParameterStatus::Active { - let mut registry = self.search_registry().write(); - let _ = registry.register(new); - } - } - (Some(old), None) => { - // New isn't valid, unregister old - let mut registry = self.search_registry().write(); - let _ = registry.unregister(&old.url); - } - (None, None) => { - // Neither valid - nothing to do - } - } - - Ok(()) - } - - /// Handle deletion of a SearchParameter resource. - /// - /// Removes the parameter from the registry. Search index entries for this - /// parameter are NOT automatically cleaned up (use $reindex for that). - fn handle_search_parameter_delete(&self, resource: &Value) -> StorageResult<()> { - if let Some(url) = resource.get("url").and_then(|v| v.as_str()) { - let mut registry = self.search_registry().write(); - if let Err(e) = registry.unregister(url) { - tracing::debug!("SearchParameter unregistration skipped: {}", e); - } - } - - Ok(()) - } -} - #[async_trait] impl VersionedStorage for SqliteBackend { async fn vread( @@ -2696,7 +2592,7 @@ impl SqliteBackend { } // Build SearchParameter objects by looking up types from the registry - let search_params = self.build_search_parameters(resource_type, &parsed_params)?; + let search_params = self.build_search_parameters(tenant, resource_type, &parsed_params)?; // Build a SearchQuery let query = SearchQuery { @@ -2719,10 +2615,12 @@ impl SqliteBackend { /// for common parameters when not found. fn build_search_parameters( &self, + tenant: &TenantContext, resource_type: &str, params: &[(String, String)], ) -> StorageResult> { - let registry = self.search_registry().read(); + let registry_arc = self.tenant_registry(tenant.tenant_id().as_str()); + let registry = registry_arc.read(); let mut search_params = Vec::with_capacity(params.len()); for (name, value) in params { @@ -3532,9 +3430,9 @@ impl ReindexTarget for SqliteBackend { let resource_id = resource.id(); let content = resource.content(); - // Use the dynamic extraction + // Use the dynamic extraction over the tenant's registry let values = self - .search_extractor() + .tenant_extractor(tenant.tenant_id().as_str()) .extract(content, resource_type) .map_err(|e| internal_error(format!("Search parameter extraction failed: {}", e)))?; diff --git a/crates/persistence/src/backends/sqlite/transaction.rs b/crates/persistence/src/backends/sqlite/transaction.rs index c3979378b..2c0a7cfb7 100644 --- a/crates/persistence/src/backends/sqlite/transaction.rs +++ b/crates/persistence/src/backends/sqlite/transaction.rs @@ -551,7 +551,7 @@ impl TransactionProvider for SqliteBackend { SqliteTransaction::new( conn, tenant.clone(), - self.search_extractor().clone(), + Arc::new(self.tenant_extractor(tenant.tenant_id().as_str())), self.is_search_offloaded(), ) } diff --git a/crates/persistence/src/composite/storage.rs b/crates/persistence/src/composite/storage.rs index fd9e6726b..c40225403 100644 --- a/crates/persistence/src/composite/storage.rs +++ b/crates/persistence/src/composite/storage.rs @@ -1090,18 +1090,17 @@ impl SearchProvider for CompositeStorage { fn search_param_registry( &self, - ) -> &std::sync::Arc> { + tenant: &crate::tenant::TenantContext, + ) -> std::sync::Arc> { // Same routing as `search`: prefer the dedicated Search backend's - // registry, fall back to primary, otherwise an empty registry. The - // returned reference outlives `&self` because both providers are - // owned by `self.search_providers` for the lifetime of the composite. + // registry, fall back to primary, otherwise an empty registry. if let Some(search_backend) = self .config .backends_with_role(super::config::BackendRole::Search) .next() { if let Some(provider) = self.search_providers.get(&search_backend.id) { - return provider.search_param_registry(); + return provider.search_param_registry(tenant); } } @@ -1109,18 +1108,12 @@ impl SearchProvider for CompositeStorage { .search_providers .get(self.config.primary_id().unwrap_or("primary")) { - return provider.search_param_registry(); + return provider.search_param_registry(tenant); } - use std::sync::OnceLock; - static EMPTY: OnceLock< - std::sync::Arc>, - > = OnceLock::new(); - EMPTY.get_or_init(|| { - std::sync::Arc::new(parking_lot::RwLock::new( - crate::search::SearchParameterRegistry::new(), - )) - }) + std::sync::Arc::new(parking_lot::RwLock::new( + crate::search::SearchParameterRegistry::new(), + )) } fn supports_contained_search(&self) -> bool { @@ -1744,7 +1737,7 @@ impl CompositeStorage { for resource in resources { for include in includes { // Extract references from resource based on search param - let refs = self.extract_references(resource, &include.search_param); + let refs = self.extract_references(tenant, resource, &include.search_param); for reference in refs { // Parse reference: "ResourceType/id" @@ -1784,21 +1777,25 @@ impl CompositeStorage { /// only when the registry doesn't know about the parameter at all. /// That keeps unregistered custom parameters working as they did /// before this change. - fn extract_references(&self, resource: &StoredResource, search_param: &str) -> Vec { + fn extract_references( + &self, + tenant: &TenantContext, + resource: &StoredResource, + search_param: &str, + ) -> Vec { let content = resource.content(); let resource_type = resource.resource_type(); + let registry_arc = self.search_param_registry(tenant); let registered = { - let registry = self.search_param_registry().read(); + let registry = registry_arc.read(); registry .get_param(resource_type, search_param) .or_else(|| registry.get_param("Resource", search_param)) }; if let Some(param_def) = registered { - let extractor = crate::search::SearchParameterExtractor::new(Arc::clone( - self.search_param_registry(), - )); + let extractor = crate::search::SearchParameterExtractor::new(Arc::clone(®istry_arc)); if let Ok(values) = extractor.extract_for_param(content, ¶m_def) { // Trust the registry: if the param is registered, return what // the FHIRPath expression yields (even if empty) rather than @@ -1938,7 +1935,7 @@ impl ChainedSearchProvider for CompositeStorage { // Extract references to base_type let mut ids = Vec::new(); for resource in result.resources.items { - let refs = self.extract_references(&resource, &reverse_chain.reference_param); + let refs = self.extract_references(tenant, &resource, &reverse_chain.reference_param); for reference in refs { if let Some((ref_type, ref_id)) = reference.split_once('/') { if ref_type == base_type { @@ -1984,7 +1981,8 @@ impl CompositeStorage { // chain_builder::resolve_target_type so composite agrees with SQLite // and Postgres on ambiguous reference disambiguation. let target_types: Vec = { - let registry = self.search_param_registry().read(); + let reg = self.search_param_registry(tenant); + let registry = reg.read(); let mut types = Vec::with_capacity(parts.len() - 1); let mut current = base_type.to_string(); for ref_param in parts.iter().take(parts.len() - 1) { @@ -2013,7 +2011,8 @@ impl CompositeStorage { let terminal_query = SearchQuery::new(deepest_type).with_parameter(SearchParameter { name: terminal_param.to_string(), param_type: { - let registry = self.search_param_registry().read(); + let reg = self.search_param_registry(tenant); + let registry = reg.read(); crate::search::resolve_param_type( ®istry, deepest_type, @@ -2574,16 +2573,11 @@ mod tests { fn search_param_registry( &self, - ) -> &std::sync::Arc> { - use std::sync::OnceLock; - static EMPTY: OnceLock< - std::sync::Arc>, - > = OnceLock::new(); - EMPTY.get_or_init(|| { - std::sync::Arc::new(parking_lot::RwLock::new( - crate::search::SearchParameterRegistry::new(), - )) - }) + _tenant: &crate::tenant::TenantContext, + ) -> std::sync::Arc> { + std::sync::Arc::new(parking_lot::RwLock::new( + crate::search::SearchParameterRegistry::new(), + )) } } @@ -3844,8 +3838,11 @@ mod tests { ) -> StorageResult { Ok(0) } - fn search_param_registry(&self) -> &Arc> { - &self.registry + fn search_param_registry( + &self, + _tenant: &TenantContext, + ) -> Arc> { + Arc::clone(&self.registry) } } @@ -3878,7 +3875,8 @@ mod tests { FhirVersion::default(), ); - let refs = composite.extract_references(&resource, "subject"); + let tenant = TenantContext::new(TenantId::new("test"), TenantPermissions::full_access()); + let refs = composite.extract_references(&tenant, &resource, "subject"); assert_eq!(refs, vec!["Patient/p1".to_string()]); } @@ -4099,16 +4097,11 @@ mod tests { fn search_param_registry( &self, - ) -> &std::sync::Arc> { - use std::sync::OnceLock; - static EMPTY: OnceLock< - std::sync::Arc>, - > = OnceLock::new(); - EMPTY.get_or_init(|| { - std::sync::Arc::new(parking_lot::RwLock::new( - crate::search::SearchParameterRegistry::new(), - )) - }) + _tenant: &crate::tenant::TenantContext, + ) -> std::sync::Arc> { + std::sync::Arc::new(parking_lot::RwLock::new( + crate::search::SearchParameterRegistry::new(), + )) } } } diff --git a/crates/persistence/src/core/search.rs b/crates/persistence/src/core/search.rs index dc4dab43d..212d7d6b6 100644 --- a/crates/persistence/src/core/search.rs +++ b/crates/persistence/src/core/search.rs @@ -277,13 +277,20 @@ pub trait SearchProvider: ResourceStorage { async fn search_count(&self, tenant: &TenantContext, query: &SearchQuery) -> StorageResult; - /// Returns the backend's search parameter registry. + /// Returns the search parameter registry **for a tenant**. /// - /// The registry is the single source of truth for search parameter type - /// resolution (see [`crate::search::resolve_param_type`]). REST extractors - /// and chained-search builders both consult it so they cannot disagree on - /// whether a given param is a Date vs. Token vs. Reference, etc. - fn search_param_registry(&self) -> &Arc>; + /// Search-parameter resolution is tenant-scoped: every tenant sees the shared + /// base params (embedded + spec + custom), plus its own stored (POSTed) + /// params overlaid on top. So `acme1` and `acme2` may resolve searches + /// against different parameter sets. The returned registry is the source of + /// truth for param-type resolution (see [`crate::search::resolve_param_type`]); + /// REST extractors and chained-search builders both consult it so they cannot + /// disagree on whether a given param is a Date vs. Token vs. Reference, etc. + /// + /// Returns an owned `Arc` (cloned from the per-tenant cache); bind it to a + /// local before `.read()`. + fn search_param_registry(&self, tenant: &TenantContext) + -> Arc>; /// Whether this backend can evaluate `_contained=true|both` searches (which /// require contained-resource indexing). Defaults to `false`; backends that @@ -413,7 +420,8 @@ where return Ok(Vec::new()); } - let extractor = SearchParameterExtractor::new(provider.search_param_registry().clone()); + let tenant_registry = provider.search_param_registry(tenant); + let extractor = SearchParameterExtractor::new(tenant_registry.clone()); let key = |r: &StoredResource| format!("{}/{}", r.resource_type(), r.id()); // Don't re-include primary matches. @@ -443,8 +451,7 @@ where if res.resource_type() != directive.source_type { continue; } - let def = provider - .search_param_registry() + let def = tenant_registry .read() .get_param(res.resource_type(), &directive.search_param); let Some(def) = def else { continue }; diff --git a/crates/persistence/src/search/chain_resolver.rs b/crates/persistence/src/search/chain_resolver.rs index 1a5b965ab..ec293dac8 100644 --- a/crates/persistence/src/search/chain_resolver.rs +++ b/crates/persistence/src/search/chain_resolver.rs @@ -161,7 +161,8 @@ where // Per-link target types, resolved from the registry (single-target params) // with a heuristic fallback for ambiguous references. let target_types: Vec = { - let registry = storage.search_param_registry().read(); + let reg = storage.search_param_registry(tenant); + let registry = reg.read(); let mut types = Vec::with_capacity(parts.len() - 1); let mut current = base_type.to_string(); for ref_param in parts.iter().take(parts.len() - 1) { @@ -187,7 +188,8 @@ where let terminal_param = parts[parts.len() - 1]; let deepest_type = target_types.last().map(String::as_str).unwrap_or(base_type); let terminal_type = { - let registry = storage.search_param_registry().read(); + let reg = storage.search_param_registry(tenant); + let registry = reg.read(); resolve_param_type( ®istry, deepest_type, @@ -297,7 +299,8 @@ where None => vec![], }; let search_param_type = { - let registry = storage.search_param_registry().read(); + let reg = storage.search_param_registry(tenant); + let registry = reg.read(); resolve_param_type( ®istry, &reverse_chain.source_type, @@ -317,12 +320,13 @@ where let result = storage.search(tenant, &source_query).await?; - let extractor = SearchParameterExtractor::new(storage.search_param_registry().clone()); + let extractor = SearchParameterExtractor::new(storage.search_param_registry(tenant)); let mut ids = Vec::new(); for resource in result.resources.items { let refs = extract_references( &extractor, storage, + tenant, &resource, &reverse_chain.reference_param, ); @@ -342,6 +346,7 @@ where fn extract_references( extractor: &SearchParameterExtractor, storage: &S, + tenant: &TenantContext, resource: &crate::types::StoredResource, search_param: &str, ) -> Vec @@ -352,7 +357,8 @@ where let resource_type = resource.resource_type(); let registered = { - let registry = storage.search_param_registry().read(); + let reg = storage.search_param_registry(tenant); + let registry = reg.read(); registry .get_param(resource_type, search_param) .or_else(|| registry.get_param("Resource", search_param)) diff --git a/crates/persistence/src/search/mod.rs b/crates/persistence/src/search/mod.rs index fdd5635bc..46594f740 100644 --- a/crates/persistence/src/search/mod.rs +++ b/crates/persistence/src/search/mod.rs @@ -71,6 +71,7 @@ pub mod range; pub mod registry; pub mod reindex; pub mod seeder; +pub mod tenant_registries; pub mod text_fold; pub mod writer; @@ -90,6 +91,12 @@ pub use reindex::{ ReindexOperation, ReindexProgress, ReindexRequest, ReindexSource, ReindexStatus, ReindexTarget, ReindexableStorage, ResourcePage, }; -pub use seeder::{SeedOutcome, seed_spec_search_parameters}; +pub use seeder::{ + SeedOutcome, seed_spec_compartment_definitions, seed_spec_search_parameters, + seed_tenant_conformance, +}; +pub use tenant_registries::{ + StoredParamLoader, TenantSearchRegistries, search_parameter_create_affects_overlay, +}; pub use text_fold::fold_text; pub use writer::SearchIndexWriter; diff --git a/crates/persistence/src/search/reindex.rs b/crates/persistence/src/search/reindex.rs index 3d2e8e198..53eda6148 100644 --- a/crates/persistence/src/search/reindex.rs +++ b/crates/persistence/src/search/reindex.rs @@ -405,8 +405,9 @@ pub struct ReindexOperation { source: Arc, /// Every search index that must be rebuilt. More than one on a composite. writers: Vec>, - /// The search parameter extractor. - extractor: Arc, + /// The per-tenant search parameter registries; the extractor is built from + /// the reindexed tenant's registry so a tenant's stored params are honored. + registries: Arc, /// Active jobs. jobs: Arc>>, /// Cancellation channels. @@ -421,9 +422,9 @@ impl ReindexOperation { /// the search index live in the same place. pub fn new( storage: Arc, - extractor: Arc, + registries: Arc, ) -> Self { - Self::with_parts(storage.clone(), vec![storage], extractor) + Self::with_parts(storage.clone(), vec![storage], registries) } /// Creates a reindex manager that reads from `source` and rebuilds every @@ -437,12 +438,12 @@ impl ReindexOperation { pub fn with_parts( source: Arc, writers: Vec>, - extractor: Arc, + registries: Arc, ) -> Self { Self { source, writers, - extractor, + registries, jobs: Arc::new(RwLock::new(HashMap::new())), cancel_channels: Arc::new(RwLock::new(HashMap::new())), audit: None, @@ -516,7 +517,7 @@ impl ReindexOperation { // Clone references for the background task let source = self.source.clone(); let writers = self.writers.clone(); - let extractor = self.extractor.clone(); + let registries = self.registries.clone(); let jobs = self.jobs.clone(); let audit = self.audit.clone(); let job_id_clone = job_id.clone(); @@ -529,7 +530,7 @@ impl ReindexOperation { request, source, writers, - extractor, + registries, jobs.clone(), cancel_rx, ) @@ -695,10 +696,13 @@ async fn run_reindex( request: ReindexRequest, source: Arc, writers: Vec>, - extractor: Arc, + registries: Arc, jobs: Arc>>, mut cancel_rx: mpsc::Receiver<()>, ) { + // Extract with the reindexed tenant's registry (base + that tenant's overlay). + let extractor = + SearchParameterExtractor::new(registries.for_tenant(tenant.tenant_id().as_str())); // Mark as started { let mut jobs_guard = jobs.write(); diff --git a/crates/persistence/src/search/seeder.rs b/crates/persistence/src/search/seeder.rs index 015e1201e..8144302ec 100644 --- a/crates/persistence/src/search/seeder.rs +++ b/crates/persistence/src/search/seeder.rs @@ -141,3 +141,124 @@ where ); Ok(outcome) } + +/// Seeds one tenant with both spec conformance resource sets — SearchParameters +/// (#235) and CompartmentDefinitions (#237/#238) — from `data_dir`. Best-effort +/// per set: an error in one is logged and does not block the other. Returns the +/// combined outcome. Idempotent; safe to call at startup and on tenant +/// provisioning. +pub async fn seed_tenant_conformance( + storage: &S, + fhir_version: FhirVersion, + data_dir: &Path, + tenant_id: &str, +) -> SeedOutcome +where + S: ResourceStorage + ?Sized, +{ + let mut total = SeedOutcome { + created: 0, + existing: 0, + failed: 0, + }; + let mut add = |r: StorageResult, kind: &str| match r { + Ok(o) => { + total.created += o.created; + total.existing += o.existing; + total.failed += o.failed; + } + Err(e) => tracing::warn!(tenant = %tenant_id, "{kind} seeding failed: {e}"), + }; + add( + seed_spec_search_parameters(storage, fhir_version, data_dir, tenant_id).await, + "SearchParameter", + ); + add( + seed_spec_compartment_definitions(storage, fhir_version, data_dir, tenant_id).await, + "CompartmentDefinition", + ); + total +} + +/// Seeds `storage` with the spec CompartmentDefinition bundle for +/// `fhir_version`, under `tenant_id`. +/// +/// The compartment analogue of [`seed_spec_search_parameters`]: storage — not +/// the codegen'd membership table — is what `GET /CompartmentDefinition` and +/// the web UI read. Compartment *search* membership is unaffected (it still +/// resolves through `helios_fhir::get_compartment_params`); these stored copies +/// exist only for API discovery. +/// +/// Same idempotency contract: every definition carries its bundle id +/// (`patient`, `encounter`, …), so a concurrent second writer's `create` fails +/// `AlreadyExists` and is treated as already-seeded; existing resources are +/// never clobbered. When the tenant already holds at least the bundle's count, +/// the pass short-circuits to a single `count`. +pub async fn seed_spec_compartment_definitions( + storage: &S, + fhir_version: FhirVersion, + data_dir: &Path, + tenant_id: &str, +) -> StorageResult +where + S: ResourceStorage + ?Sized, +{ + let tenant = TenantContext::new(TenantId::new(tenant_id), TenantPermissions::full_access()); + let loader = helios_fhir::compartment::CompartmentDefinitionLoader::new(fhir_version); + + let resources: Vec = match loader.load_spec_resources(data_dir) { + Ok(resources) => resources, + Err(e) => { + // No bundle is a supported minimal deployment; nothing to seed. + tracing::warn!("CompartmentDefinition seeding: no spec bundle loaded: {e}"); + Vec::new() + } + }; + if resources.is_empty() { + return Ok(SeedOutcome { + created: 0, + existing: 0, + failed: 0, + }); + } + + let present = storage + .count(&tenant, Some("CompartmentDefinition")) + .await?; + if present as usize >= resources.len() { + return Ok(SeedOutcome { + created: 0, + existing: resources.len(), + failed: 0, + }); + } + + let mut outcome = SeedOutcome { + created: 0, + existing: 0, + failed: 0, + }; + for resource in resources { + match storage + .create(&tenant, "CompartmentDefinition", resource, fhir_version) + .await + { + Ok(_) => outcome.created += 1, + Err(StorageError::Resource(ResourceError::AlreadyExists { .. })) => { + outcome.existing += 1; + } + Err(e) => { + outcome.failed += 1; + tracing::warn!("CompartmentDefinition seeding: create failed: {e}"); + } + } + } + tracing::info!( + created = outcome.created, + existing = outcome.existing, + failed = outcome.failed, + tenant = %tenant_id, + "Seeded spec CompartmentDefinitions into storage" + ); + Ok(outcome) +} diff --git a/crates/persistence/src/search/tenant_registries.rs b/crates/persistence/src/search/tenant_registries.rs new file mode 100644 index 000000000..3b41aea8e --- /dev/null +++ b/crates/persistence/src/search/tenant_registries.rs @@ -0,0 +1,263 @@ +//! Per-tenant SearchParameter registries. +//! +//! Search-parameter resolution is tenant-scoped: `acme1` may POST a custom +//! `SearchParameter` (or override a standard one via `(base, code)` shadowing, +//! see #239/#242) that `acme2` must not see, and the FHIR search API must +//! behave differently per tenant id. +//! +//! The model is **shared base + per-tenant overlay**: +//! - a single **base** [`SearchParameterRegistry`] holds the params that are +//! identical for every tenant — the embedded fallbacks, the spec bundle, and +//! any custom-directory params — loaded once at backend construction; +//! - each tenant's registry is `base.clone()` plus that tenant's **stored** +//! (POSTed) active params, registered on top so they shadow the base by +//! precedence. Cloning the base is cheap (both indexes hold +//! `Arc`). +//! +//! Per-tenant registries are built lazily on first use and cached for the +//! process lifetime; a tenant's registry is dropped (rebuilt on next access) +//! when that tenant writes a `SearchParameter` or the TTL refresh fires. The +//! **loader** closure — supplied by the primary backend — reads a tenant's +//! stored params from storage; it captures the connection pool, not the +//! backend, so an Elasticsearch backend that *shares* this container resolves +//! per-tenant params through the primary's storage without its own DB handle. + +use std::collections::HashMap; +use std::sync::Arc; + +use parking_lot::RwLock; + +use crate::search::{SearchParameterDefinition, SearchParameterRegistry}; + +/// Loads a tenant's stored (POSTed) active SearchParameter definitions from +/// storage. Returns an empty vec when the tenant has none (or the backend has +/// no store, e.g. S3). +pub type StoredParamLoader = Arc Vec + Send + Sync>; + +/// Whether creating this `SearchParameter` resource can change a tenant's +/// overlay. Only `status: active` params are registered into per-tenant +/// registries, so a create of any other status (notably the `draft` spec copies +/// seeded into every tenant) need not invalidate the cache — which keeps bulk +/// seeding from triggering an O(n²) rebuild storm. Update/delete are rare and +/// invalidate unconditionally. +pub fn search_parameter_create_affects_overlay(resource: &serde_json::Value) -> bool { + resource.get("status").and_then(|s| s.as_str()) == Some("active") +} + +/// A shared base registry plus lazily-built, cached per-tenant registries. +/// +/// Backends hold `Arc` in place of the former single +/// `Arc>`. [`for_tenant`](Self::for_tenant) is +/// the per-tenant analogue of what `SearchProvider::search_param_registry` +/// returns. +pub struct TenantSearchRegistries { + /// Params identical for every tenant (embedded + spec + custom). Populated + /// at construction via [`base`](Self::base); read-mostly thereafter. + base: Arc>, + /// Per-tenant registries (`base` clone + tenant stored overlay), keyed by + /// tenant id. Built on first access, dropped on invalidation. + per_tenant: RwLock>>>, + /// Reads a tenant's stored active params from storage. + loader: StoredParamLoader, +} + +impl TenantSearchRegistries { + /// Creates a container over an empty base and the given stored-param loader. + /// Callers populate the base via [`base`](Self::base) immediately after. + pub fn new(loader: StoredParamLoader) -> Self { + Self { + base: Arc::new(RwLock::new(SearchParameterRegistry::new())), + per_tenant: RwLock::new(HashMap::new()), + loader, + } + } + + /// Creates a container whose tenants never have stored overlays — every + /// tenant sees exactly the base. Used by backends without a store (S3). + pub fn base_only() -> Self { + Self::new(Arc::new(|_tenant: &str| Vec::new())) + } + + /// The shared base registry. Construction-time loading (embedded/spec/custom) + /// registers into this; any change here invalidates the per-tenant cache so + /// the overlays pick it up. + pub fn base(&self) -> &Arc> { + &self.base + } + + /// Returns the registry for `tenant_id`, building and caching it on first + /// use: a clone of the base with the tenant's stored active params overlaid. + pub fn for_tenant(&self, tenant_id: &str) -> Arc> { + if let Some(reg) = self.per_tenant.read().get(tenant_id) { + return reg.clone(); + } + // Build outside the map lock. A concurrent builder for the same tenant + // is harmless — the last writer wins and both hold equivalent content. + let mut reg = self.base.read().clone(); + for def in (self.loader)(tenant_id) { + // A stored param may legitimately shadow a base spec param; ignore + // duplicate-url rejections (already-registered canonical URLs). + let _ = reg.register(def); + } + let arc = Arc::new(RwLock::new(reg)); + self.per_tenant + .write() + .insert(tenant_id.to_string(), arc.clone()); + arc + } + + /// Drops a tenant's cached registry so the next access rebuilds it from + /// storage. Called when that tenant writes a `SearchParameter`. + pub fn invalidate(&self, tenant_id: &str) { + self.per_tenant.write().remove(tenant_id); + } + + /// Drops every cached per-tenant registry (the TTL refresh: storage is the + /// source of truth, so each tenant's overlay is re-read on next access). + pub fn invalidate_all(&self) { + self.per_tenant.write().clear(); + } + + /// Number of currently-cached tenant registries (for diagnostics/tests). + pub fn cached_tenant_count(&self) -> usize { + self.per_tenant.read().len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::search::{SearchParameterSource, SearchParameterStatus}; + use crate::types::SearchParamType; + + fn def( + url: &str, + base: &str, + code: &str, + source: SearchParameterSource, + ) -> SearchParameterDefinition { + SearchParameterDefinition::new(url, code, SearchParamType::String, format!("{base}.{code}")) + .with_base([base]) + .with_source(source) + .with_status(SearchParameterStatus::Active) + } + + fn registries_with( + stored: HashMap>, + ) -> TenantSearchRegistries { + let stored = Arc::new(stored); + let regs = TenantSearchRegistries::new(Arc::new(move |t: &str| { + stored.get(t).cloned().unwrap_or_default() + })); + // Base: one standard param present for all tenants. + regs.base() + .write() + .register(def( + "http://hl7.org/fhir/SearchParameter/Patient-name", + "Patient", + "name", + SearchParameterSource::Embedded, + )) + .unwrap(); + regs + } + + #[test] + fn every_tenant_sees_the_shared_base() { + let regs = registries_with(HashMap::new()); + for t in ["acme1", "acme2", "default"] { + let reg = regs.for_tenant(t); + assert!( + reg.read().get_param("Patient", "name").is_some(), + "{t} missing base param" + ); + } + } + + #[test] + fn a_tenants_stored_param_is_isolated() { + let mut stored = HashMap::new(); + stored.insert( + "acme1".to_string(), + vec![def( + "http://acme.health/fhir/SearchParameter/patient-nickname", + "Patient", + "nickname", + SearchParameterSource::Stored, + )], + ); + let regs = registries_with(stored); + + assert!( + regs.for_tenant("acme1") + .read() + .get_param("Patient", "nickname") + .is_some() + ); + assert!( + regs.for_tenant("acme2") + .read() + .get_param("Patient", "nickname") + .is_none() + ); + // Both still have the shared base param. + assert!( + regs.for_tenant("acme2") + .read() + .get_param("Patient", "name") + .is_some() + ); + } + + #[test] + fn invalidate_rebuilds_from_loader() { + // A mutable-ish loader via Arc> to simulate a storage change. + let store: Arc>>> = + Arc::new(RwLock::new(HashMap::new())); + let store2 = store.clone(); + let regs = TenantSearchRegistries::new(Arc::new(move |t: &str| { + store2.read().get(t).cloned().unwrap_or_default() + })); + regs.base() + .write() + .register(def( + "http://hl7.org/fhir/SearchParameter/Patient-name", + "Patient", + "name", + SearchParameterSource::Embedded, + )) + .unwrap(); + + assert!( + regs.for_tenant("acme1") + .read() + .get_param("Patient", "nickname") + .is_none() + ); + // Storage gains a param for acme1; visible only after invalidation. + store.write().insert( + "acme1".to_string(), + vec![def( + "http://acme.health/fhir/SearchParameter/patient-nickname", + "Patient", + "nickname", + SearchParameterSource::Stored, + )], + ); + assert!( + regs.for_tenant("acme1") + .read() + .get_param("Patient", "nickname") + .is_none(), + "cached" + ); + regs.invalidate("acme1"); + assert!( + regs.for_tenant("acme1") + .read() + .get_param("Patient", "nickname") + .is_some(), + "rebuilt" + ); + } +} diff --git a/crates/persistence/tests/compartment_definition_seeding.rs b/crates/persistence/tests/compartment_definition_seeding.rs new file mode 100644 index 000000000..b52db988d --- /dev/null +++ b/crates/persistence/tests/compartment_definition_seeding.rs @@ -0,0 +1,162 @@ +//! Integration tests for storage-backed CompartmentDefinitions (#237/#238): +//! seeding the store from the spec bundle so `GET /CompartmentDefinition` +//! discovers the definitions the server resolves compartment membership from. + +#![cfg(feature = "sqlite")] + +use std::path::PathBuf; + +use helios_fhir::FhirVersion; +use helios_persistence::backends::sqlite::{SqliteBackend, SqliteBackendConfig}; +use helios_persistence::core::ResourceStorage; +use helios_persistence::search::{seed_spec_compartment_definitions, seed_tenant_conformance}; +use helios_persistence::tenant::{TenantContext, TenantId, TenantPermissions}; +use serde_json::json; + +/// The workspace data directory holding `compartment-definitions-r4.json`. +fn workspace_data_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../data") +} + +fn create_backend() -> SqliteBackend { + let backend = SqliteBackend::with_config( + ":memory:", + SqliteBackendConfig { + data_dir: Some(workspace_data_dir()), + ..Default::default() + }, + ) + .expect("create in-memory SQLite backend"); + backend.init_schema().expect("init schema"); + backend +} + +fn tenant(id: &str) -> TenantContext { + TenantContext::new(TenantId::new(id), TenantPermissions::full_access()) +} + +#[tokio::test] +async fn seeding_is_idempotent_and_discoverable() { + let backend = create_backend(); + let data_dir = workspace_data_dir(); + + let first = seed_spec_compartment_definitions(&backend, FhirVersion::R4, &data_dir, "default") + .await + .expect("first seed"); + // R4 ships 5 compartments (Device, Encounter, Patient, Practitioner, + // RelatedPerson); questionnaire is excluded. + assert_eq!(first.created, 5); + assert_eq!(first.failed, 0); + + // Discoverable via a storage read (what GET /CompartmentDefinition executes). + let stored = backend + .count(&tenant("default"), Some("CompartmentDefinition")) + .await + .expect("count seeded"); + assert_eq!(stored, 5); + + let patient = backend + .read(&tenant("default"), "CompartmentDefinition", "patient") + .await + .expect("read patient compartment") + .expect("patient compartment present"); + assert_eq!(patient.content()["code"], json!("Patient")); + + // A second boot takes the fast path and writes nothing. + let second = seed_spec_compartment_definitions(&backend, FhirVersion::R4, &data_dir, "default") + .await + .expect("second seed"); + assert_eq!(second.created, 0); + assert_eq!(second.failed, 0); + + // Other tenants are unaffected by a single-tenant seed. + let other = backend + .count(&tenant("acme"), Some("CompartmentDefinition")) + .await + .expect("count other tenant"); + assert_eq!(other, 0); +} + +/// The combined helper seeds both SearchParameters and CompartmentDefinitions +/// for a tenant in one call. +#[tokio::test] +async fn seed_tenant_conformance_seeds_both_sets() { + let backend = create_backend(); + let data_dir = workspace_data_dir(); + + let outcome = seed_tenant_conformance(&backend, FhirVersion::R4, &data_dir, "acme").await; + assert_eq!(outcome.failed, 0); + + let sp = backend + .count(&tenant("acme"), Some("SearchParameter")) + .await + .expect("count SearchParameter"); + let cd = backend + .count(&tenant("acme"), Some("CompartmentDefinition")) + .await + .expect("count CompartmentDefinition"); + assert!(sp > 1300, "expected the R4 search-param set, got {sp}"); + assert_eq!(cd, 5); +} + +/// A CompartmentDefinition already stored under a spec bundle id is reported +/// `existing`, not clobbered. +#[tokio::test] +async fn seeding_reports_existing_and_never_clobbers() { + let backend = create_backend(); + let data_dir = workspace_data_dir(); + + let preexisting = json!({ + "resourceType": "CompartmentDefinition", + "id": "patient", + "url": "http://acme.health/fhir/CompartmentDefinition/custom-patient", + "status": "active", + "code": "Patient", + "search": true, + "resource": [] + }); + backend + .create( + &tenant("default"), + "CompartmentDefinition", + preexisting, + FhirVersion::R4, + ) + .await + .expect("pre-create under a spec id"); + + let outcome = + seed_spec_compartment_definitions(&backend, FhirVersion::R4, &data_dir, "default") + .await + .expect("seed over a present spec id"); + assert!( + outcome.existing >= 1, + "the pre-existing spec id should be reported existing, got {outcome:?}" + ); + assert_eq!(outcome.failed, 0); + + // The pre-existing resource was left untouched (create never clobbers). + let read = backend + .read(&tenant("default"), "CompartmentDefinition", "patient") + .await + .expect("read pre-existing") + .expect("still present"); + assert_eq!( + read.content()["url"], + json!("http://acme.health/fhir/CompartmentDefinition/custom-patient") + ); +} + +/// With no bundle in the data directory, seeding is a no-op (not an error). +#[tokio::test] +async fn seeding_with_missing_bundle_is_a_noop() { + let backend = create_backend(); + let empty_dir = tempfile::tempdir().expect("temp data dir"); + + let outcome = + seed_spec_compartment_definitions(&backend, FhirVersion::R4, empty_dir.path(), "default") + .await + .expect("seed with no bundle"); + assert_eq!(outcome.created, 0); + assert_eq!(outcome.failed, 0); +} diff --git a/crates/persistence/tests/elasticsearch_tests.rs b/crates/persistence/tests/elasticsearch_tests.rs index c40fd50df..a1c2dd265 100644 --- a/crates/persistence/tests/elasticsearch_tests.rs +++ b/crates/persistence/tests/elasticsearch_tests.rs @@ -576,15 +576,14 @@ mod es_integration { use std::sync::Arc; use helios_fhir::FhirVersion; - use parking_lot::RwLock; use serde_json::json; use helios_persistence::backends::elasticsearch::{ElasticsearchBackend, ElasticsearchConfig}; use helios_persistence::core::{Backend, BackendCapability, BackendKind, ResourceStorage}; use helios_persistence::error::{ResourceError, StorageError}; use helios_persistence::search::{ - SearchParameterDefinition, SearchParameterLoader, SearchParameterRegistry, - SearchParameterSource, SearchParameterStatus, + SearchParameterDefinition, SearchParameterLoader, SearchParameterSource, + SearchParameterStatus, TenantSearchRegistries, }; use helios_persistence::tenant::{TenantContext, TenantId, TenantPermissions}; @@ -636,7 +635,7 @@ mod es_integration { } /// Builds a search parameter registry loaded from the FHIR spec data files. - fn build_search_registry() -> Arc> { + fn build_search_registry() -> Arc { let data_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() .and_then(|p| p.parent()) @@ -644,7 +643,8 @@ mod es_integration { .unwrap_or_else(|| PathBuf::from("data")); let loader = SearchParameterLoader::new(FhirVersion::default()); - let mut registry = SearchParameterRegistry::new(); + let registries = Arc::new(TenantSearchRegistries::base_only()); + let mut registry = registries.base().write(); // Load embedded (minimal) params first if let Ok(params) = loader.load_embedded() { @@ -683,7 +683,8 @@ mod es_integration { xpath: None, }); - Arc::new(RwLock::new(registry)) + drop(registry); + registries } /// Creates an ElasticsearchBackend connected to the shared testcontainers ES instance. diff --git a/crates/persistence/tests/mongodb_tests.rs b/crates/persistence/tests/mongodb_tests.rs index 22225cbee..c163df41a 100644 --- a/crates/persistence/tests/mongodb_tests.rs +++ b/crates/persistence/tests/mongodb_tests.rs @@ -2440,7 +2440,8 @@ async fn mongodb_integration_search_parameter_create_registers_active() { .await .unwrap(); - let registry = backend.search_registry().read(); + let reg = backend.search_param_registry(&tenant); + let registry = reg.read(); let param = registry.get_param("Patient", "mongo-nickname"); assert!( param.is_some(), @@ -2461,8 +2462,6 @@ async fn mongodb_integration_search_parameter_create_registers_active() { /// the SQLite integration test's contract exercised over the Mongo cursor path. #[tokio::test] async fn mongodb_integration_refresh_rebuilds_stored_parameters() { - use helios_persistence::search::registry::SearchParameterSource; - let Some(backend) = create_backend("search_param_refresh").await else { eprintln!( "Skipping mongodb_integration_refresh_rebuilds_stored_parameters (requires Docker or HFS_TEST_MONGODB_URL)" @@ -2471,6 +2470,7 @@ async fn mongodb_integration_refresh_rebuilds_stored_parameters() { }; let tenant = create_tenant("tenant-search-param-refresh"); + let other = create_tenant("tenant-search-param-other"); backend .create( @@ -2492,52 +2492,50 @@ async fn mongodb_integration_refresh_rebuilds_stored_parameters() { .await .unwrap(); - // Simulate a freshly booted cluster-mate: storage holds the parameter but - // its registry has never seen it (drop the write-hook registration). - backend - .search_registry() - .write() - .unregister_source(SearchParameterSource::Stored); - assert!( - backend - .search_registry() - .read() - .get_param("Patient", "mongo-refresh-nickname") - .is_none(), - "not visible before the refresh" - ); + // The write refreshes the stored-param cache: the parameter resolves for its + // tenant and is isolated from others. + { + let reg = backend.search_param_registry(&tenant); + assert!( + reg.read() + .get_param("Patient", "mongo-refresh-nickname") + .is_some(), + "visible to the owning tenant after the write" + ); + } + { + let reg = backend.search_param_registry(&other); + assert!( + reg.read() + .get_param("Patient", "mongo-refresh-nickname") + .is_none(), + "isolated from other tenants" + ); + } - let stored = backend + // The TTL refresh drops the cached per-tenant registries; the next access + // re-reads storage (how a cluster-mate's write becomes visible). + let _ = backend + .search_param_registry(&tenant) + .read() + .get_param("Patient", "mongo-refresh-nickname"); + backend .refresh_stored_search_parameters() .await .expect("refresh from storage"); - assert_eq!(stored, 1); - assert!( - backend - .search_registry() - .read() - .get_param("Patient", "mongo-refresh-nickname") - .is_some(), - "visible after the refresh" - ); + assert_eq!(backend.tenant_registries().cached_tenant_count(), 0); - // Delete it from storage; the next refresh drops it from resolution. + // Delete it from storage; it leaves the owning tenant's resolution. backend .delete(&tenant, "SearchParameter", "mongo-refresh-nickname") .await .unwrap(); - let stored = backend - .refresh_stored_search_parameters() - .await - .expect("refresh after delete"); - assert_eq!(stored, 0); + let reg = backend.search_param_registry(&tenant); assert!( - backend - .search_registry() - .read() + reg.read() .get_param("Patient", "mongo-refresh-nickname") .is_none(), - "gone after the refresh" + "gone after the delete" ); } @@ -2572,7 +2570,8 @@ async fn mongodb_integration_search_parameter_create_draft_not_registered() { .await .unwrap(); - let registry = backend.search_registry().read(); + let reg = backend.search_param_registry(&tenant); + let registry = reg.read(); let param = registry.get_param("Patient", "mongo-draft"); assert!( param.is_none(), @@ -2612,7 +2611,8 @@ async fn mongodb_integration_search_parameter_update_status_change() { .unwrap(); { - let registry = backend.search_registry().read(); + let reg = backend.search_param_registry(&tenant); + let registry = reg.read(); let param = registry.get_param("Condition", "mongo-statuschange"); assert!( param.is_some(), @@ -2644,13 +2644,15 @@ async fn mongodb_integration_search_parameter_update_status_change() { .await .unwrap(); - let registry = backend.search_registry().read(); - let param = registry.get_param("Condition", "mongo-statuschange"); - assert!(param.is_some(), "Parameter should still exist in registry"); - assert_eq!( - param.unwrap().status, - SearchParameterStatus::Retired, - "Status should be updated to retired" + // A per-tenant registry overlays only a tenant's *active* stored params, so + // retiring the parameter removes it from that tenant's search resolution. + let reg = backend.search_param_registry(&tenant); + let registry = reg.read(); + assert!( + registry + .get_param("Condition", "mongo-statuschange") + .is_none(), + "a retired custom parameter should no longer resolve for the tenant" ); } @@ -2686,7 +2688,8 @@ async fn mongodb_integration_search_parameter_delete_unregisters() { .unwrap(); { - let registry = backend.search_registry().read(); + let reg = backend.search_param_registry(&tenant); + let registry = reg.read(); assert!( registry .get_param("Observation", "mongo-todelete") @@ -2699,7 +2702,8 @@ async fn mongodb_integration_search_parameter_delete_unregisters() { .await .unwrap(); - let registry = backend.search_registry().read(); + let reg = backend.search_param_registry(&tenant); + let registry = reg.read(); assert!( registry .get_param("Observation", "mongo-todelete") @@ -2843,7 +2847,8 @@ async fn mongodb_integration_search_parameter_registry_updates_when_offloaded() .unwrap(); { - let registry = backend.search_registry().read(); + let reg = backend.search_param_registry(&tenant); + let registry = reg.read(); let param = registry.get_param("Patient", "mongo-offloaded-code"); assert!( param.is_some(), @@ -2864,7 +2869,8 @@ async fn mongodb_integration_search_parameter_registry_updates_when_offloaded() .await .unwrap(); - let registry = backend.search_registry().read(); + let reg = backend.search_param_registry(&tenant); + let registry = reg.read(); assert!( registry .get_param("Patient", "mongo-offloaded-code") diff --git a/crates/persistence/tests/s3_es_tests.rs b/crates/persistence/tests/s3_es_tests.rs index d2dd6e6a1..d7d2cba27 100644 --- a/crates/persistence/tests/s3_es_tests.rs +++ b/crates/persistence/tests/s3_es_tests.rs @@ -17,7 +17,6 @@ use std::path::PathBuf; use std::sync::Arc; use helios_fhir::FhirVersion; -use parking_lot::RwLock; use serde_json::json; use helios_persistence::backends::elasticsearch::{ElasticsearchBackend, ElasticsearchConfig}; @@ -28,7 +27,7 @@ use helios_persistence::composite::{ use helios_persistence::core::search::SearchProvider; use helios_persistence::core::{Backend, BackendKind, ResourceStorage}; use helios_persistence::error::{ResourceError, StorageError}; -use helios_persistence::search::{SearchParameterLoader, SearchParameterRegistry}; +use helios_persistence::search::{SearchParameterLoader, TenantSearchRegistries}; use helios_persistence::tenant::{TenantContext, TenantId, TenantPermissions}; use helios_persistence::types::{SearchParamType, SearchParameter, SearchQuery, SearchValue}; @@ -198,7 +197,7 @@ async fn ensure_bucket(client: &Client, bucket: &str) { .expect("failed to create test bucket"); } -fn build_search_registry() -> Arc> { +fn build_search_registry() -> Arc { let data_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() .and_then(|p| p.parent()) @@ -206,20 +205,21 @@ fn build_search_registry() -> Arc> { .unwrap_or_else(|| PathBuf::from("data")); let loader = SearchParameterLoader::new(FhirVersion::default()); - let mut registry = SearchParameterRegistry::new(); - - if let Ok(params) = loader.load_embedded() { - for p in params { - let _ = registry.register(p); + let registries = Arc::new(TenantSearchRegistries::base_only()); + { + let mut registry = registries.base().write(); + if let Ok(params) = loader.load_embedded() { + for p in params { + let _ = registry.register(p); + } } - } - if let Ok(params) = loader.load_from_spec_file(&data_dir) { - for p in params { - let _ = registry.register(p); + if let Ok(params) = loader.load_from_spec_file(&data_dir) { + for p in params { + let _ = registry.register(p); + } } } - - Arc::new(RwLock::new(registry)) + registries } // ============================================================================ diff --git a/crates/persistence/tests/search_param_seeding.rs b/crates/persistence/tests/search_param_seeding.rs index c765ce9ce..ca6436671 100644 --- a/crates/persistence/tests/search_param_seeding.rs +++ b/crates/persistence/tests/search_param_seeding.rs @@ -8,7 +8,7 @@ use std::path::PathBuf; use helios_fhir::FhirVersion; use helios_persistence::backends::sqlite::{SqliteBackend, SqliteBackendConfig}; -use helios_persistence::core::ResourceStorage; +use helios_persistence::core::{ResourceStorage, SearchProvider}; use helios_persistence::search::seed_spec_search_parameters; use helios_persistence::tenant::{TenantContext, TenantId, TenantPermissions}; use serde_json::json; @@ -189,15 +189,12 @@ async fn seeding_with_missing_spec_bundle_seeds_only_fallbacks() { assert_eq!(outcome.failed, 0); } -/// The TTL-cache contract: a SearchParameter present in storage (written by -/// this node or a cluster-mate) enters search resolution on a refresh, and a -/// deleted one leaves it. +/// Per-tenant registries: a tenant's stored SearchParameter enters that +/// tenant's search resolution (write-hook invalidation + lazy rebuild), stays +/// isolated from other tenants, and leaves on delete. #[tokio::test] -async fn refresh_rebuilds_stored_parameters_from_storage() { - use helios_persistence::search::registry::SearchParameterSource; - +async fn stored_parameters_are_per_tenant() { let backend = create_backend(); - let registry = backend.search_registry(); let nickname = json!({ "resourceType": "SearchParameter", @@ -210,44 +207,78 @@ async fn refresh_rebuilds_stored_parameters_from_storage() { "type": "string", "expression": "Patient.name.where(use='nickname').given" }); + + // Before it exists, no tenant resolves it — but every tenant has the base. + let acme1 = tenant("acme1"); + let acme2 = tenant("acme2"); + assert!( + backend + .search_param_registry(&acme1) + .read() + .get_param("Patient", "nickname") + .is_none() + ); + assert!( + backend + .search_param_registry(&acme1) + .read() + .get_param("Patient", "name") + .is_some(), + "shared base param present" + ); + + // POST it under acme1 only. backend - .create( - &tenant("default"), - "SearchParameter", - nickname, - FhirVersion::R4, - ) + .create(&acme1, "SearchParameter", nickname, FhirVersion::R4) .await .expect("store the parameter"); - // Simulate a freshly booted cluster-mate: its storage holds the parameter - // but its registry has never seen it (drop the write-hook registration). - registry - .write() - .unregister_source(SearchParameterSource::Stored); + // Visible to acme1, isolated from acme2. assert!( - registry.read().get_param("Patient", "nickname").is_none(), - "not visible before the refresh" + backend + .search_param_registry(&acme1) + .read() + .get_param("Patient", "nickname") + .is_some(), + "visible to acme1 after the write" ); - - let stored = backend.refresh_stored_search_parameters().expect("refresh"); - assert_eq!(stored, 1); assert!( - registry.read().get_param("Patient", "nickname").is_some(), - "visible after the refresh" + backend + .search_param_registry(&acme2) + .read() + .get_param("Patient", "nickname") + .is_none(), + "isolated from acme2" ); - // Delete it from storage; the next refresh drops it from resolution. + // Delete removes it from acme1's resolution. backend - .delete(&tenant("default"), "SearchParameter", "acme-nickname") + .delete(&acme1, "SearchParameter", "acme-nickname") .await .expect("delete the parameter"); - let stored = backend - .refresh_stored_search_parameters() - .expect("refresh after delete"); - assert_eq!(stored, 0); assert!( - registry.read().get_param("Patient", "nickname").is_none(), - "gone after the refresh" + backend + .search_param_registry(&acme1) + .read() + .get_param("Patient", "nickname") + .is_none(), + "gone from acme1 after delete" ); } + +/// The TTL-cache contract: `refresh_stored_search_parameters` drops the cached +/// per-tenant registries so the next access re-reads storage (how a +/// cluster-mate's write becomes visible). +#[tokio::test] +async fn refresh_invalidates_cached_tenant_registries() { + let backend = create_backend(); + + // Warm two tenants' caches. + let _ = backend.search_param_registry(&tenant("acme1")); + let _ = backend.search_param_registry(&tenant("acme2")); + assert_eq!(backend.tenant_registries().cached_tenant_count(), 2); + + let cleared = backend.refresh_stored_search_parameters().expect("refresh"); + assert_eq!(cleared, 2, "refresh reports the tenants it invalidated"); + assert_eq!(backend.tenant_registries().cached_tenant_count(), 0); +} diff --git a/crates/persistence/tests/sqlite_tests.rs b/crates/persistence/tests/sqlite_tests.rs index e75d93501..04efe6b23 100644 --- a/crates/persistence/tests/sqlite_tests.rs +++ b/crates/persistence/tests/sqlite_tests.rs @@ -2326,7 +2326,7 @@ async fn test_reindex_operation_full() { ); // Create reindex operation - let reindex = ReindexOperation::new(backend.clone(), backend.search_extractor().clone()); + let reindex = ReindexOperation::new(backend.clone(), backend.tenant_registries().clone()); // Start reindex let job_id = reindex @@ -2402,7 +2402,7 @@ async fn test_reindex_operation_cancel() { backend.clear_search_index(&tenant).await.unwrap(); // Create reindex operation with small batch size to make it slower - let reindex = ReindexOperation::new(backend.clone(), backend.search_extractor().clone()); + let reindex = ReindexOperation::new(backend.clone(), backend.tenant_registries().clone()); // Start reindex let request = ReindexRequest::all().with_batch_size(5); @@ -2492,7 +2492,7 @@ async fn test_reindex_fans_out_to_every_target() { let reindex = ReindexOperation::with_parts( backend.clone(), vec![backend.clone(), secondary.clone()], - backend.search_extractor().clone(), + backend.tenant_registries().clone(), ); let request = ReindexRequest::for_types(vec!["Patient"]).clear_existing(); @@ -2897,7 +2897,8 @@ async fn test_search_parameter_create_registers_in_registry() { .unwrap(); // Verify the parameter is registered - let registry = backend.search_registry().read(); + let reg = backend.search_param_registry(&tenant); + let registry = reg.read(); let param = registry.get_param("Patient", "nickname"); assert!( param.is_some(), @@ -2941,7 +2942,8 @@ async fn test_search_parameter_create_draft_not_registered() { .unwrap(); // Verify the parameter is NOT registered (draft status) - let registry = backend.search_registry().read(); + let reg = backend.search_param_registry(&tenant); + let registry = reg.read(); let param = registry.get_param("Patient", "draft"); assert!( param.is_none(), @@ -2979,7 +2981,8 @@ async fn test_search_parameter_delete_unregisters() { // Verify it's registered { - let registry = backend.search_registry().read(); + let reg = backend.search_param_registry(&tenant); + let registry = reg.read(); assert!(registry.get_param("Observation", "todelete").is_some()); } @@ -2990,7 +2993,8 @@ async fn test_search_parameter_delete_unregisters() { .unwrap(); // Verify it's unregistered - let registry = backend.search_registry().read(); + let reg = backend.search_param_registry(&tenant); + let registry = reg.read(); assert!( registry.get_param("Observation", "todelete").is_none(), "Deleted SearchParameter should be unregistered" @@ -3027,7 +3031,8 @@ async fn test_search_parameter_update_status_change() { // Verify it's registered and active { - let registry = backend.search_registry().read(); + let reg = backend.search_param_registry(&tenant); + let registry = reg.read(); let param = registry.get_param("Condition", "statuschange"); assert!(param.is_some()); assert_eq!( @@ -3054,14 +3059,13 @@ async fn test_search_parameter_update_status_change() { .await .unwrap(); - // Verify status is updated in registry - let registry = backend.search_registry().read(); - let param = registry.get_param("Condition", "statuschange"); - assert!(param.is_some(), "Parameter should still exist in registry"); - assert_eq!( - param.unwrap().status, - helios_persistence::search::SearchParameterStatus::Retired, - "Status should be updated to retired" + // A per-tenant registry overlays only a tenant's *active* stored params, so + // retiring the parameter removes it from that tenant's search resolution. + let reg = backend.search_param_registry(&tenant); + let registry = reg.read(); + assert!( + registry.get_param("Condition", "statuschange").is_none(), + "a retired custom parameter should no longer resolve for the tenant" ); } diff --git a/crates/rest/src/config.rs b/crates/rest/src/config.rs index 8e4e94e6f..b25b945a5 100644 --- a/crates/rest/src/config.rs +++ b/crates/rest/src/config.rs @@ -21,6 +21,7 @@ //! | `HFS_DEFAULT_FHIR_VERSION` | R4 | Default FHIR version (R4, R4B, R5, R6) | //! | `HFS_TENANT_ROUTING_MODE` | header_only | Tenant routing mode (header_only, url_path, both) | //! | `HFS_TENANT_STRICT_VALIDATION` | false | Error if URL and header tenant disagree | +//! | `HFS_TENANT_REQUIRE_PROVISIONED` | false | Reject tenants not provisioned via the admin API (registry backends only) | //! | `HFS_JWT_TENANT_CLAIM` | tenant_id | JWT claim name for tenant (future use) | //! | `HFS_TERMINOLOGY_SERVER` | (none) | HTS base URL for `:in`/`:not-in` search and FHIRPath terminology functions | //! @@ -202,6 +203,15 @@ pub struct MultitenancyConfig { pub strict_validation: bool, /// JWT claim name containing tenant ID (for future JWT-based tenant resolution). pub jwt_tenant_claim: String, + /// If true, only tenants provisioned through the admin API are valid: a + /// request resolving to a tenant absent from the registry is rejected rather + /// than implicitly created. The default (`"default"`) tenant is + /// auto-provisioned at startup, so single-tenant/unauthenticated deployments + /// keep working. Only enforced on backends that maintain a tenant registry. + /// + /// Defaults to `false` for backward compatibility (ad-hoc tenant ids keep + /// working); set `HFS_TENANT_REQUIRE_PROVISIONED=true` for the strict model. + pub require_provisioned_tenant: bool, } impl Default for MultitenancyConfig { @@ -210,6 +220,7 @@ impl Default for MultitenancyConfig { routing_mode: TenantRoutingMode::HeaderOnly, strict_validation: false, jwt_tenant_claim: "tenant_id".to_string(), + require_provisioned_tenant: false, } } } @@ -229,10 +240,15 @@ impl MultitenancyConfig { let jwt_tenant_claim = std::env::var("HFS_JWT_TENANT_CLAIM").unwrap_or_else(|_| "tenant_id".to_string()); + let require_provisioned_tenant = std::env::var("HFS_TENANT_REQUIRE_PROVISIONED") + .map(|s| s.to_lowercase() == "true" || s == "1") + .unwrap_or(false); + Self { routing_mode, strict_validation, jwt_tenant_claim, + require_provisioned_tenant, } } } @@ -775,6 +791,13 @@ pub struct ServerConfig { #[arg(long, env = "HFS_SOF_ENABLED", default_value = "true")] pub sof_enabled: bool, + /// Seed the spec conformance resources (SearchParameters and + /// CompartmentDefinitions) into primary storage for every provisioned tenant + /// — at startup and on tenant provisioning. Disable to leave storage + /// untouched (e.g. in tests that assert exact resource counts). + #[arg(long, env = "HFS_SEED_CONFORMANCE", default_value = "true")] + pub seed_conformance: bool, + /// Export sink type: "fs" (default, local filesystem) or "s3" (AWS S3). #[arg(long, env = "HFS_EXPORT_SINK", default_value = "fs")] pub export_sink: String, @@ -908,6 +931,7 @@ impl Default for ServerConfig { elasticsearch_username: None, elasticsearch_password: None, sof_enabled: true, + seed_conformance: true, export_sink: "fs".to_string(), export_dir: "./exports".to_string(), export_s3_bucket: None, @@ -1029,6 +1053,7 @@ impl ServerConfig { elasticsearch_username: None, elasticsearch_password: None, sof_enabled: true, + seed_conformance: true, export_sink: "fs".to_string(), export_dir: "./exports".to_string(), export_s3_bucket: None, @@ -1497,6 +1522,7 @@ mod tests { routing_mode: TenantRoutingMode::UrlPath, strict_validation: true, jwt_tenant_claim: "custom_claim".to_string(), + ..Default::default() }; assert_eq!(config.routing_mode, TenantRoutingMode::UrlPath); assert!(config.strict_validation); diff --git a/crates/rest/src/extractors/tenant.rs b/crates/rest/src/extractors/tenant.rs index bcec652b6..0d9e76336 100644 --- a/crates/rest/src/extractors/tenant.rs +++ b/crates/rest/src/extractors/tenant.rs @@ -136,6 +136,24 @@ where parts: &mut Parts, state: &AppState, ) -> Result { + let extractor = Self::resolve(parts, state).await?; + enforce_provisioned(state, &extractor).await?; + Ok(extractor) + } +} + +impl TenantExtractor { + /// Resolves the tenant for a request from JWT claim / header / URL / default, + /// applying strict-validation consistency checks. Provisioned-tenant + /// enforcement is applied separately by the extractor + /// ([`enforce_provisioned`]). + async fn resolve( + parts: &mut Parts, + state: &AppState, + ) -> Result + where + S: helios_persistence::core::ResourceStorage + Send + Sync, + { let config = state.config(); // If auth is enabled and a Principal with a tenant_id is present, @@ -230,6 +248,42 @@ where } } +/// Rejects a resolved tenant that is not provisioned, when +/// `HFS_TENANT_REQUIRE_PROVISIONED` is set and the backend maintains a tenant +/// registry. The internal system tenant is always allowed; backends without a +/// registry (mock stores, minimal deployments) are never enforced, so the +/// common single-tenant and test configurations are unaffected. +async fn enforce_provisioned( + state: &AppState, + extractor: &TenantExtractor, +) -> Result<(), (StatusCode, String)> +where + S: helios_persistence::core::ResourceStorage + Send + Sync, +{ + if !state.config().multitenancy.require_provisioned_tenant { + return Ok(()); + } + let storage = state.storage(); + if !storage.supports_tenant_registry() { + return Ok(()); + } + let id = extractor.tenant_id(); + if id == helios_persistence::tenant::SYSTEM_TENANT { + return Ok(()); + } + match storage.get_tenant(id).await { + Ok(Some(_)) => Ok(()), + Ok(None) => Err(( + StatusCode::NOT_FOUND, + format!("Unknown tenant '{id}': tenants must be provisioned via the admin API"), + )), + Err(e) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Tenant lookup failed: {e}"), + )), + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/rest/src/handlers/admin_tenants.rs b/crates/rest/src/handlers/admin_tenants.rs index 4a91246c8..8a9e405dc 100644 --- a/crates/rest/src/handlers/admin_tenants.rs +++ b/crates/rest/src/handlers/admin_tenants.rs @@ -65,6 +65,28 @@ fn require_registry(storage: &S) -> RestResult<()> { } } +/// Seeds a newly-provisioned tenant with the spec conformance resources +/// (SearchParameters and CompartmentDefinitions), mirroring the per-tenant +/// startup seed. Best-effort: failures are logged, never surfaced to the caller +/// (the tenant is registered regardless; the next startup seed completes it). +async fn seed_new_tenant( + storage: &S, + config: &crate::config::ServerConfig, + tenant_id: &str, +) { + let data_dir = config + .data_dir + .clone() + .unwrap_or_else(|| std::path::PathBuf::from("./data")); + helios_persistence::search::seed_tenant_conformance( + storage, + config.default_fhir_version, + &data_dir, + tenant_id, + ) + .await; +} + /// Validates a tenant id: non-empty, within length, no whitespace, a /// conservative identifier charset, and never the internal system sentinel. fn validate_tenant_id(id: &str) -> RestResult<()> { @@ -187,6 +209,16 @@ where let record = state.storage().register_tenant(&id, display_name).await?; debug!(tenant = %id, "Registered tenant"); + + // Seed the new tenant with the conformance resources (spec SearchParameters + // and CompartmentDefinitions) so `GET /SearchParameter` and + // `GET /CompartmentDefinition` are populated for it, matching what the + // server seeds every provisioned tenant with at startup. Best-effort: a + // failed seed logs and still returns the created tenant. + if state.config().seed_conformance { + seed_new_tenant(state.storage(), state.config(), &id).await; + } + audit( &state, AuditAction::Create, diff --git a/crates/rest/src/handlers/capabilities.rs b/crates/rest/src/handlers/capabilities.rs index 9f3879819..d686fa9a7 100644 --- a/crates/rest/src/handlers/capabilities.rs +++ b/crates/rest/src/handlers/capabilities.rs @@ -102,7 +102,8 @@ where state.base_url().to_string() }; - let capability_statement = build_capability_statement(&state, fhir_version, &base_url); + let capability_statement = + build_capability_statement(&state, tenant.context(), fhir_version, &base_url); // Negotiate response format let negotiated = negotiate_format(&req_headers, None); @@ -129,6 +130,7 @@ where /// Builds a CapabilityStatement describing server capabilities for a specific FHIR version. fn build_capability_statement( state: &AppState, + tenant: &helios_persistence::tenant::TenantContext, version: FhirVersion, base_url: &str, ) -> serde_json::Value @@ -163,7 +165,8 @@ where .map(|t| (t, state.storage().modifiers_for_param_type(t))) .collect(); - let registry = state.storage().search_param_registry().read(); + let registry_arc = state.storage().search_param_registry(tenant); + let registry = registry_arc.read(); // Reverse-include index (target type -> "Source:code" tokens) for advertising // each resource's real `searchRevInclude` targets. diff --git a/crates/rest/src/handlers/compartment.rs b/crates/rest/src/handlers/compartment.rs index 6ea6efcee..762b61731 100644 --- a/crates/rest/src/handlers/compartment.rs +++ b/crates/rest/src/handlers/compartment.rs @@ -94,7 +94,8 @@ where // Convert REST params to persistence SearchQuery. Scope the registry read // guard tightly so it doesn't span any await. let mut query = { - let registry = state.storage().search_param_registry().read(); + let reg = state.storage().search_param_registry(tenant.context()); + let registry = reg.read(); build_search_query(&target_type, &search_params, ®istry)? }; @@ -193,7 +194,8 @@ where // restricted to the compartment. Building happens under the registry read // lock (no await); the lock is released before any search executes. let queries: Vec = { - let registry = state.storage().search_param_registry().read(); + let reg = state.storage().search_param_registry(tenant.context()); + let registry = reg.read(); crate::fhir_types::get_resource_type_names_for_version(fhir_version) .iter() .filter_map(|target_type| { diff --git a/crates/rest/src/handlers/search.rs b/crates/rest/src/handlers/search.rs index 8d6cec81d..b3d156a62 100644 --- a/crates/rest/src/handlers/search.rs +++ b/crates/rest/src/handlers/search.rs @@ -199,7 +199,8 @@ where // also apply to reference/uri, which resolve locally — only reject those // when the parameter is a token. See assessment item A2c. { - let registry = state.storage().search_param_registry().read(); + let reg = state.storage().search_param_registry(tenant.context()); + let registry = reg.read(); for (key, _) in &pairs { let Some((base, modifier)) = key.split_once(':') else { continue; @@ -232,7 +233,8 @@ where // guard tightly so it doesn't span any await — parking_lot guards aren't // Send by default, which would make this async fn !Send. let mut query = { - let registry = state.storage().search_param_registry().read(); + let reg = state.storage().search_param_registry(tenant.context()); + let registry = reg.read(); // Under `Prefer: handling=strict`, reject unknown search parameters // (the lenient default ignores them). if strict { @@ -424,7 +426,8 @@ where // system search — parameters there are interpreted across many resource // types, so a per-"Resource" registry check would false-positive. let mut query = { - let registry = state.storage().search_param_registry().read(); + let reg = state.storage().search_param_registry(tenant.context()); + let registry = reg.read(); build_search_query("Resource", &search_params, ®istry)? }; diff --git a/crates/rest/tests/admin_tenants.rs b/crates/rest/tests/admin_tenants.rs index a69f6f42a..6749e2d0a 100644 --- a/crates/rest/tests/admin_tenants.rs +++ b/crates/rest/tests/admin_tenants.rs @@ -45,6 +45,9 @@ async fn create_test_server() -> TestServer { }, base_url: "http://localhost:8080".to_string(), default_tenant: "default-tenant".to_string(), + // These tests assert exact per-tenant resource counts, so don't let + // provisioning seed the conformance set on top. + seed_conformance: false, ..ServerConfig::for_testing() }; diff --git a/crates/rest/tests/compartment_definition_endpoint.rs b/crates/rest/tests/compartment_definition_endpoint.rs new file mode 100644 index 000000000..96bb12916 --- /dev/null +++ b/crates/rest/tests/compartment_definition_endpoint.rs @@ -0,0 +1,70 @@ +//! Integration test: seeded CompartmentDefinitions are discoverable through the +//! normal FHIR route `GET /CompartmentDefinition`, reading primary storage. + +use std::path::PathBuf; +use std::sync::Arc; + +use axum::http::StatusCode; +use axum_test::TestServer; +use helios_fhir::FhirVersion; +use helios_persistence::backends::sqlite::{SqliteBackend, SqliteBackendConfig}; +use helios_persistence::search::seed_spec_compartment_definitions; +use helios_rest::ServerConfig; + +fn data_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(|p| p.parent()) + .map(|p| p.join("data")) + .unwrap_or_else(|| PathBuf::from("data")) +} + +#[tokio::test] +async fn get_compartment_definition_returns_seeded_resources() { + let backend = SqliteBackend::with_config( + ":memory:", + SqliteBackendConfig { + data_dir: Some(data_dir()), + ..Default::default() + }, + ) + .expect("create SQLite backend"); + backend.init_schema().expect("init schema"); + let backend = Arc::new(backend); + + // Seed the default tenant, then serve. + seed_spec_compartment_definitions(&*backend, FhirVersion::R4, &data_dir(), "default") + .await + .expect("seed compartment definitions"); + + let config = ServerConfig { + base_url: "http://localhost:8080".to_string(), + default_tenant: "default".to_string(), + ..ServerConfig::for_testing() + }; + let state = helios_rest::AppState::new(Arc::clone(&backend), config); + let app = helios_rest::routing::fhir_routes::create_routes(state); + let server = TestServer::new(app).expect("create test server"); + + let resp = server.get("/CompartmentDefinition").await; + assert_eq!(resp.status_code(), StatusCode::OK); + let body: serde_json::Value = resp.json(); + assert_eq!(body["resourceType"], "Bundle"); + assert_eq!(body["type"], "searchset"); + let entries = body["entry"].as_array().expect("entries"); + assert_eq!(entries.len(), 5, "R4 ships 5 compartment definitions"); + + let codes: Vec<&str> = entries + .iter() + .filter_map(|e| e["resource"]["code"].as_str()) + .collect(); + assert!(codes.contains(&"Patient")); + assert!(codes.contains(&"Encounter")); + + // Read one by id. + let patient = server.get("/CompartmentDefinition/patient").await; + assert_eq!(patient.status_code(), StatusCode::OK); + let pbody: serde_json::Value = patient.json(); + assert_eq!(pbody["resourceType"], "CompartmentDefinition"); + assert_eq!(pbody["code"], "Patient"); +} diff --git a/crates/rest/tests/per_tenant_search_params.rs b/crates/rest/tests/per_tenant_search_params.rs new file mode 100644 index 000000000..f7718a29a --- /dev/null +++ b/crates/rest/tests/per_tenant_search_params.rs @@ -0,0 +1,192 @@ +//! Integration tests: the in-memory SearchParameter registry is per-tenant, so +//! a custom `SearchParameter` POSTed under one tenant changes that tenant's FHIR +//! search API (and CapabilityStatement) without affecting another tenant. + +use std::path::PathBuf; +use std::sync::Arc; + +use axum::http::{HeaderName, HeaderValue, StatusCode}; +use axum_test::TestServer; +use helios_persistence::backends::sqlite::{SqliteBackend, SqliteBackendConfig}; +use helios_rest::ServerConfig; +use serde_json::{Value, json}; + +const X_TENANT_ID: HeaderName = HeaderName::from_static("x-tenant-id"); + +fn data_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(|p| p.parent()) + .map(|p| p.join("data")) + .unwrap_or_else(|| PathBuf::from("data")) +} + +async fn server() -> TestServer { + let backend = SqliteBackend::with_config( + ":memory:", + SqliteBackendConfig { + data_dir: Some(data_dir()), + ..Default::default() + }, + ) + .expect("create SQLite backend"); + backend.init_schema().expect("init schema"); + let config = ServerConfig { + base_url: "http://localhost:8080".to_string(), + default_tenant: "default".to_string(), + ..ServerConfig::for_testing() + }; + let state = helios_rest::AppState::new(Arc::new(backend), config); + let app = helios_rest::routing::fhir_routes::create_routes(state); + TestServer::new(app).expect("create test server") +} + +/// A custom SearchParameter (`Patient.name.where(use='nickname')`), POSTed under +/// one tenant, resolves searches and appears in `/metadata` for that tenant +/// only. +#[tokio::test] +async fn custom_search_parameter_is_tenant_scoped() { + let server = server().await; + + let sp = json!({ + "resourceType": "SearchParameter", + "id": "patient-nickname", + "url": "http://acme.health/fhir/SearchParameter/patient-nickname", + "name": "nickname", + "status": "active", + "code": "nickname", + "base": ["Patient"], + "type": "string", + "expression": "Patient.name.where(use = 'nickname').given" + }); + + // POST the custom parameter under acme1 only. + let created = server + .post("/SearchParameter") + .add_header(X_TENANT_ID, HeaderValue::from_static("acme1")) + .json(&sp) + .await; + assert_eq!(created.status_code(), StatusCode::CREATED); + + // A Patient with a nickname, created under each tenant (indexed per-tenant). + let patient = json!({ + "resourceType": "Patient", + "id": "p1", + "name": [{ "use": "nickname", "given": ["Ace"] }, { "family": "Adams" }] + }); + for t in ["acme1", "acme2"] { + let resp = server + .put("/Patient/p1") + .add_header(X_TENANT_ID, HeaderValue::from_str(t).unwrap()) + .json(&patient) + .await; + assert!( + resp.status_code() == StatusCode::CREATED || resp.status_code() == StatusCode::OK, + "{t} patient create: {}", + resp.status_code() + ); + } + + // acme1 resolves the custom search and matches the patient. + let hit = server + .get("/Patient?nickname=Ace") + .add_header(X_TENANT_ID, HeaderValue::from_static("acme1")) + .await; + assert_eq!(hit.status_code(), StatusCode::OK); + let body: Value = hit.json(); + assert_eq!( + body["entry"].as_array().map(|e| e.len()).unwrap_or(0), + 1, + "acme1 should resolve the custom nickname search" + ); + + // acme2 has no such parameter — under strict handling it is rejected. + let strict = server + .get("/Patient?nickname=Ace") + .add_header(X_TENANT_ID, HeaderValue::from_static("acme2")) + .add_header( + HeaderName::from_static("prefer"), + HeaderValue::from_static("handling=strict"), + ) + .await; + assert_eq!( + strict.status_code(), + StatusCode::BAD_REQUEST, + "acme2 must not know the acme1-only 'nickname' parameter" + ); + + // GET /SearchParameter is likewise isolated. + let acme1_sp: Value = server + .get("/SearchParameter?code=nickname") + .add_header(X_TENANT_ID, HeaderValue::from_static("acme1")) + .await + .json(); + assert_eq!( + acme1_sp["entry"].as_array().map(|e| e.len()).unwrap_or(0), + 1 + ); + let acme2_sp: Value = server + .get("/SearchParameter?code=nickname") + .add_header(X_TENANT_ID, HeaderValue::from_static("acme2")) + .await + .json(); + assert_eq!( + acme2_sp["entry"].as_array().map(|e| e.len()).unwrap_or(0), + 0 + ); +} + +/// `/metadata` (CapabilityStatement) advertises the custom parameter for the +/// owning tenant only. +#[tokio::test] +async fn capability_statement_is_tenant_scoped() { + let server = server().await; + + let sp = json!({ + "resourceType": "SearchParameter", + "id": "patient-nickname", + "url": "http://acme.health/fhir/SearchParameter/patient-nickname", + "name": "nickname", + "status": "active", + "code": "nickname", + "base": ["Patient"], + "type": "string", + "expression": "Patient.name.where(use = 'nickname').given" + }); + server + .post("/SearchParameter") + .add_header(X_TENANT_ID, HeaderValue::from_static("acme1")) + .json(&sp) + .await; + + let advertises_nickname = |cap: &Value| -> bool { + cap["rest"] + .as_array() + .into_iter() + .flatten() + .flat_map(|r| r["resource"].as_array().into_iter().flatten()) + .filter(|res| res["type"] == "Patient") + .flat_map(|res| res["searchParam"].as_array().into_iter().flatten()) + .any(|p| p["name"] == "nickname") + }; + + let acme1: Value = server + .get("/metadata") + .add_header(X_TENANT_ID, HeaderValue::from_static("acme1")) + .await + .json(); + assert!( + advertises_nickname(&acme1), + "acme1 metadata should list nickname" + ); + + let acme2: Value = server + .get("/metadata") + .add_header(X_TENANT_ID, HeaderValue::from_static("acme2")) + .await + .json(); + assert!( + !advertises_nickname(&acme2), + "acme2 metadata must not list nickname" + ); +} diff --git a/crates/rest/tests/persistence_operations.rs b/crates/rest/tests/persistence_operations.rs index 270128bff..f5fcdca0f 100644 --- a/crates/rest/tests/persistence_operations.rs +++ b/crates/rest/tests/persistence_operations.rs @@ -159,7 +159,7 @@ fn server_with_ops() -> (TestServer, Arc, CollectorSink) { // reindex driver emits its own lifecycle events from the background job, so // it needs the sink directly — unlike purge, which the handler audits. let reindex = Arc::new( - ReindexOperation::new(backend.clone(), backend.search_extractor().clone()) + ReindexOperation::new(backend.clone(), backend.tenant_registries().clone()) .with_audit(Arc::new(sink.clone()) as Arc, "Device/hfs"), ); @@ -194,7 +194,7 @@ fn server_with_principal(scopes: &str) -> (TestServer, Arc) { let reindex = Arc::new(ReindexOperation::new( backend.clone(), - backend.search_extractor().clone(), + backend.tenant_registries().clone(), )); let state = helios_rest::AppState::new( backend.clone(), diff --git a/crates/rest/tests/tenant_provisioning.rs b/crates/rest/tests/tenant_provisioning.rs new file mode 100644 index 000000000..e698ab884 --- /dev/null +++ b/crates/rest/tests/tenant_provisioning.rs @@ -0,0 +1,84 @@ +//! Integration tests for provisioned-only tenant enforcement +//! (`HFS_TENANT_REQUIRE_PROVISIONED`): a request resolving to a tenant that was +//! never provisioned through the admin API is rejected, while provisioned +//! tenants work normally. + +use std::path::PathBuf; +use std::sync::Arc; + +use axum::http::{HeaderName, HeaderValue, StatusCode}; +use axum_test::TestServer; +use helios_persistence::backends::sqlite::{SqliteBackend, SqliteBackendConfig}; +use helios_persistence::core::ResourceStorage; +use helios_rest::ServerConfig; +use helios_rest::config::MultitenancyConfig; + +const X_TENANT_ID: HeaderName = HeaderName::from_static("x-tenant-id"); + +async fn create_server(require_provisioned: bool) -> (TestServer, Arc) { + let data_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(|p| p.parent()) + .map(|p| p.join("data")) + .unwrap_or_else(|| PathBuf::from("data")); + + let backend = SqliteBackend::with_config( + ":memory:", + SqliteBackendConfig { + data_dir: Some(data_dir), + ..Default::default() + }, + ) + .expect("create SQLite backend"); + backend.init_schema().expect("init schema"); + let backend = Arc::new(backend); + + let config = ServerConfig { + multitenancy: MultitenancyConfig { + require_provisioned_tenant: require_provisioned, + ..Default::default() + }, + base_url: "http://localhost:8080".to_string(), + default_tenant: "default".to_string(), + ..ServerConfig::for_testing() + }; + + let state = helios_rest::AppState::new(Arc::clone(&backend), config); + let app = helios_rest::routing::fhir_routes::create_routes(state); + let server = TestServer::new(app).expect("create test server"); + (server, backend) +} + +#[tokio::test] +async fn unprovisioned_tenant_is_rejected_when_enforced() { + let (server, backend) = create_server(true).await; + backend + .register_tenant("acme", None) + .await + .expect("provision acme"); + + // A provisioned tenant is served normally. + let ok = server + .get("/Patient") + .add_header(X_TENANT_ID, HeaderValue::from_static("acme")) + .await; + assert_eq!(ok.status_code(), StatusCode::OK); + + // An unprovisioned tenant is rejected. + let rejected = server + .get("/Patient") + .add_header(X_TENANT_ID, HeaderValue::from_static("ghost")) + .await; + assert_eq!(rejected.status_code(), StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn unprovisioned_tenant_is_allowed_when_not_enforced() { + // Default (backward-compatible) behavior: any tenant id works. + let (server, _backend) = create_server(false).await; + let resp = server + .get("/Patient") + .add_header(X_TENANT_ID, HeaderValue::from_static("ghost")) + .await; + assert_eq!(resp.status_code(), StatusCode::OK); +} diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 29fbd7db8..fc6ccf928 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -9,7 +9,39 @@ repository.workspace = true homepage = "https://github.com/HeliosSoftware/hfs/tree/main/crates/ui" keywords = ["helios-software", "fhir", "ui", "htmx"] +[features] +# FHIR version features forward to helios-fhir so the viewer read paths +# (SearchParameter + CompartmentDefinition fetched from the FHIR API) cover +# exactly the versions the server is built with. R4 is the workspace default. +default = ["R4"] +R4 = ["helios-fhir/R4"] +R4B = ["helios-fhir/R4B"] +R5 = ["helios-fhir/R5"] +R6 = ["helios-fhir/R6"] + [dependencies] +# Read paths into the rest of the workspace: the SearchParameter spec loader +# and registry types (#238) and the per-version compartment-params table +# (#237). Data flows one way — the UI depends on the workspace, never the +# reverse. +helios-fhir = { path = "../fhir", version = "0.2.1", default-features = false } + +# Deserializes the FHIR resources fetched from the server's own +# `/SearchParameter` and `/CompartmentDefinition` endpoints, and derives the +# query-string extractors. Both already in the workspace dependency graph. +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# The UI reads conformance data (SearchParameter, CompartmentDefinition) from +# the server's own FHIR API over HTTP — storage is the source of truth. Plain +# HTTP against a loopback URL, so no TLS backend is needed. +reqwest = { version = "0.12", default-features = false, features = ["json"] } +async-trait = "0.1" + +# `OutboundAuthProvider`: the credentials the UI attaches to its server-to-server +# self-calls (a service token when auth is enabled; a no-op when it is not). +helios-auth = { path = "../auth", version = "0.2.1" } + # Web framework (matches the version used by helios-hfs / helios-rest) axum = "0.8" @@ -49,7 +81,6 @@ helios-observability = { path = "../observability", version = "0.2.1" } # registry (list/register/deregister) + per-tenant resource counts. The UI # depends on the rest of the server, never the reverse. helios-persistence = { path = "../persistence", version = "0.2.1", default-features = false, features = ["R4"] } -serde = { version = "1", features = ["derive"] } [dev-dependencies] # Parses the .ftl sources so tests can enforce key-set parity across locales. diff --git a/crates/ui/README.md b/crates/ui/README.md index 37be6c375..abaaaffd5 100644 --- a/crates/ui/README.md +++ b/crates/ui/README.md @@ -163,6 +163,20 @@ Mounted under `/ui` when running `hfs` (the `ui` feature is on by default; the `headless` feature disables it): - `GET /ui` — full landing page (`pages/index.html` → `layouts/base.html`). +- `GET /ui/queries` — saved FHIR queries per resource type (#234), hydrated + client-side from the per-user settings document. +- `GET /ui/search-parameters` — read-only SearchParameter viewer (#238): + Resource Filter rail, type/source facet rows, paginated table, and a detail + panel, over the same snapshot the storage backends seed their registries + from (embedded fallback + the spec bundle in `HFS_DATA_DIR`). Every filter + is a link, so the whole screen works without JavaScript; the write half + lands behind #235. +- `GET /ui/compartments` — Compartment viewer & route tester (#237): the + vendored spec `CompartmentDefinition`s (`data/compartments/`, parity-tested + against the codegen'd `get_compartment_params()` table), a Members tab, and + a tester that answers "is this type in this compartment, via which + parameters, and what search does the server run?" — resolved through the + same table the REST compartment handler consults. - `GET /ui/status` — a system-status read path. Returns the `partials/status.html` **fragment** on `HX-Request`, and the **full page** on a hard navigation — demonstrating the same URL working with and without JS. diff --git a/crates/ui/assets/app.css b/crates/ui/assets/app.css index 5cd226a12..99588e94b 100644 --- a/crates/ui/assets/app.css +++ b/crates/ui/assets/app.css @@ -96,6 +96,12 @@ svg { display: block; } +/* Class display values (flex, inline-flex) must not defeat the hidden + attribute on script-toggled regions. */ +[hidden] { + display: none !important; +} + /* ---------- Sidebar ---------- */ .sidebar { @@ -1408,3 +1414,750 @@ html[data-nav="collapsed"] .brand__version { left: 0; } } + +/* ---- Registry viewers: shared filter layout (#237/#238) ----------------- */ + +/* These pages want the full pane width; .content keeps its dashboard cap. */ +.content--wide { + max-width: none; +} + +.notice { + margin: 0 0 16px; + padding: 10px 14px; + font-size: 13px; + border-radius: 10px; +} + +.notice--warn { + color: var(--text); + background: var(--warn-soft); + border: 1px solid var(--warn); +} + +.filter-layout { + display: grid; + grid-template-columns: 280px minmax(0, 1fr) 360px; + gap: 20px; + align-items: start; +} + +.filter-layout--two { + grid-template-columns: 280px minmax(0, 1fr); +} + +.version-switch { + display: flex; + gap: 4px; + margin-top: 10px; +} + +.version-switch a { + padding: 4px 10px; + border: 1px solid var(--outline); + border-radius: 999px; + font-size: 12px; + font-weight: 500; + color: var(--muted); + text-decoration: none; +} + +.version-switch a[aria-current="true"] { + background: var(--accent-soft); + border-color: var(--accent); + color: var(--text-strong); +} + +/* Resource Filter rail (shared primitive; the Resources screen reuses it). */ + +.filter-rail { + position: sticky; + top: 20px; + display: flex; + flex-direction: column; + gap: 10px; + max-height: calc(100vh - 110px); + padding: 14px; +} + +.filter-rail__search { + display: flex; + align-items: center; + gap: 8px; + padding: 0 10px; + background: var(--input-bg); + border: 1px solid var(--surface-border); + border-radius: 10px; + color: var(--muted); +} + +.filter-rail__search input { + flex: 1; + height: 34px; + border: 0; + background: transparent; + font: inherit; + font-size: 13px; + color: var(--text); + outline: none; +} + +.filter-rail__heading { + margin: 6px 4px 0; + font-size: 10px; + font-weight: 600; + letter-spacing: 0.6px; + text-transform: uppercase; + color: var(--muted); +} + +.filter-rail__list { + display: flex; + flex-direction: column; + gap: 2px; + overflow-y: auto; +} + +.filter-rail__group { + display: flex; + flex-direction: column; + gap: 2px; +} + +.filter-rail__item { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 7px 10px; + border-radius: 9px; + font-size: 13px; + font-weight: 500; + color: var(--text); + text-decoration: none; +} + +.filter-rail__item:hover { + background: var(--accent-soft); +} + +.filter-rail__item[aria-current="true"] { + background: var(--accent-soft); + color: var(--text-strong); + font-weight: 600; +} + +.filter-rail__item .count { + margin-left: auto; + font-size: 11px; + color: var(--muted); + font-variant-numeric: tabular-nums; +} + +.filter-rail__note { + margin: 8px 4px 0; + padding-top: 10px; + border-top: 1px solid var(--surface-border); + font-size: 11.5px; + line-height: 1.5; + color: var(--muted); +} + +.filter-center { + display: flex; + flex-direction: column; + gap: 14px; + min-width: 0; +} + +/* Facet chip rows */ + +.facets { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; + padding: 12px 14px; +} + +.facets--bare { + padding: 0 0 12px; +} + +.facet-label { + margin-right: 4px; + font-size: 10px; + font-weight: 600; + letter-spacing: 0.6px; + text-transform: uppercase; + color: var(--muted); +} + +.chip { + display: inline-flex; + align-items: center; + gap: 6px; + height: 26px; + padding: 0 10px; + border: 1px solid var(--surface-border); + border-radius: 999px; + font-size: 12px; + color: var(--text); + text-decoration: none; +} + +.chip:hover { + border-color: var(--accent); +} + +.chip[aria-current="true"] { + background: var(--accent-soft); + border-color: var(--accent); + color: var(--text-strong); + font-weight: 600; +} + +.chip .count { + font-size: 11px; + color: var(--muted); + font-variant-numeric: tabular-nums; +} + +/* Data table */ + +.table-wrap { + overflow-x: auto; +} + +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.data-table th { + padding: 12px 14px 8px; + border-bottom: 1px solid var(--surface-border); + font-size: 10px; + font-weight: 600; + letter-spacing: 0.6px; + text-transform: uppercase; + text-align: left; + color: var(--muted); +} + +.data-table td { + padding: 9px 14px; + border-bottom: 1px solid var(--surface-border); + vertical-align: top; +} + +.data-table tbody tr:hover td, +.data-table tbody tr[aria-selected="true"] td { + background: var(--accent-soft); +} + +.row-link { + display: flex; + flex-direction: column; + gap: 2px; + color: inherit; + text-decoration: none; +} + +.row-link .code, +.data-table .code { + font-weight: 600; + color: var(--text-strong); +} + +.url { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 11px; + color: var(--muted); + overflow-wrap: anywhere; +} + +.table-foot { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + font-size: 12px; + color: var(--muted); +} + +.pagination { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.btn--current { + background: var(--accent-soft); + border-color: var(--accent); + color: var(--text-strong); +} + +/* Status tags (small pills). Distinct from .pill, the large control chip. */ + +.tag { + display: inline-flex; + align-items: center; + height: 20px; + padding: 0 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; +} + +.tag--type { + background: var(--accent-soft); + color: var(--text); +} + +.tag--param { + background: var(--accent-soft); + color: var(--text); + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-weight: 500; +} + +.tag--embedded { + background: var(--input-bg); + border: 1px solid var(--surface-border); + color: var(--muted); +} + +.tag--stored, +.tag--member { + background: var(--ok-soft); + color: var(--ok); +} + +.tag--config, +.tag--overrides, +.tag--shadowed { + background: var(--warn-soft); + color: var(--warn); +} + +.tag--conflict, +.tag--excluded { + background: var(--danger-soft); + color: var(--danger); +} + +/* Detail panel (right rail on the SearchParameter viewer) */ + +.detail { + position: sticky; + top: 20px; + display: flex; + flex-direction: column; + gap: 12px; + max-height: calc(100vh - 110px); + padding: 16px; + overflow-y: auto; +} + +.detail__title { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + font-size: 15px; + color: var(--text-strong); +} + +.detail__field { + display: flex; + flex-direction: column; + gap: 5px; + font-size: 13px; +} + +.detail__field > span { + font-size: 10px; + font-weight: 600; + letter-spacing: 0.6px; + text-transform: uppercase; + color: var(--muted); +} + +.detail__field code { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 11.5px; + overflow-wrap: anywhere; +} + +.detail__field input { + height: 34px; + padding: 0 10px; + font: inherit; + font-size: 13px; + color: var(--text); + background: var(--input-bg); + border: 1px solid var(--surface-border); + border-radius: 9px; + outline: none; +} + +.detail__field input:focus { + border-color: var(--accent); +} + +.detail__tags { + display: flex; + flex-wrap: wrap; + gap: 5px; +} + +.detail__code { + margin: 0; + padding: 10px 12px; + background: var(--input-bg); + border-radius: 9px; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 12px; + line-height: 1.55; + overflow-x: auto; + color: var(--text); + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.detail__hint { + margin: 0; + font-size: 11.5px; + line-height: 1.5; + color: var(--muted); +} + +.detail__hint-inline { + font-size: 11.5px; + color: var(--muted); +} + +/* Lint findings (the validation surface the editor grows into) */ + +.lint { + display: flex; + flex-direction: column; + gap: 6px; +} + +.lint__item { + display: flex; + gap: 8px; + padding: 8px 10px; + border-radius: 9px; + font-size: 12px; + line-height: 1.45; +} + +.lint__item--error { + background: var(--danger-soft); + border: 1px solid var(--danger); +} + +.lint__item--warn { + background: var(--warn-soft); + border: 1px solid var(--warn); +} + +.lint__item--info { + background: var(--accent-soft); + border: 1px solid var(--accent); +} + +.lint__sev { + flex-shrink: 0; + font-weight: 700; +} + +.lint__item--error .lint__sev { color: var(--danger); } +.lint__item--warn .lint__sev { color: var(--warn); } +.lint__item--info .lint__sev { color: var(--accent); } + +/* Compartment viewer: tabs, members, tester */ + +.tabs { + display: flex; + gap: 4px; + padding: 8px; +} + +.tab { + padding: 8px 16px; + border-radius: 9px; + font-size: 13px; + font-weight: 600; + color: var(--muted); + text-decoration: none; +} + +.tab:hover { + color: var(--text); +} + +.tab[aria-current="true"] { + background: var(--accent-soft); + color: var(--text-strong); +} + +.panel { + padding: 18px; +} + +.kv-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px 18px; + margin-bottom: 14px; +} + +.detail__field--wide { + grid-column: 1 / -1; +} + +.members { + display: flex; + flex-direction: column; + max-height: 560px; + overflow-y: auto; +} + +.member { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 6px; + border-bottom: 1px solid var(--surface-border); + font-size: 13px; +} + +.member__name { + flex-shrink: 0; + width: 220px; + font-weight: 600; + color: var(--text-strong); +} + +.member__params { + display: flex; + flex: 1; + flex-wrap: wrap; + gap: 5px; + align-items: center; +} + +.tester { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: end; +} + +.tester .detail__field { + min-width: 170px; +} + +.tester-result { + margin-top: 16px; + display: flex; + flex-direction: column; + gap: 8px; + padding: 14px 16px; + background: var(--bg-content); + border: 1px solid var(--surface-border); + border-radius: 12px; +} + +.tester-result__title { + margin: 0; + font-size: 13px; + color: var(--text-strong); +} + +.tester-result__title--ok { color: var(--ok); } +.tester-result__title--danger { color: var(--danger); } + +@media (max-width: 1250px) { + .filter-layout { + grid-template-columns: 1fr; + } + + .filter-rail, + .detail { + position: static; + max-height: none; + } + + .member__name { + width: 150px; + } + + .kv-grid { + grid-template-columns: 1fr; + } +} + +/* Visual builder rows (conditions / includes / result controls) */ + +.builder-sections { + display: flex; + flex-direction: column; + gap: 14px; + padding-top: 4px; +} + +.builder-section__title { + margin: 0 0 8px; + font-size: 10px; + font-weight: 600; + letter-spacing: 0.6px; + text-transform: uppercase; + color: var(--muted); +} + +.builder-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px 18px; +} + +.builder-rows { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 8px; +} + +.builder-row { + display: flex; + gap: 6px; + align-items: center; +} + +.builder-row__key, +.builder-row__modifier, +.builder-row__value { + height: 32px; + padding: 0 10px; + font: inherit; + font-size: 12.5px; + color: var(--text); + background: var(--input-bg); + border: 1px solid var(--surface-border); + border-radius: 9px; + outline: none; +} + +.builder-row__key { + width: 190px; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 12px; +} + +.builder-row__modifier { + width: 110px; + color: var(--muted); +} + +.builder-row__value { + flex: 1; + min-width: 0; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 12px; +} + +.builder-row__key:focus, +.builder-row__modifier:focus, +.builder-row__value:focus { + border-color: var(--accent); +} + +.builder-row__remove { + flex-shrink: 0; + width: 26px; + height: 26px; + padding: 0; + border: 0; + border-radius: 7px; + background: none; + color: var(--muted); + font-size: 14px; + line-height: 1; + cursor: pointer; +} + +.builder-row__remove:hover { + background: var(--accent-soft); + color: var(--text-strong); +} + +.builder-add { + align-self: flex-start; +} + +/* In-page results card */ + +.query-results { + margin-bottom: 24px; +} + +.query-results__head { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px 4px; +} + +.query-results__meta { + flex: 1; + font-size: 12px; + color: var(--muted); + font-variant-numeric: tabular-nums; +} + +@media (max-width: 900px) { + .builder-grid { + grid-template-columns: 1fr; + } + + .builder-row { + flex-wrap: wrap; + } +} + +/* Queries page: resource picker rail + main column (hi-fi Search layout) */ + +.queries-layout { + display: grid; + grid-template-columns: 260px minmax(0, 1fr); + gap: 20px; + align-items: start; +} + +.queries-main { + min-width: 0; +} + +/* The picker rail reuses .filter-rail with + + +
+ {% if view.tester.kind == "member" %} +

{{ i18n.t_arg("cmp-result-member", "params", view.tester.params.clone()) }}

+
{{ view.tester.route }}
+
+{{ i18n.t("cmp-result-flat") }}
+{{ view.tester.body }}
+

{{ i18n.t("cmp-result-member-note") }}

+ {% else if view.tester.kind == "self" %} +

{{ i18n.t("cmp-result-self") }}

+
{{ view.tester.route }}
+
+{{ view.tester.body }}
+

{{ i18n.t("cmp-result-self-note") }}

+ {% else if view.tester.kind == "notmember" %} +

{{ i18n.t_arg("cmp-result-notmember", "type", view.tester.target.clone()) }}

+
{{ view.tester.body }}
+

{{ i18n.t("cmp-result-notmember-note") }}

+ {% else %} +

{{ i18n.t_arg("cmp-result-fanout", "count", view.tester.total) }}

+
{{ view.tester.route }}
+

{{ view.tester.member_types }}

+

{{ i18n.t("cmp-result-fanout-note") }}

+ {% endif %} +
+ {% endif %} + + + +{% endblock %} diff --git a/crates/ui/templates/pages/queries.html b/crates/ui/templates/pages/queries.html index c1f60f6b8..5f580b218 100644 --- a/crates/ui/templates/pages/queries.html +++ b/crates/ui/templates/pages/queries.html @@ -2,12 +2,39 @@ {% block title %}{{ i18n.t("queries-heading") }} — {{ i18n.t("app-title") }}{% endblock %} +{% block content_class %} content--wide{% endblock %} + {% block content %}

{{ i18n.t("queries-heading") }}

{{ i18n.t("queries-lede") }}

+
+ + + +
+ + +
+ + +{% endblock %} diff --git a/crates/ui/templates/partials/param-options.html b/crates/ui/templates/partials/param-options.html new file mode 100644 index 000000000..b80230750 --- /dev/null +++ b/crates/ui/templates/partials/param-options.html @@ -0,0 +1,3 @@ + +{% for p in params %} +{% endfor %} diff --git a/crates/ui/tests/i18n_http.rs b/crates/ui/tests/i18n_http.rs index 7f04d99e8..1cb02da4a 100644 --- a/crates/ui/tests/i18n_http.rs +++ b/crates/ui/tests/i18n_http.rs @@ -11,7 +11,14 @@ use http_body_util::BodyExt; use tower::ServiceExt; fn app() -> Router { - helios_ui::mount(Router::new(), "9.9.9", None) + helios_ui::mount_with_conformance_source( + Router::new(), + "9.9.9", + None, + None, + std::sync::Arc::new(helios_ui::StaticConformanceSource::empty()), + helios_fhir::FhirVersion::R4, + ) } async fn body_text(response: axum::response::Response) -> String { diff --git a/crates/ui/tests/router_http.rs b/crates/ui/tests/router_http.rs index 5fd45bdd0..86c1cc5dc 100644 --- a/crates/ui/tests/router_http.rs +++ b/crates/ui/tests/router_http.rs @@ -12,7 +12,19 @@ use http_body_util::BodyExt; use tower::ServiceExt; fn app() -> Router { - helios_ui::mount(Router::new(), "9.9.9", None) + // Inject an offline conformance source seeded from the shipped `data/` + // bundles, so the SearchParameter/CompartmentDefinition viewers render real + // data without a running server (production fetches these over HTTP). + helios_ui::mount_with_conformance_source( + Router::new(), + "9.9.9", + Some(std::path::PathBuf::from("../../data")), + None, + std::sync::Arc::new(helios_ui::StaticConformanceSource::from_data_dir( + std::path::Path::new("../../data"), + )), + helios_fhir::FhirVersion::R4, + ) } async fn body_text(response: axum::response::Response) -> String { @@ -114,11 +126,136 @@ async fn embedded_assets_are_served() { async fn non_ui_paths_fall_through_to_the_fhir_app() { // Stand-in for the FHIR REST router: proves /ui never shadows it. let fhir_app = Router::new().route("/Patient", get(|| async { "fhir handled" })); - let response = helios_ui::mount(fhir_app, "9.9.9", None) - .oneshot(Request::get("/Patient").body(Body::empty()).unwrap()) + let response = helios_ui::mount_with_conformance_source( + fhir_app, + "9.9.9", + Some(std::path::PathBuf::from("../../data")), + None, + std::sync::Arc::new(helios_ui::StaticConformanceSource::empty()), + helios_fhir::FhirVersion::R4, + ) + .oneshot(Request::get("/Patient").body(Body::empty()).unwrap()) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(body_text(response).await, "fhir handled"); +} + +#[tokio::test] +async fn search_parameters_page_serves_the_registry_view() { + let response = app() + .oneshot( + Request::get("/ui/search-parameters?base=Patient") + .body(Body::empty()) + .unwrap(), + ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); - assert_eq!(body_text(response).await, "fhir handled"); + let html = body_text(response).await; + assert!(html.contains("")); + // The Resource Filter rail and the facet rows are server-rendered. + assert!(html.contains(r#"id="sp-rail-list""#)); + assert!(html.contains("base=Patient")); + // Real registry data, not placeholders: Patient supports `name`. + assert!(html.contains("http://hl7.org/fhir/SearchParameter/Patient-name")); + // This page, not Home, carries aria-current in the sidebar. + assert!(html.contains(r#"href="/ui/search-parameters" aria-current="page""#)); +} + +#[tokio::test] +async fn search_parameters_selection_renders_the_detail_panel() { + let response = app() + .oneshot( + Request::get( + "/ui/search-parameters?base=Patient&sel=http%3A%2F%2Fhl7.org%2Ffhir%2FSearchParameter%2FPatient-name", + ) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + let html = body_text(response).await; + assert!(html.contains(r#"aria-selected="true""#)); + // The detail panel shows the FHIRPath expression of the spec parameter. + assert!(html.contains("Patient.name")); +} + +#[tokio::test] +async fn compartments_page_defaults_to_patient() { + let response = app() + .oneshot( + Request::get("/ui/compartments") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + let html = body_text(response).await; + assert!(html.contains("http://hl7.org/fhir/CompartmentDefinition/patient")); + assert!(html.contains(r#"href="/ui/compartments" aria-current="page""#)); +} + +#[tokio::test] +async fn compartment_tester_resolves_membership_via_get() { + let response = app() + .oneshot( + Request::get("/ui/compartments?def=Patient&tab=tester&id=example&target=Observation") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + let html = body_text(response).await; + // The equivalent flat search the server runs, straight from the + // codegen'd table the REST handler consults. + assert!(html.contains("subject=Patient/example")); + assert!(html.contains("performer=Patient/example")); +} + +#[tokio::test] +async fn compartment_tester_reports_non_members_as_404() { + let response = app() + .oneshot( + Request::get("/ui/compartments?def=Patient&tab=tester&id=example&target=Medication") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + let html = body_text(response).await; + assert!(html.contains("404 Not Found")); + assert!(html.contains("OperationOutcome")); +} + +#[tokio::test] +async fn queries_param_catalog_is_a_registry_fed_fragment() { + let response = app() + .oneshot( + Request::get("/ui/queries/params?type=Patient") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + let html = body_text(response).await; + assert!(html.contains(r#""#)); + // Real registry data: Patient's own params plus Resource-level ones. + assert!(html.contains(r#"value="birthdate""#)); + assert!(html.contains(r#"value="_id""#)); + // Not applicable to Patient. + assert!(!html.contains(r#"value="clinical-status""#)); + assert!(!html.contains(" Arc { /// Builds the UI router over the given store. A fresh router per request is /// fine — they all share the same backend `Arc`, so state persists. fn app(store: &Arc) -> Router { - helios_ui::mount(Router::new(), "9.9.9", Some(Arc::clone(store))) + helios_ui::mount_with_conformance_source( + Router::new(), + "9.9.9", + None, + Some(Arc::clone(store)), + Arc::new(helios_ui::StaticConformanceSource::empty()), + FhirVersion::R4, + ) } async fn body_text(response: axum::response::Response) -> String { @@ -134,9 +141,12 @@ async fn delete_deregisters_a_tenant() { let store = store(); post_form(&store, "id=acme&display_name=Acme").await; + // Provisioning a tenant seeds it with conformance resources (the embedded + // fallback SearchParameters at minimum), so a plain deregister leaves it + // data-discovered. Purge to remove the tenant entirely. let res = app(&store) .oneshot( - Request::delete("/ui/tenants/acme") + Request::delete("/ui/tenants/acme?purge=true") .body(Body::empty()) .unwrap(), ) @@ -144,7 +154,7 @@ async fn delete_deregisters_a_tenant() { .unwrap(); assert_eq!(res.status(), StatusCode::OK); - // Gone from the registry (no data seeded, so it disappears entirely). + // Gone from the registry and its seeded data purged, so it disappears. let (_, rows) = get(&store, "/ui/tenants/rows").await; assert!(!rows.contains(">acme<") && !rows.contains("Acme")); } @@ -152,10 +162,17 @@ async fn delete_deregisters_a_tenant() { #[tokio::test] async fn page_reports_registry_unavailable_without_a_store() { // No storage handle → the page renders the "unavailable" notice, not a crash. - let res = helios_ui::mount(Router::new(), "9.9.9", None) - .oneshot(Request::get("/ui/tenants").body(Body::empty()).unwrap()) - .await - .unwrap(); + let res = helios_ui::mount_with_conformance_source( + Router::new(), + "9.9.9", + None, + None, + Arc::new(helios_ui::StaticConformanceSource::empty()), + FhirVersion::R4, + ) + .oneshot(Request::get("/ui/tenants").body(Body::empty()).unwrap()) + .await + .unwrap(); assert_eq!(res.status(), StatusCode::OK); let html = body_text(res).await; assert!(html.contains("not available")); diff --git a/data/compartment-definitions-r4.json b/data/compartment-definitions-r4.json new file mode 100644 index 000000000..9d0b4e34b --- /dev/null +++ b/data/compartment-definitions-r4.json @@ -0,0 +1,3076 @@ +{ + "resourceType": "Bundle", + "type": "collection", + "entry": [ + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/device", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "device", + "text": { + "status": "generated", + "div": "

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
Accountsubject
Appointmentactor
AppointmentResponseactor
AuditEventagent
ChargeItementerer, performer-actor
Claimprocedure-udi, item-udi, detail-udi, subdetail-udi
Communicationsender, recipient
CommunicationRequestsender, recipient
Compositionauthor
DetectedIssueauthor
DeviceRequestdevice, subject, requester, performer
DeviceUseStatementdevice
DiagnosticReportsubject
DocumentManifestsubject, author
DocumentReferencesubject, author
ExplanationOfBenefitprocedure-udi, item-udi, detail-udi, subdetail-udi
Flagauthor
Groupmember
Invoiceparticipant
Listsubject, source
Mediasubject
MedicationAdministrationdevice
MessageHeadertarget
Observationsubject, device
Provenanceagent
QuestionnaireResponseauthor
RequestGroupauthor
RiskAssessmentperformer
Scheduleactor
ServiceRequestperformer, requester
Specimensubject
SupplyRequestrequester
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "url": "http://hl7.org/fhir/CompartmentDefinition/device", + "version": "4.0.1", + "name": "Base FHIR compartment definition for Device", + "status": "draft", + "experimental": true, + "date": "2019-11-01T09:29:23+11:00", + "publisher": "FHIR Project Team", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://hl7.org/fhir" + } + ] + } + ], + "description": "There is an instance of the device compartment for each Device resource, and the identity of the compartment is the same as the Device. The set of resources associated with a particular device", + "code": "Device", + "search": true, + "resource": [ + { + "code": "Account", + "param": [ + "subject" + ] + }, + { + "code": "ActivityDefinition" + }, + { + "code": "AdverseEvent" + }, + { + "code": "AllergyIntolerance" + }, + { + "code": "Appointment", + "param": [ + "actor" + ] + }, + { + "code": "AppointmentResponse", + "param": [ + "actor" + ] + }, + { + "code": "AuditEvent", + "param": [ + "agent" + ] + }, + { + "code": "Basic" + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BodyStructure" + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan" + }, + { + "code": "CareTeam" + }, + { + "code": "CatalogEntry" + }, + { + "code": "ChargeItem", + "param": [ + "enterer", + "performer-actor" + ] + }, + { + "code": "ChargeItemDefinition" + }, + { + "code": "Claim", + "param": [ + "procedure-udi", + "item-udi", + "detail-udi", + "subdetail-udi" + ] + }, + { + "code": "ClaimResponse" + }, + { + "code": "ClinicalImpression" + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "sender", + "recipient" + ] + }, + { + "code": "CommunicationRequest", + "param": [ + "sender", + "recipient" + ] + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "author" + ] + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition" + }, + { + "code": "Consent" + }, + { + "code": "Contract" + }, + { + "code": "Coverage" + }, + { + "code": "CoverageEligibilityRequest" + }, + { + "code": "CoverageEligibilityResponse" + }, + { + "code": "DetectedIssue", + "param": [ + "author" + ] + }, + { + "code": "Device" + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest", + "param": [ + "device", + "subject", + "requester", + "performer" + ] + }, + { + "code": "DeviceUseStatement", + "param": [ + "device" + ] + }, + { + "code": "DiagnosticReport", + "param": [ + "subject" + ] + }, + { + "code": "DocumentManifest", + "param": [ + "subject", + "author" + ] + }, + { + "code": "DocumentReference", + "param": [ + "subject", + "author" + ] + }, + { + "code": "EffectEvidenceSynthesis" + }, + { + "code": "Encounter" + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest" + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare" + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "procedure-udi", + "item-udi", + "detail-udi", + "subdetail-udi" + ] + }, + { + "code": "FamilyMemberHistory" + }, + { + "code": "Flag", + "param": [ + "author" + ] + }, + { + "code": "Goal" + }, + { + "code": "GraphDefinition" + }, + { + "code": "Group", + "param": [ + "member" + ] + }, + { + "code": "GuidanceResponse" + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingStudy" + }, + { + "code": "Immunization" + }, + { + "code": "ImmunizationEvaluation" + }, + { + "code": "ImmunizationRecommendation" + }, + { + "code": "ImplementationGuide" + }, + { + "code": "InsurancePlan" + }, + { + "code": "Invoice", + "param": [ + "participant" + ] + }, + { + "code": "Library" + }, + { + "code": "Linkage" + }, + { + "code": "List", + "param": [ + "subject", + "source" + ] + }, + { + "code": "Location" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport" + }, + { + "code": "Media", + "param": [ + "subject" + ] + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration", + "param": [ + "device" + ] + }, + { + "code": "MedicationDispense" + }, + { + "code": "MedicationKnowledge" + }, + { + "code": "MedicationRequest" + }, + { + "code": "MedicationStatement" + }, + { + "code": "MedicinalProduct" + }, + { + "code": "MedicinalProductAuthorization" + }, + { + "code": "MedicinalProductContraindication" + }, + { + "code": "MedicinalProductIndication" + }, + { + "code": "MedicinalProductIngredient" + }, + { + "code": "MedicinalProductInteraction" + }, + { + "code": "MedicinalProductManufactured" + }, + { + "code": "MedicinalProductPackaged" + }, + { + "code": "MedicinalProductPharmaceutical" + }, + { + "code": "MedicinalProductUndesirableEffect" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader", + "param": [ + "target" + ] + }, + { + "code": "MolecularSequence" + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionOrder" + }, + { + "code": "Observation", + "param": [ + "subject", + "device" + ] + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation" + }, + { + "code": "Patient" + }, + { + "code": "PaymentNotice" + }, + { + "code": "PaymentReconciliation" + }, + { + "code": "Person" + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner" + }, + { + "code": "PractitionerRole" + }, + { + "code": "Procedure" + }, + { + "code": "Provenance", + "param": [ + "agent" + ] + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "author" + ] + }, + { + "code": "RelatedPerson" + }, + { + "code": "RequestGroup", + "param": [ + "author" + ] + }, + { + "code": "ResearchDefinition" + }, + { + "code": "ResearchElementDefinition" + }, + { + "code": "ResearchStudy" + }, + { + "code": "ResearchSubject" + }, + { + "code": "RiskAssessment", + "param": [ + "performer" + ] + }, + { + "code": "RiskEvidenceSynthesis" + }, + { + "code": "Schedule", + "param": [ + "actor" + ] + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "performer", + "requester" + ] + }, + { + "code": "Slot" + }, + { + "code": "Specimen", + "param": [ + "subject" + ] + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceNucleicAcid" + }, + { + "code": "SubstancePolymer" + }, + { + "code": "SubstanceProtein" + }, + { + "code": "SubstanceReferenceInformation" + }, + { + "code": "SubstanceSourceMaterial" + }, + { + "code": "SubstanceSpecification" + }, + { + "code": "SupplyDelivery" + }, + { + "code": "SupplyRequest", + "param": [ + "requester" + ] + }, + { + "code": "Task" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "TestReport" + }, + { + "code": "TestScript" + }, + { + "code": "ValueSet" + }, + { + "code": "VerificationResult" + }, + { + "code": "VisionPrescription" + } + ] + } + }, + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/encounter", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "encounter", + "text": { + "status": "generated", + "div": "

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
CarePlanencounter
CareTeamencounter
ChargeItemcontext
Claimencounter
ClinicalImpressionencounter
Communicationencounter
CommunicationRequestencounter
Compositionencounter
Conditionencounter
DeviceRequestencounter
DiagnosticReportencounter
DocumentManifestrelated-ref
DocumentReferenceencounter
Encounter{def}
ExplanationOfBenefitencounter
Mediaencounter
MedicationAdministrationcontext
MedicationRequestencounter
NutritionOrderencounter
Observationencounter
Procedureencounter
QuestionnaireResponseencounter
RequestGroupencounter
ServiceRequestencounter
VisionPrescriptionencounter
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "url": "http://hl7.org/fhir/CompartmentDefinition/encounter", + "version": "4.0.1", + "name": "Base FHIR compartment definition for Encounter", + "status": "draft", + "experimental": true, + "date": "2019-11-01T09:29:23+11:00", + "publisher": "FHIR Project Team", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://hl7.org/fhir" + } + ] + } + ], + "description": "There is an instance of the encounter compartment for each encounter resource, and the identity of the compartment is the same as the encounter. The set of resources associated with a particular encounter", + "code": "Encounter", + "search": true, + "resource": [ + { + "code": "Account" + }, + { + "code": "ActivityDefinition" + }, + { + "code": "AdverseEvent" + }, + { + "code": "AllergyIntolerance" + }, + { + "code": "Appointment" + }, + { + "code": "AppointmentResponse" + }, + { + "code": "AuditEvent" + }, + { + "code": "Basic" + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BodyStructure" + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan", + "param": [ + "encounter" + ] + }, + { + "code": "CareTeam", + "param": [ + "encounter" + ] + }, + { + "code": "CatalogEntry" + }, + { + "code": "ChargeItem", + "param": [ + "context" + ] + }, + { + "code": "ChargeItemDefinition" + }, + { + "code": "Claim", + "param": [ + "encounter" + ] + }, + { + "code": "ClaimResponse" + }, + { + "code": "ClinicalImpression", + "param": [ + "encounter" + ] + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "encounter" + ] + }, + { + "code": "CommunicationRequest", + "param": [ + "encounter" + ] + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "encounter" + ] + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition", + "param": [ + "encounter" + ] + }, + { + "code": "Consent" + }, + { + "code": "Contract" + }, + { + "code": "Coverage" + }, + { + "code": "CoverageEligibilityRequest" + }, + { + "code": "CoverageEligibilityResponse" + }, + { + "code": "DetectedIssue" + }, + { + "code": "Device" + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest", + "param": [ + "encounter" + ] + }, + { + "code": "DeviceUseStatement" + }, + { + "code": "DiagnosticReport", + "param": [ + "encounter" + ] + }, + { + "code": "DocumentManifest", + "param": [ + "related-ref" + ] + }, + { + "code": "DocumentReference", + "param": [ + "encounter" + ] + }, + { + "code": "EffectEvidenceSynthesis" + }, + { + "code": "Encounter", + "param": [ + "{def}" + ] + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest" + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare" + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "encounter" + ] + }, + { + "code": "FamilyMemberHistory" + }, + { + "code": "Flag" + }, + { + "code": "Goal" + }, + { + "code": "GraphDefinition" + }, + { + "code": "Group" + }, + { + "code": "GuidanceResponse" + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingStudy" + }, + { + "code": "Immunization" + }, + { + "code": "ImmunizationEvaluation" + }, + { + "code": "ImmunizationRecommendation" + }, + { + "code": "ImplementationGuide" + }, + { + "code": "InsurancePlan" + }, + { + "code": "Invoice" + }, + { + "code": "Library" + }, + { + "code": "Linkage" + }, + { + "code": "List" + }, + { + "code": "Location" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport" + }, + { + "code": "Media", + "param": [ + "encounter" + ] + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration", + "param": [ + "context" + ] + }, + { + "code": "MedicationDispense" + }, + { + "code": "MedicationKnowledge" + }, + { + "code": "MedicationRequest", + "param": [ + "encounter" + ] + }, + { + "code": "MedicationStatement" + }, + { + "code": "MedicinalProduct" + }, + { + "code": "MedicinalProductAuthorization" + }, + { + "code": "MedicinalProductContraindication" + }, + { + "code": "MedicinalProductIndication" + }, + { + "code": "MedicinalProductIngredient" + }, + { + "code": "MedicinalProductInteraction" + }, + { + "code": "MedicinalProductManufactured" + }, + { + "code": "MedicinalProductPackaged" + }, + { + "code": "MedicinalProductPharmaceutical" + }, + { + "code": "MedicinalProductUndesirableEffect" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader" + }, + { + "code": "MolecularSequence" + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionOrder", + "param": [ + "encounter" + ] + }, + { + "code": "Observation", + "param": [ + "encounter" + ] + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation" + }, + { + "code": "Patient" + }, + { + "code": "PaymentNotice" + }, + { + "code": "PaymentReconciliation" + }, + { + "code": "Person" + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner" + }, + { + "code": "PractitionerRole" + }, + { + "code": "Procedure", + "param": [ + "encounter" + ] + }, + { + "code": "Provenance" + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "encounter" + ] + }, + { + "code": "RelatedPerson" + }, + { + "code": "RequestGroup", + "param": [ + "encounter" + ] + }, + { + "code": "ResearchDefinition" + }, + { + "code": "ResearchElementDefinition" + }, + { + "code": "ResearchStudy" + }, + { + "code": "ResearchSubject" + }, + { + "code": "RiskAssessment" + }, + { + "code": "RiskEvidenceSynthesis" + }, + { + "code": "Schedule" + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "encounter" + ] + }, + { + "code": "Slot" + }, + { + "code": "Specimen" + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceNucleicAcid" + }, + { + "code": "SubstancePolymer" + }, + { + "code": "SubstanceProtein" + }, + { + "code": "SubstanceReferenceInformation" + }, + { + "code": "SubstanceSourceMaterial" + }, + { + "code": "SubstanceSpecification" + }, + { + "code": "SupplyDelivery" + }, + { + "code": "SupplyRequest" + }, + { + "code": "Task" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "TestReport" + }, + { + "code": "TestScript" + }, + { + "code": "ValueSet" + }, + { + "code": "VerificationResult" + }, + { + "code": "VisionPrescription", + "param": [ + "encounter" + ] + } + ] + } + }, + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/patient", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "patient", + "text": { + "status": "generated", + "div": "

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
Accountsubject
AdverseEventsubject
AllergyIntolerancepatient, recorder, asserter
Appointmentactor
AppointmentResponseactor
AuditEventpatient
Basicpatient, author
BodyStructurepatient
CarePlanpatient, performer
CareTeampatient, participant
ChargeItemsubject
Claimpatient, payee
ClaimResponsepatient
ClinicalImpressionsubject
Communicationsubject, sender, recipient
CommunicationRequestsubject, sender, recipient, requester
Compositionsubject, author, attester
Conditionpatient, asserter
Consentpatient
Coveragepolicy-holder, subscriber, beneficiary, payor
CoverageEligibilityRequestpatient
CoverageEligibilityResponsepatient
DetectedIssuepatient
DeviceRequestsubject, performer
DeviceUseStatementsubject
DiagnosticReportsubject
DocumentManifestsubject, author, recipient
DocumentReferencesubject, author
Encounterpatient
EnrollmentRequestsubject
EpisodeOfCarepatient
ExplanationOfBenefitpatient, payee
FamilyMemberHistorypatient
Flagpatient
Goalpatient
Groupmember
ImagingStudypatient
Immunizationpatient
ImmunizationEvaluationpatient
ImmunizationRecommendationpatient
Invoicesubject, patient, recipient
Listsubject, source
MeasureReportpatient
Mediasubject
MedicationAdministrationpatient, performer, subject
MedicationDispensesubject, patient, receiver
MedicationRequestsubject
MedicationStatementsubject
MolecularSequencepatient
NutritionOrderpatient
Observationsubject, performer
Patientlink
Personpatient
Procedurepatient, performer
Provenancepatient
QuestionnaireResponsesubject, author
RelatedPersonpatient
RequestGroupsubject, participant
ResearchSubjectindividual
RiskAssessmentsubject
Scheduleactor
ServiceRequestsubject, performer
Specimensubject
SupplyDeliverypatient
SupplyRequestsubject
VisionPrescriptionpatient
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "url": "http://hl7.org/fhir/CompartmentDefinition/patient", + "version": "4.0.1", + "name": "Base FHIR compartment definition for Patient", + "status": "draft", + "experimental": true, + "date": "2019-11-01T09:29:23+11:00", + "publisher": "FHIR Project Team", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://hl7.org/fhir" + } + ] + } + ], + "description": "There is an instance of the patient compartment for each patient resource, and the identity of the compartment is the same as the patient. When a patient is linked to another patient, all the records associated with the linked patient are in the compartment associated with the target of the link.. The set of resources associated with a particular patient", + "code": "Patient", + "search": true, + "resource": [ + { + "code": "Account", + "param": [ + "subject" + ] + }, + { + "code": "ActivityDefinition" + }, + { + "code": "AdverseEvent", + "param": [ + "subject" + ] + }, + { + "code": "AllergyIntolerance", + "param": [ + "patient", + "recorder", + "asserter" + ] + }, + { + "code": "Appointment", + "param": [ + "actor" + ] + }, + { + "code": "AppointmentResponse", + "param": [ + "actor" + ] + }, + { + "code": "AuditEvent", + "param": [ + "patient" + ] + }, + { + "code": "Basic", + "param": [ + "patient", + "author" + ] + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BodyStructure", + "param": [ + "patient" + ] + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan", + "param": [ + "patient", + "performer" + ] + }, + { + "code": "CareTeam", + "param": [ + "patient", + "participant" + ] + }, + { + "code": "CatalogEntry" + }, + { + "code": "ChargeItem", + "param": [ + "subject" + ] + }, + { + "code": "ChargeItemDefinition" + }, + { + "code": "Claim", + "param": [ + "patient", + "payee" + ] + }, + { + "code": "ClaimResponse", + "param": [ + "patient" + ] + }, + { + "code": "ClinicalImpression", + "param": [ + "subject" + ] + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "subject", + "sender", + "recipient" + ] + }, + { + "code": "CommunicationRequest", + "param": [ + "subject", + "sender", + "recipient", + "requester" + ] + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "subject", + "author", + "attester" + ] + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition", + "param": [ + "patient", + "asserter" + ] + }, + { + "code": "Consent", + "param": [ + "patient" + ] + }, + { + "code": "Contract" + }, + { + "code": "Coverage", + "param": [ + "policy-holder", + "subscriber", + "beneficiary", + "payor" + ] + }, + { + "code": "CoverageEligibilityRequest", + "param": [ + "patient" + ] + }, + { + "code": "CoverageEligibilityResponse", + "param": [ + "patient" + ] + }, + { + "code": "DetectedIssue", + "param": [ + "patient" + ] + }, + { + "code": "Device" + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest", + "param": [ + "subject", + "performer" + ] + }, + { + "code": "DeviceUseStatement", + "param": [ + "subject" + ] + }, + { + "code": "DiagnosticReport", + "param": [ + "subject" + ] + }, + { + "code": "DocumentManifest", + "param": [ + "subject", + "author", + "recipient" + ] + }, + { + "code": "DocumentReference", + "param": [ + "subject", + "author" + ] + }, + { + "code": "EffectEvidenceSynthesis" + }, + { + "code": "Encounter", + "param": [ + "patient" + ] + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest", + "param": [ + "subject" + ] + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare", + "param": [ + "patient" + ] + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "patient", + "payee" + ] + }, + { + "code": "FamilyMemberHistory", + "param": [ + "patient" + ] + }, + { + "code": "Flag", + "param": [ + "patient" + ] + }, + { + "code": "Goal", + "param": [ + "patient" + ] + }, + { + "code": "GraphDefinition" + }, + { + "code": "Group", + "param": [ + "member" + ] + }, + { + "code": "GuidanceResponse" + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingStudy", + "param": [ + "patient" + ] + }, + { + "code": "Immunization", + "param": [ + "patient" + ] + }, + { + "code": "ImmunizationEvaluation", + "param": [ + "patient" + ] + }, + { + "code": "ImmunizationRecommendation", + "param": [ + "patient" + ] + }, + { + "code": "ImplementationGuide" + }, + { + "code": "InsurancePlan" + }, + { + "code": "Invoice", + "param": [ + "subject", + "patient", + "recipient" + ] + }, + { + "code": "Library" + }, + { + "code": "Linkage" + }, + { + "code": "List", + "param": [ + "subject", + "source" + ] + }, + { + "code": "Location" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport", + "param": [ + "patient" + ] + }, + { + "code": "Media", + "param": [ + "subject" + ] + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration", + "param": [ + "patient", + "performer", + "subject" + ] + }, + { + "code": "MedicationDispense", + "param": [ + "subject", + "patient", + "receiver" + ] + }, + { + "code": "MedicationKnowledge" + }, + { + "code": "MedicationRequest", + "param": [ + "subject" + ] + }, + { + "code": "MedicationStatement", + "param": [ + "subject" + ] + }, + { + "code": "MedicinalProduct" + }, + { + "code": "MedicinalProductAuthorization" + }, + { + "code": "MedicinalProductContraindication" + }, + { + "code": "MedicinalProductIndication" + }, + { + "code": "MedicinalProductIngredient" + }, + { + "code": "MedicinalProductInteraction" + }, + { + "code": "MedicinalProductManufactured" + }, + { + "code": "MedicinalProductPackaged" + }, + { + "code": "MedicinalProductPharmaceutical" + }, + { + "code": "MedicinalProductUndesirableEffect" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader" + }, + { + "code": "MolecularSequence", + "param": [ + "patient" + ] + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionOrder", + "param": [ + "patient" + ] + }, + { + "code": "Observation", + "param": [ + "subject", + "performer" + ] + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation" + }, + { + "code": "Patient", + "param": [ + "link" + ] + }, + { + "code": "PaymentNotice" + }, + { + "code": "PaymentReconciliation" + }, + { + "code": "Person", + "param": [ + "patient" + ] + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner" + }, + { + "code": "PractitionerRole" + }, + { + "code": "Procedure", + "param": [ + "patient", + "performer" + ] + }, + { + "code": "Provenance", + "param": [ + "patient" + ] + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "subject", + "author" + ] + }, + { + "code": "RelatedPerson", + "param": [ + "patient" + ] + }, + { + "code": "RequestGroup", + "param": [ + "subject", + "participant" + ] + }, + { + "code": "ResearchDefinition" + }, + { + "code": "ResearchElementDefinition" + }, + { + "code": "ResearchStudy" + }, + { + "code": "ResearchSubject", + "param": [ + "individual" + ] + }, + { + "code": "RiskAssessment", + "param": [ + "subject" + ] + }, + { + "code": "RiskEvidenceSynthesis" + }, + { + "code": "Schedule", + "param": [ + "actor" + ] + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "subject", + "performer" + ] + }, + { + "code": "Slot" + }, + { + "code": "Specimen", + "param": [ + "subject" + ] + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceNucleicAcid" + }, + { + "code": "SubstancePolymer" + }, + { + "code": "SubstanceProtein" + }, + { + "code": "SubstanceReferenceInformation" + }, + { + "code": "SubstanceSourceMaterial" + }, + { + "code": "SubstanceSpecification" + }, + { + "code": "SupplyDelivery", + "param": [ + "patient" + ] + }, + { + "code": "SupplyRequest", + "param": [ + "subject" + ] + }, + { + "code": "Task" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "TestReport" + }, + { + "code": "TestScript" + }, + { + "code": "ValueSet" + }, + { + "code": "VerificationResult" + }, + { + "code": "VisionPrescription", + "param": [ + "patient" + ] + } + ] + } + }, + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/practitioner", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "practitioner", + "text": { + "status": "generated", + "div": "

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
Accountsubject
AdverseEventrecorder
AllergyIntolerancerecorder, asserter
Appointmentactor
AppointmentResponseactor
AuditEventagent
Basicauthor
CarePlanperformer
CareTeamparticipant
ChargeItementerer, performer-actor
Claimenterer, provider, payee, care-team
ClaimResponserequestor
ClinicalImpressionassessor
Communicationsender, recipient
CommunicationRequestsender, recipient, requester
Compositionsubject, author, attester
Conditionasserter
CoverageEligibilityRequestenterer, provider
CoverageEligibilityResponserequestor
DetectedIssueauthor
DeviceRequestrequester, performer
DiagnosticReportperformer
DocumentManifestsubject, author, recipient
DocumentReferencesubject, author, authenticator
Encounterpractitioner, participant
EpisodeOfCarecare-manager
ExplanationOfBenefitenterer, provider, payee, care-team
Flagauthor
Groupmember
Immunizationperformer
Invoiceparticipant
Linkageauthor
Listsource
Mediasubject, operator
MedicationAdministrationperformer
MedicationDispenseperformer, receiver
MedicationRequestrequester
MedicationStatementsource
MessageHeaderreceiver, author, responsible, enterer
NutritionOrderprovider
Observationperformer
Patientgeneral-practitioner
PaymentNoticeprovider
PaymentReconciliationrequestor
Personpractitioner
Practitioner{def}
PractitionerRolepractitioner
Procedureperformer
Provenanceagent
QuestionnaireResponseauthor, source
RequestGroupparticipant, author
ResearchStudyprincipalinvestigator
RiskAssessmentperformer
Scheduleactor
ServiceRequestperformer, requester
Specimencollector
SupplyDeliverysupplier, receiver
SupplyRequestrequester
VisionPrescriptionprescriber
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "url": "http://hl7.org/fhir/CompartmentDefinition/practitioner", + "version": "4.0.1", + "name": "Base FHIR compartment definition for Practitioner", + "status": "draft", + "experimental": true, + "date": "2019-11-01T09:29:23+11:00", + "publisher": "FHIR Project Team", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://hl7.org/fhir" + } + ] + } + ], + "description": "There is an instance of the practitioner compartment for each Practitioner resource, and the identity of the compartment is the same as the Practitioner. The set of resources associated with a particular practitioner", + "code": "Practitioner", + "search": true, + "resource": [ + { + "code": "Account", + "param": [ + "subject" + ] + }, + { + "code": "ActivityDefinition" + }, + { + "code": "AdverseEvent", + "param": [ + "recorder" + ] + }, + { + "code": "AllergyIntolerance", + "param": [ + "recorder", + "asserter" + ] + }, + { + "code": "Appointment", + "param": [ + "actor" + ] + }, + { + "code": "AppointmentResponse", + "param": [ + "actor" + ] + }, + { + "code": "AuditEvent", + "param": [ + "agent" + ] + }, + { + "code": "Basic", + "param": [ + "author" + ] + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BodyStructure" + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan", + "param": [ + "performer" + ] + }, + { + "code": "CareTeam", + "param": [ + "participant" + ] + }, + { + "code": "CatalogEntry" + }, + { + "code": "ChargeItem", + "param": [ + "enterer", + "performer-actor" + ] + }, + { + "code": "ChargeItemDefinition" + }, + { + "code": "Claim", + "param": [ + "enterer", + "provider", + "payee", + "care-team" + ] + }, + { + "code": "ClaimResponse", + "param": [ + "requestor" + ] + }, + { + "code": "ClinicalImpression", + "param": [ + "assessor" + ] + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "sender", + "recipient" + ] + }, + { + "code": "CommunicationRequest", + "param": [ + "sender", + "recipient", + "requester" + ] + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "subject", + "author", + "attester" + ] + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition", + "param": [ + "asserter" + ] + }, + { + "code": "Consent" + }, + { + "code": "Contract" + }, + { + "code": "Coverage" + }, + { + "code": "CoverageEligibilityRequest", + "param": [ + "enterer", + "provider" + ] + }, + { + "code": "CoverageEligibilityResponse", + "param": [ + "requestor" + ] + }, + { + "code": "DetectedIssue", + "param": [ + "author" + ] + }, + { + "code": "Device" + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest", + "param": [ + "requester", + "performer" + ] + }, + { + "code": "DeviceUseStatement" + }, + { + "code": "DiagnosticReport", + "param": [ + "performer" + ] + }, + { + "code": "DocumentManifest", + "param": [ + "subject", + "author", + "recipient" + ] + }, + { + "code": "DocumentReference", + "param": [ + "subject", + "author", + "authenticator" + ] + }, + { + "code": "EffectEvidenceSynthesis" + }, + { + "code": "Encounter", + "param": [ + "practitioner", + "participant" + ] + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest" + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare", + "param": [ + "care-manager" + ] + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "enterer", + "provider", + "payee", + "care-team" + ] + }, + { + "code": "FamilyMemberHistory" + }, + { + "code": "Flag", + "param": [ + "author" + ] + }, + { + "code": "Goal" + }, + { + "code": "GraphDefinition" + }, + { + "code": "Group", + "param": [ + "member" + ] + }, + { + "code": "GuidanceResponse" + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingStudy" + }, + { + "code": "Immunization", + "param": [ + "performer" + ] + }, + { + "code": "ImmunizationEvaluation" + }, + { + "code": "ImmunizationRecommendation" + }, + { + "code": "ImplementationGuide" + }, + { + "code": "InsurancePlan" + }, + { + "code": "Invoice", + "param": [ + "participant" + ] + }, + { + "code": "Library" + }, + { + "code": "Linkage", + "param": [ + "author" + ] + }, + { + "code": "List", + "param": [ + "source" + ] + }, + { + "code": "Location" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport" + }, + { + "code": "Media", + "param": [ + "subject", + "operator" + ] + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration", + "param": [ + "performer" + ] + }, + { + "code": "MedicationDispense", + "param": [ + "performer", + "receiver" + ] + }, + { + "code": "MedicationKnowledge" + }, + { + "code": "MedicationRequest", + "param": [ + "requester" + ] + }, + { + "code": "MedicationStatement", + "param": [ + "source" + ] + }, + { + "code": "MedicinalProduct" + }, + { + "code": "MedicinalProductAuthorization" + }, + { + "code": "MedicinalProductContraindication" + }, + { + "code": "MedicinalProductIndication" + }, + { + "code": "MedicinalProductIngredient" + }, + { + "code": "MedicinalProductInteraction" + }, + { + "code": "MedicinalProductManufactured" + }, + { + "code": "MedicinalProductPackaged" + }, + { + "code": "MedicinalProductPharmaceutical" + }, + { + "code": "MedicinalProductUndesirableEffect" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader", + "param": [ + "receiver", + "author", + "responsible", + "enterer" + ] + }, + { + "code": "MolecularSequence" + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionOrder", + "param": [ + "provider" + ] + }, + { + "code": "Observation", + "param": [ + "performer" + ] + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation" + }, + { + "code": "Patient", + "param": [ + "general-practitioner" + ] + }, + { + "code": "PaymentNotice", + "param": [ + "provider" + ] + }, + { + "code": "PaymentReconciliation", + "param": [ + "requestor" + ] + }, + { + "code": "Person", + "param": [ + "practitioner" + ] + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner", + "param": [ + "{def}" + ] + }, + { + "code": "PractitionerRole", + "param": [ + "practitioner" + ] + }, + { + "code": "Procedure", + "param": [ + "performer" + ] + }, + { + "code": "Provenance", + "param": [ + "agent" + ] + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "author", + "source" + ] + }, + { + "code": "RelatedPerson" + }, + { + "code": "RequestGroup", + "param": [ + "participant", + "author" + ] + }, + { + "code": "ResearchDefinition" + }, + { + "code": "ResearchElementDefinition" + }, + { + "code": "ResearchStudy", + "param": [ + "principalinvestigator" + ] + }, + { + "code": "ResearchSubject" + }, + { + "code": "RiskAssessment", + "param": [ + "performer" + ] + }, + { + "code": "RiskEvidenceSynthesis" + }, + { + "code": "Schedule", + "param": [ + "actor" + ] + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "performer", + "requester" + ] + }, + { + "code": "Slot" + }, + { + "code": "Specimen", + "param": [ + "collector" + ] + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceNucleicAcid" + }, + { + "code": "SubstancePolymer" + }, + { + "code": "SubstanceProtein" + }, + { + "code": "SubstanceReferenceInformation" + }, + { + "code": "SubstanceSourceMaterial" + }, + { + "code": "SubstanceSpecification" + }, + { + "code": "SupplyDelivery", + "param": [ + "supplier", + "receiver" + ] + }, + { + "code": "SupplyRequest", + "param": [ + "requester" + ] + }, + { + "code": "Task" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "TestReport" + }, + { + "code": "TestScript" + }, + { + "code": "ValueSet" + }, + { + "code": "VerificationResult" + }, + { + "code": "VisionPrescription", + "param": [ + "prescriber" + ] + } + ] + } + }, + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/relatedPerson", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "relatedPerson", + "text": { + "status": "generated", + "div": "

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
AdverseEventrecorder
AllergyIntoleranceasserter
Appointmentactor
AppointmentResponseactor
Basicauthor
CarePlanperformer
CareTeamparticipant
ChargeItementerer, performer-actor
Claimpayee
Communicationsender, recipient
CommunicationRequestsender, recipient, requester
Compositionauthor
Conditionasserter
Coveragepolicy-holder, subscriber, payor
DocumentManifestauthor, recipient
DocumentReferenceauthor
Encounterparticipant
ExplanationOfBenefitpayee
Invoicerecipient
MedicationAdministrationperformer
MedicationStatementsource
Observationperformer
Patientlink
Personlink
Procedureperformer
Provenanceagent
QuestionnaireResponseauthor, source
RelatedPerson{def}
RequestGroupparticipant
Scheduleactor
ServiceRequestperformer
SupplyRequestrequester
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "url": "http://hl7.org/fhir/CompartmentDefinition/relatedPerson", + "version": "4.0.1", + "name": "Base FHIR compartment definition for RelatedPerson", + "status": "draft", + "experimental": true, + "date": "2019-11-01T09:29:23+11:00", + "publisher": "FHIR Project Team", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://hl7.org/fhir" + } + ] + } + ], + "description": "There is an instance of the relatedPerson compartment for each relatedPerson resource, and the identity of the compartment is the same as the relatedPerson. The set of resources associated with a particular 'related person'", + "code": "RelatedPerson", + "search": true, + "resource": [ + { + "code": "Account" + }, + { + "code": "ActivityDefinition" + }, + { + "code": "AdverseEvent", + "param": [ + "recorder" + ] + }, + { + "code": "AllergyIntolerance", + "param": [ + "asserter" + ] + }, + { + "code": "Appointment", + "param": [ + "actor" + ] + }, + { + "code": "AppointmentResponse", + "param": [ + "actor" + ] + }, + { + "code": "AuditEvent" + }, + { + "code": "Basic", + "param": [ + "author" + ] + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BodyStructure" + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan", + "param": [ + "performer" + ] + }, + { + "code": "CareTeam", + "param": [ + "participant" + ] + }, + { + "code": "CatalogEntry" + }, + { + "code": "ChargeItem", + "param": [ + "enterer", + "performer-actor" + ] + }, + { + "code": "ChargeItemDefinition" + }, + { + "code": "Claim", + "param": [ + "payee" + ] + }, + { + "code": "ClaimResponse" + }, + { + "code": "ClinicalImpression" + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "sender", + "recipient" + ] + }, + { + "code": "CommunicationRequest", + "param": [ + "sender", + "recipient", + "requester" + ] + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "author" + ] + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition", + "param": [ + "asserter" + ] + }, + { + "code": "Consent" + }, + { + "code": "Contract" + }, + { + "code": "Coverage", + "param": [ + "policy-holder", + "subscriber", + "payor" + ] + }, + { + "code": "CoverageEligibilityRequest" + }, + { + "code": "CoverageEligibilityResponse" + }, + { + "code": "DetectedIssue" + }, + { + "code": "Device" + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest" + }, + { + "code": "DeviceUseStatement" + }, + { + "code": "DiagnosticReport" + }, + { + "code": "DocumentManifest", + "param": [ + "author", + "recipient" + ] + }, + { + "code": "DocumentReference", + "param": [ + "author" + ] + }, + { + "code": "EffectEvidenceSynthesis" + }, + { + "code": "Encounter", + "param": [ + "participant" + ] + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest" + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare" + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "payee" + ] + }, + { + "code": "FamilyMemberHistory" + }, + { + "code": "Flag" + }, + { + "code": "Goal" + }, + { + "code": "GraphDefinition" + }, + { + "code": "Group" + }, + { + "code": "GuidanceResponse" + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingStudy" + }, + { + "code": "Immunization" + }, + { + "code": "ImmunizationEvaluation" + }, + { + "code": "ImmunizationRecommendation" + }, + { + "code": "ImplementationGuide" + }, + { + "code": "InsurancePlan" + }, + { + "code": "Invoice", + "param": [ + "recipient" + ] + }, + { + "code": "Library" + }, + { + "code": "Linkage" + }, + { + "code": "List" + }, + { + "code": "Location" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport" + }, + { + "code": "Media" + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration", + "param": [ + "performer" + ] + }, + { + "code": "MedicationDispense" + }, + { + "code": "MedicationKnowledge" + }, + { + "code": "MedicationRequest" + }, + { + "code": "MedicationStatement", + "param": [ + "source" + ] + }, + { + "code": "MedicinalProduct" + }, + { + "code": "MedicinalProductAuthorization" + }, + { + "code": "MedicinalProductContraindication" + }, + { + "code": "MedicinalProductIndication" + }, + { + "code": "MedicinalProductIngredient" + }, + { + "code": "MedicinalProductInteraction" + }, + { + "code": "MedicinalProductManufactured" + }, + { + "code": "MedicinalProductPackaged" + }, + { + "code": "MedicinalProductPharmaceutical" + }, + { + "code": "MedicinalProductUndesirableEffect" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader" + }, + { + "code": "MolecularSequence" + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionOrder" + }, + { + "code": "Observation", + "param": [ + "performer" + ] + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation" + }, + { + "code": "Patient", + "param": [ + "link" + ] + }, + { + "code": "PaymentNotice" + }, + { + "code": "PaymentReconciliation" + }, + { + "code": "Person", + "param": [ + "link" + ] + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner" + }, + { + "code": "PractitionerRole" + }, + { + "code": "Procedure", + "param": [ + "performer" + ] + }, + { + "code": "Provenance", + "param": [ + "agent" + ] + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "author", + "source" + ] + }, + { + "code": "RelatedPerson", + "param": [ + "{def}" + ] + }, + { + "code": "RequestGroup", + "param": [ + "participant" + ] + }, + { + "code": "ResearchDefinition" + }, + { + "code": "ResearchElementDefinition" + }, + { + "code": "ResearchStudy" + }, + { + "code": "ResearchSubject" + }, + { + "code": "RiskAssessment" + }, + { + "code": "RiskEvidenceSynthesis" + }, + { + "code": "Schedule", + "param": [ + "actor" + ] + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "performer" + ] + }, + { + "code": "Slot" + }, + { + "code": "Specimen" + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceNucleicAcid" + }, + { + "code": "SubstancePolymer" + }, + { + "code": "SubstanceProtein" + }, + { + "code": "SubstanceReferenceInformation" + }, + { + "code": "SubstanceSourceMaterial" + }, + { + "code": "SubstanceSpecification" + }, + { + "code": "SupplyDelivery" + }, + { + "code": "SupplyRequest", + "param": [ + "requester" + ] + }, + { + "code": "Task" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "TestReport" + }, + { + "code": "TestScript" + }, + { + "code": "ValueSet" + }, + { + "code": "VerificationResult" + }, + { + "code": "VisionPrescription" + } + ] + } + } + ] +} diff --git a/data/compartment-definitions-r4b.json b/data/compartment-definitions-r4b.json new file mode 100644 index 000000000..ca759cbc9 --- /dev/null +++ b/data/compartment-definitions-r4b.json @@ -0,0 +1,3001 @@ +{ + "resourceType": "Bundle", + "type": "collection", + "entry": [ + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/device", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "device", + "text": { + "status": "extensions", + "div": "

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
Accountsubject
Appointmentactor
AppointmentResponseactor
AuditEventagent
ChargeItementerer, performer-actor
Claimprocedure-udi, item-udi, detail-udi, subdetail-udi
Communicationsender, recipient
CommunicationRequestsender, recipient
Compositionauthor
DetectedIssueauthor
DeviceRequestdevice, subject, requester, performer
DeviceUseStatementdevice
DiagnosticReportsubject
DocumentManifestsubject, author
DocumentReferencesubject, author
ExplanationOfBenefitprocedure-udi, item-udi, detail-udi, subdetail-udi
Flagauthor
Groupmember
Invoiceparticipant
Listsubject, source
Mediasubject
MedicationAdministrationdevice
MessageHeadertarget
Observationsubject, device
Provenanceagent
QuestionnaireResponseauthor
RequestGroupauthor
RiskAssessmentperformer
Scheduleactor
ServiceRequestperformer, requester
Specimensubject
SupplyRequestrequester
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "url": "http://hl7.org/fhir/CompartmentDefinition/device", + "version": "4.3.0", + "name": "Base FHIR compartment definition for Device", + "status": "draft", + "experimental": true, + "date": "2022-05-28T12:47:40+10:00", + "publisher": "FHIR Project Team", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://hl7.org/fhir" + } + ] + } + ], + "description": "There is an instance of the device compartment for each Device resource, and the identity of the compartment is the same as the Device. The set of resources associated with a particular device", + "code": "Device", + "search": true, + "resource": [ + { + "code": "Account", + "param": [ + "subject" + ] + }, + { + "code": "ActivityDefinition" + }, + { + "code": "AdministrableProductDefinition" + }, + { + "code": "AdverseEvent" + }, + { + "code": "AllergyIntolerance" + }, + { + "code": "Appointment", + "param": [ + "actor" + ] + }, + { + "code": "AppointmentResponse", + "param": [ + "actor" + ] + }, + { + "code": "AuditEvent", + "param": [ + "agent" + ] + }, + { + "code": "Basic" + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BodyStructure" + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan" + }, + { + "code": "CareTeam" + }, + { + "code": "CatalogEntry" + }, + { + "code": "ChargeItem", + "param": [ + "enterer", + "performer-actor" + ] + }, + { + "code": "ChargeItemDefinition" + }, + { + "code": "Citation" + }, + { + "code": "Claim", + "param": [ + "procedure-udi", + "item-udi", + "detail-udi", + "subdetail-udi" + ] + }, + { + "code": "ClaimResponse" + }, + { + "code": "ClinicalImpression" + }, + { + "code": "ClinicalUseDefinition" + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "sender", + "recipient" + ] + }, + { + "code": "CommunicationRequest", + "param": [ + "sender", + "recipient" + ] + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "author" + ] + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition" + }, + { + "code": "Consent" + }, + { + "code": "Contract" + }, + { + "code": "Coverage" + }, + { + "code": "CoverageEligibilityRequest" + }, + { + "code": "CoverageEligibilityResponse" + }, + { + "code": "DetectedIssue", + "param": [ + "author" + ] + }, + { + "code": "Device" + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest", + "param": [ + "device", + "subject", + "requester", + "performer" + ] + }, + { + "code": "DeviceUseStatement", + "param": [ + "device" + ] + }, + { + "code": "DiagnosticReport", + "param": [ + "subject" + ] + }, + { + "code": "DocumentManifest", + "param": [ + "subject", + "author" + ] + }, + { + "code": "DocumentReference", + "param": [ + "subject", + "author" + ] + }, + { + "code": "Encounter" + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest" + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare" + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceReport" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "procedure-udi", + "item-udi", + "detail-udi", + "subdetail-udi" + ] + }, + { + "code": "FamilyMemberHistory" + }, + { + "code": "Flag", + "param": [ + "author" + ] + }, + { + "code": "Goal" + }, + { + "code": "GraphDefinition" + }, + { + "code": "Group", + "param": [ + "member" + ] + }, + { + "code": "GuidanceResponse" + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingStudy" + }, + { + "code": "Immunization" + }, + { + "code": "ImmunizationEvaluation" + }, + { + "code": "ImmunizationRecommendation" + }, + { + "code": "ImplementationGuide" + }, + { + "code": "Ingredient" + }, + { + "code": "InsurancePlan" + }, + { + "code": "Invoice", + "param": [ + "participant" + ] + }, + { + "code": "Library" + }, + { + "code": "Linkage" + }, + { + "code": "List", + "param": [ + "subject", + "source" + ] + }, + { + "code": "Location" + }, + { + "code": "ManufacturedItemDefinition" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport" + }, + { + "code": "Media", + "param": [ + "subject" + ] + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration", + "param": [ + "device" + ] + }, + { + "code": "MedicationDispense" + }, + { + "code": "MedicationKnowledge" + }, + { + "code": "MedicationRequest" + }, + { + "code": "MedicationStatement" + }, + { + "code": "MedicinalProductDefinition" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader", + "param": [ + "target" + ] + }, + { + "code": "MolecularSequence" + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionOrder" + }, + { + "code": "NutritionProduct" + }, + { + "code": "Observation", + "param": [ + "subject", + "device" + ] + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation" + }, + { + "code": "PackagedProductDefinition" + }, + { + "code": "Patient" + }, + { + "code": "PaymentNotice" + }, + { + "code": "PaymentReconciliation" + }, + { + "code": "Person" + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner" + }, + { + "code": "PractitionerRole" + }, + { + "code": "Procedure" + }, + { + "code": "Provenance", + "param": [ + "agent" + ] + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "author" + ] + }, + { + "code": "RegulatedAuthorization" + }, + { + "code": "RelatedPerson" + }, + { + "code": "RequestGroup", + "param": [ + "author" + ] + }, + { + "code": "ResearchDefinition" + }, + { + "code": "ResearchElementDefinition" + }, + { + "code": "ResearchStudy" + }, + { + "code": "ResearchSubject" + }, + { + "code": "RiskAssessment", + "param": [ + "performer" + ] + }, + { + "code": "Schedule", + "param": [ + "actor" + ] + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "performer", + "requester" + ] + }, + { + "code": "Slot" + }, + { + "code": "Specimen", + "param": [ + "subject" + ] + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "SubscriptionStatus" + }, + { + "code": "SubscriptionTopic" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceDefinition" + }, + { + "code": "SupplyDelivery" + }, + { + "code": "SupplyRequest", + "param": [ + "requester" + ] + }, + { + "code": "Task" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "TestReport" + }, + { + "code": "TestScript" + }, + { + "code": "ValueSet" + }, + { + "code": "VerificationResult" + }, + { + "code": "VisionPrescription" + } + ] + } + }, + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/encounter", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "encounter", + "text": { + "status": "extensions", + "div": "

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
CarePlanencounter
CareTeamencounter
ChargeItemcontext
Claimencounter
ClinicalImpressionencounter
Communicationencounter
CommunicationRequestencounter
Compositionencounter
Conditionencounter
DeviceRequestencounter
DiagnosticReportencounter
DocumentManifestrelated-ref
DocumentReferenceencounter
Encounter{def}
ExplanationOfBenefitencounter
Mediaencounter
MedicationAdministrationcontext
MedicationRequestencounter
NutritionOrderencounter
Observationencounter
Procedureencounter
QuestionnaireResponseencounter
RequestGroupencounter
ServiceRequestencounter
VisionPrescriptionencounter
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "url": "http://hl7.org/fhir/CompartmentDefinition/encounter", + "version": "4.3.0", + "name": "Base FHIR compartment definition for Encounter", + "status": "draft", + "experimental": true, + "date": "2022-05-28T12:47:40+10:00", + "publisher": "FHIR Project Team", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://hl7.org/fhir" + } + ] + } + ], + "description": "There is an instance of the encounter compartment for each encounter resource, and the identity of the compartment is the same as the encounter. The set of resources associated with a particular encounter", + "code": "Encounter", + "search": true, + "resource": [ + { + "code": "Account" + }, + { + "code": "ActivityDefinition" + }, + { + "code": "AdministrableProductDefinition" + }, + { + "code": "AdverseEvent" + }, + { + "code": "AllergyIntolerance" + }, + { + "code": "Appointment" + }, + { + "code": "AppointmentResponse" + }, + { + "code": "AuditEvent" + }, + { + "code": "Basic" + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BodyStructure" + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan", + "param": [ + "encounter" + ] + }, + { + "code": "CareTeam", + "param": [ + "encounter" + ] + }, + { + "code": "CatalogEntry" + }, + { + "code": "ChargeItem", + "param": [ + "context" + ] + }, + { + "code": "ChargeItemDefinition" + }, + { + "code": "Citation" + }, + { + "code": "Claim", + "param": [ + "encounter" + ] + }, + { + "code": "ClaimResponse" + }, + { + "code": "ClinicalImpression", + "param": [ + "encounter" + ] + }, + { + "code": "ClinicalUseDefinition" + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "encounter" + ] + }, + { + "code": "CommunicationRequest", + "param": [ + "encounter" + ] + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "encounter" + ] + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition", + "param": [ + "encounter" + ] + }, + { + "code": "Consent" + }, + { + "code": "Contract" + }, + { + "code": "Coverage" + }, + { + "code": "CoverageEligibilityRequest" + }, + { + "code": "CoverageEligibilityResponse" + }, + { + "code": "DetectedIssue" + }, + { + "code": "Device" + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest", + "param": [ + "encounter" + ] + }, + { + "code": "DeviceUseStatement" + }, + { + "code": "DiagnosticReport", + "param": [ + "encounter" + ] + }, + { + "code": "DocumentManifest", + "param": [ + "related-ref" + ] + }, + { + "code": "DocumentReference", + "param": [ + "encounter" + ] + }, + { + "code": "Encounter", + "param": [ + "{def}" + ] + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest" + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare" + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceReport" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "encounter" + ] + }, + { + "code": "FamilyMemberHistory" + }, + { + "code": "Flag" + }, + { + "code": "Goal" + }, + { + "code": "GraphDefinition" + }, + { + "code": "Group" + }, + { + "code": "GuidanceResponse" + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingStudy" + }, + { + "code": "Immunization" + }, + { + "code": "ImmunizationEvaluation" + }, + { + "code": "ImmunizationRecommendation" + }, + { + "code": "ImplementationGuide" + }, + { + "code": "Ingredient" + }, + { + "code": "InsurancePlan" + }, + { + "code": "Invoice" + }, + { + "code": "Library" + }, + { + "code": "Linkage" + }, + { + "code": "List" + }, + { + "code": "Location" + }, + { + "code": "ManufacturedItemDefinition" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport" + }, + { + "code": "Media", + "param": [ + "encounter" + ] + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration", + "param": [ + "context" + ] + }, + { + "code": "MedicationDispense" + }, + { + "code": "MedicationKnowledge" + }, + { + "code": "MedicationRequest", + "param": [ + "encounter" + ] + }, + { + "code": "MedicationStatement" + }, + { + "code": "MedicinalProductDefinition" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader" + }, + { + "code": "MolecularSequence" + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionOrder", + "param": [ + "encounter" + ] + }, + { + "code": "NutritionProduct" + }, + { + "code": "Observation", + "param": [ + "encounter" + ] + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation" + }, + { + "code": "PackagedProductDefinition" + }, + { + "code": "Patient" + }, + { + "code": "PaymentNotice" + }, + { + "code": "PaymentReconciliation" + }, + { + "code": "Person" + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner" + }, + { + "code": "PractitionerRole" + }, + { + "code": "Procedure", + "param": [ + "encounter" + ] + }, + { + "code": "Provenance" + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "encounter" + ] + }, + { + "code": "RegulatedAuthorization" + }, + { + "code": "RelatedPerson" + }, + { + "code": "RequestGroup", + "param": [ + "encounter" + ] + }, + { + "code": "ResearchDefinition" + }, + { + "code": "ResearchElementDefinition" + }, + { + "code": "ResearchStudy" + }, + { + "code": "ResearchSubject" + }, + { + "code": "RiskAssessment" + }, + { + "code": "Schedule" + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "encounter" + ] + }, + { + "code": "Slot" + }, + { + "code": "Specimen" + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "SubscriptionStatus" + }, + { + "code": "SubscriptionTopic" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceDefinition" + }, + { + "code": "SupplyDelivery" + }, + { + "code": "SupplyRequest" + }, + { + "code": "Task" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "TestReport" + }, + { + "code": "TestScript" + }, + { + "code": "ValueSet" + }, + { + "code": "VerificationResult" + }, + { + "code": "VisionPrescription", + "param": [ + "encounter" + ] + } + ] + } + }, + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/patient", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "patient", + "text": { + "status": "extensions", + "div": "

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
Accountsubject
AdverseEventsubject
AllergyIntolerancepatient, recorder, asserter
Appointmentactor
AppointmentResponseactor
AuditEventpatient
Basicpatient, author
BodyStructurepatient
CarePlanpatient, performer
CareTeampatient, participant
ChargeItemsubject
Claimpatient, payee
ClaimResponsepatient
ClinicalImpressionsubject
Communicationsubject, sender, recipient
CommunicationRequestsubject, sender, recipient, requester
Compositionsubject, author, attester
Conditionpatient, asserter
Consentpatient
Coveragepolicy-holder, subscriber, beneficiary, payor
CoverageEligibilityRequestpatient
CoverageEligibilityResponsepatient
DetectedIssuepatient
DeviceRequestsubject, performer
DeviceUseStatementsubject
DiagnosticReportsubject
DocumentManifestsubject, author, recipient
DocumentReferencesubject, author
Encounterpatient
EnrollmentRequestsubject
EpisodeOfCarepatient
ExplanationOfBenefitpatient, payee
FamilyMemberHistorypatient
Flagpatient
Goalpatient
Groupmember
ImagingStudypatient
Immunizationpatient
ImmunizationEvaluationpatient
ImmunizationRecommendationpatient
Invoicesubject, patient, recipient
Listsubject, source
MeasureReportpatient
Mediasubject
MedicationAdministrationpatient, performer, subject
MedicationDispensesubject, patient, receiver
MedicationRequestsubject
MedicationStatementsubject
MolecularSequencepatient
NutritionOrderpatient
Observationsubject, performer
Patientlink
Personpatient
Procedurepatient, performer
Provenancepatient
QuestionnaireResponsesubject, author
RelatedPersonpatient
RequestGroupsubject, participant
ResearchSubjectindividual
RiskAssessmentsubject
Scheduleactor
ServiceRequestsubject, performer
Specimensubject
SupplyDeliverypatient
SupplyRequestsubject
VisionPrescriptionpatient
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "url": "http://hl7.org/fhir/CompartmentDefinition/patient", + "version": "4.3.0", + "name": "Base FHIR compartment definition for Patient", + "status": "draft", + "experimental": true, + "date": "2022-05-28T12:47:40+10:00", + "publisher": "FHIR Project Team", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://hl7.org/fhir" + } + ] + } + ], + "description": "There is an instance of the patient compartment for each patient resource, and the identity of the compartment is the same as the patient. When a patient is linked to another patient, all the records associated with the linked patient are in the compartment associated with the target of the link.. The set of resources associated with a particular patient", + "code": "Patient", + "search": true, + "resource": [ + { + "code": "Account", + "param": [ + "subject" + ] + }, + { + "code": "ActivityDefinition" + }, + { + "code": "AdministrableProductDefinition" + }, + { + "code": "AdverseEvent", + "param": [ + "subject" + ] + }, + { + "code": "AllergyIntolerance", + "param": [ + "patient", + "recorder", + "asserter" + ] + }, + { + "code": "Appointment", + "param": [ + "actor" + ] + }, + { + "code": "AppointmentResponse", + "param": [ + "actor" + ] + }, + { + "code": "AuditEvent", + "param": [ + "patient" + ] + }, + { + "code": "Basic", + "param": [ + "patient", + "author" + ] + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BodyStructure", + "param": [ + "patient" + ] + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan", + "param": [ + "patient", + "performer" + ] + }, + { + "code": "CareTeam", + "param": [ + "patient", + "participant" + ] + }, + { + "code": "CatalogEntry" + }, + { + "code": "ChargeItem", + "param": [ + "subject" + ] + }, + { + "code": "ChargeItemDefinition" + }, + { + "code": "Citation" + }, + { + "code": "Claim", + "param": [ + "patient", + "payee" + ] + }, + { + "code": "ClaimResponse", + "param": [ + "patient" + ] + }, + { + "code": "ClinicalImpression", + "param": [ + "subject" + ] + }, + { + "code": "ClinicalUseDefinition" + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "subject", + "sender", + "recipient" + ] + }, + { + "code": "CommunicationRequest", + "param": [ + "subject", + "sender", + "recipient", + "requester" + ] + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "subject", + "author", + "attester" + ] + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition", + "param": [ + "patient", + "asserter" + ] + }, + { + "code": "Consent", + "param": [ + "patient" + ] + }, + { + "code": "Contract" + }, + { + "code": "Coverage", + "param": [ + "policy-holder", + "subscriber", + "beneficiary", + "payor" + ] + }, + { + "code": "CoverageEligibilityRequest", + "param": [ + "patient" + ] + }, + { + "code": "CoverageEligibilityResponse", + "param": [ + "patient" + ] + }, + { + "code": "DetectedIssue", + "param": [ + "patient" + ] + }, + { + "code": "Device" + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest", + "param": [ + "subject", + "performer" + ] + }, + { + "code": "DeviceUseStatement", + "param": [ + "subject" + ] + }, + { + "code": "DiagnosticReport", + "param": [ + "subject" + ] + }, + { + "code": "DocumentManifest", + "param": [ + "subject", + "author", + "recipient" + ] + }, + { + "code": "DocumentReference", + "param": [ + "subject", + "author" + ] + }, + { + "code": "Encounter", + "param": [ + "patient" + ] + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest", + "param": [ + "subject" + ] + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare", + "param": [ + "patient" + ] + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceReport" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "patient", + "payee" + ] + }, + { + "code": "FamilyMemberHistory", + "param": [ + "patient" + ] + }, + { + "code": "Flag", + "param": [ + "patient" + ] + }, + { + "code": "Goal", + "param": [ + "patient" + ] + }, + { + "code": "GraphDefinition" + }, + { + "code": "Group", + "param": [ + "member" + ] + }, + { + "code": "GuidanceResponse" + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingStudy", + "param": [ + "patient" + ] + }, + { + "code": "Immunization", + "param": [ + "patient" + ] + }, + { + "code": "ImmunizationEvaluation", + "param": [ + "patient" + ] + }, + { + "code": "ImmunizationRecommendation", + "param": [ + "patient" + ] + }, + { + "code": "ImplementationGuide" + }, + { + "code": "Ingredient" + }, + { + "code": "InsurancePlan" + }, + { + "code": "Invoice", + "param": [ + "subject", + "patient", + "recipient" + ] + }, + { + "code": "Library" + }, + { + "code": "Linkage" + }, + { + "code": "List", + "param": [ + "subject", + "source" + ] + }, + { + "code": "Location" + }, + { + "code": "ManufacturedItemDefinition" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport", + "param": [ + "patient" + ] + }, + { + "code": "Media", + "param": [ + "subject" + ] + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration", + "param": [ + "patient", + "performer", + "subject" + ] + }, + { + "code": "MedicationDispense", + "param": [ + "subject", + "patient", + "receiver" + ] + }, + { + "code": "MedicationKnowledge" + }, + { + "code": "MedicationRequest", + "param": [ + "subject" + ] + }, + { + "code": "MedicationStatement", + "param": [ + "subject" + ] + }, + { + "code": "MedicinalProductDefinition" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader" + }, + { + "code": "MolecularSequence", + "param": [ + "patient" + ] + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionOrder", + "param": [ + "patient" + ] + }, + { + "code": "NutritionProduct" + }, + { + "code": "Observation", + "param": [ + "subject", + "performer" + ] + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation" + }, + { + "code": "PackagedProductDefinition" + }, + { + "code": "Patient", + "param": [ + "link" + ] + }, + { + "code": "PaymentNotice" + }, + { + "code": "PaymentReconciliation" + }, + { + "code": "Person", + "param": [ + "patient" + ] + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner" + }, + { + "code": "PractitionerRole" + }, + { + "code": "Procedure", + "param": [ + "patient", + "performer" + ] + }, + { + "code": "Provenance", + "param": [ + "patient" + ] + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "subject", + "author" + ] + }, + { + "code": "RegulatedAuthorization" + }, + { + "code": "RelatedPerson", + "param": [ + "patient" + ] + }, + { + "code": "RequestGroup", + "param": [ + "subject", + "participant" + ] + }, + { + "code": "ResearchDefinition" + }, + { + "code": "ResearchElementDefinition" + }, + { + "code": "ResearchStudy" + }, + { + "code": "ResearchSubject", + "param": [ + "individual" + ] + }, + { + "code": "RiskAssessment", + "param": [ + "subject" + ] + }, + { + "code": "Schedule", + "param": [ + "actor" + ] + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "subject", + "performer" + ] + }, + { + "code": "Slot" + }, + { + "code": "Specimen", + "param": [ + "subject" + ] + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "SubscriptionStatus" + }, + { + "code": "SubscriptionTopic" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceDefinition" + }, + { + "code": "SupplyDelivery", + "param": [ + "patient" + ] + }, + { + "code": "SupplyRequest", + "param": [ + "subject" + ] + }, + { + "code": "Task" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "TestReport" + }, + { + "code": "TestScript" + }, + { + "code": "ValueSet" + }, + { + "code": "VerificationResult" + }, + { + "code": "VisionPrescription", + "param": [ + "patient" + ] + } + ] + } + }, + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/practitioner", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "practitioner", + "text": { + "status": "extensions", + "div": "

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
Accountsubject
AdverseEventrecorder
AllergyIntolerancerecorder, asserter
Appointmentactor
AppointmentResponseactor
AuditEventagent
Basicauthor
CarePlanperformer
CareTeamparticipant
ChargeItementerer, performer-actor
Claimenterer, provider, payee, care-team
ClaimResponserequestor
ClinicalImpressionassessor
Communicationsender, recipient
CommunicationRequestsender, recipient, requester
Compositionsubject, author, attester
Conditionasserter
CoverageEligibilityRequestenterer, provider
CoverageEligibilityResponserequestor
DetectedIssueauthor
DeviceRequestrequester, performer
DiagnosticReportperformer
DocumentManifestsubject, author, recipient
DocumentReferencesubject, author, authenticator
Encounterpractitioner, participant
EpisodeOfCarecare-manager
ExplanationOfBenefitenterer, provider, payee, care-team
Flagauthor
Groupmember
Immunizationperformer
Invoiceparticipant
Linkageauthor
Listsource
Mediasubject, operator
MedicationAdministrationperformer
MedicationDispenseperformer, receiver
MedicationRequestrequester
MedicationStatementsource
MessageHeaderreceiver, author, responsible, enterer
NutritionOrderprovider
Observationperformer
Patientgeneral-practitioner
PaymentNoticeprovider
PaymentReconciliationrequestor
Personpractitioner
Practitioner{def}
PractitionerRolepractitioner
Procedureperformer
Provenanceagent
QuestionnaireResponseauthor, source
RequestGroupparticipant, author
ResearchStudyprincipalinvestigator
RiskAssessmentperformer
Scheduleactor
ServiceRequestperformer, requester
Specimencollector
SupplyDeliverysupplier, receiver
SupplyRequestrequester
VisionPrescriptionprescriber
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "url": "http://hl7.org/fhir/CompartmentDefinition/practitioner", + "version": "4.3.0", + "name": "Base FHIR compartment definition for Practitioner", + "status": "draft", + "experimental": true, + "date": "2022-05-28T12:47:40+10:00", + "publisher": "FHIR Project Team", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://hl7.org/fhir" + } + ] + } + ], + "description": "There is an instance of the practitioner compartment for each Practitioner resource, and the identity of the compartment is the same as the Practitioner. The set of resources associated with a particular practitioner", + "code": "Practitioner", + "search": true, + "resource": [ + { + "code": "Account", + "param": [ + "subject" + ] + }, + { + "code": "ActivityDefinition" + }, + { + "code": "AdministrableProductDefinition" + }, + { + "code": "AdverseEvent", + "param": [ + "recorder" + ] + }, + { + "code": "AllergyIntolerance", + "param": [ + "recorder", + "asserter" + ] + }, + { + "code": "Appointment", + "param": [ + "actor" + ] + }, + { + "code": "AppointmentResponse", + "param": [ + "actor" + ] + }, + { + "code": "AuditEvent", + "param": [ + "agent" + ] + }, + { + "code": "Basic", + "param": [ + "author" + ] + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BodyStructure" + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan", + "param": [ + "performer" + ] + }, + { + "code": "CareTeam", + "param": [ + "participant" + ] + }, + { + "code": "CatalogEntry" + }, + { + "code": "ChargeItem", + "param": [ + "enterer", + "performer-actor" + ] + }, + { + "code": "ChargeItemDefinition" + }, + { + "code": "Citation" + }, + { + "code": "Claim", + "param": [ + "enterer", + "provider", + "payee", + "care-team" + ] + }, + { + "code": "ClaimResponse", + "param": [ + "requestor" + ] + }, + { + "code": "ClinicalImpression", + "param": [ + "assessor" + ] + }, + { + "code": "ClinicalUseDefinition" + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "sender", + "recipient" + ] + }, + { + "code": "CommunicationRequest", + "param": [ + "sender", + "recipient", + "requester" + ] + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "subject", + "author", + "attester" + ] + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition", + "param": [ + "asserter" + ] + }, + { + "code": "Consent" + }, + { + "code": "Contract" + }, + { + "code": "Coverage" + }, + { + "code": "CoverageEligibilityRequest", + "param": [ + "enterer", + "provider" + ] + }, + { + "code": "CoverageEligibilityResponse", + "param": [ + "requestor" + ] + }, + { + "code": "DetectedIssue", + "param": [ + "author" + ] + }, + { + "code": "Device" + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest", + "param": [ + "requester", + "performer" + ] + }, + { + "code": "DeviceUseStatement" + }, + { + "code": "DiagnosticReport", + "param": [ + "performer" + ] + }, + { + "code": "DocumentManifest", + "param": [ + "subject", + "author", + "recipient" + ] + }, + { + "code": "DocumentReference", + "param": [ + "subject", + "author", + "authenticator" + ] + }, + { + "code": "Encounter", + "param": [ + "practitioner", + "participant" + ] + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest" + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare", + "param": [ + "care-manager" + ] + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceReport" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "enterer", + "provider", + "payee", + "care-team" + ] + }, + { + "code": "FamilyMemberHistory" + }, + { + "code": "Flag", + "param": [ + "author" + ] + }, + { + "code": "Goal" + }, + { + "code": "GraphDefinition" + }, + { + "code": "Group", + "param": [ + "member" + ] + }, + { + "code": "GuidanceResponse" + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingStudy" + }, + { + "code": "Immunization", + "param": [ + "performer" + ] + }, + { + "code": "ImmunizationEvaluation" + }, + { + "code": "ImmunizationRecommendation" + }, + { + "code": "ImplementationGuide" + }, + { + "code": "Ingredient" + }, + { + "code": "InsurancePlan" + }, + { + "code": "Invoice", + "param": [ + "participant" + ] + }, + { + "code": "Library" + }, + { + "code": "Linkage", + "param": [ + "author" + ] + }, + { + "code": "List", + "param": [ + "source" + ] + }, + { + "code": "Location" + }, + { + "code": "ManufacturedItemDefinition" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport" + }, + { + "code": "Media", + "param": [ + "subject", + "operator" + ] + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration", + "param": [ + "performer" + ] + }, + { + "code": "MedicationDispense", + "param": [ + "performer", + "receiver" + ] + }, + { + "code": "MedicationKnowledge" + }, + { + "code": "MedicationRequest", + "param": [ + "requester" + ] + }, + { + "code": "MedicationStatement", + "param": [ + "source" + ] + }, + { + "code": "MedicinalProductDefinition" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader", + "param": [ + "receiver", + "author", + "responsible", + "enterer" + ] + }, + { + "code": "MolecularSequence" + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionOrder", + "param": [ + "provider" + ] + }, + { + "code": "NutritionProduct" + }, + { + "code": "Observation", + "param": [ + "performer" + ] + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation" + }, + { + "code": "PackagedProductDefinition" + }, + { + "code": "Patient", + "param": [ + "general-practitioner" + ] + }, + { + "code": "PaymentNotice", + "param": [ + "provider" + ] + }, + { + "code": "PaymentReconciliation", + "param": [ + "requestor" + ] + }, + { + "code": "Person", + "param": [ + "practitioner" + ] + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner", + "param": [ + "{def}" + ] + }, + { + "code": "PractitionerRole", + "param": [ + "practitioner" + ] + }, + { + "code": "Procedure", + "param": [ + "performer" + ] + }, + { + "code": "Provenance", + "param": [ + "agent" + ] + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "author", + "source" + ] + }, + { + "code": "RegulatedAuthorization" + }, + { + "code": "RelatedPerson" + }, + { + "code": "RequestGroup", + "param": [ + "participant", + "author" + ] + }, + { + "code": "ResearchDefinition" + }, + { + "code": "ResearchElementDefinition" + }, + { + "code": "ResearchStudy", + "param": [ + "principalinvestigator" + ] + }, + { + "code": "ResearchSubject" + }, + { + "code": "RiskAssessment", + "param": [ + "performer" + ] + }, + { + "code": "Schedule", + "param": [ + "actor" + ] + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "performer", + "requester" + ] + }, + { + "code": "Slot" + }, + { + "code": "Specimen", + "param": [ + "collector" + ] + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "SubscriptionStatus" + }, + { + "code": "SubscriptionTopic" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceDefinition" + }, + { + "code": "SupplyDelivery", + "param": [ + "supplier", + "receiver" + ] + }, + { + "code": "SupplyRequest", + "param": [ + "requester" + ] + }, + { + "code": "Task" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "TestReport" + }, + { + "code": "TestScript" + }, + { + "code": "ValueSet" + }, + { + "code": "VerificationResult" + }, + { + "code": "VisionPrescription", + "param": [ + "prescriber" + ] + } + ] + } + }, + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/relatedPerson", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "relatedPerson", + "text": { + "status": "extensions", + "div": "

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
AdverseEventrecorder
AllergyIntoleranceasserter
Appointmentactor
AppointmentResponseactor
Basicauthor
CarePlanperformer
CareTeamparticipant
ChargeItementerer, performer-actor
Claimpayee
Communicationsender, recipient
CommunicationRequestsender, recipient, requester
Compositionauthor
Conditionasserter
Coveragepolicy-holder, subscriber, payor
DocumentManifestauthor, recipient
DocumentReferenceauthor
Encounterparticipant
ExplanationOfBenefitpayee
Invoicerecipient
MedicationAdministrationperformer
MedicationStatementsource
Observationperformer
Patientlink
Personlink
Procedureperformer
Provenanceagent
QuestionnaireResponseauthor, source
RelatedPerson{def}
RequestGroupparticipant
Scheduleactor
ServiceRequestperformer
SupplyRequestrequester
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "url": "http://hl7.org/fhir/CompartmentDefinition/relatedPerson", + "version": "4.3.0", + "name": "Base FHIR compartment definition for RelatedPerson", + "status": "draft", + "experimental": true, + "date": "2022-05-28T12:47:40+10:00", + "publisher": "FHIR Project Team", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://hl7.org/fhir" + } + ] + } + ], + "description": "There is an instance of the relatedPerson compartment for each relatedPerson resource, and the identity of the compartment is the same as the relatedPerson. The set of resources associated with a particular 'related person'", + "code": "RelatedPerson", + "search": true, + "resource": [ + { + "code": "Account" + }, + { + "code": "ActivityDefinition" + }, + { + "code": "AdministrableProductDefinition" + }, + { + "code": "AdverseEvent", + "param": [ + "recorder" + ] + }, + { + "code": "AllergyIntolerance", + "param": [ + "asserter" + ] + }, + { + "code": "Appointment", + "param": [ + "actor" + ] + }, + { + "code": "AppointmentResponse", + "param": [ + "actor" + ] + }, + { + "code": "AuditEvent" + }, + { + "code": "Basic", + "param": [ + "author" + ] + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BodyStructure" + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan", + "param": [ + "performer" + ] + }, + { + "code": "CareTeam", + "param": [ + "participant" + ] + }, + { + "code": "CatalogEntry" + }, + { + "code": "ChargeItem", + "param": [ + "enterer", + "performer-actor" + ] + }, + { + "code": "ChargeItemDefinition" + }, + { + "code": "Citation" + }, + { + "code": "Claim", + "param": [ + "payee" + ] + }, + { + "code": "ClaimResponse" + }, + { + "code": "ClinicalImpression" + }, + { + "code": "ClinicalUseDefinition" + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "sender", + "recipient" + ] + }, + { + "code": "CommunicationRequest", + "param": [ + "sender", + "recipient", + "requester" + ] + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "author" + ] + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition", + "param": [ + "asserter" + ] + }, + { + "code": "Consent" + }, + { + "code": "Contract" + }, + { + "code": "Coverage", + "param": [ + "policy-holder", + "subscriber", + "payor" + ] + }, + { + "code": "CoverageEligibilityRequest" + }, + { + "code": "CoverageEligibilityResponse" + }, + { + "code": "DetectedIssue" + }, + { + "code": "Device" + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest" + }, + { + "code": "DeviceUseStatement" + }, + { + "code": "DiagnosticReport" + }, + { + "code": "DocumentManifest", + "param": [ + "author", + "recipient" + ] + }, + { + "code": "DocumentReference", + "param": [ + "author" + ] + }, + { + "code": "Encounter", + "param": [ + "participant" + ] + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest" + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare" + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceReport" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "payee" + ] + }, + { + "code": "FamilyMemberHistory" + }, + { + "code": "Flag" + }, + { + "code": "Goal" + }, + { + "code": "GraphDefinition" + }, + { + "code": "Group" + }, + { + "code": "GuidanceResponse" + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingStudy" + }, + { + "code": "Immunization" + }, + { + "code": "ImmunizationEvaluation" + }, + { + "code": "ImmunizationRecommendation" + }, + { + "code": "ImplementationGuide" + }, + { + "code": "Ingredient" + }, + { + "code": "InsurancePlan" + }, + { + "code": "Invoice", + "param": [ + "recipient" + ] + }, + { + "code": "Library" + }, + { + "code": "Linkage" + }, + { + "code": "List" + }, + { + "code": "Location" + }, + { + "code": "ManufacturedItemDefinition" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport" + }, + { + "code": "Media" + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration", + "param": [ + "performer" + ] + }, + { + "code": "MedicationDispense" + }, + { + "code": "MedicationKnowledge" + }, + { + "code": "MedicationRequest" + }, + { + "code": "MedicationStatement", + "param": [ + "source" + ] + }, + { + "code": "MedicinalProductDefinition" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader" + }, + { + "code": "MolecularSequence" + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionOrder" + }, + { + "code": "NutritionProduct" + }, + { + "code": "Observation", + "param": [ + "performer" + ] + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation" + }, + { + "code": "PackagedProductDefinition" + }, + { + "code": "Patient", + "param": [ + "link" + ] + }, + { + "code": "PaymentNotice" + }, + { + "code": "PaymentReconciliation" + }, + { + "code": "Person", + "param": [ + "link" + ] + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner" + }, + { + "code": "PractitionerRole" + }, + { + "code": "Procedure", + "param": [ + "performer" + ] + }, + { + "code": "Provenance", + "param": [ + "agent" + ] + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "author", + "source" + ] + }, + { + "code": "RegulatedAuthorization" + }, + { + "code": "RelatedPerson", + "param": [ + "{def}" + ] + }, + { + "code": "RequestGroup", + "param": [ + "participant" + ] + }, + { + "code": "ResearchDefinition" + }, + { + "code": "ResearchElementDefinition" + }, + { + "code": "ResearchStudy" + }, + { + "code": "ResearchSubject" + }, + { + "code": "RiskAssessment" + }, + { + "code": "Schedule", + "param": [ + "actor" + ] + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "performer" + ] + }, + { + "code": "Slot" + }, + { + "code": "Specimen" + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "SubscriptionStatus" + }, + { + "code": "SubscriptionTopic" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceDefinition" + }, + { + "code": "SupplyDelivery" + }, + { + "code": "SupplyRequest", + "param": [ + "requester" + ] + }, + { + "code": "Task" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "TestReport" + }, + { + "code": "TestScript" + }, + { + "code": "ValueSet" + }, + { + "code": "VerificationResult" + }, + { + "code": "VisionPrescription" + } + ] + } + } + ] +} diff --git a/data/compartment-definitions-r5.json b/data/compartment-definitions-r5.json new file mode 100644 index 000000000..f15eb6e81 --- /dev/null +++ b/data/compartment-definitions-r5.json @@ -0,0 +1,3253 @@ +{ + "resourceType": "Bundle", + "type": "collection", + "entry": [ + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/device", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "device", + "text": { + "status": "extensions", + "div": "

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
Accountsubject
Appointmentactor
AppointmentResponseactor
AuditEventagent
ChargeItementerer, performer-actor
Claimprocedure-udi, item-udi, detail-udi, subdetail-udi
Communicationsender, recipient
CommunicationRequestinformation-provider, recipient
Compositionauthor
DetectedIssueauthor
DeviceAssociationdevice
DeviceRequestsubject, requester, performer
DiagnosticReportsubject
DocumentReferencesubject, author
ExplanationOfBenefitprocedure-udi, item-udi, detail-udi, subdetail-udi
Flagauthor
Groupmember
Invoiceparticipant
Listsubject, source
MessageHeadertarget
Observationsubject, device
Provenanceagent
QuestionnaireResponseauthor
RequestOrchestrationauthor
ResearchSubjectsubject
RiskAssessmentperformer
Scheduleactor
ServiceRequestperformer, requester
Specimensubject
SupplyRequestrequester
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "url": "http://hl7.org/fhir/CompartmentDefinition/device", + "version": "5.0.0", + "name": "Base FHIR compartment definition for Device", + "status": "draft", + "experimental": true, + "date": "2023-03-26T15:21:02+11:00", + "publisher": "FHIR Project Team", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://hl7.org/fhir" + } + ] + } + ], + "description": "There is an instance of the device compartment for each Device resource, and the identity of the compartment is the same as the Device. The set of resources associated with a particular device", + "code": "Device", + "search": true, + "resource": [ + { + "code": "Account", + "param": [ + "subject" + ] + }, + { + "code": "ActivityDefinition" + }, + { + "code": "ActorDefinition" + }, + { + "code": "AdministrableProductDefinition" + }, + { + "code": "AdverseEvent" + }, + { + "code": "AllergyIntolerance" + }, + { + "code": "Appointment", + "param": [ + "actor" + ] + }, + { + "code": "AppointmentResponse", + "param": [ + "actor" + ] + }, + { + "code": "ArtifactAssessment" + }, + { + "code": "AuditEvent", + "param": [ + "agent" + ] + }, + { + "code": "Basic" + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BiologicallyDerivedProductDispense" + }, + { + "code": "BodyStructure" + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan" + }, + { + "code": "CareTeam" + }, + { + "code": "ChargeItem", + "param": [ + "enterer", + "performer-actor" + ] + }, + { + "code": "ChargeItemDefinition" + }, + { + "code": "Citation" + }, + { + "code": "Claim", + "param": [ + "procedure-udi", + "item-udi", + "detail-udi", + "subdetail-udi" + ] + }, + { + "code": "ClaimResponse" + }, + { + "code": "ClinicalImpression" + }, + { + "code": "ClinicalUseDefinition" + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "sender", + "recipient" + ] + }, + { + "code": "CommunicationRequest", + "param": [ + "information-provider", + "recipient" + ] + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "author" + ] + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition" + }, + { + "code": "ConditionDefinition" + }, + { + "code": "Consent" + }, + { + "code": "Contract" + }, + { + "code": "Coverage" + }, + { + "code": "CoverageEligibilityRequest" + }, + { + "code": "CoverageEligibilityResponse" + }, + { + "code": "DetectedIssue", + "param": [ + "author" + ] + }, + { + "code": "Device" + }, + { + "code": "DeviceAssociation", + "param": [ + "device" + ] + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceDispense" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest", + "param": [ + "subject", + "requester", + "performer" + ] + }, + { + "code": "DeviceUsage" + }, + { + "code": "DiagnosticReport", + "param": [ + "subject" + ] + }, + { + "code": "DocumentReference", + "param": [ + "subject", + "author" + ] + }, + { + "code": "Encounter" + }, + { + "code": "EncounterHistory" + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest" + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare" + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceReport" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "procedure-udi", + "item-udi", + "detail-udi", + "subdetail-udi" + ] + }, + { + "code": "FamilyMemberHistory" + }, + { + "code": "Flag", + "param": [ + "author" + ] + }, + { + "code": "FormularyItem" + }, + { + "code": "GenomicStudy" + }, + { + "code": "Goal" + }, + { + "code": "GraphDefinition" + }, + { + "code": "Group", + "param": [ + "member" + ] + }, + { + "code": "GuidanceResponse" + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingSelection" + }, + { + "code": "ImagingStudy" + }, + { + "code": "Immunization" + }, + { + "code": "ImmunizationEvaluation" + }, + { + "code": "ImmunizationRecommendation" + }, + { + "code": "ImplementationGuide" + }, + { + "code": "Ingredient" + }, + { + "code": "InsurancePlan" + }, + { + "code": "InventoryItem" + }, + { + "code": "InventoryReport" + }, + { + "code": "Invoice", + "param": [ + "participant" + ] + }, + { + "code": "Library" + }, + { + "code": "Linkage" + }, + { + "code": "List", + "param": [ + "subject", + "source" + ] + }, + { + "code": "Location" + }, + { + "code": "ManufacturedItemDefinition" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport" + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration" + }, + { + "code": "MedicationDispense" + }, + { + "code": "MedicationKnowledge" + }, + { + "code": "MedicationRequest" + }, + { + "code": "MedicationStatement" + }, + { + "code": "MedicinalProductDefinition" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader", + "param": [ + "target" + ] + }, + { + "code": "MolecularSequence" + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionIntake" + }, + { + "code": "NutritionOrder" + }, + { + "code": "NutritionProduct" + }, + { + "code": "Observation", + "param": [ + "subject", + "device" + ] + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation" + }, + { + "code": "PackagedProductDefinition" + }, + { + "code": "Patient" + }, + { + "code": "PaymentNotice" + }, + { + "code": "PaymentReconciliation" + }, + { + "code": "Permission" + }, + { + "code": "Person" + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner" + }, + { + "code": "PractitionerRole" + }, + { + "code": "Procedure" + }, + { + "code": "Provenance", + "param": [ + "agent" + ] + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "author" + ] + }, + { + "code": "RegulatedAuthorization" + }, + { + "code": "RelatedPerson" + }, + { + "code": "RequestOrchestration", + "param": [ + "author" + ] + }, + { + "code": "Requirements" + }, + { + "code": "ResearchStudy" + }, + { + "code": "ResearchSubject", + "param": [ + "subject" + ] + }, + { + "code": "RiskAssessment", + "param": [ + "performer" + ] + }, + { + "code": "Schedule", + "param": [ + "actor" + ] + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "performer", + "requester" + ] + }, + { + "code": "Slot" + }, + { + "code": "Specimen", + "param": [ + "subject" + ] + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "SubscriptionStatus" + }, + { + "code": "SubscriptionTopic" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceDefinition" + }, + { + "code": "SubstanceNucleicAcid" + }, + { + "code": "SubstancePolymer" + }, + { + "code": "SubstanceProtein" + }, + { + "code": "SubstanceReferenceInformation" + }, + { + "code": "SubstanceSourceMaterial" + }, + { + "code": "SupplyDelivery" + }, + { + "code": "SupplyRequest", + "param": [ + "requester" + ] + }, + { + "code": "Task" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "TestPlan" + }, + { + "code": "TestReport" + }, + { + "code": "TestScript" + }, + { + "code": "Transport" + }, + { + "code": "ValueSet" + }, + { + "code": "VerificationResult" + }, + { + "code": "VisionPrescription" + } + ] + } + }, + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/encounter", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "encounter", + "text": { + "status": "extensions", + "div": "

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
CarePlanencounter
ChargeItemencounter
Claimencounter
ClinicalImpressionencounter
Communicationencounter
CommunicationRequestencounter
Compositionencounter
Conditionencounter
DeviceRequestencounter
DiagnosticReportencounter
DocumentReferencecontext
Encounter{def}
EncounterHistoryencounter
ExplanationOfBenefitencounter
MedicationAdministrationencounter
MedicationDispenseencounter
MedicationRequestencounter
MedicationStatementencounter
NutritionIntakeencounter
NutritionOrderencounter
Observationencounter
Procedureencounter
QuestionnaireResponseencounter
RequestOrchestrationencounter
ServiceRequestencounter
VisionPrescriptionencounter
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "url": "http://hl7.org/fhir/CompartmentDefinition/encounter", + "version": "5.0.0", + "name": "Base FHIR compartment definition for Encounter", + "status": "draft", + "experimental": true, + "date": "2023-03-26T15:21:02+11:00", + "publisher": "FHIR Project Team", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://hl7.org/fhir" + } + ] + } + ], + "description": "There is an instance of the encounter compartment for each encounter resource, and the identity of the compartment is the same as the encounter. The set of resources associated with a particular encounter", + "code": "Encounter", + "search": true, + "resource": [ + { + "code": "Account" + }, + { + "code": "ActivityDefinition" + }, + { + "code": "ActorDefinition" + }, + { + "code": "AdministrableProductDefinition" + }, + { + "code": "AdverseEvent" + }, + { + "code": "AllergyIntolerance" + }, + { + "code": "Appointment" + }, + { + "code": "AppointmentResponse" + }, + { + "code": "ArtifactAssessment" + }, + { + "code": "AuditEvent" + }, + { + "code": "Basic" + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BiologicallyDerivedProductDispense" + }, + { + "code": "BodyStructure" + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan", + "param": [ + "encounter" + ] + }, + { + "code": "CareTeam" + }, + { + "code": "ChargeItem", + "param": [ + "encounter" + ] + }, + { + "code": "ChargeItemDefinition" + }, + { + "code": "Citation" + }, + { + "code": "Claim", + "param": [ + "encounter" + ] + }, + { + "code": "ClaimResponse" + }, + { + "code": "ClinicalImpression", + "param": [ + "encounter" + ] + }, + { + "code": "ClinicalUseDefinition" + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "encounter" + ] + }, + { + "code": "CommunicationRequest", + "param": [ + "encounter" + ] + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "encounter" + ] + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition", + "param": [ + "encounter" + ] + }, + { + "code": "ConditionDefinition" + }, + { + "code": "Consent" + }, + { + "code": "Contract" + }, + { + "code": "Coverage" + }, + { + "code": "CoverageEligibilityRequest" + }, + { + "code": "CoverageEligibilityResponse" + }, + { + "code": "DetectedIssue" + }, + { + "code": "Device" + }, + { + "code": "DeviceAssociation" + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceDispense" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest", + "param": [ + "encounter" + ] + }, + { + "code": "DeviceUsage" + }, + { + "code": "DiagnosticReport", + "param": [ + "encounter" + ] + }, + { + "code": "DocumentReference", + "param": [ + "context" + ] + }, + { + "code": "Encounter", + "param": [ + "{def}" + ] + }, + { + "code": "EncounterHistory", + "param": [ + "encounter" + ] + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest" + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare" + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceReport" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "encounter" + ] + }, + { + "code": "FamilyMemberHistory" + }, + { + "code": "Flag" + }, + { + "code": "FormularyItem" + }, + { + "code": "GenomicStudy" + }, + { + "code": "Goal" + }, + { + "code": "GraphDefinition" + }, + { + "code": "Group" + }, + { + "code": "GuidanceResponse" + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingSelection" + }, + { + "code": "ImagingStudy" + }, + { + "code": "Immunization" + }, + { + "code": "ImmunizationEvaluation" + }, + { + "code": "ImmunizationRecommendation" + }, + { + "code": "ImplementationGuide" + }, + { + "code": "Ingredient" + }, + { + "code": "InsurancePlan" + }, + { + "code": "InventoryItem" + }, + { + "code": "InventoryReport" + }, + { + "code": "Invoice" + }, + { + "code": "Library" + }, + { + "code": "Linkage" + }, + { + "code": "List" + }, + { + "code": "Location" + }, + { + "code": "ManufacturedItemDefinition" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport" + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration", + "param": [ + "encounter" + ] + }, + { + "code": "MedicationDispense", + "param": [ + "encounter" + ] + }, + { + "code": "MedicationKnowledge" + }, + { + "code": "MedicationRequest", + "param": [ + "encounter" + ] + }, + { + "code": "MedicationStatement", + "param": [ + "encounter" + ] + }, + { + "code": "MedicinalProductDefinition" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader" + }, + { + "code": "MolecularSequence" + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionIntake", + "param": [ + "encounter" + ] + }, + { + "code": "NutritionOrder", + "param": [ + "encounter" + ] + }, + { + "code": "NutritionProduct" + }, + { + "code": "Observation", + "param": [ + "encounter" + ] + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation" + }, + { + "code": "PackagedProductDefinition" + }, + { + "code": "Patient" + }, + { + "code": "PaymentNotice" + }, + { + "code": "PaymentReconciliation" + }, + { + "code": "Permission" + }, + { + "code": "Person" + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner" + }, + { + "code": "PractitionerRole" + }, + { + "code": "Procedure", + "param": [ + "encounter" + ] + }, + { + "code": "Provenance" + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "encounter" + ] + }, + { + "code": "RegulatedAuthorization" + }, + { + "code": "RelatedPerson" + }, + { + "code": "RequestOrchestration", + "param": [ + "encounter" + ] + }, + { + "code": "Requirements" + }, + { + "code": "ResearchStudy" + }, + { + "code": "ResearchSubject" + }, + { + "code": "RiskAssessment" + }, + { + "code": "Schedule" + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "encounter" + ] + }, + { + "code": "Slot" + }, + { + "code": "Specimen" + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "SubscriptionStatus" + }, + { + "code": "SubscriptionTopic" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceDefinition" + }, + { + "code": "SubstanceNucleicAcid" + }, + { + "code": "SubstancePolymer" + }, + { + "code": "SubstanceProtein" + }, + { + "code": "SubstanceReferenceInformation" + }, + { + "code": "SubstanceSourceMaterial" + }, + { + "code": "SupplyDelivery" + }, + { + "code": "SupplyRequest" + }, + { + "code": "Task" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "TestPlan" + }, + { + "code": "TestReport" + }, + { + "code": "TestScript" + }, + { + "code": "Transport" + }, + { + "code": "ValueSet" + }, + { + "code": "VerificationResult" + }, + { + "code": "VisionPrescription", + "param": [ + "encounter" + ] + } + ] + } + }, + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/patient", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "patient", + "text": { + "status": "extensions", + "div": "

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
Accountsubject
AdverseEventsubject
AllergyIntolerancepatient, participant
Appointmentactor
AppointmentResponseactor
AuditEventpatient
Basicpatient, author
BiologicallyDerivedProductDispensepatient
BodyStructurepatient
CarePlanpatient
CareTeampatient, participant
ChargeItemsubject
Claimpatient, payee
ClaimResponsepatient
ClinicalImpressionsubject
Communicationsubject, sender, recipient
CommunicationRequestsubject, information-provider, recipient, requester
Compositionsubject, author, attester
Conditionpatient, participant-actor
Consentsubject
Contractpatient
Coveragepolicy-holder, subscriber, beneficiary, paymentby-party
CoverageEligibilityRequestpatient
CoverageEligibilityResponsepatient
DetectedIssuepatient
DeviceAssociationsubject, operator
DeviceRequestsubject, performer
DeviceUsagepatient
DiagnosticReportsubject
DocumentReferencesubject, author
Encounterpatient
EncounterHistorypatient
EnrollmentRequestsubject
EpisodeOfCarepatient
ExplanationOfBenefitpatient, payee
FamilyMemberHistorypatient
Flagpatient
GenomicStudypatient
Goalpatient
Groupmember
GuidanceResponsepatient
ImagingSelectionpatient
ImagingStudypatient
Immunizationpatient
ImmunizationEvaluationpatient
ImmunizationRecommendationpatient
Invoicesubject, patient, recipient
Listsubject, source
MeasureReportpatient
MedicationAdministrationpatient, subject
MedicationDispensesubject, patient, receiver
MedicationRequestsubject
MedicationStatementsubject
MolecularSequencesubject
NutritionIntakesubject, source
NutritionOrderpatient
Observationsubject, performer
Patient{def}, link
Personpatient
Procedurepatient, performer
Provenancepatient
QuestionnaireResponsesubject, author
RelatedPersonpatient
RequestOrchestrationsubject, participant
ResearchSubjectsubject
RiskAssessmentsubject
Scheduleactor
ServiceRequestsubject, performer
Specimensubject
SupplyDeliverypatient
SupplyRequestsubject
Taskpatient, focus
VisionPrescriptionpatient
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "url": "http://hl7.org/fhir/CompartmentDefinition/patient", + "version": "5.0.0", + "name": "Base FHIR compartment definition for Patient", + "status": "draft", + "experimental": true, + "date": "2023-03-26T15:21:02+11:00", + "publisher": "FHIR Project Team", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://hl7.org/fhir" + } + ] + } + ], + "description": "There is an instance of the patient compartment for each patient resource, and the identity of the compartment is the same as the patient. When a patient is linked to another patient resource, the records associated with the linked patient resource will not be returned as part of the compartment search. Those records will be returned only with another compartment search using the \"id\" for the linked patient resource.\n \nIn cases where two patients have been merged rather than linked, associated resources should be moved to the target patient as part of the merge process, so the patient compartment for the target patient would include all relevant data, and the patient compartment for the source patient would include only the linked Patient and possibly remnant resources like AuditEvent.. The set of resources associated with a particular patient", + "code": "Patient", + "search": true, + "resource": [ + { + "code": "Account", + "param": [ + "subject" + ] + }, + { + "code": "ActivityDefinition" + }, + { + "code": "ActorDefinition" + }, + { + "code": "AdministrableProductDefinition" + }, + { + "code": "AdverseEvent", + "param": [ + "subject" + ] + }, + { + "code": "AllergyIntolerance", + "param": [ + "patient", + "participant" + ] + }, + { + "code": "Appointment", + "param": [ + "actor" + ] + }, + { + "code": "AppointmentResponse", + "param": [ + "actor" + ] + }, + { + "code": "ArtifactAssessment" + }, + { + "code": "AuditEvent", + "param": [ + "patient" + ] + }, + { + "code": "Basic", + "param": [ + "patient", + "author" + ] + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BiologicallyDerivedProductDispense", + "param": [ + "patient" + ] + }, + { + "code": "BodyStructure", + "param": [ + "patient" + ] + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan", + "param": [ + "patient" + ] + }, + { + "code": "CareTeam", + "param": [ + "patient", + "participant" + ] + }, + { + "code": "ChargeItem", + "param": [ + "subject" + ] + }, + { + "code": "ChargeItemDefinition" + }, + { + "code": "Citation" + }, + { + "code": "Claim", + "param": [ + "patient", + "payee" + ] + }, + { + "code": "ClaimResponse", + "param": [ + "patient" + ] + }, + { + "code": "ClinicalImpression", + "param": [ + "subject" + ] + }, + { + "code": "ClinicalUseDefinition" + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "subject", + "sender", + "recipient" + ] + }, + { + "code": "CommunicationRequest", + "param": [ + "subject", + "information-provider", + "recipient", + "requester" + ] + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "subject", + "author", + "attester" + ] + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition", + "param": [ + "patient", + "participant-actor" + ] + }, + { + "code": "ConditionDefinition" + }, + { + "code": "Consent", + "param": [ + "subject" + ] + }, + { + "code": "Contract", + "param": [ + "patient" + ] + }, + { + "code": "Coverage", + "param": [ + "policy-holder", + "subscriber", + "beneficiary", + "paymentby-party" + ] + }, + { + "code": "CoverageEligibilityRequest", + "param": [ + "patient" + ] + }, + { + "code": "CoverageEligibilityResponse", + "param": [ + "patient" + ] + }, + { + "code": "DetectedIssue", + "param": [ + "patient" + ] + }, + { + "code": "Device" + }, + { + "code": "DeviceAssociation", + "param": [ + "subject", + "operator" + ] + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceDispense" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest", + "param": [ + "subject", + "performer" + ] + }, + { + "code": "DeviceUsage", + "param": [ + "patient" + ] + }, + { + "code": "DiagnosticReport", + "param": [ + "subject" + ] + }, + { + "code": "DocumentReference", + "param": [ + "subject", + "author" + ] + }, + { + "code": "Encounter", + "param": [ + "patient" + ] + }, + { + "code": "EncounterHistory", + "param": [ + "patient" + ] + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest", + "param": [ + "subject" + ] + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare", + "param": [ + "patient" + ] + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceReport" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "patient", + "payee" + ] + }, + { + "code": "FamilyMemberHistory", + "param": [ + "patient" + ] + }, + { + "code": "Flag", + "param": [ + "patient" + ] + }, + { + "code": "FormularyItem" + }, + { + "code": "GenomicStudy", + "param": [ + "patient" + ] + }, + { + "code": "Goal", + "param": [ + "patient" + ] + }, + { + "code": "GraphDefinition" + }, + { + "code": "Group", + "param": [ + "member" + ] + }, + { + "code": "GuidanceResponse", + "param": [ + "patient" + ] + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingSelection", + "param": [ + "patient" + ] + }, + { + "code": "ImagingStudy", + "param": [ + "patient" + ] + }, + { + "code": "Immunization", + "param": [ + "patient" + ] + }, + { + "code": "ImmunizationEvaluation", + "param": [ + "patient" + ] + }, + { + "code": "ImmunizationRecommendation", + "param": [ + "patient" + ] + }, + { + "code": "ImplementationGuide" + }, + { + "code": "Ingredient" + }, + { + "code": "InsurancePlan" + }, + { + "code": "InventoryItem" + }, + { + "code": "InventoryReport" + }, + { + "code": "Invoice", + "param": [ + "subject", + "patient", + "recipient" + ] + }, + { + "code": "Library" + }, + { + "code": "Linkage" + }, + { + "code": "List", + "param": [ + "subject", + "source" + ] + }, + { + "code": "Location" + }, + { + "code": "ManufacturedItemDefinition" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport", + "param": [ + "patient" + ] + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration", + "param": [ + "patient", + "subject" + ] + }, + { + "code": "MedicationDispense", + "param": [ + "subject", + "patient", + "receiver" + ] + }, + { + "code": "MedicationKnowledge" + }, + { + "code": "MedicationRequest", + "param": [ + "subject" + ] + }, + { + "code": "MedicationStatement", + "param": [ + "subject" + ] + }, + { + "code": "MedicinalProductDefinition" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader" + }, + { + "code": "MolecularSequence", + "param": [ + "subject" + ] + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionIntake", + "param": [ + "subject", + "source" + ] + }, + { + "code": "NutritionOrder", + "param": [ + "patient" + ] + }, + { + "code": "NutritionProduct" + }, + { + "code": "Observation", + "param": [ + "subject", + "performer" + ] + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation" + }, + { + "code": "PackagedProductDefinition" + }, + { + "code": "Patient", + "param": [ + "{def}", + "link" + ] + }, + { + "code": "PaymentNotice" + }, + { + "code": "PaymentReconciliation" + }, + { + "code": "Permission" + }, + { + "code": "Person", + "param": [ + "patient" + ] + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner" + }, + { + "code": "PractitionerRole" + }, + { + "code": "Procedure", + "param": [ + "patient", + "performer" + ] + }, + { + "code": "Provenance", + "param": [ + "patient" + ] + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "subject", + "author" + ] + }, + { + "code": "RegulatedAuthorization" + }, + { + "code": "RelatedPerson", + "param": [ + "patient" + ] + }, + { + "code": "RequestOrchestration", + "param": [ + "subject", + "participant" + ] + }, + { + "code": "Requirements" + }, + { + "code": "ResearchStudy" + }, + { + "code": "ResearchSubject", + "param": [ + "subject" + ] + }, + { + "code": "RiskAssessment", + "param": [ + "subject" + ] + }, + { + "code": "Schedule", + "param": [ + "actor" + ] + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "subject", + "performer" + ] + }, + { + "code": "Slot" + }, + { + "code": "Specimen", + "param": [ + "subject" + ] + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "SubscriptionStatus" + }, + { + "code": "SubscriptionTopic" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceDefinition" + }, + { + "code": "SubstanceNucleicAcid" + }, + { + "code": "SubstancePolymer" + }, + { + "code": "SubstanceProtein" + }, + { + "code": "SubstanceReferenceInformation" + }, + { + "code": "SubstanceSourceMaterial" + }, + { + "code": "SupplyDelivery", + "param": [ + "patient" + ] + }, + { + "code": "SupplyRequest", + "param": [ + "subject" + ] + }, + { + "code": "Task", + "param": [ + "patient", + "focus" + ] + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "TestPlan" + }, + { + "code": "TestReport" + }, + { + "code": "TestScript" + }, + { + "code": "Transport" + }, + { + "code": "ValueSet" + }, + { + "code": "VerificationResult" + }, + { + "code": "VisionPrescription", + "param": [ + "patient" + ] + } + ] + } + }, + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/practitioner", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "practitioner", + "text": { + "status": "extensions", + "div": "

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
Accountsubject
AdverseEventrecorder
AllergyIntoleranceparticipant
Appointmentactor
AppointmentResponseactor
AuditEventagent
Basicauthor
BiologicallyDerivedProductDispenseperformer
CareTeamparticipant
ChargeItementerer, performer-actor
Claimenterer, provider, payee, care-team
ClaimResponserequestor
ClinicalImpressionperformer
Communicationsender, recipient
CommunicationRequestinformation-provider, recipient, requester
Compositionsubject, author, attester
Conditionparticipant-actor
CoverageEligibilityRequestenterer, provider
CoverageEligibilityResponserequestor
DetectedIssueauthor
DeviceAssociationoperator
DeviceRequestrequester, performer
DiagnosticReportperformer
DocumentReferencesubject, author, attester
Encounterpractitioner, participant
EpisodeOfCarecare-manager
ExplanationOfBenefitenterer, provider, payee, care-team
Flagauthor
Groupmember
Immunizationperformer
Invoiceparticipant
Linkageauthor
Listsource
MedicationDispenseperformer, receiver
MedicationRequestrequester
MedicationStatementsource
MessageHeaderreceiver, author, responsible
NutritionIntakesource
NutritionOrderprovider
Observationperformer
Patientgeneral-practitioner
PaymentNoticereporter
PaymentReconciliationrequestor
Personpractitioner
Practitioner{def}
PractitionerRolepractitioner
Procedureperformer
Provenanceagent
QuestionnaireResponseauthor, source
RequestOrchestrationparticipant, author
RiskAssessmentperformer
Scheduleactor
ServiceRequestperformer, requester
Specimencollector
SupplyDeliverysupplier, receiver
SupplyRequestrequester
VisionPrescriptionprescriber
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "url": "http://hl7.org/fhir/CompartmentDefinition/practitioner", + "version": "5.0.0", + "name": "Base FHIR compartment definition for Practitioner", + "status": "draft", + "experimental": true, + "date": "2023-03-26T15:21:02+11:00", + "publisher": "FHIR Project Team", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://hl7.org/fhir" + } + ] + } + ], + "description": "There is an instance of the practitioner compartment for each Practitioner resource, and the identity of the compartment is the same as the Practitioner. The set of resources associated with a particular practitioner", + "code": "Practitioner", + "search": true, + "resource": [ + { + "code": "Account", + "param": [ + "subject" + ] + }, + { + "code": "ActivityDefinition" + }, + { + "code": "ActorDefinition" + }, + { + "code": "AdministrableProductDefinition" + }, + { + "code": "AdverseEvent", + "param": [ + "recorder" + ] + }, + { + "code": "AllergyIntolerance", + "param": [ + "participant" + ] + }, + { + "code": "Appointment", + "param": [ + "actor" + ] + }, + { + "code": "AppointmentResponse", + "param": [ + "actor" + ] + }, + { + "code": "ArtifactAssessment" + }, + { + "code": "AuditEvent", + "param": [ + "agent" + ] + }, + { + "code": "Basic", + "param": [ + "author" + ] + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BiologicallyDerivedProductDispense", + "param": [ + "performer" + ] + }, + { + "code": "BodyStructure" + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan" + }, + { + "code": "CareTeam", + "param": [ + "participant" + ] + }, + { + "code": "ChargeItem", + "param": [ + "enterer", + "performer-actor" + ] + }, + { + "code": "ChargeItemDefinition" + }, + { + "code": "Citation" + }, + { + "code": "Claim", + "param": [ + "enterer", + "provider", + "payee", + "care-team" + ] + }, + { + "code": "ClaimResponse", + "param": [ + "requestor" + ] + }, + { + "code": "ClinicalImpression", + "param": [ + "performer" + ] + }, + { + "code": "ClinicalUseDefinition" + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "sender", + "recipient" + ] + }, + { + "code": "CommunicationRequest", + "param": [ + "information-provider", + "recipient", + "requester" + ] + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "subject", + "author", + "attester" + ] + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition", + "param": [ + "participant-actor" + ] + }, + { + "code": "ConditionDefinition" + }, + { + "code": "Consent" + }, + { + "code": "Contract" + }, + { + "code": "Coverage" + }, + { + "code": "CoverageEligibilityRequest", + "param": [ + "enterer", + "provider" + ] + }, + { + "code": "CoverageEligibilityResponse", + "param": [ + "requestor" + ] + }, + { + "code": "DetectedIssue", + "param": [ + "author" + ] + }, + { + "code": "Device" + }, + { + "code": "DeviceAssociation", + "param": [ + "operator" + ] + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceDispense" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest", + "param": [ + "requester", + "performer" + ] + }, + { + "code": "DeviceUsage" + }, + { + "code": "DiagnosticReport", + "param": [ + "performer" + ] + }, + { + "code": "DocumentReference", + "param": [ + "subject", + "author", + "attester" + ] + }, + { + "code": "Encounter", + "param": [ + "practitioner", + "participant" + ] + }, + { + "code": "EncounterHistory" + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest" + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare", + "param": [ + "care-manager" + ] + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceReport" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "enterer", + "provider", + "payee", + "care-team" + ] + }, + { + "code": "FamilyMemberHistory" + }, + { + "code": "Flag", + "param": [ + "author" + ] + }, + { + "code": "FormularyItem" + }, + { + "code": "GenomicStudy" + }, + { + "code": "Goal" + }, + { + "code": "GraphDefinition" + }, + { + "code": "Group", + "param": [ + "member" + ] + }, + { + "code": "GuidanceResponse" + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingSelection" + }, + { + "code": "ImagingStudy" + }, + { + "code": "Immunization", + "param": [ + "performer" + ] + }, + { + "code": "ImmunizationEvaluation" + }, + { + "code": "ImmunizationRecommendation" + }, + { + "code": "ImplementationGuide" + }, + { + "code": "Ingredient" + }, + { + "code": "InsurancePlan" + }, + { + "code": "InventoryItem" + }, + { + "code": "InventoryReport" + }, + { + "code": "Invoice", + "param": [ + "participant" + ] + }, + { + "code": "Library" + }, + { + "code": "Linkage", + "param": [ + "author" + ] + }, + { + "code": "List", + "param": [ + "source" + ] + }, + { + "code": "Location" + }, + { + "code": "ManufacturedItemDefinition" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport" + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration" + }, + { + "code": "MedicationDispense", + "param": [ + "performer", + "receiver" + ] + }, + { + "code": "MedicationKnowledge" + }, + { + "code": "MedicationRequest", + "param": [ + "requester" + ] + }, + { + "code": "MedicationStatement", + "param": [ + "source" + ] + }, + { + "code": "MedicinalProductDefinition" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader", + "param": [ + "receiver", + "author", + "responsible" + ] + }, + { + "code": "MolecularSequence" + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionIntake", + "param": [ + "source" + ] + }, + { + "code": "NutritionOrder", + "param": [ + "provider" + ] + }, + { + "code": "NutritionProduct" + }, + { + "code": "Observation", + "param": [ + "performer" + ] + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation" + }, + { + "code": "PackagedProductDefinition" + }, + { + "code": "Patient", + "param": [ + "general-practitioner" + ] + }, + { + "code": "PaymentNotice", + "param": [ + "reporter" + ] + }, + { + "code": "PaymentReconciliation", + "param": [ + "requestor" + ] + }, + { + "code": "Permission" + }, + { + "code": "Person", + "param": [ + "practitioner" + ] + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner", + "param": [ + "{def}" + ] + }, + { + "code": "PractitionerRole", + "param": [ + "practitioner" + ] + }, + { + "code": "Procedure", + "param": [ + "performer" + ] + }, + { + "code": "Provenance", + "param": [ + "agent" + ] + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "author", + "source" + ] + }, + { + "code": "RegulatedAuthorization" + }, + { + "code": "RelatedPerson" + }, + { + "code": "RequestOrchestration", + "param": [ + "participant", + "author" + ] + }, + { + "code": "Requirements" + }, + { + "code": "ResearchStudy" + }, + { + "code": "ResearchSubject" + }, + { + "code": "RiskAssessment", + "param": [ + "performer" + ] + }, + { + "code": "Schedule", + "param": [ + "actor" + ] + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "performer", + "requester" + ] + }, + { + "code": "Slot" + }, + { + "code": "Specimen", + "param": [ + "collector" + ] + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "SubscriptionStatus" + }, + { + "code": "SubscriptionTopic" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceDefinition" + }, + { + "code": "SubstanceNucleicAcid" + }, + { + "code": "SubstancePolymer" + }, + { + "code": "SubstanceProtein" + }, + { + "code": "SubstanceReferenceInformation" + }, + { + "code": "SubstanceSourceMaterial" + }, + { + "code": "SupplyDelivery", + "param": [ + "supplier", + "receiver" + ] + }, + { + "code": "SupplyRequest", + "param": [ + "requester" + ] + }, + { + "code": "Task" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "TestPlan" + }, + { + "code": "TestReport" + }, + { + "code": "TestScript" + }, + { + "code": "Transport" + }, + { + "code": "ValueSet" + }, + { + "code": "VerificationResult" + }, + { + "code": "VisionPrescription", + "param": [ + "prescriber" + ] + } + ] + } + }, + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/relatedPerson", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "relatedPerson", + "text": { + "status": "extensions", + "div": "

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
AdverseEventrecorder
AllergyIntoleranceparticipant
Appointmentactor
AppointmentResponseactor
Basicauthor
CareTeamparticipant
ChargeItementerer, performer-actor
Claimpayee
Communicationsender, recipient
CommunicationRequestinformation-provider, recipient, requester
Compositionauthor
Conditionparticipant-actor
Coveragepolicy-holder, subscriber, paymentby-party
DocumentReferenceauthor
Encounterparticipant
ExplanationOfBenefitpayee
Invoicerecipient
MedicationStatementsource
NutritionIntakesource
Observationperformer
Patientlink
Personlink
Procedureperformer
Provenanceagent
QuestionnaireResponseauthor, source
RelatedPerson{def}
RequestOrchestrationparticipant
Scheduleactor
ServiceRequestperformer
SupplyRequestrequester
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "url": "http://hl7.org/fhir/CompartmentDefinition/relatedPerson", + "version": "5.0.0", + "name": "Base FHIR compartment definition for RelatedPerson", + "status": "draft", + "experimental": true, + "date": "2023-03-26T15:21:02+11:00", + "publisher": "FHIR Project Team", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://hl7.org/fhir" + } + ] + } + ], + "description": "There is an instance of the relatedPerson compartment for each relatedPerson resource, and the identity of the compartment is the same as the relatedPerson. The set of resources associated with a particular 'related person'", + "code": "RelatedPerson", + "search": true, + "resource": [ + { + "code": "Account" + }, + { + "code": "ActivityDefinition" + }, + { + "code": "ActorDefinition" + }, + { + "code": "AdministrableProductDefinition" + }, + { + "code": "AdverseEvent", + "param": [ + "recorder" + ] + }, + { + "code": "AllergyIntolerance", + "param": [ + "participant" + ] + }, + { + "code": "Appointment", + "param": [ + "actor" + ] + }, + { + "code": "AppointmentResponse", + "param": [ + "actor" + ] + }, + { + "code": "ArtifactAssessment" + }, + { + "code": "AuditEvent" + }, + { + "code": "Basic", + "param": [ + "author" + ] + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BiologicallyDerivedProductDispense" + }, + { + "code": "BodyStructure" + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan" + }, + { + "code": "CareTeam", + "param": [ + "participant" + ] + }, + { + "code": "ChargeItem", + "param": [ + "enterer", + "performer-actor" + ] + }, + { + "code": "ChargeItemDefinition" + }, + { + "code": "Citation" + }, + { + "code": "Claim", + "param": [ + "payee" + ] + }, + { + "code": "ClaimResponse" + }, + { + "code": "ClinicalImpression" + }, + { + "code": "ClinicalUseDefinition" + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "sender", + "recipient" + ] + }, + { + "code": "CommunicationRequest", + "param": [ + "information-provider", + "recipient", + "requester" + ] + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "author" + ] + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition", + "param": [ + "participant-actor" + ] + }, + { + "code": "ConditionDefinition" + }, + { + "code": "Consent" + }, + { + "code": "Contract" + }, + { + "code": "Coverage", + "param": [ + "policy-holder", + "subscriber", + "paymentby-party" + ] + }, + { + "code": "CoverageEligibilityRequest" + }, + { + "code": "CoverageEligibilityResponse" + }, + { + "code": "DetectedIssue" + }, + { + "code": "Device" + }, + { + "code": "DeviceAssociation" + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceDispense" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest" + }, + { + "code": "DeviceUsage" + }, + { + "code": "DiagnosticReport" + }, + { + "code": "DocumentReference", + "param": [ + "author" + ] + }, + { + "code": "Encounter", + "param": [ + "participant" + ] + }, + { + "code": "EncounterHistory" + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest" + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare" + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceReport" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "payee" + ] + }, + { + "code": "FamilyMemberHistory" + }, + { + "code": "Flag" + }, + { + "code": "FormularyItem" + }, + { + "code": "GenomicStudy" + }, + { + "code": "Goal" + }, + { + "code": "GraphDefinition" + }, + { + "code": "Group" + }, + { + "code": "GuidanceResponse" + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingSelection" + }, + { + "code": "ImagingStudy" + }, + { + "code": "Immunization" + }, + { + "code": "ImmunizationEvaluation" + }, + { + "code": "ImmunizationRecommendation" + }, + { + "code": "ImplementationGuide" + }, + { + "code": "Ingredient" + }, + { + "code": "InsurancePlan" + }, + { + "code": "InventoryItem" + }, + { + "code": "InventoryReport" + }, + { + "code": "Invoice", + "param": [ + "recipient" + ] + }, + { + "code": "Library" + }, + { + "code": "Linkage" + }, + { + "code": "List" + }, + { + "code": "Location" + }, + { + "code": "ManufacturedItemDefinition" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport" + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration" + }, + { + "code": "MedicationDispense" + }, + { + "code": "MedicationKnowledge" + }, + { + "code": "MedicationRequest" + }, + { + "code": "MedicationStatement", + "param": [ + "source" + ] + }, + { + "code": "MedicinalProductDefinition" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader" + }, + { + "code": "MolecularSequence" + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionIntake", + "param": [ + "source" + ] + }, + { + "code": "NutritionOrder" + }, + { + "code": "NutritionProduct" + }, + { + "code": "Observation", + "param": [ + "performer" + ] + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation" + }, + { + "code": "PackagedProductDefinition" + }, + { + "code": "Patient", + "param": [ + "link" + ] + }, + { + "code": "PaymentNotice" + }, + { + "code": "PaymentReconciliation" + }, + { + "code": "Permission" + }, + { + "code": "Person", + "param": [ + "link" + ] + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner" + }, + { + "code": "PractitionerRole" + }, + { + "code": "Procedure", + "param": [ + "performer" + ] + }, + { + "code": "Provenance", + "param": [ + "agent" + ] + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "author", + "source" + ] + }, + { + "code": "RegulatedAuthorization" + }, + { + "code": "RelatedPerson", + "param": [ + "{def}" + ] + }, + { + "code": "RequestOrchestration", + "param": [ + "participant" + ] + }, + { + "code": "Requirements" + }, + { + "code": "ResearchStudy" + }, + { + "code": "ResearchSubject" + }, + { + "code": "RiskAssessment" + }, + { + "code": "Schedule", + "param": [ + "actor" + ] + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "performer" + ] + }, + { + "code": "Slot" + }, + { + "code": "Specimen" + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "SubscriptionStatus" + }, + { + "code": "SubscriptionTopic" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceDefinition" + }, + { + "code": "SubstanceNucleicAcid" + }, + { + "code": "SubstancePolymer" + }, + { + "code": "SubstanceProtein" + }, + { + "code": "SubstanceReferenceInformation" + }, + { + "code": "SubstanceSourceMaterial" + }, + { + "code": "SupplyDelivery" + }, + { + "code": "SupplyRequest", + "param": [ + "requester" + ] + }, + { + "code": "Task" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "TestPlan" + }, + { + "code": "TestReport" + }, + { + "code": "TestScript" + }, + { + "code": "Transport" + }, + { + "code": "ValueSet" + }, + { + "code": "VerificationResult" + }, + { + "code": "VisionPrescription" + } + ] + } + } + ] +} diff --git a/data/compartment-definitions-r6.json b/data/compartment-definitions-r6.json new file mode 100644 index 000000000..0dece1963 --- /dev/null +++ b/data/compartment-definitions-r6.json @@ -0,0 +1,4051 @@ +{ + "resourceType": "Bundle", + "type": "collection", + "entry": [ + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/device", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "device", + "text": { + "status": "generated", + "div": "

Generated Narrative: CompartmentDefinition device

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
Accountsubject
Appointmentactor
AppointmentResponseactor
AuditEventagent
Claimprocedure-udi, item-udi, detail-udi, subdetail-udi
Communicationsender, recipient
CommunicationRequestinformation-provider, recipient
Compositionauthor
DetectedIssueauthor
Device{def}
DeviceAlertsubject, device, annunciator-device, acknowledged-by
DeviceAssociationdevice, focus
DeviceMetricdevice
DeviceRequestsubject, requester, performer, device
DiagnosticReportsubject
DocumentReferencesubject, author
ExplanationOfBenefitprocedure-udi, item-udi, detail-udi, subdetail-udi
Flagauthor
Groupmember
Invoiceparticipant
Listsubject, source
MessageHeaderreceiver
Observationsubject, device
Provenanceagent
QuestionnaireResponseauthor
RequestOrchestrationauthor
ResearchSubjectsubject
RiskAssessmentperformer
Scheduleactor
ServiceRequestperformer, requester
Specimensubject
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-wg", + "valueCode": "fhir" + } + ], + "url": "http://hl7.org/fhir/CompartmentDefinition/device", + "version": "6.0.0-ballot3", + "name": "Base FHIR compartment definition for Device", + "status": "draft", + "experimental": false, + "date": "2025-12-17T09:50:17+00:00", + "publisher": "HL7 International / FHIR Infrastructure", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://www.hl7.org/Special/committees/fiwg" + } + ] + } + ], + "description": "There is an instance of the device compartment for each Device resource, and the identity of the compartment is the same as the Device. The set of resources associated with a particular device", + "code": "Device", + "search": true, + "resource": [ + { + "code": "Account", + "param": [ + "subject" + ], + "startParam": "period", + "endParam": "period" + }, + { + "code": "ActivityDefinition" + }, + { + "code": "ActorDefinition" + }, + { + "code": "AdministrableProductDefinition" + }, + { + "code": "AdverseEvent", + "startParam": "occurrence", + "endParam": "occurrence" + }, + { + "code": "AllergyIntolerance", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Appointment", + "param": [ + "actor" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "AppointmentResponse", + "param": [ + "actor" + ] + }, + { + "code": "ArtifactAssessment", + "startParam": "date", + "endParam": "date" + }, + { + "code": "AuditEvent", + "param": [ + "agent" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Basic", + "startParam": "created", + "endParam": "created" + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BodyStructure" + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan", + "startParam": "date", + "endParam": "date" + }, + { + "code": "CareTeam", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Claim", + "param": [ + "procedure-udi", + "item-udi", + "detail-udi", + "subdetail-udi" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "ClaimResponse", + "startParam": "created", + "endParam": "created" + }, + { + "code": "ClinicalUseDefinition" + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "sender", + "recipient" + ], + "startParam": "sent", + "endParam": "sent" + }, + { + "code": "CommunicationRequest", + "param": [ + "information-provider", + "recipient" + ], + "startParam": "authored", + "endParam": "authored" + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "author" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition" + }, + { + "code": "Consent", + "startParam": "period", + "endParam": "period" + }, + { + "code": "Contract" + }, + { + "code": "Coverage", + "startParam": "period", + "endParam": "period" + }, + { + "code": "CoverageEligibilityRequest", + "startParam": "created", + "endParam": "created" + }, + { + "code": "CoverageEligibilityResponse", + "startParam": "created", + "endParam": "created" + }, + { + "code": "DetectedIssue", + "param": [ + "author" + ], + "startParam": "identified", + "endParam": "identified" + }, + { + "code": "Device", + "param": [ + "{def}" + ] + }, + { + "code": "DeviceAlert", + "param": [ + "subject", + "device", + "annunciator-device", + "acknowledged-by" + ], + "startParam": "occurrence", + "endParam": "occurrence" + }, + { + "code": "DeviceAssociation", + "param": [ + "device", + "focus" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceMetric", + "param": [ + "device" + ] + }, + { + "code": "DeviceRequest", + "param": [ + "subject", + "requester", + "performer", + "device" + ], + "startParam": "event-date", + "endParam": "event-date" + }, + { + "code": "DiagnosticReport", + "param": [ + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "DocumentReference", + "param": [ + "subject", + "author" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Encounter", + "startParam": "date-start", + "endParam": "date-start" + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest" + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare", + "startParam": "date", + "endParam": "date" + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "procedure-udi", + "item-udi", + "detail-udi", + "subdetail-udi" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "FamilyMemberHistory", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Flag", + "param": [ + "author" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Goal", + "startParam": "start-date", + "endParam": "start-date" + }, + { + "code": "Group", + "param": [ + "member" + ] + }, + { + "code": "GuidanceResponse" + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingSelection", + "startParam": "issued", + "endParam": "issued" + }, + { + "code": "ImagingStudy", + "startParam": "started", + "endParam": "started" + }, + { + "code": "Immunization", + "startParam": "date", + "endParam": "date" + }, + { + "code": "ImplementationGuide" + }, + { + "code": "Ingredient" + }, + { + "code": "InsurancePlan" + }, + { + "code": "InsuranceProduct" + }, + { + "code": "Invoice", + "param": [ + "participant" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Library" + }, + { + "code": "List", + "param": [ + "subject", + "source" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Location" + }, + { + "code": "ManufacturedItemDefinition" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration", + "startParam": "date", + "endParam": "date" + }, + { + "code": "MedicationDispense", + "startParam": "whenhandedover", + "endParam": "whenhandedover" + }, + { + "code": "MedicationRequest", + "startParam": "combo-date", + "endParam": "combo-date" + }, + { + "code": "MedicationStatement", + "startParam": "effective", + "endParam": "effective" + }, + { + "code": "MedicinalProductDefinition" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader", + "param": [ + "receiver" + ] + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionIntake", + "startParam": "date", + "endParam": "date" + }, + { + "code": "NutritionOrder", + "startParam": "datetime", + "endParam": "datetime" + }, + { + "code": "NutritionProduct" + }, + { + "code": "Observation", + "param": [ + "subject", + "device" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation", + "startParam": "date", + "endParam": "date" + }, + { + "code": "PackagedProductDefinition" + }, + { + "code": "Patient" + }, + { + "code": "PaymentNotice", + "startParam": "created", + "endParam": "created" + }, + { + "code": "PaymentReconciliation", + "startParam": "created", + "endParam": "created" + }, + { + "code": "Person" + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner" + }, + { + "code": "PractitionerRole", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Procedure", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Provenance", + "param": [ + "agent" + ], + "startParam": "when", + "endParam": "when" + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "author" + ], + "startParam": "answer-date", + "endParam": "answer-date" + }, + { + "code": "RegulatedAuthorization" + }, + { + "code": "RelatedPerson" + }, + { + "code": "RequestOrchestration", + "param": [ + "author" + ] + }, + { + "code": "Requirements" + }, + { + "code": "ResearchStudy", + "startParam": "date", + "endParam": "date" + }, + { + "code": "ResearchSubject", + "param": [ + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "RiskAssessment", + "param": [ + "performer" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Schedule", + "param": [ + "actor" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "performer", + "requester" + ], + "startParam": "occurrence", + "endParam": "occurrence" + }, + { + "code": "Slot" + }, + { + "code": "Specimen", + "param": [ + "subject" + ], + "startParam": "collected", + "endParam": "collected" + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "SubscriptionStatus" + }, + { + "code": "SubscriptionTopic" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceDefinition" + }, + { + "code": "Task", + "startParam": "period", + "endParam": "period" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "ValueSet" + }, + { + "code": "VisionPrescription", + "startParam": "datewritten", + "endParam": "datewritten" + } + ] + } + }, + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/encounter", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "encounter", + "text": { + "status": "generated", + "div": "

Generated Narrative: CompartmentDefinition encounter

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
CarePlanencounter
Claimencounter
Communicationencounter
CommunicationRequestencounter
Compositionencounter
Conditionencounter
DeviceAlertencounter
DeviceRequestencounter
DiagnosticReportencounter
DocumentReferencecontext
Encounter{def}
ExplanationOfBenefitencounter
MedicationAdministrationencounter
MedicationDispenseencounter
MedicationRequestencounter
MedicationStatementencounter
NutritionIntakeencounter
NutritionOrderencounter
Observationencounter
Procedureencounter
QuestionnaireResponseencounter
RequestOrchestrationencounter
ServiceRequestencounter
VisionPrescriptionencounter
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-wg", + "valueCode": "fhir" + } + ], + "url": "http://hl7.org/fhir/CompartmentDefinition/encounter", + "version": "6.0.0-ballot3", + "name": "Base FHIR compartment definition for Encounter", + "status": "draft", + "experimental": false, + "date": "2025-12-17T09:50:17+00:00", + "publisher": "HL7 International / FHIR Infrastructure", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://www.hl7.org/Special/committees/fiwg" + } + ] + } + ], + "description": "There is an instance of the encounter compartment for each encounter resource, and the identity of the compartment is the same as the encounter. The set of resources associated with a particular encounter", + "code": "Encounter", + "search": true, + "resource": [ + { + "code": "Account", + "startParam": "period", + "endParam": "period" + }, + { + "code": "ActivityDefinition" + }, + { + "code": "ActorDefinition" + }, + { + "code": "AdministrableProductDefinition" + }, + { + "code": "AdverseEvent", + "startParam": "occurrence", + "endParam": "occurrence" + }, + { + "code": "AllergyIntolerance", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Appointment", + "startParam": "date", + "endParam": "date" + }, + { + "code": "AppointmentResponse" + }, + { + "code": "ArtifactAssessment", + "startParam": "date", + "endParam": "date" + }, + { + "code": "AuditEvent", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Basic", + "startParam": "created", + "endParam": "created" + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BodyStructure" + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan", + "param": [ + "encounter" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "CareTeam", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Claim", + "param": [ + "encounter" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "ClaimResponse", + "startParam": "created", + "endParam": "created" + }, + { + "code": "ClinicalUseDefinition" + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "encounter" + ], + "startParam": "sent", + "endParam": "sent" + }, + { + "code": "CommunicationRequest", + "param": [ + "encounter" + ], + "startParam": "authored", + "endParam": "authored" + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "encounter" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition", + "param": [ + "encounter" + ] + }, + { + "code": "Consent", + "startParam": "period", + "endParam": "period" + }, + { + "code": "Contract" + }, + { + "code": "Coverage", + "startParam": "period", + "endParam": "period" + }, + { + "code": "CoverageEligibilityRequest", + "startParam": "created", + "endParam": "created" + }, + { + "code": "CoverageEligibilityResponse", + "startParam": "created", + "endParam": "created" + }, + { + "code": "DetectedIssue", + "startParam": "identified", + "endParam": "identified" + }, + { + "code": "Device" + }, + { + "code": "DeviceAlert", + "param": [ + "encounter" + ], + "startParam": "occurrence", + "endParam": "occurrence" + }, + { + "code": "DeviceAssociation", + "startParam": "date", + "endParam": "date" + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest", + "param": [ + "encounter" + ], + "startParam": "event-date", + "endParam": "event-date" + }, + { + "code": "DiagnosticReport", + "param": [ + "encounter" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "DocumentReference", + "param": [ + "context" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Encounter", + "param": [ + "{def}" + ], + "startParam": "date-start", + "endParam": "date-start" + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest" + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare", + "startParam": "date", + "endParam": "date" + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "encounter" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "FamilyMemberHistory", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Flag", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Goal", + "startParam": "start-date", + "endParam": "start-date" + }, + { + "code": "Group" + }, + { + "code": "GuidanceResponse" + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingSelection", + "startParam": "issued", + "endParam": "issued" + }, + { + "code": "ImagingStudy", + "startParam": "started", + "endParam": "started" + }, + { + "code": "Immunization", + "startParam": "date", + "endParam": "date" + }, + { + "code": "ImplementationGuide" + }, + { + "code": "Ingredient" + }, + { + "code": "InsurancePlan" + }, + { + "code": "InsuranceProduct" + }, + { + "code": "Invoice", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Library" + }, + { + "code": "List", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Location" + }, + { + "code": "ManufacturedItemDefinition" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration", + "param": [ + "encounter" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "MedicationDispense", + "param": [ + "encounter" + ], + "startParam": "whenhandedover", + "endParam": "whenhandedover" + }, + { + "code": "MedicationRequest", + "param": [ + "encounter" + ], + "startParam": "combo-date", + "endParam": "combo-date" + }, + { + "code": "MedicationStatement", + "param": [ + "encounter" + ], + "startParam": "effective", + "endParam": "effective" + }, + { + "code": "MedicinalProductDefinition" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader" + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionIntake", + "param": [ + "encounter" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "NutritionOrder", + "param": [ + "encounter" + ], + "startParam": "datetime", + "endParam": "datetime" + }, + { + "code": "NutritionProduct" + }, + { + "code": "Observation", + "param": [ + "encounter" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation", + "startParam": "date", + "endParam": "date" + }, + { + "code": "PackagedProductDefinition" + }, + { + "code": "Patient" + }, + { + "code": "PaymentNotice", + "startParam": "created", + "endParam": "created" + }, + { + "code": "PaymentReconciliation", + "startParam": "created", + "endParam": "created" + }, + { + "code": "Person" + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner" + }, + { + "code": "PractitionerRole", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Procedure", + "param": [ + "encounter" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Provenance", + "startParam": "when", + "endParam": "when" + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "encounter" + ], + "startParam": "answer-date", + "endParam": "answer-date" + }, + { + "code": "RegulatedAuthorization" + }, + { + "code": "RelatedPerson" + }, + { + "code": "RequestOrchestration", + "param": [ + "encounter" + ] + }, + { + "code": "Requirements" + }, + { + "code": "ResearchStudy", + "startParam": "date", + "endParam": "date" + }, + { + "code": "ResearchSubject", + "startParam": "date", + "endParam": "date" + }, + { + "code": "RiskAssessment", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Schedule", + "startParam": "date", + "endParam": "date" + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "encounter" + ], + "startParam": "occurrence", + "endParam": "occurrence" + }, + { + "code": "Slot" + }, + { + "code": "Specimen", + "startParam": "collected", + "endParam": "collected" + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "SubscriptionStatus" + }, + { + "code": "SubscriptionTopic" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceDefinition" + }, + { + "code": "Task", + "startParam": "period", + "endParam": "period" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "ValueSet" + }, + { + "code": "VisionPrescription", + "param": [ + "encounter" + ], + "startParam": "datewritten", + "endParam": "datewritten" + } + ] + } + }, + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/group", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "group", + "text": { + "status": "generated", + "div": "

Generated Narrative: CompartmentDefinition group

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
AdverseEventsubject
Appointmentsubject, actor
AppointmentResponseactor
AuditEventagent
CarePlansubject
CareTeamsubject, participant
Claimsubject
ClaimResponsesubject
Communicationsubject
CommunicationRequestsubject
Conditionsubject
Consentsubject, actor, grantee
DetectedIssuesubject
DeviceAlertsubject
DeviceAssociationsubject, focus
DeviceRequestsubject, requester
DiagnosticReportsubject
DocumentReferenceattester, author, subject
Encountersubject, participant
EnrollmentRequestgroup
EnrollmentResponsegroup
EpisodeOfCaresubject
ExplanationOfBenefitsubject
Flagsubject
Goalsubject
GuidanceResponsesubject
ImagingSelectionsubject
ImagingStudysubject
Invoicesubject
MeasureReportsubject
MedicationAdministrationsubject
MedicationDispensesubject
MedicationRequestsubject
MedicationStatementsubject
NutritionIntakesubject
NutritionOrdersubject
Observationspecimen, subject, performer
Proceduresubject
Provenanceagent
RequestOrchestrationparticipant, subject
ResearchStudyeligibility
ResearchSubjectsubject
RiskAssessmentsubject
ServiceRequestsubject, performer
Specimensubject
Tasksubject
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-wg", + "valueCode": "fhir" + } + ], + "url": "http://hl7.org/fhir/CompartmentDefinition/group", + "version": "6.0.0-ballot3", + "name": "Base FHIR compartment definition for Group", + "status": "draft", + "experimental": false, + "date": "2025-12-17T09:50:17+00:00", + "publisher": "HL7 International / FHIR Infrastructure", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://www.hl7.org/Special/committees/fiwg" + } + ] + } + ], + "description": "There is an instance of the group compartment for each Group resource, and the identity of the compartment is the same as the Group. The set of resources associated with a particular group", + "code": "Group", + "search": true, + "resource": [ + { + "code": "Account", + "startParam": "period", + "endParam": "period" + }, + { + "code": "ActivityDefinition" + }, + { + "code": "ActorDefinition" + }, + { + "code": "AdministrableProductDefinition" + }, + { + "code": "AdverseEvent", + "param": [ + "subject" + ], + "startParam": "occurrence", + "endParam": "occurrence" + }, + { + "code": "AllergyIntolerance", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Appointment", + "param": [ + "subject", + "actor" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "AppointmentResponse", + "param": [ + "actor" + ] + }, + { + "code": "ArtifactAssessment", + "startParam": "date", + "endParam": "date" + }, + { + "code": "AuditEvent", + "param": [ + "agent" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Basic", + "startParam": "created", + "endParam": "created" + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BodyStructure" + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan", + "param": [ + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "CareTeam", + "param": [ + "subject", + "participant" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Claim", + "param": [ + "subject" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "ClaimResponse", + "param": [ + "subject" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "ClinicalUseDefinition" + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "subject" + ], + "startParam": "sent", + "endParam": "sent" + }, + { + "code": "CommunicationRequest", + "param": [ + "subject" + ], + "startParam": "authored", + "endParam": "authored" + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "startParam": "date", + "endParam": "date" + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition", + "param": [ + "subject" + ] + }, + { + "code": "Consent", + "param": [ + "subject", + "actor", + "grantee" + ], + "startParam": "period", + "endParam": "period" + }, + { + "code": "Contract" + }, + { + "code": "Coverage", + "startParam": "period", + "endParam": "period" + }, + { + "code": "CoverageEligibilityRequest", + "startParam": "created", + "endParam": "created" + }, + { + "code": "CoverageEligibilityResponse", + "startParam": "created", + "endParam": "created" + }, + { + "code": "DetectedIssue", + "param": [ + "subject" + ], + "startParam": "identified", + "endParam": "identified" + }, + { + "code": "Device" + }, + { + "code": "DeviceAlert", + "param": [ + "subject" + ], + "startParam": "occurrence", + "endParam": "occurrence" + }, + { + "code": "DeviceAssociation", + "param": [ + "subject", + "focus" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest", + "param": [ + "subject", + "requester" + ], + "startParam": "event-date", + "endParam": "event-date" + }, + { + "code": "DiagnosticReport", + "param": [ + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "DocumentReference", + "param": [ + "attester", + "author", + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Encounter", + "param": [ + "subject", + "participant" + ], + "startParam": "date-start", + "endParam": "date-start" + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest", + "param": [ + "group" + ] + }, + { + "code": "EnrollmentResponse", + "param": [ + "group" + ] + }, + { + "code": "EpisodeOfCare", + "param": [ + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "subject" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "FamilyMemberHistory", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Flag", + "param": [ + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Goal", + "param": [ + "subject" + ], + "startParam": "start-date", + "endParam": "start-date" + }, + { + "code": "Group" + }, + { + "code": "GuidanceResponse", + "param": [ + "subject" + ] + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingSelection", + "param": [ + "subject" + ], + "startParam": "issued", + "endParam": "issued" + }, + { + "code": "ImagingStudy", + "param": [ + "subject" + ], + "startParam": "started", + "endParam": "started" + }, + { + "code": "Immunization", + "startParam": "date", + "endParam": "date" + }, + { + "code": "ImplementationGuide" + }, + { + "code": "Ingredient" + }, + { + "code": "InsurancePlan" + }, + { + "code": "InsuranceProduct" + }, + { + "code": "Invoice", + "param": [ + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Library" + }, + { + "code": "List", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Location" + }, + { + "code": "ManufacturedItemDefinition" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport", + "param": [ + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration", + "param": [ + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "MedicationDispense", + "param": [ + "subject" + ], + "startParam": "whenhandedover", + "endParam": "whenhandedover" + }, + { + "code": "MedicationRequest", + "param": [ + "subject" + ], + "startParam": "combo-date", + "endParam": "combo-date" + }, + { + "code": "MedicationStatement", + "param": [ + "subject" + ], + "startParam": "effective", + "endParam": "effective" + }, + { + "code": "MedicinalProductDefinition" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader" + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionIntake", + "param": [ + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "NutritionOrder", + "param": [ + "subject" + ], + "startParam": "datetime", + "endParam": "datetime" + }, + { + "code": "NutritionProduct" + }, + { + "code": "Observation", + "param": [ + "specimen", + "subject", + "performer" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation", + "startParam": "date", + "endParam": "date" + }, + { + "code": "PackagedProductDefinition" + }, + { + "code": "Patient" + }, + { + "code": "PaymentNotice", + "startParam": "created", + "endParam": "created" + }, + { + "code": "PaymentReconciliation", + "startParam": "created", + "endParam": "created" + }, + { + "code": "Person" + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner" + }, + { + "code": "PractitionerRole", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Procedure", + "param": [ + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Provenance", + "param": [ + "agent" + ], + "startParam": "when", + "endParam": "when" + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "startParam": "answer-date", + "endParam": "answer-date" + }, + { + "code": "RegulatedAuthorization" + }, + { + "code": "RelatedPerson" + }, + { + "code": "RequestOrchestration", + "param": [ + "participant", + "subject" + ] + }, + { + "code": "Requirements" + }, + { + "code": "ResearchStudy", + "param": [ + "eligibility" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "ResearchSubject", + "param": [ + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "RiskAssessment", + "param": [ + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Schedule", + "startParam": "date", + "endParam": "date" + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "subject", + "performer" + ], + "startParam": "occurrence", + "endParam": "occurrence" + }, + { + "code": "Slot" + }, + { + "code": "Specimen", + "param": [ + "subject" + ], + "startParam": "collected", + "endParam": "collected" + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "SubscriptionStatus" + }, + { + "code": "SubscriptionTopic" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceDefinition" + }, + { + "code": "Task", + "param": [ + "subject" + ], + "startParam": "period", + "endParam": "period" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "ValueSet" + }, + { + "code": "VisionPrescription", + "startParam": "datewritten", + "endParam": "datewritten" + } + ] + } + }, + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/patient", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "patient", + "text": { + "status": "generated", + "div": "

Generated Narrative: CompartmentDefinition patient

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
Accountsubject
AdverseEventsubject
AllergyIntolerancepatient, asserter
Appointmentactor
AppointmentResponseactor
AuditEventpatient
Basicpatient, author
BiologicallyDerivedProductpatient
BodyStructurepatient
CarePlanpatient
CareTeampatient, participant
Claimsubject, payee
ClaimResponsesubject
Communicationsubject, sender, recipient
CommunicationRequestsubject, information-provider, recipient, requester
Compositionsubject, author, attester
Conditionpatient, asserter
Consentsubject
Contractpatient
Coveragepolicy-holder, subscriber, beneficiary, paymentby-party
CoverageEligibilityRequestpatient
CoverageEligibilityResponsepatient
DetectedIssuepatient
DeviceAlertsubject, acknowledged-by
DeviceAssociationpatient, subject, focus
DeviceRequestsubject, performer, requester
DiagnosticReportsubject
DocumentReferencesubject, author
Encounterpatient
EnrollmentRequestpatient
EnrollmentResponsepatient
EpisodeOfCaresubject
ExplanationOfBenefitsubject, payee
FamilyMemberHistorypatient
Flagpatient
Goalpatient
Groupmember
GuidanceResponsepatient
ImagingSelectionpatient
ImagingStudypatient
Immunizationpatient
Invoicesubject, patient, recipient
Listsubject, source
MeasureReportpatient
MedicationAdministrationpatient, subject
MedicationDispensesubject, patient, receiver
MedicationRequestsubject
MedicationStatementsubject
NutritionIntakesubject, source
NutritionOrderpatient
Observationsubject, performer
Patient{def}, link
Personpatient
Procedurepatient, performer
Provenancepatient
QuestionnaireResponsesubject, author
RelatedPersonpatient
RequestOrchestrationsubject, participant
ResearchSubjectsubject
RiskAssessmentsubject
Scheduleactor
ServiceRequestsubject, performer
Specimensubject
Taskpatient, focus-reference
VisionPrescriptionpatient
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-wg", + "valueCode": "fhir" + } + ], + "url": "http://hl7.org/fhir/CompartmentDefinition/patient", + "version": "6.0.0-ballot3", + "name": "Base FHIR compartment definition for Patient", + "status": "draft", + "experimental": false, + "date": "2025-12-17T09:50:17+00:00", + "publisher": "HL7 International / FHIR Infrastructure", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://www.hl7.org/Special/committees/fiwg" + } + ] + } + ], + "description": "There is an instance of the patient compartment for each patient resource, and the identity of the compartment is the same as the patient. When a patient is linked to another patient resource, the records associated with the linked patient resource will not be returned as part of the compartment search. Those records will be returned only with another compartment search using the \"id\" for the linked patient resource.\n \nIn cases where two patients have been merged rather than linked, associated resources should be moved to the target patient as part of the merge process, so the patient compartment for the target patient would include all relevant data, and the patient compartment for the source patient would include only the linked Patient and possibly remnant resources like AuditEvent.. The set of resources associated with a particular patient", + "code": "Patient", + "search": true, + "resource": [ + { + "code": "Account", + "param": [ + "subject" + ], + "startParam": "period", + "endParam": "period" + }, + { + "code": "ActivityDefinition" + }, + { + "code": "ActorDefinition" + }, + { + "code": "AdministrableProductDefinition" + }, + { + "code": "AdverseEvent", + "param": [ + "subject" + ], + "startParam": "occurrence", + "endParam": "occurrence" + }, + { + "code": "AllergyIntolerance", + "param": [ + "patient", + "asserter" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Appointment", + "param": [ + "actor" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "AppointmentResponse", + "param": [ + "actor" + ] + }, + { + "code": "ArtifactAssessment", + "startParam": "date", + "endParam": "date" + }, + { + "code": "AuditEvent", + "param": [ + "patient" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Basic", + "param": [ + "patient", + "author" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct", + "param": [ + "patient" + ] + }, + { + "code": "BodyStructure", + "param": [ + "patient" + ] + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan", + "param": [ + "patient" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "CareTeam", + "param": [ + "patient", + "participant" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Claim", + "param": [ + "subject", + "payee" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "ClaimResponse", + "param": [ + "subject" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "ClinicalUseDefinition" + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "subject", + "sender", + "recipient" + ], + "startParam": "sent", + "endParam": "sent" + }, + { + "code": "CommunicationRequest", + "param": [ + "subject", + "information-provider", + "recipient", + "requester" + ], + "startParam": "authored", + "endParam": "authored" + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "subject", + "author", + "attester" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition", + "param": [ + "patient", + "asserter" + ] + }, + { + "code": "Consent", + "param": [ + "subject" + ], + "startParam": "period", + "endParam": "period" + }, + { + "code": "Contract", + "param": [ + "patient" + ] + }, + { + "code": "Coverage", + "param": [ + "policy-holder", + "subscriber", + "beneficiary", + "paymentby-party" + ], + "startParam": "period", + "endParam": "period" + }, + { + "code": "CoverageEligibilityRequest", + "param": [ + "patient" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "CoverageEligibilityResponse", + "param": [ + "patient" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "DetectedIssue", + "param": [ + "patient" + ], + "startParam": "identified", + "endParam": "identified" + }, + { + "code": "Device" + }, + { + "code": "DeviceAlert", + "param": [ + "subject", + "acknowledged-by" + ], + "startParam": "occurrence", + "endParam": "occurrence" + }, + { + "code": "DeviceAssociation", + "param": [ + "patient", + "subject", + "focus" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest", + "param": [ + "subject", + "performer", + "requester" + ], + "startParam": "event-date", + "endParam": "event-date" + }, + { + "code": "DiagnosticReport", + "param": [ + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "DocumentReference", + "param": [ + "subject", + "author" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Encounter", + "param": [ + "patient" + ], + "startParam": "date-start", + "endParam": "date-start" + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest", + "param": [ + "patient" + ] + }, + { + "code": "EnrollmentResponse", + "param": [ + "patient" + ] + }, + { + "code": "EpisodeOfCare", + "param": [ + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "subject", + "payee" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "FamilyMemberHistory", + "param": [ + "patient" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Flag", + "param": [ + "patient" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Goal", + "param": [ + "patient" + ], + "startParam": "start-date", + "endParam": "start-date" + }, + { + "code": "Group", + "param": [ + "member" + ] + }, + { + "code": "GuidanceResponse", + "param": [ + "patient" + ] + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingSelection", + "param": [ + "patient" + ], + "startParam": "issued", + "endParam": "issued" + }, + { + "code": "ImagingStudy", + "param": [ + "patient" + ], + "startParam": "started", + "endParam": "started" + }, + { + "code": "Immunization", + "param": [ + "patient" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "ImplementationGuide" + }, + { + "code": "Ingredient" + }, + { + "code": "InsurancePlan" + }, + { + "code": "InsuranceProduct" + }, + { + "code": "Invoice", + "param": [ + "subject", + "patient", + "recipient" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Library" + }, + { + "code": "List", + "param": [ + "subject", + "source" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Location" + }, + { + "code": "ManufacturedItemDefinition" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport", + "param": [ + "patient" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration", + "param": [ + "patient", + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "MedicationDispense", + "param": [ + "subject", + "patient", + "receiver" + ], + "startParam": "whenhandedover", + "endParam": "whenhandedover" + }, + { + "code": "MedicationRequest", + "param": [ + "subject" + ], + "startParam": "combo-date", + "endParam": "combo-date" + }, + { + "code": "MedicationStatement", + "param": [ + "subject" + ], + "startParam": "effective", + "endParam": "effective" + }, + { + "code": "MedicinalProductDefinition" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader" + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionIntake", + "param": [ + "subject", + "source" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "NutritionOrder", + "param": [ + "patient" + ], + "startParam": "datetime", + "endParam": "datetime" + }, + { + "code": "NutritionProduct" + }, + { + "code": "Observation", + "param": [ + "subject", + "performer" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation", + "startParam": "date", + "endParam": "date" + }, + { + "code": "PackagedProductDefinition" + }, + { + "code": "Patient", + "param": [ + "{def}", + "link" + ] + }, + { + "code": "PaymentNotice", + "startParam": "created", + "endParam": "created" + }, + { + "code": "PaymentReconciliation", + "startParam": "created", + "endParam": "created" + }, + { + "code": "Person", + "param": [ + "patient" + ] + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner" + }, + { + "code": "PractitionerRole", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Procedure", + "param": [ + "patient", + "performer" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Provenance", + "param": [ + "patient" + ], + "startParam": "when", + "endParam": "when" + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "subject", + "author" + ], + "startParam": "answer-date", + "endParam": "answer-date" + }, + { + "code": "RegulatedAuthorization" + }, + { + "code": "RelatedPerson", + "param": [ + "patient" + ] + }, + { + "code": "RequestOrchestration", + "param": [ + "subject", + "participant" + ] + }, + { + "code": "Requirements" + }, + { + "code": "ResearchStudy", + "startParam": "date", + "endParam": "date" + }, + { + "code": "ResearchSubject", + "param": [ + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "RiskAssessment", + "param": [ + "subject" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Schedule", + "param": [ + "actor" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "subject", + "performer" + ], + "startParam": "occurrence", + "endParam": "occurrence" + }, + { + "code": "Slot" + }, + { + "code": "Specimen", + "param": [ + "subject" + ], + "startParam": "collected", + "endParam": "collected" + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "SubscriptionStatus" + }, + { + "code": "SubscriptionTopic" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceDefinition" + }, + { + "code": "Task", + "param": [ + "patient", + "focus-reference" + ], + "startParam": "period", + "endParam": "period" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "ValueSet" + }, + { + "code": "VisionPrescription", + "param": [ + "patient" + ], + "startParam": "datewritten", + "endParam": "datewritten" + } + ] + } + }, + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/practitioner", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "practitioner", + "text": { + "status": "generated", + "div": "

Generated Narrative: CompartmentDefinition practitioner

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
Accountsubject
AdverseEventrecorder
AllergyIntoleranceasserter
Appointmentactor
AppointmentResponseactor
AuditEventagent
Basicauthor
BiologicallyDerivedProductcollector
CareTeamparticipant
Claimenterer, provider, payee, care-team
ClaimResponserequestor
Communicationsender, recipient
CommunicationRequestinformation-provider, recipient, requester
Compositionsubject, author, attester
Conditionasserter
CoverageEligibilityRequestenterer, provider
CoverageEligibilityResponserequestor
DetectedIssueauthor
DeviceAlertacknowledged-by
DeviceAssociationsubject, focus
DeviceRequestrequester, performer
DiagnosticReportperformer
DocumentReferencesubject, author, attester
Encounterpractitioner, participant
EpisodeOfCarecare-manager
ExplanationOfBenefitenterer, provider, payee, care-team
Flagauthor
Groupmember
Immunizationperformer
Invoiceparticipant
Listsource
MedicationDispenseperformer, receiver
MedicationRequestrequester
MedicationStatementsource
MessageHeaderreceiver
NutritionIntakesource
NutritionOrderrequester
Observationperformer
Patientgeneral-practitioner
PaymentNoticereporter
PaymentReconciliationrequestor
Personpractitioner
Practitioner{def}
PractitionerRolepractitioner
Procedureperformer
Provenanceagent
QuestionnaireResponseauthor, source
RequestOrchestrationparticipant, author
RiskAssessmentperformer
Scheduleactor
ServiceRequestperformer, requester
Specimencollector
VisionPrescriptionprescriber
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-wg", + "valueCode": "fhir" + } + ], + "url": "http://hl7.org/fhir/CompartmentDefinition/practitioner", + "version": "6.0.0-ballot3", + "name": "Base FHIR compartment definition for Practitioner", + "status": "draft", + "experimental": false, + "date": "2025-12-17T09:50:17+00:00", + "publisher": "HL7 International / FHIR Infrastructure", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://www.hl7.org/Special/committees/fiwg" + } + ] + } + ], + "description": "There is an instance of the practitioner compartment for each Practitioner resource, and the identity of the compartment is the same as the Practitioner. The set of resources associated with a particular practitioner", + "code": "Practitioner", + "search": true, + "resource": [ + { + "code": "Account", + "param": [ + "subject" + ], + "startParam": "period", + "endParam": "period" + }, + { + "code": "ActivityDefinition" + }, + { + "code": "ActorDefinition" + }, + { + "code": "AdministrableProductDefinition" + }, + { + "code": "AdverseEvent", + "param": [ + "recorder" + ], + "startParam": "occurrence", + "endParam": "occurrence" + }, + { + "code": "AllergyIntolerance", + "param": [ + "asserter" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Appointment", + "param": [ + "actor" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "AppointmentResponse", + "param": [ + "actor" + ] + }, + { + "code": "ArtifactAssessment", + "startParam": "date", + "endParam": "date" + }, + { + "code": "AuditEvent", + "param": [ + "agent" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Basic", + "param": [ + "author" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct", + "param": [ + "collector" + ] + }, + { + "code": "BodyStructure" + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan", + "startParam": "date", + "endParam": "date" + }, + { + "code": "CareTeam", + "param": [ + "participant" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Claim", + "param": [ + "enterer", + "provider", + "payee", + "care-team" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "ClaimResponse", + "param": [ + "requestor" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "ClinicalUseDefinition" + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "sender", + "recipient" + ], + "startParam": "sent", + "endParam": "sent" + }, + { + "code": "CommunicationRequest", + "param": [ + "information-provider", + "recipient", + "requester" + ], + "startParam": "authored", + "endParam": "authored" + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "subject", + "author", + "attester" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition", + "param": [ + "asserter" + ] + }, + { + "code": "Consent", + "startParam": "period", + "endParam": "period" + }, + { + "code": "Contract" + }, + { + "code": "Coverage", + "startParam": "period", + "endParam": "period" + }, + { + "code": "CoverageEligibilityRequest", + "param": [ + "enterer", + "provider" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "CoverageEligibilityResponse", + "param": [ + "requestor" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "DetectedIssue", + "param": [ + "author" + ], + "startParam": "identified", + "endParam": "identified" + }, + { + "code": "Device" + }, + { + "code": "DeviceAlert", + "param": [ + "acknowledged-by" + ], + "startParam": "occurrence", + "endParam": "occurrence" + }, + { + "code": "DeviceAssociation", + "param": [ + "subject", + "focus" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest", + "param": [ + "requester", + "performer" + ], + "startParam": "event-date", + "endParam": "event-date" + }, + { + "code": "DiagnosticReport", + "param": [ + "performer" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "DocumentReference", + "param": [ + "subject", + "author", + "attester" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Encounter", + "param": [ + "practitioner", + "participant" + ], + "startParam": "date-start", + "endParam": "date-start" + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest" + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare", + "param": [ + "care-manager" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "enterer", + "provider", + "payee", + "care-team" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "FamilyMemberHistory", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Flag", + "param": [ + "author" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Goal", + "startParam": "start-date", + "endParam": "start-date" + }, + { + "code": "Group", + "param": [ + "member" + ] + }, + { + "code": "GuidanceResponse" + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingSelection", + "startParam": "issued", + "endParam": "issued" + }, + { + "code": "ImagingStudy", + "startParam": "started", + "endParam": "started" + }, + { + "code": "Immunization", + "param": [ + "performer" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "ImplementationGuide" + }, + { + "code": "Ingredient" + }, + { + "code": "InsurancePlan" + }, + { + "code": "InsuranceProduct" + }, + { + "code": "Invoice", + "param": [ + "participant" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Library" + }, + { + "code": "List", + "param": [ + "source" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Location" + }, + { + "code": "ManufacturedItemDefinition" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration", + "startParam": "date", + "endParam": "date" + }, + { + "code": "MedicationDispense", + "param": [ + "performer", + "receiver" + ], + "startParam": "whenhandedover", + "endParam": "whenhandedover" + }, + { + "code": "MedicationRequest", + "param": [ + "requester" + ], + "startParam": "combo-date", + "endParam": "combo-date" + }, + { + "code": "MedicationStatement", + "param": [ + "source" + ], + "startParam": "effective", + "endParam": "effective" + }, + { + "code": "MedicinalProductDefinition" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader", + "param": [ + "receiver" + ] + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionIntake", + "param": [ + "source" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "NutritionOrder", + "param": [ + "requester" + ], + "startParam": "datetime", + "endParam": "datetime" + }, + { + "code": "NutritionProduct" + }, + { + "code": "Observation", + "param": [ + "performer" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation", + "startParam": "date", + "endParam": "date" + }, + { + "code": "PackagedProductDefinition" + }, + { + "code": "Patient", + "param": [ + "general-practitioner" + ] + }, + { + "code": "PaymentNotice", + "param": [ + "reporter" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "PaymentReconciliation", + "param": [ + "requestor" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "Person", + "param": [ + "practitioner" + ] + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner", + "param": [ + "{def}" + ] + }, + { + "code": "PractitionerRole", + "param": [ + "practitioner" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Procedure", + "param": [ + "performer" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Provenance", + "param": [ + "agent" + ], + "startParam": "when", + "endParam": "when" + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "author", + "source" + ], + "startParam": "answer-date", + "endParam": "answer-date" + }, + { + "code": "RegulatedAuthorization" + }, + { + "code": "RelatedPerson" + }, + { + "code": "RequestOrchestration", + "param": [ + "participant", + "author" + ] + }, + { + "code": "Requirements" + }, + { + "code": "ResearchStudy", + "startParam": "date", + "endParam": "date" + }, + { + "code": "ResearchSubject", + "startParam": "date", + "endParam": "date" + }, + { + "code": "RiskAssessment", + "param": [ + "performer" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Schedule", + "param": [ + "actor" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "performer", + "requester" + ], + "startParam": "occurrence", + "endParam": "occurrence" + }, + { + "code": "Slot" + }, + { + "code": "Specimen", + "param": [ + "collector" + ], + "startParam": "collected", + "endParam": "collected" + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "SubscriptionStatus" + }, + { + "code": "SubscriptionTopic" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceDefinition" + }, + { + "code": "Task", + "startParam": "period", + "endParam": "period" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "ValueSet" + }, + { + "code": "VisionPrescription", + "param": [ + "prescriber" + ], + "startParam": "datewritten", + "endParam": "datewritten" + } + ] + } + }, + { + "fullUrl": "http://hl7.org/fhir/CompartmentDefinition/relatedPerson", + "resource": { + "resourceType": "CompartmentDefinition", + "id": "relatedPerson", + "text": { + "status": "generated", + "div": "

Generated Narrative: CompartmentDefinition relatedPerson

\r\nThe following resources may be in this compartment:\r\n

\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
ResourceInclusion Criteria
AdverseEventrecorder
AllergyIntoleranceasserter
Appointmentactor
AppointmentResponseactor
Basicauthor
CareTeamparticipant
Claimpayee
Communicationsender, recipient
CommunicationRequestinformation-provider, recipient, requester
Compositionauthor
Conditionasserter
Coveragepolicy-holder, subscriber, paymentby-party
DeviceAlertacknowledged-by
DeviceAssociationsubject, focus
DeviceRequestperformer, requester
DocumentReferenceauthor
Encounterparticipant
ExplanationOfBenefitpayee
Invoicerecipient
MedicationStatementsource
NutritionIntakesource
Observationperformer
Patientlink
Personlink
Procedureperformer
Provenanceagent
QuestionnaireResponseauthor, source
RelatedPerson{def}
RequestOrchestrationparticipant
Scheduleactor
ServiceRequestperformer
\r\n

\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n

\r\n

\r\n\r\n

\r\n

\r\nThe following resources are never in this compartment:\r\n

\r\n
" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-wg", + "valueCode": "fhir" + } + ], + "url": "http://hl7.org/fhir/CompartmentDefinition/relatedPerson", + "version": "6.0.0-ballot3", + "name": "Base FHIR compartment definition for RelatedPerson", + "status": "draft", + "experimental": false, + "date": "2025-12-17T09:50:17+00:00", + "publisher": "HL7 International / FHIR Infrastructure", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://www.hl7.org/Special/committees/fiwg" + } + ] + } + ], + "description": "There is an instance of the relatedPerson compartment for each relatedPerson resource, and the identity of the compartment is the same as the relatedPerson. The set of resources associated with a particular 'related person'", + "code": "RelatedPerson", + "search": true, + "resource": [ + { + "code": "Account", + "startParam": "period", + "endParam": "period" + }, + { + "code": "ActivityDefinition" + }, + { + "code": "ActorDefinition" + }, + { + "code": "AdministrableProductDefinition" + }, + { + "code": "AdverseEvent", + "param": [ + "recorder" + ], + "startParam": "occurrence", + "endParam": "occurrence" + }, + { + "code": "AllergyIntolerance", + "param": [ + "asserter" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Appointment", + "param": [ + "actor" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "AppointmentResponse", + "param": [ + "actor" + ] + }, + { + "code": "ArtifactAssessment", + "startParam": "date", + "endParam": "date" + }, + { + "code": "AuditEvent", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Basic", + "param": [ + "author" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "Binary" + }, + { + "code": "BiologicallyDerivedProduct" + }, + { + "code": "BodyStructure" + }, + { + "code": "Bundle" + }, + { + "code": "CapabilityStatement" + }, + { + "code": "CarePlan", + "startParam": "date", + "endParam": "date" + }, + { + "code": "CareTeam", + "param": [ + "participant" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Claim", + "param": [ + "payee" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "ClaimResponse", + "startParam": "created", + "endParam": "created" + }, + { + "code": "ClinicalUseDefinition" + }, + { + "code": "CodeSystem" + }, + { + "code": "Communication", + "param": [ + "sender", + "recipient" + ], + "startParam": "sent", + "endParam": "sent" + }, + { + "code": "CommunicationRequest", + "param": [ + "information-provider", + "recipient", + "requester" + ], + "startParam": "authored", + "endParam": "authored" + }, + { + "code": "CompartmentDefinition" + }, + { + "code": "Composition", + "param": [ + "author" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "ConceptMap" + }, + { + "code": "Condition", + "param": [ + "asserter" + ] + }, + { + "code": "Consent", + "startParam": "period", + "endParam": "period" + }, + { + "code": "Contract" + }, + { + "code": "Coverage", + "param": [ + "policy-holder", + "subscriber", + "paymentby-party" + ], + "startParam": "period", + "endParam": "period" + }, + { + "code": "CoverageEligibilityRequest", + "startParam": "created", + "endParam": "created" + }, + { + "code": "CoverageEligibilityResponse", + "startParam": "created", + "endParam": "created" + }, + { + "code": "DetectedIssue", + "startParam": "identified", + "endParam": "identified" + }, + { + "code": "Device" + }, + { + "code": "DeviceAlert", + "param": [ + "acknowledged-by" + ], + "startParam": "occurrence", + "endParam": "occurrence" + }, + { + "code": "DeviceAssociation", + "param": [ + "subject", + "focus" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "DeviceDefinition" + }, + { + "code": "DeviceMetric" + }, + { + "code": "DeviceRequest", + "param": [ + "performer", + "requester" + ], + "startParam": "event-date", + "endParam": "event-date" + }, + { + "code": "DiagnosticReport", + "startParam": "date", + "endParam": "date" + }, + { + "code": "DocumentReference", + "param": [ + "author" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Encounter", + "param": [ + "participant" + ], + "startParam": "date-start", + "endParam": "date-start" + }, + { + "code": "Endpoint" + }, + { + "code": "EnrollmentRequest" + }, + { + "code": "EnrollmentResponse" + }, + { + "code": "EpisodeOfCare", + "startParam": "date", + "endParam": "date" + }, + { + "code": "EventDefinition" + }, + { + "code": "Evidence" + }, + { + "code": "EvidenceVariable" + }, + { + "code": "ExampleScenario" + }, + { + "code": "ExplanationOfBenefit", + "param": [ + "payee" + ], + "startParam": "created", + "endParam": "created" + }, + { + "code": "FamilyMemberHistory", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Flag", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Goal", + "startParam": "start-date", + "endParam": "start-date" + }, + { + "code": "Group" + }, + { + "code": "GuidanceResponse" + }, + { + "code": "HealthcareService" + }, + { + "code": "ImagingSelection", + "startParam": "issued", + "endParam": "issued" + }, + { + "code": "ImagingStudy", + "startParam": "started", + "endParam": "started" + }, + { + "code": "Immunization", + "startParam": "date", + "endParam": "date" + }, + { + "code": "ImplementationGuide" + }, + { + "code": "Ingredient" + }, + { + "code": "InsurancePlan" + }, + { + "code": "InsuranceProduct" + }, + { + "code": "Invoice", + "param": [ + "recipient" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Library" + }, + { + "code": "List", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Location" + }, + { + "code": "ManufacturedItemDefinition" + }, + { + "code": "Measure" + }, + { + "code": "MeasureReport", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Medication" + }, + { + "code": "MedicationAdministration", + "startParam": "date", + "endParam": "date" + }, + { + "code": "MedicationDispense", + "startParam": "whenhandedover", + "endParam": "whenhandedover" + }, + { + "code": "MedicationRequest", + "startParam": "combo-date", + "endParam": "combo-date" + }, + { + "code": "MedicationStatement", + "param": [ + "source" + ], + "startParam": "effective", + "endParam": "effective" + }, + { + "code": "MedicinalProductDefinition" + }, + { + "code": "MessageDefinition" + }, + { + "code": "MessageHeader" + }, + { + "code": "NamingSystem" + }, + { + "code": "NutritionIntake", + "param": [ + "source" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "NutritionOrder", + "startParam": "datetime", + "endParam": "datetime" + }, + { + "code": "NutritionProduct" + }, + { + "code": "Observation", + "param": [ + "performer" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "ObservationDefinition" + }, + { + "code": "OperationDefinition" + }, + { + "code": "OperationOutcome" + }, + { + "code": "Organization" + }, + { + "code": "OrganizationAffiliation", + "startParam": "date", + "endParam": "date" + }, + { + "code": "PackagedProductDefinition" + }, + { + "code": "Patient", + "param": [ + "link" + ] + }, + { + "code": "PaymentNotice", + "startParam": "created", + "endParam": "created" + }, + { + "code": "PaymentReconciliation", + "startParam": "created", + "endParam": "created" + }, + { + "code": "Person", + "param": [ + "link" + ] + }, + { + "code": "PlanDefinition" + }, + { + "code": "Practitioner" + }, + { + "code": "PractitionerRole", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Procedure", + "param": [ + "performer" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "Provenance", + "param": [ + "agent" + ], + "startParam": "when", + "endParam": "when" + }, + { + "code": "Questionnaire" + }, + { + "code": "QuestionnaireResponse", + "param": [ + "author", + "source" + ], + "startParam": "answer-date", + "endParam": "answer-date" + }, + { + "code": "RegulatedAuthorization" + }, + { + "code": "RelatedPerson", + "param": [ + "{def}" + ] + }, + { + "code": "RequestOrchestration", + "param": [ + "participant" + ] + }, + { + "code": "Requirements" + }, + { + "code": "ResearchStudy", + "startParam": "date", + "endParam": "date" + }, + { + "code": "ResearchSubject", + "startParam": "date", + "endParam": "date" + }, + { + "code": "RiskAssessment", + "startParam": "date", + "endParam": "date" + }, + { + "code": "Schedule", + "param": [ + "actor" + ], + "startParam": "date", + "endParam": "date" + }, + { + "code": "SearchParameter" + }, + { + "code": "ServiceRequest", + "param": [ + "performer" + ], + "startParam": "occurrence", + "endParam": "occurrence" + }, + { + "code": "Slot" + }, + { + "code": "Specimen", + "startParam": "collected", + "endParam": "collected" + }, + { + "code": "SpecimenDefinition" + }, + { + "code": "StructureDefinition" + }, + { + "code": "StructureMap" + }, + { + "code": "Subscription" + }, + { + "code": "SubscriptionStatus" + }, + { + "code": "SubscriptionTopic" + }, + { + "code": "Substance" + }, + { + "code": "SubstanceDefinition" + }, + { + "code": "Task", + "startParam": "period", + "endParam": "period" + }, + { + "code": "TerminologyCapabilities" + }, + { + "code": "ValueSet" + }, + { + "code": "VisionPrescription", + "startParam": "datewritten", + "endParam": "datewritten" + } + ] + } + } + ] +} diff --git a/locales/de/main.ftl b/locales/de/main.ftl index 858a15457..28c19fde1 100644 --- a/locales/de/main.ftl +++ b/locales/de/main.ftl @@ -84,6 +84,7 @@ nav-batch-transaction = Batch / Transaktion nav-bulk-export = Bulk-Export nav-sql-on-fhir = SQL-on-FHIR nav-capability-conformance = Capability & Konformität +nav-search-parameters = Suchparameter nav-admin-ops = Admin / Betrieb nav-subscriptions = Abonnements nav-tenants = Mandanten @@ -166,11 +167,123 @@ queries-rename-prompt = Neuer Name queries-confirm-delete = „{ $name }“ löschen? queries-unavailable = Gespeicherte Abfragen sind nicht verfügbar: Das Storage-Backend dieses Servers unterstützt keine Benutzereinstellungen. +## SearchParameter-Ansicht (#238) + +sp-heading = Suchparameter +sp-lede = Durchsuche die Parameter, mit denen dieser Server Suchen auflöst, gefiltert nach Basis-Ressourcentyp. Spezifikationsparameter sind schreibgeschützt; tenant-spezifisches Bearbeiten kommt, sobald Suchparameter im Storage liegen. +sp-version-label = FHIR-Version +sp-spec-missing = Das vollständige Spezifikations-Bundle (search-parameters-*.json) wurde im Datenverzeichnis nicht gefunden — es werden nur die minimalen eingebetteten Fallback-Parameter angezeigt. +sp-rail-label = Ressourcenfilter +sp-rail-search = Typen filtern +sp-rail-recent = Zuletzt verwendet +sp-rail-types = Ressourcentypen +sp-rail-all = Alle Typen +sp-facet-type = Typ +sp-facet-type-label = Nach Parametertyp filtern +sp-facet-source = Quelle +sp-facet-source-label = Nach Quelle filtern +sp-source-embedded = eingebettet +sp-source-stored = gespeichert +sp-source-config = Konfiguration +sp-chip-conflict = Konflikt +sp-chip-overrides = überschreibt Spez. +sp-chip-shadowed = verdeckt +sp-col-code = Code +sp-col-type = Typ +sp-col-base = Basis +sp-col-expression = Ausdruck +sp-col-source = Quelle +sp-total = { $count } Parameter +sp-pagination-label = Seiten +sp-page-prev = Zurück +sp-page-next = Weiter +sp-detail-label = Parameterdetails +sp-detail-empty = Kein Parameter ausgewählt +sp-detail-empty-hint = Wähle eine Zeile, um Definition, Ausdruck und die Auflösung im Register zu prüfen. +sp-detail-readonly = Spezifikationsparameter (aus der Datendatei einkompiliert) — schreibgeschützt. +sp-field-url = Kanonische URL +sp-field-name = Name +sp-field-status = Status +sp-field-base = Basis-Ressourcentypen +sp-field-expression = FHIRPath-Ausdruck +sp-field-description = Beschreibung +sp-field-target = Zieltypen +sp-field-components = Komponenten +sp-status-hint = Der Loader stuft den Draft-Status der Spezifikation beim Laden auf active hoch. +sp-note-conflict = Doppeltes (base, code) innerhalb derselben Quelle wie { $url } — das Register lehnt diese Kollision ab (DuplicateCode). +sp-note-overrides = Überschreibt { $url } auf (base, code): eine gespeicherte Definition hat Vorrang vor dem Spezifikationsparameter und löst daher die Suchen auf. Das Register loggt ein WARN mit beiden URLs. +sp-note-shadowed = Verdeckt durch { $url } auf (base, code): eine Quelle mit höherem Vorrang löst die Suchen für diesen Slot auf. +sp-note-empty-expression = Leerer Ausdruck: der Extractor indexiert keine Zeilen, jede Suche über diesen Parameter liefert stillschweigend nichts. +sp-note-no-target = Referenzparameter ohne Zieltypen: verkettete Suche kann den referenzierten Typ nicht auflösen. +sp-note-choice-type = Choice-Typ-Ausdruck: der Extractor schreibt ofType(T) / as T vor der Auswertung gegen das gespeicherte JSON auf das konkrete Element um (z. B. valueQuantity). +sp-writes-pending = Anlegen, Überschreiben und Löschen von Tenant-Parametern kommt, sobald Suchparameter in der Datenbank gespeichert werden (#235). + +## Compartment-Ansicht & Tester (#237) + +cmp-heading = Compartments +cmp-lede = Die Compartment-Definitionen, mit denen dieser Server /{"{"}compartment{"}"}/{"{"}id{"}"}/{"{"}type{"}"}-Anfragen routet, und ein Tester, der beantwortet: Ist dieser Typ in diesem Compartment, über welche Parameter, und welche Suche führt der Server aus? +cmp-rail-label = Compartment-Definitionen +cmp-rail-heading = Compartments +cmp-rail-note = Die Basisdefinitionen werden mit dem Server ausgeliefert (aus der FHIR-Spezifikation generiert). Sie zu bearbeiten setzt eine tenant-spezifische Override-Schicht voraus — offene Frage im Issue. +cmp-tabs-label = Compartment-Bereiche +cmp-tab-definition = Definition +cmp-tab-members = Mitglieder +cmp-tab-tester = Tester +cmp-field-code = Code +cmp-field-status = Status +cmp-field-url = Kanonische URL +cmp-field-version = Version +cmp-field-publisher = Herausgeber +cmp-field-description = Beschreibung +cmp-field-search = search +cmp-field-experimental = experimental +cmp-search-why = Aus würde bedeuten, dass keine Compartment-Route für dieses Compartment auflöst. +cmp-on = an +cmp-off = aus +cmp-yes = ja +cmp-no = nein +cmp-readonly-note = Schreibgeschützt: diese Werte stammen aus den in den Server einkompilierten Spezifikationsdefinitionen. +cmp-filter-members = Mitglieder +cmp-filter-all = Alle Typen +cmp-filter-excluded = Ausgeschlossen +cmp-member = Mitglied +cmp-excluded = ausgeschlossen +cmp-tester-id = Id +cmp-tester-target = Zieltyp (oder *) +cmp-tester-run = Testen +cmp-result-member = ✓ Mitglied — über { $params } +cmp-result-flat = // äquivalente flache Suche +cmp-result-member-note = Der Server löst die Compartment-Route zu dieser Suche über die Referenzparameter des Typs auf. +cmp-result-self = ✓ Mitglied — die Compartment-Ressource selbst ({"{"}def{"}"}) +cmp-result-self-note = Die Compartment-Instanz ist trivialerweise in ihrem eigenen Compartment; die Route liest die Ressource direkt. +cmp-result-notmember = ✕ { $type } ist kein Mitglied dieses Compartments +cmp-result-notmember-note = Der Server antwortet mit 404 und einem OperationOutcome für Typen, die keine Compartment-Mitglieder sind. +cmp-result-fanout = Fächert auf { $count } Mitgliedstypen auf +cmp-result-fanout-note = Ausgeschlossene Typen werden übersprungen, nicht fehlgeschlagen — der Fan-out lässt Nicht-Mitgliedstypen weg statt zu scheitern. queries-builder-heading = Such-Builder queries-url-label = FHIR-Such-URL queries-url-placeholder = GET /Patient?name=smith&birthdate=ge1980-01-01 -queries-builder-hint = Bearbeite die GET-URL direkt. Ausführen öffnet die Suche in einem neuen Tab und trägt sie unter „Zuletzt" ein; mit einem Namen bleibt sie in der Liste unten gespeichert. +queries-builder-hint = Bearbeite die GET-URL direkt oder über die Zeilen darunter — beide bleiben synchron. Ausführen führt die Suche hier aus und trägt sie unter „Zuletzt" ein; mit einem Namen bleibt sie in der Liste gespeichert. queries-recent = Zuletzt queries-recent-heading = Letzte Suchen queries-recent-empty = Noch keine letzten Suchen — führe eine aus, um sie hier einzutragen. queries-invalid-url = Gib eine Suche wie GET /Patient?name=smith ein — der Ressourcentyp kommt aus dem Pfad. + +queries-conditions = Bedingungen +queries-add-condition = Bedingung hinzufügen +queries-includes = Includes +queries-result-controls = Ergebnis-Steuerung +queries-remove = Entfernen +queries-param-placeholder = Parameter +queries-value-placeholder = Wert +queries-results = Ergebnisse +queries-results-total = { $count } Ergebnisse +queries-results-included = { $count } eingeschlossen +queries-results-empty = Keine Ergebnisse. +queries-open-tab = In neuem Tab öffnen +queries-col-updated = Aktualisiert +queries-prev = Zurück +queries-next = Weiter + +queries-rail-heading = Ressourcentypen +queries-rail-filter = Typen filtern diff --git a/locales/en/main.ftl b/locales/en/main.ftl index 10ee7f3b6..2bc60d61b 100644 --- a/locales/en/main.ftl +++ b/locales/en/main.ftl @@ -88,6 +88,7 @@ nav-batch-transaction = Batch / Transaction nav-bulk-export = Bulk Export nav-sql-on-fhir = SQL-on-FHIR nav-capability-conformance = Capability & Conformance +nav-search-parameters = Search Parameters nav-admin-ops = Admin / Ops nav-subscriptions = Subscriptions nav-tenants = Tenants @@ -170,11 +171,123 @@ queries-rename-prompt = New name queries-confirm-delete = Delete "{ $name }"? queries-unavailable = Saved queries are unavailable: this server's storage backend does not support per-user settings. +## SearchParameter viewer (#238) + +sp-heading = Search parameters +sp-lede = Browse the parameters this server resolves searches against, filtered by base resource type. Spec parameters are read-only; tenant-scoped editing arrives once search parameters live in storage. +sp-version-label = FHIR version +sp-spec-missing = The full spec bundle (search-parameters-*.json) was not found in the data directory — only the minimal embedded fallback parameters are shown. +sp-rail-label = Resource filter +sp-rail-search = Filter types +sp-rail-recent = Recently used +sp-rail-types = Resource types +sp-rail-all = All types +sp-facet-type = Type +sp-facet-type-label = Filter by parameter type +sp-facet-source = Source +sp-facet-source-label = Filter by source +sp-source-embedded = embedded +sp-source-stored = stored +sp-source-config = config +sp-chip-conflict = conflict +sp-chip-overrides = overrides spec +sp-chip-shadowed = shadowed +sp-col-code = Code +sp-col-type = Type +sp-col-base = Base +sp-col-expression = Expression +sp-col-source = Source +sp-total = { $count } parameters +sp-pagination-label = Pages +sp-page-prev = Previous +sp-page-next = Next +sp-detail-label = Parameter detail +sp-detail-empty = No parameter selected +sp-detail-empty-hint = Select a row to inspect its definition, expression, and how it resolves against the registry. +sp-detail-readonly = Spec parameter (compiled in from the data file) — read-only. +sp-field-url = Canonical URL +sp-field-name = Name +sp-field-status = Status +sp-field-base = Base resource types +sp-field-expression = FHIRPath expression +sp-field-description = Description +sp-field-target = Target types +sp-field-components = Components +sp-status-hint = The loader promotes the spec's draft status to active on load. +sp-note-conflict = Duplicate (base, code) within the same source as { $url } — the registry rejects this collision (DuplicateCode). +sp-note-overrides = Overrides { $url } on (base, code): a Stored definition outranks the spec parameter, so this one resolves searches. The registry logs a WARN naming both URLs. +sp-note-shadowed = Shadowed by { $url } on (base, code): a higher-precedence source resolves searches for this slot. +sp-note-empty-expression = Empty expression: the extractor indexes zero rows, so every search on this parameter silently returns empty. +sp-note-no-target = Reference parameter with no target types: chained search cannot resolve the referenced type. +sp-note-choice-type = Choice-type expression: the extractor rewrites ofType(T) / as T to the concrete element (for example valueQuantity) before evaluating against raw stored JSON. +sp-writes-pending = Creating, overriding, and deleting tenant parameters lands once search parameters are stored in the database (#235). + +## Compartment viewer & tester (#237) + +cmp-heading = Compartments +cmp-lede = The compartment definitions this server routes /{"{"}compartment{"}"}/{"{"}id{"}"}/{"{"}type{"}"} requests with, and a tester that answers: is this type in this compartment, via which parameters, and what search does the server run? +cmp-rail-label = Compartment definitions +cmp-rail-heading = Compartments +cmp-rail-note = Base definitions ship with the server (codegen'd from the FHIR spec). Editing them implies a tenant-scoped override layer — open question on the issue. +cmp-tabs-label = Compartment sections +cmp-tab-definition = Definition +cmp-tab-members = Members +cmp-tab-tester = Tester +cmp-field-code = Code +cmp-field-status = Status +cmp-field-url = Canonical URL +cmp-field-version = Version +cmp-field-publisher = Publisher +cmp-field-description = Description +cmp-field-search = search +cmp-field-experimental = experimental +cmp-search-why = Off would mean no compartment route resolves for this compartment. +cmp-on = on +cmp-off = off +cmp-yes = yes +cmp-no = no +cmp-readonly-note = Read-only: these values come from the spec definitions compiled into the server. +cmp-filter-members = Members +cmp-filter-all = All types +cmp-filter-excluded = Excluded +cmp-member = member +cmp-excluded = excluded +cmp-tester-id = Id +cmp-tester-target = Target type (or *) +cmp-tester-run = Test +cmp-result-member = ✓ member — via { $params } +cmp-result-flat = // equivalent flat search +cmp-result-member-note = The server resolves the compartment route to this search over the type's reference parameters. +cmp-result-self = ✓ member — the compartment resource itself ({"{"}def{"}"}) +cmp-result-self-note = The compartment instance is trivially in its own compartment; the route reads the resource directly. +cmp-result-notmember = ✕ { $type } is not a member of this compartment +cmp-result-notmember-note = The server returns 404 with an OperationOutcome for types that are not compartment members. +cmp-result-fanout = Fans out to { $count } member types +cmp-result-fanout-note = Excluded types are skipped, not failed — the fan-out drops non-member types rather than erroring. queries-builder-heading = Search builder queries-url-label = FHIR search URL queries-url-placeholder = GET /Patient?name=smith&birthdate=ge1980-01-01 -queries-builder-hint = Edit the GET URL directly. Run opens the search in a new tab and records it under Recent; give it a name to keep it in the saved list below. +queries-builder-hint = Edit the GET URL directly or through the rows below — they stay in sync. Run executes the search here and records it under Recent; give it a name to keep it in the saved list. queries-recent = Recent queries-recent-heading = Recent searches queries-recent-empty = No recent searches yet — Run one to record it here. queries-invalid-url = Enter a search like GET /Patient?name=smith — the resource type comes from the path. + +queries-conditions = Conditions +queries-add-condition = Add condition +queries-includes = Includes +queries-result-controls = Result controls +queries-remove = Remove +queries-param-placeholder = parameter +queries-value-placeholder = value +queries-results = Results +queries-results-total = { $count } results +queries-results-included = { $count } included +queries-results-empty = No results. +queries-open-tab = Open in new tab +queries-col-updated = Updated +queries-prev = Previous +queries-next = Next + +queries-rail-heading = Resource types +queries-rail-filter = Filter types diff --git a/locales/es/main.ftl b/locales/es/main.ftl index 199696336..5c9732d44 100644 --- a/locales/es/main.ftl +++ b/locales/es/main.ftl @@ -84,6 +84,7 @@ nav-batch-transaction = Lote / Transacción nav-bulk-export = Exportación masiva nav-sql-on-fhir = SQL-on-FHIR nav-capability-conformance = Capacidad y conformidad +nav-search-parameters = Parámetros de búsqueda nav-admin-ops = Administración / Operaciones nav-subscriptions = Suscripciones nav-tenants = Tenants @@ -166,11 +167,123 @@ queries-rename-prompt = Nuevo nombre queries-confirm-delete = ¿Eliminar «{ $name }»? queries-unavailable = Las consultas guardadas no están disponibles: el backend de almacenamiento de este servidor no admite configuración por usuario. +## Visor de SearchParameters (#238) + +sp-heading = Parámetros de búsqueda +sp-lede = Explora los parámetros con los que este servidor resuelve las búsquedas, filtrados por tipo de recurso base. Los parámetros de la especificación son de solo lectura; la edición por tenant llegará cuando los parámetros vivan en el almacenamiento. +sp-version-label = Versión FHIR +sp-spec-missing = No se encontró el bundle completo de la especificación (search-parameters-*.json) en el directorio de datos — solo se muestran los parámetros mínimos embebidos. +sp-rail-label = Filtro de recursos +sp-rail-search = Filtrar tipos +sp-rail-recent = Usados recientemente +sp-rail-types = Tipos de recurso +sp-rail-all = Todos los tipos +sp-facet-type = Tipo +sp-facet-type-label = Filtrar por tipo de parámetro +sp-facet-source = Origen +sp-facet-source-label = Filtrar por origen +sp-source-embedded = embebido +sp-source-stored = almacenado +sp-source-config = configuración +sp-chip-conflict = conflicto +sp-chip-overrides = anula la spec +sp-chip-shadowed = eclipsado +sp-col-code = Código +sp-col-type = Tipo +sp-col-base = Base +sp-col-expression = Expresión +sp-col-source = Origen +sp-total = { $count } parámetros +sp-pagination-label = Páginas +sp-page-prev = Anterior +sp-page-next = Siguiente +sp-detail-label = Detalle del parámetro +sp-detail-empty = Ningún parámetro seleccionado +sp-detail-empty-hint = Selecciona una fila para inspeccionar su definición, su expresión y cómo se resuelve en el registro. +sp-detail-readonly = Parámetro de la especificación (compilado desde el archivo de datos) — solo lectura. +sp-field-url = URL canónica +sp-field-name = Nombre +sp-field-status = Estado +sp-field-base = Tipos de recurso base +sp-field-expression = Expresión FHIRPath +sp-field-description = Descripción +sp-field-target = Tipos destino +sp-field-components = Componentes +sp-status-hint = El cargador promueve el estado draft de la especificación a active al cargar. +sp-note-conflict = (base, code) duplicado dentro del mismo origen que { $url } — el registro rechaza esta colisión (DuplicateCode). +sp-note-overrides = Anula a { $url } en (base, code): una definición almacenada tiene precedencia sobre el parámetro de la spec, así que esta resuelve las búsquedas. El registro emite un WARN con ambas URLs. +sp-note-shadowed = Eclipsado por { $url } en (base, code): un origen de mayor precedencia resuelve las búsquedas de este slot. +sp-note-empty-expression = Expresión vacía: el extractor no indexa ninguna fila, así que toda búsqueda con este parámetro devuelve vacío en silencio. +sp-note-no-target = Parámetro de referencia sin tipos destino: la búsqueda encadenada no puede resolver el tipo referenciado. +sp-note-choice-type = Expresión de tipo choice: el extractor reescribe ofType(T) / as T al elemento concreto (por ejemplo valueQuantity) antes de evaluar contra el JSON almacenado. +sp-writes-pending = Crear, anular y borrar parámetros por tenant llegará cuando los parámetros de búsqueda se guarden en la base de datos (#235). + +## Visor y probador de compartments (#237) + +cmp-heading = Compartimentos +cmp-lede = Las definiciones de compartment con las que este servidor enruta las peticiones /{"{"}compartment{"}"}/{"{"}id{"}"}/{"{"}type{"}"}, y un probador que responde: ¿está este tipo en este compartment, mediante qué parámetros, y qué búsqueda ejecuta el servidor? +cmp-rail-label = Definiciones de compartment +cmp-rail-heading = Compartimentos +cmp-rail-note = Las definiciones base vienen con el servidor (generadas desde la especificación FHIR). Editarlas implica una capa de overrides por tenant — pregunta abierta en el issue. +cmp-tabs-label = Secciones del compartment +cmp-tab-definition = Definición +cmp-tab-members = Miembros +cmp-tab-tester = Probador +cmp-field-code = Código +cmp-field-status = Estado +cmp-field-url = URL canónica +cmp-field-version = Versión +cmp-field-publisher = Editor +cmp-field-description = Descripción +cmp-field-search = search +cmp-field-experimental = experimental +cmp-search-why = Apagado significaría que ninguna ruta de compartment resuelve para este compartment. +cmp-on = activado +cmp-off = desactivado +cmp-yes = sí +cmp-no = no +cmp-readonly-note = Solo lectura: estos valores provienen de las definiciones de la especificación compiladas en el servidor. +cmp-filter-members = Miembros +cmp-filter-all = Todos los tipos +cmp-filter-excluded = Excluidos +cmp-member = miembro +cmp-excluded = excluido +cmp-tester-id = Id +cmp-tester-target = Tipo destino (o *) +cmp-tester-run = Probar +cmp-result-member = ✓ miembro — vía { $params } +cmp-result-flat = // búsqueda plana equivalente +cmp-result-member-note = El servidor resuelve la ruta de compartment a esta búsqueda sobre los parámetros de referencia del tipo. +cmp-result-self = ✓ miembro — el propio recurso del compartment ({"{"}def{"}"}) +cmp-result-self-note = La instancia del compartment está trivialmente en su propio compartment; la ruta lee el recurso directamente. +cmp-result-notmember = ✕ { $type } no es miembro de este compartment +cmp-result-notmember-note = El servidor devuelve 404 con un OperationOutcome para tipos que no son miembros del compartment. +cmp-result-fanout = Se expande a { $count } tipos miembro +cmp-result-fanout-note = Los tipos excluidos se omiten, no fallan — el fan-out descarta los tipos no miembro en lugar de dar error. queries-builder-heading = Constructor de búsquedas queries-url-label = URL de búsqueda FHIR queries-url-placeholder = GET /Patient?name=smith&birthdate=ge1980-01-01 -queries-builder-hint = Edita la URL GET directamente. Ejecutar abre la búsqueda en una pestaña nueva y la registra en Recientes; ponle un nombre para conservarla en la lista de abajo. +queries-builder-hint = Edita la URL GET directamente o mediante las filas de abajo — se mantienen sincronizadas. Ejecutar corre la búsqueda aquí mismo y la registra en Recientes; ponle un nombre para conservarla en la lista. queries-recent = Recientes queries-recent-heading = Búsquedas recientes queries-recent-empty = Aún no hay búsquedas recientes — ejecuta una para registrarla aquí. queries-invalid-url = Escribe una búsqueda como GET /Patient?name=smith — el tipo de recurso sale de la ruta. + +queries-conditions = Condiciones +queries-add-condition = Añadir condición +queries-includes = Includes +queries-result-controls = Controles de resultado +queries-remove = Quitar +queries-param-placeholder = parámetro +queries-value-placeholder = valor +queries-results = Resultados +queries-results-total = { $count } resultados +queries-results-included = { $count } incluidos +queries-results-empty = Sin resultados. +queries-open-tab = Abrir en pestaña nueva +queries-col-updated = Actualizado +queries-prev = Anterior +queries-next = Siguiente + +queries-rail-heading = Tipos de recurso +queries-rail-filter = Filtrar tipos