Introducing Camo: A class-based ES6 ODM for Mongo-like databases

Edit: Updated Camo code to v0.12.1

What is Camo?

Camo is an ES6 ODM with class-based models. A few of its main features are: dead-simple schema declaration, intuitive schema inheritance, and support for multiple database backends.

A simple Camo model might look like this:

var Document = require('camo').Document;

class Car extends Document {  
    constructor() {
        super();

        this.make = String;
        this.miles = Number;
        this.numWheels = {
            type: Number;
            default: 4
        };
    }
}

To install, just use:

npm install camo --save

AND

npm install nedb --save  
OR  
npm install mongodb --save  

Why another ODM?

As much as I wanted to like Mongoose, like everyone else seemed to, I just couldn't get myself to embrace it. It may have been because I was still new to JavaScript, and I hadn't quite embraced the functional programming aspect as much as I should have, but I was left disappointed with how models were declared and the lack of schema inheritance (or at least it's clunkiness).

Coming from a Java background, I'm very fond of classes. So designing models without easily being able to use classes or inheritance was difficult for me. Seeing as ES6 has traditional class and inheritance support, I was surprised to see no Node.js ODMs were class-based.

Lastly, I liked how in SQL you could easily switch between small, portable databases like SQLite for development and larger, more scalable databases like PostgreSQL for production. Seeing as NeDB nicely fills that gap for MongoDB, I wanted an ODM that allowed me to easily switch between these databases.

Features

Classes

As mentioned above, Camo revolves around ES6 classes for creating models. Each model should either extend the Document class, the EmbeddedDocument class, or another model. Schemas are declared in the constructors, where you can specify the data type, defaults, choices, and other validators.

Virtuals, methods, and statics can be used to manipulate and retrieve the model's data, just like a normal, non-persisted class.

var Document = require('camo').Document;

class Manufacturer extends Document {  
    constructor() {
        super();

        this.name = String;
    }
}

class Car extends Document {  
    constructor() {
        super();

        this.make = Manufacturer;
        this.model = String;
        this.year = {
            type: Number,
            min: 1900,
            max: 2015
        };
        this.miles = {
            type: Number,
            min: 0
        };
        this.numWheels = {
            type: Number;
            default: 4
        };
    }

    get isUnderWarranty() {
        return this.miles < 50000;
    }

    milesPerYear() {
        return this.miles / (new Date().getFullYear() - this.year)
    };
}

Embedded documents

One of the main advantages to using MongoDB is its nested data structure, and you should be able to easily take advantage of that in your ODM. Camo has built-in embedded document support, so you can treat nested document data just like you would a normal document.

var Document = require('camo').Document;  
var EmbeddedDocument = require('camo').EmbeddedDocument;

class Warranty extends EmbeddedDocument {  
    constructor() {
        super();

        this.miles = Number;
        this.years = Number;
        this.isUnlimitedMiles = Boolean;
    }

    isCovered(car) {
        var thisYear = new Date().getFullYear();
        return ((car.miles <= this.miles) || this.isUnlimitedMiles) &&
            ((thisYear - car.year) <= this.years)
    };
}

class Manufacturer extends Document {  
    constructor() {
        super();

        this.name = String;

        this.basicWarranty = Warranty;
        this.powertrainWarranty = Warranty;
        this.corrosionWarranty = Warranty;
    }
}

Multi-database support

Not every project requires a huge database with replication, load balancing, and support for millions of reads/writes per second, which is exactly why it was important for Camo to support multiple database backends, like NeDB.

Most projects start out small and eventually grow in to larger, more popular products that require faster and more robust databases. With NeDB, you get a subset of MongoDB's most commonly used API commands without the need to set up the full database. Its like having the equivalent of SQLite, but for Mongo. To switch between the databases, just provide a different connection string.

var connect = require('camo').connect;

var uri;  
var neUri = 'nedb://memory';  
var mongoUri = 'mongodb://localhost/car-app';

uri = neUri;  
if (process.env.NODE_ENV === 'production') {  
    uri = mongoUri;
}

connect(uri).then(function(db) {  
    // Ready to use Camo!
});

As of v0.5.5, Camo supports MongoDB and NeDB. We're planning to add support for more Mongo-like databases, including LokiJS, and TaffyDB. With NeDB, and the additions of LokiJS and TaffyDB, you can use Camo in the browser as well.

Inheritance

Arguably one of the best features of Camo is the schema inheritance. Simply use the ES6 class inheritance to extend a schema.

var Document = require('camo').Document;

class Vehicle extends Document {  
    constructor() {
        super();

        this.make = String;
        this.model = String;
        this.year = Number;
        this.miles = Number;
    }
}

class Car extends Vehicle {  
    constructor() {
        super();

        this.numberOfDoors = String;
    }
}

The subclasses can override or extend any schemas, virtuals, methods, or statics, which results in more readable, and simpler, code.

Is Camo production-ready?

While Camo already has quite a few features and only a few known bugs, it is still very much a work in progress. Here is a short list of the main features we still want to add to the project before declaring v1.0:

  • Return a Query object from findOne/find/delete/etc
  • Add skip/limit support to queries
  • Add an option to only populate specified references
  • Add support for LokiJS and TaffyDB

That being said, Camo is already being used in production code over at Polymetrics (a SaaS that provides metrics for Stripe). The code-base for Polymetrics was originally built on Mongoose, but was then replaced with Camo without any problems. Testing and development is much easier now that we can easily switch between databases.

Camo made designing the models for Polymetrics much easier as well. Much of the data we need to download and save from Stripe has the same fields (like dates, meta-data, etc), so extending a common schema allowed us to write less code as well as enabling us to only need to change the base schema instead of having to make the same change to many files.

Conclusion

Head on over to the npm or Github pages and check out the project. The README is currently the best source for documentation, so if something is missing please let me know.

As always, any suggestions, questions, feedback, or pull requests are welcome! Feel free to contact me with via Twitter, Github, or email.