This topic contains 0 replies, has 1 voice, and was last updated by Garry Christensen 9 years, 6 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 endWhen 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 endThis 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
You must be logged in to reply to this topic.