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)
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
- Snapd / Snappy (Already done)
- sudo snap install postgresql10
- 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
- pg.sh (Gist)
- ./pg.sh fhirbase
- select id, resource->'name', resource->'gender' from patient where id = 'd3af67c9-0c02-45f2-bc91-fea45af3ee83';
- Alias pg commands
- sudo snap alias postgresql10.pgctl pg_ctl
- 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.
- fhirping
- Github ( https://github.com/patterns/fhircare_umbrella )
- Axios (credits https://vuejs.org/v2/cookbook/using-axios-to-consume-apis.html )
- Components (credits https://css-tricks.com/intro-to-vue-2-components-props-slots/ )
- Recursion (credits https://stackoverflow.com/a/47312172 )
- Github ( https://github.com/patterns/fhircare_umbrella )
Final Thoughts
There it is, proof of concept -- FHIR ping (Glitch hosted)