1 var Client = require('mysql').Client, 2 dataset = require("./dataset"), 3 model = require("./model"), 4 adapters = require("./adapters"), 5 comb = require("comb"), 6 hitch = comb.hitch, 7 Promise = comb.Promise, 8 PromiseList = comb.PromiseList, 9 plugins = require("./plugins"), 10 migration = require("./migrations"), 11 Table = require("./table").Table; 12 13 var connectionReady = false; 14 15 var LOGGER = comb.logging.Logger.getLogger("moose"); 16 new comb.logging.BasicConfigurator().configure(); 17 LOGGER.level = comb.logging.Level.INFO; 18 19 /** 20 * @class A singleton class that acts as the entry point for all actions performed in moose. 21 * 22 * @constructs 23 * @name moose 24 * @augments Migrations 25 * @param options 26 * 27 * @property {String} database the default database to use, this property can only be used after the conneciton has 28 * initialized. 29 * @property {moose.adapters} adapter the adapter moose is using. <b>READ ONLY</b> 30 * 31 */ 32 var Moose = comb.singleton(migration, { 33 instance : { 34 35 /** 36 * @lends moose.prototype 37 */ 38 39 /** 40 * the models that are created and ready to be used. 41 * @private 42 */ 43 models : null, 44 45 /** 46 * The schemas that are created and ready to be used as the base of a model. 47 * @private 48 */ 49 schemas : null, 50 51 /** 52 *The type of database moose will be connecting to. Currently only mysql is supported. 53 */ 54 type : "mysql", 55 56 57 constructor : function(options) { 58 this.__deferredSchemas = []; 59 this.__deferredModels = []; 60 this.models = {}; 61 this.schemas = {}; 62 this.super(arguments); 63 }, 64 65 /** 66 * Initialize the connection information, and prepare moose to communicate with the DB. 67 * All models, and schemas, and models that are created before this method has been called will be deferred. 68 * 69 * @example 70 * 71 * moose.createConnection({ 72 * host : "127.0.0.1", 73 * port : 3306, 74 * type : "mysql", 75 * maxConnections : 1, 76 * minConnections : 1, 77 * user : "test", 78 * password : "testpass", 79 * database : 'test' 80 *}); 81 * 82 * @param {Object} options the options used to initialize the database connection. 83 * @params {Number} [options.maxConnections = 10] the number of connections to pool. 84 * @params {Number} [options.minConnections = 3] the number of connections to pool. 85 * @param {String} [options.type = "mysql"] the type of database to communicate with. 86 * @params {String} options.user the user to authenticate as. 87 * @params {String} options.password the password of the user. 88 * @params {String} options.database the name of the database to use, the database 89 * specified here is the default database for all connections. 90 */ 91 createConnection : function(options) { 92 //allow only one connection 93 //We can use the private singleton value, because moose is actually a singleton 94 //TODO change this to allow connections to multiple databases 95 if (!connectionReady) { 96 options = options || {}; 97 this.options = options; 98 this.type = options.type || "mysql"; 99 var adapter = this.adapter; 100 if (adapter) { 101 this.client = new adapter.client(options); 102 } else { 103 throw "moose : " + this.type + " is not supported"; 104 } 105 //initialize the schema object with the default database 106 var db = this.client.database; 107 this.schemas[db] = {}; 108 this.models[db] = {}; 109 connectionReady = true; 110 } 111 }, 112 113 /** 114 * Closes all connections to the database. 115 */ 116 closeConnection : function() { 117 var ret; 118 if (connectionReady) { 119 ret = this.client.close(); 120 connectionReady = false; 121 } else { 122 ret = new Promise(); 123 ret.callback(); 124 } 125 return ret; 126 }, 127 128 /** 129 * Retrieves a connection to the database. Can be used to work with the database driver directly. 130 * 131 * @param {Boolean} autoClose if set to true then a new connection will be created, 132 * otherwise a connection will be retrieved from a connection pool. 133 * @param {String} [database] the database to perform the query on, if not defined 134 * then the default database from {@link moose#createConnection} 135 * will be used . 136 * 137 * @return {Query} a query object. 138 */ 139 getConnection : function(autoClose, database) { 140 LOGGER.debug("MOOSE : GET CONNECTION " + database); 141 return this.client.getConnection(autoClose, database); 142 }, 143 144 /** 145 * Creates a context for performing database transactions. 146 * When using a transaction be sure to call commit with finished! 147 * @example 148 * 149 * var trans = moose.transaction(); 150 * Do lots of stuff 151 * . 152 * . 153 * . 154 * trans.commit(); 155 * 156 * @param {String} [database] the database to perform the query on, if not defined 157 * then the default database from {@link moose#createConnection} 158 * will be used. 159 * 160 * @return {TransactionQuery} a transaction context. 161 */ 162 transaction : function(database) { 163 return this.client.transaction(database); 164 }, 165 166 /** 167 * Creates a dataset to operate on a particular table. 168 * 169 * @param tableName the name of the table to perfrom operations on. 170 * @param {String} [database] the database to perform the query on, if not defined 171 * then the default database from {@link moose#createConnection} 172 * will be used 173 * 174 * @return {Dataset} a dataset to operate on a particular table. 175 */ 176 getDataset : function(tableName, database) { 177 if (tableName) { 178 return dataset.getDataSet(tableName, this.getConnection(false, database), 179 this.type); 180 } else { 181 throw new Error("Table name required get getting a dataset"); 182 } 183 }, 184 185 /** 186 * Execute raw SQL. 187 * 188 * @example 189 * moose.execute("select * from myTable"); 190 * 191 * moose.execute("select * from myTable", "myOtherDB"); 192 * 193 * @param sql the SQL to execute 194 * @param {String} [database] the database to perform the query on, if not defined 195 * then the default database from {@link moose#createConnection} 196 * will be used 197 * 198 * @returns {comb.Promise} a promise that will be called after the SQL execution completes. 199 */ 200 execute : function(sql, database) { 201 var promise = new Promise(); 202 var db = this.getConnection(true, database); 203 db.query(sql).then(function(res) { 204 promise.callback(res); 205 }, hitch(promise, "errback")); 206 return promise; 207 }, 208 209 /** 210 * Load a {@link moose.Table} to be used by a model or directly. 211 * This is typically called before, one creates a new model. 212 * 213 * @example 214 * 215 * moose.loadSchema("testTable").then(function(schema){ 216 * moose.addModel(schema, ...); 217 * }); 218 * 219 * 220 * @param {String} tableName the name of the table to load 221 * @param {String} [database] the database to retreive the table from, if not defined 222 * then the default database from {@link moose#createConnection} 223 * will be used 224 * 225 * 226 * @return {comb.Promise} A promise is called back with a table ready for use. 227 */ 228 loadSchema : function(tableName, database) { 229 var promise = new Promise(); 230 if (connectionReady) { 231 var db = database || this.client.database; 232 var schema; 233 if ((schema = this.schemas[db]) == null) { 234 schema = this.schemas[db] = {}; 235 } 236 LOGGER.debug("LOAD SCHEMA DB " + database); 237 if (!(tableName in schema)) { 238 this.adapter.schema(tableName, this.getConnection(false, database)) 239 .then(hitch(this, function(table) { 240 var db = table.database; 241 if (table) { 242 var schema; 243 if ((schema = this.schemas[db]) == null) { 244 schema = this.schemas[db] = {}; 245 } 246 //put the schema under the right database 247 schema[tableName] = table; 248 } 249 promise.callback(table); 250 }), hitch(promise, "errback")); 251 } else { 252 promise.callback(schema[tableName]); 253 } 254 } else { 255 LOGGER.debug("SCHEMA LOAD DEFERRED"); 256 this.__deferredSchemas.push(tableName); 257 } 258 return promise; 259 }, 260 261 262 /** 263 * Use to load a group of tables. 264 * @example 265 * //load from the default database 266 * moose.loadSchemas(["testTable", "testTable2", ....]).then(function(schema1, schema2,....){ 267 * moose.addModel(schema1, ...); 268 * moose.addModel(schema2, ...); 269 * }); 270 * 271 * //load schemas from a particular db 272 * moose.loadShemas(["table1", "table2"], "yourDb").then(function(table1, table2){ 273 * //do something... 274 * }); 275 * //load table from multiple databases 276 * moose.loadSchemas({db1 : ["table1","table2"], db2 : ["table3","table4"]}).then(function(table1,table2, table3,table4){ 277 * //do something.... 278 * }); 279 * 280 * 281 * @param {Array<String>|Object} tableNames 282 * <ul> 283 * <li>If an array of strings is used they are assumed to be all from the same database.</li> 284 * <li>If an object is passed the key is assumed to be the database, and the value should be an array of strings</li> 285 * </ul> 286 * 287 * @param {String} [database] the database to retreive the table from, if not defined 288 * then the default database from {@link moose#createConnection} 289 * will be used 290 * 291 * 292 * 293 * @return {comb.Promise} A promise that is called back with the tables in the same order that they were contained in the array. 294 */ 295 loadSchemas : function(tableNames, database) { 296 var ret = new Promise(), pl, ps; 297 if (comb.isArray(tableNames)) { 298 if (tableNames.length) { 299 ps = tableNames.map(function(name) { 300 return this.loadSchema(name, database); 301 }, this); 302 pl = new PromiseList(ps); 303 pl.addCallback(function(r) { 304 // loop through and load the results 305 ret.callback.apply(ret, r.map(function(o) { 306 return o[1]; 307 })); 308 }); 309 pl.addErrback(hitch(ret, "errback")); 310 } else { 311 ret.callback(null); 312 } 313 } else if (comb.isObject((tableNames))) { 314 ps = []; 315 for (var i in tableNames) { 316 //load the schemas 317 ps.push(this.loadSchemas(tableNames[i], i)); 318 } 319 pl = new PromiseList(ps).then(function(r) { 320 var tables = []; 321 r.forEach(function(ts) { 322 //remove the first item 323 ts.shift(); 324 tables.push.apply(tables, ts); 325 }); 326 //apply it so they are called back as arguments; 327 ret.callback.apply(ret, tables); 328 }, comb.hitch(ret, "errback")); 329 330 } else { 331 throw new Error("tables names must be an array"); 332 } 333 return ret; 334 }, 335 336 /** 337 * <p>Adds a model to moose.</p> 338 * </br> 339 * <b>NOTE</b> 340 * <ul> 341 * <li>If a {@link moose.Table} is the first parameter then the {moose.Model} is returned immediately</li> 342 * <li>If a table name is the first parameter then a {comb.Promise} is returned, and called back with the model once it is loaded</li> 343 * </ul> 344 * 345 * @example 346 * 347 * moose.addModel(yourTable, { 348 * plugins : [PLUGIN1, PLUGIN2, PLUGIN3] 349 * instance : { 350 * myInstanceMethod : funciton(){}, 351 * getters : { 352 * myProp : function(){ 353 * return prop; 354 * } 355 * }, 356 * 357 * setters : { 358 * myProp : function(val){ 359 * prop = val; 360 * } 361 * } 362 * }, 363 * 364 * static : { 365 * myStaticMethod : function(){ 366 * 367 * }, 368 * 369 * getters : { 370 * myStaticProp : function(){ 371 * return prop; 372 * } 373 * }, 374 * 375 * setters : { 376 * myStaticProp : function(val){ 377 * prop = val; 378 * } 379 * } 380 * }, 381 * 382 * pre : { 383 * save : function(){ 384 * 385 * } 386 * }, 387 * 388 * post : { 389 * save : function(){ 390 * 391 * } 392 * } 393 * }); 394 * 395 * //or 396 * 397 * moose.addModel("myTable", {}).then(function(model){ 398 * //do something 399 * });; 400 * 401 * //or 402 * moose.addModel("myTable", "myOtherDB").then(function(model){ 403 * //do something 404 * }); 405 * 406 * 407 * @param {String|moose.Table} table the table to be used as the base for this model. 408 * Factory for a new Model. 409 * @param {String} [database] the database to retreive the table from, if not defined 410 * then the default database from {@link moose#createConnection} 411 * will be used 412 * 413 * @param {Object} options - Similar to {@link comb.define} with a few other conveniences 414 * @param {Array} options.plugins a list of plugins to enable on the model. 415 * @param {Object} options.pre an object containing key value pairs of events, and the corresponding callback. 416 * <pre class="code"> 417 * { 418 * pre : { 419 * save : funciton(){}, 420 * update : function(){}, 421 * load : function(){}, 422 * remove : function(){} 423 * } 424 * } 425 * </pre> 426 * 427 * @param {Object} options.post an object containing key value pairs of events, and the corresponding callback. 428 * <pre class="code"> 429 * { 430 * post : { 431 * save : funciton(){}, 432 * update : function(){}, 433 * load : function(){}, 434 * remove : function(){} 435 * } 436 * } 437 * </pre> 438 * 439 * 440 * @return {comb.Promise|Model} see description. 441 * 442 */ 443 addModel : function(table, database, options) { 444 var promise = new Promise(), m; 445 if (table instanceof Table) { 446 if (comb.isObject(database)) { 447 options = database; 448 database = this.client.database; 449 } 450 options = options || {}; 451 database = table.database || this.client.database,models; 452 if (!(models = this.models[database])) { 453 models = this.models[database] = {}; 454 } 455 m = models[table.tableName] = model.create(table, this, options); 456 return m; 457 } else { 458 if (connectionReady) { 459 if (comb.isObject(database)) { 460 options = database; 461 database = this.client.database; 462 } 463 options = options || {}; 464 var models; 465 if (!(models = this.models[database])) { 466 models = this.models[database] = {}; 467 } 468 if (models[table]) { 469 promise.callback(models[table]); 470 } else { 471 if (typeof table == "string") { 472 var schemas = this.schemas[database]; 473 if (!schemas || !table in this.schemas[database]) { 474 this.loadSchema(table, database).then( 475 function(schema) { 476 var m = (models[table] = model.create(schema, this, options)); 477 promise.callback(m); 478 }, hitch(promise, "errback")); 479 } else { 480 m = (models[table] = model.create(schemas[table], this, options)); 481 promise.callback(m); 482 } 483 } 484 } 485 } else { 486 this.__deferredModels.push(arguments); 487 } 488 } 489 return promise; 490 }, 491 492 /** 493 * Retrieve an already created table. 494 * 495 * @param {String} tableName the name of the table 496 * @param {String} [database] the database the table resides in. 497 * If database is not provided then the default database is assumed. 498 * 499 * @return {moose.Table} return the table or null of it is not found. 500 */ 501 getSchema : function(tableName, database) { 502 var m = null; 503 var db = database || this.client.database; 504 var schema = this.schemas[db]; 505 if (schema) { 506 if (tableName in schema) { 507 m = schema[tableName]; 508 } 509 } 510 return m; 511 }, 512 513 /** 514 * Retrieve an already created model. 515 * 516 * @param {String} tableName the name of the table the model wraps. 517 * @param {String} [database] the database the model is part of. This typically is only used if the 518 * models table is in a database other than the default. 519 * then the default database from {@link moose#createConnection} 520 * will be used 521 * 522 * 523 * @return {moose.Model} return the model or null of it is not found. 524 */ 525 getModel : function(tableName, database) { 526 var m = null; 527 var db = database || this.client.database; 528 var models = this.models[db]; 529 if (models) { 530 if (tableName in models) { 531 m = models[tableName]; 532 } 533 } 534 return m; 535 }, 536 537 /**@ignore*/ 538 getters : { 539 adapter : function() { 540 return adapters[this.type]; 541 }, 542 543 database : function(){ 544 return connectionReady ? this.client.database : null; 545 } 546 }, 547 548 549 /**@ignore*/ 550 setters : { 551 database : function(database) { 552 if (connectionReady) { 553 this.client.database = database; 554 } 555 } 556 } 557 } 558 }); 559 560 var moose = exports; 561 module.exports = moose = new Moose(); 562 563 moose.Table = Table; 564 /** 565 * @namespace 566 */ 567 moose.adapters = adapters; 568 /** 569 * @namespace 570 */ 571 moose.plugins = plugins;