Article Dmitry Zasypkin · Oct 7, 2020 8m read

FHIR Terminology Service specification describes a set of operations on CodeSystem, ValueSet and ConceptMap resources. Among those operations, the following four operations appear to be the most widely adopted ones:

Developing a partial implementation of the specification has been an effective way to explore the new FHIR framework introduced in IRIS for Health 2020.1. The implementation includes four operations listed above, and supports read and search interactions for CodeSystem and ValueSet resources.

It's important to note that the implementation uses plain ObjectScript persistent classes as source terminology tables.

Installation and Testing with a Sample Strategy

The following list outlines installation and basic testing steps:

  1. Install IRIS for Health 2020.1 or newer.
  2. Set up a new namespace using System Administration > Configuration > System Configuration > Namespaces menu of the Portal, or by running the command do ##class(HS.HC.Util.Installer).InstallFoundation("<name for the new namespace>") in HSLIB namespace. Then import classes from src/cls and samples/cls folders of intersystems-ru/fhir-terminology-service GitHub repository.
  3. Create a custom FHIR metadata set based on R4 set with additional search parameters defined in dummy-search-parameters.json. This can be done either by using ##class(HS.FHIRServer.ConsoleSetup).Setup() interactive utility or by running the command:
do ##class(HS.FHIRServer.Installer).InstallMetadataSet("<metadataSetKey>", "<metadata set description>", "HL7v40", $lb("<directory with dummy-search-parameters.json>"), 1)
  • This step is required in order for $expand and $validate-code operations to support HTTP GET requests.
  • Note that FHIR metadata set files packaged with InterSystems IRIS reside in <installation directory>/dev/fhir/fhir-metadata directory.
  1. Create a new FHIR endpoint based on the new metadata set and on Sample.iscru.fhir.fts.SimpleStrategy class. Again, this can be achieved either by using the interactive utility or by running the command:
do ##class(HS.FHIRServer.Installer).InstallInstance("<web app URI, e.g. /csp/terminology>", "Sample.iscru.fhir.fts.SimpleStrategy", "<metadataSetKey>")
  1. Allow unauthenticated access to the new endpoint: use the interactive utility or run the following commands:
set strategy = ##class(HS.FHIRServer.API.InteractionsStrategy).GetStrategyForEndpoint("<web app URI>")
set config = strategy.GetServiceConfigData()
set config.DebugMode = 4
do strategy.SaveServiceConfigData(config)
  1. Populate Sample.iscru.fhir.fts.model.CodeTable:
do ##class(Sample.iscru.fhir.fts.model.CodeTable).Populate(10)
  1. Import fhir-terminology-service.postman_collection.json file into Postman, adjust url variable defined within the collection, and test the service against Sample.iscru.fhir.fts.model.CodeTable which is a simple persistent class.

Supported FHIR Interactions

Currently the only supported search parameter for both CodeSystem and ValueSet is url.

Both HTTP GET and HTTP POST methods are supported for the four operations listed above.

The table below lists some of the possible HTTP GET requests against Sample.iscru.fhir.fts.model.CodeTable class.

URI
(to be prepended with http://<server>:<port><web app URI>)
Description
/metadataGet endpoint's Capability Statement resource.
/CodeSystem/Sample.iscru.fhir.fts.model.CodeTableRead CodeSystem resource corresponding to Sample.iscru.fhir.fts.model.CodeTable class.
/ValueSet/Sample.iscru.fhir.fts.model.CodeTableRead ValueSet resource corresponding to Sample.iscru.fhir.fts.model.CodeTable class.
/CodeSystem?url=urn:CodeSystem:CodeTableSearch CodeSystem resource by url.
/CodeSystemOutput all avaliable CodeSystem resources.
/ValueSet?url=urn:ValueSet:CodeTableSearch ValueSet resource by url.
/ValueSetOutput all avaliable ValueSet resources.
/CodeSystem/$lookup?system=urn:CodeSystem:CodeTable&code=TESTGiven system and code, get all the details about the concept.
/ValueSet/$expand?url=urn:ValueSet:CodeTableExpand the ValueSet.
/CodeSystem/Sample.iscru.fhir.fts.model.CodeTable/$validate-code?code=TESTValidate that a code is in the code system.

Creating a Custom Strategy

In order to expose your own persistent classes as FHIR code systems/value sets, you would need to create your custom strategy class by subclassing iscru.fhir.fts.FTSStrategy, and then create FHIR endpoint based on the new custom strategy (see above installation step #4).

One class parameter and at least three methods must be overridden by your strategy class:

  • StrategyKey class parameter should be assigned some unique value. Name of the current class seems to be a good option.
  • getCodeTablePackage() class method should return package name for a given code system (or value set) identified by its canonical URL. Typically all terminology classes belong to one package, so this method would usually return one and the same package name regardless of argument values.
  • getCodePropertyName() and getDisplayPropertyName() class methods should return names of class properties that correspond to code and display concept elements. Different classes may have different properties mapped to terminology code/display elements.

Other methods and parameters of iscru.fhir.fts.FTSStrategy that you might find appropriate to override are as follows:

  • listCodeTableClasses() class method needs to be overridden in order to support search requests that result in returning all available code systems (or value sets). This method is supposed to return a list of class names of all available terminology classes. Sample.iscru.fhir.fts.SimpleStrategy contains the following basic implementation of this method:
/// Returns a list of all available code table classes.
ClassMethod listCodeTableClasses() As %List
{
	#dim sql As %String = "SELECT name FROM %Dictionary.ClassDefinition WHERE name LIKE '" _ ..#codeTablePACKAGE _ ".%' ORDER BY name"
	#dim resultSet As %SQL.StatementResult = ##class(%SQL.Statement).%ExecDirect(, sql)
	if (resultSet.%SQLCODE '= 0) && (resultSet.%SQLCODE '= 100) $$$ThrowStatus($$$ERROR($$$SQLError, resultSet.%SQLCODE, resultSet.%Message))

	#dim return As %List = ""
	while resultSet.%Next()
	{
		set return = return _ $lb(resultSet.name)
	}
	
	quit return
}
  • isExcludedProperty() class method has to be overridden if any particular properties of your persistent classes should not show up in CodeSystem resources. By default, this method filters Collection, Identity, Internal, MultiDimensional and Private properties out. Note that object reference and stream properties are currently not supported and ignored by the framework.

  • codeSystemUrlPREFIX and valueSetUrlPREFIX class parameters and getCodeSystemForClassname(), getValueSetForClassname(), determineCodeTableClassname() and determineCodeSystemForValueSet() methods control how class names are mapped to canonical URLs and vice versa. By default, the following naming scheme is used for canonical URLs: |CodeSystem|ValueSet| |-|-| |urn:CodeSystem:<short class name>|urn:ValueSet:<short class name>|

Note that logical id (aka server id) of a CodeSystem/ValueSet resource equals full name of its corresponding class.

TO DO

Currently lacking is support for versioning of code systems, concept hierarchies and $subsumes operation, ConceptMap resource and plenty of other stuff. Ideas and pull requests are very welcome!

1
2 614
Article Dmitry Zasypkin · May 22, 2020 2m read

If you work with interoperability productions of InterSystems IRIS or Ensemble, no doubt you are familiar with the Message Viewer page. The page supports filtering messages according to filter criteria you enter in the Basic and/or Extended Criteria sections. Extended Criteria conditions are specified as property-operator-value triples. Once you click Search button, such triples become WHERE clause conditions of a generated SQL query executed against message header/body tables.

As a side note, one can view recently generated message queries by following the instructions listed in the doc.

Unfortunately stream properties cannot be used in the filtering conditions since most of the SQL WHERE predicates (including LIKE) are applied to OID of a stream - not to its contents. But there is a way to overcome that limitation *to some extent* using a simple two-step approach:

1) Subclass EnsPortal.MsgFilter.Assistant as follows

Class iscru.interop.MsgFilterAssistant Extends EnsPortal.MsgFilter.Assistant
{
ClassMethod GetSQLCondition(pOperator As %String, pProp As %String, pValue As %String, pDisplay As %Boolean = 0, pNoIndex As %Boolean = 0) As %String
 {
  if (pValue = "") || ((pOperator '= "Like") && (pOperator '= "NotLike")) quit ##super(pOperator, pProp, pValue, pDisplay, pNoIndex)
  
  if ("%%" = $extract(pValue, *-2, *-1))
  {
    set pValue = "'" _ $extract(pValue, 1, *-3) _ "' ESCAPE '" _ $extract(pValue, *) _ "'"
  }
  else
  {
    set pValue = "'" _ $replace(pValue, "'", "''") _ "'"
  }
  quit "substring(" _ pProp _ ", 1, 3000000) " _ $case(pOperator, "Like": "LIKE", "NotLike": "NOT LIKE") _ " " _ pValue
 }
}

2) Execute the following command in the current namespace:
         set ^EnsPortal.Settings("MessageViewer", "AssistantClass") = "<name of the class created in step 1>"

Now you can apply Like/NotLike conditions to stream properties of message bodies with one limitation: only the first 3 mln bytes of a stream are taken into account when applying Like/NotLike pattern. Anyway this is more of a proof-of-concept solution that does not "scale", but still works well in some cases.

If the 3M limitation is not acceptable then it might make sense to look into implementing Like operator with stream argument, wrapping it in a stored procedure method, and using that procedure for WHERE subclause in the code above. Perhaps that can be a good topic for another article... 

0
2 651