UNPKG

modella

Version:

simplified models

1,053 lines (870 loc) 20.8 kB
;(function(){ /** * Require the given path. * * @param {String} path * @return {Object} exports * @api public */ function require(path, parent, orig) { var resolved = require.resolve(path); // lookup failed if (null == resolved) { orig = orig || path; parent = parent || 'root'; var err = new Error('Failed to require "' + orig + '" from "' + parent + '"'); err.path = orig; err.parent = parent; err.require = true; throw err; } var module = require.modules[resolved]; // perform real require() // by invoking the module's // registered function if (!module._resolving && !module.exports) { var mod = {}; mod.exports = {}; mod.client = mod.component = true; module._resolving = true; module.call(this, mod.exports, require.relative(resolved), mod); delete module._resolving; module.exports = mod.exports; } return module.exports; } /** * Registered modules. */ require.modules = {}; /** * Registered aliases. */ require.aliases = {}; /** * Resolve `path`. * * Lookup: * * - PATH/index.js * - PATH.js * - PATH * * @param {String} path * @return {String} path or null * @api private */ require.resolve = function(path) { if (path.charAt(0) === '/') path = path.slice(1); var paths = [ path, path + '.js', path + '.json', path + '/index.js', path + '/index.json' ]; for (var i = 0; i < paths.length; i++) { var path = paths[i]; if (require.modules.hasOwnProperty(path)) return path; if (require.aliases.hasOwnProperty(path)) return require.aliases[path]; } }; /** * Normalize `path` relative to the current path. * * @param {String} curr * @param {String} path * @return {String} * @api private */ require.normalize = function(curr, path) { var segs = []; if ('.' != path.charAt(0)) return path; curr = curr.split('/'); path = path.split('/'); for (var i = 0; i < path.length; ++i) { if ('..' == path[i]) { curr.pop(); } else if ('.' != path[i] && '' != path[i]) { segs.push(path[i]); } } return curr.concat(segs).join('/'); }; /** * Register module at `path` with callback `definition`. * * @param {String} path * @param {Function} definition * @api private */ require.register = function(path, definition) { require.modules[path] = definition; }; /** * Alias a module definition. * * @param {String} from * @param {String} to * @api private */ require.alias = function(from, to) { if (!require.modules.hasOwnProperty(from)) { throw new Error('Failed to alias "' + from + '", it does not exist'); } require.aliases[to] = from; }; /** * Return a require function relative to the `parent` path. * * @param {String} parent * @return {Function} * @api private */ require.relative = function(parent) { var p = require.normalize(parent, '..'); /** * lastIndexOf helper. */ function lastIndexOf(arr, obj) { var i = arr.length; while (i--) { if (arr[i] === obj) return i; } return -1; } /** * The relative require() itself. */ function localRequire(path) { var resolved = localRequire.resolve(path); return require(resolved, parent, path); } /** * Resolve relative to the parent. */ localRequire.resolve = function(path) { var c = path.charAt(0); if ('/' == c) return path.slice(1); if ('.' == c) return require.normalize(p, path); // resolve deps by returning // the dep in the nearest "deps" // directory var segs = parent.split('/'); var i = lastIndexOf(segs, 'deps') + 1; if (!i) i = 0; path = segs.slice(0, i + 1).join('/') + '/deps/' + path; return path; }; /** * Check if module is defined at `path`. */ localRequire.exists = function(path) { return require.modules.hasOwnProperty(localRequire.resolve(path)); }; return localRequire; }; require.register("component-indexof/index.js", function(exports, require, module){ module.exports = function(arr, obj){ if (arr.indexOf) return arr.indexOf(obj); for (var i = 0; i < arr.length; ++i) { if (arr[i] === obj) return i; } return -1; }; }); require.register("component-emitter/index.js", function(exports, require, module){ /** * Module dependencies. */ var index = require('indexof'); /** * Expose `Emitter`. */ module.exports = Emitter; /** * Initialize a new `Emitter`. * * @api public */ function Emitter(obj) { if (obj) return mixin(obj); }; /** * Mixin the emitter properties. * * @param {Object} obj * @return {Object} * @api private */ function mixin(obj) { for (var key in Emitter.prototype) { obj[key] = Emitter.prototype[key]; } return obj; } /** * Listen on the given `event` with `fn`. * * @param {String} event * @param {Function} fn * @return {Emitter} * @api public */ Emitter.prototype.on = function(event, fn){ this._callbacks = this._callbacks || {}; (this._callbacks[event] = this._callbacks[event] || []) .push(fn); return this; }; /** * Adds an `event` listener that will be invoked a single * time then automatically removed. * * @param {String} event * @param {Function} fn * @return {Emitter} * @api public */ Emitter.prototype.once = function(event, fn){ var self = this; this._callbacks = this._callbacks || {}; function on() { self.off(event, on); fn.apply(this, arguments); } fn._off = on; this.on(event, on); return this; }; /** * Remove the given callback for `event` or all * registered callbacks. * * @param {String} event * @param {Function} fn * @return {Emitter} * @api public */ Emitter.prototype.off = Emitter.prototype.removeListener = Emitter.prototype.removeAllListeners = function(event, fn){ this._callbacks = this._callbacks || {}; // all if (0 == arguments.length) { this._callbacks = {}; return this; } // specific event var callbacks = this._callbacks[event]; if (!callbacks) return this; // remove all handlers if (1 == arguments.length) { delete this._callbacks[event]; return this; } // remove specific handler var i = index(callbacks, fn._off || fn); if (~i) callbacks.splice(i, 1); return this; }; /** * Emit `event` with the given args. * * @param {String} event * @param {Mixed} ... * @return {Emitter} */ Emitter.prototype.emit = function(event){ this._callbacks = this._callbacks || {}; var args = [].slice.call(arguments, 1) , callbacks = this._callbacks[event]; if (callbacks) { callbacks = callbacks.slice(0); for (var i = 0, len = callbacks.length; i < len; ++i) { callbacks[i].apply(this, args); } } return this; }; /** * Return array of callbacks for `event`. * * @param {String} event * @return {Array} * @api public */ Emitter.prototype.listeners = function(event){ this._callbacks = this._callbacks || {}; return this._callbacks[event] || []; }; /** * Check if this emitter has `event` handlers. * * @param {String} event * @return {Boolean} * @api public */ Emitter.prototype.hasListeners = function(event){ return !! this.listeners(event).length; }; }); require.register("ForbesLindesay-is-browser/client.js", function(exports, require, module){ module.exports = true; }); require.register("modella/index.js", function(exports, require, module){ /** * Module dependencies */ var modella = module.exports = require('./lib/model'); modella.utils = {}; modella.utils.clone = require('./lib/utils/clone'); modella.utils.type = require('./lib/utils/type'); }); require.register("modella/lib/model.js", function(exports, require, module){ /** * Module dependendencies */ var proto = require('./proto'); var statics = require('./static'); var clone = require('./utils/clone'); try { var Emitter = require('emitter'); } catch(e) { var Emitter = require('emitter-component'); } /** * Expose `createModel`. */ module.exports = createModel; /** * Create a new model constructor with the given `name`. * * @param {String} name * @return {Function} * @api public */ function createModel(name) { if ('string' !== typeof name) { throw new TypeError('model name required'); } /** * Initialize a new model with the given `attrs`. * * @param {Object} attrs * @api public */ function Model(attrs) { if (!(this instanceof Model)) { return new Model(attrs); } if(Array.isArray(attrs)) { var result = []; for(var i = 0; i < attrs.length; ++i) { result.push(new Model(attrs[i])); } return result; } attrs = (attrs) ? clone(attrs) : {}; this.attrs = {}; this._callbacks = {}; this.dirty = {}; this.errors = []; Model.emit('initializing', this, attrs); // Set the default values for (var attr in Model._defaults) { if (Model.attrs[attr]) { this.attrs[attr] = clone(Model._defaults[attr]); } } // Only add attributes that are in the model schema for (var attr in attrs) { if (Model.attrs[attr]) { this.attrs[attr] = attrs[attr]; } } Model.emit('initialize', this); } // mixin `Emitter` Emitter(Model); // Statics Model.modelName = name; Model.base = '/' + name.toLowerCase(); Model.attrs = {}; Model.validators = []; Model._defaults = {}; for (var key in statics) { Model[key] = statics[key]; } // Prototype Model.prototype = {}; Model.prototype.model = Model; for (var key in proto) { Model.prototype[key] = proto[key]; } return Model; } }); require.register("modella/lib/proto.js", function(exports, require, module){ /** * Module dependencies. */ var clone = require('./utils/clone'); var noop = function(){}; try { var Emitter = require('emitter'); } catch(e) { var Emitter = require('emitter-component'); } /** * Mixin emitter. */ Emitter(exports); /** * Register an error `msg` on `attr`. * * @param {String} attr * @param {String} msg * @return {Object} self * @api public */ exports.error = function(attr, msg){ this.errors.push({ attr: attr, message: msg }); return this; }; /** * Check if this model is new. * * @return {Boolean} * @api public */ exports.isNew = function(){ var key = this.model.primaryKey; return ! this.has(key); }; /** * Get / set the primary key. * * @param {Mixed} val * @return {Mixed} * @api public */ exports.primary = function(val){ var key = this.model.primaryKey; if (0 == arguments.length) return this[key](); return this[key](val); }; /** * Validate the model and return a boolean. * * Example: * * user.isValid() * // => false * * user.errors * // => [{ attr: ..., message: ... }] * * @return {Boolean} * @api public */ exports.isValid = function(){ this.validate(); return 0 == this.errors.length; }; /** * Return `false` or an object * containing the "dirty" attributes. * * Optionally check for a specific `attr`. * * @param {String} [attr] * @return {Object|Boolean} * @api public */ exports.changed = function(attr){ if (1 == arguments.length) { return !! this.dirty[attr]; } var ret = {}; for (var attr in this.dirty) { ret[attr] = this.dirty[attr]; } return ret; }; /** * Perform validations. * * @api private */ exports.validate = function() { var fns = this.model.validators || []; this.errors = []; for (var i = 0, len = fns.length; i < len; i++) fns[i](this); if(this.errors.length) { this.model.emit('invalid', this, this.errors); this.emit('invalid', this.errors); } else { this.model.emit('valid', this, null); this.emit('valid', null); } }; /** * Destroy the model and mark it as `.removed` * and invoke `fn(err)`. * * Events: * * - `removing` before deletion * - `remove` on deletion * * @param {Function} [fn] * @api public */ exports.remove = function(){ var args = [].slice.call(arguments), fn = args.pop() || noop, self = this; var remove = this.model.remove; // Backwards-compatibiity with _sync if (!this.model.remove && this.model._sync) { remove = this.model._sync.remove; } if (this.isNew()) { return fn(new Error('not saved')); } this.run('removing', function() { remove.apply(self, args.concat(res)); }); function res(err, body) { if(err) { self.emit('error', err); return fn(err); } self.removed = true; self.model.emit('remove', self); self.emit('remove'); fn(null, self); } }; /** * Save and invoke `fn(err)`. * * Events: * * - `save` on updates and saves * - `saving` pre-update or save, after validation * * @param {Function} [fn] * @api public */ exports.save = function(){ var args = [].slice.call(arguments), fn = args.pop() || noop, operation = 'save', self = this; var isNew = this.isNew(); if (!isNew) { var changed = this.changed(); operation = 'update'; if(!changed) return fn(null, this); } var save = this.model[operation]; // Backwards-compatibiity with _sync if (!this.model[operation] && this.model._sync) { save = this.model._sync[operation]; } if (!this.isValid()) { this.primary(null); return fn(new Error('validation failed')); } this.run('saving', function(err) { if (err) return fn(err); if(isNew) { self.run('creating', function(err) { if (err) return fn(err); save.apply(self, args.concat(res)); }); } else { save.apply(self, args.concat(res)); } }); function res(err, body) { if (err) { self.emit('error', err); return fn(err); } if (body) { self.primary(body.id || body._id); for(var attr in self.model.attrs) { self.attrs[attr] = body[attr]; } } self.dirty = {}; if(isNew) { self.model.emit('create', self); self.emit('create'); } self.model.emit('save', self); self.emit('save'); fn(null, self); } }; /** * Set multiple `attrs`. * * @param {Object} attrs * @return {Object} self * @api public */ exports.set = function(attrs){ this.model.emit('setting', this, attrs); this.emit('setting', attrs); for (var key in attrs) { if(this.model.attrs[key]) { this[key](attrs[key]); } } return this; }; /** * Get `attr` value. * * @param {String} attr * @return {Mixed} * @api public */ exports.get = function(attr){ return this.attrs[attr]; }; /** * Check if `attr` is present (not `null` or `undefined`). * * @param {String} attr * @return {Boolean} * @api public */ exports.has = function(attr){ return undefined !== this.attrs[attr]; }; /** * Return the JSON representation of the model. * * @return {Object} * @api public */ exports.toJSON = function(){ return clone(this.attrs); }; /** * Run functions beforehand * * @param {String} event * @param {Function} fn * @api private */ exports.run = function(ev, done) { var modelFns = this.model.listeners(ev), fns = this.listeners(ev); var self = this; self.errors = []; function next(err) { if(err) return done(err); if (ev === 'saving' && (self.errors.length || !self.isValid())) { return done(new Error('validation failed')); } if(modelFns.length) { modelFns.shift().call(self, self, next); } else if(fns.length) { fns.shift().call(self, next); } else { done(err); } } next(); }; }); require.register("modella/lib/static.js", function(exports, require, module){ /** * Module dependencies. */ var clone = require('./utils/clone'), type = require('./utils/type'), isBrowser = require('is-browser'), noop = function(){}; /** * Add validation `fn()`. * * @param {Function} fn * @return {Function} self * @api public */ exports.validate = function(fn){ this.validators.push(fn); return this; }; /** * Use the given plugin `fn()`. If `env` is specified, only use the plugin on the appropriate client. * * @param {String} [env] * @param {Function} fn * @return {Function} self * @api public */ exports.use = function(env, fn){ if(!fn) { fn = env; env = undefined; } if(!env) { fn(this); } else { var rbrowser = /browser|client/, rserver = /server|node|node.js/; if(isBrowser && rbrowser.test(env)) fn(this); else if(!isBrowser && rserver.test(env)) fn(this); } return this; }; /** * Define attr with the given `name` and `options`. * * @param {String} name * @param {Object} options * @return {Function} self * @api public */ exports.attr = function(name, options){ if(!this.attrs[name]) { this.attrs[name] = options || {}; // implied pk if ('_id' === name || 'id' === name) { this.attrs[name].primaryKey = true; this.primaryKey = name; } // getter / setter method this.prototype[name] = function(val){ if (0 === arguments.length) { return clone(this.attrs[name]); } var prev = this.attrs[name]; val = clone(val); // Check if it actually changed var changed = false, newType = type(val); if(newType == 'object' || newType == 'array') { changed = true; } else changed = this.attrs[name] != val; if(changed) { this.dirty[name] = val; this.attrs[name] = val; this.model.emit('change', this, name, val, prev); this.model.emit('change ' + name, this, val, prev); this.emit('change', name, val, prev); this.emit('change ' + name, val, prev); } return this; }; } else { // Copy in the options if it already exists for(var key in options) { this.attrs[name][key] = options[key]; } } // Check for defaultValue if (options && options.defaultValue !== undefined) { this._defaults[name] = options.defaultValue; } this.emit('attr', name, options); return this; }; }); require.register("modella/lib/utils/clone.js", function(exports, require, module){ /** * TODO: cleanup. This is a pretty big hack. inlined `clone` because * it cannot handle objectid instances */ /** * Module dependencies */ var type = require('./type'); /** * Expose `clone` */ module.exports = clone; /** * Clone values. * * @param {Mixed} val * @return {Mixed} * @api public */ function clone(obj) { switch (type(obj)) { case 'object': // Hack for BSON IDs if(obj.toHexString) return obj; var copy = {}; for (var key in obj) { copy[key] = clone(obj[key]); } return copy; case 'array': var copy = new Array(obj.length); for (var i = 0, l = obj.length; i < l; i++) { copy[i] = clone(obj[i]); } return copy; case 'regexp': // from millermedeiros/amd-utils - MIT var flags = ''; flags += obj.multiline ? 'm' : ''; flags += obj.global ? 'g' : ''; flags += obj.ignoreCase ? 'i' : ''; return new RegExp(obj.source, flags); case 'date': return new Date(obj.getTime()); default: // string, number, boolean, … return obj; } } }); require.register("modella/lib/utils/type.js", function(exports, require, module){ /** * https://github.com/component/type * * TODO: un-bundle, once we have a way to use component and node together */ /** * toString ref. */ var toString = Object.prototype.toString; /** * Return the type of `val`. * * @param {Mixed} val * @return {String} * @api public */ module.exports = function(val){ switch (toString.call(val)) { case '[object Function]': return 'function'; case '[object Date]': return 'date'; case '[object RegExp]': return 'regexp'; case '[object Arguments]': return 'arguments'; case '[object Array]': return 'array'; } if (val === null) return 'null'; if (val === undefined) return 'undefined'; if (val === Object(val)) return 'object'; return typeof val; }; }); require.alias("component-emitter/index.js", "modella/deps/emitter/index.js"); require.alias("component-emitter/index.js", "emitter/index.js"); require.alias("component-indexof/index.js", "component-emitter/deps/indexof/index.js"); require.alias("ForbesLindesay-is-browser/client.js", "modella/deps/is-browser/client.js"); require.alias("ForbesLindesay-is-browser/client.js", "modella/deps/is-browser/index.js"); require.alias("ForbesLindesay-is-browser/client.js", "is-browser/index.js"); require.alias("ForbesLindesay-is-browser/client.js", "ForbesLindesay-is-browser/index.js");if (typeof exports == "object") { module.exports = require("modella"); } else if (typeof define == "function" && define.amd) { define(function(){ return require("modella"); }); } else { this["modella"] = require("modella"); }})();