      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;