Invisible.js
Invisible is a JavaScript (and CoffeeScript!) library that leverages browserify to achieve the Holy Grail of web programming: model reuse in the client and the server.
Installation and setup
Install with npm:
npm install invisible
Wire up Invisible into your express app:
var express = require("express");
var path = require("path");
var invisible = require("invisible");
var app = express();
invisible.createServer(app, path.join(__dirname, "models"))
The second parameter is the directory where Invisible will look for model files.
Extending models
To make your models available everywhere, define them and call Invisible.createModel
.
// models/person.js
var Invisible = require("invisible");
var crypto = require("crypto");
var _s = require("underscore.string");
function Person(firstName, lastName, email){
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
Person.prototype.fullName = function(){
return this.firstName + ' ' + this.lastName;
}
Person.prototype.getAvatarUrl = function(){
cleanMail = _s.trim(this.email).toLowerCase();
hash = crypto.createHash("md5").update(cleanMail).digest("hex");
return "http://www.gravatar.com/avatar/" + hash;
}
module.exports = Invisible.createModel("Person", Person);
Now your models will be available under the Invisible namespace. Require as usual in the server:
var Invisible = require("invisible")
var john = new Invisible.Person("John", "Doe", "[email protected]");
john.fullName(); //John Doe
In the client, just add the invisible script:
<script src="invisible.js"></script>
<script>
var jane = new Invisible.Person("Jane", "Doe", "[email protected]");
alert(jane.fullName()); //Jane Doe
</script>
Invisible.js uses browserify to expose you server defined models in the browser, so you can use any broserify-able library to implement them. Note that this integration is seamless, no need to build a bundle, Invisible.js does that for you on the fly.
REST and MongoDB integration
In addition to making your models available everywhere, Invisible extends them with methods to handle persistence. In the server this means interacting with MongoDB and in the client making requests to an auto-generated REST API, that subsequently performs the same DB action.
Save
jane.save(function(err, result){
if (err){
console.log("something went wrong");
} else {
console.log("Jane's id is " + jane._id);
console.log("which equals " + result._id);
}
})
The first time the save method is called, it creates the model in the database and sets its _id
attribute.
Subsequent calls update the model. Validations are called upon saving, see the validations section for details.
Note that a full Invisible model instance is passed to the callback, and the calling instance is also updated when the process is done.
Delete
jane.delete(function(err, result){
if (err){
console.log("something went wrong");
} else {
console.log("Jane is no more.");
}
})
Query
Invisible.Person.query(function(err, results){
if (err){
console.log("something went wrong");
} else {
console.log("Saved persons are:");
for (var i = 0; i < results.length; i++){
console.log(results[i].fullName());
}
}
});
Invisible.Person.query({firstName: "Jane"}, function(err, results){
if (err){
console.log("something went wrong");
} else {
console.log("Persons named Jane are:");
for (var i = 0; i < results.length; i++){
console.log(results[i].fullName());
}
}
});
Invisible.Person.query({}, {sort: "lastName", limit: 10}, function(err, results){
if (err){
console.log("something went wrong");
} else {
console.log("First 10 persons are:");
for (var i = 0; i < results.length; i++){
console.log(results[i].fullName());
}
}
});
Queries the database for existent models. The first two optional arguments correspond to the Query object and Query options in the MongoDB Node.JS driver. The one difference is that when using ids you can pass strings, that will be converted to ObjectID when necessary.
Find by id
Invisible.Person.findById(jane._id, function(err, model){
if (err){
console.log("something went wrong");
} else {
console.log("Jane's name is " + model.firstName);
console.log("But we knew that!");
}
})
Looks into the database for a model with the specified _id
value. As in the query method, you can pass
a string id instead of an ObjectID instance.
Validations
Invisible.js integrates with revalidator to handle model validations.
A validate
method is added to each model which looks for a defined validation schema, and is executed each time
a model is saved, both in the client and the server. For example:
function Person(email){
this.email = email;
}
Person.prototype.validations = {
properties: {
email: {
format: 'email',
required: true
}
}
}
var john = new Person("[email protected]");
john.save(function(err, result){
console.log("All OK here.");
});
john.email = "invalid";
john.save(function(err, result){
console.log(err);
/* Prints: {valid: false, errors: [{attribute: 'format',
property: 'email', message: 'is not a valid email'}]} */
})
Invisible.js also introduces "method" validations, which allow you to specify a method which should be called in the validation process. This way asynchronic validations, such as querying the database, can be performed:
Person.prototype.validations = {
methods: ['checkUnique']
}
Person.prototype.checkUnique = function(cb) {
Invisible.Person.query({email: this.email}, function(err, res){
if (res.length > 0){
cb({valid: false, errors: ["email already registered"]});
} else {
cb({valid: true, errors: []});
}
});
}
The custom validation method takes a callback and should call it with the return format of revalidator: an object with a "valid" boolean field and an "errors" list. Note that the method validations are only called if the properties validations succeed, and stop the validation process upon the first failure.
Real time events
Invisible.js uses socket.io to emmit an event whenever something changes for a model, and lets you add listener functions to react to those changes in realtime.
Invisible.Person.onNew(function(model){
console.log(model.fullName() + " has been created");
});
Invisible.Person.onUpdate(function(model){
console.log(model.fullName() + " has been updated");
});
Invisible.Person.onDelete(function(model){
console.log(model.fullName() + " has been deleted");
});