FHIR Proof

To FHIR and back, a round trip ping/echo.

By 興怡 "HL7Lot11" 杜

To catapult us in, we'll begin with the code. Rather than lead with the why, what, or how.

Goal

FHIR data round trip (proof of concept)

Subsystems diagram
Subsystems

Fhirbase

The first and most important piece is Fhirbase  (Github). I plan to write more about Fhirbase in a future post, but you'll get a abundance of information from their helpful documentation. I went to their Get Started page, and followed those instructions with just a a small detour. For me, the easiest way to install a PostgreSQL local dev instance was through the Snapcraft package.

    Which boils down to
  1. Snapd / Snappy (Already done)
  2. sudo snap install postgresql10
  3. sudo adduser postgres

After the Fhirbase installation is done, you can list the procedures:


$ sudo -iu postgres
$ postgresql10.pgctl -D $HOME/snap/postgresql10/common/data -l $HOME/snap/postgresql10/common/logs/logfile start
$ postgresql10.psql -h 127.0.0.1 -d fhirbase
fhirbase=# \df
fhirbase=# \sf fhirbase_read
      
The fhirbase_read procedure should be displayed:

CREATE OR REPLACE FUNCTION public.fhirbase_read(resource_type text, id text)
 RETURNS jsonb
 LANGUAGE plpgsql
AS $function$

// snip...
// ...snip
      
    Optional steps
  1. pg.sh (Gist)
    1. ./pg.sh fhirbase
    2. select id, resource->'name', resource->'gender' from patient where id = 'd3af67c9-0c02-45f2-bc91-fea45af3ee83';
  2. Alias pg commands
    1. sudo snap alias postgresql10.pgctl pg_ctl
    2. sudo snap alias postgresql10.psql psql

fhirbuffer

Once you have Fhirbase, the next step is to export the data for use by client tasks. That's our fhirbuffer service. fhirbuffer is based on the gRPC basics for Go walkthrough.

We start by writing a protocol definition: fhirbuffer.proto


// Protocol buffer description for a FHIR persistence tier service

syntax = "proto3";

package fhirbuffer;

// Interface exported by the server.
service Fhirbuffer {

 // Obtains the healthcare resource that matches the search criteria.
 rpc Read(Search) returns (Record) {}

 // Modifies the healthcare resource 
 rpc Update(Change) returns (Record) {}

}

// A search criteria to request the healthcare resource.
message Search {
 // A ID is the UUID of the record 
 string id = 1;

 // The resource type
 string type = 2;
}

// A modification to change the healthcare resource.
message Change {
 bytes resource = 1;
}

// A healthcare resource returned from the data store.
message Record {
 bytes resource = 1;
}
      
For now, we're happy with just the Read() and Update() calls for our service. Read accepts a Search parameter which translates into a request message with the id and type fields. The id is a UUID value that uniquely identifies a patient, in this case. The type is the resource type which, for our purposes, will be "Patient". The result returned will be a Record response. Note that the resource field is declared as bytes. These bytes will hold JSON. If this looks familiar, you're right! It's no accident that this definition matches the stored procedure fhirbase_read/update exactly. In this respect, fhirbuffer is a paper-thin wrapper around Fhirbase.

It's time to generate. Hopefully, you've already done the gRPC basics guide because the toolchain is required to proceed. To do anything with our .proto file, we need to run protoc. As an alternative, we can run the commands inside a Docker container. You can save this Gist as the Dockerfile. Then build the image, and run. For example, assuming test is the save directory (where you have the .proto file, and Dockerfile) here are the steps from the shell prompt:


$ cd test
$ docker build -t protoc .
$ docker run -ti --rm -v $PWD:/app -w /app --entrypoint sh protoc
# protoc -I ./ fhirbuffer.proto --go_out=plugins=grpc:.
# exit
      
That should produce a new file with a .pb.go extension. Awesome right? Just soak it in. We're not going to do anything since it is generated code, but what we have here is the interface. It gets the contract down in code so that we can start making calls from Go. If you're not impressed that's okay too, we'll get another chance later with some Elixir.

So what do we do with the new generated fhirbuffer.pb.go file? We'll start by making the service which interacts with the Fhirbase database. Skipping a few lines for clarity, here is our main function:


package main

import (
// snip...
// ...snip

	pb "github.com/patterns/fhirbuffer"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)
// snip...
// ...snip

func main() {
	flag.Parse()
	addr := fmt.Sprintf("localhost:%d", *port)
	lis, err := net.Listen("tcp", addr)
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	log.Println("Listening on ", addr)
	var opts []grpc.ServerOption
	if *tls {
	// snip...
	// ...snip
	}
	grpcServer := grpc.NewServer(opts...)
	pb.RegisterFhirbufferServer(grpcServer, newServer())
	grpcServer.Serve(lis)
}
      
We're not straying far from the gRPC basics guide. The only difference is the import pb line, and the register call pb.RegisterFhirbufferServer. Even the register function was generated for us. Now let's examine what's behind newServer() in fhirbuffer.go:

package main

import (
	"context"
	"log"

	"github.com/jackc/pgx"
	pb "github.com/patterns/fhirbuffer"
)

var (
	databaseConfig *pgx.ConnConfig = &pgx.ConnConfig{Host: "127.0.0.1", User: "postgres", Password: "postgres", Database: "fhirbase"}
)

type fhirbuffer struct{}

func (s *fhirbuffer) Read(ctx context.Context, req *pb.Search) (*pb.Record, error) {
	conn, err := pgx.Connect(*databaseConfig)
	if err != nil {
		log.Printf("Database connection, %v", err)
		return &pb.Record{}, err
	}
	defer conn.Close()

	qr := conn.QueryRow("SELECT PUBLIC.fhirbase_read( $1 , $2 )", req.Type, req.Id)
	return s.runStmt(ctx, qr)
}

func (s *fhirbuffer) Update(ctx context.Context, req *pb.Change) (*pb.Record, error) {
	conn, err := pgx.Connect(*databaseConfig)
	if err != nil {
		log.Printf("Database connection, %v", err)
		return &pb.Record{}, err
	}
	defer conn.Close()

	qr := conn.QueryRow("SELECT PUBLIC.fhirbase_update( $1 )", req.Resource)
	return s.runStmt(ctx, qr)
}
// snip...
// ...snip

func newServer() *fhirbuffer {
	s := &fhirbuffer{}
	return s
}
      
So newServer() simply returns a struct which we declare. Also, we code the logic of Read() and Update(). Whose sole purpose is execution of the fhirbase_read and fhirbase_update stored procedures, respectively. We went with QueryRow because the expected return result will be one row, and this row result doesn't require that we make a Close() call explicitly. Most of the logic is database connection related, and we relied on the examples (Jack Christensen's PGX) for guidance.

Additional notes, the types like Record, Search, and Change were generated. We aren't automated out of a job though, we still had to code. By fleshing out Read() and Update(), we're implementing the interface. We'll get compiler warnings, if we forget to code something that's required by the generated contract in fhirbuffer.pb.go. We also made a client as shown in the gRPC basics guide, and it is indispensable as a makeshift test harness during each iteration.

fhirshape

So far, so good. We have the foundational persistence layer from the Fhirbase database, then we made fhirbuffer to export the Patient resource. At this point, it's a good time to remember that the FHIR standard is predominantly delivered as a RESTful API.

"Aligning FHIR APIs to the REST architectural style ensure that all transactions are stateless ....." ―2.16.2 FHIR and Architectural Principles, Scalability

Which leads to the question, "Why do we need the fhirbuffer service? why not spool the JSON directly from Fhirbase and/or pair it to the REST service." I hope to go into this in another post. For now, try to suspend your disbelief as we make the trek into Elixir.

In creating our REST service fhirshape, we referred to a few How-To articles that show the --no-html and --no-webpack options. The beacon of light, though, was the walkthrough by Kevin Hoffman. It showed that you could do gRPC from Elixir. On top of that, you'll do it in style with mutual TLS. I won't repeat his great article, except this piece to kick things off: "I used this gRPC library." Sorry that wasn't as dramatic as I thought. It's after clicking into the link.

Here's the snippet courtesy of the library author, Tony Han


$ protoc --elixir_out=plugins=grpc:./lib/ *.proto
      
BOOM. Riiiight? Who's impressed now?

That fhirbuffer.proto file we made for our Go project earlier, it's also going to work here in our Elixir project. With our same Docker container, the steps are:


$ cd test
$ docker run -ti --rm -v $PWD:/app -w /app --entrypoint sh protoc
$ protoc --elixir_out=plugins=grpc:. fhirbuffer.proto
# exit
      
That should produce a new fhirbuffer.pb.ex file. Just like that, you now have your Search, Change, Record types in Elixir.

Placing the new .pb.ex file inside the fhircare_umbrella/apps/fhirshape/lib directory is all that's required to begin. As we saw before, we'll need to write code. The interesting point to keep in mind this round, is that fhirshape consumes the gRPC functions. Let's look at the logic for our Healthcare context to see how fhirshape is a client to fhirbuffer.

Starting with the read call:


defmodule Fhirshape.Healthcare do

// snip...
// ...snip
 
  def get_patient!(id) do
    {:ok, json} = read_resource(id, "Patient")
    %Patient{resource: json}
  end

// snip...
// ...snip

  defp read_resource(id, type) do

    // snip...
    // ...snip

    opts = [cred: cred]
    request = Fhirbuffer.Search.new(id: id, type: type)

    case GRPC.Stub.connect(@fhirbuffer_addr, opts) do
      {:ok, channel} ->
        try do
          {:ok, reply} = Fhirbuffer.Fhirbuffer.Stub.read(channel, request)
          {:ok, reply.resource}
        after
          GRPC.Stub.disconnect(channel)
        end

      _ ->
        {:error, "gRPC connect failed (check TLS credentials)"}
    end
  end
      
The get_patient!() function is invoked by the patient_controller in order to handle a GET request for a Patient resource. We pass-through to read_resource() which seems a waste, but it was in anticipation of negotiating other resources besides Patients. Notice that read_resource() isn't Patient specific, it's resource type agnostic. This is thanks to the Fhirbase fhirbase_read() procedure which itself isn't coupled to one resource type. The Fhirbuffer.Search type, and Fhirbuffer.Fhirbuffer.Stub.read() function were generated by protoc. So after the gRPC connection is established, we can call Fhirbuffer.Fhirbuffer.Stub.read() with the Fhirbuffer.Search filter criteria.

Comparing the update call, the logic flow is very simular:


// snip...
// ...snip

  def update_patient(%Patient{} = patient, attrs) do
    proposed = Poison.decode!(attrs)
    source = Poison.decode!(patient.resource)

    cond do
      Map.has_key?(source, "id") && source["id"] == proposed["id"] ->
        {:ok, savjson} =
          source
          |> Map.merge(proposed)
          |> Poison.encode!()
          |> update_resource()

        {:ok, %Patient{resource: savjson}}

      true ->
        Logger.debug("Patient ID is ambiguous #{inspect(attrs)}")
        {:error, %Ecto.Changeset{}}
    end
  end

// snip...
// ...snip

  defp update_resource(json) do

    // snip...
    // ...snip

    opts = [cred: cred]
    request = Fhirbuffer.Change.new(resource: json)

    case GRPC.Stub.connect(@fhirbuffer_addr, opts) do
      {:ok, channel} ->
        try do
          {:ok, reply} = Fhirbuffer.Fhirbuffer.Stub.update(channel, request)
          {:ok, reply.resource}
        after
          GRPC.Stub.disconnect(channel)
        end

      _ ->
        {:error, "gRPC connect failed (check TLS credentials)"}
    end
  end
      
The real difference is in the preparation of the proposed changes. Before making any update, we verify that the UUID of the proposed changes matches the UUID of the resource record in the database. Then we use the Map.merge() function to combine the two sets so that new fields will be added, and modified fields are updated.

The other tidbits worth mentioning are Insomnia, and CORS. Insomnia is a tool like Postman for testing REST development. CORS was troublesome until others explained things like this guide which uses Corsica plugin.

fhirping

Almost finished! The last puzzle piece is the UI, that's the client to fhirshape's REST API, which we've named fhirping. Without further ado, the app hosted on Glitch. There, the source is available and remixing is easy.

  1. fhirping
    1. Github ( https://github.com/patterns/fhircare_umbrella )
      1. Axios (credits https://vuejs.org/v2/cookbook/using-axios-to-consume-apis.html )
      2. Components (credits https://css-tricks.com/intro-to-vue-2-components-props-slots/ )
      3. Recursion (credits https://stackoverflow.com/a/47312172 )

Final Thoughts

There it is, proof of concept -- FHIR ping  (Glitch hosted)