@icanbwell/fhirproof

FHIR Mongo object document mapper with built in validation

Usage no npm install needed!

<script type="module">
  import icanbwellFhirproof from 'https://cdn.skypack.dev/@icanbwell/fhirproof';
</script>

README

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.

// organization.model.js
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.

//organization.route.js
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);
      //try catch logic to handle 400s if desired.  Create validates by default
      res.status(201).json(organization.toJSON());
    } catch (error) {
      if (error.statusCode && error.operationOutcome) {
        // we handle status 400, 404 as extended error objects
        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.

// organization.model.js from above
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;
  }

  //new search method
  static async search(queries, options) {
    // NOTICE: you have access to mongodb collection if you find yourself wanting to get closer to the metal
    // console.log(this.collection)
    // console.log(this.historyCollection)
    // console.log(this.db)
    // console.log(this.validator)
    const cursor = await this.collection.find(queries, options);
    return cursor.toArray();
  }

  //new instance method
  addExtension({ valueUrl, value }) {
    this.resource.extension.push({ valueUrl: value });
    return this.save();
  }

  //override create method to make sure organization.name is present
  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

# load configuration files to connect to mongo
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.

//organization.model.js
const { fhirProofEvent } = require('@icanbwell/fhirproof');

// ...Other lines from  above
fhirProofEvent.on('mongoconnect', async () => {
  try {
    await Organization.initialize().createIndex(
      { name: 1 },
      {
        unique: true,
        sparse: true,
      }
    );

    // collection directly
    // await Organization.collection.createIndex(...args)
  } catch (error) {
    console.error(`[OrganizationModel] - Failed to create index ${error}`);
    throw error;
  }
});

Roadmap

  1. Support more FHIR Versions. Currently the default only supports R4 FHIR schemas
  2. Support more node versions. Will want to add in some transpiler to support back to node 8
  3. 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.