📜 ⬆️ ⬇️

Development of dynamic REST services on document-oriented DB Bagri

Not so long ago, looking through the CNews tape, I came across the announcement of the conference “ IT in Healthcare: Waiting for a Breakthrough .” It turns out that “since 2011, a large-scale state project is being implemented in Russia to introduce the Unified State Information System in the Healthcare Sector (EHISS)”. Having gone deep into the material, I found out that EGIZS is based on the standards of the Health Language 7 organization widely used in the West (hereinafter HL7). And the HL7 standards are based on XML. There was a desire to build a prototype of the system processing HL7 documents on the Bagri document database and, if the prototype is successful, prepare a report on it to the conference.

image


Had some time to go into the study of documents HL7. Then, by the way, and on Habré I found a good cycle of articles about this technology from Wayfarer15 . Along the way, I found out that the latest actively developed standard in this area is Fast Healthcare Interoperability Resources (hereinafter FHIR). FHIR is based on REST technology and the exchange of XML / JSON documents through REST resources.

How does this apply to Bagri? It turned out that quite: about a month ago, support for REST was added to Bagri, as well as the ability to dynamically determine REST resources in XQuery modules using RESTXQ annotations. Those. any FHIR resource can be dynamically created and published, even without restarting the Bagri servers. Let's try?
')

We create a prototype of the FHIR server in 45 minutes ..


For this we need:


Create a new scheme in the Bagri configuration file (<bagri_home> /config/config.xml), let's call it FHIR.

FHIR scheme in the Bagri configuration file
<schema name="FHIR" active="true"> <version>1</version> <createdAt>2016-11-09T23:14:40.096+03:00</createdAt> <createdBy>admin</createdBy> <description>FHIR: schema for FHIR XML demo</description> <properties> <!--      --> <entry name="xdm.schema.ports.first">11000</entry> <entry name="xdm.schema.ports.last">11100</entry> <entry name="xdm.schema.members">localhost</entry> <entry name="xdm.schema.thread.pool">16</entry> <entry name="xdm.schema.query.cache">true</entry> <!--    ,     XML --> <entry name="xdm.schema.store.data.path">../data/fhir/xml</entry> <entry name="xdm.schema.store.type">File</entry> <entry name="xdm.schema.format.default">XML</entry> <entry name="xdm.schema.partition.count">271</entry> <entry name="xdm.schema.population.size">1</entry> <entry name="xdm.schema.buffer.size">64</entry> <entry name="xdm.schema.store.enabled">true</entry> <entry name="xdm.schema.data.cache">NEVER</entry> <entry name="xdm.schema.data.stats.enabled">true</entry> <entry name="xdm.schema.trans.backup.async">0</entry> <entry name="xdm.schema.trans.backup.sync">1</entry> <entry name="xdm.schema.trans.backup.read">false</entry> <entry name="xdm.schema.data.backup.read">false</entry> <entry name="xdm.schema.data.backup.async">1</entry> <entry name="xdm.schema.data.backup.sync">0</entry> <entry name="xdm.schema.dict.backup.sync">0</entry> <entry name="xdm.schema.dict.backup.async">1</entry> <entry name="xdm.schema.dict.backup.read">true</entry> <entry name="xdm.schema.query.backup.async">0</entry> <entry name="xdm.schema.query.backup.sync">0</entry> <entry name="xdm.schema.query.backup.read">true</entry> <entry name="xdm.schema.transaction.timeout">60000</entry> <entry name="xdm.schema.health.threshold.low">25</entry> <entry name="xdm.schema.health.threshold.high">0</entry> <entry name="xdm.schema.store.tx.buffer.size">2048</entry> <entry name="xdm.schema.population.buffer.size">1000000</entry> <entry name="xdm.schema.query.parallel">true</entry> <entry name="xdm.schema.partition.pool">32</entry> <entry name="xqj.schema.baseUri">file:/../data/fhir/xml/</entry> <entry name="xqj.schema.orderingMode">2</entry> <entry name="xqj.schema.queryLanguageTypeAndVersion">1</entry> <entry name="xqj.schema.bindingMode">0</entry> <entry name="xqj.schema.boundarySpacePolicy">1</entry> <entry name="xqj.schema.scrollability">1</entry> <entry name="xqj.schema.holdability">2</entry> <entry name="xqj.schema.copyNamespacesModePreserve">1</entry> <entry name="xqj.schema.queryTimeout">0</entry> <entry name="xqj.schema.defaultFunctionNamespace">http://www.w3.org/2005/xpath-functions</entry> <entry name="xqj.schema.defaultElementTypeNamespace">http://www.w3.org/2001/XMLSchema</entry> <entry name="xqj.schema.copyNamespacesModeInherit">1</entry> <entry name="xqj.schema.defaultOrderForEmptySequences">2</entry> <entry name="xqj.schema.defaultCollationUri">http://www.w3.org/2005/xpath-functions/collation/codepoint</entry> <entry name="xqj.schema.constructionMode">1</entry> </properties> <!--    Patient --> <collections> <collection id="1" name="Patients"> <version>1</version> <createdAt>2016-11-09T23:14:40.096+03:00</createdAt> <createdBy>admin</createdBy> <docType>/{http://hl7.org/fhir}Patient</docType> <description>All patient documents</description> <enabled>true</enabled> </collection> </collections> <fragments/> <!--    /Patient/id/@value     id  --> <indexes> <index name="idx_patient_id"> <version>1</version> <createdAt>2016-11-09T23:14:40.096+03:00</createdAt> <createdBy>admin</createdBy> <docType>/{http://hl7.org/fhir}Patient</docType> <path>/{http://hl7.org/fhir}Patient/{http://hl7.org/fhir}id/@value</path> <dataType xmlns:xs="http://www.w3.org/2001/XMLSchema">xs:string</dataType> <caseSensitive>true</caseSensitive> <range>false</range> <unique>true</unique> <description>Patient id</description> <enabled>true</enabled> </index> </indexes> <resources> <!--  ,  ,      http://localhost:3030/ --> <resource name="common"> <version>1</version> <createdAt>2016-11-09T23:14:40.096+03:00</createdAt> <createdBy>admin</createdBy> <path>/</path> <module>common_module</module> <description>FHIR Conformance resource exposed via REST</description> <enabled>true</enabled> </resource> <!--  ,     http://localhost:3030/Patient --> <resource name="patient"> <version>1</version> <createdAt>2016-11-09T23:14:40.096+03:00</createdAt> <createdBy>admin</createdBy> <path>/Patient</path> <module>patient_module</module> <description>FHIR Patient resource exposed via REST</description> <enabled>true</enabled> </resource> </resources> <triggers/> </schema> 


We will unpack the test data on a local disk in the <bagri_home> / data / fhir / xml directory. I wrote about working with JSON documents in Bagri in the previous article , so in this example, to save space, I will show only work with data in XML format.

At the time of writing, the FHIR specification defined 110 standard resources that can be accessed by the server. Some of them are official ones and are used to provide information about the system itself, and the rest are application resources that perform work with medical data. The service resource Conformance is mandatory for implementation and provides information about the available functionality of the system. The presence or absence of other resources and their behavior is determined by the fact that we declare in Conformance.

Application resources, according to the FHIR specification, can publish the following methods:

Resource level operations:


Operations at the resource type level:


For illustrative purposes, we implement 2 resources: the already designated Conformance and the Patient application resource. Conformance will determine what functionality will be available to clients of the Patient resource.

Below in the text will be a lot of emoticons. Do not worry, these are XQuery syntax costs :).

The implementation of Conformance for our prototype looks quite simple: create a new XQuery module <bagri_home> /data/fhir/common_module.xq. In the header, we will declare the used version of the language, the namespace of the module, and the namespace of the used external schemas:

 xquery version "3.1"; module namespace conf = "http://hl7.org/fhir"; declare namespace rest = "http://www.expath.org/restxq"; 

Next comes the code of the function that implements the required behavior of the resource:
 declare %rest:GET (:   HTTP,      :) %rest:path("/metadata") (:     ,   URL:) %rest:produces("application/fhir+xml") (:     XML :) %rest:query-param("_format", "{$format}") (:     _format :) function conf:get-conformance($format as xs:string?) as item() { if (exists($format) and not ($format = ("application/xml", "application/fhir+xml"))) then "The endpoint produce response in application/fhir+xml format, but [" || $format || "] specified" else <CapabilityStatement xmlns="http://hl7.org/fhir"> <id value="FhirServer"/> <url value="http://localhost:3030/metadata"/> <version value="1.1-SNAPSHOT"/> <name value="Bagri FHIR Server Conformance Statement"/> <status value="draft"/> <experimental value="true"/> <date value="{fn:current-dateTime()}"/> <publisher value="Bagri Project"/> <contact> <name value="Maxim Petrov"/> <telecom> <system value="other"/> <value value="@mfalifax"/> <use value="work"/> </telecom> </contact> <description value="Standard Conformance Statement for the open source Reference FHIR Server provided by Bagri"/> <kind value="instance"/> <instantiates value="http://hl7.org/fhir/Conformance/terminology-server"/> <software> <name value="Reference Server"/> <version value="1.1-SNAPSHOT"/> <releaseDate value="2016-11-10"/> </software> <implementation> <description value="FHIR Server running at http://localhost:3030/"/> <url value="http://localhost:3030/"/> </implementation> <fhirVersion value="1.7.0"/> <acceptUnknown value="both"/> <format value="application/fhir+xml"/> <rest> <mode value="server"/> <!--   ,   --> <resource> <type value="Patient"/> <profile> <reference value="http://fhir3.healthintersections.com.au/open/StructureDefinition/patient"/> </profile> <!--  ,   --> <interaction> <code value="read"/> </interaction> <interaction> <code value="vread"/> </interaction> <interaction> <code value="search-type"/> </interaction> <interaction> <code value="update"/> </interaction> <interaction> <code value="create"/> </interaction> <interaction> <code value="delete"/> </interaction> <readHistory value="true"/> <updateCreate value="true"/> <!-- ,       search --> <searchParam> <name value="birthdate"/> <definition value="http://hl7.org/fhir/SearchParameter/Patient-birthdate"/> <type value="date"/> <documentation value="The patient's date of birth"/> <!--    equals --> <modifier value="exact"/> </searchParam> <searchParam> <name value="gender"/> <definition value="http://hl7.org/fhir/SearchParameter/Patient-gender"/> <type value="token"/> <documentation value="Gender of the patient"/> <modifier value="exact"/> </searchParam> <searchParam> <name value="identifier"/> <definition value="http://hl7.org/fhir/SearchParameter/Patient-identifier"/> <type value="token"/> <documentation value="A patient identifier"/> <!--    contains --> <modifier value="contains"/> </searchParam> <searchParam> <name value="name"/> <definition value="http://hl7.org/fhir/SearchParameter/Patient-name"/> <type value="string"/> <documentation value="A server defined search that may match any of the string fields in the HumanName, including family, give, prefix, suffix and/or text"/> <modifier value="contains"/> </searchParam> <searchParam> <name value="telecom"/> <definition value="http://hl7.org/fhir/SearchParameter/Patient-telecom"/> <type value="token"/> <documentation value="The value in any kind of telecom details of the patient"/> <modifier value="contains"/> </searchParam> </resource> </rest> </CapabilityStatement> }; 


Actually, this is the only method that makes up the resource Conformance. Its task is to determine other system access points and parameters that can be used in these interactions.

For the Patient application resource, create another XQuery module:

<bagri_home> /data/fhir/patient_module.xq. Also in the header we will declare the used namespaces:

 module namespace fhir = "http://hl7.org/fhir/patient"; declare namespace http = "http://www.expath.org/http"; declare namespace rest = "http://www.expath.org/restxq"; declare namespace bgdm = "http://bagridb.com/bagri-xdm"; declare namespace p = "http://hl7.org/fhir"; 

Implement the read method:

 declare %rest:GET (:   HTTP,      :) %rest:path("/{id}") (:     ; id -    :) %rest:produces("application/fhir+xml") (:     :) function fhir:get-patient-by-id($id as xs:string) as element()? { collection("Patients")/p:Patient[p:id/@value = $id] }; 

It looks, in my opinion, very attractive: the implementation of the required functionality in just one line! But as you know, the devil is in the details. In addition to the basic behavior, the FHIR specification also defines numerous additional situations and HTTP statuses and headers that the service must return in such cases. Let's try to rewrite the read method shown above, taking into account the advanced requirements:

 declare %rest:GET %rest:path("/{id}") %rest:produces("application/fhir+xml") function fhir:get-patient-by-id($id as xs:string) as element()* { let $itr := collection("Patients")/p:Patient[p:id/@value = $id] return if ($itr) then (<rest:response> <http:response status="200"> (:    ? :) {if ($itr/p:meta/p:versionId/@value) then ( (:  ETag       Patient :) <http:header name="ETag" value="W/"{$itr/p:meta/p:versionId/@value}""/>, (:  Content-Location   ,       :) <http:header name="Content-Location" value="/Patient/{$id}/_history/{$itr/p:meta/p:versionId/@value}"/> ) else ( (:   Content-Location      :) <http:header name="Content-Location" value="/Patient/{$id}"/> )} (:  Last-Modified   /    :) <http:header name="Last-Modified" value="{format-dateTime(xs:dateTime($itr/p:meta/p:lastUpdated/@value), "[FNn,3-3], [D] [MNn,3-3] [Y] [H01]:[m01]:[s01] [z,*-6]")}"/> </http:response> </rest:response>, $itr) else (:   404     id   :) <rest:response> <http:response status="404" message="Patient with id={$id} was not found."/> </rest:response> }; 

To indicate the status and headers of the HTTP response, the http: response structure is used, which should be transmitted in the first element of the returned data sequence. Also note that you had to change the return type with element ()? to element () * to transfer this service information to the REST server.

Of course, such a complete implementation of the requirements of the specification is much more verbose. But I don’t presume to say with what language / technology you can fulfill the requirements of FHIR more compactly. On the other hand, XQuery’s ability to work with XML and data sequences is very attractive.

Below, I will not be distracted by the processing of all possible additional scenarios; in the example above, it was shown how to return additional statuses and HTTP headers to the server.
The basic implementation of the vread method looks very similar:

 declare %rest:GET %rest:path("/{id}/_history/{vid}") (:            :) %rest:produces("application/fhir+xml") function fhir:get-patient-by-id-version($id as xs:string, $vid as xs:string) as element()? { collection("Patients")/p:Patient[p:id/@value = $id and p:meta/p:versionId/@value = $vid] }; 

The next method is search. In the resource Conformance, we indicated that we can search for patients by 5 parameters: name, birthday, gender, identifier and telecom. We also specified exactly how the search parameter is used, through the modifier element, which can take the following values: missing | exact | contains | not | text | in | not-in | below | above | type. Their description and the corresponding behavior of the search engine can be found here .

 declare %rest:GET %rest:produces("application/fhir+xml") %rest:query-param("identifier", "{$identifier}") (:      :) %rest:query-param("birthdate", "{$birthdate}") (:  http;     :) %rest:query-param("gender", "{$gender}") %rest:query-param("name", "{$name}") %rest:query-param("telecom", "{$telecom}") function fhir:search-patients($identifier as xs:string?, $birthdate as xs:date?, $gender as xs:string?, $name as xs:string?, $telecom as xs:string?) as element()* { (:    (),    :) let $itr := collection("Patients")/p:Patient[ (not(exists($gender)) or p:gender/@value = $gender) and (not(exists($birthdate)) or p:birthDate/@value = $birthdate) and (not(exists($name)) or contains(data(p:text), $name)) and (not(exists($identifier)) or contains(p:identifier/p:value/@value, $identifier)) and (not(exists($telecom)) or contains(string-join(p:telecom/p:value/@value, " "), $telecom))] (:     Bundle :) return <Bundle xmlns="http://hl7.org/fhir"> <id value="{bgdm:get-uuid()}" /> (:   bundle ID :) <meta> <lastUpdated value="{current-dateTime()}" /> </meta> <type value="searchset" /> <total value="{count($itr)}" /> <link> <relation value="self" /> <url value="http://bagridb.com/Patient/search?name=test" /> </link> {for $ptn in $itr return <entry> <resource>{$ptn}</resource> </entry> } </Bundle> }; 

create - create a new Patient resource, or a new version of an existing resource.

 declare %rest:POST (:      POST :) %rest:consumes("application/fhir+xml") (:           XML :) %rest:produces("application/fhir+xml") (:          :) function fhir:create-patient($content as xs:string) as element()? { let $doc := parse-xml($content) (:      XML,     :) let $uri := xs:string($doc/p:Patient/p:id/@value) || ".xml" (:  uri   :) let $uri := bgdm:store-document(xs:anyURI($uri), $content, ()) (:        uri,         2   :) let $content := bgdm:get-document-content($uri) (:    ,    ,      ,            :) let $doc := parse-xml($content) return $doc/p:Patient }; 

update - the creation of a new version of the existing Patient resource, or the creation of a new resource, if the patient with the specified identifier is not yet registered in the system.

 declare %rest:PUT (:      PUT :) %rest:path("/{id}"). (:       :) %rest:consumes("application/fhir+xml") %rest:produces("application/fhir+xml") function fhir:update-patient($id as xs:string, $content as xs:string) as element()? { for $uri in fhir:get-patient-uri($id) (:        :) let $uri := bgdm:store-document($uri, $content, ()) let $content := bgdm:get-document-content($uri, ()) let $doc := parse-xml($content) return $doc/p:Patient }; 

delete - deletes the Patient resource registered in the system.

 declare %rest:DELETE (:  , ,   DELETE :) %rest:path("/{id}") function fhir:delete-patient($id as xs:string) as item()? { for $uri in fhir:get-patient-uri($id) return bgdm:remove-document($uri) (:     :) }; 

An auxiliary method used from the update and delete functions:

 declare %private function fhir:get-patient-uri($id as xs:string) as xs:anyURI? { (:    XQuery :) let $query := ' declare namespace p = "http://hl7.org/fhir"; declare variable $id external; for $ptn in fn:collection("Patients")/p:Patient where $ptn/p:id/@value = $id return $ptn' (:  ,    uri ,    :) let $uri := bgdm:query-document-uris($query, ("id", $id), ()) return xs:anyURI($uri) }; 

As you can see, in the implementation of resource management logic, the XQuery functions provided by the Bagri libraries are used. Here is a brief description of them:

 bgdm:get-uuid() as xs:string -    uuid bgdm:query-document-uris(xs:string, xs:anyType*, xs:anyAtomicType*) as xs:string* -  uri ,      XQuery bgdm:store-document(xs:anyURI, xs:string, xs:anyAtomicType*) as xs:anyURI -     ,      bgdm:get-document-content(xs:anyURI) as xs:string* -     bgdm:remove-document(xs:anyURI) as xs:anyURI -   

This completes the implementation of the server modules running the FHIR resource management logic. I think in 45 minutes we did it :). In the next part of the article, I would like to show how to launch the resources developed above and test them. And, of course, it would be very interesting to listen to what the highly respected audience Habra thinks about this.

Source: https://habr.com/ru/post/315392/


All Articles