UNPKG

patio

Version:
676 lines (634 loc) 24.3 kB
var comb = require("comb"), asyncArray = comb.async.array, when = comb.when, isBoolean = comb.isBoolean, isArray = comb.isArray, isHash = comb.isHash, isUndefined = comb.isUndefined, isInstanceOf = comb.isInstanceOf, isEmpty = comb.isEmpty, Dataset = require("../dataset"), ModelError = require("../errors").ModelError, Promise = comb.Promise, PromiseList = comb.PromiseList; var QueryPlugin = comb.define(null, { instance: { /**@lends patio.Model.prototype*/ _getPrimaryKeyQuery: function () { var q = {}, pk = this.primaryKey; for (var i = 0, l = pk.length; i < l; i++) { var k = pk[i]; q[k] = this[k]; } return q; }, _clearPrimaryKeys: function () { var pk = this.primaryKey; for (var i = 0, l = pk.length; i < l; i++) { this.__values[pk[i]] = null; } }, __callHooks: function (event, options, cb) { var self = this; return this._hook("pre", event, options) .chain(function () { return cb(); }) .chain(function () { return self._hook("post", event, options); }); }, reload: function () { if (this.synced) { var self = this; return this .__callHooks("load", null, function () { return self.__reload(); }) .chain(function () { return self; }); } else { throw new ModelError("Model " + this.tableName + " has not been synced"); } }, /** * Forces the reload of the data for a particular model instance. The Promise returned is resolved with the * model. * * @example * * myModel.reload().chain(function(model){ * //model === myModel * }); * * @return {comb.Promise} resolved with the model instance. */ __reload: function () { if (!this.__isNew) { var self = this; return this.dataset.naked().filter(this._getPrimaryKeyQuery()).one().chain(function (values) { self.__setFromDb(values, true); return self; }); } else { return when(this); } }, /** * This method removes the instance of the model. If the model {@link patio.Model#isNew} then the promise is * resolved with a 0 indicating no rows were affected. Otherwise the model is removed, primary keys are cleared * and the model's isNew flag is set to true. * * @example * myModel.remove().chain(function(){ * //model is deleted * assert.isTrue(myModel.isNew); * }); * * //dont use a transaction to remove this model * myModel.remove({transaction : false}).chain(function(){ * //model is deleted * assert.isTrue(myModel.isNew); * }); * * @param {Object} [options] additional options. * @param {Boolean} [options.transaction] boolean indicating if a transaction should be used when * removing the model. * * @return {comb.Promise} called back after the deletion is successful. */ remove: function (options) { if (this.synced) { if (!this.__isNew) { var self = this; return this._checkTransaction(options, function () { return self.__callHooks("remove", [options], function () { return self._remove(options); }) .chain(function () { self._clearPrimaryKeys(); self.__isNew = true; if (self._static.emitOnRemove) { self.emit("remove", this); self._static.emit("remove", this); } }) .chain(function () { return self; }); }); } else { return when(0); } } else { throw new ModelError("Model " + this.tableName + " has not been synced"); } }, _remove: function () { return this.dataset.filter(this._getPrimaryKeyQuery()).remove(); }, /** * @private * Called after a save action to reload the model properties, * abstracted out so this can be overidden by sub classes */ _saveReload: function (options) { options || (options = {}); var reload = isBoolean(options.reload) ? options.reload : this._static.reloadOnSave; return reload ? this.__reload() : when(this); }, /** * @private * Called after an update action to reload the model properties, * abstracted out so this can be overidden by sub classes */ _updateReload: function (options) { options = options || {}; options || (options = {}); var reload = isBoolean(options.reload) ? options.reload : this._static.reloadOnUpdate; return reload ? this.__reload() : when(this); }, /** * Updates a model. This action checks if the model is not new and values have changed. * If the model is new then the {@link patio.Model#save} action is called. * * When updating a model you can pass values you want set as the first argument. * * {@code * * someModel.update({ * myVal1 : "newValue1", * myVal2 : "newValue2", * myVal3 : "newValue3" * }).chain(function(){ * //do something * }, errorHandler); * * } * * Or you can set values on the model directly * * {@code * * someModel.myVal1 = "newValue1"; * someModel.myVal2 = "newValue2"; * someModel.myVal3 = "newValue3"; * * //update model with current values * someModel.update().chain(function(){ * //do something * }); * * } * * Update also accepts an options object as the second argument allowing the overriding of default behavior. * * To override <code>useTransactions</code> you can set the <code>transaction</code> option. * * {@code * someModel.update(null, {transaction : false}); * } * * You can also override the <code>reloadOnUpdate</code> property by setting the <code>reload</code> option. * {@code * someModel.update(null, {reload : false}); * } * * @param {Object} [vals] optional values hash to set on the model before saving. * @param {Object} [options] additional options. * @param {Boolean} [options.transaction] boolean indicating if a transaction should be used when * updating the model. * @param {Boolean} [options.reload] boolean indicating if the model should be reloaded after the update. This will take * precedence over {@link patio.Model.reloadOnUpdate} * * @return {comb.Promise} resolved when the update action has completed. */ update: function (vals, options) { if (this.synced) { if (!this.__isNew) { var self = this; return this._checkTransaction(options, function () { if (isHash(vals)) { self.__set(vals); } var saveChange = !isEmpty(self.__changed); return self.__callHooks("update", [options], function () { return saveChange ? self._update(options) : null; }) .chain(function () { return self._updateReload(options); }) .chain(function () { if (self._static.emitOnUpdate) { self.emit("update", self); self._static.emit("update", self); } }) .chain(function () { return self; }); }); } else if (this.__isNew && this.__isChanged) { return this.save(vals, options); } else { return when(this); } } else { throw new ModelError("Model " + this.tableName + " has not been synced"); } }, _update: function (options) { var ret = this.dataset.filter(this._getPrimaryKeyQuery()).update(this.__changed); this.__changed = {}; return ret; }, /** * Saves a model. This action checks if the model is new and values have changed. * If the model is not new then the {@link patio.Model#update} action is called. * * When saving a model you can pass values you want set as the first argument. * * {@code * * someModel.save({ * myVal1 : "newValue1", * myVal2 : "newValue2", * myVal3 : "newValue3" * }).chain(function(){ * //do something * }, errorHandler); * * } * * Or you can set values on the model directly * * {@code * * someModel.myVal1 = "newValue1"; * someModel.myVal2 = "newValue2"; * someModel.myVal3 = "newValue3"; * * //update model with current values * someModel.save().chain(function(){ * //do something * }); * * } * * Save also accepts an options object as the second argument allowing the overriding of default behavior. * * To override <code>useTransactions</code> you can set the <code>transaction</code> option. * * {@code * someModel.save(null, {transaction : false}); * } * * You can also override the <code>reloadOnSave</code> property by setting the <code>reload</code> option. * {@code * someModel.save(null, {reload : false}); * } * * * @param {Object} [vals] optional values hash to set on the model before saving. * @param {Object} [options] additional options. * @param {Boolean} [options.transaction] boolean indicating if a transaction should be used when * saving the model. * @param {Boolean} [options.reload] boolean indicating if the model should be reloaded after the save. This will take * precedence over {@link patio.Model.reloadOnSave} * * @return {comb.Promise} resolved when the save action has completed. */ save: function (vals, options) { if (this.synced) { if (this.__isNew) { var self = this; return this._checkTransaction(options, function () { if (isHash(vals)) { self.__set(vals); } return self.__callHooks("save", [options], function () { return self._save(options); }) .chain(function () { return self._saveReload(options); }) .chain(function () { if (self._static.emitOnSave) { self.emit("save", self); self._static.emit("save", self); } }) .chain(function () { return self; }); }); } else { return this.update(vals, options); } } else { throw new ModelError("Model " + this.tableName + " has not been synced"); } }, _save: function (options) { var pk = this._static.primaryKey[0], self = this; return this.dataset.insert(this._toObject()).chain(function (id) { self.__ignore = true; if (id) { self[pk] = id; } self.__ignore = false; self.__isNew = false; self.__isChanged = false; return self; }); }, getUpdateSql: function () { return this.updateDataset.filter(this._getPrimaryKeyQuery()).updateSql(this.__changed); }, getInsertSql: function () { return this.insertDataset.insertSql(this._toObject()); }, getRemoveSql: function () { return this.removeDataset.filter(this._getPrimaryKeyQuery()).deleteSql; }, getters: { updateSql: function () { return this.getUpdateSql(); }, insertSql: function () { return this.getInsertSql(); }, removeSql: function () { return this.getRemoveSql(); }, deleteSql: function () { return this.removeSql; } } }, static: { /**@lends patio.Model*/ /** * Set to false to prevent the emitting on an event when a model is saved. * @default true */ emitOnSave: true, /** * Set to false to prevent the emitting on an event when a model is updated. * @default true */ emitOnUpdate: true, /** * Set to false to prevent the emitting on an event when a model is removed. * @default true */ emitOnRemove: true, /** * Retrieves a record by the primaryKey/s of a table. * * @example * * var User = patio.getModel("user"); * * User.findById(1).chain(function(userOne){ * * }); * * //assume the primary key is a compostie of first_name and last_name * User.findById(["greg", "yukon"]).chain(function(userOne){ * * }); * * * @param {*} id the primary key of the record to find. * * @return {comb.Promise} called back with the record or null if one is not found. */ findById: function (id) { var pk = this.primaryKey; pk = pk.length === 1 ? pk[0] : pk; var q = {}; if (isArray(id) && isArray(pk)) { if (id.length === pk.length) { pk.forEach(function (k, i) { q[k] = id[i]; }); } else { throw new ModelError("findById : ids length does not equal the primaryKeys length."); } } else { q[pk] = id; } return this.filter(q).one(); }, /** * Finds a single model according to the supplied filter. * See {@link patio.Dataset#filter} for filter options. * * * * @param id */ find: function (id) { return this.filter.apply(this, arguments).first(); }, /** * Finds a single model according to the supplied filter. * See {@link patio.Dataset#filter} for filter options. If the model * does not exist then a new one is created as passed back. * @param q */ findOrCreate: function (q) { var self = this; return this.find(q).chain(function (res) { return res || self.create(q); }); }, /** * Update multiple rows with a set of values. * * @example * var User = patio.getModel("user"); * * //BEGIN * //UPDATE `user` SET `password` = NULL WHERE (`last_accessed` <= '2011-01-27') * //COMMIT * User.update({password : null}, function(){ * return this.lastAccessed.lte(comb.date.add(new Date(), "year", -1)); * }); * //same as * User.update({password : null}, {lastAccess : {lte : comb.date.add(new Date(), "year", -1)}}); * * //UPDATE `user` SET `password` = NULL WHERE (`last_accessed` <= '2011-01-27') * User.update({password : null}, function(){ * return this.lastAccessed.lte(comb.date.add(new Date(), "year", -1)); * }, {transaction : false}); * * @param {Object} vals a hash of values to update. See {@link patio.Dataset#update}. * @param query a filter to apply to the UPDATE. See {@link patio.Dataset#filter}. * * @param {Object} [options] additional options. * @param {Boolean} [options.transaction] boolean indicating if a transaction should be used when * updating the models. * * @return {Promise} a promise that is resolved once the update statement has completed. */ update: function (vals, /*?object*/query, options) { options = options || {}; var dataset = this.dataset; return this._checkTransaction(options, function () { if (!isUndefined(query)) { dataset = dataset.filter(query); } return dataset.update(vals); }); }, /** * Remove(delete) models. This can be used to do a mass delete of models. * * @example * var User = patio.getModel("user"); * * //remove all users * User.remove(); * * //remove all users who's names start with a. * User.remove({name : /A%/i}); * * //remove all users who's names start with a, without a transaction. * User.remove({name : /A%/i}, {transaction : false}); * * @param {Object} [q] query to filter the rows to remove. See {@link patio.Dataset#filter}. * @param {Object} [options] additional options. * @param {Boolean} [options.transaction] boolean indicating if a transaction should be used when * removing the models. * @param {Boolean} [options.load=true] boolean set to prevent the loading of each model. This is more efficient * but the pre/post remove hooks not be notified of the deletion. * * @return {comb.Promise} called back when the removal completes. */ remove: function (q, options) { options = options || {}; var loadEach = isBoolean(options.load) ? options.load : true; //first find all records so we call alert the middleware for each model var ds = this.dataset.filter(q); return this._checkTransaction(options, function () { if (loadEach) { return ds.map(function (r) { //todo this sucks find a better way! return r.remove(options); }); } else { return ds.remove(); } }); }, /** * Similar to remove but takes an id or an array for a composite key. * * @example * * User.removeById(1); * * @param id id or an array for a composite key, to find the model by * @param {Object} [options] additional options. * @param {Boolean} [options.transaction] boolean indicating if a transaction should be used when * removing the model. * * @return {comb.Promise} called back when the removal completes. */ removeById: function (id, options) { var self = this; return this._checkTransaction(options, function () { return self.findById(id).chain(function (model) { return model ? model.remove(options) : null; }); }); }, /** * Save either a new model or list of models to the database. * * @example * var Student = patio.getModel("student"); * Student.save([ * { * firstName:"Bob", * lastName:"Yukon", * gpa:3.689, * classYear:"Senior" * }, * { * firstName:"Greg", * lastName:"Horn", * gpa:3.689, * classYear:"Sohpmore" * }, * { * firstName:"Sara", * lastName:"Malloc", * gpa:4.0, * classYear:"Junior" * }, * { * firstName:"John", * lastName:"Favre", * gpa:2.867, * classYear:"Junior" * }, * { * firstName:"Kim", * lastName:"Bim", * gpa:2.24, * classYear:"Senior" * }, * { * firstName:"Alex", * lastName:"Young", * gpa:1.9, * classYear:"Freshman" * } * ]).chain(function(users){ * //work with the users * }); * * Save a single record * MyModel.save(m1); * * @param {patio.Model|Object|patio.Model[]|Object[]} record the record/s to save. * @param {Object} [options] additional options. * @param {Boolean} [options.transaction] boolean indicating if a transaction should be used when * saving the models. * * @return {comb.Promise} called back with the saved record/s. */ save: function (items, options) { options = options || {}; var isArr = isArray(items), Self = this; return this._checkTransaction(options, function () { return asyncArray(items) .map(function (o) { if (!isInstanceOf(o, Self)) { o = new Self(o); } return o.save(null, options); }) .chain(function (res) { return isArr ? res : res[0]; }); }); } } }).as(exports, "QueryPlugin"); Dataset.ACTION_METHODS.concat(Dataset.QUERY_METHODS).forEach(function (m) { if (!QueryPlugin[m]) { QueryPlugin[m] = function () { if (this.synced) { var ds = this.dataset; return ds[m].apply(ds, arguments); } else { throw new ModelError("Model " + this.tableName + " has not been synced"); } }; } });