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:
express = require("express");
path = require("path");
invisible = require("invisible");
app = express();
invisible.createServer(app, path.join(__dirname, "models"))
Extending models
To make your models available everywhere, define them and call Invisible.createModel
Invisible = require("invisible");
crypto = require("crypto");
_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:
Invisible = require("invisible")
john = new Invisible.Person("John", "Doe", "john.doe@mail.com");
john.fullName();
In the client, just add the invisible script:
<script src="invisible.js"></script>
<script>
jane = new Invisible.Person("Jane", "Doe", "jane.doe@mail.com");
alert(jane.fullName());
</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
}
}
}
john = new Person("john.doe@none.com");
john.save(function(err, result){
console.log("All OK here.");
});
john.email = "invalid";
john.save(function(err, result){
console.log(err);
})
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");
});