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