Class: MedicalRecords::Client
- Inherits:
-
Common::Client::Base
- Object
- Common::Client::Base
- MedicalRecords::Client
- Defined in:
- lib/medical_records/client.rb
Overview
Core class responsible for Medical Records API interface operations
Constant Summary collapse
- DEFAULT_COUNT =
Default number of records to request per call when searching
9999
- PHYSICIAN_PROCEDURE_NOTE =
LOINC codes for clinical notes
'11506-3'
- DISCHARGE_SUMMARY =
Physician procedure note
'18842-5'
- CONSULT_RESULT =
Discharge summary
'11488-4'
- BLOOD_PRESSURE =
LOINC codes for vitals
'85354-9'
- BREATHING_RATE =
Blood Pressure
'9279-1'
- HEART_RATE =
Breathing Rate
'8867-4'
- HEIGHT =
Heart Rate
'8302-2'
- TEMPERATURE =
Height
'8310-5'
- WEIGHT =
Temperature
'29463-7'
- PULSE_OXIMETRY =
Weight
'59408-5,2708-6, '
- MICROBIOLOGY =
LOINC codes for labs & tests
'79381-0'
- PATHOLOGY =
Gastrointestinal pathogens panel
'60567-5'
- EKG =
Comprehensive pathology report panel
'11524-6'
- RADIOLOGY =
EKG Study
'18748-4'
Constants included from Common::Client::Concerns::MhvFhirSessionClient
Common::Client::Concerns::MhvFhirSessionClient::LOCK_RETRY_DELAY, Common::Client::Concerns::MhvFhirSessionClient::RETRY_ATTEMPTS
Constants included from Common::Client::Concerns::MhvLockedSessionClient
Common::Client::Concerns::MhvLockedSessionClient::LOCK_RETRY_DELAY, Common::Client::Concerns::MhvLockedSessionClient::RETRY_ATTEMPTS
Instance Attribute Summary
Attributes included from Common::Client::Concerns::MhvFhirSessionClient
Attributes included from Common::Client::Concerns::MHVJwtSessionClient
Attributes included from Common::Client::Concerns::MhvLockedSessionClient
Instance Method Summary collapse
-
#base_path ⇒ String
Base path for dependent URLs.
-
#fetch_nested_value(object, field_path) ⇒ Object
protected
Fetches the value of a potentially nested field from a given object.
-
#fhir_client ⇒ FHIR::Client
Create a new FHIR::Client instance based on the client_session.
- #fhir_read(fhir_model, id) ⇒ Object protected
-
#fhir_search(fhir_model, params) ⇒ FHIR::Bundle
protected
Perform a FHIR search.
-
#fhir_search_query(fhir_model, params) ⇒ FHIR::ClientReply
protected
Perform a FHIR search.
- #get_allergy(allergy_id) ⇒ Object
- #get_clinical_note(note_id) ⇒ Object
- #get_condition(condition_id) ⇒ Object
- #get_diagnostic_report(record_id) ⇒ Object
- #get_patient_by_identifier(fhir_client, identifier) ⇒ Object
- #get_vaccine(vaccine_id) ⇒ Object
- #handle_api_errors(result) ⇒ Object protected
- #list_allergies ⇒ Object
- #list_clinical_notes ⇒ Object
- #list_conditions ⇒ Object
- #list_labs_and_tests ⇒ Object
-
#list_labs_document_reference ⇒ FHIR::Bundle
protected
Fetch EKG and Radiology results for the given patient.
- #list_vaccines ⇒ Object
-
#list_vitals ⇒ Object
Function args are accepted and ignored for compatibility with MedicalRecords::LighthouseClient.
-
#merge_bundles(bundle1, bundle2) ⇒ Object
protected
Merge two FHIR bundles into one, with an updated total count.
-
#paginate_bundle_entries(entries, page_size, page_num) ⇒ Object
protected
Apply pagination to the entries in a FHIR::Bundle object.
-
#sessionless_fhir_client(bearer_token) ⇒ FHIR::Client
Create a new FHIR::Client instance, given the provided bearer token.
-
#sort_bundle(bundle, field, order = :asc) ⇒ Object
protected
Sort the FHIR::Bundle entries on a given field and sort order.
-
#sort_bundle_with_criteria(bundle, order = :asc) ⇒ Object
protected
Sort the FHIR::Bundle entries based on a provided block.
-
#sort_lab_entries(entries) ⇒ Object
protected
Sort the FHIR::Bundle entries for lab & test results in reverse chronological order.
Methods included from Common::Client::Concerns::MhvFhirSessionClient
#fetch_patient_by_subject_id, #get_patient_fhir_id, #get_session, #incomplete?, #invalid?, #perform_phr_refresh, #save_session
Methods included from Common::Client::Concerns::MHVJwtSessionClient
#auth_body, #auth_headers, #decode_jwt_token, #extract_token_expiration, #get_jwt_from_headers, #get_session, #get_session_tagged, #jwt_bearer_token, #patient_fhir_id, #session_config_key, #user_key, #validate_session_params
Methods included from Common::Client::Concerns::MhvLockedSessionClient
#authenticate, #initialize, #invalid?, #lock_and_get_session, #obtain_redis_lock, #refresh_session, #release_redis_lock
Methods included from SentryLogging
#log_exception_to_sentry, #log_message_to_sentry, #non_nil_hash?, #normalize_level, #rails_logger, #set_sentry_metadata
Methods inherited from Common::Client::Base
#config, configuration, #connection, #delete, #get, #perform, #post, #put, #raise_backend_exception, #raise_not_authenticated, #request, #sanitize_headers!, #service_name
Instance Method Details
#base_path ⇒ String
Returns Base path for dependent URLs.
45 46 47 |
# File 'lib/medical_records/client.rb', line 45 def base_path "#{Settings.mhv.medical_records.host}/fhir/" end |
#fetch_nested_value(object, field_path) ⇒ Object (protected)
Fetches the value of a potentially nested field from a given object.
341 342 343 344 345 |
# File 'lib/medical_records/client.rb', line 341 def fetch_nested_value(object, field_path) field_path.split('.').reduce(object) do |obj, method| obj.respond_to?(method) ? obj.send(method) : nil end end |
#fhir_client ⇒ FHIR::Client
Create a new FHIR::Client instance based on the client_session. Use an existing client if one already exists in this instance.
74 75 76 77 78 |
# File 'lib/medical_records/client.rb', line 74 def fhir_client raise MedicalRecords::PatientNotFound if patient_fhir_id.nil? @fhir_client ||= sessionless_fhir_client(jwt_bearer_token) end |
#fhir_read(fhir_model, id) ⇒ Object (protected)
226 227 228 229 230 |
# File 'lib/medical_records/client.rb', line 226 def fhir_read(fhir_model, id) result = fhir_client.read(fhir_model, id) handle_api_errors(result) if result.resource.nil? result.resource end |
#fhir_search(fhir_model, params) ⇒ FHIR::Bundle (protected)
Perform a FHIR search. This method will continue making queries until all results have been returned.
196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/medical_records/client.rb', line 196 def fhir_search(fhir_model, params) reply = fhir_search_query(fhir_model, params) combined_bundle = reply.resource loop do break unless reply.resource.next_link reply = fhir_client.next_page(reply) combined_bundle = merge_bundles(combined_bundle, reply.resource) end combined_bundle end |
#fhir_search_query(fhir_model, params) ⇒ FHIR::ClientReply (protected)
Perform a FHIR search. Returns the first page of results only.
215 216 217 218 219 220 221 222 223 224 |
# File 'lib/medical_records/client.rb', line 215 def fhir_search_query(fhir_model, params) default_headers = { 'Cache-Control': 'no-cache' } params[:headers] = default_headers.merge(params.fetch(:headers, {})) params[:search][:parameters].merge!(_count: DEFAULT_COUNT) result = fhir_client.search(fhir_model, params) handle_api_errors(result) if result.resource.nil? result end |
#get_allergy(allergy_id) ⇒ Object
102 103 104 |
# File 'lib/medical_records/client.rb', line 102 def get_allergy(allergy_id) fhir_read(FHIR::AllergyIntolerance, allergy_id) end |
#get_clinical_note(note_id) ⇒ Object
160 161 162 |
# File 'lib/medical_records/client.rb', line 160 def get_clinical_note(note_id) fhir_read(FHIR::DocumentReference, note_id) end |
#get_condition(condition_id) ⇒ Object
133 134 135 |
# File 'lib/medical_records/client.rb', line 133 def get_condition(condition_id) fhir_read(FHIR::Condition, condition_id) end |
#get_diagnostic_report(record_id) ⇒ Object
170 171 172 |
# File 'lib/medical_records/client.rb', line 170 def get_diagnostic_report(record_id) fhir_read(FHIR::DiagnosticReport, record_id) end |
#get_patient_by_identifier(fhir_client, identifier) ⇒ Object
80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/medical_records/client.rb', line 80 def get_patient_by_identifier(fhir_client, identifier) result = fhir_client.search(FHIR::Patient, { search: { parameters: { identifier: } }, headers: { 'Cache-Control': 'no-cache' } }) # MHV will return a 202 if and only if the patient does not exist. It will not return 202 for # multiple patients found. raise MedicalRecords::PatientNotFound if result.response[:code] == 202 resource = result.resource handle_api_errors(result) if resource.nil? resource end |
#get_vaccine(vaccine_id) ⇒ Object
112 113 114 |
# File 'lib/medical_records/client.rb', line 112 def get_vaccine(vaccine_id) fhir_read(FHIR::Immunization, vaccine_id) end |
#handle_api_errors(result) ⇒ Object (protected)
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 |
# File 'lib/medical_records/client.rb', line 232 def handle_api_errors(result) if result.code.present? && result.code >= 400 body = JSON.parse(result.body) diagnostics = body['issue']&.first&.fetch('diagnostics', nil) diagnostics = "Error fetching data#{": #{diagnostics}" if diagnostics}" # Special-case exception handling if result.code == 500 && diagnostics.include?('HAPI-1363') # "HAPI-1363: Either No patient or multiple patient found" raise MedicalRecords::PatientNotFound end # Default exception handling raise Common::Exceptions::BackendServiceException.new( "MEDICALRECORDS_#{result.code}", status: result.code, detail: diagnostics, source: self.class.to_s ) end end |
#list_allergies ⇒ Object
95 96 97 98 99 100 |
# File 'lib/medical_records/client.rb', line 95 def list_allergies bundle = fhir_search(FHIR::AllergyIntolerance, search: { parameters: { patient: patient_fhir_id, 'clinical-status': 'active', 'verification-status:not': 'entered-in-error' } }) sort_bundle(bundle, :recordedDate, :desc) end |
#list_clinical_notes ⇒ Object
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/medical_records/client.rb', line 137 def list_clinical_notes loinc_codes = "#{PHYSICIAN_PROCEDURE_NOTE},#{DISCHARGE_SUMMARY},#{CONSULT_RESULT}" bundle = fhir_search(FHIR::DocumentReference, search: { parameters: { patient: patient_fhir_id, type: loinc_codes, 'status:not': 'entered-in-error' } }) # Sort the bundle of notes based on the date field appropriate to each note type. sort_bundle_with_criteria(bundle, :desc) do |resource| loinc_code = if resource.respond_to?(:type) && resource.type.respond_to?(:coding) resource.type.coding&.find do |coding| coding.respond_to?(:system) && coding.system == 'http://loinc.org' end&.code end case loinc_code when PHYSICIAN_PROCEDURE_NOTE, CONSULT_RESULT resource.date when DISCHARGE_SUMMARY resource.context&.period&.end end end end |
#list_conditions ⇒ Object
126 127 128 129 130 131 |
# File 'lib/medical_records/client.rb', line 126 def list_conditions bundle = fhir_search(FHIR::Condition, search: { parameters: { patient: patient_fhir_id, 'verification-status:not': 'entered-in-error' } }) sort_bundle(bundle, :recordedDate, :desc) end |
#list_labs_and_tests ⇒ Object
164 165 166 167 168 |
# File 'lib/medical_records/client.rb', line 164 def list_labs_and_tests bundle = fhir_search(FHIR::DiagnosticReport, search: { parameters: { patient: patient_fhir_id, 'status:not': 'entered-in-error' } }) sort_bundle(bundle, :effectiveDateTime, :desc) end |
#list_labs_document_reference ⇒ FHIR::Bundle (protected)
Fetch EKG and Radiology results for the given patient
182 183 184 185 186 187 |
# File 'lib/medical_records/client.rb', line 182 def list_labs_document_reference loinc_codes = "#{EKG},#{RADIOLOGY}" fhir_search(FHIR::DocumentReference, search: { parameters: { patient: patient_fhir_id, type: loinc_codes, 'status:not': 'entered-in-error' } }) end |
#list_vaccines ⇒ Object
106 107 108 109 110 |
# File 'lib/medical_records/client.rb', line 106 def list_vaccines bundle = fhir_search(FHIR::Immunization, search: { parameters: { patient: patient_fhir_id, 'status:not': 'entered-in-error' } }) sort_bundle(bundle, :occurrenceDateTime, :desc) end |
#list_vitals ⇒ Object
Function args are accepted and ignored for compatibility with MedicalRecords::LighthouseClient
117 118 119 120 121 122 123 124 |
# File 'lib/medical_records/client.rb', line 117 def list_vitals(*) # loinc_codes = # "#{BLOOD_PRESSURE},#{BREATHING_RATE},#{HEART_RATE},#{HEIGHT},#{TEMPERATURE},#{WEIGHT},#{PULSE_OXIMETRY}" bundle = fhir_search(FHIR::Observation, search: { parameters: { patient: patient_fhir_id, category: 'vital-signs', 'status:not': 'entered-in-error' } }) sort_bundle(bundle, :effectiveDateTime, :desc) end |
#merge_bundles(bundle1, bundle2) ⇒ Object (protected)
Merge two FHIR bundles into one, with an updated total count.
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 |
# File 'lib/medical_records/client.rb', line 261 def merge_bundles(bundle1, bundle2) unless bundle1.resourceType == 'Bundle' && bundle2.resourceType == 'Bundle' raise 'Both inputs must be FHIR Bundles' end # Clone the first bundle to avoid modifying the original merged_bundle = bundle1.clone # Merge the entries from the second bundle into the merged_bundle merged_bundle.entry ||= [] bundle2.entry&.each do |entry| merged_bundle.entry << entry end # Update the total count in the merged bundle merged_bundle.total = merged_bundle.entry.count merged_bundle end |
#paginate_bundle_entries(entries, page_size, page_num) ⇒ Object (protected)
Apply pagination to the entries in a FHIR::Bundle object. This assumes sorting has already taken place.
288 289 290 291 292 293 294 295 |
# File 'lib/medical_records/client.rb', line 288 def paginate_bundle_entries(entries, page_size, page_num) start_index = (page_num - 1) * page_size end_index = start_index + page_size paginated_entries = entries[start_index...end_index] # Return the paginated result or an empty array if no entries paginated_entries || [] end |
#sessionless_fhir_client(bearer_token) ⇒ FHIR::Client
Create a new FHIR::Client instance, given the provided bearer token. This method does not require a client_session to have been initialized.
56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/medical_records/client.rb', line 56 def sessionless_fhir_client(bearer_token) # FHIR debug level is extremely verbose, printing the full contents of every response body. ::FHIR.logger.level = Logger::INFO FHIR::Client.new(base_path).tap do |client| client.use_r4 client.default_json client.use_minimal_preference client.set_bearer_token(bearer_token) end end |
#sort_bundle(bundle, field, order = :asc) ⇒ Object (protected)
Sort the FHIR::Bundle entries on a given field and sort order. If a field is not present, that entry is sorted to the end.
305 306 307 308 309 310 |
# File 'lib/medical_records/client.rb', line 305 def sort_bundle(bundle, field, order = :asc) field = field.to_s sort_bundle_with_criteria(bundle, order) do |resource| fetch_nested_value(resource, field) end end |
#sort_bundle_with_criteria(bundle, order = :asc) ⇒ Object (protected)
Sort the FHIR::Bundle entries based on a provided block. The block should handle different resource types and define how to extract the sorting value from each.
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 |
# File 'lib/medical_records/client.rb', line 319 def sort_bundle_with_criteria(bundle, order = :asc) sorted_entries = bundle.entry.sort do |entry1, entry2| value1 = yield(entry1.resource) value2 = yield(entry2.resource) if value2.nil? -1 elsif value1.nil? 1 else order == :asc ? value1 <=> value2 : value2 <=> value1 end end bundle.entry = sorted_entries bundle end |
#sort_lab_entries(entries) ⇒ Object (protected)
Sort the FHIR::Bundle entries for lab & test results in reverse chronological order. Different entry types use different date fields for sorting.
353 354 355 356 357 358 359 360 361 362 363 364 |
# File 'lib/medical_records/client.rb', line 353 def sort_lab_entries(entries) entries.sort_by! do |entry| case entry when FHIR::DiagnosticReport -entry.effectiveDateTime.to_i when FHIR::DocumentReference -entry.date.to_i else 0 end end end |