Camo
A class-based ES6 ODM for Mongo-like databases.
Install / Use
/learn @scottwrobinson/CamoREADME
Camo
Supporters
<a href="https://pingbot.dev?ref=github-camo"> PingBot.dev <img alt="Monitoring for your servers, vendors, and infrastructure." src="https://s3.pingbot.dev/images/landing-header.png" /> </a>Jump To
- <a href="#why-do-we-need-another-odm">Why do we need another ODM?</a>
- <a href="#advantages">Advantages</a>
- <a href="#install-and-run">Install and Run</a>
- <a href="#quick-start">Quick Start</a>
- <a href="#connect-to-the-database">Connect to the Database</a>
- <a href="#declaring-your-document">Declaring Your Document</a>
- <a href="#embedded-documents">Embedded Documents</a>
- <a href="#creating-and-saving">Creating and Saving</a>
- <a href="#loading">Loading</a>
- <a href="#deleting">Deleting</a>
- <a href="#counting">Counting</a>
- <a href="#hooks">Hooks</a>
- <a href="#misc">Misc.</a>
- <a href="#transpiler-support">Transpiler Support</a>
- <a href="#contributing">Contributing</a>
- <a href="#contact">Contact</a>
- <a href="#copyright-license">Copyright & License</a>
Why do we need another ODM?
Short answer, we probably don't. Camo was created for two reasons: to bring traditional-style classes to MongoDB JavaScript, and to support NeDB as a backend (which is much like the SQLite-alternative to Mongo).
Throughout development this eventually turned in to a library full of ES6 features. Coming from a Java background, its easier for me to design and write code in terms of classes, and I suspect this is true for many JavaScript beginners. While ES6 classes don't bring any new functionality to the language, they certainly do make it much easier to jump in to OOP with JavaScript, which is reason enough to warrent a new library, IMO.
Advantages
So, why use Camo?
- ES6: ES6 features are quickly being added to Node, especially now that it has merged with io.js. With all of these new features being released, Camo is getting a head start in writing tested and proven ES6 code. This also means that native Promises are built-in to Camo, so no more
promisify-ing your ODM or waiting for Promise support to be added natively. - Easy to use: While JavaScript is a great language overall, it isn't always the easiest for beginners to pick up. Camo aims to ease that transition by providing familiar-looking classes and a simple interface. Also, there is no need to install a full MongoDB instance to get started thanks to the support of NeDB.
- Multiple backends: Camo was designed and built with multiple Mongo-like backends in mind, like NeDB, LokiJS*, and TaffyDB*. With NeDB support, for example, you don't need to install a full MongoDB instance for development or for smaller projects. This also allows you to use Camo in the browser, since databases like NeDB supports in-memory storage.
- Lightweight: Camo is just a very thin wrapper around the backend databases, which mean you won't be sacrificing performance.
* Support coming soon.
Install and Run
To use Camo, you must first have installed Node >2.0.x, then run the following commands:
npm install camo --save
And at least ONE of the following:
npm install nedb --save
OR
npm install mongodb --save
Quick Start
Camo was built with ease-of-use and ES6 in mind, so you might notice it has more of an OOP feel to it than many existing libraries and ODMs. Don't worry, focusing on object-oriented design doesn't mean we forgot about functional techniques or asynchronous programming. Promises are built-in to the API. Just about every call you make interacting with the database (find, save, delete, etc) will return a Promise. No more callback hell :)
For a short tutorial on using Camo, check out this article.
Connect to the Database
Before using any document methods, you must first connect to your underlying database. All supported databases have their own unique URI string used for connecting. The URI string usually describes the network location or file location of the database. However, some databases support more than just network or file locations. NeDB, for example, supports storing data in-memory, which can be specified to Camo via nedb://memory. See below for details:
- MongoDB:
- Format: mongodb://[username:password@]host[:port][/db-name]
- Example:
var uri = 'mongodb://scott:abc123@localhost:27017/animals';
- NeDB:
- Format: nedb://[directory-path] OR nedb://memory
- Example:
var uri = 'nedb:///Users/scott/data/animals';
So to connect to an NeDB database, use the following:
var connect = require('camo').connect;
var database;
var uri = 'nedb:///Users/scott/data/animals';
connect(uri).then(function(db) {
database = db;
});
Declaring Your Document
All models must inherit from the Document class, which handles much of the interface to your backend NoSQL database.
var Document = require('camo').Document;
class Company extends Document {
constructor() {
super();
this.name = String;
this.valuation = {
type: Number,
default: 10000000000,
min: 0
};
this.employees = [String];
this.dateFounded = {
type: Date,
default: Date.now
};
}
static collectionName() {
return 'companies';
}
}
Notice how the schema is declared right in the constructor as member variables. All public member variables (variables that don't start with an underscore [_]) are added to the schema.
The name of the collection can be set by overriding the static collectionName() method, which should return the desired collection name as a string. If one isn't given, then Camo uses the name of the class and naively appends an 's' to the end to make it plural.
Schemas can also be defined using the this.schema() method. For example, in the constructor() method you could use:
this.schema({
name: String,
valuation: {
type: Number,
default: 10000000000,
min: 0
},
employees: [String],
dateFounded: {
type: Date,
default: Date.now
}
});
Currently supported variable types are:
StringNumberBooleanBufferDateObjectArrayEmbeddedDocument- Document Reference
Arrays can either be declared as either un-typed (using Array or []), or typed (using the [TYPE] syntax, like [String]). Typed arrays are enforced by Camo on .save() and an Error will be thrown if a value of the wrong type is saved in the array. Arrays of references are also supported.
To declare a member variable in the schema, either directly assign it one of the types listed above, or assign it an object with options, like this:
this.primeNumber = {
type: Number,
default: 2,
min: 0,
max: 25,
choices: [2, 3, 5, 7, 11, 13, 17, 19, 23],
unique: true
}
The default option supports both values and no-argument functions (like Date.now). Currently the supported options/validators are:
type: The value's type (required)default: The value to be assigned if none is provided (optional)min: The minimum value a Number can be (optional)max: The maximum value a Number can be (optional)choices: A list of possible values (optional)match: A regex string that should match the value (optional)validate: A 1-argument function that returnsfalseif the value is invalid (optional)unique: A boolean value indicating if a 'unique' index should be set (optional)required: A boolean value indicating if a key value is required (optional)
To reference another document, just use its class name as the type.
class Dog extends Document {
constructor() {
super();
this.name = String;
this.breed = String;
}
}
class Person extends Document {
constructor() {
super();
this.pet = Dog;
this.name = String;
this.age = String;
}
static collectionName() {
return 'people';
}
}
Embedded Documents
Embedded documents can also be used within Documents. You must declare them separately from the main Document that it is being used in. EmbeddedDocuments are good for when you need an Object, but also need enforced schemas, validation, defaults, hooks, and member functions. All of the options (type, default, min, etc) mentioned above work on EmbeddedDocuments as well.
var Document = require('camo').Document;
var EmbeddedDocument = require('camo').EmbeddedDocument;
class Money extends EmbeddedDocument {
constructor() {
super();
this.value = {
type: Number,
choices: [1, 5, 10, 20, 50, 100]
};
this.currency = {
type: String,
default: 'usd'
}
}
}
class Wallet extends Document {
constructor() {
super();
this.contents = [Money];
}
}
var wallet = Wallet.create();
wallet.contents.push(Money.create());
wallet.contents[0].value = 5;
wallet.contents.push(Money.create());
wallet.contents[1].value = 100;
wallet.save().then(function() {
console.log('Both Wallet and Money objects were saved!');
});
Creating and Saving
To create a new instance of our document, we need to use the .create() method, which handles all of the construction for us.
var lassie = Dog.create({
name: 'Lassie',
breed: 'Collie'
});
lassie.save().then(function(l) {
console.log(l._id);
});
Once a document is saved, it will automatically be assigned a unique identifier by the backend database. This ID can be accessed by the ._id property.
If you specified a default value (or function) for a schema variable, that value will be assi
