Article
· Oct 16, 2023 10m read

Using FHIR Adapter to offer FHIR services over legacy systems - Reading a Resouce

We resume our series of articles on the FHIR Adapter tool available to HealthShare HealthConnect and InterSystems IRIS users.

In the previous articles we have presented the small application on which we set up our workshop and showed the architecture deployed in our IRIS instance after installing the FHIR Adapter. In today's article we will see an example of how we can perform one of the most common CRUD (Create - Read - Update - Delete) operations, the reading operation, and we will do it by recovering a Resource.

What is a Resource?

A Resource in FHIR corresponds to a type of clinical information that is relevant, this information can be a patient (Patient), a request to a laboratory (ServiceRequest) or a diagnosis (Condition), etc. Each resource defines the type of data that comprises it, as well as the restrictions on the data and the relationships with other types of resources. Each resource allows the extension of the information it contains, thus allowing needs to be covered that are outside the FHIR 80% (covering the needs used by more than the 80% of the users).

For the example in this article we are going to use the most common resource, the Patient. Let's look at its definition:

{
  "resourceType" : "Patient",
  // from Resource: id, meta, implicitRules, and language
  // from DomainResource: text, contained, extension, and modifierExtension
  "identifier" : [{ Identifier }], // An identifier for this patient
  "active" : <boolean>, // Whether this patient's record is in active use
  "name" : [{ HumanName }], // A name associated with the patient
  "telecom" : [{ ContactPoint }], // A contact detail for the individual
  "gender" : "<code>", // male | female | other | unknown
  "birthDate" : "<date>", // The date of birth for the individual
  // deceased[x]: Indicates if the individual is deceased or not. One of these 2:
  "deceasedBoolean" : <boolean>,
  "deceasedDateTime" : "<dateTime>",
  "address" : [{ Address }], // An address for the individual
  "maritalStatus" : { CodeableConcept }, // Marital (civil) status of a patient
  // multipleBirth[x]: Whether patient is part of a multiple birth. One of these 2:
  "multipleBirthBoolean" : <boolean>,
  "multipleBirthInteger" : <integer>,
  "photo" : [{ Attachment }], // Image of the patient
  "contact" : [{ // A contact party (e.g. guardian, partner, friend) for the patient
    "relationship" : [{ CodeableConcept }], // The kind of relationship
    "name" : { HumanName }, // I A name associated with the contact person
    "telecom" : [{ ContactPoint }], // I A contact detail for the person
    "address" : { Address }, // I Address for the contact person
    "gender" : "<code>", // male | female | other | unknown
    "organization" : { Reference(Organization) }, // I Organization that is associated with the contact
    "period" : { Period } // The period during which this contact person or organization is valid to be contacted relating to this patient
  }],
  "communication" : [{ // A language which may be used to communicate with the patient about his or her health
    "language" : { CodeableConcept }, // R!  The language which can be used to communicate with the patient about his or her health
    "preferred" : <boolean> // Language preference indicator
  }],
  "generalPractitioner" : [{ Reference(Organization|Practitioner|
   PractitionerRole) }], // Patient's nominated primary care provider
  "managingOrganization" : { Reference(Organization) }, // Organization that is the custodian of the patient record
  "link" : [{ // Link to a Patient or RelatedPerson resource that concerns the same actual individual
    "other" : { Reference(Patient|RelatedPerson) }, // R!  The other patient or related person resource that the link refers to
    "type" : "<code>" // R!  replaced-by | replaces | refer | seealso
  }]
}

As you can see, it covers practically all the administrative information needs of a patient.

Recovering a patient from our HIS

If you remember from previous articles we have deployed a PostgreSQL database that simulates the database of an HIS system, let's take a look at the example tables that we have in our particular HIS.

There are not many, but they will be enough for our example. Let's look at our patient table in a little more detail.

Here we have our 3 example patients, as you can see each one has a unique identifier (ID) as well as a series of administrative data relevant to the health organization. Our first objective will be to obtain the FHIR resource for one of our patients.

Patient consultation

How can we request patient data from our server? According to the implementation specification made by FHIR, we must perform a GET via REST to a URL with the address of our server, the name of the resource and the identifier. We must invoke:

http://SERVER_PATH/Patient/{id}

For our example we will perform a search for Juan López Hurtado, with his id = 1, so we must invoke the following URL:

http://localhost:52774/Adapter/r4/Patient/1

For testing we will use Postman as a client. Let's see what the server's response is:

{
    "resourceType": "Patient",
    "address": [
        {
            "city": "TERUEL",
            "line": [
                "CALLE SUSPIROS 39 2ºA"
            ],
            "postalCode": "98345"
        }
    ],
    "birthDate": "1966-11-23",
    "gender": "M",
    "id": "1",
    "identifier": [
        {
            "type": {
                "text": "ID"
            },
            "value": "1"
        },
        {
            "type": {
                "text": "NHC"
            },
            "value": "588392"
        },
        {
            "type": {
                "text": "DNI"
            },
            "value": "12345678X"
        }
    ],
    "name": [
        {
            "family": "LÓPEZ HURTADO",
            "given": [
                "JUAN"
            ]
        }
    ],
    "telecom": [
        {
            "system": "phone",
            "value": "844324239"
        },
        {
            "system": "email",
            "value": "juanitomaravilla@terra.es"
        }
    ]
}

Now let's analyze the path that our request has taken within our production:

Here we have the path:

  1. Arrival of the request to our BS InteropService.
  2. Forwarding the request to the BP that we have configured as the destination of our BS where the patient identifier of the call received will be recovered.
  3. Query from our BO FromAdapterToHIS to our HIS database.
  4. Forwarding the patient data to our BP and transforming it to an FHIR Patient resource.
  5. Forwarding the response to the BS.

Let's take a look at the type of message we receive in our BP ProcessFHIRBP:

Let's look at three attributes that will be key to identifying what type of operation has been requested from the client:

  • Request.RequestMethod: which tells us what type of operation we are going to perform. In this example, the search for a patient will be a GET.
  • Request.RequestPath: this attribute contains the path of the request that arrived at our server, this attribute will indicate the resource on which we will work and in this case it will include the specific identifier for its recovery.
  • Quick.StreamId: FHIR Adapter will transform every FHIR message received into a Stream and assign it an identifier that will be saved in this attribute. For this example we will not need it since we are performing a GET and we are not sending any FHIR object.

Let's continue with the journey of our message by analyzing in depth the GLP responsible for the processing.

ProcessFHIRBP:

We have implemented a BPL in our production that will manage the FHIR messaging that we receive from our Business Service. Let's see how it is implemented:

Let's see the operations that we will perform in each step:

Manage FHIR object:

We will invoke the BO FromAdapterToHIS responsible for the connection to the HIS database and which will be in charge of the database query.

Method ManageFHIR(requestData As HS.FHIRServer.Interop.Request, response As Adapter.Message.FHIRResponse) As %Status
{
  set sc = $$$OK
  set response = ##class(Adapter.Message.FHIRResponse).%New()

  if (requestData.Request.RequestPath = "Bundle")
  {
    If requestData.QuickStreamId '= "" {
        Set quickStreamIn = ##class(HS.SDA3.QuickStream).%OpenId(requestData.QuickStreamId,, .tSC)
        
        set dynamicBundle = ##class(%DynamicAbstractObject).%FromJSON(quickStreamIn)

        set sc = ..GetBundle(dynamicBundle, .response)
      }
    
  }
  elseif (requestData.Request.RequestPath [ "Patient")
  {
    if (requestData.Request.RequestMethod = "POST")
    {
      If requestData.QuickStreamId '= "" {
        Set quickStreamIn = ##class(HS.SDA3.QuickStream).%OpenId(requestData.QuickStreamId,, .tSC)
        
        set dynamicPatient = ##class(%DynamicAbstractObject).%FromJSON(quickStreamIn)

        set sc = ..InsertPatient(dynamicPatient, .response)
      }      
    }
    elseif (requestData.Request.RequestMethod = "GET")
    {
      set patientId = $Piece(requestData.Request.RequestPath,"/",2)
      set sc = ..GetPatient(patientId, .response)
    }

  }
  Return sc
}

Our BO will check the message received of type HS.FHIRServer.Interop.Request, in this case by setting a GET and indicating in the path that corresponds to the Patient resource the GetPatient Method will be invoked, which we will see below:

Method GetPatient(patientId As %String, Output patient As Adapter.Message.FHIRResponse) As %Status
{
  Set tSC = $$$OK
  set sql="SELECT id, name, lastname, phone, address, city, email, nhc, postal_code, birth_date, dni, gender FROM his.patient WHERE id = ?"
  //perform the Select
  set tSC = ..Adapter.ExecuteQuery(.resultSet, sql, patientId)
  
  If resultSet.Next() {
     set personResult = {"id":(resultSet.GetData(1)), "name": (resultSet.GetData(2)), 
        "lastname": (resultSet.GetData(3)), "phone": (resultSet.GetData(4)), 
        "address": (resultSet.GetData(5)), "city": (resultSet.GetData(6)),
        "email": (resultSet.GetData(7)), "nhc": (resultSet.GetData(8)),
        "postalCode": (resultSet.GetData(9)), "birthDate": (resultSet.GetData(10)),
        "dni": (resultSet.GetData(11)), "gender": (resultSet.GetData(12)), "type": ("Patient")}

   } else {
     set personResult = {}
   }
  
  //create the response message
  do patient.Resource.Insert(personResult.%ToJSON())
 
	Return tSC
}

As you can see, this method only launches a query on the database of our HIS and recovers all the patient information, then generates a DynamicObject that is subsequently transformed into a String and stored in a variable of the type Adapter.Message.FHIRResponse. We have defined the Resource property as a String list to be able to display the response later in the trace. You could define it directly as DynamicObjects, saving subsequent transformations.

Check if Bundle:

With the response from our BO we check if it is a Bundle type (we will explain it in a future article) or if it is just a Resource.

Create Dynamic Object:

We transform the BO response into a DynamicObject and assign it to a temporary context variable (context.temporalDO). The function used for the transformation is the following:

##class(%DynamicAbstractObject).%FromJSON(context.FHIRObject.Resource.GetAt(1))

FHIR transform:

With our temporary variable of type DynamicObject we launch a transformation of it to an object of class HS.FHIR.DTL.vR4.Model.Resource.Patient. If we want to look for other types of resources we would have to define particular transformations for each type. Let's see our transformation:

This transformation allows us to have an object that our BS InteropService can interpret. We will store the result in the variable context.PatientResponse.

Assign resource to Stream:

We convert the variable context.PatientResponse obtained in FHIR transform to Stream.

Transform to QuickStream:

We assign to the response variable all the data that we must return to our client:

 set qs=##class(HS.SDA3.QuickStream).%New()
 set response.QuickStreamId = qs.%Id()
 set copyStatus = qs.CopyFrom(context.JSONPayloadStream)
 set response.Response.ResponseFormatCode="JSON"
 set response.Response.Status=200
 set response.ContentType="application/fhir+json"
 set response.CharSet = "utf8"
  

In this case we are always returning a 200 response. In a production environment we should check if we have correctly recovered the searched resource, and if not, modify the status of the response from 200 to a 404 corresponding to a "Not found". As you can see in this code fragment, the object HS.FHIR.DTL.vR4.Model.Resource.Patient
is transformed into a Stream and stored as a HS.SDA3.QuickStream, adding the identifier of said object to the QuickStreamID attribute, subsequently our InteropService service will return the result correctly as a JSON.

Conclusion:

Let's summarize what we have done:

  1. We have sent a GET type request to search for a Patient resource with a defined ID.
  2. The BS InteropService has forwarded the request to the configured BP.
  3. The BP has invoked the BO responsible for interacting with the HIS database.
  4. The configured BO has retrieved the patient data from the HIS database.
  5. The BP has transformed the result into an object understandable by the BS created by default InteropService.
  6. The BS has received the response and has forwarded it to the client.

As you can see, the operation is relatively simple, if we want to add more types of resources to our server we will only have to add in our BO the query to the tables of our database that correspond to the new resource to be recovered and include in our BP the transformation of the result of our BO to an object of type HS.FHIR.DTL.vR4.Model.Resource.* that corresponds.

In our next article we will review how we can add new FHIR resources of the Patient type to our HIS database.

Thank you all for your attention!

Discussion (3)1
Log in or sign up to continue