      1 var comb = require("comb"),
      2 		logging = comb.logging,
      3 		Logger = logging.Logger,
      4 		LOGGER = Logger.getLogger("comb.migrations");
      5 		hitch = comb.hitch,
      6 		Promise = comb.Promise,
      7 		PromiseList = comb.PromiseList,
      8 		fs = require("fs"),
      9 		path = require("path"),
     10 		tables = require("./table"),
     11 		Table = tables.Table;
     12
     13 /**
     14  *@class This is a plugin for moose to add migration functionality.
     15  * Migrations are the preferred way of handling table creation, deletion, and altering.
     16  * <p>Adds
     17  * <ul>
     18  *     <li>migrate - perform a migration</li>
     19  *     <li>createTable - to create a new table</li>
     20  *     <li>dropTable - drop a table</li>
     21  *     <li>alterTable - alter an existing table</li>
     22  * </ul>
     23  *
     24  * Migrations are done through files contained in a particular directory, the directory should only contain migrations.
     25  * In order for moose to determine which versions to use
     26  * the file names must end in <versionNumber>.js where versionNumber is a integer value representing the version num.
     27  * An example directory structure might look like the following:
     28  *
     29  * <pre class="code">
     30  * -migrations
     31  *      - createFirstTables.1.js
     32  *      - shortDescription.2.js
     33  *      - another.3.js
     34  *      .
     35  *      .
     36  *      .
     37  *      -lastMigration.n.js
     38  * </pre>
     39  *
     40  * In order to easily identify where certain schema alterations have taken place it is a good idea to provide a brief
     41  * but meaningful migration name.
     42  *
     43  * createEmployee.1.js
     44  * </br>
     45  *
     46  * In order to run a migraton all one has to do is call moose.migrate(options);
     47  *
     48  * <pre class="code">
     49  *  moose.migrate({
     50  *               connection : {user : "test", password : "testpass", database : 'test'}, //Connection information to connect to the database.
     51  *               dir : "location of migrations", //Location of the directory containing the migrations.
     52  *               start : 0,//What version to start migrating at.
     53  *               end : 0,  //What version to stop migrations at.
     54  *               up : true //set to true to go up in migrations, false to rollback
     55  *  });
     56  * </pre>
     57  *
     58  * <p>Example migration file</b>
     59  * <pre class="code">
     60  *
     61  * //Up function used to migrate up a version
     62  * exports.up = function() {
     63  *   //create a new table
     64  *   moose.createTable("company", function(table) {
     65  *       //the table instance is passed in.
     66  *       //add columns
     67  *       table.column("id", types.INT({allowNull : false, autoIncrement : true}));
     68  *       table.column("companyName", types.VARCHAR({length : 20, allowNull : false}));
     69  *       //set the primary key
     70  *       table.primaryKey("id");
     71  *   });
     72  *   moose.createTable("employee", function(table) {
     73  *       table.column("id", types.INT({allowNull : false, autoIncrement : true}));
     74  *       table.column("firstname", types.VARCHAR({length : 20, allowNull : false}));
     75  *       table.column("lastname", types.VARCHAR({length : 20, allowNull : false}));
     76  *       table.column("midinitial", types.CHAR({length : 1}));
     77  *       table.column("gender", types.ENUM({enums : ["M", "F"], allowNull : false}));
     78  *       table.column("street", types.VARCHAR({length : 50, allowNull : false}));
     79  *       table.column("city", types.VARCHAR({length : 20, allowNull : false}));
     80  *       table.primaryKey("id");
     81  *   });
     82  *
     83  *   moose.createTable("companyEmployee", function(table) {
     84  *       table.column("companyId", types.INT({allowNull : false}));
     85  *       table.column("employeeId", types.INT({allowNull : false}));
     86  *       table.primaryKey(["companyId", "employeeId"]);
     87  *       table.foreignKey({companyId : {company : "id"}, employeeId : {employee : "id"}});
     88  *   });
     89  *   moose.createTable("works", function(table) {
     90  *       table.column("id", types.INT({allowNull : false, autoIncrement : true}));
     91  *       table.column("eid", types.INT({allowNull : false}));
     92  *       table.column("companyName", types.VARCHAR({length : 20, allowNull : false}));
     93  *       table.column("salary", types.DOUBLE({size : 8, digits : 2, allowNull : false}));
     94  *       table.primaryKey("id");
     95  *       table.foreignKey("eid", {employee : "id"});
     96  *
     97  *   });
     98  *};
     99  *
    100  * //Down function used to migrate down version
    101  *exports.down = function() {
    102  *    moose.dropTable("companyEmployee");
    103  *    moose.dropTable("works");
    104  *    moose.dropTable("employee");
    105  *    moose.dropTable("company");
    106  *};
    107  * </pre>
    108  *@name Migrations
    109  *
    110  * */
    111 module.exports = exports = comb.define(null, {
    112 			instance : {
    113 				/**@lends Migrations.prototype*/
    114
    115
    116 				constructor : function() {
    117 					this.__deferredMigrations = [];
    118 				},
    119
    120 				/**
    121 				 * Performs the deferred migrations function
    122 				 *
    123 				 * Used by create/drop/alter Table functions when within a migraton
    124 				 * */
    125 				__migrateFun : function(fun, table) {
    126 					var ret = new Promise();
    127 					var adapter = this.adapter;
    128 					var conn = this.getConnection(false, table.database);
    129 					adapter[fun](table, conn).then(hitch(ret, "callback"), hitch(ret, "errback"));
    130 					delete this.schemas[table.database || this.client.database][table.tableName];
    131 					return ret;
    132 				},
    133
    134 				/**
    135 				 * <p>Creates a new table. This function should be used while performing a migration.</p>
    136 				 * <p>If the table should be created in another DB then the table should have the database set on it.</p>
    137 				 *
    138 				 * @example
    139 				 * //default database table creation
    140 				 * moose.createTable("test", function(table){
    141 				 *     table.column("id", types.INT())
    142 				 *     table.primaryKey("id");
    143 				 * });
    144 				 *
    145 				 * //create a table in another database
    146 				 * moose.createTable("test", function(table){
    147 				 *     table.database = "otherDB";
    148 				 *     table.column("id", types.INT())
    149 				 *     table.primaryKey("id");
    150 				 * });
    151 				 *
    152 				 *
    153 				 * @param {String} tableName the name of the table to create
    154 				 * @param {Funciton} cb this funciton is callback with the table
    155 				 *      - All table properties should be specified within this block
    156 				 *
    157 				 * @return {comb.Promise} There are two different results that the promise can be called back with.
    158 				 * <ol>
    159 				 *     <li>If a migration is currently being performed then the promise is called back with a
    160 				 *     function that should be called to actually perform the migration.</li>
    161 				 *     <li>If the called outside of a migration then the table is created immediately and
    162 				 *     the promise is called back with the result.</li>
    163 				 * </ol>
    164 				 *
    165 				 * */
    166 				createTable : function(tableName, cb) {
    167 					var table = new Table(tableName, {});
    168 					cb(table);
    169 					//add it to the moose schema map
    170 					var db = table.database || this.client.database, schema;
    171 					if ((schema = this.schemas[db]) == null) {
    172 						schema = this.schemas[db] = {};
    173 					}
    174 					schema[tableName] = table;
    175 					if (!this.__inMigration) {
    176 						return this.__migrateFun("createTable", table);
    177 					} else {
    178 						var ret = new Promise();
    179 						ret.callback(hitch(this, "__migrateFun", "createTable", table));
    180 						this.__deferredMigrations.push(ret);
    181 						return ret;
    182 					}
    183 				},
    184
    185
    186 				/**
    187 				 * Drops a table
    188 				 *
    189 				 * @example
    190 				 *
    191 				 * //drop table in default database
    192 				 * moose.dropTable("test");
    193 				 *
    194 				 * //drop table in another database.
    195 				 * moose.dropTable("test", "otherDB");
    196 				 *
    197 				 * @param {String} table the name of the table
    198 				 * @param {String} [database] the database that the table resides in, if a database is not
    199 				 *                            provided then the default database is used.
    200 				 * @return {comb.Promise} There are two different results that the promise can be called back with.
    201 				 * <ol>
    202 				 *     <li>If a migration is currently being performed then the promise is called back with a
    203 				 *     function that should be called to actually perform the migration.</li>
    204 				 *     <li>If the called outside of a migration then the table is dropped immediately and
    205 				 *     the promise is called back with the result.</li>
    206 				 * </ol>
    207 				 **/
    208 				dropTable : function(table, database) {
    209 					table = new Table(table, {database : database});
    210 					//delete from the moose schema map
    211 					var db = database || this.client.database;
    212 					var schema = this.schemas[db];
    213 					if (schema && table in schema) {
    214 						delete schema[table];
    215 					}
    216 					if (!this.__inMigration) {
    217 						return this.__migrateFun("dropTable", table);
    218 					} else {
    219 						var ret = new Promise();
    220 						ret.callback(hitch(this, "__migrateFun", "dropTable", table));
    221 						this.__deferredMigrations.push(ret);
    222 						return ret;
    223 					}
    224 				},
    225
    226 				/**
    227 				 * Alters a table
    228 				 *
    229 				 * @example :
    230 				 *
    231 				 * //alter table in default database
    232 				 * moose.alterTable("test", function(table){
    233 				 *     table.rename("test2");
    234 				 *     table.addColumn("myColumn", types.STRING());
    235 				 * });
    236 				 *
    237 				 * //alter table in another database
    238 				 * moose.alterTable("test", "otherDB", function(table){
    239 				 *     table.rename("test2");
    240 				 *     table.addColumn("myColumn", types.STRING());
    241 				 * });
    242 				 *
    243 				 * @param {String} name The name of the table to alter.
    244 				 * @param {String} [database] the database that the table resides in, if a database is not
    245 				 *                            provided then the default database is used.
    246 				 * @param {Function} cb the function to execute with the table passed in as the first argument.
    247 				 *
    248 				 * @return {comb.Promise} There are two different results that the promise can be called back with.
    249 				 * <ol>
    250 				 *     <li>If a migration is currently being performed then the promise is called back with a
    251 				 *     function that should be called to actually perform the migration.</li>
    252 				 *     <li>If the called outside of a migration then the table is altered immediately and
    253 				 *     the promise is called back with the result.</li>
    254 				 * </ol>
    255 				 * */
    256 				alterTable : function(name, database, cb) {
    257 					if (comb.isFunction(database)) {
    258 						cb = database;
    259 					}
    260 					var ret = new Promise();
    261 					var db = database || this.client.database;
    262 					var schema = this.schemas[db];
    263 					if (schema && name in schema) {
    264 						delete schema[name];
    265 					}
    266 					if (!this.__inMigration) {
    267 						this.loadSchema(name, db).then(function(table) {
    268 							cb(table);
    269 							this.__migrateFun("alterTable", table).then(hitch(ret, "callback"), hitch(ret, "errback"));
    270 						}, hitch(ret, "errback"));
    271 					} else {
    272 						this.loadSchema(name, db).then(hitch(this, function(table) {
    273 							cb(table);
    274 							ret.callback(hitch(this, "__migrateFun", "alterTable", table));
    275 							schema = this.schemas[db];
    276 							if (schema && name in schema) {
    277 								delete schema[name];
    278 							}
    279 						}), hitch(ret, "errback"));
    280 						this.__deferredMigrations.push(ret);
    281 						return ret;
    282 					}
    283 				},
    284
    285 				/**
    286 				 * Performs the migration.
    287 				 *
    288 				 * @param {Function} fun the function to call to perform the migration
    289 				 *                  this is an up or down function in a migration file, i.e. exports.up|down.
    290 				 *
    291 				 * @return {comb.Promise} called back after the migration completes, and the next can continue.
    292 				 */
    293 				__doMigrate : function(fun) {
    294 					var ret = new Promise();
    295 					this.__inMigration = true;
    296 					fun();
    297 					//all calls should be deferred
    298 					if (this.__deferredMigrations.length) {
    299 						var defList = new PromiseList(this.__deferredMigrations);
    300 						defList.then(hitch(this, function(res) {
    301 							this.__deferredMigrations.length = 0;
    302 							//map my to retrieve the actual responses
    303 							//and call the associated method
    304 							var responses = [],	i = 0, len = res.length;
    305 							var next = hitch(this, function(r) {
    306 								responses.push(r);
    307 								if (i < len) {
    308 									f = res[i][1];
    309 									f().then(next, function(err) {
    310 										LOGGER.error(err);
    311 									});
    312 								} else {
    313 									ret.callback(responses);
    314 								}
    315 								i++;
    316 							});
    317 							next();
    318 							//reset for another migration
    319 							//listen for the promises to complete
    320 						}), function(err) {
    321 							for (var len = err.length, i = len - 1; i > 0; i--) {
    322 								LOGGER.error(err[i]);
    323 							}
    324 						});
    325 					} else {
    326 						ret.callback();
    327 					}
    328 					this.__inMigration = false;
    329 					return ret;
    330 				},
    331
    332 				/**
    333 				 * Perform a migration.
    334 				 *
    335 				 * @example
    336 				 *  moose.migrate({
    337 				 *               connection : {user : "test", password : "testpass", database : 'test'}, //Connection information to connect to the database.
    338 				 *               dir : "location of migrations", //Location of the directory containing the migrations.
    339 				 *               start : 0,//What version to start migrating at.
    340 				 *               end : 0,  //What version to stop migrations at.
    341 				 *               up : true //set to true to go up in migrations, false to rollback
    342 				 *  });
    343
    344 				 *
    345 				 * <p><b>NOTE</b></br>
    346 				 *  If you start at 0 and end at 0 your migrations will inlude the file only at "migrationName".0.js
    347 				 *  if you specify start : 0 and end 1 then your migrations will include files "migraiton0".0.js and "migration1".1.js,
    348 				 *  the names being whatever you specify before the *.0 and *.1
    349 				 *  </p>
    350
    351 				 *
    352 				 * <p>Example migration file</b>
    353 				 * @example
    354 				 *
    355 				 * //Up function used to migrate up a version
    356 				 * exports.up = function() {
    357 				 *   //create a new table
    358 				 *   moose.createTable("company", function(table) {
    359 				 *       //the table instance is passed in.
    360 				 *       //add columns
    361 				 *       table.column("id", types.INT({allowNull : false, autoIncrement : true}));
    362 				 *       table.column("companyName", types.VARCHAR({length : 20, allowNull : false}));
    363 				 *       //set the primary key
    364 				 *       table.primaryKey("id");
    365 				 *   });
    366 				 *   moose.createTable("employee", function(table) {
    367 				 *       table.column("id", types.INT({allowNull : false, autoIncrement : true}));
    368 				 *       table.column("firstname", types.VARCHAR({length : 20, allowNull : false}));
    369 				 *       table.column("lastname", types.VARCHAR({length : 20, allowNull : false}));
    370 				 *       table.column("midinitial", types.CHAR({length : 1}));
    371 				 *       table.column("gender", types.ENUM({enums : ["M", "F"], allowNull : false}));
    372 				 *       table.column("street", types.VARCHAR({length : 50, allowNull : false}));
    373 				 *       table.column("city", types.VARCHAR({length : 20, allowNull : false}));
    374 				 *       table.primaryKey("id");
    375 				 *   });
    376 				 *
    377 				 *   moose.createTable("companyEmployee", function(table) {
    378 				 *       table.column("companyId", types.INT({allowNull : false}));
    379 				 *       table.column("employeeId", types.INT({allowNull : false}));
    380 				 *       table.primaryKey(["companyId", "employeeId"]);
    381 				 *       table.foreignKey({companyId : {company : "id"}, employeeId : {employee : "id"}});
    382 				 *   });
    383 				 *   moose.createTable("works", function(table) {
    384 				 *       table.column("id", types.INT({allowNull : false, autoIncrement : true}));
    385 				 *       table.column("eid", types.INT({allowNull : false}));
    386 				 *       table.column("companyName", types.VARCHAR({length : 20, allowNull : false}));
    387 				 *       table.column("salary", types.DOUBLE({size : 8, digits : 2, allowNull : false}));
    388 				 *       table.primaryKey("id");
    389 				 *       table.foreignKey("eid", {employee : "id"});
    390 				 *
    391 				 *   });
    392 				 *};
    393 				 *
    394 				 * //Down function used to migrate down version
    395 				 *exports.down = function() {
    396 				 *    moose.dropTable("companyEmployee");
    397 				 *    moose.dropTable("works");
    398 				 *    moose.dropTable("employee");
    399 				 *    moose.dropTable("company");
    400 				 *};
    401 				 *
    402 				 * @param {Object} options the options to specify how to perform the migration.
    403 				 * @param {Object} options.connection : @see moose.createConnection
    404 				 * @param {String} options.dir Location of the directory where migrations are located.
    405 				 * @param {Boolean} [options.up = true] If true will migrate up otherwise down.
    406 				 * @param {Number} [options.start = 0] The migration to start at.
    407 				 * @param {Number} [options.end=Infinity] The migration to end at this, end is inclusive
    408 				 *
    409 				 * @return {comb.Promise} Called back after all migrations have completed.
    410 				 */
    411 				migrate : function(options) {
    412 					var promise = new Promise();
    413 					var dir, file, migrationDir = comb.isBoolean(options.up) ? options.up : true;
    414 					var start = typeof options.end == "number" ? options.start : 0,
    415 							end = typeof options.end == "number" ? options.end : Infinity;
    416
    417 					//if a coonection is not provided or we dont already have a connection
    418 					//throw an error
    419 					if (options.connection || this.__connectionReady) {
    420 						//set our current state, allows us to push any create/alter/delete
    421 						//transactions into our deferred
    422
    423 						//init our connecton information
    424 						!this.__connectionReady && this.createConnection(options.connection);
    425 						if (options.dir) dir = path.resolve(process.cwd(), options.dir);
    426 						//read the directory
    427 						fs.readdir(dir, hitch(this, function(err, files) {
    428 							var funs = [], fun = migrationDir ? "up" : "down", parts, num, file, extName = path.extname, baseName = path.basename;
    429 							for (var i = 0, len = files.length; i < len; i++) {
    430 								file = files[i];
    431 								if (extName(file) == ".js") {
    432 									//its a js file otherwise ignore
    433 									//split the path to get the num
    434 									parts = baseName(file, ".js").split(".");
    435 									//get the migration number
    436 									num = parseInt(parts.pop());
    437 									//if it is a valid index
    438 									if (!isNaN(num) && num >= start && num <= end) {
    439 										var cls = require(path.resolve(dir, file));
    440 										if (cls[fun]) {
    441 											//add it
    442 											funs[num - start] = hitch(cls, fun);
    443 										}
    444 									}
    445 								}
    446 							}
    447 							if (!migrationDir) {
    448 								funs.reverse();
    449 							}
    450 							if (funs.length) {
    451 								i = 0,len = funs.length;
    452 								var doMig = hitch(this, this.__doMigrate);
    453 								var next = hitch(this, function(res) {
    454 									if (i < len) {
    455 										doMig(funs[i]).then(next, function(err) {
    456 											LOGGER.log(err);
    457 										});
    458 									} else {
    459 										promise.callback();
    460 									}
    461 									i++;
    462 								});
    463 								next();
    464 							} else {
    465 								promise.callback();
    466 							}
    467 						}));
    468 					} else {
    469 						throw new Error("when migrating a connection is required");
    470 					}
    471 					return promise;
    472 				}
    473
    474 			}
    475 		});