Mongore
provides an easy way to create native JavaScript classes with MongoDB backend.
The idea is to make Mongo pretty much invisible and still enjoy Mongo-powered persistency and fast search.
Fair warning: This project is still undertested, use at your own risk.
Lets take a look at a quick example. First, lets connect to MongoDB (for simplicity, I will ignore callbacks and validations for now):
const MongoClient = require('mongodb').MongoClient
const Mongore = require('mongore')
Mongore.Client.connect(MongoClient, "mongodb://localhost:27017/dogsDb", "dogsDb");
Now lets define a model:
// define a class to represent a dog
class Dog extends Mongore.Model
{
// create the dog with name and breed.
constructor(name, breed)
{
super();
this.name = name;
this.breed = breed;
}
// bark.
bark()
{
if (this.breed == "chihuahua") {
console.log("*angry rat noises*");
}
else {
console.log("Woof!");
}
}
// give dog a bone.
// note: cooked bones are actually dangerous for dogs, you should never give your dog cooked bones!
giveBone()
{
this.bonesEaten++;
this.bark();
}
// calculate dog's age based on when it was created in DB.
get age()
{
return (new Date()).getYear() - this.mongore.creationTime.getYear());
}
}
// define the dog fields that will be stored in DB:
Dog.buildModel(
fields: {
_id: new Mongore.Fields.StringField({source: "name", maxLength: 16}),
breed: new Mongore.Fields.ChoiceField({choices: ["chihuahua", "bulldog", "golden", "lab"]}),
bonesEaten: new Mongore.Fields.NumericField({default: 0, parser: parseInt})
}
);
Using the model is super easy!
var myDog = new Dog("Rex", "golden");
myDog.giveBone();
myDog.mongore.save();
Or to load a dog from db..
Dog.mongore.load("Rex", (myDog) => {
console.log("My dog ate ", myDog.bonesEaten, "bones.");
});
Mongore's key features are:
- Using DB models like plain JavaScript classes.
- Built-in field types with validation and automatic 'cleanup' (also convert to MongoDB validators).
- Useful added metadata like creation time, last update time, versioning, ect.
- Optimized - will only save changed fields, if anything was changed.
- Foreign keys that turn into model instances when loaded.
- Automatic collections setup.
To install via npm:
npm install mongore
Before using Mongore, you need to initialize a connection (note: its OK to define your models before initializing, you only need to connect before loading / saving objects):
const Mongore = require('./mongore');
const MongoClient = require('mongodb').MongoClient;
Mongore.Client.connect(MongoClient, Config.db.url, Config.db.name, () => {
// ready for action!
})
As you can see Mongore
don't require MongoDB
directly. This means you can use whatever version you want or wrap Mongo's client yourself, as long as the basic API of insertOne, updateOne, findOne is kept.
Now lets define some Models, ie. persistent JS objects with MongoDB backend. For this example, we'll make a simple forum app.
We'll start with the User
model:
/**
* Represent a user.
*/
class User extends Mongore.Model
{
/**
* Create a new user.
*/
constructor(username, password, userType)
{
super();
this.username = username;
this.password = password;
this.userType = userType;
}
/**
* Get if admin user.
*/
get isAdmin()
{
return this.userType === "admin";
}
/**
* Create and return a new board (don't save it).
*/
createBoard(name)
{
if (!this.isAdmin) {
throw new Error("Only admins can open boards!");
}
return new Board(name, this);
}
}
User.buildModel(
fields: {
_id: new Mongore.Fields.StringField({source: "username", maxLength: 16}),
userType: new Mongore.Fields.ChoiceField({choices: ["guest", "registered", "admin"]}),
password: new Mongore.Fields.StringField()
}
);
So what's going on above?
- First we define a
User
class that get username, password, and type. It also have a method to create a board (will be defined next) and a getter to check if type is admin. - Next, we define the persistent part of the model, which is the fields that will represent this object in MongoDB.
- First we define the _id field. We create it as a
string
with max length of 16 characters, and we make it a proxy of theusername
property. - Next we add a user type choice field, which is a string that will only accept one of the listed values: 'guest', 'registered', or 'admin'.
- Finally, we add a password field as plain text (horrible when considering security, I know).
- First we define the _id field. We create it as a
Once the fields are defined, you can use them just as you would with any JS property from the instance, as demonstrated in the constructor and in the class's methods.
As you probably know, MongoDB works with collections
, and while you can let MongoDB create collections with default values automatically, its best to define them with validations and other metadata (like indexes). So lets ask Mongore
to create a collection for Users:
User.mongore.createCollection(() => {
console.log("Users collection is now ready!");
});
Obviously you don't have to call createCollection
every time your app starts (although there's no harm in that), so its best to create short init-db.js
script, and put all the createCollection()
calls there. Call this script whenever you change or define a new Model.
Now lets create and save a new user:
var user = new User("Billy", "pass1234", "admin");
user.mongore.save(() => {
console.log("Great success!");
},
(err) => {
console.log("Error! ", err);
});
That's it! You can now check your MongoDB and see a Users
collection with a single object, Billy
.
So how do we load the user we just created? Easy!
User.mongore.load("Billy", (user) => {
console.log(`Welcome back, ${user.username}!`);
});
All the fields you define in your model are created as-if they were plain properties under your class. However, as you noticed above, we also have the mongore
object under the instance and the class itself.
This object contains the API and metadata of mongore
, and represent the bridge between your plain JS object and the Database.
- .mongore contains the API related to the Model itself, like the
load()
andcreateCollection()
methods. - .mongore contains the API related to a specific instance, like the
save()
method and other useful functions described later.
Continuing with our example, time to define the Board
model to represent a board:
/**
* Represent a board.
*/
class Board extends Mongore.Model
{
/**
* Create a new board.
*/
constructor(name, creator)
{
super();
this.name = name;
this.creator = creator;
}
/**
* Create and return a new message (don't save it).
*/
createMessage(user, text)
{
return new Message(this, user, text);
}
}
Board.buildModel(
fields: {
_id: new Mongore.Fields.StringField({source: "name", maxLength: 16}),
creator: new Mongore.Fields.ForeignKeyField({model: User, canBeNull: false}),
}
);
The name
field is similar to User.username
and will not be discussed here. The new field, creator
is interesting.
A ForeignKeyField
is a field that expect an instance of the provided Model (in this case, User
). In Mongo, it will be stores as the instance's _id
field. When an instance of Board
is loaded, it will also load all its foreign keys.
Note that the success callback will be called before foreign keys are loaded. Lets take a look at how we load a Board
instance, wait for the User
foreign key to be ready, then performing an action:
Board.mongore.load("myBoard", (board) => {
board.creator.mongore.onLoaded((user) => {
board.createMessage(user, "Hi all, just logged in! - XOXO").mongore.save();
})
})
And finally, lets create the message Model:
/**
* Represent a single text message in one of the forum's boards.
*/
class Message extends Mongore.Model
{
/**
* Create a new message.
*/
constructor(board, author, msg)
{
super();
this.board = board;
this.author = author;
this.msg = msg;
}
/**
* Give this message a thumb up.
*/
upvote()
{
this.score++;
}
/**
* Give this message a thumb down.
*/
downvote()
{
this.score--;
}
}
Message.buildModel(
fields: {
board: new Mongore.Fields.ForeignKeyField({model: Board, canBeNull: false}),
author: new Mongore.Fields.ForeignKeyField({model: User, canBeNull: false}),
msg: new Mongore.Fields.StringField(),
score: new Mongore.Fields.NumericField({parser: parseInt, index: true})
}
);
The above is pretty similar to the previous models, with a just a minor thing to notice: index: true
will set the score
field as an index, which means we can quickly search on it.
That's it for the example, now lets explore the API!
The main function that turn your JS class into a proper Model is the buildModel()
method, as demonstrated in the example above.
This method accept the following arguments (as a dictionary):
- fields: A dictionary of fields to attach to this Model. Every field will receive a corresponding getter and setter, and will be stored in MongoDB. Field types are described later.
- maxSizeInBytes: If defined, will limit this Model's collection to this number of bytes.
- maxCount: If defined, will limit number of records in this Model's collection to this amount.
- collectionName: Collection name to use for this model. If not defined, will use class name + 's'.
When you build your Model you specify one or more fields to include in the DB scheme. As briefly mentioned before, every field you define will create a getter and setter in your class with the same name. In addition, every field define validations and cleanup logic to trigger when writing / reading from DB, as well as validators to set in MongoDB itself (there are validations in app level and in Mongo's level).
There are several built-in Field types (its really easy to define new ones) and every field accept different arguments. However, all fields support the following:
- default: Default value to set for the field.
- source: If provided, will always copy value from a given property in the object before writing to DB. When loaded, will also set the property.
- sourceReadonly: If 'source' is provided and 'sourceReadonly' is true, will only copy from source when writing to DB, but won't set the property when loaded.
- canBeNull: Available for most types; allow null value.
- index: If true, will make this field an index field.
- unique: If true and indexed, will make this field unique.
Now lets describe all the field types:
A generic field type without validations or cleanup logic.
A boolean field.
A choice field that enforce the value to be from a set of options.
Have the following additional options:
- choices: List of choices to pick from.
A date field (store JavaScript Date objects).
Have the following additional options:
- autoNow: If true, will always be set to current time when saved.
A dictionary field.
Have the following additional options:
- schema: Optional dictionary of fields to describe the desired dictionary schema.
Note: when a schema is defined, you also have to define a default value that matches it.
A field that point on another Model's instance, and load it automatically when loaded from DB (in MongoDB its stored as _id).
Have the following additional options:
- model: Model type this field points on (mandatory).
A list field.
Have the following additional options:
- maxItems: Optional list size limit.
- itemsType: An optional field instance to validate all list entries before save.
- removeDuplicates: If true, will remove duplications before saving.
A number (int or float) field.
Have the following additional options:
- min: Optional min value.
- max: Optional max value.
- parser: Optional parser to treat numbers - should use parseInt for integers or parseFloat for floats (default to parseFloat).
A string field.
Have the following additional options:
- maxLength: Optional min length.
- minLength: Optional max length.
Same as a StringField, but validate email pattern.
The Mongore.Model
is the base class all your models should extend. Except for the fields you define, which will became getters and setters under your class, most of your model's API will remain unchanged as mongore's API is kept under the mongore
accessor.
However, there are some callbacks you can implement to respond to DB related events:
Will be called whenever the instance is successfully loaded from DB.
Will be called before saving an instance to the DB.
Will be called after an instance was successfully saved.
Will be called after the instance was deleted from DB.
Every Model class you define will be coupled with a mongore
API that implements the Model-related API.
This is the main object you access to use Mongore when you don't have an instance.
The following is the key functionality of the class-level API:
Load and return an instance from DB.
Delete an instance from DB.
Note: won't invoke afterDeletedFromDB
callbacks.
Delete multiple instances from DB.
Note: won't invoke afterDeletedFromDB
callbacks.
Create the collection (with all validation and indexes) for this Model.
Drop the collection of this Model.
Get the default value for a given field name.
Get the collection name used for this model.
List with all field names of this model (not including internal stuff).
Every model instance you create will be coupled with a mongore
API that implements the Instance API.
This is the main object you access to use Mongore.
The following is the key functionality of the instance-level API:
Save the instance to DB.
Reload this object from DB.
Delete this object from DB.
Optional callback to invoke on first time this instance is loaded from DB.
Return if this instance have any changed fields that needs to be saved to DB.
Get a list of field names that are dirty.
Set a field to its default value.
Get the creation time of the object (first time it was insert to DB). Note: does not update automatically, call reload() to update this.
Get the last update() / insert() time of this object. Note: does not update automatically, call reload() to update this.
An auto-incremented number representing the number of times this object was updated in DB. Note: does not update automatically, call reload() to update this.
Get the instance primary id field.
Return if the object was loaded from DB.
Return if this specific instance was ever saved to DB.
Return if the specific instance was either loaded from DB, or ever saved to it. In other words, if its present in DB.
Return last time this object was loaded (or reloaded) from DB.
Collection name used for this instance.
List with all field names of this model (not including internal stuff).
If you want to receive logs from Mongore
, you can set a logger instance with Mongore.setLogger()
.
The logger should implement all the basic logging methods (error, warn, info, verbose, debug, silly), and it is recommended to add an automatic label to mark that these logs originalte from Mongore, so they won't get mixed up with your other logs (or as an alternative - send to a different file).
Mongore
is distributed under the MIT license and is free for any purpose.