Intersystems IRIS for Health has excellent support for the FHIR industry standard. The main features are:
1. FHIR Server
2. FHIR Database
3. REST and ObjectScript API for CRUD operations on FHIR resources (patient, questionnaire, vaccines, etc.)
This article demonstrates how to use each of these features, as well as presenting an angular frontend for creating and viewing Quiz-like FHIR resources.
Step 1 - deploying your FHIR Server using InterSystems IRIS for Health
To create your FHIR Server, you must add the following instructions into iris.script file (from: https://openexchange.intersystems.com/package/iris-fhir-template)
zn "HSLIB"
set namespace="FHIRSERVER"
Set appKey = "/fhir/r4"
Set strategyClass = "HS.FHIRServer.Storage.Json.InteractionsStrategy"
set metadataPackages = $lb("hl7.fhir.r4.core@4.0.1")
set importdir="/opt/irisapp/src"
//Install a Foundation namespace and change to it
Do ##class(HS.HC.Util.Installer).InstallFoundation(namespace)
zn namespace
// Install elements that are required for a FHIR-enabled namespace
Do ##class(HS.FHIRServer.Installer).InstallNamespace()
// Install an instance of a FHIR Service into the current namespace
Do ##class(HS.FHIRServer.Installer).InstallInstance(appKey, strategyClass, metadataPackages)
// Configure FHIR Service instance to accept unauthenticated requests
set strategy = ##class(HS.FHIRServer.API.InteractionsStrategy).GetStrategyForEndpoint(appKey)
set config = strategy.GetServiceConfigData()
set config.DebugMode = 4
do strategy.SaveServiceConfigData(config)
zw ##class(HS.FHIRServer.Tools.DataLoader).SubmitResourceFiles("/opt/irisapp/fhirdata/", "FHIRServer", appKey)
do $System.OBJ.LoadDir("/opt/irisapp/src","ck",,1)
zn "%SYS"
Do ##class(Security.Users).UnExpireUserPasswords("*")
zn "FHIRSERVER"
zpm "load /opt/irisapp/ -v":1:1
//zpm "install fhir-portal"
halt
ObjectScriptObjectScript
Using the utility class HS.FHIRServer.Installer, you can create your FHIR Server.
Step 2 - Use the FHIR REST or ObjectScript API to read, update, delete and find FHIR data
I like to use the ObjectScript class HS.FHIRServer.Service to do all CRUD operations.
To get all FHIR data from a resource type (like questionnaire):
/// Retreive all the records of questionnaire
ClassMethod GetAllQuestionnaire() As %Status
{
set tSC = $$$OK
Set %response.ContentType = ..#CONTENTTYPEJSON
Set %response.Headers("Access-Control-Allow-Origin")="*"
Try {
set fhirService = ##class(HS.FHIRServer.Service).EnsureInstance(..#URL)
set request = ##class(HS.FHIRServer.API.Data.Request).%New()
set request.RequestPath = "/Questionnaire/"
set request.RequestMethod = "GET"
do fhirService.DispatchRequest(request, .pResponse)
set json = pResponse.Json
set resp = []
set iter = json.entry.%GetIterator()
while iter.%GetNext(.key, .value) {
do resp.%Push(value.resource)
}
write resp.%ToJSON()
} Catch Err {
set tSC = 1
set message = {}
set message.type= "ERROR"
set message.details = "Error on get all questionnairies"
}
Quit tSC
}
ObjectScriptObjectScript
To get a specific data item from the FHIR data repository (like a questionnaire ocurrence):
/// Retreive a questionnaire by id
ClassMethod GetQuestionnaire(id As %String) As %Status
{
set tSC = $$$OK
Set %response.ContentType = ..#CONTENTTYPEJSON
Set %response.Headers("Access-Control-Allow-Origin")="*"
Try {
set fhirService = ##class(HS.FHIRServer.Service).EnsureInstance(..#URL)
set request = ##class(HS.FHIRServer.API.Data.Request).%New()
set request.RequestPath = "/Questionnaire/"_id
set request.RequestMethod = "GET"
do fhirService.DispatchRequest(request, .pResponse)
write pResponse.Json.%ToJSON()
} Catch Err {
set tSC = 1
set message = {}
set message.type= "ERROR"
set message.details = "Error on get the questionnaire"
}
Quit tSC
}
ObjectScriptObjectScript
To create a new FHIR resource ocurrence (like a new questionnaire):
/// Create questionnaire
ClassMethod CreateQuestionnaire() As %Status
{
set tSC = $$$OK
Set %response.ContentType = ..#CONTENTTYPEJSON
Set %response.Headers("Access-Control-Allow-Origin")="*"
Try {
set fhirService = ##class(HS.FHIRServer.Service).EnsureInstance(..#URL)
set request = ##class(HS.FHIRServer.API.Data.Request).%New()
set request.RequestPath = "/Questionnaire/"
set request.RequestMethod = "POST"
set data = {}.%FromJSON(%request.Content)
set data.resourceType = "Questionnaire"
set request.Json = data
do fhirService.DispatchRequest(request, .response)
write response.Json.%ToJSON()
} Catch Err {
set tSC = 1
set message = {}
set message.type= "ERROR"
set message.details = "Error on create questionnaire"
}
Return tSC
}
ObjectScriptObjectScript
To Update a FHIR resource (like a questionnaire):
/// Update a questionnaire
ClassMethod UpdateQuestionnaire(id As %String) As %Status
{
set tSC = $$$OK
Set %response.ContentType = ..#CONTENTTYPEJSON
Set %response.Headers("Access-Control-Allow-Origin")="*"
Try {
set fhirService = ##class(HS.FHIRServer.Service).EnsureInstance(..#URL)
set request = ##class(HS.FHIRServer.API.Data.Request).%New()
set request.RequestPath = "/Questionnaire/"_id
set request.RequestMethod = "PUT"
set data = {}.%FromJSON(%request.Content)
set data.resourceType = "Questionnaire"
set request.Json = data
do fhirService.DispatchRequest(request, .response)
write response.Json.%ToJSON()
}Catch Err {
set tSC = 1
set message = {}
set message.type= "ERROR"
set message.details = "Error on update questionnaire"
}
Return tSC
}
ObjectScriptObjectScript
To delete a FHIR resource ocurrency (like a questionnary):
/// Delete a questionnaire by id
ClassMethod DeleteQuestionnaire(id As %String) As %Status
{
set tSC = $$$OK
Set %response.ContentType = ..#CONTENTTYPEJSON
Set %response.Headers("Access-Control-Allow-Origin")="*"
Try {
set fhirService = ##class(HS.FHIRServer.Service).EnsureInstance(..#URL)
set request = ##class(HS.FHIRServer.API.Data.Request).%New()
set request.RequestPath = "/Questionnaire/"_id
set request.RequestMethod = "DELETE"
do fhirService.DispatchRequest(request, .pResponse)
} Catch Err {
set tSC = 1
set message = {}
set message.type= "ERROR"
set message.details = "Error on delete the questionnaire"
}
Quit tSC
}
ObjectScriptObjectScript
As you can see with you want to create use POST, to update use PUT, to delete use DELETE and to query use GET verb.
Step 3 - Create an Angular client to consume your FHIR Server App
I created an angular app using PrimeNG and installing the package npm install --save @types/fhir. This package has all FHIR types mapped to TypeScript.
Angular controller class:
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Period, Questionnaire } from 'fhir/r4';
import { ConfirmationService, MessageService, SelectItem } from 'primeng/api';
import { QuestionnaireService } from './questionnaireservice';
const QUESTIONNAIREID = 'questionnaireId';
@Component({
selector: 'app-questionnaire',
templateUrl: './questionnaire.component.html',
providers: [MessageService, ConfirmationService],
styleUrls: ['./questionnaire.component.css'],
encapsulation: ViewEncapsulation.None
})
export class QuestionnaireComponent implements OnInit {
public questionnaire: Questionnaire;
public questionnairies: Questionnaire[];
public selectedQuestionnaire: Questionnaire;
public questionnaireId: string;
public sub: any;
public publicationStatusList: SelectItem[];
constructor(
private questionnaireService: QuestionnaireService,
private router: Router,
private route: ActivatedRoute,
private confirmationService: ConfirmationService,
private messageService: MessageService){
this.publicationStatusList = [
{label: 'Draft', value: 'draft'},
{label: 'Active', value: 'active'},
{label: 'Retired', value: 'retired'},
{label: 'Unknown', value: 'unknown'}
]
}
ngOnInit() {
this.reset();
this.listQuestionnaires();
this.sub = this.route.params.subscribe(params => {
this.questionnaireId = String(+params[QUESTIONNAIREID]);
if (!Number.isNaN(this.questionnaireId)) {
this.loadQuestionnaire(this.questionnaireId);
}
});
}
private loadQuestionnaire(questionnaireId) {
this.questionnaireService.load(questionnaireId).subscribe(response => {
this.questionnaire = response;
this.selectedQuestionnaire = this.questionnaire;
if(!response.effectivePeriod) {
this.questionnaire.effectivePeriod = <Period>{};
}
}, error => {
console.log(error);
this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on load questionnaire.' });
});
}
public loadQuestions() {
if(this.questionnaire && this.questionnaire.id) {
this.router.navigate(['/question', this.questionnaire.id]);
} else {
this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Choose a questionnaire.' });
}
}
private listQuestionnaires() {
this.questionnaireService.list().subscribe(response => {
this.questionnairies = response;
this.reset();
}, error => {
console.log(error);
this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on load the questionnaries.' });
});
}
public onChangeQuestionnaire() {
if (this.selectedQuestionnaire && !this.selectedQuestionnaire.id) {
this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Select a questionnaire.' });
} else {
if(this.selectedQuestionnaire && this.selectedQuestionnaire.id) {
this.loadQuestionnaire(this.selectedQuestionnaire.id);
}
}
}
public reset() {
this.questionnaire = <Questionnaire>{};
this.questionnaire.effectivePeriod = <Period>{};
}
public save() {
if(this.questionnaire.id && this.questionnaire.id != "") {
this.questionnaireService.update(this.questionnaire).subscribe(
(resp) => {
this.messageService.add({
severity: 'success',
summary: 'Success', detail: 'Questionnaire saved.'
});
this.listQuestionnaires()
this.loadQuestionnaire(this.questionnaire.id);
},
error => {
console.log(error);
this.messageService.add({
severity: 'error',
summary: 'Error', detail: 'Error on save the questionnaire.'
});
}
);
} else {
this.questionnaireService.save(this.questionnaire).subscribe(
(resp) => {
this.messageService.add({
severity: 'success',
summary: 'Success', detail: 'Questionnaire saved.'
});
this.listQuestionnaires()
this.loadQuestionnaire(resp.id);
},
error => {
console.log(error);
this.messageService.add({
severity: 'error',
summary: 'Error', detail: 'Error on save the questionnaire.'
});
}
);
}
}
public delete(id: string) {
if (!this.questionnaire || !this.questionnaire.id) {
this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Select a questionnaire.' });
} else {
this.confirmationService.confirm({
message: 'Do you confirm?',
accept: () => {
this.questionnaireService.delete(id).subscribe(
() => {
this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Questionnaire deleted.' });
this.listQuestionnaires();
this.reset();
},
error => {
console.log(error);
this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on delete questionnaire.' });
}
);
}
});
}
}
}
ObjectScriptObjectScript
Angular HTML file
<p-toast [style]="{marginTop: '80px', width: '320px'}"></p-toast>
<p-card>
<div class="p-fluid p-formgrid grid">
<div class="field col-12 lg:col-12 md:col-12">
<p-dropdown id="dropquestions1" [options]="questionnairies" [(ngModel)]="selectedQuestionnaire"
(onChange)="onChangeQuestionnaire()" placeholder="Select a Questionnaire" optionLabel="title"
[filter]="true" [showClear]="true"></p-dropdown>
</div>
</div>
<p-tabView>
<p-tabPanel leftIcon="fa fa-question" header="Basic Data">
<div class="p-fluid p-formgrid grid">
<div class="field col-3 lg:col-3 md:col-12">
<label for="txtname">Name</label>
<input class="inputfield w-full" id="txtname" required type="text" [(ngModel)]="questionnaire.name" pInputText placeholder="Name">
</div>
<div class="field col-7 lg:col-7 md:col-12">
<label for="txttitle">Title</label>
<input class="inputfield w-full" id="txttitle" required type="text" [(ngModel)]="questionnaire.title" pInputText placeholder="Title">
</div>
<div class="field col-2 lg:col-2 md:col-12">
<label for="txtdate">Date</label>
<p-inputMask id="txtdate" mask="9999-99-99" [(ngModel)]="questionnaire.date" placeholder="9999-99-99" slotChar="yyyy-mm-dd"></p-inputMask>
</div>
<div class="field col-2 lg:col-2 md:col-12">
<label for="txtstatus">Status</label>
<p-dropdown [options]="publicationStatusList" [(ngModel)]="questionnaire.status"></p-dropdown>
</div>
<div class="field col-3 lg:col-3 md:col-12">
<label for="txtpublisher">Publisher</label>
<input class="inputfield w-full" id="txtpublisher" required type="text" [(ngModel)]="questionnaire.publisher" pInputText placeholder="Publisher">
</div>
<div class="field col-2 lg:col-2 md:col-12">
<label for="txtstartperiod">Start Period</label>
<p-inputMask id="txtstartperiod" mask="9999-99-99" [(ngModel)]="questionnaire.effectivePeriod.start" placeholder="9999-99-99" slotChar="yyyy-mm-dd"></p-inputMask>
</div>
<div class="field col-2 lg:col-2 md:col-12">
<label for="txtendperiod">End Period</label>
<p-inputMask id="txtendperiod" mask="9999-99-99" [(ngModel)]="questionnaire.effectivePeriod.end" placeholder="9999-99-99" slotChar="yyyy-mm-dd"></p-inputMask>
</div>
<div class="field col-12 lg:col-12 md:col-12">
<label for="txtcontent">Description</label>
<p-editor [(ngModel)]="questionnaire.description" [style]="{'height':'100px'}"></p-editor>
</div>
</div>
<div class="grid justify-content-end">
<button pButton pRipple type="button" label="New Record" (click)="reset()"
class="p-button-rounded p-button-success mr-2 mb-2"></button>
<button pButton pRipple type="button" label="Save" (click)="save()"
class="p-button-rounded p-button-info mr-2 mb-2"></button>
<button pButton pRipple type="button" label="Delete" (click)="delete(questionnaire.id)"
class="p-button-rounded p-button-danger mr-2 mb-2"></button>
<button pButton pRipple type="button" label="Questions" (click)="loadQuestions()"
class="p-button-rounded p-button-info mr-2 mb-2"></button>
</div>
</p-tabPanel>
</p-tabView>
</p-card>
<p-confirmDialog #cd header="Atenção" icon="pi pi-exclamation-triangle">
<p-footer>
<button type="button" pButton icon="pi pi-times" label="Não" (click)="cd.reject()"></button>
<button type="button" pButton icon="pi pi-check" label="Sim" (click)="cd.accept()"></button>
</p-footer>
</p-confirmDialog>
ObjectScriptObjectScript
Angular Service class
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
import { take } from 'rxjs/operators';
import { Questionnaire } from 'fhir/r4';
@Injectable({
providedIn: 'root'
})
export class QuestionnaireService {
private url = environment.host2 + 'questionnaire';
constructor(private http: HttpClient) { }
public save(Questionnaire: Questionnaire): Observable<Questionnaire> {
return this.http.post<Questionnaire>(this.url, Questionnaire).pipe(take(1));
}
public update(Questionnaire: Questionnaire): Observable<Questionnaire> {
return this.http.put<Questionnaire>(`${this.url}/${Questionnaire.id}`, Questionnaire).pipe(take(1));
}
public load(id: string): Observable<Questionnaire> {
return this.http.get<Questionnaire>(`${this.url}/${id}`).pipe(take(1));
}
public delete(id: string): Observable<any> {
return this.http.delete(`${this.url}/${id}`).pipe(take(1));
}
public list(): Observable<Questionnaire[]> {
return this.http.get<Questionnaire[]>(this.url).pipe(take(1));
}
}
ObjectScriptObjectScript
Step 4 - Application in action
1. Go to https://openexchange.intersystems.com/package/FHIR-Questionnaires application.
2. Clone/git pull the repo into any local directory
$ git clone https://github.com/yurimarx/fhir-questions.git
3. Open the terminal in this directory and run:
$ docker-compose up -d
4. Open the webapp: http://localhost:52773/fhirquestions/index.html
See some pictures: