UNPKG

document.js

Version:

General purpose ODM supporting memory and mongo database adapters

323 lines (259 loc) 8.33 kB
require('progenitor.js')(); var extend = require('extend'), inflect = require('i')(), adapterCache = { }, noop = function() { }; Document = Object.progeny('Document', { init: function(options, relation) { this._data = {}; this._changes = {}; assignOptions.call(this, options); this.relation = relation; this.RSVP = { success: noop, error: noop }; this.loaded = false; }, isValid: function() { var i, name, valid = true, validate = this.class.validate || {}, presence = validate.presence || [], format = validate.format || {}, formatNames = Object.keys(format), custom = validate.custom || []; for(i=0; i<presence.length; ++i) { if(! this.get(presence[i])) { valid = false; break; } } if(!valid) return false; for(i=0; i<formatNames.length; ++i) { name = formatNames[i]; if(!format[name].test(this.get(name))) { valid = false; break; } } for(i=0; i<custom.length; ++i) { if(!custom[i].call(this)) { valid = false; break; } } return valid; }, get: function(name) { if(name == 'id') return this.id; if(this.class.belongsTo.indexOf(name) >= 0) { var id = this.get(inflect.foreign_key(name)); return id && Document[inflect.classify(name)].find(id); } else if(this.class.namedFields[name]) { return (name in this._data) ? this._data[name] : null; } else if(typeof this[name] === 'function') { return this[name].call(this); } }, set: function() { var name, from, to, options = arguments[0], keyValue = arguments.length == 2, result = {}; if(!arguments[0]) return; if(keyValue) { options = {}; options[arguments[0]] = arguments[1]; } for(name in options) { var belongsTo = this.class.belongsTo.indexOf(name) >= 0; if(belongsTo) { var key = inflect.foreign_key(name); options[key] = options[name].id; name = key; } if(belongsTo || this.class.namedFields[name]) { to = typeof options[name] === 'undefined' ? null : options[name]; if((from = this.get(name)) != to) { this._changes[name] = [from, to]; } if(name === '_id') this.id = to; this._data[name] = result[name] = to; } } return (keyValue ? result[arguments[0]] : result); }, changedAttributes: function() { return extend({}, this._changes); }, save: function() { var that = this, changedAttrs = {}, notValid = !this.isValid(), noChanges = !Object.keys(this.changedAttributes()).length; if(notValid || (noChanges && this.persisted)) { this.loaded = true; return this; } this.loaded = false; this.relation = new Document.Relation(this.class, this); if(this.persisted) { Object.keys(this._changes).forEach(function(key) { changedAttrs[key] = that.get(key); }); return this.relation.update(changedAttrs); } else { (this.class.beforeCreate || []).forEach(function(fn) { fn.call(that); }); return this.relation.create(extend({}, this._data)); } }, update: function() { this.set.apply(this, arguments); return this.save(); }, destroy: function() { var that = this; this.loaded = false; this.class.adapter().remove({_id: this.id}, function(value) { that.loaded = true; that.kept(that.isDestroyed = !!value); }); return this; }, kept: function(options) { this.set(options); this.loaded = true; if(this.RSVP.wasKept = !!options) { this._changes = {}; this.persisted = true; this.RSVP.success(this); } else { this.RSVP.error(); } }, then: function(success, error) { this.RSVP.success = success || noop; this.RSVP.error = error || noop; if(this.loaded) { if(this.RSVP.wasKept) { this.RSVP.success(this); } else { this.RSVP.error(); } } }, asJSON: function() { var name, json = { id: this.id.toString(), createdAt: this.get('createdAt'), updatedAt: this.get('updatedAt') }, allow = this.class.allow || []; for(var i = 0; i < allow.length; ++i) { name = allow[i]; if(/(.+)_id$/.test(name)) { var value = this.get(name), shortName = name.slice(0,-3); value = value ? value.toString() : value; json[shortName] || (json[shortName] = { id: value }); } else { value = this.get(name); value = typeof (value && value.asJSON) === 'function' ? value.asJSON() : value; } json[name] = value; } return json; } }, { classMethods: { namedFields: null, shortFields: null, defaultFields: { ObjectID: { _id: '_id' }, Date: { createdAt: 'cT', updatedAt: 'uT' } }, adapter: function() { return adapterCache[this.className] || (adapterCache[this.className] = new Document.Adapter(this)); }, inherited: function(base) { var fields; this[base.className] = base; base.fields || (base.fields = {}); base.belongsTo || (base.belongsTo = []); base.fields.ObjectID = extend({}, this.defaultFields.ObjectID, base.fields.ObjectID); base.fields.Date = extend({}, this.defaultFields.Date, base.fields.Date); base.namedFields = {}; base.shortFields = {}; base.belongsTo.forEach(function(name) { base.fields.ObjectID[inflect.foreign_key(name)] = name.slice(0,1)+'_id'; }); for(var type in base.fields) { for(var attr in (fields = base.fields[type])) { base.namedFields[attr] = fields[attr]; base.shortFields[fields[attr]] = attr; } } }, find: function(id) { var model; if(id && (typeof id == 'string' || id._bsontype === 'ObjectID')) { this.loaded = false; id = Document.Adapter.ids.isValid(id) ? Document.Adapter.ids.next(id) : id; var relation = new Document.Relation(this, model = new this({_id: id})); return (model.relation = relation).find(); } else { return new Document.Relation(this).find(id); } }, count: function() { return new Document.Relation(this).count() }, first: function() { return adapterDirection.call(this, 'first') }, last: function() { return adapterDirection.call(this, 'last') }, shortToLong: function(opts) { return _translateFields.call(this, this.shortFields, opts); }, longToShort: function(opts) { return _translateFields.call(this, this.namedFields, opts); } } }); function assignOptions(options) { var id = options.id || options._id; this.persisted = false; if(id) { if(Document.Adapter.ids.isValid(id)) { this.id = Document.Adapter.ids.next(id); } else if(id._bsontype === 'ObjectID') { this.id = id; } else { this.id = id.toString(); } } else if(id === null) { this.id = null; } else { this.id = Document.Adapter.ids.next(); } this.set(extend({_id: this.id }, optionsWithout(options, ['id', '_id']))); this._changes = {}; } function adapterDirection(name) { // implements .first and .last var model = new this({_id: null}); this.adapter()[name].call(this.adapter(), function(options) { options = model.class.shortToLong(options); model.kept(options); }); return model; } function optionsWithout(options, items) { var other = {}; for(var o in options) { if(items.indexOf(o) == -1) { other[o] = options[o]; } } return other; } function _translateFields(fields, opts) { if(!opts || typeof opts !== 'object') return opts; var options = {}; Object.keys(opts).forEach(function(key) { options[fields[key] || key] = opts[key]; }); return options; } extend(Document, { Relation: require('./relation'), Count: require('./count') }); module.exports = function(options) { options = (options || {}); options.store || (options.store = 'memory'); if('memory mongo'.split(' ').indexOf(options.store) == -1) { throw new Error('Only the `memory` and `mongo` stores are available.'); } require('./memory_adapter'); require('./mongo_adapter'); Document.Adapter = require('./'+options.store+'_adapter'); return Document; };