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