FHIRProof
FHIRProof is an ODM for mongo that provides object document mapper to FHIR with all R4 resources and validation by default. It's meant to be heavily extensinble. It provides you the following mongo specific operations:
- Model.create (validates by default)
- Model.find
- Model.findOne
- Model.findById
- Model.findByIdAndUpdate (validates by default)
- Model.delete
- Model.validate
- Model.history
- Model.historyByVersionId
- instance.save (validates by default)
- instance.toJSON (converts to fhir compliant JSON, removes invalid fhir expression)
- instance.history
- instance.historyByVersionId
It also exposes the collection on the constructor and object, so you can do anything via mongo you would normally do.
Installation
From your terminal, run
npm install --save @icanbwell/fhirproof
Examples
Connecting to MongoDB
Pass in your connection string and any options you would normally pass to Mongodb.
const { mongoconnect } = require('@icanbwell/fhirproof');
await mongoconnect(
('mongodb://localhost/my_database',
{
useNewUrlParser: true,
useUnifiedTopology: true,
})
);
Define a Model
You will want to inherit from the BaseFHIRModel
for all FHIR Resources. Below is an example of creating an Organization resource. This format is required. You can do more, but this is the minimum.
const { BaseFHIRModel, CONSTANTS } = require('@icanbwell/fhirproof');
const resourceName = 'Organization';
const collectionName = CONSTANTS.COLLECTION.ORGANIZATION;
class Organization extends BaseFHIRModel {
constructor(resource, _id) {
super(collectionName, collectionName);
this.resource = new this.Resource(resource);
this._id = _id;
}
}
Organization.resourceName = resourceName;
Organization.collectionName = collectionName;
module.exports = Organization;
And using it in a router.
const express = require('express');
const Organization = require('./organization.model');
const router = express.Router();
router
.get('/Organization/:id', async (req, res, next) => {
const organization = await Organization.initialize().findById(req.param.id);
res.status(200).json(organization.toJSON());
})
.get('/Organization/:id/history', async (req, res, next) => {
const organizationHistory = await Organization.initialize().history(
req.param.id
);
res.status(200).json(organizationHistory);
})
.post('/Organization', async (req, res, next) => {
try {
const organization = await Organization.initialize().create(req.body);
res.status(201).json(organization.toJSON());
} catch (error) {
if (error.statusCode && error.operationOutcome) {
return res.status(error.statusCode).json(error.operationOutcome);
}
next(error);
}
});
module.exports = router;
Extending Models
Of course, everyone needs to add more capabilities to support their use case. Below are two examples. The first is adding a completely new method. The second is adding more functionality ontop of an existing method.
const { BaseFHIRModel, CONSTANTS } = require('@icanbwell/fhirproof');
const resourceName = 'Organization';
const collectionName = CONSTANTS.COLLECTION.ORGANIZATION;
class Organization extends BaseFHIRModel {
constructor(resource, _id) {
super(collectionName, collectionName);
this.resource = new this.Resource(resource);
this._id = _id;
}
static async search(queries, options) {
const cursor = await this.collection.find(queries, options);
return cursor.toArray();
}
addExtension({ valueUrl, value }) {
this.resource.extension.push({ valueUrl: value });
return this.save();
}
static async create(payload) {
if (!payload.name) {
throw new Error('organization.name is required');
}
return await super.create(payload);
}
}
Organization.resourceName = resourceName;
Organization.collectionName = collectionName;
Errors
We try to handle errors as http status code errors. When certain situations arise, we return these errors extending from the base Error object. See ./src/http-errors
for more information.
For bad request (status code 400), we have the BadRequestError with statusCode
and operationOutcome
property. For not found, (status code 404), we have the NotFoundError with statusCode
and operationOutcome
.
Playing in node console
node -r dotenv/config
> const { mongoconnect, BaseFHIRModel } = require('@icanbwell/fhirproof')
undefined
> mongoconnect('mongodb://localhost:27017')
Promise { <pending> }
> const Organization = require('./example/organization/organization.model')
undefined
> Organization
[class Organization extends BaseFHIRModel] {
resourceName: 'Organization',
collectionName: 'Organization'
}
Organization.initialize().create({resourceType: 'Organizatin', name: "This is a test"}).then(data => org = data).catch(err => error = err)
Promise { <pending> }
> org
Uncaught ReferenceError: org is not defined
> error
BadRequestError: Invalid resourceType 'Organizatin'
at /Users/nathanhall/bwell/marketplace-vendor/marketplace-vendor-onboarding/src/server/fhir/base-fhir.model.js:54:23
at new Promise (<anonymous>)
at Function.create (/Users/nathanhall/bwell/marketplace-vendor/marketplace-vendor-onboarding/src/server/fhir/base-fhir.model.js:51:12)
at REPL48:1:27
at Script.runInThisContext (vm.js:133:18)
at REPLServer.defaultEval (repl.js:484:29)
at bound (domain.js:413:15)
at REPLServer.runBound [as eval] (domain.js:424:12)
at REPLServer.onLine (repl.js:817:10)
at REPLServer.emit (events.js:327:22) {
operationOutcome: { resourceType: 'OperationOutcome', issue: [ [Object] ] },
statusCode: 400
}
> Organization.initialize().create({resourceType: 'Organization', name: "This is a test"}).then(data => org = data).catch(err => error = err)
Promise { <pending> }
org.toJSON()
{
resourceType: 'Organization',
id: '606c91947c00311223dfb08b',
meta: { versionId: '1', lastUpdated: '2021-04-06T16:51:32+00:00' },
name: 'This is a test'
}
> org.resource.name
'This is a test'
> org.resource.name = 'Update me'
'Update me'
> org.save()
Promise { <pending> }
> org.name
undefined
> org.resource.name
'Update me'
> org._id
606c91947c00311223dfb08b
> org.history().then(data => history = data)
Promise { <pending> }
> history
[
{
_id: 606c91947c00311223dfb08c,
resourceType: 'Organization',
meta: { versionId: '1', lastUpdated: '2021-04-06T16:51:32+00:00' },
name: 'This is a test',
id: 606c91947c00311223dfb08b
},
{
_id: 606c91fe7c00311223dfb08d,
resourceType: 'Organization',
id: 606c91947c00311223dfb08b,
meta: { versionId: '2', lastUpdated: '2021-04-06T16:53:18+00:00' },
name: 'Update me'
}
]
Adding Indexing
You'll probably want to add indexes to things you search for frequently. We tried to make it as easy as possible. Using mongoconnect
, when successful, fires an event call 'mongoconnect'. Listening to this event with fhirProofEvent
you can use our class method to create indexes, or operate on the collection directly.
const { fhirProofEvent } = require('@icanbwell/fhirproof');
fhirProofEvent.on('mongoconnect', async () => {
try {
await Organization.initialize().createIndex(
{ name: 1 },
{
unique: true,
sparse: true,
}
);
} catch (error) {
console.error(`[OrganizationModel] - Failed to create index ${error}`);
throw error;
}
});
Roadmap
- Support more FHIR Versions. Currently the default only supports R4 FHIR schemas
- Support more node versions. Will want to add in some transpiler to support back to node 8
- more!?
Contributing
The primary purpose of FHIRProof is to build a great data model for FHIR, mongo, and nodejs... making it faster and easier to use. To do this, we have FHIRProof on github for everyone to make it better.
Shout out to @asymmetrik/node-fhir-server-core and @asymmetrik/fhir-json-schema-validator for building the pieces to put this together.
Why Not Mongoose?
Because I couldn't find all the generated mongoose schemas in FHIR, I like JSON Schemas, and using ObjectRef
is tricky with how FHIR does relations.
License
FHIRProof is MIT licensed.