パッケージの詳細

camo

scottwrobinson17.5kMIT0.12.5

A class-based ES6 ODM for Mongo-like databases.

es6, odm, mongodb, nedb

readme

Camo

Supporters

PingBot.dev Monitoring for your servers, vendors, and infrastructure.

Jump To

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:

  • String
  • Number
  • Boolean
  • Buffer
  • Date
  • Object
  • Array
  • EmbeddedDocument
  • 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 returns false if 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 assigned on creation of the object.

An alternative to .save() is .findOneAndUpdate(query, update, options). This static method will find and update (or insert) a document in one atomic operation (atomicity is guaranteed in MongoDB only). Using the {upsert: true} option will return a new document if one is not found with the given query.

Loading

Both the find and delete methods following closely (but not always exactly) to the MongoDB API, so it should feel fairly familiar.

If querying an object by id, you must use _id and not id.

To retrieve an object, you have a few methods available to you.

  • .findOne(query, options) (static method)
  • .find(query, options) (static method)

The .findOne() method will return the first document found, even if multiple documents match the query. .find() will return all documents matching the query. Each should be called as static methods on the document type you want to load.

Dog.findOne({ name: 'Lassie' }).then(function(l) {
    console.log('Got Lassie!');
    console.log('Her unique ID is', l._id);
});

.findOne() currently accepts the following option:

  • populate: Boolean value to load all or no references. Pass an array of field names to only populate the specified references
    • Person.findOne({name: 'Billy'}, {populate: true}) populates all references in Person object
    • Person.findOne({name: 'Billy'}, {populate: ['address', 'spouse']}) populates only 'address' and 'spouse' in Person object

.find() currently accepts the following options:

  • populate: Boolean value to load all or no references. Pass an array of field names to only populate the specified references
    • Person.find({lastName: 'Smith'}, {populate: true}) populates all references in Person object
    • Person.find({lastName: 'Smith'}, {populate: ['address', 'spouse']}) populates only 'address' and 'spouse' in Person object
  • sort: Sort the documents by the given field(s)
    • Person.find({}, {sort: '-age'}) sorts by age in descending order
    • Person.find({}, {sort: ['age', 'name']}) sorts by ascending age and then name, alphabetically
  • limit: Limits the number of documents returned
    • Person.find({}, {limit: 5}) returns a maximum of 5 Person objects
  • skip: Skips the given number of documents and returns the rest
    • Person.find({}, {skip: 5}) skips the first 5 Person objects and returns all others

Deleting

To remove documents from the database, use one of the following:

  • .delete()
  • .deleteOne(query, options) (static method)
  • .deleteMany(query, options) (static method)
  • .findOneAndDelete(query, options) (static method)

The .delete() method should only be used on an instantiated document with a valid id. The other three methods should be used on the class of the document(s) you want to delete.

Dog.deleteMany({ breed: 'Collie' }).then(function(numDeleted) {
    console.log('Deleted', numDeleted, 'Collies from the database.');
});

Counting

To get the number of matching documents for a query without actually retrieving all of the data, use the .count() method.

Dog.count({ breed: 'Collie' }).then(function(count) {
    console.log('Found', count, 'Collies.');
});

Hooks

Camo provides hooks for you to execute code before and after critical parts of your database interactions. For each hook you use, you may return a value (which, as of now, will be discarded) or a Promise for executing asynchronous code. Using Promises throughout Camo allows us to not have to provide separate async and sync hooks, thus making your code simpler and easier to understand.

Hooks can be used not only on Document objects, but EmbeddedDocument objects as well. The embedded object's hooks will be called when it's parent Document is saved/validated/deleted (depending on the hook you provide).

In order to create a hook, you must override a class method. The hooks currently provided, and their corresponding methods, are:

  • pre-validate: preValidate()
  • post-validate: postValidate()
  • pre-save: preSave()
  • post-save: postSave()
  • pre-delete: preDelete()
  • post-delete: postDelete()

Here is an example of using a hook (pre-delete, in this case):

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

        this.employees = [Person]
    }

    static collectionName() {
        return 'companies';
    }

    preDelete() {
        var deletes = [];
        this.employees.forEach(function(e) {
            var p = new Promise(function(resolve, reject) {
                resolve(e.delete());
            });

            deletes.push(p);
        });

        return Promise.all(deletes);
    }
}

The code above shows a pre-delete hook that deletes all the employees of the company before it itself is deleted. As you can see, this is much more convenient than needing to always remember to delete referenced employees in the application code.

Note: The .preDelete() and .postDelete() hooks are only called when calling .delete() on a Document instance. Calling .deleteOne() or .deleteMany() will not trigger the hook methods.

Misc.

  • camo.getClient(): Retrieves the Camo database client
  • camo.getClient().driver(): Retrieves the underlying database driver (MongoClient or a map of NeDB collections)
  • Document.toJSON(): Serializes the given document to just the data, which includes nested and referenced data

Transpiler Support

While many transpilers won't have any problem with Camo, some need extra resources/plugins to work correctly:

Contributing

Feel free to open new issues or submit pull requests for Camo. If you'd like to contact me before doing so, feel free to get in touch (see Contact section below).

Before opening an issue or submitting a PR, I ask that you follow these guidelines:

Issues

  • Please state whether your issue is a question, feature request, or bug report.
  • Always try the latest version of Camo before opening an issue.
  • If the issue is a bug, be sure to clearly state your problem, what you expected to happen, and what all you have tried to resolve it.
  • Always try to post simplified code that shows the problem. Use Gists for longer examples.

Pull Requests

  • If your PR is a new feature, please consult with me first.
  • Any PR should contain only one feature or bug fix. If you have more than one, please submit them as separate PRs.
  • Always try to include relevant tests with your PRs. If you aren't sure where a test should go or how to create one, feel free to ask.
  • Include updates to the README when needed.
  • Do not update the package version or CHANGELOG. I'll handle that for each release.

Contact

You can contact me with questions, issues, or ideas at either of the following:

For short questions and faster responses, try Twitter.

Copyright & License

Copyright (c) 2021 Scott Robinson

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

更新履歴

0.12.3 (2016-11-04)

Bugfixes:

  • Merged PR that allows changes to persist in postValidate and preSave hooks (#85). Fixes #43.

0.12.2 (2016-06-27)

Bugfixes:

  • Merged PR that prevents methods from being serialized in toJSON() (#79)

0.12.1 (2016-03-02)

Features:

  • Updated README to warn about frequently changing API
  • Added Contributing and Contact sections to README Bugfixes:
  • Fixed issue that prevented save() from being aborted when Promise.reject was returned in a hook (#57)
  • Replaced all .id references to ._id in README
  • Fixed issue where schema types Array and [] were not properly being validated (#53)
  • In README, changed an incorrect reference of .findMany() to .find() (#54)
  • Fixed issue where updated nested data wasn't returned when using .findOneAndUpdate() with NeDB (#55)

0.12.0 (2016-02-24)

Features:

  • Added support for sorting with multiple keys (#22)
  • Added support for accepting strings as Dates (#46)
  • Deprecated loadMany, loadOne, loadOneAndUpdate, and loadOneAndDelete in favor of find, findOne, findOneAndUpdate, and findOneAndDelete, respectively (#37)
  • Added documentation on transpiler support for Camo Bugfixes:
  • Changed 'schema({})' to 'this.schema({})' in README since original throws ReferenceError (#52)

0.11.4 (2016-01-19)

Bugfixes:

  • Fixed issue with saving/loading deeply nested embedded documents (#35)
  • Removed _id and _schema._id from EmbeddedDocument class (#31)
  • Allow user to specify a custom _id (#29)

0.11.3 (2015-12-29)

Features:

  • Added new ValidationError object Bugfixes:
  • Improved some validation tests
  • Fixed min validation
  • Fixed validation for array of embedded documents
  • Moved collectionName method to BaseDocument so EmbeddedDocument can use it (#26)
  • Deprecated id alias from document object (#20)
  • Fixed serialization test for MongoDB IDs

0.11.2 (2015-12-15)

Bugfixes:

  • Fixed issue with running 'canonicalize' tests on travis-ci

0.11.1 (2015-12-15)

Bugfixes:

  • Removed unused harmony-reflect dependency

0.11.0 (2015-12-15)

Features:

  • --harmony-proxies flag is no longer required
  • Class names now declared in static collectionName(). Declaration through constructor is depracated (#16)
  • Added new required schema property (#18 and #19) Bugfixes:
  • Fixed some inconsistencies with id aliasing (partial fix to #20)

0.10.0 (2015-11-12)

Features:

  • Added support for setting the 'unique' index on a field
  • Added support for specifying which reference fields should be populated in loadOne() and loadMany() Bugfixes:
  • Fixed issue in isNativeId where we weren't returning a proper Boolean.

0.9.1 (2015-11-06)

Features:

  • Added support for testing in travis-ci.org Bugfixes:
  • Fixed issue #10 where IDs in queries had to be ObjectId. Now string IDs are automatically cast to ObjectId.

0.9.0 (2015-10-30)

Features:

  • Added support for sort option on .loadMany()
  • Added support for limit option on .loadMany()
  • Added support for skip option on .loadMany()
  • Added .toJSON() to Document and EmbeddedDocument for serialization
  • Updated 'engines' property to '>=2.0.0' in package.json Bugfixes:
  • Fixed issue #14 where Documents couldn't be initialized with an array of EmbeddedDocument objects via .create()

0.8.0 (2015-10-12)

Features:

  • Added support for custom validation on schemas via validate property Bugfixes:
  • Fixed issue #9 where no member values could be set as undefined

0.7.1 (2015-08-21)

Bugfixes:

  • Fixed issue #8 where virtual setters were not used on initialization
  • loadMany() now loads all documents if query is not provided
  • deleteMany() now deletes all documents if query is not provided

0.7.0 (2015-08-18)

Features:

  • Added loadOneAndUpdate static method to Document class
  • Added loadOneAndDelete static method to Document class

0.6.0 (2015-08-10)

Features:

  • Added in-memory support for NeDB
  • Added regex validator to Document

0.5.7 (2015-08-06)

Bugfixes:

  • Fixed issue where schema() wasn't canonicalizing schema definitions.
  • Updated README to show an example of using schema().

0.5.6 (2015-07-20)

Features:

  • README additions
  • New test for overriding schemas

0.5.5 (2015-07-15)

Bugfixes:

  • Fixed issue where _id was being reassigned in Mongo, and fixed issue with populating references in Mongo.
  • Fixed issue with Mongo driver where reference validation checks failed.
  • Fixed test Issues.#4 for when running in Mongo.

0.5.4 (2015-07-09)

Bugfixes:

  • Fixed issue where Dates were saved in different formats (integers, Date objects, etc). Added way to canonicalize them so all dates look the same in the DB and are also loaded as Date objects.

0.5.3 (2015-07-01)

Bugfixes:

  • Fixed issue in .loadMany() where references in arrays were getting loaded too many times. (#4).
    • Added test in issues.test.js
  • Fixed issue in .loadMany() where muliple references to the same object were only getting loaded once. (#5).
    • Added test in issues.test.js

0.5.2 (2015-06-30)

  • Version bump, thanks to NPM.

0.5.1 (2015-06-30)

Bugfixes:

  • Fixed validation and referencing so Documents can be referenced by their object or ID.

0.5.0 (2015-06-26)

Features:

  • Exposed getClient() method for retrieving the active Camo client.
  • Added options parameter to connect() so options can be passed to backend DB client.
  • Static method Document.fromData() is now a private helper method. Static method .create() should be used instead.

Bugfixes:

  • In Document._fromData(), added check to see if ID exists before assigning
  • Changed BaseDocument._fromData() so it returns data in same form as it was passed.
    • i.e. Array of data returned as array, single object returned as single object.
  • Fixed bug where assigning an array of Documents in .create() lost the references.
  • Stopped using the depracated _.extend() alias. Now using _.assign() instead. (#1).
  • Fixed get and set issue with Proxy (#3).

0.4.0 (2015-06-22)

Features:

  • Changed .isModel() to .isDocument().
  • Added EmbeddedDocument class and tests.
    • The following features work with EmbeddedDocuments: = Schema options: default, min, max, type, choices = All types supported in Document also work in EmbeddedDocument = Array of EmbeddedDocuments = Pre/post validate, save, and delete hooks

0.3.2 (2015-06-19)

Bugfix:

  • Added forked version of harmony-reflect. Only difference is it uses a global to ensure it runs only once.

0.3.1 (2015-06-19)

Bugfix:

  • Moved Proxy/Reflect shim to index. Seems to fix problem where shim broke Proxies (even worse).

0.3.0 (2015-06-18)

Features:

  • Added support for MongoDB using node-mongodb-native as the backend.
  • Added .toCanonicalId() and .isNativeId() to DatabaseClient and its child classes.

0.2.1 (2015-06-17)

  • README fix.

0.2.0 (2015-06-17)

Features:

  • Added the following Document hooks:
    • preValidate()
    • postValidate()
    • preSave()
    • postSave()
    • preDelete()
    • postDelete()

0.1.1 (2015-06-17)

Features:

  • Updated README to include 'javascript' declaration for syntax highlighting.
  • Added 'homepage' to package.json.
  • Added 'repository' to package.json.
  • Added 'email' to 'author' in package.json.

0.1.0 (2015-06-17)

Features:

  • Minor version bump.
  • No longer need to use .schema() in Document subclass. Now all public variables (any variable not starting with an underscore) are used in the schema.
  • Implemented .count() in Document, Client, and NeDbClient to get Document count without retrieving data.
  • Added options parameter to .loadOne() and .loadMany() to specify whether references should be populated.
  • Added support for circular dependencies.
  • Added README.
  • Added CHANGELOG.

0.0.1 (2015-06-15)

Initial release.