This topic contains 0 replies, has 1 voice, and was last updated by  Garry Christensen 3 years, 10 months ago.

FHIR Connectathon Experience Pt 2

  • In part one, I reported on the success and some of the issues relating to setting up out outbound feed from a clinical system (RIS) using Iguana to implement a FHIR interface. This time I’ll delve into placing a FHIR server in front of that same database.

    Before getting started though, I’d like to highlight one of the really useful features of Iguana that made this so much easier. One of the first requisites for my project was to develop a template for data types and resources and these were based on the published specifications. Once parsed, this resulted in a fully structured XML resource for the patient, ready to be populated. Mapping data now became a breeze because of the auto-complete function in Translator. I could easily see what nodes were available and select those to populate. Magic!

    On top of that, it’s re-usable from one project to the next. Of course, there’s also a downside. FHIR specifies that only populated nodes are to be included in the XML. Before sending any resource, the fhirDropEmpty() function removes any empty nodes.

    Moving on to the server functionality, I always knew this would be more of a challenge. I needed to implement the following functionality: return the results of queries through a GET, create a patient record sent with a POST and update patient details through a PUT.

    Of course, the FHIR server channel is very intimately tied to the database below it. I’ll try to stay away from the specifics of the database I used for this trial but there will unfortunately be some references that only work when this code is used with my database. I’m certain that you’ll be able to adapt it as necessary.

    The full project files are available at: https://drive.google.com/file/d/0B_ZkeyKb1tpmdUZTakJFR1BUZ1U/edit?usp=sharing

    The top level of the server if quite simple, look to see what operation is being performed and call the appropriate function:

    function main(Data)
    
       queue.push{data=Data}
       local R = net.http.parseRequest{data=Data}
       if R.method == 'GET' then
          RespondGet(R)
       elseif R.method == 'DELETE' then
          -- DELETE not supported
          local xOutcome = fhirCreate('OperationOutcome')
          xOutcome.issue.severity.value = 'error'
          xOutcome.issue.type.system.value = 'http://hl7.org/fhir/issue-type'
          xOutcome.issue.type.code.value = 'DELETE not-supported'
          xOutcome.issue.details.value = 'DELETE not supported'
          local sOutcome = '\r\n' .. xOutcome:fhirDropEmpty():S()
    
          net.http.respond{code=405,body=sOutcome,entity_type='text/xml'}
          iguana.logWarning('DELETE not supported')
    
       elseif R.method == 'POST' then
          RespondPost(R)
       elseif R.method == 'PUT' then
          RespondPut(R)
       end
    end

    The GET action is the simplest to implement and test so it’s a good place to start. There are 2 possible ways to use the GET – get a specific resource (http://localhost:6544/patient/@123) or perform a search for all resources that match zero or more parameter values (http://localhost:6544/patient/search?identifier=654321). As of version 5.5 the HTTPS channel will respond to URLs that are longer than the base. When the channel URL is set to http://localhost:6544/patient, both the above URLs will be directed to the translator script.

    See http://wiki.interfaceware.com/1407.html for details

          local tWhere = {}
          if R.location:find('@') then
             -- Return specific resource
             -- External reesource ID if includes alpha
             if R.location:split('@')[2]:find('%a') then
                table.insert(tWhere, "fhir.fhirid_external = '" .. R.location:split('@')[2] .. "'")         
             else
                table.insert(tWhere, 'fhir.fhirid = ' .. R.location:split('@')[2])
             end
    
             -- Get the rows from the database
             local tPatientList = getPatientDataList(dbConn, tWhere)
             if #tPatientList > 0 then
                -- map the patient list to xml so I can reuse the mapping function from the client channel
                local xPatientList = xml.parse(tPatientList[1]:toXML())
                -- map the patient details into the fhir resource
                local xPatient, sPatientID, sFhirID = MapPatient(xPatientList.Row)
    
                local sResponse = '\r\n' .. xPatient:fhirDropEmpty():S()
                iguana.logInfo('Query response: \r\n' .. sResponse) 
                -- return resource
                net.http.respond{entity_type='text/xml', body=sResponse}
             else
                -- no matches
                local xOutcome = fhirCreate('OperationOutcome')
                xOutcome.issue.severity.value = 'error'
                xOutcome.issue.type.system.value = 'http://hl7.org/fhir/issue-type'
                xOutcome.issue.type.code.value = 'Patient resource not-found'
                xOutcome.issue.details.value = 'Resource not found'
    
                local sOutcome = '\r\n' .. xOutcome:fhirDropEmpty():S()
    
                iguana.logWarning('Resource not found \r\n' .. sResponse) 
    
                net.http.respond{code=404,body=sOutcome,entity_type='text/xml'}
             end
          else
             -- search database
             if R.params.count then
                sReturnLimit = R.params.count
             end
             -- set the selected rows by the page number
             if not R.params.page then
                table.insert(tWhere, 'ROWNUM <= ' .. sReturnLimit)          else             nPage = tonumber(R.params.page)             table.insert(tWhere, 'ROWNUM >= ' .. (nPage-1) * sReturnLimit + 1 .. ' AND ROWNUM <= ' .. nPage * sReturnLimit )
             end
    
             if R.location == '/patient/search' then
                -- build the where conditions for the sql query
                for sKey, sValue in pairs(R.params) do
                   -- only equivelance supported
                   if sKey == '_format' then
                   elseif sKey == '_id' and sValue:find('%a') then
                      table.insert(tWhere, "fhir.fhirid_external = '" .. sValue .. "'") 
                   elseif sKey == '_id' then
                      table.insert(tWhere, "fhir.fhirid = " .. sValue) 
                   elseif sKey == 'family' then
                      table.insert(tWhere, "lower(reg.lastname) like '%" .. sValue:lower() .. "%'") 
                   elseif sKey == 'given' then
                      table.insert(tWhere, "lower(reg.firstname) like '%" .. sValue:lower() .. "%'") 
                   elseif sKey == 'name' then
                      table.insert(tWhere, "lower(reg.firstname || reg.lastname) like '%" .. sValue:lower() .. "%'") 
                   elseif sKey == 'gender' then
                      local sSearch = 'Unknown'
                      if sValue:upper() == 'M' then
                         sSearch = 'Male'
                      elseif sValue:upper() == 'F' then
                         sSearch = 'Female'
                      end
                      table.insert(tWhere, "lower(reg.sex) =  '" .. sSearch:lower() .. "'") 
                   elseif sKey == 'birthdate' then
                      table.insert(tWhere, "reg.dob = to_date('" .. sValue:sub(1,10) .. "', 'yyyy-mm-dd')") 
                   elseif sKey == 'address' then
                      table.insert(tWhere, [[lower(reg.street || reg.street2 || reg.city || reg.zip || reg.state
                         || reg.country) like '%]] .. sValue:lower() .. "%'") 
                   elseif sKey == 'identifier' then
                      -- include Australian specific identifiers
                      table.insert(tWhere, "(reg.patientid = '" .. sValue .. [['
                         or addreg.medicarenumber = ']] .. sValue .. [['
                         or addreg.dvanumber = ']] .. sValue .. [['
                         or addreg.safetynet = ']] .. sValue .. [['
                         or addreg.pensionnumber = ']] .. sValue .. [['
                         or addreg.HCCnumber = ']] .. sValue .. "')") 
                   end
                end
             end
             -- query the database
             local tPatientList = getPatientDataList(dbConn, tWhere)
    
             -- create the bundle
             local xResponse = fhirCreate('Atom')
             -- insert the patient resources into bundle
             mapPatientResponse(tPatientList, xResponse, R)
             -- remove any xml nodes that are empty
             xResponse:fhirDropEmpty()
    
             local sResponse = '\r\n' .. xResponse:S()
             iguana.logInfo('Query response: \r\n' .. sResponse) 
             -- send the response
             net.http.respond{entity_type='application/xml', body=sResponse}
    
          end

    The first thing the script needs to do is to look at which URL was used and take the appropriate action. In the code above, the tWhere table is a collection of where conditions that are appended to the database query. This is one place where the code example really is tied to the data structure that I’m using. In both the resource query and the resource search, I end up this a result set from the SQL query which contains the patient data.

    As I indicated in the previous post, the mapping of the patient data will be different for every system and sometimes you need to understand what the data represents. That’s not always obvious in FHIR. Take identifiers as an example. A patient may have several identifiers – MRN, SSN, etc. The FHIR specification does not include a standard Type parameter for the identifier. The Label is intended for display so URN and MRN are equally valid, depending on the site preference.

    In Australia, there can many national identifiers for different purposes so when one of the other Connectathon FHIR clients asked if they could query by identifier, I said ‘Which one?’. They said, ‘I don’t know, in my database it’s called PT number.’

    It became clear very early in this Connectathon that a standard set of identifier codes was needed. That was quite simple. FHIR supports extensions for every element so it was just a matter of defining a standard extension for Australian identifiers and because the right people were in the room at the time, this is now part of the Australian specific definition.

    Pretty simple in this case but anyone can define their own extensions. Think of the terror of V2 Z segments and multiply it by every field .

    Here’s the basic patient mapping code. Creating this was simple once I had the data and the patient resource:

    function MapPatient(d)
       -- create a patient resource
       local p = fhirCreate('Patient')
    
       local sPatID = d.PATIENTID.value:S()
       local sFhirID = d.FHIRID.value:S()
    
       -- set the identifiers, new node for each identifier
       setID({d.PATIENTID}, p, 'MRN', 'MR', 'http://fujisynapse.com.au/fhir/profile/identifier')
       setID({d.MEDICARENUMBER, d.MEDICAREEXP}, p, 'Medicare Number', 'MC', 'http://dhs.gov.au/medicare-card')
    
       if d.DVACOLOUR.value:S() == 'Gold' then
          setID({d.DVANUMBER, d.DVAEXP}, p, 'DVG', 'http://dva.gov.au/id-card')
       elseif d.DVACOLOUR.value:S() == 'Orange' then
          setID({d.DVANUMBER, d.DVAEXP}, p, 'DVO', 'http://dva.gov.au/id-card')
       elseif d.DVACOLOUR.value:S() == 'White' then
          setID({d.DVANUMBER, d.DVAEXP}, p, 'DVW', 'http://dva.gov.au/id-card')
       elseif d.DVACOLOUR.value:S() ~= '' then
          setID({d.DVANUMBER, d.DVAEXP}, p, 'DVA', 'http://dva.gov.au/id-card')
       end
    
       setID({d.HCCNUMBER, d.HEALTHCAREEXP}, p, 'Healthcare Card Number', 'HC', 'http://dhs.gov.au/healthcare-card')
       setID({d.PENSIONNUMBER, d.PENSIONEXP}, p, 'Pension Number', 'PEN', 'http://dhs.gov.au/pension')
       setID({d.SAFETYNET, d.SAFETYNETEXP}, p, 'Safety Net Number', 'SN', 'http://dhs.gov.au/safetynet-card')
    
       if d.LASTNAME.value:S() ~= '' then
          p.name.use.value = 'official'
          p.name.family.value =d.LASTNAME.value:S()
          p.name.given.value =d.FIRSTNAME.value:S() .. ' ' .. d.MIDDLE.value:S()
          p.name.prefix.value = d.PREFIX.value:S()
       end
       -- if there's an alias then add another name node
       if d.LASTNAMEALIAS.value:S() ~= '' then
          xAlias = p:fhirInsertComplex('name', 'humanName')
          xAlias.family.value = d.LASTNAMEALIAS.value
          xAlias.given.value = d.FIRSTNAMEALIAS.value
          xAlias.use.value = 'anonymous'
       end
    
       p.birthDate.value = d.DOB.value:S():sub(1,10)
    
       if d.SEX.value:S() ~= '' then
          p.gender.coding.code.value = d.SEX.value:S()
          if d.SEX.value:S() == 'M' then
             p.gender.coding.display.value = 'Male'
             p.gender.text.value = 'Male'
          elseif d.SEX.value:S() == 'F' then
             p.gender.coding.display.value = 'Female'
             p.gender.text.value = 'Female'
          else
             p.gender.coding.display.value = 'Unknown'
             p.gender.text.value = 'Unknown'
          end
          p.gender.coding.system.value = ' http://hl7.org/fhir/vs/administrative-gender'
       end
    
       p.address.line.value = d.STREET.value:S()
       line2 = p.address:fhirInsertPrimitive('line')
       line2.value = d.STREET2.value:S()
       p.address.city.value = d.CITY.value:S()
       p.address.state.value = d.STATE.value:S()
       p.address.zip.value = d.ZIP.value:S()
       if not p.address:fhirIsEmpty() then
          p.address.use.value = 'home'
       end
       -- postal address 
       padd2 = p:fhirInsertComplex('address', 'address')
       padd2.line.value = d.PATIENTADDRESS2.value:S()
       padd2.city.value = d.PATIENTCITY2.value:S()
       padd2.state.value = d.PATIENTSTATE2.value:S()
       padd2.zip.value = d.PATIENTZIP2.value:S()
       -- extension to specify that this is not a physical location
       if not padd2:fhirIsEmpty() then
          local e = padd2:fhirInsertComplex('extension', 'extension', 1)
          e.url.value = 'http://hl7.org/fhir/Profileiso-21090/address-use'
          e:fhirInsertPrimitive('valueCode')
          e.valueCode.value = 'PST'
          padd2.use.value = 'home'
       end
    
       if d.HOMEPHONE.value:S() ~= '' then
          p.telecom.value.value = d.HOMEPHONE.value
          p.telecom.use.value = 'home'
          p.telecom.system.value = 'phone'  
       end
       if d.EMPLOYERPHONE.value:S() ~= '' then
          ph = p:fhirInsertComplex('telecom', 'contact')
          ph.value.value = d.EMPLOYERPHONE.value:S()
          ph.use.value = 'work'
          ph.system.value = 'phone'
       end
       if d.MOBILEPHONE.value:S() ~= '' then
          ph = p:fhirInsertComplex('telecom', 'contact')
          ph.value.value = d.MOBILEPHONE.value:S()
          ph.use.value = 'mobile'
          ph.system.value = 'phone'
       end
       if d.EMAIL.value:S() ~= '' then
          ph = p:fhirInsertComplex('telecom', 'contact')
          ph.value.value = d.EMAIL.value:S()
          ph.use.value = 'home'
          ph.system.value = 'email'
       end
    
       p.deceasedDatetime.value = d.PATIENTDATEOFDEATH.value:S():sub(1,10)
    
       tEM = d.EMERGENCYCONTACT.value:S():split('%(')
       tEName = tEM[1]:split(',')
       p.contact.name.family.value = tEName[1]
       if #tEName > 1 then
          p.contact.name.given.value = tEName[2]
       end
       if #tEM > 1 then
          p.contact.relationship.text.value = tEM[2]:gsub('%)', '')
       end
       if d.EMERGENCYPHONE.value:S() ~= '' then
          p.contact.telecom.value.value = d.EMERGENCYPHONE.value:S()
          p.contact.telecom.system.value = 'phone'
       end
    
       -- always include a human readable summary
       local sSummary = '

    ‘ sSummary = sSummary .. ” sSummary = sSummary .. ‘
    ‘ sSummary = sSummary .. ‘
    ‘ sSummary = sSummary .. ‘
    <table&rt;
    <tbody&rt;
    <tr&rt;
    <td&rt;Patient ID:</td&rt;
    <td&rt;<b&rt;’ .. d.PATIENTID.value:S() .. ‘</b&rt;</td&rt;
    </tr&rt;
    <tr&rt;
    <td&rt;Name:</td&rt;
    <td&rt;<b&rt;’ .. d.FIRSTNAME.value:S() .. ‘ ‘ .. d.LASTNAME.value:S() .. ‘</b&rt;</td&rt;
    <td&rt;Address:</td&rt;
    <td&rt;<b&rt;’ .. d.STREET.value:S() .. ‘ ‘ .. d.STREET2.value:S() .. ‘, ‘ .. d.CITY.value:S() .. ‘, ‘ .. d.STATE.value:S() .. ‘, ‘ .. d.ZIP.value:S() .. ‘</b&rt;</td&rt;
    </tr&rt;
    </tbody&rt;
    </table&rt;
    ‘ p.text.status.value = ‘generated’ p.text.div:setInner(sSummary) return p, sPatID, sFhirID end function setID(tID, p, sLabel, sType, sSystem) — the the identifier isn’t blank if tID[1].value:S() ~= ” then –find the last identifier node _,Id = p:fhirFindChild(‘identifier’) –set the values Id.use.value = ‘official’ Id.system.value = sSystem Id.label.value = sLabel Id.key.value = tID[1].value:S() — add an extension and set the values local e = Id:fhirInsertComplex(‘extension’, ‘extension’, 1) e.url.value = ‘http://hl7.org.au/fhir/profile/extensions#’ .. sType .. ‘-type’ e:fhirInsertPrimitive(‘valueCode’) e.valueCode.value = sType if tID[2] and tID[2].value:S() ~= ” then Id.period[‘end’].value = tID[2].value:S():sub(1,10) end — insert another empty identifier node ready for the next identifier tNext = p:fhirInsertComplex(‘identifier’, ‘identifier’) end return Id, tNext end

    When the GET is a search for resources, the database query returns multiple rows. Each of the rows is returned in it’s own resource inside an Atom bundle. This is a well defined internet standard. Of course, there is no control over what may be searched and it’s simple to ask for all the patients in the database, such as “http://localhost:654…rch?_format=XML”. The definition of the bundle allows the results to be broken into pages and provides links to the next and previous pages. The search parameters may include both page number and the number of responses per page.

    In this example, the pages are managed through the SQL query and the supported query parameters filter the results. Once the result set is returned, create an Atom bundle and include the resources for each row:

    function mapPatientResponse(tPatientList, xResponse, R)
       local sURL = R.headers.Host .. R.location .. '?'
       local nNext, nPrev 
       local sThis = '1'
    
       -- build the link url
       for p,v in pairs(R.params) do
          if p == '_page' then
             nNext = v + 1
             nPrev = v - 1
             sThis = v 
          else
             sURL = sURL .. p .. '=' .. v .. '&' 
          end
       end
    
       -- set the bundle details
       xResponse.title:setInner('Search results for Patient resources')
       xResponse.id:setInner('urn:uuid:' .. util.guid(128))
       xResponse.link.href = sURL .. 'page=' .. sThis .. '&count=' .. sReturnLimit
       xResponse:child("link", 2).rel = sURL .. 'page=1&count=' .. sReturnLimit
       if nPrev then
          xResponse:child("link", 3).rel = sURL .. 'page=' .. nPrev .. '&count=' .. sReturnLimit
       else
          xResponse:child("link", 3).rel = ''
       end
       if nNext then
          xResponse:child("link", 4).rel = sURL .. 'page=' .. nNext .. '&count=' .. sReturnLimit
       else
          xResponse:child("link", 4).rel = ''
       end
       xResponse:child("link", 5).rel = ''
       xResponse.totalResults:setInner(tostring(#tPatientList))
       xResponse.updated:setInner(fhirNow())
       xResponse.author.name:setInner('Synapse RIS')
       xResponse.Signature.xmlns = ''
    
       xResponse.entry.link.rel = ''
       xResponse.entry.author.name:setInner('Synapse RIS')
       xResponse.entry.category.scheme = ''
       xResponse.entry.summary.type = ''
       xResponse.entry.summary.div.xmlns = ''
    
       local nInc = 1
       for n = 1, #tPatientList do
          -- insert the patient resources
          if n < #tPatientList then
             -- copy the blank one
             xResponse:fhirDuplicateChild('entry')
          else
             nInc = 0
          end
    
          -- convert the result set to xml so I can reuse the mappings from the client
          xPatientList = xml.parse(tPatientList[n]:toXML())
          -- create the patient resource and map data
          local xPatient, sPatientID, sFhirID = MapPatient(xPatientList.Row)
    
          -- set the resource details
          local xEntry = xResponse[xResponse:fhirFindChild('entry') - nInc]
          -- insert the patient node into the response
          xEntry.content:fhirAddNode(xPatient)
          xEntry.title:setInner('Patient ' .. tostring(n))
          xEntry.id:setInner(sMyURL .. '@' .. sFhirID)
          -- use the current time
          xEntry.updated:setInner(fhirNow())
    
          -- always include a human readable summary
          xEntry.summary.type = 'xhtml'
          xEntry.summary.div.xmlns = 'http://www.w3.org/1999/xhtml'
          sSummary = '

    ‘ sSummary = sSummary .. ‘
    <table&rt;
    <tbody&rt;
    <tr&rt;
    <td&rt;Patient ID:</td&rt;
    <td&rt;<b&rt;’ .. tPatientList[n].PATIENTID:S() .. ‘</b&rt;</td&rt;
    </tr&rt;
    <tr&rt;
    <td&rt;Name:</td&rt;
    <td&rt;<b&rt;’ .. tPatientList[n].FIRSTNAME:S() .. ‘ ‘ .. tPatientList[n].LASTNAME:S() .. ‘</b&rt;</td&rt;
    </tr&rt;
    </tbody&rt;
    </table&rt;
    ‘ xEntry.summary.div:setInner(sSummary) end return xResponse end

    This was tested against many clients at the Connectathon and worked well. Now the fun really begins.

    There needs to be functionality to create and update patient details in the database from FHIR POST and PUT interactions. In both cases the patient resource is contained in the body of the request. By definition, POST creates a new resource where the receiving system allocates the resource ID. The PUT allows an update of an existing resource and the resource ID is specified in the URL. Logically, both of those are not to complex.

    The first consideration arises with the ability to create a resource with a PUT. The resource ID in the receiving system is dictated by the sending system. From the Connectathon experience, most systems sending resources did not use the ‘sociable’ protocol of looking to see if it exists in the receiving system first and using that resource ID if it does. It’s far simpler for the sending system to implement if it just sends a PUT and lets the receiver work out if it needs to be created or updated.

    So what resource ID will the sending system specify? Everyone will approach it differently but it will probably be their own resource ID or some other number, or perhaps include alpha characters as well. As the receiver, I have no way of knowing what schema they will use. Just as importantly, if I have feeds from multiple sources, can I guarantee that there will be no duplication?

    Perhaps namespaces is the answer but I don’t see where they could fit. Sure, if a patient administration system (PAS) sends my system a new patient using a PUT, I can create it with their resource ID and tag that ID to the sending system. But that patient now represents a resource in my system so it’s under my namespace when I pass it to a downstream system or someone else queries my database – it will certainly have my URL attached to it in a resource reference. Should I use my own resource ID?

    Suddenly we have multiple systems trying to manage resource IDs from multiple other systems and work out who is querying for a resource, which namespace to apply and which ID to use. Let’s face it, the patient in my system should be unique and it should have 1 resource ID and it should be mine, period.

    But alas, that’s not the FHIR spec. I need to cater for a PUT where the sending system can specify what my resource will be. That means I need to place some constraints on the interaction. I need to make sure that the resource ID has not already been used for a different patient. Here again, we need to define the meaning of the content so I can choose a suitable unique key, like MRN. I also need to ensure that the resource ID I generate for my resources will not conflict with an existing external resource ID.

    In this case, I’ve assigned the ‘MR’ identifier type (as defined locally through an extension) as the unique key and I’ll insist that all external IDs must have an alphabetic character so it won’t conflict with my numeric only IDs.

    The rest is just the reverse of the query, extract the information from the resource and map it to the correct database fields, check that the data integrity rules for my database are met, and then update the patient row if it exists or create it if it doesn’t.

    In that respect, a large part of this code is error checking. This is only the code for the PUT as it covers the same functionality as in a POST:

    function RespondPut(R)
       -- update/create a patient resource
    
       -- parse the resource
       local bOK, xPatient = pcall(xml.parse,R.body)
       local sResourceID = R.location:gsub('/patient',''):sub(3)
    
       if not bOK then
          -- error parsing resource
          local xOutcome = fhirCreate('OperationOutcome')
          xOutcome.issue.severity.value = 'error'
          xOutcome.issue.type.system.value = 'http://hl7.org/fhir/issue-type'
          xOutcome.issue.type.code.value = 'XML Structure'
          xOutcome.issue.details.value = 'Unable to parse XML'
          local sOutcome = '\r\n' .. xOutcome:fhirDropEmpty():S()
    
          net.http.respond{code = 400, body=sOutcome,entity_type='text/xml'} 
          iguana.logWarning('Unable to parse XML /r/n' .. R.body)
    
       else
          if not xPatient.Patient then
             -- no patient resource
             local xOutcome = fhirCreate('OperationOutcome')
             xOutcome.issue.severity.value = 'error'
             xOutcome.issue.type.system.value = 'http://hl7.org/fhir/issue-type'
             xOutcome.issue.type.code.value = 'Patient resource not-found'
             xOutcome.issue.details.value = 'Patient details missing'
             local sOutcome = '\r\n' .. xOutcome:fhirDropEmpty():S()
             net.http.respond{code = 400, body=sOutcome,entity_type='text/xml'}
             iguana.logWarning('Patient details missing')         
          else
             local xPat = xPatient.Patient
    
             -- map the data from the patient resource to tables that match the database tables
             local tReg, tAddReg, sPatientID = mapPatientData(xPat, sPatientID)
    
             local dbConn = RISConnection()            
             if dbConn.query then
                -- look for existing resource id
                local sQuery = ''
                if sResourceID:find('%a') then
                   -- contains alpha so external resource id
                   sQuery = [[SELECT patientid 
                   FROM fhir_id
                   WHERE fhirid_external = ']] .. sResourceID .. "'"               
                else
                   sQuery = [[SELECT patientid 
                   FROM fhir_id
                   WHERE fhirid = ']] .. sResourceID .. "'"                           
                end
    
                local tResult = dbConn:query{sql=sQuery}
    
                if #tResult > 0 and sPatientID ~= tResult[1].PATIENTID:S() then
                   -- duplicate patient
                   local xOutcome = fhirCreate('OperationOutcome')
                   xOutcome.issue.severity.value = 'error'
                   xOutcome.issue.type.system.value = 'http://hl7.org/fhir/issue-type'
                   xOutcome.issue.type.code.value = 'Duplicate patient business-rule'
                   xOutcome.issue.details.value = 'Patient ID already exists for different resource or resource identifies different patient'
                   local sOutcome = '\r\n' .. xOutcome:fhirDropEmpty():S()
                   net.http.respond{code = 400, body=sOutcome,entity_type='text/xml'}
                   iguana.logWarning('Patient ID already exists')
                elseif #tResult > 0 then
                   -- patient exists so update
                   -- build the sql merge command from each table
                   sRegMerge = db.BuildMerge('registration', 'patientid', tReg)
                   sAddRegMerge = db.BuildMerge('additionalregistration', 'patientid', tAddReg)
    
                   -- begin transacttion and execute the merge commands
                   dbConn:begin()
    
                   local bOK, sDBResult = db.pexecute(dbConn, sRegMerge)
    
                   if bOK then
                      bOK, sDBResult = db.pexecute(dbConn, sAddRegMerge)
                   end
    
                   if not bOK then
                      -- merge failure
                      dbConn:rollback()
                      iguana.logInfo('Database failure: /r/n' .. sDBResult)
    
                      local xOutcome = fhirCreate('OperationOutcome')
                      xOutcome.issue.severity.value = 'error'
                      xOutcome.issue.type.system.value = 'http://hl7.org/fhir/issue-type'
                      xOutcome.issue.type.code.value = 'Proccessing'
                      xOutcome.issue.details.value = 'Data could not be written to database'
                      local sOutcome = '\r\n' .. xOutcome:fhirDropEmpty():S()
                      net.http.respond{code = 500, body=sOutcome,entity_type='text/xml'}
                   else
                      -- commit data
                      dbConn:commit()
                      iguana.logInfo('Patient created in database')
    
                      -- now get the data created and build a new patient resource to return
                      tWhere = {"reg.patientid = '" .. sPatientID .. "'"}
                      local tPatientList = getPatientDataList(dbConn, tWhere)
    
                      if #tPatientList == 0 then
                         -- not found 🙁
                         local xOutcome = fhirCreate('OperationOutcome')
                         xOutcome.issue.severity.value = 'error'
                         xOutcome.issue.type.system.value = 'http://hl7.org/fhir/issue-type'
                         xOutcome.issue.type.code.value = 'Proccessing'
                         xOutcome.issue.details.value = 'Data could not be written to database'
                         local sOutcome = '\r\n' .. xOutcome:fhirDropEmpty():S()
                         net.http.respond{code = 500, body=sOutcome,entity_type='text/xml'}
                      else
                         -- convert the result set to xml so I can re-use the mapping from the client
                         local xPatientList = xml.parse(tPatientList[1]:toXML())
                         local xPatient, sPatientID, sFhirID = MapPatient(xPatientList.Row)
    
                         local sResponse = '\r\n' .. xPatient:fhirDropEmpty():S()
                         iguana.logInfo('Update Patient response: \r\n' .. sResponse) 
    
                         -- return the response
                         local sLocation = R.headers.Host .. '/patient/@' .. sResourceID
                         net.http.respond{code=200, entity_type='text/xml', headers = {'Content-Location: ' .. sLocation}, body=sResponse}
                      end
                   end
                elseif sPatientID == '' then
                   -- error no identifier supplied
                   local xOutcome = fhirCreate('OperationOutcome')
                   xOutcome.issue.severity.value = 'error'
                   xOutcome.issue.type.system.value = 'http://hl7.org/fhir/issue-type'
                   xOutcome.issue.type.code.value = 'Patient ID required'
                   xOutcome.issue.details.value = 'Patient ID with label MR not supplied'
                   local sOutcome = '\r\n' .. xOutcome:fhirDropEmpty():S()
                   net.http.respond{code=422, body=sOutcome, entity_type='text/xml'}
                   iguana.logWarning('Patient ID not supplied')
                elseif not tReg.lastname then
                   -- error no last name supplied
                   local xOutcome = fhirCreate('OperationOutcome')
                   xOutcome.issue.severity.value = 'error'
                   xOutcome.issue.type.system.value = 'http://hl7.org/fhir/issue-type'
                   xOutcome.issue.type.code.value = 'Family name required'
                   xOutcome.issue.details.value = 'Family name not supplied'
                   local sOutcome = '\r\n' .. xOutcome:fhirDropEmpty():S()
                   net.http.respond{code=422, body=sOutcome, entity_type='text/xml'}
                   iguana.logWarning('Family name not supplied')
                else
                   -- create patient
                   -- check of patient already exists
                   sQuery = [[SELECT patientid 
                   FROM registration
                   WHERE patientid = ']] .. sPatientID .. "'"            
                   tResult = dbConn:query{sql=sQuery}
    
                   if #tResult > 0 then
                      -- duplicate patient
                      local xOutcome = fhirCreate('OperationOutcome')
                      xOutcome.issue.severity.value = 'error'
                      xOutcome.issue.type.system.value = 'http://hl7.org/fhir/issue-type'
                      xOutcome.issue.type.code.value = 'Duplicate patient business-rule'
                      xOutcome.issue.details.value = 'Patient ID already exists'
                      local sOutcome = '\r\n' .. xOutcome:fhirDropEmpty():S()
                      net.http.respond{code = 400, body=sOutcome,entity_type='text/xml'}
                      iguana.logWarning('Patient ID already exists')
                   elseif not sResourceID:find('%a') then
                      -- external resources must contain an alpha
                      local xOutcome = fhirCreate('OperationOutcome')
                      xOutcome.issue.severity.value = 'error'
                      xOutcome.issue.type.system.value = 'http://hl7.org/fhir/issue-type'
                      xOutcome.issue.type.code.value = 'External ID business-rule'
                      xOutcome.issue.details.value = 'External resource IDs must contain at least one alpha character'
                      local sOutcome = '\r\n' .. xOutcome:fhirDropEmpty():S()
                      net.http.respond{code = 400, body=sOutcome,entity_type='text/xml'}
                      iguana.logWarning('Invalid external Resource ID')
                   else
                      -- build the merges
                      sRegMerge = db.BuildMerge('registration', 'patientid', tReg)
                      sAddRegMerge = db.BuildMerge('additionalregistration', 'patientid', tAddReg)
    
                      -- begin the transaction and execute
                      dbConn:begin()
    
                      local bOK, sDBResult = db.pexecute(dbConn, sRegMerge)
    
                      if bOK then
                         bOK, sDBResult = db.pexecute(dbConn, sAddRegMerge)
                      end
    
                      if bOK then
                         -- set the external resource id
                         bOK, sDBResult = db.pexecute(dbConn, [[UPDATE fhir_id
                            SET fhirid_external = ']] .. sResourceID .. [['
                            WHERE patientid = ']] .. sPatientID .. "'")
                      end
    
                      if not bOK then
                         -- error so rollback
                         dbConn:rollback()
                         iguana.logInfo('Database failure: /r/n' .. sDBResult)
    
                         local xOutcome = fhirCreate('OperationOutcome')
                         xOutcome.issue.severity.value = 'error'
                         xOutcome.issue.type.system.value = 'http://hl7.org/fhir/issue-type'
                         xOutcome.issue.type.code.value = 'Proccessing'
                         xOutcome.issue.details.value = 'Data could not be written to database'
                         local sOutcome = '\r\n' .. xOutcome:fhirDropEmpty():S()
                         net.http.respond{code = 500, body=sOutcome,entity_type='text/xml'}
                      else
                         -- commit the data
                         dbConn:commit()
                         iguana.logInfo('Patient created in database')
    
                         -- get the patient details just created 
                         tWhere = {"reg.patientid = '" .. sPatientID .. "'"}
                         local tPatientList = getPatientDataList(dbConn, tWhere)
    
                         if #tPatientList == 0 then
                            -- error, something went wrong
                            local xOutcome = fhirCreate('OperationOutcome')
                            xOutcome.issue.severity.value = 'error'
                            xOutcome.issue.type.system.value = 'http://hl7.org/fhir/issue-type'
                            xOutcome.issue.type.code.value = 'Proccessing'
                            xOutcome.issue.details.value = 'Data could not be written to database'
                            local sOutcome = '\r\n' .. xOutcome:fhirDropEmpty():S()
                            net.http.respond{code = 500, body=sOutcome,entity_type='text/xml'}
                         else
                            -- convert the result set to xml so I can re-use the mapping from the client
                            local xPatientList = xml.parse(tPatientList[1]:toXML())
                            local xPatient, sPatientID, sFhirID = MapPatient(xPatientList.Row)
    
                            local sResponse = '\r\n' .. xPatient:fhirDropEmpty():S()
                            iguana.logInfo('Create Patient response: \r\n' .. sResponse) 
    
                            -- return the response
                            local sLocation = R.headers.Host .. '/patient/@' .. sResourceID
                            net.http.respond{code=201, entity_type='text/xml', headers = {'Content-Location: ' .. sLocation}, body=sResponse}
                         end
                      end
                   end
                   dbConn:close()
                end
             else
                iguana.logInfo('Database connection failure.' )
    
                local xOutcome = fhirCreate('OperationOutcome')
                xOutcome.issue.severity.value = 'error'
                xOutcome.issue.type.system.value = 'http://hl7.org/fhir/issue-type'
                xOutcome.issue.type.code.value = 'Proccessing'
                xOutcome.issue.details.value = 'Data could not be written to database'
                local sOutcome = '\r\n' .. xOutcome:fhirDropEmpty():S()
                net.http.respond{code = 500, body=sOutcome,entity_type='text/xml'}
             end
          end
       end
    end

    The FHIR specification says:

    “If the interaction is successful, the server must return either a 200 OK if the resource was updated, or a 201 Created if the resource was created, along with a copy of the newly updated resource (which might not be the same as that submitted) with the response”

    I was surprised that most systems at the Connectathon chose to simple wrap up the resource that was sent to them as the response. True most were just storing the resource, sometimes as an XML document, so this was valid. When the server is in front of a clinical database, I think it’s important to return the actual data stored. That means querying the patient details back from the database and constructing a new resource from the actual stored data. That has been implemented in the above code.

    I think I’ve gone on too long already so I’ll wrap up my overall impressions from the Connectathon and some of the challenges to come in another post.

    The full project files are available at: https://drive.google.com/file/d/0B_ZkeyKb1tpmdUZTakJFR1BUZ1U/edit?usp=sharing

Tagged: 

You must be logged in to reply to this topic.