Model is a datastore-agnostic ORM in JavaScript. It serves as the model-component for the Geddy MVC Web framework for NodeJS.
Apache License, Version 2
Model requires version 0.6.x of Node.js or higher. If you want to run the tests, or work on Model, you'll want the Jake JavaScript build-tool.
Installing with NPM
npm install model
Model currently implements adapters for:
- Postgres
- Riak
- MongoDB
Model uses a pretty simple syntax for defining a model. (It should look familiar to anyone who has used an ORM like ActiveRecord, DataMapper, Django's models, or SQLAlchemy.)
var User = function () {
this.property('login', 'string', {required: true});
this.property('password', 'string', {required: true});
this.property('lastName', 'string');
this.property('firstName', 'string');
this.validatesPresent('login');
this.validatesFormat('login', /[a-z]+/, {message: 'Subdivisions!'});
this.validatesLength('login', {min: 3});
this.validatesConfirmed('password', 'confirmPassword');
this.validatesWithFunction('password', function (s) {
// Something that returns true or false
return s.length > 0;
});
// Can define methods for instances like this
this.someMethod = function () {
// Do some stuff
};
};
// Can also define them on the prototype
User.prototype.someOtherMethod = function () {
// Do some other stuff
};
User = model.register('User', User);
Alternatively, you can use the defineProperties
method to lay out your model's
properties in one go:
var User = function () {
this.defineProperties({
login: {type: 'string', required: true}
, password: {type: 'string', required: true}
, lastName: {type: 'string'}
, firstName: {type: 'string'}
});
}
Creating an instance of one of these models is easy:
var params = {
login: 'alex'
, password: 'lerxst'
, lastName: 'Lifeson'
, firstName: 'Alex'
};
var user = User.create(params);
Data-validation happens on the call to create
, and any validation errors show
up inside an errors
property on the instance, keyed by field name. Instances
have an isValid
method that returns a Boolean indicating whether the instance is
valid.
// Leaving out the required password field
var params = {
login: 'alex'
};
var user = User.create(params);
// Prints 'false'
console.log(user.isValid());
// Prints 'Field "password" is required'
console.log(user.errors.password);
After creating the instance, call the save
method on the instance. This method
takes a callbak in the familiar (err, data) format for Node.
if (user.isValid()) {
user.save(function (err, data) {
if (err) {
throw err;
}
console.log('New item saved!');
});
}
Use the updateProperties
method to update the values of the properties on an
instance with the appropriate validations. Then call save
on the instance.
user.updateProperties({
login: 'alerxst'
});
if (user.isValid()) {
user.save(function (err, data) {
if (err) {
throw err;
}
console.log('Item updated!');
});
}
Both the base model 'constructors,' and model instances are EventEmitters. The emit events during the create/update/remove lifecycle of model instances. In all cases, the plain-named event is fired after the event in question, the 'before'-prefixed event, of course happens before.
The 'constructor' for a model emits the following events:
- beforeCreate
- create
- beforeValidate
- validate
- beforeUpdateProperties
- updateProperties
- beforeSave (new instances, single and bulk)
- save (new instances, single and bulk)
- beforeUpdate (existing single instances, bulk updates)
- update (existing single instances, bulk updates)
- beforeRemove
- remove
Model-item instances emit these events:
- beforeUpdateProperties
- updateProperties
- beforeSave
- save
- beforeUpdate
- update
Model uses a simple API for finding and sorting items. Again, it should look familiar to anyone who has used a similar ORM for looking up records. The only wrinkle with Model is that the API is (as you might expect for a NodeJS library) asynchronous.
Methods for querying are static methods on each model constructor.
Use the first
method to find a single item. You can pass it an id, or a set of
query parameters in the form of an object-literal. In the case of a query, it
will return the first item that matches, according to whatever sort you've
specified.
var user;
User.first({login: 'alerxst'}, function (err, data) {
if (err) {
throw err;
}
user = data;
console.log('Found user');
console.dir(user);
});
Use the all
method to find lots of items. Pass it a set of query parameters in
the form of an object-literal, where each key is a field to compare, and the
value is either a simple value for comparison (equal to), or another
object-literal where the key is the comparison-operator, and the value is the
value to use for the comparison.
var users
, dt;
dt = new Date();
dt.setHours(dt.getHours() - 24);
// Find all the users created since yesterday
User.all({createdAt: {gt: dt}, function (err, data) {
if (err) {
throw err;
}
users = data;
console.log('Found users');
console.dir(users);
});
Here are some more examples of queries:
// Where "foo" is 'BAR' and "bar" is not null
{foo: 'BAR', bar: {ne: null}}
// Where "foo" begins with 'B'
{foo: {'like': 'B'}}
// Where foo is less than 2112, and bar is 'BAZ'
{foo: {lt: 2112}, bar: 'BAZ'}
Here is the list of comparison operators currently supported:
eql: equal to ne: not equal to gt: greater than lt: less than gte: greater than or equal lte: less than or equal like: like
A simple string-value for a query parameter is the same as 'eql'. {foo: 'bar'}
is the same as {foo: {eql: 'bar'}}
.
Model supports combining queries with OR and negating queries with NOT.
To perform an 'or' query, use an object-literal with a key of 'or', and an array of query-objects to represent each set of alternative conditions:
// Where "foo" is 'BAR' OR "bar" is 'BAZ'
{or: [{foo: 'BAR'}, {bar: 'BAZ'}]}
// Where "foo" is not 'BAR' OR "bar" is null OR "baz" is less than 2112
{or: [{foo {ne: 'BAR'}}, {bar: null}, {baz: {lt: 2112}}]}
To negate a query with 'not', simply use a query-object where 'not' is the key, and the value is the set of conditions to negate:
// Where NOT ("foo" is 'BAR' and "bar" is 'BAZ')
{not: {foo: 'BAR', bar: 'BAZ'}}
// Where NOT ("foo" is 'BAZ' and "bar" is less than 1001)
{not: {foo: 'BAZ', bar: {lt: 1001}}}
These OR and NOT queries can be nested and combined:
// Where ("foo" is like 'b' OR "foo" is 'foo') and NOT "foo" is 'baz'
{or: [{foo: {'like': 'b'}}, {foo: 'foo'}], not: {foo: 'baz'}}
The all
API-call for querying accepts an optional options-object after the
query-conditions. Set a 'sort' in that options-object to specifiy properties to
sort on, and the sort-direction for each one.
var users
// Find all the users who have ever been updated, and sort by
// creation-date, ascending, then last name, descending
User.all({updatedAt: {ne: null}}, {sort: {createdAt: 'asc', lastName: 'desc'}},
function (err, data) {
if (err) {
throw err;
}
users = data;
console.log('Updated users');
console.dir(users);
});
You can use a simplified syntax for specifying the sort. The default sort-direction is ascending ('asc'), so you can specify a property to sort on (or multiple properties as an array) if you want all sorts to be ascending:
// Sort by createdAt, ascending
{sort: 'createdAt'}
// Sort by createdAt, then updatedAt, then lastName,
// then firstName -- all ascending
{sort: ['createdAt', 'updatedAt', 'lastName', 'firstName']}
Model has basic support for associations: including hasMany/belongsTo and
hasOne/belongsTo. For example, if you had a User
model with a single
Profile
, and potentially many Accounts
:
var User = function () {
this.property('login', 'string', {required: true});
this.property('password', 'string', {required: true});
this.property('confirmPassword', 'string', {required: true});
this.hasOne('Profile');
this.hasMany('Accounts');
};
A Book
model that belongs to an Author
would look like this:
var Book = function () {
this.property('title', 'string');
this.property('description', 'text');
this.belongsTo('Author');
};
Add the hasOne
relationship by calling 'set' plus the name of the owned
model in singular (in this case setProfile
). Retrieve the associated item by
using 'get' plus the name of the owned model in singular (in this case
getProfile
). Here's an example:
var user = User.create({
login: 'asdf'
, password: 'zerb'
, confirmPassword: 'zerb'
});
user.save(function (err, data) {
if (err) {
throw err;
}
user.setProfile(Profile.create({}));
user.save(function (err, data) {
if (err) {
throw err;
}
user.getProfile(function (err, data) {
if (err) {
throw err;
}
console.log(profile.id ' is the same as ' + data.id);
});
});
});
Set up the hasMany
relationship by calling 'add' plus the name of the
owned model in singular (in this case addAccount
). Retrieve the associated
items with a call to 'get' plus the name of the owned model in plural (in
this case getAccounts
). An example:
var user = User.create({
login: 'asdf'
, password: 'zerb'
, confirmPassword: 'zerb'
});
user.save(function (err, data) {
if (err) {
throw err;
}
user.addAccount(Account.create({}));
user.addAccount(Account.create({}));
user.save(function (err, data) {
if (err) {
throw err;
}
user.getAccounts(function (err, data) {
if (err) {
throw err;
}
console.log('This number should be 2: ' + data.length);
});
});
});
A belongsTo
relationship is created similarly to a hasOne
: by calling 'set'
plus the name of the owner model in singular (in this case setAuthor
).
Retrieve the associated item by using 'get' plus the name of the owner model
in singular (in this case getAuthor
). Here's an example:
var book = Book.create({
title: 'How to Eat an Entire Ham'
, description: 'Such a poignant book. I cried.'
});
book.save(function (err, data) {
if (err) {
throw err;
}
book.setAuthor(Author.create({
familyName: 'Neeble'
, givenName: 'Leonard'
}));
book.save(function (err, data) {
if (err) {
throw err;
}
book.getAuthor(function (err, data) {
if (err) {
throw err;
}
console.log('This name should be "Neeble": ' + data.familyName);
});
});
});
Sometimes you need mutliple associations to the same type of model (e.g., I have lots of Friends and Relatives who are all Users). You can accomplish this in Model using named associations:
var User = function () {
this.property('familyName', 'string', {required: true});
this.property('givenName', 'string', {required: true});
this.hasMany('Friends', {model: 'Users'});
this.hasMany('Relatives', {model: 'Users'});
};
The API for this is the same as with normal associations, using the set
/add
and get
, with the appropriate association name (not the model name). For
example, in the case of Friends
, you'd use addFriend
and getFriends
.
Model JavaScript ORM copyright 2112 mde@fleegix.org.