├── .npmignore ├── index.js ├── .gitignore ├── Makefile ├── .gitmodules ├── examples ├── cli │ └── migrations │ │ ├── db.js │ │ ├── 001-add-jane.js │ │ ├── 003-coolest-pet.js │ │ ├── 000-add-pets.js │ │ └── 002-add-owners.js └── migrate.js ├── lib ├── migration.js ├── migrate.js └── set.js ├── package.json ├── History.md ├── Readme.md ├── bin └── migrate └── test └── test.migrate.js /.npmignore: -------------------------------------------------------------------------------- 1 | support 2 | test 3 | examples 4 | *.sock 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib/migrate'); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.sock 4 | .migrate 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | @node test/test.migrate.js 4 | 5 | .PHONY: test -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "support/should"] 2 | path = support/should 3 | url = git://github.com/visionmedia/should.js.git 4 | -------------------------------------------------------------------------------- /examples/cli/migrations/db.js: -------------------------------------------------------------------------------- 1 | 2 | // bad example, but you get the point ;) 3 | 4 | // $ npm install redis 5 | // $ redis-server 6 | 7 | var redis = require('redis') 8 | , db = redis.createClient(); 9 | 10 | module.exports = db; -------------------------------------------------------------------------------- /examples/cli/migrations/001-add-jane.js: -------------------------------------------------------------------------------- 1 | 2 | var db = require('./db'); 3 | 4 | exports.up = function(next){ 5 | db.rpush('pets', 'jane', next); 6 | }; 7 | 8 | exports.down = function(next){ 9 | db.rpop('pets', next); 10 | }; 11 | -------------------------------------------------------------------------------- /examples/cli/migrations/003-coolest-pet.js: -------------------------------------------------------------------------------- 1 | 2 | var db = require('./db'); 3 | 4 | exports.up = function(next){ 5 | db.set('pets:coolest', 'tobi', next); 6 | }; 7 | 8 | exports.down = function(next){ 9 | db.del('pets:coolest', next); 10 | }; 11 | -------------------------------------------------------------------------------- /examples/cli/migrations/000-add-pets.js: -------------------------------------------------------------------------------- 1 | 2 | var db = require('./db'); 3 | 4 | exports.up = function(next){ 5 | db.rpush('pets', 'tobi'); 6 | db.rpush('pets', 'loki', next); 7 | }; 8 | 9 | exports.down = function(next){ 10 | db.rpop('pets'); 11 | db.rpop('pets', next); 12 | }; 13 | -------------------------------------------------------------------------------- /examples/cli/migrations/002-add-owners.js: -------------------------------------------------------------------------------- 1 | 2 | var db = require('./db'); 3 | 4 | exports.up = function(next){ 5 | db.rpush('owners', 'taylor'); 6 | db.rpush('owners', 'tj', next); 7 | }; 8 | 9 | exports.down = function(next){ 10 | db.rpop('owners'); 11 | db.rpop('owners', next); 12 | }; 13 | -------------------------------------------------------------------------------- /lib/migration.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * migrate - Migration 4 | * Copyright (c) 2010 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Expose `Migration`. 10 | */ 11 | 12 | module.exports = Migration; 13 | 14 | function Migration(title, up, down) { 15 | this.title = title; 16 | this.up = up; 17 | this.down = down; 18 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "migrate" 3 | , "version": "0.1.2" 4 | , "description": "Abstract migration framework for node" 5 | , "keywords": ["migrate", "migrations"] 6 | , "author": "TJ Holowaychuk " 7 | , "repository": "git://github.com/visionmedia/node-migrate" 8 | , "bin": { "migrate": "./bin/migrate" } 9 | , "devDependencies": { 10 | "should": ">= 0.0.1" 11 | } 12 | , "main": "index" 13 | , "engines": { "node": ">= 0.4.x < 0.7.0" } 14 | } -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.1.2 / 2012-03-15 3 | ================== 4 | 5 | * 0.7.x support 6 | 7 | 0.1.1 / 2011-12-04 8 | ================== 9 | 10 | * Fixed a typo [kishorenc] 11 | 12 | 0.1.0 / 2011-12-03 13 | ================== 14 | 15 | * Added support for incremental migrations. [Kishore Nallan] 16 | 17 | 0.0.5 / 2011-11-07 18 | ================== 19 | 20 | * 0.6.0 support 21 | 22 | 0.0.4 / 2011-09-12 23 | ================== 24 | 25 | * Fixed: load js files only [aheckmann] 26 | 27 | 0.0.3 / 2011-09-09 28 | ================== 29 | 30 | * Fixed initial `create` support 31 | 32 | 0.0.2 / 2011-09-09 33 | ================== 34 | 35 | * Fixed `make test` 36 | 37 | 0.0.1 / 2011-04-24 38 | ================== 39 | 40 | * Initial release 41 | -------------------------------------------------------------------------------- /lib/migrate.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * migrate 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var Migration = require('./migration') 13 | , Set = require('./set'); 14 | 15 | /** 16 | * Expose the migrate function. 17 | */ 18 | 19 | exports = module.exports = migrate; 20 | 21 | /** 22 | * Library version. 23 | */ 24 | 25 | exports.version = '0.1.2'; 26 | 27 | function migrate(title, up, down) { 28 | // migration 29 | if ('string' == typeof title && up && down) { 30 | migrate.set.migrations.push(new Migration(title, up, down)); 31 | // specify migration file 32 | } else if ('string' == typeof title) { 33 | migrate.set = new Set(title); 34 | // no migration path 35 | } else if (!migrate.set) { 36 | throw new Error('must invoke migrate(path) before running migrations'); 37 | // run migrations 38 | } else { 39 | return migrate.set; 40 | } 41 | } -------------------------------------------------------------------------------- /examples/migrate.js: -------------------------------------------------------------------------------- 1 | 2 | // bad example, but you get the point ;) 3 | 4 | // $ npm install redis 5 | // $ redis-server 6 | 7 | var migrate = require('../') 8 | , redis = require('redis') 9 | , db = redis.createClient(); 10 | 11 | migrate(__dirname + '/.migrate'); 12 | 13 | migrate('add pets', function(next){ 14 | db.rpush('pets', 'tobi'); 15 | db.rpush('pets', 'loki', next); 16 | }, function(next){ 17 | db.rpop('pets'); 18 | db.rpop('pets', next); 19 | }); 20 | 21 | migrate('add jane', function(next){ 22 | db.rpush('pets', 'jane', next); 23 | }, function(next){ 24 | db.rpop('pets', next); 25 | }); 26 | 27 | migrate('add owners', function(next){ 28 | db.rpush('owners', 'taylor'); 29 | db.rpush('owners', 'tj', next); 30 | }, function(next){ 31 | db.rpop('owners'); 32 | db.rpop('owners', next); 33 | }); 34 | 35 | migrate('coolest pet', function(next){ 36 | db.set('pets:coolest', 'tobi', next); 37 | }, function(next){ 38 | db.del('pets:coolest', next); 39 | }); 40 | 41 | var set = migrate(); 42 | 43 | console.log(); 44 | set.on('save', function(){ 45 | console.log(); 46 | }); 47 | 48 | set.on('migration', function(migration, direction){ 49 | console.log(' \033[90m%s\033[0m \033[36m%s\033[0m', direction, migration.title); 50 | }); 51 | 52 | set.up(function(err){ 53 | if (err) throw err; 54 | process.exit(); 55 | }); 56 | -------------------------------------------------------------------------------- /lib/set.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * migrate - Set 4 | * Copyright (c) 2010 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var EventEmitter = require('events').EventEmitter 13 | , fs = require('fs'); 14 | 15 | /** 16 | * Expose `Set`. 17 | */ 18 | 19 | module.exports = Set; 20 | 21 | /** 22 | * Initialize a new migration `Set` with the given `path` 23 | * which is used to store data between migrations. 24 | * 25 | * @param {String} path 26 | * @api private 27 | */ 28 | 29 | function Set(path) { 30 | this.migrations = []; 31 | this.path = path; 32 | this.pos = 0; 33 | }; 34 | 35 | /** 36 | * Inherit from `EventEmitter.prototype`. 37 | */ 38 | 39 | Set.prototype.__proto__ = EventEmitter.prototype; 40 | 41 | /** 42 | * Save the migration data and call `fn(err)`. 43 | * 44 | * @param {Function} fn 45 | * @api public 46 | */ 47 | 48 | Set.prototype.save = function(fn){ 49 | var self = this 50 | , json = JSON.stringify(this); 51 | fs.writeFile(this.path, json, function(err){ 52 | self.emit('save'); 53 | fn && fn(err); 54 | }); 55 | }; 56 | 57 | /** 58 | * Load the migration data and call `fn(err, obj)`. 59 | * 60 | * @param {Function} fn 61 | * @return {Type} 62 | * @api public 63 | */ 64 | 65 | Set.prototype.load = function(fn){ 66 | this.emit('load'); 67 | fs.readFile(this.path, 'utf8', function(err, json){ 68 | if (err) return fn(err); 69 | try { 70 | fn(null, JSON.parse(json)); 71 | } catch (err) { 72 | fn(err); 73 | } 74 | }); 75 | }; 76 | 77 | /** 78 | * Run down migrations and call `fn(err)`. 79 | * 80 | * @param {Function} fn 81 | * @api public 82 | */ 83 | 84 | Set.prototype.down = function(fn, migrationName){ 85 | this.migrate('down', fn, migrationName); 86 | }; 87 | 88 | /** 89 | * Run up migrations and call `fn(err)`. 90 | * 91 | * @param {Function} fn 92 | * @api public 93 | */ 94 | 95 | Set.prototype.up = function(fn, migrationName){ 96 | this.migrate('up', fn, migrationName); 97 | }; 98 | 99 | /** 100 | * Migrate in the given `direction`, calling `fn(err)`. 101 | * 102 | * @param {String} direction 103 | * @param {Function} fn 104 | * @api public 105 | */ 106 | 107 | Set.prototype.migrate = function(direction, fn, migrationName){ 108 | var self = this; 109 | fn = fn || function(){}; 110 | this.load(function(err, obj){ 111 | if (err) { 112 | if ('ENOENT' != err.code) return fn(err); 113 | } else { 114 | self.pos = obj.pos; 115 | } 116 | self._migrate(direction, fn, migrationName); 117 | }); 118 | }; 119 | 120 | /** 121 | * Get index of given migration in list of migrations 122 | * 123 | * @api private 124 | */ 125 | 126 | function positionOfMigration(migrations, filename) { 127 | for(var i=0; i < migrations.length; ++i) { 128 | if (migrations[i].title == filename) return i; 129 | } 130 | return -1; 131 | } 132 | 133 | /** 134 | * Perform migration. 135 | * 136 | * @api private 137 | */ 138 | 139 | Set.prototype._migrate = function(direction, fn, migrationName){ 140 | var self = this 141 | , migrations 142 | , migrationPos; 143 | 144 | if (!migrationName) { 145 | migrationPos = direction == 'up' ? this.migrations.length : 0; 146 | } else if ((migrationPos = positionOfMigration(this.migrations, migrationName)) == -1) { 147 | console.error("Could not find migration: " + migrationName); 148 | process.exit(1); 149 | } 150 | 151 | switch (direction) { 152 | case 'up': 153 | migrations = this.migrations.slice(this.pos, migrationPos+1); 154 | this.pos += migrations.length; 155 | break; 156 | case 'down': 157 | migrations = this.migrations.slice(migrationPos, this.pos).reverse(); 158 | this.pos -= migrations.length; 159 | break; 160 | } 161 | 162 | function next(err, migration) { 163 | // error from previous migration 164 | if (err) return fn(err); 165 | 166 | // done 167 | if (!migration) { 168 | self.emit('complete'); 169 | self.save(fn); 170 | return; 171 | } 172 | 173 | self.emit('migration', migration, direction); 174 | migration[direction](function(err){ 175 | next(err, migrations.shift()); 176 | }); 177 | } 178 | 179 | next(null, migrations.shift()); 180 | }; 181 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # migrate 3 | 4 | Abstract migration framework for node 5 | 6 | ## Installation 7 | 8 | $ npm install migrate 9 | 10 | ## Usage 11 | 12 | ``` 13 | Usage: migrate [options] [command] 14 | 15 | Options: 16 | 17 | -c, --chdir change the working directory 18 | 19 | Commands: 20 | 21 | down migrate down 22 | up migrate up (the default command) 23 | create [title] create a new migration file with optional [title] 24 | 25 | ``` 26 | 27 | ## Creating Migrations 28 | 29 | To create a migration, execute `migrate create` with an optional title. `node-migrate` will create a node module within `./migrations/` which contains the following two exports: 30 | 31 | exports.up = function(next){ 32 | next(); 33 | }; 34 | 35 | exports.down = function(next){ 36 | next(); 37 | }; 38 | 39 | All you have to do is populate these, invoking `next()` when complete, and you are ready to migrate! 40 | 41 | For example: 42 | 43 | $ migrate create add-pets 44 | $ migrate create add-owners 45 | 46 | The first call creates `./migrations/000-add-pets.js`, which we can populate: 47 | 48 | var db = require('./db'); 49 | 50 | exports.up = function(next){ 51 | db.rpush('pets', 'tobi'); 52 | db.rpush('pets', 'loki'); 53 | db.rpush('pets', 'jane', next); 54 | }; 55 | 56 | exports.down = function(next){ 57 | db.rpop('pets'); 58 | db.rpop('pets', next); 59 | }; 60 | 61 | The second creates `./migrations/001-add-owners.js`, which we can populate: 62 | 63 | var db = require('./db'); 64 | 65 | exports.up = function(next){ 66 | db.rpush('owners', 'taylor'); 67 | db.rpush('owners', 'tj', next); 68 | }; 69 | 70 | exports.down = function(next){ 71 | db.rpop('owners'); 72 | db.rpop('owners', next); 73 | }; 74 | 75 | ## Running Migrations 76 | 77 | When first running the migrations, all will be executed in sequence. 78 | 79 | $ migrate 80 | up : migrations/000-add-pets.js 81 | up : migrations/001-add-jane.js 82 | up : migrations/002-add-owners.js 83 | up : migrations/003-coolest-pet.js 84 | migration : complete 85 | 86 | Subsequent attempts will simply output "complete", as they have already been executed in this machine. `node-migrate` knows this because it stores the current state in `./migrations/.migrate` which is typically a file that SCMs like GIT should ignore. 87 | 88 | $ migrate 89 | migration : complete 90 | 91 | If we were to create another migration using `migrate create`, and then execute migrations again, we would execute only those not previously executed: 92 | 93 | $ migrate 94 | up : migrates/004-coolest-owner.js 95 | 96 | You can also run migrations incrementally by specifying a migration. 97 | 98 | $ migrate up 002-coolest-pet.js 99 | up : migrations/000-add-pets.js 100 | up : migrations/001-add-jane.js 101 | up : migrations/002-add-owners.js 102 | migration : complete 103 | 104 | This will run up-migrations upto (and including) `002-coolest-pet.js`. Similarly you can run down-migrations upto (and including) a specific migration, instead of migrating all the way down. 105 | 106 | $ migrate down 001-add-jane.js 107 | down : migrations/002-add-owners.js 108 | down : migrations/001-add-jane.js 109 | migration : complete 110 | 111 | ## License 112 | 113 | (The MIT License) 114 | 115 | Copyright (c) 2011 TJ Holowaychuk <tj@vision-media.ca> 116 | 117 | Permission is hereby granted, free of charge, to any person obtaining 118 | a copy of this software and associated documentation files (the 119 | 'Software'), to deal in the Software without restriction, including 120 | without limitation the rights to use, copy, modify, merge, publish, 121 | distribute, sublicense, and/or sell copies of the Software, and to 122 | permit persons to whom the Software is furnished to do so, subject to 123 | the following conditions: 124 | 125 | The above copyright notice and this permission notice shall be 126 | included in all copies or substantial portions of the Software. 127 | 128 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 129 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 130 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 131 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 132 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 133 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 134 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /bin/migrate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var migrate = require('../') 8 | , join = require('path').join 9 | , fs = require('fs'); 10 | 11 | /** 12 | * Arguments. 13 | */ 14 | 15 | var args = process.argv.slice(2); 16 | 17 | /** 18 | * Option defaults. 19 | */ 20 | 21 | var options = { args: [] }; 22 | 23 | /** 24 | * Current working directory. 25 | */ 26 | 27 | var cwd; 28 | 29 | /** 30 | * Usage information. 31 | */ 32 | 33 | var usage = [ 34 | '' 35 | , ' Usage: migrate [options] [command]' 36 | , '' 37 | , ' Options:' 38 | , '' 39 | , ' -c, --chdir change the working directory' 40 | , '' 41 | , ' Commands:' 42 | , '' 43 | , ' down [name] migrate down till given migration' 44 | , ' up [name] migrate up till given migration (the default command)' 45 | , ' create [title] create a new migration file with optional [title]' 46 | , '' 47 | ].join('\n'); 48 | 49 | /** 50 | * Migration template. 51 | */ 52 | 53 | var template = [ 54 | '' 55 | , 'exports.up = function(next){' 56 | , ' next();' 57 | , '};' 58 | , '' 59 | , 'exports.down = function(next){' 60 | , ' next();' 61 | , '};' 62 | , '' 63 | ].join('\n'); 64 | 65 | // require an argument 66 | 67 | function required() { 68 | if (args.length) return args.shift(); 69 | abort(arg + ' requires an argument'); 70 | } 71 | 72 | // abort with a message 73 | 74 | function abort(msg) { 75 | console.error(' %s', msg); 76 | process.exit(1); 77 | } 78 | 79 | // parse arguments 80 | 81 | var arg; 82 | while (args.length) { 83 | arg = args.shift(); 84 | switch (arg) { 85 | case '-h': 86 | case '--help': 87 | case 'help': 88 | console.log(usage); 89 | process.exit(); 90 | break; 91 | case '-c': 92 | case '--chdir': 93 | process.chdir(cwd = required()); 94 | break; 95 | default: 96 | if (options.command) { 97 | options.args.push(arg); 98 | } else { 99 | options.command = arg; 100 | } 101 | } 102 | } 103 | 104 | /** 105 | * Load migrations. 106 | */ 107 | 108 | function migrations() { 109 | return fs.readdirSync('migrations').filter(function(file){ 110 | return file.match(/^\d+.*\.js$/); 111 | }).sort().map(function(file){ 112 | return 'migrations/' + file; 113 | }); 114 | } 115 | 116 | /** 117 | * Log a keyed message. 118 | */ 119 | 120 | function log(key, msg) { 121 | console.log(' \033[90m%s :\033[0m \033[36m%s\033[0m', key, msg); 122 | } 123 | 124 | /** 125 | * Slugify the given `str`. 126 | */ 127 | 128 | function slugify(str) { 129 | return str.replace(/\s+/g, '-'); 130 | } 131 | 132 | // create ./migrations 133 | 134 | try { 135 | fs.mkdirSync('migrations', 0774); 136 | } catch (err) { 137 | // ignore 138 | } 139 | 140 | // commands 141 | 142 | var commands = { 143 | 144 | /** 145 | * up [name] 146 | */ 147 | 148 | up: function(migrationName){ 149 | performMigration('up', migrationName); 150 | }, 151 | 152 | /** 153 | * down [name] 154 | */ 155 | 156 | down: function(migrationName){ 157 | performMigration('down', migrationName); 158 | }, 159 | 160 | /** 161 | * create [title] 162 | */ 163 | 164 | create: function(){ 165 | var migrations = fs.readdirSync('migrations').filter(function(file){ 166 | return file.match(/^\d+/); 167 | }).map(function(file){ 168 | return parseInt(file.match(/^(\d+)/)[1], 10); 169 | }).sort(function(a, b){ 170 | return a - b; 171 | }); 172 | 173 | var curr = pad((migrations.pop() || 0) + 1) 174 | , title = slugify([].slice.call(arguments).join(' ')); 175 | title = title ? curr + '-' + title : curr; 176 | create(title); 177 | } 178 | }; 179 | 180 | /** 181 | * Pad the given number. 182 | * 183 | * @param {Number} n 184 | * @return {String} 185 | */ 186 | 187 | function pad(n) { 188 | return Array(4 - n.toString().length).join('0') + n; 189 | } 190 | 191 | /** 192 | * Create a migration with the given `name`. 193 | * 194 | * @param {String} name 195 | */ 196 | 197 | function create(name) { 198 | var path = 'migrations/' + name + '.js'; 199 | log('create', join(cwd, path)); 200 | fs.writeFileSync(path, template); 201 | } 202 | 203 | /** 204 | * Perform a migration in the given `direction`. 205 | * 206 | * @param {Number} direction 207 | */ 208 | 209 | function performMigration(direction, migrationName) { 210 | migrate('migrations/.migrate'); 211 | migrations().forEach(function(path){ 212 | var mod = require(process.cwd() + '/' + path); 213 | migrate(path, mod.up, mod.down); 214 | }); 215 | 216 | var set = migrate(); 217 | 218 | set.on('migration', function(migration, direction){ 219 | log(direction, migration.title); 220 | }); 221 | 222 | set.on('save', function(){ 223 | log('migration', 'complete'); 224 | process.exit(); 225 | }); 226 | 227 | var migrationPath = migrationName 228 | ? join('migrations', migrationName) 229 | : migrationName; 230 | 231 | set[direction](null, migrationPath); 232 | } 233 | 234 | // invoke command 235 | 236 | var command = options.command || 'up'; 237 | if (!(command in commands)) abort('unknown command "' + command + '"'); 238 | command = commands[command]; 239 | command.apply(this, options.args); 240 | -------------------------------------------------------------------------------- /test/test.migrate.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var migrate = require('../') 7 | , should = require('should') 8 | , fs = require('fs'); 9 | 10 | // remove migration file 11 | 12 | try { 13 | fs.unlinkSync(__dirname + '/.migrate'); 14 | } catch (err) { 15 | // ignore 16 | } 17 | 18 | // dummy db 19 | 20 | var db = { pets: [] }; 21 | 22 | // dummy migrations 23 | 24 | migrate(__dirname + '/.migrate'); 25 | 26 | migrate('add guy ferrets', function(next){ 27 | db.pets.push({ name: 'tobi' }); 28 | db.pets.push({ name: 'loki' }); 29 | next(); 30 | }, function(next){ 31 | db.pets.pop(); 32 | db.pets.pop(); 33 | next(); 34 | }); 35 | 36 | migrate('add girl ferrets', function(next){ 37 | db.pets.push({ name: 'jane' }); 38 | next(); 39 | }, function(next){ 40 | db.pets.pop(); 41 | next(); 42 | }); 43 | 44 | migrate('add emails', function(next){ 45 | db.pets.forEach(function(pet){ 46 | pet.email = pet.name + '@learnboost.com'; 47 | }); 48 | next(); 49 | }, function(next){ 50 | db.pets.forEach(function(pet){ 51 | delete pet.email; 52 | }); 53 | next(); 54 | }); 55 | 56 | // tests 57 | 58 | migrate.version.should.match(/^\d+\.\d+\.\d+$/); 59 | 60 | // test migrating up / down several times 61 | 62 | var set = migrate(); 63 | 64 | set.up(function(){ 65 | assertPets(); 66 | set.up(function(){ 67 | assertPets(); 68 | set.down(function(){ 69 | assertNoPets(); 70 | set.down(function(){ 71 | assertNoPets(); 72 | set.up(function(){ 73 | assertPets(); 74 | testNewMigrations(); 75 | }); 76 | }); 77 | }); 78 | }); 79 | }); 80 | 81 | // test adding / running new migrations 82 | 83 | function testNewMigrations() { 84 | migrate('add dogs', function(next){ 85 | db.pets.push({ name: 'simon' }); 86 | db.pets.push({ name: 'suki' }); 87 | next(); 88 | }, function(next){ 89 | db.pets.pop(); 90 | db.pets.pop(); 91 | next(); 92 | }); 93 | 94 | set.up(function(){ 95 | assertPets.withDogs(); 96 | set.up(function(){ 97 | assertPets.withDogs(); 98 | set.down(function(){ 99 | assertNoPets(); 100 | testMigrationEvents(); 101 | }); 102 | }); 103 | }); 104 | } 105 | 106 | // test events 107 | 108 | function testMigrationEvents() { 109 | migrate('adjust emails', function(next){ 110 | db.pets.forEach(function(pet){ 111 | if (pet.email) 112 | pet.email = pet.email.replace('learnboost.com', 'lb.com'); 113 | }); 114 | next(); 115 | }, function(next){ 116 | db.pets.forEach(function(pet){ 117 | if (pet.email) 118 | pet.email = pet.email.replace('lb.com', 'learnboost.com'); 119 | }); 120 | next(); 121 | }); 122 | 123 | var migrations = [] 124 | , completed = 0 125 | , expectedMigrations = [ 126 | 'add guy ferrets' 127 | , 'add girl ferrets' 128 | , 'add emails' 129 | , 'add dogs' 130 | , 'adjust emails']; 131 | 132 | set.on('migration', function(migration, direction){ 133 | migrations.push(migration.title); 134 | direction.should.be.a('string'); 135 | }); 136 | 137 | set.on('complete', function(){ 138 | ++completed; 139 | }); 140 | 141 | set.up(function(){ 142 | db.pets[0].email.should.equal('tobi@lb.com'); 143 | migrations.should.eql(expectedMigrations); 144 | completed.should.equal(1); 145 | 146 | migrations = []; 147 | set.down(function(){ 148 | migrations.should.eql(expectedMigrations.reverse()); 149 | completed.should.equal(2); 150 | assertNoPets(); 151 | testNamedMigrations(); 152 | }); 153 | }); 154 | } 155 | 156 | // test migrations when migration name is given 157 | 158 | function testNamedMigrations() { 159 | assertNoPets(); 160 | set.up(function() { 161 | assertFirstMigration(); 162 | set.up(function() { 163 | assertSecondMigration(); 164 | set.down(function() { 165 | assertFirstMigration(); 166 | set.up(function() { 167 | assertSecondMigration(); 168 | set.down(function() { 169 | set.pos.should.equal(1); 170 | }, 'add girl ferrets'); 171 | },'add girl ferrets'); 172 | }, 'add girl ferrets'); 173 | },'add girl ferrets'); 174 | }, 'add guy ferrets'); 175 | } 176 | 177 | // helpers 178 | 179 | function assertNoPets() { 180 | db.pets.should.be.empty; 181 | set.pos.should.equal(0); 182 | } 183 | 184 | function assertPets() { 185 | db.pets.should.have.length(3); 186 | db.pets[0].name.should.equal('tobi'); 187 | db.pets[0].email.should.equal('tobi@learnboost.com'); 188 | set.pos.should.equal(3); 189 | } 190 | 191 | function assertFirstMigration() { 192 | db.pets.should.have.length(2); 193 | db.pets[0].name.should.equal('tobi'); 194 | db.pets[1].name.should.equal('loki'); 195 | set.pos.should.equal(1); 196 | } 197 | 198 | function assertSecondMigration() { 199 | db.pets.should.have.length(3); 200 | db.pets[0].name.should.equal('tobi'); 201 | db.pets[1].name.should.equal('loki'); 202 | db.pets[2].name.should.equal('jane'); 203 | set.pos.should.equal(2); 204 | } 205 | 206 | assertPets.withDogs = function(){ 207 | db.pets.should.have.length(5); 208 | db.pets[0].name.should.equal('tobi'); 209 | db.pets[0].email.should.equal('tobi@learnboost.com'); 210 | db.pets[4].name.should.equal('suki'); 211 | }; 212 | 213 | // status 214 | 215 | process.on('exit', function(){ 216 | console.log('\n ok\n'); 217 | }); --------------------------------------------------------------------------------