Generator-powered, highly extendible models made for use with co.
npm install moko
var moko = require('moko'),
validators = require('moko-validators'),
mongo = require('moko-mongo');
var User = moko('User');
User
.attr('_id')
.attr('name', { required: true })
.attr('email', { format: 'email' })
User
.use(validators)
.use(yield mongo('localhost:27017/moko-test'));
co(function*() {
var user = yield new User();
user.name = 'Ryan';
user.email = 'ryan@slingingcode.com';
yield user.save();
console.log(user._id);
})();
moko
is the spiritual successor to
modella, updated for use with ECMA
6 generators.
moko
provides bare-bones models and an API to extend them. Plugins are mixed
into models adding functionality as needed. Plugins are easy to write (see the Moko Plugin Creation
Guide) and
readily shared within the moko community (see the Moko Plugin List)
The organization is open for those who would like to join. Simply reach out to Ryan and say hi!
Moko provides two types of events, async
(powered by generators) and sync
(powered by functions). async
events happen before an operation and allow you to mutate the data,
while sync
events allow you to react to changes. In general, async
events
end with ing
(eg. saving
, creating
, initializing
).
Plugin authors are also encouraged to emit their own events to make it easy for users to hook into the plugins.
Moko will emit the following async events. Notice that you must use generators for async events, although your generators do not necessarily need to yield themselves.
initializing(attrs)
- called when a model is first initialzedsaving(dirty)
- called before savecreating(dirty)
- called before save when the model did not exist priorupdating(dirty)
- called before save when the model did exist priorExamples:
User.on('initializing', function*(user, attrs) {
attrs.name = 'Bob';
});
var user = yield new User({name: 'Stephen'});
console.log(user.name) // Logs "Bob";
User.on('creating', function*(user, attrs) {
attrs.createdAt = new Date();
});
var user = yield new User({name: 'Stephen'});
yield user.save();
User.on('saving', function*(u, dirty) {
var others = yield User.find({name: u.name}).count();
if(others) throw new Error('Will not save with non-unique name');
});
Function (not generator) events are emitted after something happens on the model.
Built in events include:
initialize(instance)
- called after an instance is done initializingchange(attr, newVal, oldVal)
- called when an attr changeschange attr(newVal, oldVal)
- called when attr
changessave
- called after savecreate
- called after save when model did not exist priorupdate
- called after save when model did exist priorUser
.attr('name')
.attr('email');
User.on('change name', function(user, name, old) {
console.log("User changed name from %s to %s", old, name);
});
co(function*() {
var user = yield new User({name: 'Bob'});
user.name = 'Steve';
})();
Fire and forget email sending on user-creation.
User.on('create', function(user) {
emails.sendWelcomeEmail(u.email, function() { }) // anonymous callback fn
});
To create a Model, simply call moko
with the name of the model. If preferred
you can also call new Moko(name)
.
var moko = require('moko');
var User = moko('User');
User instanceof moko // true
console.log(User.modelName) // => 'User'
// or
var Person = new moko('Person');
All model configuration methods are chainable.
Defines attribute name
on instances, adding change
events. (see
events below)
opts
is an object that can be used by plugins.
var User = moko('User');
User
.attr('name', { required: true })
.attr('age', { type: Number });
User.on('change name', function(u, name, old) {
console.log(old + ' changed name to ' + name);
});
var user = yield new User({name: 'Steve'});
user.name = 'Bob';
Adds a validator fn*(instance)
which can add errors to an instance.
var User = moko('User');.attr('name');
var requireNameSteve = function*(user) {
if(user.name != 'Steve') user.error('name', 'must be steve');
};
User.validate(requireNameSteve);
Configures a model to use a plugin. See the list of plugins or the plugin creation guide to get started writing your own.
var mongo = require('moko-mongo'),
validators = require('moko-validators'),
timestamps = require('moko-timestamps');
var db = yield mongo('mongodb://localhost:27017/moko-test');
User
.use(db)
.use(validators)
.use(timestamps);
A moko Model mixes in co-emitter, see the full documentation for details. It exposes the following methods:
on(event, fn*)
- add a listener for an eventonce(event, fn*)
- add a one-time listener for an eventemit(event, ...args)
- emit an event with ...args
off(event, listener)
- remove a listener for an eventremoveAllListeners()
- removes all listenershasListeners(event)
- check whether an event has listenerslisteners(event)
- get an array of listeners for an eventInstances are created by yielding
to a new Model
. This allows async events
to happen on initializing
(such as pre-populating relations from the
database).
var user = yield new User({name: 'Bob'});
Takes an object of attrs
and sets the models properties accordingly. If an
attribute is passed in that isn't defined on the model, it will be skipped.
var User = moko('User');
User.attr('name');
var bob = yield new User();
bob.set({name: 'Bob', age: 24});
bob.name == 'Bob' // true
bob.age === undefined // true, age wasn't a defined attr
Returns a cloned object of the instances attrs
.
this.body = user.toJSON(); // inside koa
moko
provides a variety of methods to persist models to a sync layer. Out of
the box it does not use have any sync layer baked in, so without using one (as a
plugin) these methods can throw errors.
If val
is undefined, returns the primary key of the model (by default
instance._id
or instance.id
, whichever exists.
If val
is specified, sets the primary key attribute (instance._id or instance.id
).
You can also specify a primary key manually at time of attribute definition:
var User = moko('User').attr('username', { primary: true });
var user = yield new User();
user.primary('bob');
console.log(user.username) // 'Bob'
user.primary() // 'Bob'
Returns whether instance.primary()
is defined.
var userA = yield new User({_id: 123 });
userA.isNew(); // false
var userB = yield new User();
userB.isNew(); // true
Will save if a sync-layer plugin has been registered. Will only save if the model is valid, otherwise will throw an error.
To save regardless of being valid or not, pass in skipValidations
as true.
try {
yield user.save();
} catch(e) {
// deal with error;
}
Will remove the model if the sync layer provides it.
Will set instance.removed
to true.
yield user.remove();
Registers an error (reason
) on attr
.
if(!user.name) user.error('name', 'is required');
Returns an error object, in the format of {attr: [errors]}
.
If attr
is specified, returns an array of errors for that attr
.
If no errors are registered for attr
it returns an empty array.
user.error('name', 'is stupid');
user.error('name', 'is short');
user.error('age', 'is too young');
user.errors() // { name: ['is stupid', 'is short'], age: ['is too young'] }
user.errors('name') // ['is stupid', 'is short']
user.errors('favoriteColor') // []
Runs all validators, and checks whether the instance has any errors or not.
If attr
is provided, reports whether that specific attr
is valid.
To support async validations, you must yield
to isValid
.
var valid = yield user.isValid();
Moko exports common utilities to make it so that plugins don't need to end up requiring the same modules.
Built in utilities include:
moko.utils.clone
- does a deep clone of an objectmoko.utils.isGenerator
- returns true if a function is a generatormoko.utils.type
- returns a string representation of type (eg. array
)