You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Tom 853b4eabac First commit 10 months ago
api First commit 10 months ago
app First commit 10 months ago
core First commit 10 months ago
public First commit 10 months ago
services First commit 10 months ago
.env First commit 10 months ago
.gitignore First commit 10 months ago
README.md First commit 10 months ago
package.json First commit 10 months ago
server.js First commit 10 months ago
yarn.lock First commit 10 months ago

README.md

SMART on FHIR | CDS Hooks Workshop

  • Scenario: supports secure application launched from the EHR Environment

In Act2 we will take a template project and SMART on FHIR enable it to access the patient fhir resource. This template is a shell that gives us a web frontend, api and core business logic to let us focus just on the bits needed for a confidential SMART on FHIR application.

The first thing we will want to do is get the template project:

bam ck:twilson63/smart-template smart
cd smart
yarn 

Next we will want to setup a sandbox on logica. This sandbox gives us a FHIR Server with an Identity server.

Create Sandbox https://sandbox.logicahealth.org

Lets start up our server:

yarn dev

Lets make it available to the internet using ngrok (In a separate terminal)

npx ngrok http 5000

Now that we have our sandbox set up and our template project ready. Lets discuss our gameplan.

The project is organized like this:

  • core - core business logic for application
  • api - api and middleware for application
  • app - frontend web application
  • services - service/data gateway

server.js - express server

We will be working in the api and services modules.

In order to properly authenticate and authorize with a SMART on FHIR server we need to support two endpoints: /launch and /index. We can call them whatever we want, but we need to support these two entry points. The launch endpoint is triggered from the host platform this endpoint will enable our app to identify itself with the server and to get an authorization code directed to our index endpoint. This is the standard three-legged OAUTH2 or authorization grant approach.

We will use a library (fhirclient) that handles defensive coding for us and makes this handshake much easier to deal with. It interacts with the express-session middleware to maintain state for a given session.

NOTE: we will be using the in memory session, when using sessions it is important to use a session store like redis in a production n-tier/serverless environment, express-session has many adapters to add as a session store

const smart = require('fhirclient')

// NOTE: if you plan to make the SMART on FHIR app work with multiple vendors
// you would handle the client and secret identifiers differently, maybe a kv store
const clientId = process.env.CLIENT_ID
const clientSecret = process.env.CLIENT_SECRET
// GET /launch
module.exports.launch = (req, res) => 
  smart(req, res)
    .authorize({
      clientId,
      clientSecret,
      scope: 'launch patient/*.read patient/MedicationRequest.write openid fhirUser',
      redirectUri: '/'
    })

What does all this mean? So in order to interact with a FHIR Server we need a client and secret, then when the EHR launches us, it will give us a launch code and a FHIR server endpoint. We will need to query the fhir server endpoint to get the identity server. Then we need to request authorization from the identity server with our clientId, secret and scope.

The identity server will validate our credentials then submit a redirect to an authorization page for the user to approve, some users may have requested to hide the authorization page, but if they have not, they must click the authorize button. This page is served from the identity server and clearly states the scope the app is requesting, in our case, we are requesting the ability to read all of the patient data, write medication requests for the patient and the current user information.

Once the user clicks the authorize button, the identity server redirects the browser back to our smart on fhir app at the documented redirectUri. Which for our app is / the root endpoint.

In this redirect the identity server sends an authorization code, we can use that code to get our launch context information and an access_token which gives us access to the FHIR server.

We can handle that request using the following code:

module.exports.ready = (req, res, next) => 
  smart(req, res).ready()
    .then(next)
    .catch(next)

While this does not appear to be doing much, it is making another trip to the server to get the session information and access_token and storing it in the session based on the state identifier.

This gets us in the door, now we need to get a patient resource from the fhir server.

With that we will use our services/fhir.js file to request the patient resource from the fhir server.

const read = client => resource => query => {
  return fetch(client.serverUrl + '/Patient/' + client.patient, {
    headers: {
      authorization: 'Bearer ' + client.access_token
    }
  }).then(res => res.json())
}

module.exports = client => ({
  read(client)
})

In order to string this all together we need to grab the fhir session info from the session store via middleware and pass it to our services.

In the api/smart.js file, we want to add this middleware:

//--------- middleware
//
// Fhir Data Type for connection info
const Fhir = (access_token, serverUrl, patient) =>
({ access_token, serverUrl, patient })

const getServerUrl = prop('serverUrl')
const getAccessToken = path(['tokenResponse', 'access_token'])
const getPatient = path(['tokenResponse', 'patient'])

// smart on fhir middleware that takes the
// state from the query param
// and retrieve session info
module.exports.session = (req, res, next) => {
  const session = req.session[req.query.state]
  req.fhir = Fhir(getAccessToken(session), getServerUrl(session), getPatient(session))
  next()

}

helper functions

const prop = key => object => object[key]
const path = keys => object => keys.reduce((v, k) => prop(k)(v), object)

Congrats, we should have now just wired up a SMART on FHIR app

Lets walk through it together

Exercises: Following the GET /api/patient example, try to create a POST /api/medicationrequest

TODO: Add medicationrequest core logic, stub out api, stub out service, add svelte logic TODO: Add RxNorm service to find medication TODO: review check out fhir server facade process

The core business logic should be completed for you, all you have to work on is the api endpoint and the fhir service.

  • Highly recommend looking at the Next.js framework for building SMART on FHIR applications. I would also recommend that your separate your application is to a similar domain centric model where you isolate your business rules into highly testable components that do not depend on your implemenation details

Domain Centric

Onion Architecture

Clean Architecture

Why

  • Sustained Fast Flow: As change requests come in all of your logic is in one place and highly testable
  • In Technology change is constant: Need to change implementation details
  • Modular - able to migrate from business logic to a service over time
  • Controlled enhancment - add caching and performance optimizations without coupling you logic.

  • Separate the concerns

    • client
    • api
    • core
    • services (FHIR, RxNorm)

    e-prescribing module

Client

  • List Medication Requests
    • Patient Name, Gender, Date of Birth
    • Practitioner Name
    • Medications
      • Name, Directions, Start Date, End Date
    • Button create new medication
    • Button to sign medications
  • Create Medication Request
    • Patient Name, Prescriber Name
    • MedList (sidebar)
    • Form
      • Medication Name (Search RxNorm)
      • Directions
    • Create Button
  • Sign Medication Requests
    • Patient
    • Provider
    • List Medications
    • Password Input
    • MFA Code Input
    • Button to Sign

API

  • GET /Patient
  • GET /Practitioner/User
  • GET /MedicationRequests
  • GET /MedicationName
  • POST /MedicationRequest
  • PUT /RxList/:ListId - create list
  • POST /RxList/:ListId - add Medication Request to List
  • DELETE /RxList/:ListId/:MedicationRequestId - remove MedcationRequest from list
  • GET /RxList/:ListId - get Rx List
  • POST /RxList/:ListId/sign - sign RxList

Core

  • Patient
  • Practitioner
  • Medication
  • MedicationRequest
  • RxList

Services

  • InMemory DataStore (RxList)
  • FHIR Service
  • RxNorm Service