Skip to content

Creating a new application for eHMP

Sam Habiel edited this page Oct 2, 2017 · 15 revisions

I (Sam Habiel) am writing this as a tutorial while figuring out how to write a new application for eHMP. This is a work in progress and may not be the most correct way of doing things--but it's as much as I can figure out given the state of the documentation.

Introduction

To write an eHMP application, there are overall three steps:

  • Obtain a source of data which you will consume using the RDK
  • Create the RDK Web Service that will expose the data
  • Create the ADK Applet that will display the data

This tutorial will contain show two examples: an application to display the patients in wards; and an application to display the weather. The first uses VistA; the other has nothing to do with VistA.

VistA Application: Part 1

I wrote an RPC to get all the patients and all the beds. It's not a trivial RPC... but I luckily wrote that code before, so it was easy to convert the old code into an RPC format.

VistA Code and Configuration

Here's the code in the routine KBANBB:

KBANBB ; OSE/SMH - Bed Board for eHMP;2017-08-28  11:41 AM
 ;;0.0;SAM'S INDUSTRIAL CONGLOMERATES;;
 ;
EN(wardbedjson) ; [Public] RPC - KBAN GET BEDS - Get all beds
 ; ZEXCEPT: debug
 ;
 ; .wardbedjson = local style return
 ; Ask Fileman for the list of the wards, taking out inactive ones
 ; File 42; .01 field only, "Packed Output, don't re-sort", use "B" index
 ; Screen inactive wards out using Fileman Screen on File.
 k wardbedjson
 n wards1,err
 n wardbed
 D LIST^DIC(42,"","@;.01","PQ","","","","B","S D0=Y D WIN^DGPMDDCF I 'X","","wards1","err")
 i $d(err) s $EC=",U101,"  ; we shouldn't ever have any messages - crash if so
 n wards2 ; better wards!
 m wards2=wards1("DILIST")
 n i s i=0
 for  s i=$o(wards2(i)) q:'i  do
 . n wardien s wardien=$piece(wards2(i,0),"^")
 . n roomien s roomien=0
 . for  s roomien=$o(^DG(405.4,"W",wardien,roomien)) q:'roomien  do
 . . quit:'$d(^DG(405.4,roomien,0))
 . . new bed set bed=$P(^(0),"^")
 . . new admien set admien=$o(^DGPM("ARM",roomien,0))
 . . new lodger,ptnode,edw,mot
 . . if admien d 
 . . . set lodger=^(admien) ; note naked sexy ref
 . . . set ptnode=^DGPM(admien,0)
 . . write:$g(debug) "ptnode: "_$g(ptnode),!
 . . ; Bed Message
 . . ; pt name^pt sex^adm date^lodger^bed oos?^bed oos msg^bed oss comment
 . . n bedmsg
 . . i $g(ptnode) d  ; if we have a patient, that's the bed msg
 . . . n dfn s dfn=$p(ptnode,"^",3)
 . . . s bedmsg=$p(^DPT(dfn,0),"^",1,2) ; Patient name and sex
 . . . ; s $p(bedmsg,"^",3)=$$FMTE^XLFDT($p(ptnode,"^")) ; Admission date
 . . . s $p(bedmsg,"^",3)=$$DATE^TIULS($p(ptnode,"^"),"AMTH DD@HR:MIN") ; Admission date using TIU API
 . . . s $p(bedmsg,"^",4)=$g(lodger)
 . . d  ; Out of Service Checks?
 . . . n oos s oos=$$OOS(roomien) ; 0 or 1^msg^comment
 . . . s $p(bedmsg,"^",5,7)=oos
 . . ;
 . . s wardbed($piece(wards2(i,0),"^",2),bed)=bedmsg
 ;
 ; Loop through inpatients to find patients without a bed
 ; Bed Message (reminder!)
 ; pt name^pt sex^adm date^lodger
 n i,j s (i,j)=""
 n counter s counter=0
 for  s i=$o(^DPT("CN",i)) q:i=""  for  s j=$o(^DPT("CN",i,j)) q:j=""  do
 . n admien s admien=^(j) ; Patient Movement IEN stored in Index
 . n dfn s dfn=j
 . n bed s bed=$get(^DPT(dfn,.101))
 . i bed'="" quit  ; if bed is not empty, quit!
 . s counter=counter+1
 . n wardname s wardname=^DPT(dfn,.1)
 . s wardbed(wardname,"NONE"_counter)=$p(^DPT(dfn,0),"^",1,2) ; name, sex
 . n admdate s admdate=$P(^DGPM(admien,0),"^")
 . s $p(wardbed(wardname,"NONE"_counter),"^",3)=$$DATE^TIULS(admdate,"AMTH DD@HR:MIN")
 . s $p(wardbed(wardname,"NONE"_counter),"^",4)=0 ; lodger
 ;
 ; Loop through lodgers to find lodgers without a bed
 ; Bed Message (reminder!)
 ; pt name^pt sex^adm date^lodger
 n i,j s (i,j)=""
 for  s i=$o(^DPT("LD",i)) q:i=""  for  s j=$o(^DPT("LD",i,j)) q:j=""  do
 . n admien s admien=^(j) ; Patient Movement IEN stored in Index
 . n dfn s dfn=j
 . n bed s bed=$get(^DPT(dfn,.108))
 . i bed'="" quit  ; if bed is not empty, quit!
 . s counter=counter+1
 . n wardname s wardname=^DPT(dfn,.107)
 . s wardbed(wardname,"NONE"_counter)=$p(^DPT(dfn,0),"^",1,2) ; name, sex
 . n admdate s admdate=$P(^DGPM(admien,0),"^")
 . s $p(wardbed(wardname,"NONE"_counter),"^",3)=$$DATE^TIULS(admdate,"AMTH DD@HR:MIN")
 . s $p(wardbed(wardname,"NONE"_counter),"^",4)=1 ; lodger
 ;
 n err
 D ENCODE^HMPJSON("wardbed","wardbedjson","err")
 quit
 ;
OOS(bedien) ; Is the bed out of service ; Public $$
 ; Input: bedien
 ; Output: 0 -> not out of service -> Active
 ;         1^reason -> Out of service and reason
 ;
 ; First OOS date in the inverse index is the latest
 N X S X=$O(^DG(405.4,bedien,"I","AINV",0))
 I 'X Q 0  ; if none, quit
 ;
 S X=$O(^(+X,0)) ; Then get ifn
 Q:'$d(^DG(405.4,bedien,"I",+X,0)) 0  ; confirm that entry exists
 ;
 N DGND S DGND=^(0)                 ; Out of Service Node
 N OOSD S OOSD=$P(DGND,"^")         ; Out of Service Date
 N OOSR S OOSR=$P(DGND,"^",4)       ; Out of Service Restore
 N NOW S NOW=$$NOW^XLFDT()          ; Now
 ;
 I OOSD>NOW Q 0                     ; If OOSD in future, bed is active
 ;
 ; at this point, OOSD is now or in the past.
 ; Is there a restore date less than today's date? if yes, bed is active
 I OOSR'="",OOSR<NOW Q 0
 ;
 ; at this point, we are sure that the bed is inactive.
 N reasonifn s reasonifn=$p(DGND,"^",2)
 N comment s comment=$p(DGND,"^",3)
 Q 1_"^"_$$GET1^DIQ(405.5,reasonifn,.01)_"^"_comment

If you call this code from direct mode, you get the following JSON (truncated):

VISTA>K ZZZ D EN^KBANBB(.ZZZ)

VISTA>F I=1:1 Q:'$D(ZZZ(I))  W ZZZ(I),!
{"2-INTERMED":{"A-1":"^^^^0","A-2":"^^^^0","B-1":"^^^^0","B-2":"^^^^0","C-1":"^^^^0","C-2":"^^^^0","D-1":"^^^^0
","D-2":"^^^^0","E-1":"^^^^0","E-2":"^^^^0","F-1":"^^^^0","F-2":"^^^^0","G-1":"^^^^0","G-2":"^^^^0"},"3 NORTH G
ASTRO":{"A-1":"^^^^0","A-2":"^^^^0","A-3":"^^^^0","A-4":"^^^^0","A-5":"^^^^0","B-1":"^^^^0","B-2":"^^^^0","B-3"
:"^^^^0","B-4":"^^^^0","B-5":"^^^^0","C-1":"^^^^0","C-2":"^^^^0","D-1":"^^^^0","E-1":"^^^^0","F-1":"^^^^0","F-2
":"^^^^0"},"3 NORTH GU":{"1-1":"ZZZRETFIVENINETYSEVEN,PATIENT^M^AUG 19@08:23^0^0","1-2":"^^^^0","1-3":"^^^^0","
1-4":"^^^^0","2-1":"ZZZRETFOURFORTYTHREE,PATIENT^M^AUG 19@08:35^0^0","2-2":"ZZZRETTHREENINETYONE,PATIENT^M^AUG 
19@08:37^0^0","3-1":"^^^^0","3-2":"ZZZRETFOURTWENTY,PATIENT^M^AUG 19@08:39^0^0","4-1":"^^^^0","F-2":"ZZZRETFONI
NETYFIVE,PATIENT^^AUG 19@08:28^0^0"},"3 NORTH SURG":{"AS-1":"PATIENT,DAN II^M^JUN 06@10:19^0^0","AS-10":"^^^^0"
,"AS-2":"^^^^0","AS-3":"^^^^0","AS-4":"^^^^0","AS-5":"^^^^0","AS-6":"^^^^0","AS-7":"^^^^0","AS-8":"^^^^0","AS-9
":"^^^^0"},"3E NORTH":{"104-5":"ALPHATEST,NEW ONE^M^MAR 10@15:08^0^0","3E-100-1":"EIGHT,INPATIENT^M^APR 03@11:2...

With that code, let's write an RPC wrapper around it, and then add it to the HMP UI CONTEXT.

Make the following entry into the REMOTE PROCEDURE file:

NAME: KBAN BED BOARD                    TAG: EN
  ROUTINE: KBANBB                       RETURN VALUE TYPE: ARRAY
  AVAILABILITY: PUBLIC
 RETURN PARAMETER DESCRIPTION:   
 JSON Array is returned as follows
  
 { ward_name:
   { bed_name: "patient_name^sex^admission_date^oos^oos date^oos message",
   { bed_name: ... },
   ward_name: ...
 }

Then go to the OPTION file and add this RPC to the HMP UI CONTEXT option:

INPUT TO WHAT FILE: REMOTE PROCEDURE// 19  OPTION  (11207 entries)
EDIT WHICH FIELD: ALL// RPC    (multiple)
   EDIT WHICH RPC SUB-FIELD: ALL// 
THEN EDIT FIELD: 


Select OPTION NAME: HMP UI CONTEXT       HMP UI CONTEXT version 20160108-01.US12
195
Select RPC: KBAN BED BOARD// KBAN BED BOARD
         ...OK? Yes//   (Yes)

  RPC: KBAN BED BOARD// 
  RPCKEY: 
  RULES: 
Select RPC: 

Okay. Time to test this RPC from the RDK machine using vista-js. This confirms that a. the RPC has been configured correctly and b. that we have connectivity to the VistA machine.

Log into the RDK machine, and cd to /opt/rdk/node_modules/vista-js/src, and then run this command. Substitute the user/ip address/port if needed:

node command-line.js  --host 172.16.2.101 --port 9210 --credentials 'mx1234;mx1234!!' --context 'HMP UI CONTEXT' --rpc 'KBAN BED BOARD'

You will get this result (truncated):

RUNNING RPC: "KBAN BED BOARD"

RESULT:
{ '2-INTERMED':
   { 'A-1': '^^^^0',
     'A-2': '^^^^0',
     'B-1': '^^^^0',
     'B-2': '^^^^0',
     'C-1': '^^^^0',
     'C-2': '^^^^0',
     'D-1': '^^^^0',
     'D-2': '^^^^0',
     'E-1': '^^^^0',
     'E-2': '^^^^0',
     'F-1': '^^^^0',
     'F-2': '^^^^0',
     'G-1': '^^^^0',
     'G-2': '^^^^0' },
  '3 NORTH GASTRO':
   { 'A-1': '^^^^0',
     'A-2': '^^^^0',
     'A-3': '^^^^0',
     'A-4': '^^^^0',
     'A-5': '^^^^0',
     'B-1': '^^^^0',
     'B-2': '^^^^0',
     'B-3': '^^^^0',
     'B-4': '^^^^0',
     'B-5': '^^^^0',
     'C-1': '^^^^0',
     'C-2': '^^^^0',
     'D-1': '^^^^0',
     'E-1': '^^^^0',
     'F-1': '^^^^0',
     'F-2': '^^^^0' },
  '3 NORTH GU':
   { '1-1': 'ZZZRETFIVENINETYSEVEN,PATIENT^M^AUG 19@08:23^0^0',
     '1-2': '^^^^0',
     '1-3': '^^^^0',
     '1-4': '^^^^0',
     '2-1': 'ZZZRETFOURFORTYTHREE,PATIENT^M^AUG 19@08:35^0^0',
     '2-2': 'ZZZRETTHREENINETYONE,PATIENT^M^AUG 19@08:37^0^0',
     '3-1': '^^^^0',
     '3-2': 'ZZZRETFOURTWENTY,PATIENT^M^AUG 19@08:39^0^0',
     ...

This concludes the VistA Code and Configuration section. Next we will talk about setting up a resource in the RDK server to serve this data.

Setting up the RDK server

You can find decent RDK documentation on the SDK page supplied in the eHMP code drop. An on-line version can be found here: https://ehmp-ui.usgovvirginia.cloudapp.usgovcloudapi.net/documentation/#/sdk.

The RDK is written in Node.js. Yes, you need to know how to write Javascript in the Node style before going on and you need to know how to use the Node.js package called "Express". Find a tutorial first somewhere on the net.

To set up a new READ resource, you need to perform two steps:

  • Create the resource in /opt/rdk/src/resouces
  • Register the resource in /opt/rdk/bin/rdk-fetch-server.js

Step -1: Learn how to call the RDK server first!

This was difficult as I couldn't locate the documentation for this, and I had to reverse engineer it. Eventually, I did find the documentation somewhere in the RDK. Right now I can't find again!

Go completely outside your VM/Docker set-up for eHMP and try this URL using cURL. The URL of eHMP on using the Chef set-up is https://172.16.1.150/. I have my own machines and DNS set-up--substitute the URL here with yours. (NB: We use -k with curl because we only have a self-signed security certificate.)

curl -k 'https://agamemnon.smh101.com/resource/resourceDirectory'

You will get the answer:

{"data":{"error":"Web Token could not be verified. Log out, clear browser cookies and log in again."},"status":401}{"data":{"error":"Web Token could not be verified. Log out, clear browser cookies and log in again."},"status":401}

Basically, you have to log-in, and once you log-in, you must provide the cookie and the JWT token with every request. Easier said than done!

First, create a file called login.json (name doesn't matter) and put in a valid user and division. The following example is a user on the Panorama VM:

{
  "accessCode": "mx1234",
  "verifyCode": "mx1234!!",
  "site": "9E7A",
  "division": "500"
}

Now, POST this file with cURL against the /resource/authentication URL. MAKE SURE TO INCLUDE THE -i in cURL because you NEED THE HEADERS for your next requests.

curl -i -k 'https://agamemnon.smh101.com/resource/authentication' -H 'Content-Type: application/json' -d @login.json

This is the output. Pay attention to the X-Set-JWT header and set-cookie header.

HTTP/1.1 200 OK
Date: Thu, 14 Sep 2017 14:34:22 GMT
Strict-Transport-Security: max-age=31536000
Content-Security-Policy-Report-Only:  script-src 'self' 'unsafe-eval' 'unsafe-inline'; object-src 'none'; img-src 'self' data:; media-src 'none'; frame-src 'none'; connect-src 'self'
X-Content-Security-Policy-Report-Only: script-src 'self' 'unsafe-eval' 'unsafe-inline'; object-src 'none';  img-src 'self' data:; media-src 'none'; frame-src 'none'; connect-src 'self'
X-WebKit-CSP-Report-Only:  script-src 'self' 'unsafe-eval' 'unsafe-inline'; object-src 'none'; img-src 'self' data:; media-src 'none'; frame-src 'none'; connect-src 'self'
Surrogate-Control: no-store
Cache-Control: no-store, no-cache, must-revalidate, proxy-revalidate
Pragma: no-cache
Expires: 0
X-Download-Options: noopen
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Request-ID: a2a91049-a759-4b04-b766-3c0e9e207ea1
X-Set-JWT: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjc3JmIjoib3UzdHRkUEctOVNYY05oUFBFM2x4dU5EeUFhTWRaYzltOGtnIiwiaWF0IjoxNTA1Mzk5NjYyfQ.k1mWhvEkMJfkJxj6gldg2NEyUWVA_3z26jFqB3b6VSI
Content-Type: application/json; charset=utf-8
Content-Length: 2424
ETag: W/"978-Bei1gelS9uDFdLq5LORNTA"
X-Response-Time: 534.188ms
Via: 1.1 web.vistacore.us
Vary: User-Agent,Accept-Encoding
set-cookie: ehmp.vistacore.rdk.sid=s%3AYufjszYzWuvnIiHwoy_RSAsmN3HsGaIX.fTPRWYOHvjx5U1%2FdveElgtyGtn%2BqpAcHc4a%2Bh31ay8o; Path=/; Expires=Thu, 14 Sep 2017 14:49:22 GMT; HttpOnly; Secure

{"data":{"uid":"urn:va:user:9E7A:10000000271","disabled":false,"divisionSelect":false,"duz":{"9E7A":"10000000271"},"expires":"2017-09-14T14:49:22.032Z","facility":"PANORAMA","firstname":"MARGARET","lastname":"XIU","preferences":{"defaultScreen":{}},"permissions":["access-general-ehmp","access-stack-graph","add-active-medication","add-allergy","add-condition-problem","add-consult-order","add-encounter","add-immunization","add-lab-order","add-med-order","add-non-va-medication","add-note","add-note-addendum","add-patient-history","add-radiology-order","add-task","add-vital","cancel-task","complete-consult-order","cosign-note","delete-note","discontinue-active-medication","discontinue-consult-order","discontinue-lab-order","discontinue-med-order","discontinue-radiology-order","edit-active-medication","edit-allergy","edit-condition-problem","edit-consult-order","edit-encounter-form","edit-lab-order","edit-med-order","edit-military-history","edit-non-va-medication","edit-note","edit-note-addendum","edit-patient-history","edit-radiology-order","edit-task","eie-allergy","eie-immunization","eie-patient-history","eie-vital","follow-up-incomplete-lab-order","manage-cds-criteria","manage-cds-definition","manage-cds-patientlist","read-active-medication","read-allergy","read-cds-criteria","read-cds-definition","read-cds-patientlist","read-cds-work-product","read-cds-work-product-inbox","read-cds-work-product-subscription","read-clinical-reminder","read-community-health-summary","read-concept-relationship","read-condition-problem","read-document","read-encounter","read-enterprise-orderable","read-fhir","read-immunization","read-medication-review","read-military-history","read-operational-data","read-order","read-orderable","read-patient-history","read-patient-record","read-task","read-vista-health-summary","read-vital","read-vler-toc","remove-condition-problem","review-result-consult-order","schedule-consult-order","sign-consult-order","sign-lab-order","sign-med-order","sign-note","sign-note-addendum","sign-radiology-order","triage-consult-order","read-cds-engine","read-cds-execute","read-cds-intent","read-cds-metric","read-cds-metric-dashboard","read-cds-metric-definition","read-cds-metric-group","read-cds-schedule","read-user-preferences"],"pcmm":[],"requiresReset":false,"section":"Medicine","sessionLength":900000,"site":"9E7A","division":"500","title":"Clinician","provider":true},"status":200}sam@icarus:~/workspace/OSEHRA$ 

Let's try the resource directory call now, but this time supplying the JWT token and the cookie to cURL, like this:

curl -k 'https://agamemnon.smh101.com/resource/resourceDirectory' -H 'Cookie: ehmp.vistacore.rdk.sid=s%3AYufjszYzWuvnIiHwoy_RSAsmN3HsGaIX.fTPRWYOHvjx5U1%2FdveElgtyGtn%2BqpAcHc4a%2Bh31ay8o' -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjc3JmIjoib3UzdHRkUEctOVNYY05oUFBFM2x4dU5EeUFhTWRaYzltOGtnIiwiaWF0IjoxNTA1Mzk5NjYyfQ.k1mWhvEkMJfkJxj6gldg2NEyUWVA_3z26jFqB3b6VSI'

You will now get a very long list of resouces. Here's the top, which I formatted by piping curl's output into python -m json.tool

{
    "data": {
        "link": [
            {
                "href": "/resource/resourcedirectory",
                "rel": "vha.read",
                "title": "resource-directory"
            },
            {
                "href": "/resource/healthcheck/healthy",
                "rel": "vha.read",
                "title": "healthcheck-healthy"
            },
            {
                "href": "/resource/healthcheck/detail/html",
                "rel": "vha.read",
                "title": "healthcheck-detail-html"
            },
            {
                "href": "/resource/healthcheck/checks",
                "rel": "vha.read",
                "title": "healthcheck-checks"
            },...

Creating the resource

You need to create a js file in /opt/rdk/src/resource that exports getResourceConfig. getResourceConfig supplies an array of URLs and links these URLs to javascript functions in your file.

Create this very minimal file as /opt/rdk/src/resources/census/bedboard.js

'use strict';

exports.getResourceConfig = function() {
    return [{
        name: 'bedboard-summary',
        path: '/summary',
        get: getBedsSummary,
        requiredPermissions: [],
        isPatientCentric: false
    },{
        name: 'bedboard-detail',
        path: '/detail',
        get: getBedsDetail,
        requiredPermissions: [],
        isPatientCentric: false
    }];
}
function getBedsSummary(req, res) {
        return res.rdkSend({Hello: "Summary"})
}
function getBedsDetail(req, res) {
        return res.rdkSend({Hello: "Detail"})
}

Registering the resource

And then, edit /opt/rdk/bin/rdk-fetch-server.js to add these one line at the end of all of the registers:

app.register('/census/bedboard', ROOT + '/src/resources/census/bedboard');

At this point, restart the RDK fetch server. I personally find it much easier to just stop the server and then run the node.js fetch process in the foreground-- this way I can see it crash if I made a mistake somewhere.

[vagrant@rdk-sam-master rdk]$ sudo stop fetch_server                                                                                                            
fetch_server stop/waiting
[vagrant@rdk-sam-master rdk]$ sudo -E $(which node) /opt/rdk/bin/rdk-fetch-server.js | /opt/rdk/node_modules/bunyan/bin/bunyan                                  

Okay. At this point, let's request the resource directory again. We should see our two new resources there:

curl -k 'https://agamemnon.smh101.com/resource/resourceDirectory' -H 'Cookie: ehmp.vistacore.rdk.sid=s%3AYufjszYzWuvnIiHwoy_RSAsmN3HsGaIX.fTPRWYOHvjx5U1%2FdveElgtyGtn%2BqpAcHc4a%2Bh31ay8o' -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjc3JmIjoib3UzdHRkUEctOVNYY05oUFBFM2x4dU5EeUFhTWRaYzltOGtnIiwiaWF0IjoxNTA1Mzk5NjYyfQ.k1mWhvEkMJfkJxj6gldg2NEyUWVA_3z26jFqB3b6VSI' | python -m json.tool
...
...
...
            {
                "href": "/resource/census/bedboard/summary",
                "rel": "vha.read",
                "title": "bedboard-summary"
            },
            {
                "href": "/resource/census/bedboard/detail",
                "rel": "vha.read",
                "title": "bedboard-detail"
            }
        ]
    },
    "status": 200
}

Great! Now let's try calling them:

curl -k 'https://agamemnon.smh101.com/resource/census/bedboard/summary' -H 'Cookie: ehmp.vistacore.rdk.sid=s%3AYufjszYzWuvnIiHwoy_RSAsmN3HsGaIX.fTPRWYOHvjx5U1%2FdveElgtyGtn%2BqpAcHc4a%2Bh31ay8o' -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjc3JmIjoib3UzdHRkUEctOVNYY05oUFBFM2x4dU5EeUFhTWRaYzltOGtnIiwiaWF0IjoxNTA1Mzk5NjYyfQ.k1mWhvEkMJfkJxj6gldg2NEyUWVA_3z26jFqB3b6VSI' ; echo
{"data":{"Hello":"Summary"},"status":200}
curl -k 'https://agamemnon.smh101.com/resource/census/bedboard/detail' -H 'Cookie: ehmp.vistacore.rdk.sid=s%3AYufjszYzWuvnIiHwoy_RSAsmN3HsGaIX.fTPRWYOHvjx5U1%2FdveElgtyGtn%2BqpAcHc4a%2Bh31ay8o' -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjc3JmIjoib3UzdHRkUEctOVNYY05oUFBFM2x4dU5EeUFhTWRaYzltOGtnIiwiaWF0IjoxNTA1Mzk5NjYyfQ.k1mWhvEkMJfkJxj6gldg2NEyUWVA_3z26jFqB3b6VSI' ; echo
{"data":{"Hello":"Detail"},"status":200}

Making our resource do something useful

Now that we have the skeleton of the resources in place, it's time to add our RPC in, starting with the summary view:

'use strict';
var vistajs = require('vista-js');
var RpcClient = vistajs.RpcClient;
var RpcParameter = vistajs.RpcParameter;
var _ = require('lodash');
var rdk = require('../../core/rdk');

function getBedsSummary(req, res) {
    // Stanza: Create vistaConfig using the user site,
    // accessCode, verifyCode, and context
    var vistaSite = req.session.user.site;
    var vistaConfig = _.extend({}, req.app.config.vistaSites[vistaSite], {
        context: req.app.config.rpcConfig.context,
        accessCode: req.session.user.accessCode,
        verifyCode: req.session.user.verifyCode
    });

    req.audit.logCategory = 'RETRIEVE';

    // Stanza: Call RPC
    var parameters = [];
    var rpcName = 'KBAN BED BOARD';
    RpcClient.callRpc(req.logger, vistaConfig, rpcName, parameters,
        function(err, result) {
            if(err) {
                req.logger.error(err, 'Bedboard response error');
                return res.status(500).rdkSend(err);
            }
            
            // Convert the JSON text output into a JSON object
            var parsedResult;
            try {
                parsedResult = JSON.parse(result);
                console.log(parsedResult);
            }
            catch (e) {
                req.logger.error(err, 'Bedboard JSON parse error');
                return res.status(500).rdkSend(err);
            }
            
            // Count the number of patients in each ward, and then send that
            // data back to the ADK as an array
            var wardData = new Array();
            // For each ward
            Object.keys(parsedResult).forEach(function(wardName) {
              // Count the patients using the reduce function
              var patientsInWard = Object.keys(parsedResult[wardName]).reduce(function(wardPatientsAccumulator, bed) {
                var bedData = parsedResult[wardName][bed];
                var patient = bedData.split('^')[0];
                var hasPatient = patient !== '';
                if (hasPatient) return wardPatientsAccumulator + 1;
                else return wardPatientsAccumulator;
              }, 0);
              // Each item we send back is a datum
              var datum = {};
              datum.name = wardName;
              datum.census = patientsInWard;
              // And we push that into an array
              wardData.push(datum);

            });
            console.log(wardData);
            // And we then send back that array
            return res.rdkSend(wardData);
        }
    );
}

Now, we get this output:

curl -k 'https://agamemnon.smh101.com/resource/census/bedboard/summary' -H 'Cookie: ehmp.vistacore.rdk.sid=s%3AYufjszYzWuvnIiHwoy_RSAsmN3HsGaIX.fTPRWYOHvjx5U1%2FdveElgtyGtn%2BqpAcHc4a%2Bh31ay8o' -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjc3JmIjoib3UzdHRkUEctOVNYY05oUFBFM2x4dU5EeUFhTWRaYzltOGtnIiwiaWF0IjoxNTA1Mzk5NjYyfQ.k1mWhvEkMJfkJxj6gldg2NEyUWVA_3z26jFqB3b6VSI' | python -m json.tool
{
    "data": [
        {
            "census": 0,
            "name": "2-INTERMED"
        },
        {
            "census": 0,
            "name": "3 NORTH GASTRO"
        },
        {
            "census": 5,
            "name": "3 NORTH GU"
        },
        {
            "census": 1,
            "name": "3 NORTH SURG"
        },
        {
            "census": 7,
            "name": "3E NORTH"
        },
...

The detail view is easier, as we just send the VistA output staight:

function getBedsDetail(req, res) {
    // Stanza: Crate vistaConfig using the user site,
    // accessCode, verifyCode, and context
    var vistaSite = req.session.user.site;
    var vistaConfig = _.extend({}, req.app.config.vistaSites[vistaSite], {
        context: req.app.config.rpcConfig.context,
        accessCode: req.session.user.accessCode,
        verifyCode: req.session.user.verifyCode
    });

    req.audit.logCategory = 'RETRIEVE';

    // Stanza: Call RPC
    var parameters = [];
    var rpcName = 'KBAN BED BOARD';
    RpcClient.callRpc(req.logger, vistaConfig, rpcName, parameters,
        function(err, result) {
            if(err) {
                req.logger.error(err, 'Bedboard response error');
                return res.status(500).rdkSend(err);
            }
            
            // Convert the JSON text output into a JSON object
            var parsedResult;
            try {
                parsedResult = JSON.parse(result);
                console.log(parsedResult);
            }
            catch (e) {
                req.logger.error(err, 'Bedboard JSON parse error');
                return res.status(500).rdkSend(err);
            }
            return res.rdkSend(parsedResult);
        });
}

Feel free to test it by getting /resource/census/bedboard/detail.

At this point, we are done writing the RDK resouces. We next move to be able to display the data using the ADK.

ADK Programming

As with the RDK, there are roughly two steps:

  • Create an applet
  • Register the applet in appletsManifest.js

The SDK (referenced above in the RDK section) has an ADK section, but it's not as good as the RDK documentation.

For better or worse, you MUST know Backbone and Marionette in order to program ADK. I know neither--and I have found my lack of knowledge hampered my progress. Knowing HTML, CSS and JS inside-out will not help you. Backbone/Marionette want to do things their own way--and you can't just use HTML and jQuery to build your page.

Creating an Applet

This is the simplest example that actually shows something useful that I could come up with.

Create this file in: /var/www/ehmp-ui/app/applets/census/applet.js. Create the census directory first.

define(['main/ADK','underscore','handlebars'],
function (ADK, _, Handlebars) {

  // The resource title down here is the resource title for the RDK call that you want to use.
  // Remember the call to /resources/resourceDirectory above. That's the one
  // that gives you the resource title corresponding to the resource URL you
  // want to invoke
  var fetchOptions = {};
  fetchOptions.resourceTitle = 'bedboard-summary';

  // There are several prepackaged views you can use from the ADK. I am using
  // one of them here, called the PillsGistView
  // Most of what's here is just boilerplate. The important part is how to get
  // the so called 'collection' - a Backbone term meaning an array of data.
  // You need to use the ADK.ResourceService for that.
  var AppletGistView = ADK.AppletViews.PillsGistView.extend({
      initialize: function(options) {
          var self = this;
          this._super = ADK.AppletViews.PillsGistView.prototype;
          this.appletOptions = {};
          this.appletOptions.collection = ADK.ResourceService.fetchCollection(fetchOptions);
          this._super.initialize.apply(this, arguments);
       }
  });

  // This is perhaps the most important part. You must return an applet with
  // a specific id and that has an array of viewTypes--with an optional
  // defaultViewType. Note how the gistView.view ties back to object
  // AppletGistView
  var applet = {};
  applet.id = "census";
  applet.viewTypes = new Array();
  gistView = {};
  gistView.type = 'gist';
  gistView.view = AppletGistView;
  gistView.chromeEnabled = true;
  applet.viewTypes.push(gistView);
  applet.defaultViewType = 'gist';
  return applet;
});

It bears repeating: You must return the variable applet with an id attribute and a viewTypes array set. Otherwise, your browser will error when trying to load your applet.

Once you are done, you can now add this applet to appletsManifest.js.

Register the applet in appletsManifest.js

The path to the file is: '/var/www/ehmp-ui/app/applets/appletsManifest.js'.

You need to add this stanza, observing Javascript formatting standards:

    {
       id: "census",
       title: "Patient Census",
       context: ['patient', 'staff'],
       showInUDWSelection: true,
       permissions: []
    }

A few items to note: the id MUST BE the same as the id in the applet we just created. The title is what the user will see as the title of the applet in eHMP. The context is either patient or staff. A bedboard does not belong to a patient, so the context should be staff; however, eHMP does not have a way of selecting applets that are just staff applets. Since I haven't figured out how to do that, I just dodged the issue by putting 'patient' there.

You must put showInUDWSelection to true in order to see the new applet in the workspace composer.

We left permissions blank for now. Actually, it's not really clear which permission should we use, since there is not really any permission in eHMP that suits this application.

Once you are done with this, and have the files saved, log-in into eHMP with a user that can edit their workspace. Find the circular icon that contains 4 windows and click on it.

Workspace Editor Select.

In the following dialog, click on the '+' sign at the top right corner.

Workspace Manager

You will get a new row that is named "User Defined Workspace 1". You can change this name. Click on "Customize".

Workspace Manager - Choose Your Name

You will get the workspace editor

Workspace Editor

At the top, find the 'Patient Census' button. You will probably need to click on the right arrow at the top to see it.

Patient Census Button

Once you find it, drag it down to the workspace. Once you release the drag, you will be asked what view you want. We only created a single view, shown here as a "Trend" view.

Select Trend View

After you click that, you will see this:

Trend View

Hit the Accept button at the bottom of the dialog, which will open up your new workspace, and you will see the patient census view in it:

Patient Census Applet

So, what we see here is just each ward, but without seeing the census. The reason is (after I found out after spent hours stepping through the source code) is the Gist view only shows the name property in the text of each 'pill'. So, to show the census, we need to slightly modify our code to edit the "Model". We do that by creating an object with parse function and attaching that object to our fetchOptions as a viewModel. Here's the new code:

define([
  'main/ADK',
  'underscore',
  'handlebars'
], function (ADK, _, Handlebars) {
  
  // NEW -->
  viewParseModel = {};
  viewParseModel.parse = function(response) {
    response.name += ': ' + response.census;
    return response;
  };

  var fetchOptions = {};
  fetchOptions.resourceTitle = 'bedboard-summary';
  fetchOptions.viewModel = viewParseModel;
  // <-- NEW

  var AppletGistView = ADK.AppletViews.PillsGistView.extend({
      initialize: function(options) {
          var self = this;
          this._super = ADK.AppletViews.PillsGistView.prototype;
          this.appletOptions = {};
          this.appletOptions.collection = ADK.ResourceService.fetchCollection(fetchOptions);
          this._super.initialize.apply(this, arguments);
       }
  });

  var applet = {};
  applet.id = "census";
  applet.viewTypes = new Array();
  gistView = {};
  gistView.type = 'gist';
  gistView.view = AppletGistView;
  gistView.chromeEnabled = true;
  applet.viewTypes.push(gistView);
  applet.defaultViewType = 'gist';
  return applet;
});

And if you refresh the screen, you will see this:

Patient Census Applet

VistA Application: Part 2

The purpose of this part is to actually create a full bedboard. You may wish to just skip to the Weather application instead if you are not interested in this.

Our previous view only used summary view of the bedboard (how many patients per ward displayed in a pill view). Now, we would like the ability for us to provide a maximized view of the applet to show us the entire bedboard for the hospital we are logged into. Here's the end result:

Full Bed Board

Because we already have the resource from the RDK to do this (the detail view), all we have to do to create this is edit the ADK.

Note that the complete source code for each file is provided at the end of this section. If any of the instructions are not clear in where you need to place a piece of code, consult the full listing to figure that part out.

Our first step is to provide a maximize button on the patient census applet. Easier said than done. After tracing the source code and looking at other examples, here's what you need to do:

In /var/www/ehmp-ui/app/screens, create file BedBoard.js with the following content

define([
    "backbone",
    "marionette"
  ], function(Backbone, Marionette) {
  'use strict';

  var screenConfig = {
    id: 'bedBoard-full',  /* IMPORTANT */
    context: 'patient',
    contentRegionLayout: 'gridOne',
    appletHeader: 'navigation',
    applets: [{
      id: 'census',       /* IMPORTANT */
      title: 'Bed Board',
      region: 'center',
      fullScreen: true,
      viewType: 'expanded' /* IMPORTANT */
    }],
    patientRequired: false
  };

  return screenConfig;
});

The most important two pieces are the id, and the applets associated with the screen. You need the id for later; and you need the correct applet id in there plus the name of the new viewType we are going to make.

After this, we need to add the new screen to the ScreensManifest.js file, also in the screen directory.

  screens.push({
      routeName: 'bedBoard-full',
      fileName: 'BedBoard'
  });

Last, you need to go to appletsManifest.js in the applets folder (/var/www/ehmp-ui/app/applets), and add the attribute maximizeScreen: 'bedBoard-full' to the census applet definition. The new definition is like this:

 id: "census",
 title: "Patient Census",
 context: ['patient', 'staff'],
 showInUDWSelection: true,
 maximizeScreen: 'bedBoard-full',
 permissions: []

The UI caches the previous configuration of the applet, so you can't just refresh and move on. Look for the trash can in the Workspace Manager in the same row as your new workspace, click on it to delete it, and then add the Patient Census widget again. At this point you will see the maximize button. When you click it, you will see an empty screen with the workspace name becoming Bed Board.

Maximize Button

Bedboard Workspace

At this point, we have proven that we can get the maximized view, we need to draw something in it.

Rather than write the main html in javascript, we can put it in a template and load it at runtime. In ...app/applets/census, create folder templates and put file bedboard.html into it with the following content:

<div id="main-content" class="container-fluid">
  <div id="wards" class="row collapse" style="display: block;">
   <p>Boo</p>
  </div>
</div>

At the top of the applet.js file, import the template like this:

define([
    'main/ADK',
    'underscore',
    'handlebars',
    'hbs!app/applets/census/templates/bedboard'
], function (ADK, _, Handlebars, BedBoardTemplate) {

Add an expanded view to the applet variable like this:

  var expandedView = {};
  expandedView.type = 'expanded'
  expandedView.chromeEnabled = true;
  expandedView.view = FullScreenView;
  applet.viewTypes.push(expandedView);

Create a new fetchOptions object for the detailed view resoruce:

  var bedboardFetchOptions = {};
  bedboardFetchOptions.resourceTitle = 'bedboard-detail';

And create the FullScreenView referenced by the expandedView:

  var FullScreenView = Backbone.Marionette.LayoutView.extend({
    initialize: function(options) {
      this.appletOptions = {};
      this.appletOptions.collection = ADK.ResourceService.fetchCollection(bedboardFetchOptions);
    },
    template: BedBoardTemplate,
    regions: {
      bedboardViewRegion: '#wards'
    }
   });

Now, if you refresh, you should see the word "boo" show up and the detailed listing of beds make it way through the wire. Sorry, I don't have a screenshot for this stage.

So last thing to do is to write some code that will take the collection and display some HTML. I found that really hard. I tried binding to the 'show' or 'render' events that Marionette sends, but when they are sent, the collection is not done loading yet--so there is nothing to put there. After digging a lot, I found that you can tie an event to the ending of the resource fetching. Bind this.appletOptions.collection to a new object we will create called this.collectionEvents. In that object, we will put a function off the key of 'fetch:success' that will let us use the data that we got and put that data into the screen. I seriously spent a lot of time with trial an error to figure this out.

The new FullScreenView now looks like this:

  var FullScreenView = Backbone.Marionette.LayoutView.extend({
    initialize: function(options) {   
      this.appletOptions = {};
      this.appletOptions.collection = ADK.ResourceService.fetchCollection(bedboardFetchOptions);
      this.bindEntityEvents(this.appletOptions.collection, this.collectionEvents);
    },
    template: BedBoardTemplate,
    regions: {
      bedboardViewRegion: '#wards'
    },
    collectionEvents: {
      'fetch:success': function(collection, resp) {
        var self = this;
        self.$el.css('height', '100%');
        self.$el.css('width', '100%');
        self.$el.css('overflow-y', 'scroll');
        $wards = self.$el.find(self.regions.bedboardViewRegion);
        Object.keys(collection.at(0).attributes).forEach(function(wardName) {
          var beds = collection.at(0).attributes[wardName];
          let html = '<div class="main col-md-4"><h2 class="sub-header">';
          html     = html + wardName + '</h2><div class="table-responsive">';
          html     = html + '<table class="table table-striped"><thead><tr><td>Bed</td>';
          html     = html + '<td>Patient</td><td>Gender</td><td>Admission Date</td>';
          html     = html + '</tr></thead><tbody>';

          Object.keys(beds).forEach(function(bedName) {
           var bedData = beds[bedName];
           var u = '^';
           var patientName = bedData.split(u)[0];
           var patientSex  = bedData.split(u)[1];
           var admissionDate = bedData.split(u)[2];
           var oos = +bedData.split(u)[4];
           html = html + '<tr><td>' + bedName;
           if (oos) {
             var oosReason = bedData.split(u)[5] + ": " + bedData.split(u)[6];
             html = html + ' <span class="glyphicon glyphicon-exclamation-sign"';
             html = html + ' aria-hidden="true" title="' + oosReason + '"></span>';
           }
           html = html + '</td><td>'; 
           if (patientName) {
             html = html + patientName;
           }
           html = html + '</td><td>'; 
           if (patientSex) {
             html = html + patientSex;
           }
           html = html + '</td><td>'; 
           if (admissionDate) {
             html = html + admissionDate.replace(/@.*/, '');
           }
           html = html + '</td></tr>';
         });

         html = html + '</tbody></table></div></div>';

         $wards.append(html);
        });
      }
    }
  });

Figuring out the correct jQuery object to bind to wasn't easy. In the end, it turns out that this.$el is the correct reference to the current applet, and you can use that to insert HTML into your applet.

The full files are provided for your use:

screens/ScreensManifest.js (truncated)

define(function() {
    'use strict';
    var screensManifest = {
        ssoLogonScreen: 'sso'
    };

    var screens = [];
    screens.push({
        routeName: 'ui-components-demo',
        fileName: 'UIComponentsDemo'
    });
    ...
    screens.push({
        routeName: 'bedBoard-full',
        fileName: 'BedBoard'
    });


    screensManifest.screens = screens;

    return screensManifest;
});

screens/BedBoard.js

define([
    "backbone",
    "marionette"
], function(Backbone, Marionette) {
    'use strict';

    var screenConfig = {
      id: 'bedBoard-full',
      context: 'patient',
      contentRegionLayout: 'gridOne',
      appletHeader: 'navigation',
      applets: [{
        id: 'census',
        title: 'Bed Board',
        region: 'center',
        fullScreen: true,
        viewType: 'expanded'
      }],
      patientRequired: false
    };

    return screenConfig;
});

applets/appletsManifest.js (truncated)

define(['main/ADK'], function(ADK) {
    'use strict';
    var appletsManifest = {};
    var crsDomain = ADK.utils.crsUtil.domain;
    var applets = [{
        id: 'ui_components_demo',
        title: 'UI Components Demo',
        context: ['demo'],
        showInUDWSelection: false,
        permissions: []
        ...
    }, {
        id: "weather",
        title: "Weather",
        context: ['patient'],
        showInUDWSelection: true,
        permissions: []
    }, {
       id: "census",
       title: "Patient Census",
       context: ['patient', 'staff'],
       showInUDWSelection: true,
       maximizeScreen: 'bedBoard-full',
       permissions: []
    }];

    appletsManifest.applets = applets;
    return appletsManifest;
});

applets/census/applet.js

define([
  'main/ADK',
  'underscore',
  'handlebars',
  'hbs!app/applets/census/templates/bedboard'
], function (ADK, _, Handlebars, BedBoardTemplate) {

  viewParseModel = {};
  viewParseModel.parse = function(response) {
    response.name += ': ' + response.census;
    return response;
  };

  var fetchOptions = {};
  fetchOptions.resourceTitle = 'bedboard-summary';
  fetchOptions.viewModel = viewParseModel;
  
  var bedboardFetchOptions = {};
  bedboardFetchOptions.resourceTitle = 'bedboard-detail';

  var AppletGistView = ADK.AppletViews.PillsGistView.extend({
    initialize: function(options) {
        var self = this;
        this._super = ADK.AppletViews.PillsGistView.prototype;
        this.appletOptions = {};
        this.appletOptions.collection = ADK.ResourceService.fetchCollection(fetchOptions);
        this._super.initialize.apply(this, arguments);
     }
  });

  var FullScreenView = Backbone.Marionette.LayoutView.extend({
    initialize: function(options) {
      this.appletOptions = {};
      this.appletOptions.collection = ADK.ResourceService.fetchCollection(bedboardFetchOptions);
      this.bindEntityEvents(this.appletOptions.collection, this.collectionEvents);
    },
    template: BedBoardTemplate,
    regions: {
      bedboardViewRegion: '#wards'
    },
    collectionEvents: {
      'fetch:success': function(collection, resp) {
        var self = this;
        self.$el.css('height', '100%');
        self.$el.css('width', '100%');
        self.$el.css('overflow-y', 'scroll');
        $wards = self.$el.find(self.regions.bedboardViewRegion);
        Object.keys(collection.at(0).attributes).forEach(function(wardName) {
          var beds = collection.at(0).attributes[wardName];
          let html = '<div class="main col-md-4"><h2 class="sub-header">';
          html     = html + wardName + '</h2><div class="table-responsive">';
          html     = html + '<table class="table table-striped"><thead><tr><td>Bed</td>';
          html     = html + '<td>Patient</td><td>Gender</td><td>Admission Date</td>';
          html     = html + '</tr></thead><tbody>';

          Object.keys(beds).forEach(function(bedName) {
           var bedData = beds[bedName];
           var u = '^';
           var patientName = bedData.split(u)[0];
           var patientSex  = bedData.split(u)[1];
           var admissionDate = bedData.split(u)[2];
           var oos = +bedData.split(u)[4];
           html = html + '<tr><td>' + bedName;
           if (oos) {
             var oosReason = bedData.split(u)[5] + ": " + bedData.split(u)[6];
             html = html + ' <span class="glyphicon glyphicon-exclamation-sign"';
             html = html + ' aria-hidden="true" title="' + oosReason + '"></span>';
           }
           html = html + '</td><td>';
           if (patientName) {
             html = html + patientName;
           }
           html = html + '</td><td>';
           if (patientSex) {
             html = html + patientSex;
           }
           html = html + '</td><td>';
           if (admissionDate) {
             html = html + admissionDate.replace(/@.*/, '');
           }
           html = html + '</td></tr>';
         });

         html = html + '</tbody></table></div></div>';

         $wards.append(html);
	});
      }
    }
  });

  var applet = {};
  applet.id = "census";
  
  applet.viewTypes = new Array();
  
  var gistView = {};
  gistView.type = 'gist';
  gistView.view = AppletGistView;
  gistView.chromeEnabled = true;
  applet.viewTypes.push(gistView);
  
  var expandedView = {};
  expandedView.type = 'expanded'
  expandedView.chromeEnabled = true;
  expandedView.view = FullScreenView;
  applet.viewTypes.push(expandedView);

  applet.defaultViewType = 'gist';
  return applet;
});

Weather Application

This is a weather application that will display the weather at the location calculated by your IP address.

Before getting started, you need to get an API key to be able to access the API at Wunderground. You can sign-up at https://www.wunderground.com/weather/api for a free account for limited usage of the API.

Since there is no VistA component, all we have to do is work on the RDK, and then work on the ADK.

RDK Set-up

The step by step approach was presented in the previous section. I won't repeat the steps here, but just a summary:

  • Create the resource in /opt/rdk/resources/
  • Add the resource to /opt/rdk/bin/rdk-fetch-server.js

In /opt/rdk/resouces/, we create a folder called weather with two files, whose contents are as follows:

weather-resource.js - provides the getResourceConfig:

'use strict';

var weather = require('./weather-info');

var getResourceConfig = function() {
    return [{
            name: 'weather-service-weatherinfo',
            path: '/info',
            interceptors: {
                operationalDataCheck: false,
                synchronize: false
            },
            requiredPermissions: [],
            isPatientCentric: false,
            get: weather.getWeatherInfo,
            healthcheck: {}
        }];
};

module.exports.getResourceConfig = getResourceConfig;

weather-info.js makes the actual HTTP call to Wunderground and sends the results back to the RDK. You need to replace {api_key} with your alpha-numeric API key. Don't keep the braces. endpoint should looks like /api/xxxsss2223344/conditions/q/autoip.json.

'use strict';

var rdk = require('../../core/rdk');
var http = require('http');

var weather = {}

weather.getWeatherInfo = function(req, res) {
  performRequest(req, res);
};

function performRequest(req, outerRes) {
  var host = 'api.wunderground.com';
  var endpoint = '/api/{api_key}/conditions/q/autoip.json'

  var options = {
    hostname: host,
    path: endpoint
  };

  var req = http.request(options, function(res) {
    var responseString = '';

    res.on('data', function(data) {
      responseString += data;
    });

    res.on('end', function() {
      var responseObject = JSON.parse(responseString);
      outerRes.status(rdk.httpstatus.ok).rdkSend({'data': responseObject });
    });
  });

  req.end();
}

You will expose this as /weather in /opt/rdk/bin/rdk-fetch-server.js:

app.register('/weather', ROOT + '/src/resources/weather/weather-resource');                                                                                     |

Restart the RDK fetch server, and run the authorizations web service call, the resource directory call, and then test the endpoint.

I personally find it much easier to just stop the server and then run the node.js fetch process in the foreground-- this way I can see it crash if I made a mistake somewhere.

[vagrant@rdk-sam-master rdk]$ sudo stop fetch_server                                                                                                            
fetch_server stop/waiting
[vagrant@rdk-sam-master rdk]$ sudo -E $(which node) /opt/rdk/bin/rdk-fetch-server.js | /opt/rdk/node_modules/bunyan/bin/bunyan                                  

Authenticate by following "Step -1" above when we did the RDK set-up for VistA. Keep in your clipboard the cookie value and the JWT token value.

Okay. At this point, let's request the resource directory. We should see the weather resource (replace the cookie and authorization header with the correct values.

curl -k 'https://agamemnon.smh101.com/resource/resourceDirectory' -H 'Cookie: ehmp.vistacore.rdk.sid=s%3AYufjszYzWuvnIiHwoy_RSAsmN3HsGaIX.fTPRWYOHvjx5U1%2FdveElgtyGtn%2BqpAcHc4a%2Bh31ay8o' -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjc3JmIjoib3UzdHRkUEctOVNYY05oUFBFM2x4dU5EeUFhTWRaYzltOGtnIiwiaWF0IjoxNTA1Mzk5NjYyfQ.k1mWhvEkMJfkJxj6gldg2NEyUWVA_3z26jFqB3b6VSI' | python -m json.tool
...
    {
        "href": "/resource/weather/info",
        "rel": "vha.read",
        "title": "weather-service-weatherinfo"
    }
...

Call the API at /resource/weather/info now:

curl -k 'https://agamemnon.smh101.com/resource/weather/info' -H 'Cookie: ehmp.vistacore.rdk.sid=s%3AYufjszYzWuvnIiHwoy_RSAsmN3HsGaIX.fTPRWYOHvjx5U1%2FdveElgtyGtn%2BqpAcHc4a%2Bh31ay8o' -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjc3JmIjoib3UzdHRkUEctOVNYY05oUFBFM2x4dU5EeUFhTWRaYzltOGtnIiwiaWF0IjoxNTA1Mzk5NjYyfQ.k1mWhvEkMJfkJxj6gldg2NEyUWVA_3z26jFqB3b6VSI' | python -m json.tool
{
    "data": {
        "current_observation": {
            "UV": "6.0",
            "dewpoint_c": 19,
            "dewpoint_f": 66,
            "dewpoint_string": "66 F (19 C)",
            "display_location": {
                "city": "Arlington",
                "country": "US",
                "country_iso3166": "US",
                "elevation": "75.9",
                "full": "Arlington, VA",
                "latitude": "38.88999939",
                "longitude": "-77.08999634",
                "magic": "1",
                "state": "VA",
                "state_name": "Virginia",
                "wmo": "99999",
                "zip": "22201"
            },
            "estimated": {},
            "feelslike_c": "27",
            "feelslike_f": "81",
            "feelslike_string": "81 F (27 C)",
            "forecast_url": "http://www.wunderground.com/US/VA/Arlington.html",
            "heat_index_c": 27,
            "heat_index_f": 81,
            "heat_index_string": "81 F (27 C)",
            "history_url": "http://www.wunderground.com/weatherstation/WXDailyHistory.asp?ID=KVAARLIN59",
            "icon": "mostlycloudy",
            "icon_url": "http://icons.wxug.com/i/c/k/mostlycloudy.gif",
            "image": {
                "link": "http://www.wunderground.com",
                "title": "Weather Underground",
                "url": "http://icons.wxug.com/graphics/wu2/logo_130x80.png"
            },
            "local_epoch": "1505498287",
            "local_time_rfc822": "Fri, 15 Sep 2017 13:58:07 -0400",
            "local_tz_long": "America/New_York",
            "local_tz_offset": "-0400",
            "local_tz_short": "EDT",
            "nowcast": "",
            "ob_url": "http://www.wunderground.com/cgi-bin/findweather/getForecast?query=38.879158,-77.095284",
            "observation_epoch": "1505498284",
            "observation_location": {
                "city": "Lyon Park, Arlington",
                "country": "US",
                "country_iso3166": "US",
                "elevation": "282 ft",
                "full": "Lyon Park, Arlington, Virginia",
                "latitude": "38.879158",
                "longitude": "-77.095284",
                "state": "Virginia"
            },
            "observation_time": "Last Updated on September 15, 1:58 PM EDT",
            "observation_time_rfc822": "Fri, 15 Sep 2017 13:58:04 -0400",
            "precip_1hr_in": "0.00",
            "precip_1hr_metric": " 0",
            "precip_1hr_string": "0.00 in ( 0 mm)",
            "precip_today_in": "0.00",
            "precip_today_metric": "0",
            "precip_today_string": "0.00 in (0 mm)",
            "pressure_in": "30.07",
            "pressure_mb": "1018",
            "pressure_trend": "-",
            "relative_humidity": "64%",
            "solarradiation": "878",
            "station_id": "KVAARLIN59",
            "temp_c": 26.1,
            "temp_f": 79,
            "temperature_string": "79.0 F (26.1 C)",
            "visibility_km": "16.1",
            "visibility_mi": "10.0",
            "weather": "Mostly Cloudy",
            "wind_degrees": 230,
            "wind_dir": "SW",
            "wind_gust_kph": 0,
            "wind_gust_mph": 0,
            "wind_kph": 0,
            "wind_mph": 0,
            "wind_string": "Calm",
            "windchill_c": "NA",
            "windchill_f": "NA",
            "windchill_string": "NA"
        },
        "response": {
            "features": {
                "conditions": 1
            },
            "termsofService": "http://www.wunderground.com/weather/api/d/terms.html",
            "version": "0.1"
        }
    },
    "status": 200
}

ADK Set-up

As a summary of what we did before, we will create an applet by adding a folder with applet.js to /var/www/ehmp-ui/app/applets and then registring the applet by putting it in /var/www/ehmp-ui/app/applets/appletsManifest.js.

Make a folder called weather in /var/www/ehmp-ui/app/applets and create the following applet.js:

define([
    'main/ADK',
    "backbone",
    "marionette",
    "underscore",
], function(ADK, Backbone, Marionette, _) {
    "use strict";
    var _super;

    var Columns = [{
            name: 'country',
            label: 'Country',
            cell: 'string'
        }, {
            name: 'location',
            label: 'Location',
            cell: 'string'
        }, {
            name: 'temperature',
            label: 'Temperature',
            cell: 'string'
        }, {
            name: 'weather',
            label: 'Weather',
            cell: 'string'
        }, {
            name: 'wind',
            label: 'Wind',
            cell: 'string'
        }   
    ];  

    var fetchOptions = {
        resourceTitle: 'weather-service-weatherinfo',
        pageable: false,
        cache: true,
        criteria: {},
        viewModel: {
            parse: function(response) {
                //return response.current_observation;
                var weather = {};
                weather.station_id = response.current_observation.station_id;
                weather.weather = response.current_observation.weather;
                weather.location = response.current_observation.display_location.full;
                weather.country = response.current_observation.display_location.country;
                weather.temperature = response.current_observation.temperature_string;
                weather.wind = response.current_observation.wind_string;
                return weather;
            }
        }
    };

    var AppletLayoutView = ADK.Applets.BaseGridApplet.extend({
        _super: ADK.Applets.BaseGridApplet.prototype,
        initialize: function(options) {
            this.appletOptions = {
                columns: Columns,
                filterEnabled: false, 
                collection: ADK.PatientRecordService.fetchCollection(fetchOptions)
            };

            this.dataGridOptions = this.appletOptions;
            this._super.initialize.apply(this, arguments);
        },
        onRender: function() {
            this._super.onRender.apply(this, arguments);
        }
    });

    var applet = {
        id: "weather",
        viewTypes: [{
            type: 'summary',
            view: AppletLayoutView, 
            chromeEnabled: true
        }],
        defaultViewType: 'summary'
    };

    return applet;
});

In /var/www/ehmp-ui/app/applets/appletsManifest.js, add this, observing JS syntax rules:

{ 
  id: "weather", 
  title: "Weather", 
  context: ['patient'], 
  showInUDWSelection: true, 
  permissions: [] 
}

Once you do that you can now perform the same steps that you performed for the VistA application in order to find the new applet and load it in the browser.

This is my end result:

Weather Applet

And this concludes the tutorial!