diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f7efcbb..2c47c8a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -4,9 +4,6 @@ on: push: workflow_dispatch: pull_request: - schedule: - # Fetch new base image updates every night at 1am - - cron: '0 1 * * *' jobs: build-with-samply: diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cb7e5a..a1e5cf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Samply.Focus v0.8.0 2024-11-04 +## Major changes +* EHDS2 query support + +## Minor changes +* Separated exporter API key CLA from authorization header CLA + + +# Samply.Focus v0.8.0 2024-11-04 + In this release, we are supporting 4 types of SQL queries for Exliquid and Organoids ## Major changes diff --git a/Cargo.toml b/Cargo.toml index 8ad8cae..b9d6527 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "focus" -version = "0.8.0" +version = "0.9.0" edition = "2021" license = "Apache-2.0" @@ -11,12 +11,12 @@ base64 = "0.22.1" reqwest = { version = "0.12", default-features = false, features = ["json", "default-tls"] } serde = { version = "1.0.152", features = ["serde_derive"] } serde_json = "1.0" -thiserror = "1.0.38" +thiserror = "2.0.3" chrono = "0.4.31" indexmap = "2.1.0" tokio = { version = "1.25.0", default-features = false, features = ["signal", "rt-multi-thread", "macros"] } beam-lib = { git = "https://github.com/samply/beam", branch = "develop", features = ["http-util"] } -laplace_rs = {git = "https://github.com/samply/laplace-rs.git", tag = "v0.3.0" } +laplace_rs = {git = "https://github.com/samply/laplace-rs.git", tag = "v0.4.0" } uuid = "1.8.0" rand = { default-features = false, version = "0.8.5" } futures-util = { version = "0.3", default-features = false, features = ["std"] } diff --git a/README.md b/README.md index 7e5c2b1..03f0914 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,8 @@ PROJECTS_NO_OBFUSCATION = "exliquid;dktk_supervisors;exporter;ehds2" # Projects QUERIES_TO_CACHE = "queries_to_cache.conf" # The path to a file containing base64 encoded queries whose results are to be cached. If not set, no results are cached PROVIDER = "name" #EUCAIM provider name PROVIDER_ICON = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=" # Base64 encoded EUCAIM provider icon -AUTH_HEADER = "ApiKey XXXX" #Authorization header +AUTH_HEADER = "[Auth Type] XXXX" #Authorization header for accessing the store; Auth Type e.g. ApiKey, Basic, ... +EXPORTER_API_KEY = "XXXX" # Value of header x-api-key for accessing the Exporter application ``` In order to use Postgres querying, a Docker image built with the feature "dktk" needs to be used and this optional variable set: diff --git a/resources/cql/DKTK_STRAT_GENETIC_VARIANT b/resources/cql/DKTK_STRAT_GENETIC_VARIANT new file mode 100644 index 0000000..1021262 --- /dev/null +++ b/resources/cql/DKTK_STRAT_GENETIC_VARIANT @@ -0,0 +1,5 @@ +define GeneticVariantCount: +if InInitialPopulation then [Observation: Code '69548-6' from loinc] else {} as List + +define GeneticVariantCode: +First (from [Observation: Code '69548-6' from loinc] O return O.component.where(code.coding contains Code '48018-6' from loinc).value.coding.code.first()) diff --git a/resources/cql/EHDS2_IN_INITIAL_POPULATION b/resources/cql/EHDS2_IN_INITIAL_POPULATION new file mode 100644 index 0000000..80876f8 --- /dev/null +++ b/resources/cql/EHDS2_IN_INITIAL_POPULATION @@ -0,0 +1 @@ +define InInitialPopulation: diff --git a/resources/cql/EHDS2_OBSERVATION b/resources/cql/EHDS2_OBSERVATION new file mode 100644 index 0000000..a5542ac --- /dev/null +++ b/resources/cql/EHDS2_OBSERVATION @@ -0,0 +1,125 @@ +define ObservationList: + if InInitialPopulation then [Observation] else {} as List + +// Return the value in the given system for the given Observation +define function ObservationValueCode(observation FHIR.Observation, system_var String): + (observation.value as CodeableConcept).coding.where(system = system_var).code.first() + +// Check to see if there are any Observation resources with the +// given system/value pair. +define function ExistsObservationValueCode(system_var String, code_var String): + exists from [Observation] O + where ObservationValueCode(O, system_var) = code_var + +// Return the value associated with the extension with the given URL for the given Observation +define function BaseObservationExtensionValue(observation FHIR.Observation, url_var String): + observation.extension.where(url = url_var).value + +// Return the value of the Observation extension with the given URL +define function ObservationExtensionValue(observation FHIR.Observation, url_var String): + SensibleString(BaseObservationExtensionValue(observation, url_var)) + +define function ExistsObservationExtensionValue(url_var String, value_var String): + exists from [Observation] O + where ObservationExtensionValue(O, url_var) = value_var + +// Return the pathogen type for the given Observation +define function PathogenCode(observation FHIR.Observation): + ObservationValueCode(observation, 'https://ecdc.amr/pathogen-codes') + +// Check to see if there are any Observation resources with the +// given pathogen type. +define function ExistsPathogenCode(code_var String): + ExistsObservationValueCode('https://ecdc.amr/pathogen-codes', code_var) + +// Return the antibiotic type for the given Observation +define function AntibioticCode(observation FHIR.Observation): + ObservationValueCode(observation, 'https://ecdc.amr/antibiotic-codes') + +// Check to see if there are any Observation resources with the +// given antibiotic type. +define function ExistsAntibioticCode(code_var String): + ExistsObservationValueCode('https://ecdc.amr/antibiotic-codes', code_var) + +// Return the resistance type for the given Observation +define function SirCode(observation FHIR.Observation): + ObservationValueCode(observation, 'https://ecdc.amr/sir-codes') + +// Check to see if there are any Observation resources with the +// given resistance type. +define function ExistsSirCode(code_var String): + ExistsObservationValueCode('https://ecdc.amr/sir-codes', code_var) + +// Return the data source for the given Observation +define function DataSource(observation FHIR.Observation): + ObservationExtensionValue(observation, 'https://ecdc.amr/fhir/StructureDefinition/ObservationDataSource') + +// Check to see if there are any Observation resources with the +// given data source. +define function ExistsDataSource(value_var String): + ExistsObservationExtensionValue('https://ecdc.amr/fhir/StructureDefinition/ObservationDataSource', value_var) + +// Return the isolate ID for the given Observation +define function IsolateId(observation FHIR.Observation): + ObservationExtensionValue(observation, 'https://ecdc.amr/fhir/StructureDefinition/ObservationIsolateId') + +// Check to see if there are any Observation resources with the +// given isolate ID. +define function ExistsIsolateId(value_var String): + ExistsObservationExtensionValue('https://ecdc.amr/fhir/StructureDefinition/ObservationIsolateId', value_var) + +// Return the patient type for the given Observation +define function PatientType(observation FHIR.Observation): + ObservationExtensionValue(observation, 'https://ecdc.amr/fhir/StructureDefinition/ObservationPatientType') + +// Check to see if there are any Observation resources with the +// given patient type. +define function ExistsPatientType(value_var String): + ExistsObservationExtensionValue('https://ecdc.amr/fhir/StructureDefinition/ObservationPatientType', value_var) + +// Return the reference guidelines SIR for the given Observation +define function ReferenceGuidelinesSir(observation FHIR.Observation): + ObservationExtensionValue(observation, 'https://ecdc.amr/fhir/StructureDefinition/ObservationReferenceGuidelinesSIR') + +// Check to see if there are any Observation resources with the +// given reference guidelines SIR. +define function ExistsReferenceGuidelinesSir(value_var String): + ExistsObservationExtensionValue('https://ecdc.amr/fhir/StructureDefinition/ObservationReferenceGuidelinesSIR', value_var) + +// Return the reporting country for the given Observation +define function ReportingCountry(observation FHIR.Observation): + ObservationExtensionValue(observation, 'https://ecdc.amr/fhir/StructureDefinition/ObservationReportingCountry') + +// Check to see if there are any Observation resources with the +// given reporting country. +define function ExistsReportingCountry(value_var String): + ExistsObservationExtensionValue('https://ecdc.amr/fhir/StructureDefinition/ObservationReportingCountry', value_var) + +// Return the year from the date used for statistics +define function YearDateUsedForStatistics(observation FHIR.Observation): + year from observation.issued + +// Return the month from the date used for statistics +define function MonthDateUsedForStatistics(observation FHIR.Observation): + month from observation.issued + +// Return the year-month from the date used for statistics +define function YearMonthDateUsedForStatistics(observation FHIR.Observation): + ToString(YearDateUsedForStatistics(observation)) + '-' + ToString(MonthDateUsedForStatistics(observation)) + +// Return the the date valid from +define function DateValidFrom(observation FHIR.Observation): + ToDate(observation.effective as dateTime) + +// Return the year from the date valid from +define function YearDateValidFrom(observation FHIR.Observation): + year from DateValidFrom(observation) + +// Return the month from the date valid from +define function MonthDateValidFrom(observation FHIR.Observation): + month from DateValidFrom(observation) + +// Return the year-month from the date valid from +define function YearMonthDateValidFrom(observation FHIR.Observation): + ToString(YearDateValidFrom(observation)) + '-' + ToString(MonthDateValidFrom(observation)) + diff --git a/resources/cql/EHDS2_PATIENT b/resources/cql/EHDS2_PATIENT new file mode 100644 index 0000000..d01ab92 --- /dev/null +++ b/resources/cql/EHDS2_PATIENT @@ -0,0 +1,30 @@ +// Return the value of the Patient extension with the given URL. +// Return "Unknown" if not available or empty. +define function PatientExtensionValue(url_var String): + SensibleString(Patient.extension.where(url = url_var).value) + +// Return gender of patient +define Gender: + SensibleString(Patient.gender) + +// Return age of patient, as an integer +define AgeInYears: + ToInteger(PatientExtensionValue('https://ecdc.amr/fhir/StructureDefinition/PatientAge')) + +// Round patient age to the nearest multiple of 10 (i/p for a histogram). +// Return result as a string +define AgeClass: + ToString((AgeInYears div 10) * 10) + +// Return ID of hospital associated with patient +define HospitalId: + PatientExtensionValue('https://ecdc.amr/fhir/StructureDefinition/PatientHospitalId') + +// Return hospital unit type associated with patient +define HospitalUnitType: + PatientExtensionValue('https://ecdc.amr/fhir/StructureDefinition/PatientHospitalUnitType') + +// Return laboratory code associated with patient +define LaboratoryCode: + PatientExtensionValue('https://ecdc.amr/fhir/StructureDefinition/PatientLaboratoryCode') + diff --git a/resources/cql/EHDS2_SPECIMEN b/resources/cql/EHDS2_SPECIMEN new file mode 100644 index 0000000..7bdca37 --- /dev/null +++ b/resources/cql/EHDS2_SPECIMEN @@ -0,0 +1,13 @@ +define SpecimenList: + if InInitialPopulation then [Specimen] else {} as List + +// Return the isolate ID for the given Specimen +define function SpecimenIsolateId(specimen FHIR.Specimen): + SensibleString(specimen.id) + +// Check to see if there are any Specimen resources with the +// given isolate ID. +define function ExistsSpecimenIsolateId(id_var String): + exists from [Specimen] S + where SpecimenIsolateId(S) = id_var + diff --git a/resources/cql/EHDS2_UTIL b/resources/cql/EHDS2_UTIL new file mode 100644 index 0000000..b47e4c5 --- /dev/null +++ b/resources/cql/EHDS2_UTIL @@ -0,0 +1,7 @@ +// Return "val" if the supplied string value is sensible. +// Return "Unknown" if null or empty. +define function SensibleString(val String): + if (val is null or val = '' or val = '-') + then 'Unkown' + else val + diff --git a/src/config.rs b/src/config.rs index d45a408..9eca57d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -158,10 +158,15 @@ struct CliArgs { #[clap(long, env, value_parser)] provider_icon: Option, + // TODO - refactor to include multiple authorization headers for multiple stores / applications at the same time /// Authorization header #[clap(long, env, value_parser)] auth_header: Option, + /// Exporter API key + #[clap(long, env, value_parser)] + exporter_api_key: Option, + /// Postgres connection string #[cfg(feature = "query-sql")] #[clap(long, env, value_parser)] @@ -198,6 +203,7 @@ pub(crate) struct Config { pub provider: Option, pub provider_icon: Option, pub auth_header: Option, + pub exporter_api_key: Option, #[cfg(feature = "query-sql")] pub postgres_connection_string: Option, #[cfg(feature = "query-sql")] @@ -243,6 +249,7 @@ impl Config { provider: cli_args.provider, provider_icon: cli_args.provider_icon, auth_header: cli_args.auth_header, + exporter_api_key: cli_args.exporter_api_key, #[cfg(feature = "query-sql")] postgres_connection_string: cli_args.postgres_connection_string, #[cfg(feature = "query-sql")] diff --git a/src/exporter.rs b/src/exporter.rs index c3eb932..7208f3c 100644 --- a/src/exporter.rs +++ b/src/exporter.rs @@ -42,10 +42,10 @@ pub async fn post_exporter_query(body: &String, task_type: TaskType) -> Result