Article
Yuri Marx · Nov 30, 2022 14m read

Creating FHIR questionnaires

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

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
}

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
}

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
}

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
}

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
}

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.' });
                        }
                    );
                }
            });
        }
    }

    
}

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>

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));
  }

}

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:

4
1 236
Discussion (0)1
Log in or sign up to continue