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'
- 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'
- 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(page_size = 999, page_num = 1) ⇒ FHIR::Bundle
Fetch Lab & Tests results for the given patient.
-
#list_labs_chemhem_diagnostic_report ⇒ FHIR::Bundle
protected
Fetch Chemistry/Hematology results for the given patient.
-
#list_labs_document_reference ⇒ FHIR::Bundle
protected
Fetch EKG and Radiology results for the given patient.
-
#list_labs_other_diagnostic_report ⇒ FHIR::Bundle
protected
Fetch Microbiology and Pathology results for the given patient.
- #list_vaccines ⇒ Object
- #list_vitals ⇒ Object
-
#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
#get_session, #incomplete?, #invalid?
Methods included from Common::Client::Concerns::MHVJwtSessionClient
#get_session, #session_config_key, #user_key
Methods included from Common::Client::Concerns::MhvLockedSessionClient
#authenticate, #initialize, #invalid?, #refresh_session
Methods included from SentryLogging
#log_exception_to_sentry, #log_message_to_sentry, #non_nil_hash?, #normalize_level, #rails_logger
Methods inherited from Common::Client::Base
configuration, #raise_backend_exception
Instance Method Details
#base_path ⇒ String
Returns Base path for dependent URLs.
43 44 45 |
# File 'lib/medical_records/client.rb', line 43 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.
386 387 388 389 390 |
# File 'lib/medical_records/client.rb', line 386 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.
72 73 74 |
# File 'lib/medical_records/client.rb', line 72 def fhir_client @fhir_client ||= sessionless_fhir_client(jwt_bearer_token) end |
#fhir_read(fhir_model, id) ⇒ Object (protected)
271 272 273 274 275 |
# File 'lib/medical_records/client.rb', line 271 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.
244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/medical_records/client.rb', line 244 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. Filters out FHIR records that are not active.
264 265 266 267 268 269 |
# File 'lib/medical_records/client.rb', line 264 def fhir_search_query(fhir_model, params) 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
95 96 97 |
# File 'lib/medical_records/client.rb', line 95 def get_allergy(allergy_id) fhir_read(FHIR::AllergyIntolerance, allergy_id) end |
#get_clinical_note(note_id) ⇒ Object
149 150 151 |
# File 'lib/medical_records/client.rb', line 149 def get_clinical_note(note_id) fhir_read(FHIR::DocumentReference, note_id) end |
#get_condition(condition_id) ⇒ Object
123 124 125 |
# File 'lib/medical_records/client.rb', line 123 def get_condition(condition_id) fhir_search(FHIR::Condition, search: { parameters: { _id: condition_id, _include: '*' } }) end |
#get_diagnostic_report(record_id) ⇒ Object
153 154 155 |
# File 'lib/medical_records/client.rb', line 153 def get_diagnostic_report(record_id) fhir_search(FHIR::DiagnosticReport, search: { parameters: { _id: record_id, _include: '*' } }) end |
#get_patient_by_identifier(fhir_client, identifier) ⇒ Object
76 77 78 79 80 81 82 83 84 |
# File 'lib/medical_records/client.rb', line 76 def get_patient_by_identifier(fhir_client, identifier) result = fhir_client.search(FHIR::Patient, { search: { parameters: { identifier: } }, headers: { 'Cache-Control': 'no-cache' } }) resource = result.resource handle_api_errors(result) if resource.nil? resource end |
#get_vaccine(vaccine_id) ⇒ Object
108 109 110 |
# File 'lib/medical_records/client.rb', line 108 def get_vaccine(vaccine_id) fhir_read(FHIR::Immunization, vaccine_id) end |
#handle_api_errors(result) ⇒ Object (protected)
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/medical_records/client.rb', line 277 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
86 87 88 89 90 91 92 93 |
# File 'lib/medical_records/client.rb', line 86 def list_allergies bundle = fhir_search(FHIR::AllergyIntolerance, { search: { parameters: { patient: patient_fhir_id, 'clinical-status': 'active' } }, headers: { 'Cache-Control': 'no-cache' } }) sort_bundle(bundle, :recordedDate, :desc) end |
#list_clinical_notes ⇒ Object
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/medical_records/client.rb', line 127 def list_clinical_notes loinc_codes = "#{PHYSICIAN_PROCEDURE_NOTE},#{DISCHARGE_SUMMARY}" bundle = fhir_search(FHIR::DocumentReference, search: { parameters: { patient: patient_fhir_id, type: loinc_codes } }) # 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 resource.date when DISCHARGE_SUMMARY resource.context&.period&.end end end end |
#list_conditions ⇒ Object
118 119 120 121 |
# File 'lib/medical_records/client.rb', line 118 def list_conditions bundle = fhir_search(FHIR::Condition, search: { parameters: { patient: patient_fhir_id } }) sort_bundle(bundle, :recordedDate, :desc) end |
#list_labs_and_tests(page_size = 999, page_num = 1) ⇒ FHIR::Bundle
Fetch Lab & Tests results for the given patient. This combines the results of three separate calls.
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'lib/medical_records/client.rb', line 163 def list_labs_and_tests(page_size = 999, page_num = 1) combined_bundle = FHIR::Bundle.new combined_bundle.type = 'searchset' # Make the individual API calls. labs_diagrep_chemhem = list_labs_chemhem_diagnostic_report labs_diagrep_other = list_labs_other_diagnostic_report labs_docref = list_labs_document_reference # TODO: Figure out how to do this in threads. # labs_diagrep_chemhem_thread = Thread.new { list_labs_chemhem_diagnostic_report(patient_id) } # labs_diagrep_other_thread = Thread.new { list_labs_other_diagnostic_report(patient_id) } # labs_docref_thread = Thread.new { list_labs_document_reference(patient_id) } # labs_diagrep_chemhem_thread.join # labs_diagrep_other_thread.join # labs_docref_thread.join # labs_diagrep_chemhem = labs_diagrep_chemhem_thread.value # labs_diagrep_other = labs_diagrep_other_thread.value # labs_docref = labs_docref_thread.value # Merge the entry arrays into the combined bundle. combined_bundle.entry.concat(labs_diagrep_chemhem.entry) if labs_diagrep_chemhem.entry combined_bundle.entry.concat(labs_diagrep_other.entry) if labs_diagrep_other.entry combined_bundle.entry.concat(labs_docref.entry) if labs_docref.entry # Ensure an accurate total count for the combined bundle. combined_bundle.total = (labs_diagrep_chemhem&.total || 0) + (labs_diagrep_other&.total || 0) + (labs_docref&.total || 0) # Sort the combined_bundle.entry array by date in reverse chronological order sort_lab_entries(combined_bundle.entry) # Apply pagination combined_bundle.entry = paginate_bundle_entries(combined_bundle.entry, page_size, page_num) combined_bundle end |
#list_labs_chemhem_diagnostic_report ⇒ FHIR::Bundle (protected)
Fetch Chemistry/Hematology results for the given patient
209 210 211 212 |
# File 'lib/medical_records/client.rb', line 209 def list_labs_chemhem_diagnostic_report fhir_search(FHIR::DiagnosticReport, search: { parameters: { patient: patient_fhir_id, category: 'LAB' } }) end |
#list_labs_document_reference ⇒ FHIR::Bundle (protected)
Fetch EKG and Radiology results for the given patient
231 232 233 234 235 |
# File 'lib/medical_records/client.rb', line 231 def list_labs_document_reference loinc_codes = "#{EKG},#{RADIOLOGY}" fhir_search(FHIR::DocumentReference, search: { parameters: { patient: patient_fhir_id, type: loinc_codes } }) end |
#list_labs_other_diagnostic_report ⇒ FHIR::Bundle (protected)
Fetch Microbiology and Pathology results for the given patient
220 221 222 223 |
# File 'lib/medical_records/client.rb', line 220 def list_labs_other_diagnostic_report loinc_codes = "#{MICROBIOLOGY},#{PATHOLOGY}" fhir_search(FHIR::DiagnosticReport, search: { parameters: { patient: patient_fhir_id, code: loinc_codes } }) end |
#list_vaccines ⇒ Object
99 100 101 102 103 104 105 106 |
# File 'lib/medical_records/client.rb', line 99 def list_vaccines bundle = fhir_search(FHIR::Immunization, { search: { parameters: { patient: patient_fhir_id } }, headers: { 'Cache-Control': 'no-cache' } }) sort_bundle(bundle, :occurrenceDateTime, :desc) end |
#list_vitals ⇒ Object
112 113 114 115 116 |
# File 'lib/medical_records/client.rb', line 112 def list_vitals loinc_codes = "#{BLOOD_PRESSURE},#{BREATHING_RATE},#{HEART_RATE},#{HEIGHT},#{TEMPERATURE},#{WEIGHT}" bundle = fhir_search(FHIR::Observation, search: { parameters: { patient: patient_fhir_id, code: loinc_codes } }) sort_bundle(bundle, :effectiveDateTime, :desc) end |
#merge_bundles(bundle1, bundle2) ⇒ Object (protected)
Merge two FHIR bundles into one, with an updated total count.
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 |
# File 'lib/medical_records/client.rb', line 306 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.
333 334 335 336 337 338 339 340 |
# File 'lib/medical_records/client.rb', line 333 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.
54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/medical_records/client.rb', line 54 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.
350 351 352 353 354 355 |
# File 'lib/medical_records/client.rb', line 350 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.
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 |
# File 'lib/medical_records/client.rb', line 364 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.
398 399 400 401 402 403 404 405 406 407 408 409 |
# File 'lib/medical_records/client.rb', line 398 def sort_lab_entries(entries) entries.sort_by! do |entry| case entry when FHIR::DiagnosticReport -(entry.effectiveDateTime&.to_i || 0) when FHIR::DocumentReference -(entry.date&.to_i || 0) else 0 end end end |