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 		});