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;