      1 var comb = require("comb"),
      2         hitch = comb.hitch,
      3         dataset = require("../dataset"),
      4         Promise = comb.Promise,
      5         PromiseList = comb.PromiseList;
      6
      7
      8 //make a map of connections per model;
      9 var connections = {};
     10
     11 /**
     12  * @private
     13  *
     14  * Create a dataset to query for a particular model
     15  *
     16  * @param {Model} model the model the dataset should wrap.
     17  * @param {Boolean} [hydrate=true] if set to true then the dataset will create Model instances for te results of the query, otherwise the raw results are returned.
     18  *
     19  * @return {Dataset} the dataset.
     20  */
     21 var getDataset = function(model, hydrate) {
     22     if (typeof hydrate == "undefined") {
     23         hydrate = true;
     24     }
     25     var  table = model.table, tableName = table.tableName, connection = model.moose.getConnection(false, table.database);
     26     /*if((connection = connections[tableName]) == null){
     27      console.log("GET CONNECTION");
     28      connection  = (connections[tableName] = model.moose.getConnection(false));
     29      }*/
     30     if (hydrate) {
     31         return dataset.getDataSet(table.tableName, connection, table.type, model);
     32     } else {
     33         return dataset.getDataSet(table.tableName, connection, table.type);
     34     }
     35 };
     36
     37 /**
     38  *
     39  * @private
     40  * Helper function to help Models expose Dataset functions as native methods.
     41  *
     42  * @param {String} op the operation that is being proxied
     43  * @param {Boolean} [hydrate=true] whether or not the results need to be hydrated to full model instances.
     44  *
     45  * @returns {Function} A function that will perform the proxied operation.
     46  */
     47 var proxyDataset = function(op, hydrate) {
     48     return function(options, callback, errback) {
     49         if (typeof options == "function") {
     50             callback = options;
     51             errback = callback;
     52             options = null;
     53         }
     54         var dataset = getDataset(this, hydrate);
     55         if (typeof options == "object") {
     56             dataset.find(options);
     57         } else {
     58             callback = options;
     59             errback = callback;
     60         }
     61         return dataset[op](callback, errback);
     62     };
     63 };
     64
     65
     66 /**
     67  *@class Adds query support to a model. The QueryPlugin exposes methods to save, update, create, and delete.
     68  * The plugin also exposes static functions to query Models. The functions exposed on class are.
     69  * <ul>
     70  *     <li>filter</li>
     71  *     <li>findById</li>
     72  *     <li>count</li>
     73  *     <li>join</li>
     74  *     <li>where</li>
     75  *     <li>select</li>
     76  *     <li>all</li>
     77  *     <li>forEach</li>
     78  *     <li>first</li>
     79  *     <li>one</li>
     80  *     <li>last</li>
     81  * </ul>
     82  *
     83  * All queries require an action to be called on them before the results are fetched. The action methods are :
     84  *
     85  * <ul>
     86  *     <li>all</li>
     87  *     <li>forEach</li>
     88  *     <li>first</li>
     89  *     <li>one</li>
     90  *     <li>last</li>
     91  *
     92  * </ul>
     93  *
     94  * The action items accept a callback that will be called with the results. They also return a promise that will be
     95  * called with the results.
     96  *
     97  * <p>Assume we have an Employee model.</p>
     98  *
     99  * <p>
    100  *      <b>first</b></br>
    101  *      Get the record in a dataset, this query does not require an action method
    102  *      <pre class="code">
    103  *          Employee.first() => select * from employee limit 1
    104  *      </pre>
    105  * </p>
    106  *
    107  * <p>
    108  *      <b>filter</b></br>
    109  *      Sets the where clause on a query. See {@link Dataset}
    110  *      <pre class="code">
    111  *          //Equality Checks
    112  *          Employee.filter({eid : 1})
    113  *                  => select * from employee where eid = 1
    114  *          Employee.filter({eid : {gt : 1}})
    115  *                  => select * from employee where eid > 1
    116  *          Employee.filter({eid : {gte : 1}})
    117  *                  => select * from employee where eid >= 1
    118  *          Employee.filter({eid : {lt : 1}})
    119  *                  => select * from employee where eid < 1
    120  *          Employee.filter({eid : {lte : 1}})
    121  *                  => select * from employee where eid <= 1
    122  *          //Nested query in filter
    123  *          Employee.filter({eid : {gt : 1}, lastname : "bob"})
    124  *                  => select * from employee where eid > 1 and lastname = 'bob';
    125  *          Employee.filter({eid : [1,2,3], lastname : "bob"})
    126  *                  => select * from employee where eid in (1,2,3) and lastname = 'bob'
    127  *      </pre>
    128  * </p>
    129  * <p>
    130  *      <b>findById</b></br>
    131  *      Find a record in a dataset by id, this query does not require an action method
    132  *      <pre class="code">
    133  *          Employee.findById(1) => select * from employee where eid = 1
    134  *      </pre>
    135  * </p>
    136  * <p>
    137  *      <b>count</b></br>
    138  *      Find the number of records in a dataset, this query does not require an action method
    139  *      <pre class="code">
    140  *          Employee.count() => select count(*) as count from employee
    141  *          Employee.filter({eid : {gte : 1}}).count()
    142  *                  => select count(*) as count from employee  where eid > 1
    143  *      </pre>
    144  * </p>
    145  * <p>
    146  *      <b>join</b></br>
    147  *      Get Join two models together, this will not create model instances for the result.
    148  *      <pre class="code">
    149  *          Employee.join("words", {eid : "eid"}).where({"employee.eid" : 1})
    150  *                  => select * from employee inner join works on employee.id=works.id where employee.eid = 1
    151  *      </pre>
    152  * </p>
    153  *
    154  * <p>
    155  *      <b>Where</b></br>
    156  *      Sets the where clause on a query. See {@link Dataset}
    157  *      <pre class="code">
    158  *          //Equality Checks
    159  *          Employee.where({eid : 1})
    160  *                  => select * from employee where eid = 1
    161  *          Employee.where({eid : {gt : 1}})
    162  *                  => select * from employee where eid > 1
    163  *          Employee.where({eid : {gte : 1}})
    164  *                  => select * from employee where eid >= 1
    165  *          Employee.where({eid : {lt : 1}})
    166  *                  => select * from employee where eid < 1
    167  *          Employee.where({eid : {lte : 1}})
    168  *                  => select * from employee where eid <= 1
    169  *          //Nested query in filter
    170  *          Employee.where({eid : {gt : 1}, lastname : "bob"})
    171  *                  => select * from employee where eid > 1 and lastname = 'bob';
    172  *          Employee.where({eid : [1,2,3], lastname : "bob"})
    173  *                  => select * from employee where eid in (1,2,3) and lastname = 'bob'
    174  *      </pre>
    175  * </p>
    176  *
    177  * <p>
    178  *      <b>select</b></br>
    179  *      Selects only certain columns to return, this will not create model instances for the result.
    180  *      <pre class="code">
    181  *          Employee.select(eid).where({firstname : { gt : "bob"}})
    182  *                  => select eid from employee where firstname > "bob"
    183  *      </pre>
    184  * </p>
    185  *
    186  *
    187  * <p>
    188  *      <b>all, foreach, first, one, last</b></br>
    189  *      These methods all act as action methods and fetch the results immediately. Each method accepts a query, callback, and errback.
    190  *      The methods return a promise that can be used to listen for results also.
    191  *      <pre class="code">
    192  *          Employee.all()
    193  *                  => select * from employee
    194  *          Employee.forEach(function(){})
    195  *                  => select * from employee
    196  *          Employee.forEach({eid : [1,2,3]}, function(){}))
    197  *                  => select * from employee where eid in (1,2,3)
    198  *          Employee.one()
    199  *                  => select * from employee limit 1
    200  *      </pre>
    201  * </p>
    202  *
    203  * @name QueryPlugin
    204  * @memberOf moose.plugins
    205  *
    206  * @borrows Dataset#all as all
    207  * @borrows Dataset#forEach as forEach
    208  * @borrows Dataset#first as first
    209  * @borrows Dataset#one as one
    210  * @borrows Dataset#last as last
    211  * @borrows SQL#join as join
    212  * @borrows SQL#where as where
    213  * @borrows SQL#select as select
    214  *
    215  */
    216 exports.QueryPlugin = comb.define(null, {
    217     instance : {
    218     /**@lends moose.plugins.QueryPlugin.prototype*/
    219
    220         /**
    221          * Force the reload of the data for a particular model instance.
    222          *
    223          * @example
    224          *
    225          * myModel.reload().then(function(myModel){
    226          *    //work with this instance
    227          * });
    228          *
    229          * @return {comb.Promise} called back with the reloaded model instance.
    230          */
    231         reload : function() {
    232             var pk = this.primaryKey;
    233             var q = {};
    234             if (pk) {
    235                 if (pk instanceof Array) {
    236                     for (var i = pk.length - 1; i > 0; i--) {
    237                         var p = pk[i];
    238                         q[p] = this[p];
    239                     }
    240                 } else {
    241                     q[pk] = this[pk];
    242                 }
    243
    244             } else {
    245                 q = this.toSql();
    246             }
    247
    248             var retPromise = new Promise();
    249             getDataset(this.constructor).find(q).one().then(hitch(retPromise, "callback"), hitch(retPromise, "errback"));
    250             return retPromise;
    251         },
    252
    253         /**
    254          * Remove this model.
    255          *
    256          * @param {Function} errback called in the deletion fails.
    257          *
    258          * @return {comb.Promise} called back after the deletion is successful
    259          */
    260         remove : function(errback) {
    261             var pk = this.primaryKey;
    262             if (pk) {
    263                 var q = {};
    264                 if (pk instanceof Array) {
    265                     for (var i = pk.length - 1; i > 0; i--) {
    266                         var p = pk[i];
    267                         q[p] = this[p];
    268                     }
    269                 } else {
    270                     q[pk] = this[pk];
    271                 }
    272
    273             } else {
    274                 q = this.toSql();
    275             }
    276             var retPromise = new Promise();
    277             this._hook("pre", "remove").then(hitch(this, function() {
    278                 var dataset = getDataset(this);
    279                 dataset.remove(null, q).exec().then(hitch(this, function(results) {
    280                     this.__isNew = true;
    281                     var columns = this.table.columns, ret = {};
    282                     for (var i in columns) {
    283                         this["_" + i] = null;
    284                     }
    285                     this._hook("post", "remove").then(hitch(retPromise, "callback"));
    286                 }), hitch(retPromise, "errback"));
    287             }), hitch(retPromise, "errback"));
    288             retPromise.addErrback(errback);
    289             return retPromise;
    290         },
    291
    292         /**
    293          * Update a model with new values.
    294          *
    295          * @example
    296          *
    297          * someModel.update({
    298          *      myVal1 : "newValue1",
    299          *      myVal2 : "newValue2",
    300          *      myVal3 : "newValue3"
    301          *      }).then(..do something);
    302          *
    303          * //or
    304          *
    305          * someModel.myVal1 = "newValue1";
    306          * someModel.myVal2 = "newValue2";
    307          * someModel.myVal3 = "newValue3";
    308          *
    309          * someModel.update().then(..so something);
    310          *
    311          * @param {Object} [options] values to update this model with
    312          * @param {Function} [errback] function to call if the update fails, the promise will errback also if it fails.
    313          *
    314          * @return {comb.Promise} called on completion or error of update.
    315          */
    316         update : function(options, errback) {
    317             if (!this.__isNew && this.__isChanged) {
    318                 for (var i in options) {
    319                     if (this.table.validate(i, options[i])) {
    320                         this[i] = options;
    321                     }
    322                 }
    323                 var pk = this.primaryKey;
    324                 if (pk) {
    325                     var q = {};
    326                     if (pk instanceof Array) {
    327                         for (var i = pk.length - 1; i > 0; i--) {
    328                             var p = pk[i];
    329                             q[p] = this[p];
    330                         }
    331                     } else {
    332                         q[pk] = this[pk];
    333                     }
    334
    335                 } else {
    336                     q = this.toSql();
    337                 }
    338                 var retPromise = new Promise();
    339                 this._hook("pre", "update").then(hitch(this, function() {
    340                     var dataset = getDataset(this);
    341                     dataset.update(this.toSql(), q).exec().then(hitch(this, function() {
    342                         this.__isChanged = false;
    343                         this._hook("post", "update").then(hitch(this, function() {
    344                             retPromise.callback(this);
    345                         }));
    346                     })),hitch(retPromise, "errback");
    347                 }));
    348                 retPromise.addErrback(errback);
    349                 return retPromise;
    350             } else if (this.__isNew && this.__isChanged) {
    351                 return this.save(options, errback);
    352             } else {
    353                 throw new Error("Cannot call update on an unchanged object");
    354             }
    355         },
    356
    357         /**
    358          * Save a model with new values.
    359          *
    360          * @example
    361          *
    362          * someModel.save({
    363          *      myVal1 : "newValue1",
    364          *      myVal2 : "newValue2",
    365          *      myVal3 : "newValue3"
    366          *      }).then(..do something);
    367          *
    368          * //or
    369          *
    370          * someModel.myVal1 = "newValue1";
    371          * someModel.myVal2 = "newValue2";
    372          * someModel.myVal3 = "newValue3";
    373          *
    374          * someModel.save().then(..so something);
    375          *
    376          * @param {Object} [options] values to save this model with
    377          * @param {Function} [errback] function to call if the save fails, the promise will errback also if it fails.
    378          *
    379          * @return {comb.Promise} called on completion or error of save.
    380          */
    381         save : function(options, errback) {
    382             if (this.__isNew) {
    383                 var pk = this.primaryKey, thisPk = null;
    384                 if (pk instanceof Array) {
    385                     pk = null;
    386                 }
    387                 if (options) {
    388                     for (var i in options) {
    389                         this[i] = options;
    390                     }
    391                 }
    392                 thisPk = this.primaryKeyValue;
    393                 var retPromise = new Promise();
    394                 this._hook("pre", "save").then(hitch(this, function() {
    395                     getDataset(this).save(this.toSql(), !thisPk).then(hitch(this, function(res) {
    396                         this.__isNew = false;
    397                         this.__isChanged = false;
    398                         if (pk && !thisPk) {
    399                             this[pk] = this.table.columns[pk].fromSql(res);
    400                         }
    401                         this._hook("post", "save").then(hitch(this, function() {
    402                             retPromise.callback(this);
    403                         }));
    404                     }), hitch(retPromise, "errback"));
    405                 }));
    406                 retPromise.addErrback(errback);
    407                 return retPromise;
    408             } else {
    409                 return this.update(options, errback);
    410             }
    411         },
    412
    413         /**
    414          * Serializes all values in this model to the sql equivalent.
    415          */
    416         toSql : function() {
    417             var columns = this.table.columns, ret = {};
    418             for (var i in columns) {
    419                 ret[i] = columns[i].toSql(this[i]);
    420             }
    421             return ret;
    422         }
    423
    424     },
    425
    426     static : {
    427
    428     /**@lends moose.plugins.QueryPlugin*/
    429
    430         /**
    431          * Filter a model to return a subset of results. {@link SQL#find}
    432          *
    433          * <p><b>This function requires all, forEach, one, last,
    434          *       or count to be called inorder for the results to be fetched</b></p>
    435          * @param {Object} [options] query to filter the dataset by.
    436          * @param {Boolean} [hydrate=true] if true model instances will be the result of the query,
    437          *                                  otherwise just the results will be returned.
    438          *
    439          *@return {Dataset} A dataset to query, and or fetch results.
    440          */
    441         filter : function(options, hydrate) {
    442             return getDataset(this, hydrate).find(options);
    443         },
    444
    445         /**
    446          * Retrieves a record by the primarykey of a table.
    447          * @param {*} id the primary key record to find.
    448          *
    449          * @return {comb.Promise} called back with the record or null if one is not found.
    450          */
    451         findById : function(id) {
    452             var pk = this.table.pk;
    453             var q = {};
    454             q[pk] = id;
    455             return this.filter(q).one();
    456         },
    457
    458         /**
    459          * Update multiple rows with a set of values.
    460          *
    461          * @param {Object} vals the values to set on each row.
    462          * @param {Object} [options] query to limit the rows that are updated
    463          * @param {Function} [callback] function to call after the update is complete.
    464          * @param {Function} [errback] function to call if the update errors.
    465          *
    466          * @return {comb.Promise|Dataset} if just values were passed in then a dataset is returned and exec has to be
    467          *                                  called in order to complete the update.
    468          *                                If options, callback, or errback are provided then the update is executed
    469          *                                and a promise is returned that will be called back when the update completes.
    470          */
    471         update : function(vals, /*?object*/options, /*?callback*/callback, /*?function*/errback) {
    472             var args = Array.prototype.slice.call(arguments);
    473             var dataset = getDataset(this);
    474             if (args.length > 1) {
    475                 vals = args[0];
    476                 options = args[1];
    477                 if (typeof options == "function") {//then execute right away we have a callback
    478                     callback = options;
    479                     if (args.length == 3) errback = args[2];
    480                     var retPromise = new Promise();
    481                     dataset.update(vals).exec().then(function() {
    482                         retPromise.callback(true);
    483                     }, hitch(retPromise, "errback"));
    484                     retPromise.then(callback, errback);
    485                     return retPromise;
    486                 } else if (typeof options == "object") {
    487                     if (args.length > 2) {
    488                         callback = args[2];
    489                     }
    490                     if (args.length == 4) errback = args[3];
    491                     dataset.update(vals, options);
    492                     if (callback || errback) {
    493                         retPromise = new Promise();
    494                         dataset.exec().then(function() {
    495                             retPromise.callback(true);
    496                         }, hitch(retPromise, "errback"));
    497                         retPromise.then(callback, errback);
    498                         return retPromise;
    499                     }
    500                 }
    501             } else if (args.length == 1) {
    502                 //then just call update and let them manually
    503                 //execute it later by calling exec or a
    504                 //command function like one, all, etc...
    505                 return dataset.update(args[0]);
    506             }
    507             return dataset;
    508         },
    509
    510         /**
    511          * Remove rows from the Model.
    512          *
    513          * @param {Object} [q] query to filter the rows to remove
    514          * @param {Function} [errback] function to call if the removal fails.
    515          *
    516          * @return {comb.Promise} called back when the removal completes.
    517          */
    518         remove : function(q, errback) {
    519             var retPromise = new Promise();
    520             //first find all records so we call alert all associations and all other crap that needs to be
    521             //done in middle ware
    522             var p = new Promise();
    523             var pls = [];
    524             getDataset(this).find(q).all(function(items) {
    525                 //todo this sucks find a better way!
    526                 var pl = items.map(function(r) {
    527                     return r.remove();
    528                 });
    529                 new PromiseList(pl).then(hitch(p, "callback"), hitch(p, "errback"));
    530             }, hitch(p, "errback"));
    531             p.addErrback(errback);
    532             return p;
    533         },
    534
    535         /**
    536          * Save either a new model or list of models to the database.
    537          *
    538          * @example
    539          *
    540          * //Save a group of records
    541          * MyModel.save([m1,m2, m3]);
    542          *
    543          * Save a single record
    544          * MyModel.save(m1);
    545          *
    546          * @param {Array|Object} record the record/s to save to the database
    547          * @param {Function} [errback] function to execute if the save fails
    548          *
    549          * @return {comb.Promise} called back with the saved record/s.
    550          */
    551         save : function(options, errback) {
    552             var ps;
    553             if (options instanceof Array) {
    554                 ps = options.map(function(o) {
    555                     return this.save(o);
    556                 }, this);
    557                 var pl = new PromiseList(ps);
    558                 pl.addErrback(errback);
    559                 return pl;
    560             } else {
    561                 var promise = new Promise();
    562                 this.load(options).then(function(m) {
    563                     m.save().then(hitch(promise, "callback"), hitch(promise, "errback"));
    564                 }, hitch(promise, "errback"));
    565                 promise.addErrback(errback);
    566                 return promise;
    567             }
    568         },
    569
    570         /**
    571          * Retrieve the number of records in the database.
    572          *
    573          * @param {Function} [callback] function to execute with the result
    574          * @param {Function} [errback] funciton to execute if the operation fails
    575          *
    576          * @return {comb.Promise} called back with the result, or errors if the operation fails.
    577          */
    578         count : function(callback, errback) {
    579             var ret = new Promise();
    580             getDataset(this).count().one(function(count) {
    581                 ret.callback(count.count);
    582             }, hitch(ret, "errback"));
    583             ret.then(callback, errback);
    584             return ret;
    585         },
    586
    587         join : function() {
    588             var d = getDataset(this, false);
    589             return d.join.apply(d, arguments);
    590         },
    591
    592         where : function() {
    593             var d = getDataset(this);
    594             return d.where.apply(d, arguments);
    595         },
    596
    597         select : function() {
    598             var d = getDataset(this);
    599             return d.select.apply(d, arguments);
    600         },
    601
    602         all : proxyDataset("all", true),
    603
    604
    605         forEach : proxyDataset("forEach", true),
    606
    607
    608         first : proxyDataset("first", true),
    609
    610         one : proxyDataset("one", true),
    611
    612         last : proxyDataset("last", true),
    613
    614
    615         /**@ignore*/
    616         getters : {
    617
    618             dataset : function() {
    619                 return getDataset(this, false);
    620             }
    621         }
    622     }
    623 });
    624
    625