Integrating with FHIR APIs using Flex
This tutorial shows how to access a FHIR via a Flex Service using the Flex SDK. For this tutorial, we will create a Flex service to connect to http://test.fhir.org/r3/, The HL7 FHIR Foundation's test API server.
Prerequisites
- node.js
v6.9.1
Setup
Create a node.js project with the following npm modules installed:
Defining the service objects
To start, you must define a set of Service Objects. Service Objects are abstractions of data objects in systems of record that can be mapped to Kinvey collections.
In the root of your project, create index.js
. Here, you will first initialize the flex SDK:
const sdk = require('kinvey-flex-sdk');
const handlers = require ('./lib/handlers');
// Initiate the Flex SDK Service
sdk.service((err, flex) => {
The service objects and handlers are defined in the index.js
file. The handlers are defined seperately in lib/handlers
. This separation is done for two purposes:
- to increase modularity and reusability of code
- to facilitate unit testing
We will define three service objects: Patient
, Medication
, and Appointment
.
const data = flex.data; // gets the FlexData object from the service
// Define Service Objects
const patient = data.serviceObject('Patient');
const medication = data.serviceObject('Medication');
const appointment = data.serviceObject('Appointment');
For each Service Object, we will create functions to handle four data events: create
, update
, getById
, and getByQuery
. Each handler will point to a function that will handle that event.
// wire up the events that we want to process
patient.onGetById(handlers.getPatientById);
patient.onGetByQuery(handlers.getPatientByQuery);
patient.onInsert(handlers.createEntity);
patient.onUpdate(handlers.updateEntity);
medication.onGetById(handlers.getMedicationById);
medication.onGetByQuery(handlers.getMedicationByQuery);
medication.onInsert(handlers.createEntity);
medication.onUpdate(handlers.updateEntity);
appointment.onGetById(handlers.getAppointmentById);
appointment.onGetByQuery(handlers.getAppointmentByQuery);
appointment.onInsert(handlers.createEntity);
appointment.onUpdate(handlers.updateEntity);
});
Creating Handler Functions
The handler functions themselves are defined in lib/handlers.js
. These functions each have two parts:
- Make a request to
lib/fhir-client
for processing the FHIR call and converting the response to JSON - Transform the result into valid Kinvey entities in
transformers.js
First, we wire up the handler functions for the Patient
service object. For getById
we will define this function:
function getPatientById(context, complete, modules) {
return _read('Patient', context.entityId, complete, modules);
}
The getPatientById
function calls a common _read
function, passing the Patient
resource type and the entityId
from the context. We implement the _read
function to make a call to fhir-client.js
to retrieve the data and convert it to JSON.
function _read(resource, id, complete, modules) {
fhir.read(resource, id, (err, result) => {
if(err) {
const statusCode = err.statusCode;
delete err.statusCode;
const response = complete().setBody(err);
switch (statusCode) {
case 404:
response.notFound();
break;
case 400:
response.badRequest();
break;
case 401:
response.unauthorized();
break;
case 403:
response.forbidden();
break;
case 405:
response.notAllowed();
break;
default:
response.runtimeError();
}
return response.done();
}
return _sendTransformedResponse(result, complete, modules);
});
}
Similarly, for getByQuery
, create getPatientByQuery
function:
function getPatientByQuery(context, complete, modules) {
return _search('Patient', context.query.query, complete, modules);
}
This function calls a common _search
function:
function _search(resource, query, complete, modules) {
let parsedQuery;
if (query && typeof query === 'string') {
try {
parsedQuery = JSON.parse(query);
} catch (e) {
return complete().setBody(e).runtimeError().done();
}
}
fhir.search(resource, parsedQuery, (err, result) => {
if(err) {
const statusCode = err.statusCode;
delete err.statusCode;
const response = complete().setBody(err);
switch (statusCode) {
case 404:
response.notFound();
break;
case 400:
response.badRequest();
break;
case 401:
response.unauthorized();
break;
case 403:
response.forbidden();
break;
case 405:
response.notAllowed();
break;
default:
response.runtimeError();
}
return response.done();
}
return transformers.transformSearchResultArray(result.entry || [], modules, (err, transformedResult) => {
if (err) {
return complete().setBody(err).runtimeError().done();
}
return complete().setBody(transformedResult).ok().next();
});
});
}
Next, we need to create handlers for create and update. Since these operations rely on a body parameter to determine the type, we can create one handler and function to handle all service objects:
function createEntity(context, complete, modules) {
const transformedRequestBody = transformers.transformWrite(context.body);
if (transformedRequestBody instanceof Error) {
return complete().setBody(transformedRequestBody).runtimeError().done();
}
return _create(transformedRequestBody, complete, modules);
}
function updateEntity(context, complete, modules) {
const transformedRequestBody = transformers.transformWrite(context.body);
if (transformedRequestBody instanceof Error) {
return complete().setBody(transformedRequestBody).runtimeError().done();
}
return _update(transformedRequestBody, complete, modules);
}
function _create(body, complete, modules) {
fhir.create(body, (err, result) => {
if (err) {
return complete().setBody(err).runtimeError().done();
}
return _sendTransformedResponse(result, complete, modules);
});
}
function _update(body, complete, modules) {
fhir.update(body, (err, result) => {
if (err) {
return complete().setBody(err).runtimeError().done();
}
return _sendTransformedResponse(result, complete, modules);
});
}
Finally, export your handler functions:
exports.getPatientById = getPatientById;
exports.getPatientByQuery = getPatientByQuery;
exports.createEntity = createEntity;
exports.updateEntity = updateEntity;
The remaining service object handlers can be created in the same way.
Client
Create a new file lib/fhir-client.js
. This file will be used to access the FHIR web service.
For the purpose of this tutorial, we will hardcode the FHIR service's URL:
const Client = require('fhir-json-client');
const client = new Client('http://test.fhir.org/r3/');
We create a simple function to handle responses from fhir clients:
function _parseResponse(err, response, item, callback) {
if (err != null) {
return callback(err);
} else if (response != null && response.statusCode > 299) {
return callback(response)
} else {
return callback(err, item);
}
}
Next, we create lightweight wrapper functions for each of the fhir-client methods we are implementing, and export them:
exports.read = function read(resource, item, callback) {
client.read(resource, item, (err, response, item) => {
_parseResponse(err, response, item, callback);
});
};
exports.search = (resource, query, callback) => {
client.search(resource, query, (err, response, item) => {
_parseResponse(err, response, item, callback);
});
};
exports.create = (entity, callback) => {
client.create(entity, (err, response, item) => {
_parseResponse(err, response, item, callback);
});
};
exports.update = (entity, callback) => {
client.update(entity, (err, response, item) => {
_parseResponse(err, response, item, callback);
});
};
The only remaining task is to ensure that the entities are properly transformed.
Transformations
Create file lib/transformers.js
. This file will be used by your handler to translate data between Kinvey and the FHIR service. First, we need to transform any entities that are returned from the FHIR service into Kinvey-style entities. We do this by implementing a function transformEntity
:
function transformEntity(entity, modules) {
if (typeof entity !== 'object') {
return new Error('The entity must be an object.');
}
if (Array.isArray(entity)) {
return new Error('Arrays are not permitted. Only a single entity may be supplied.');
}
if (entity.id == null) {
return new Error('No id field (_id) present in the resulting entity');
}
if (modules == null || modules.kinveyEntity == null || modules.kinveyEntity.entity == null) {
return new Error('A valid modules object must be supplied');
}
const mappedEntity = modules.kinveyEntity.entity(entity);
mappedEntity._id = entity.id;
return mappedEntity;
}
This function checks that the entity to be transformed is valid, and then uses the Flex-SDK's kinveyEntity module to convert the entity into a Kinvey-style entity by remapping FHIR's id
to Kinvey's _id
and adding in _acl
and _kmd
metadata.
Next, we will need a transformation function to convert Kinvey entities into FHIR style entities (note this is still done in JSON, the fhir-json-client
will later convert to FHIR's native format.)
We create transformWrite
:
function transformWrite(entity) {
if (typeof entity !== 'object') {
return new Error('The entity must be an object.');
}
if (Array.isArray(entity)) {
return new Error('Arrays are not permitted. Only a single entity may be supplied.');
}
if (modules == null || modules.kinveyEntity == null || modules.kinveyEntity.entity == null) {
return new Error('A valid modules object must be supplied');
}
if (entity._id != null) {
entity.id = entity._id;
}
delete entity._id;
delete entity._kmd;
delete entity._acl;
return entity;
}
Finally, we create a function to handle the result of FHIR searches. FHIR searches return the result inside a key called entry
within the result body. To convert the result to Kinvey's format, we must remove the context root entry
and just return an array of Kinvey entities:
function transformSearchResultArray(array, modules, callback) {
if (Array.isArray(array) === false) {
if (typeof array === 'object') {
array = [array];
} else {
return setImmediate(() => callback(new Error('Item transformed must be an array or object')));
}
}
if (typeof modules === 'function'
|| modules == null
|| modules.kinveyEntity == null
|| modules.kinveyEntity.entity == null) {
if (typeof modules === 'function' && callback == null) {
callback = modules;
}
if (callback !== null) {
return setImmediate(() => callback(new Error('A valid modules object must be supplied')));
}
}
if (callback == null || typeof callback !== 'function') {
throw new Error('A callback function must be supplied');
}
return async.map(array, (item, cb) => {
return setImmediate(() => cb(null, transformEntity(item.resource)));
}, (err, transformedResult) => {
if (err) {
return callback(new Error(err));
}
return callback(null, transformedResult || []);
});
}
The above function creates a map of the result set and invokes transformEntity
for each entity in the result set.
After exporting the transformation functions, your service should be up and running. The above can be repeated for the `Medication` and `Appointment` service objects, or any other service objects.:
```javascript
exports.transformSearchResultArray = transformSearchResultArray;
exports.transformEntity = transformEntity;
exports.transformWrite = transformWrite;
You can find the full source code for this tutorial at https://github.com/Kinvey/fhir-flex-reference