UNPKG

orm

Version:

NodeJS Object-relational mapping

788 lines (679 loc) 20.8 kB
var Utilities = require("./Utilities"); var Property = require("./Property"); var Hook = require("./Hook"); var enforce = require("enforce"); var Promise = require("bluebird"); exports.Instance = Instance; var INSTNCE_METHOD_NAMES = ["save", "remove", "validate"]; function Instance(Model, opts) { opts = opts || {}; opts.data = opts.data || {}; opts.extra = opts.extra || {}; opts.keys = opts.keys || "id"; opts.changes = (opts.is_new ? Object.keys(opts.data) : []); opts.extrachanges = []; opts.associations = {}; opts.originalKeyValues = {}; var promiseFunctionPostfix = Model.settings.get('promiseFunctionPostfix'); var instance_saving = false; var events = {}; var instance = {}; var emitEvent = function () { var args = Array.prototype.slice.apply(arguments); var event = args.shift(); if (!events.hasOwnProperty(event)) return; events[event].map(function (cb) { cb.apply(instance, args); }); }; var rememberKeys = function () { var i, prop; for(i = 0; i < opts.keyProperties.length; i++) { prop = opts.keyProperties[i]; opts.originalKeyValues[prop.name] = opts.data[prop.name]; } }; var shouldSaveAssocs = function (saveOptions) { if (Model.settings.get("instance.saveAssociationsByDefault")) { return saveOptions.saveAssociations !== false; } else { return !!saveOptions.saveAssociations; } }; var handleValidations = function (cb) { var pending = [], errors = [], required, alwaysValidate; Hook.wait(instance, opts.hooks.beforeValidation, function (err) { var k, i; if (err) { return saveError(cb, err); } var checks = new enforce.Enforce({ returnAllErrors : Model.settings.get("instance.returnAllErrors") }); for (k in opts.validations) { required = false; if (Model.allProperties[k]) { required = Model.allProperties[k].required; alwaysValidate = Model.allProperties[k].alwaysValidate; } else { for (i = 0; i < opts.one_associations.length; i++) { if (opts.one_associations[i].field === k) { required = opts.one_associations[i].required; break; } } } if (!alwaysValidate && !required && instance[k] == null) { continue; // avoid validating if property is not required and is "empty" } for (i = 0; i < opts.validations[k].length; i++) { checks.add(k, opts.validations[k][i]); } } checks.context("instance", instance); checks.context("model", Model); checks.context("driver", opts.driver); return checks.check(instance, cb); }); }; var saveError = function (cb, err) { instance_saving = false; emitEvent("save", err, instance); Hook.trigger(instance, opts.hooks.afterSave, false); if (typeof cb === "function") { cb(err, instance); } }; var saveInstance = function (saveOptions, cb) { // what this condition means: // - If the instance is in state mode // - AND it's not an association that is asking it to save // -> return has already saved if (instance_saving && saveOptions.saveAssociations !== false) { return cb(null, instance); } instance_saving = true; handleValidations(function (err) { if (err) { return saveError(cb, err); } if (opts.is_new) { waitHooks([ "beforeCreate", "beforeSave" ], function (err) { if (err) { return saveError(cb, err); } return saveNew(saveOptions, getInstanceData(), cb); }); } else { waitHooks([ "beforeSave" ], function (err) { if (err) { return saveError(cb, err); } return savePersisted(saveOptions, getInstanceData(), cb); }); } }); }; var runAfterSaveActions = function (cb, create, err) { instance_saving = false; emitEvent("save", err, instance); if (create) { Hook.trigger(instance, opts.hooks.afterCreate, !err); } Hook.trigger(instance, opts.hooks.afterSave, !err); cb(); }; var getInstanceData = function () { var data = {}, prop; for (var k in opts.data) { if (!opts.data.hasOwnProperty(k)) continue; prop = Model.allProperties[k]; if (prop) { /* if (opts.data[k] == null && (prop.type == 'serial' || typeof prop.defaultValue == 'function')) { continue; } */ if (opts.driver.propertyToValue) { data[k] = opts.driver.propertyToValue(opts.data[k], prop); } else { data[k] = opts.data[k]; } } else { data[k] = opts.data[k]; } } return data; }; var waitHooks = function (hooks, next) { var nextHook = function () { if (hooks.length === 0) { return next(); } Hook.wait(instance, opts.hooks[hooks.shift()], function (err) { if (err) { return next(err); } return nextHook(); }); }; return nextHook(); }; var saveNew = function (saveOptions, data, cb) { var i, prop; var finish = function (err) { runAfterSaveActions(function () { if (err) return cb(err); saveInstanceExtra(cb); }, true); } data = Utilities.transformPropertyNames(data, Model.allProperties); opts.driver.insert(opts.table, data, opts.keyProperties, function (save_err, info) { if (save_err) { return saveError(cb, save_err); } opts.changes.length = 0; for (i = 0; i < opts.keyProperties.length; i++) { prop = opts.keyProperties[i]; opts.data[prop.name] = info.hasOwnProperty(prop.name) ? info[prop.name] : data[prop.name]; } opts.is_new = false; rememberKeys(); if (!shouldSaveAssocs(saveOptions)) { return finish(); } return saveAssociations(finish); }); }; var savePersisted = function (saveOptions, data, cb) { var changes = {}, conditions = {}, i, prop; var next = function (saved) { var finish = function () { saveInstanceExtra(cb); } if(!saved && !shouldSaveAssocs(saveOptions)) { finish(); } else { if (!shouldSaveAssocs(saveOptions)) { runAfterSaveActions(function () { finish(); }, false); } else { saveAssociations(function (err, assocSaved) { if (saved || assocSaved) { runAfterSaveActions(function () { if (err) return cb(err); finish(); }, false, err); } else { finish(); } }); } } } if (opts.changes.length === 0) { next(false); } else { for (i = 0; i < opts.changes.length; i++) { changes[opts.changes[i]] = data[opts.changes[i]]; } for (i = 0; i < opts.keyProperties.length; i++) { prop = opts.keyProperties[i]; conditions[prop.mapsTo] = opts.originalKeyValues[prop.name]; } changes = Utilities.transformPropertyNames(changes, Model.allProperties); opts.driver.update(opts.table, changes, conditions, function (err) { if (err) { return saveError(cb, err); } opts.changes.length = 0; rememberKeys(); next(true); }); } }; var saveAssociations = function (cb) { var pending = 1, errored = false, i, j; var saveAssociation = function (accessor, instances) { pending += 1; instance[accessor](instances, function (err) { if (err) { if (errored) return; errored = true; return cb(err, true); } if (--pending === 0) { return cb(null, true); } }); }; var _saveOneAssociation = function (assoc) { if (!instance[assoc.name] || typeof instance[assoc.name] !== "object") return; if (assoc.reversed) { // reversed hasOne associations should behave like hasMany if (!Array.isArray(instance[assoc.name])) { instance[assoc.name] = [ instance[assoc.name] ]; } for (var i = 0; i < instance[assoc.name].length; i++) { if (!instance[assoc.name][i].isInstance) { instance[assoc.name][i] = new assoc.model(instance[assoc.name][i]); } saveAssociation(assoc.setAccessor, instance[assoc.name][i]); } return; } if (!instance[assoc.name].isInstance) { instance[assoc.name] = new assoc.model(instance[assoc.name]); } saveAssociation(assoc.setAccessor, instance[assoc.name]); }; for (i = 0; i < opts.one_associations.length; i++) { _saveOneAssociation(opts.one_associations[i]); } var _saveManyAssociation = function (assoc) { var assocVal = instance[assoc.name]; if (!Array.isArray(assocVal)) return; if (!opts.associations[assoc.name].changed) return; for (j = 0; j < assocVal.length; j++) { if (!assocVal[j].isInstance) { assocVal[j] = new assoc.model(assocVal[j]); } } saveAssociation(assoc.setAccessor, assocVal); }; for (i = 0; i < opts.many_associations.length; i++) { _saveManyAssociation(opts.many_associations[i]); } if (--pending === 0) { return cb(null, false); } }; var saveInstanceExtra = function (cb) { if (opts.extrachanges.length === 0) { if (cb) return cb(null, instance); else return; } var data = {}; var conditions = {}; for (var i = 0; i < opts.extrachanges.length; i++) { if (!opts.data.hasOwnProperty(opts.extrachanges[i])) continue; if (opts.extra[opts.extrachanges[i]]) { data[opts.extrachanges[i]] = opts.data[opts.extrachanges[i]]; if (opts.driver.propertyToValue) { data[opts.extrachanges[i]] = opts.driver.propertyToValue(data[opts.extrachanges[i]], opts.extra[opts.extrachanges[i]]); } } else { data[opts.extrachanges[i]] = opts.data[opts.extrachanges[i]]; } } for (i = 0; i < opts.extra_info.id.length; i++) { conditions[opts.extra_info.id_prop[i]] = opts.extra_info.id[i]; conditions[opts.extra_info.assoc_prop[i]] = opts.data[opts.keys[i]]; } opts.driver.update(opts.extra_info.table, data, conditions, function (err) { return cb(err); }); }; var removeInstance = function (cb) { if (opts.is_new) { return cb(null); } var conditions = {}; for (var i = 0; i < opts.keys.length; i++) { conditions[opts.keys[i]] = opts.data[opts.keys[i]]; } Hook.wait(instance, opts.hooks.beforeRemove, function (err) { if (err) { emitEvent("remove", err, instance); if (typeof cb === "function") { cb(err, instance); } return; } emitEvent("beforeRemove", instance); opts.driver.remove(opts.table, conditions, function (err, data) { Hook.trigger(instance, opts.hooks.afterRemove, !err); emitEvent("remove", err, instance); if (typeof cb === "function") { cb(err, instance); } instance = undefined; }); }); }; var saveInstanceProperty = function (key, value) { var changes = {}, conditions = {}; changes[key] = value; if (Model.properties[key]) { if (opts.driver.propertyToValue) { changes[key] = opts.driver.propertyToValue(changes[key], Model.properties[key]); } } for (var i = 0; i < opts.keys.length; i++) { conditions[opts.keys[i]] = opts.data[opts.keys[i]]; } Hook.wait(instance, opts.hooks.beforeSave, function (err) { if (err) { Hook.trigger(instance, opts.hooks.afterSave, false); emitEvent("save", err, instance); return; } opts.driver.update(opts.table, changes, conditions, function (err) { if (!err) { opts.data[key] = value; } Hook.trigger(instance, opts.hooks.afterSave, !err); emitEvent("save", err, instance); }); }); }; var setInstanceProperty = function (key, value) { var prop = Model.allProperties[key] || opts.extra[key]; if (prop) { if ('valueToProperty' in opts.driver) { value = opts.driver.valueToProperty(value, prop); } if (opts.data[key] !== value) { opts.data[key] = value; return true; } } return false; } // ('data.a.b', 5) => opts.data.a.b = 5 var setPropertyByPath = function (path, value) { if (typeof path == 'string') { path = path.split('.'); } else if (!Array.isArray(path)) { return; } var propName = path.shift(); var prop = Model.allProperties[propName] || opts.extra[propName]; var currKey, currObj; if (!prop) { return; } if (path.length == 0) { instance[propName] = value; return; } currObj = instance[propName]; while(currObj && path.length > 0 ) { currKey = path.shift(); if (path.length > 0) { currObj = currObj[currKey]; } else if (currObj[currKey] !== value) { currObj[currKey] = value; opts.changes.push(propName); } } } var addInstanceProperty = function (key) { var defaultValue = undefined; var prop = Model.allProperties[key]; // This code was first added, and then commented out in a later commit. // Its presence doesn't affect tests, so I'm just gonna log if it ever gets called. // If someone complains about noise, we know it does something, and figure it out then. if (instance.hasOwnProperty(key)) console.log("Overwriting instance property"); if (key in opts.data) { defaultValue = opts.data[key]; } else if (prop && 'defaultValue' in prop) { defaultValue = prop.defaultValue; } setInstanceProperty(key, defaultValue); Object.defineProperty(instance, key, { get: function () { var val = opts.data[key]; if (val === undefined) { return null; } else { return opts.data[key]; } }, set: function (val) { if (prop.key === true) { if (prop.type == 'serial' && opts.data[key] != null) { return; } else { opts.originalKeyValues[prop.name] = opts.data[prop.name]; } } if (!setInstanceProperty(key, val)) { return; } if (opts.autoSave) { saveInstanceProperty(key, val); } else if (opts.changes.indexOf(key) === -1) { opts.changes.push(key); } }, enumerable: !(prop && !prop.enumerable) }); }; var addInstanceExtraProperty = function (key) { if (!instance.hasOwnProperty("extra")) { instance.extra = {}; } Object.defineProperty(instance.extra, key, { get: function () { return opts.data[key]; }, set: function (val) { setInstanceProperty(key, val); /*if (opts.autoSave) { saveInstanceProperty(key, val); }*/if (opts.extrachanges.indexOf(key) === -1) { opts.extrachanges.push(key); } }, enumerable: true }); }; var i, k; for (k in Model.allProperties) { addInstanceProperty(k); } for (k in opts.extra) { addInstanceProperty(k); } for (k in opts.methods) { Object.defineProperty(instance, k, { value : opts.methods[k].bind(instance), enumerable : false, writable : true }); } for (k in opts.extra) { addInstanceExtraProperty(k); } Object.defineProperty(instance, "on", { value: function (event, cb) { if (!events.hasOwnProperty(event)) { events[event] = []; } events[event].push(cb); return this; }, enumerable: false, writable: true }); Object.defineProperty(instance, "save", { value: function () { var arg = null, objCount = 0; var data = {}, saveOptions = {}, cb = null; while (arguments.length > 0) { arg = Array.prototype.shift.call(arguments); switch (typeof arg) { case 'object': switch (objCount) { case 0: data = arg; break; case 1: saveOptions = arg; break; } objCount++; break; case 'function': cb = arg; break; default: var err = new Error("Unknown parameter type '" + (typeof arg) + "' in Instance.save()"); err.model = Model.table; throw err; } } for (var k in data) { if (data.hasOwnProperty(k)) { this[k] = data[k]; } } saveInstance(saveOptions, function (err) { if (!cb) return; if (err) return cb(err); return cb(null, instance); }); return this; }, enumerable: false, writable: true }); Object.defineProperty(instance, "saved", { value: function () { return opts.changes.length === 0; }, enumerable: false, writable: true }); Object.defineProperty(instance, "remove", { value: function (cb) { removeInstance(cb); return this; }, enumerable: false, writable: true }); Object.defineProperty(instance, "set", { value: setPropertyByPath, enumerable: false, writable: true }); Object.defineProperty(instance, "markAsDirty", { value: function (propName) { if (propName != undefined) { opts.changes.push(propName); } }, enumerable: false, writable: true }); Object.defineProperty(instance, "dirtyProperties", { get: function () { return opts.changes; }, enumerable: false }); Object.defineProperty(instance, "isDirty", { value: function () { return opts.changes.length > 0; }, enumerable: false }); Object.defineProperty(instance, "isInstance", { value: true, enumerable: false }); Object.defineProperty(instance, "isPersisted", { value: function () { return !opts.is_new; }, enumerable: false, writable: true }); Object.defineProperty(instance, "isShell", { value: function () { return opts.isShell; }, enumerable: false }); Object.defineProperty(instance, "validate", { value: function (cb) { handleValidations(function (errors) { cb(null, errors || false); }); }, enumerable: false, writable: true }); Object.defineProperty(instance, "__singleton_uid", { value: function (cb) { return opts.uid; }, enumerable: false }); Object.defineProperty(instance, "__opts", { value: opts, enumerable: false }); Object.defineProperty(instance, "model", { value: function (cb) { return Model; }, enumerable: false }); for (var j = 0; j < INSTNCE_METHOD_NAMES.length; j++) { var name = INSTNCE_METHOD_NAMES[j]; Object.defineProperty(instance, name + promiseFunctionPostfix, { value: Promise.promisify(instance[name]), enumerable: false, writable: true }); } for (i = 0; i < opts.keyProperties.length; i++) { var prop = opts.keyProperties[i]; if (!(prop.name in opts.data)) { opts.changes = Object.keys(opts.data); break; } } rememberKeys(); opts.setupAssociations(instance); for (i = 0; i < opts.one_associations.length; i++) { var asc = opts.one_associations[i]; if (!asc.reversed && !asc.extension) { for (k in asc.field) { if (!instance.hasOwnProperty(k)) { addInstanceProperty(k); } } } if (asc.name in opts.data) { var d = opts.data[asc.name]; var mapper = function (obj) { return obj.isInstance ? obj : new asc.model(obj); }; if (Array.isArray(d)) { instance[asc.name] = d.map(mapper); } else { instance[asc.name] = mapper(d); } delete opts.data[asc.name]; } } for (i = 0; i < opts.many_associations.length; i++) { var aName = opts.many_associations[i].name; opts.associations[aName] = { changed: false, data: opts.many_associations[i] }; if (Array.isArray(opts.data[aName])) { instance[aName] = opts.data[aName]; delete opts.data[aName]; } } Hook.wait(instance, opts.hooks.afterLoad, function (err) { process.nextTick(function () { emitEvent("ready", err); }); }); return instance; }