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