├── .gitignore ├── bin └── mongo-migrate.js ├── default-config.json ├── package.json ├── lib ├── migrate.js ├── db.js └── set.js ├── additional-license.md ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | \node_modules 2 | \.idea -------------------------------------------------------------------------------- /bin/mongo-migrate.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../index.js'); 4 | -------------------------------------------------------------------------------- /default-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mongoAppDb" : { 3 | "host" : "localhost", 4 | "db" : "app-db" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongodb-migrate", 3 | "version": "2.0.2", 4 | "description": "Migration framework for mongo in node", 5 | "keywords": [ 6 | "mongo", 7 | "migrate", 8 | "migrations" 9 | ], 10 | "author": { 11 | "name": "Austin Floyd", 12 | "email": "texsc98@gmail.com" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/afloyd/mongo-migrate" 17 | }, 18 | "dependencies": { 19 | "mongodb": ">=1.3.19 <3.0", 20 | "verror": "^1.6.0" 21 | }, 22 | "main": "index", 23 | "engines": { 24 | "node": ">= 0.8.x" 25 | }, 26 | "bin": { 27 | "mongo-migrate": "./bin/mongo-migrate.js" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/migrate.js: -------------------------------------------------------------------------------- 1 | var Set = require('./set'); 2 | 3 | exports = module.exports = migrate; 4 | 5 | function Migration(title, up, down, num) { 6 | this.num = num; 7 | this.title = title; 8 | this.up = up; 9 | this.down = down; 10 | } 11 | 12 | exports.version = '0.0.1'; 13 | 14 | function migrate(opts) { 15 | opts = opts || {}; 16 | // migration 17 | if ('string' == typeof opts.title && opts.up && opts.down) { 18 | migrate.set.migrations.push(new Migration(opts.title, opts.up, opts.down, opts.num)); 19 | // specify migration file 20 | } else if (opts.migrationCollection) { 21 | migrate.set = new Set(opts.db, opts.migrationCollection); 22 | // no migration path 23 | } else if (!migrate.set) { 24 | throw new Error('must invoke migrate(path) before running migrations'); 25 | // run migrations 26 | } else { 27 | return migrate.set; 28 | } 29 | } -------------------------------------------------------------------------------- /additional-license.md: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2011 TJ Holowaychuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/db.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | getConnection: getConnection 3 | }; 4 | 5 | function getDbOpts(opts) { 6 | opts = opts || { 7 | host: 'localhost', 8 | db: 'my-app', 9 | port: 27017 10 | }; 11 | opts.port = opts.port || 27017; 12 | return opts; 13 | } 14 | 15 | function getReplicaSetServers(opts, mongodb) { 16 | var replServers = opts.replicaSet.map(function(replicaSet) { 17 | var split = replicaSet.split(":"); 18 | var host = split[0] || 'localhost'; 19 | var port = parseInt(split[1]) || 27017; 20 | return new mongodb.Server(host, port); 21 | }); 22 | return new mongodb.ReplSet(replServers); 23 | } 24 | 25 | function getMigrationsCollection(db, mongodb) { 26 | // for mongodb 2.x 27 | if (typeof db.collection !== 'undefined') { 28 | return db.collection('migrations'); 29 | } 30 | 31 | return new mongodb.Collection(db, 'migrations'); 32 | } 33 | 34 | function getConnection(opts, cb) { 35 | var mongodb = require('mongodb'); 36 | 37 | if (opts.connectionString) { 38 | var MongoClient = mongodb.MongoClient; 39 | MongoClient.connect(opts.connectionString, function(err, db) { 40 | if (err) { 41 | return cb(err); 42 | } 43 | 44 | console.log("Connected correctly to server"); 45 | 46 | cb(null, { 47 | connection: db, 48 | migrationCollection: getMigrationsCollection(db, mongodb) 49 | }); 50 | }); 51 | return; 52 | } 53 | 54 | opts = getDbOpts(opts); 55 | var svr = null; 56 | 57 | //if replicaSet option is set then use a replicaSet connection 58 | if (opts.replicaSet) { 59 | svr = getReplicaSetServers(opts, mongodb); 60 | } else { 61 | //simple connection 62 | svr = new mongodb.Server(opts.host, opts.port, opts.server || {}); 63 | } 64 | 65 | new mongodb.Db(opts.db, svr, {safe: true}).open(function(err, db) { 66 | if (err) { 67 | return cb(err); 68 | } 69 | 70 | var complete = function(authErr, res) { 71 | if (authErr) { 72 | return cb(authErr); 73 | } 74 | 75 | cb(null, { 76 | connection: db, 77 | migrationCollection: getMigrationsCollection(db, mongodb) 78 | }); 79 | }; 80 | 81 | if (opts.username) { 82 | db.authenticate(opts.username, opts.password, opts.authOptions || {}, complete); 83 | } else { 84 | complete(null, null); 85 | } 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /lib/set.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * migrate - Set 3 | * Copyright (c) 2010 TJ Holowaychuk 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | var EventEmitter = require('events').EventEmitter, 11 | fs = require('fs'), 12 | mongodb = require('mongodb'); 13 | 14 | /** 15 | * Expose `Set`. 16 | */ 17 | module.exports = Set; 18 | 19 | /** 20 | * Initialize a new migration `Set` with the given `path` 21 | * which is used to store data between migrations. 22 | * 23 | * @param {Object} db 24 | * @param {Object} migrationCollection 25 | * @api private 26 | */ 27 | function Set(db, migrationCollection) { 28 | this.db = db; 29 | this.migrations = []; 30 | this.pos = 0; 31 | this.migrationCollection = migrationCollection; 32 | } 33 | 34 | /** 35 | * Inherit from `EventEmitter.prototype`. 36 | */ 37 | Set.prototype.__proto__ = EventEmitter.prototype; 38 | 39 | /** 40 | * Save the migration data and call `fn(err)`. 41 | * 42 | * @param {Function} fn 43 | * @api public 44 | */ 45 | Set.prototype.save = function (fn) { 46 | this.emit('save'); 47 | fn && fn(undefined); 48 | }; 49 | 50 | /** 51 | * Load the migration data and call `fn(err, obj)`. 52 | * 53 | * @param {Function} fn 54 | * @return {Type} 55 | * @api public 56 | */ 57 | Set.prototype.load = function (fn) { 58 | this.emit('load'); 59 | fn(null, {}); 60 | }; 61 | 62 | /** 63 | * Run down migrations and call `fn(err)`. 64 | * 65 | * @param {Function} fn 66 | * @api public 67 | */ 68 | Set.prototype.down = function (fn, lastMigrationNum) { 69 | this.migrate('down', fn, lastMigrationNum); 70 | }; 71 | 72 | /** 73 | * Run up migrations and call `fn(err)`. 74 | * 75 | * @param {Function} fn 76 | * @api public 77 | */ 78 | Set.prototype.up = function (fn, lastMigrationNum) { 79 | this.migrate('up', fn, lastMigrationNum); 80 | }; 81 | 82 | /** 83 | * Migrate in the given `direction`, calling `fn(err)`. 84 | * 85 | * @param {String} direction 86 | * @param {Function} fn 87 | * @param {Number} lastMigrationNum 88 | * @api public 89 | */ 90 | Set.prototype.migrate = function (direction, fn, lastMigrationNum) { 91 | var self = this; 92 | fn = fn || function () {}; 93 | this.load(function (err, obj) { 94 | if (err) { 95 | if ('ENOENT' != err.code) { 96 | return fn(err); 97 | } 98 | } 99 | else { 100 | self.pos = obj.pos; 101 | } 102 | self._migrate(direction, fn, lastMigrationNum); 103 | }); 104 | }; 105 | 106 | /** 107 | * Get index of given migration in list of migrations 108 | * 109 | * @api private 110 | */ 111 | function positionOfMigration(migrations, filename) { 112 | for (var i = 0; i < migrations.length; ++i) { 113 | if (migrations[i].title == filename) { 114 | return i; 115 | } 116 | } 117 | return -1; 118 | } 119 | 120 | /** 121 | * Perform migration. 122 | * 123 | * @api private 124 | */ 125 | Set.prototype._migrate = function (direction, fn, lastMigrationNum) { 126 | var self = this, 127 | isDirectionUp = direction === 'up'/*, 128 | migrations, 129 | migrationPos*/; 130 | 131 | //No migrations to run 132 | if (!this.migrations.length) { 133 | self.emit('complete'); 134 | self.save(fn); 135 | return; 136 | } 137 | 138 | if (isDirectionUp) { 139 | //migrations = this.migrations.slice(this.pos, migrationPos + 1); 140 | this.pos += this.migrations.length; 141 | } else { 142 | //migrations = this.migrations.slice(migrationPos, this.pos);//.reverse(); 143 | this.pos -= this.migrations.length; 144 | } 145 | 146 | function next(err, migration) { 147 | // error from previous migration 148 | if (err) { 149 | return fn(err); 150 | } 151 | 152 | // done 153 | if (!migration) { 154 | self.emit('complete'); 155 | self.save(fn); 156 | return; 157 | } 158 | 159 | self.emit('migration', migration, direction); 160 | try { 161 | migration[direction](self.db, function (migrationErr) { 162 | if (migrationErr) { 163 | console.error('Error inside migration: ', migration.title, '\nError: ', migrationErr); 164 | //Revert this migration the opposite way 165 | return migration[direction === 'up' ? 'down' : 'up'](self.db, function (migrateDownErr) { 166 | if (migrateDownErr) { 167 | console.error('Error migrating back down: ', migration.title, '\nerr: ', migrateDownErr); 168 | console.error('The database may be in a corrupted state!'); 169 | } 170 | 171 | process.exit(1); 172 | }); 173 | } 174 | 175 | if (isDirectionUp) { 176 | self.migrationCollection.insert({ 177 | num: migration.num || parseInt(migration.title.match(/\d+/)[0].split('-')[0], 10), 178 | name: migration.title.split('/').pop().split('.js')[0], 179 | executed: new Date() 180 | }, function (err, objects) { 181 | if (err) { 182 | console.error('Error saving migration run: ', migration.title, '\nerr: ', err); 183 | process.exit(1); 184 | } 185 | 186 | next(err, self.migrations.shift()); 187 | }); 188 | } else { 189 | self.migrationCollection.findAndModify({ num: migration.num }, [], {}, { remove: true }, function (err, doc) { 190 | if (err) { 191 | console.error('Error removing migration from DB: ', migration.title, '\nerr: ', err); 192 | process.exit(1); 193 | } 194 | 195 | next(err, self.migrations.shift()); 196 | }); 197 | } 198 | }); 199 | } catch (ex) { 200 | console.error('Error inside migration: ', migration.title, '\nError: ', ex); 201 | migration[direction === 'up' ? 'down' : 'up'](self.db, function (migrateDownErr) { 202 | if (migrateDownErr) { 203 | console.error('Error migrating back down: ', migration.title, '\nerr: ', migrateDownErr); 204 | console.error('The database may be in a corrupted state!'); 205 | } 206 | 207 | process.exit(1); 208 | }); 209 | } 210 | } 211 | 212 | next(null, this.migrations.shift()); 213 | }; 214 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mongo-migrate 2 | #### NPM: mongodb-migrate 3 | ============= 4 | 5 | Built starting with a framework from: https://github.com/visionmedia/node-migrate 6 | 7 | 8 | ## Installation 9 | $ npm install mongodb-migrate 10 | 11 | ## Usage 12 | ``` 13 | Usage: node mongodb-migrate [options] [command] 14 | 15 | Options: 16 | -runmm, --runMongoMigrate Run the migration from the command line 17 | -c, --chdir Change the working directory (if you wish to store your migrations outside of this folder 18 | -dbc, --dbConfig Valid JSON string containing db settings (overrides -c, -cfg, & -dbn), like this: 19 | -dbc='{ "host": "localhost", "db": "mydbname", "port": 27017, "username": "myuser", "password": "mypwd"}' 20 | -cfg, --config DB config file name 21 | -dbn, --dbPropName Property name for the database connection in the config file. The configuration file should 22 | contain something like 23 | { 24 | appDb : { //appDb would be the dbPropName 25 | host: 'localhost', 26 | db: 'mydbname', 27 | //port: '27017' //include a port if necessary 28 | } 29 | } 30 | 31 | Commands: 32 | down [revision] migrate down (stop at optional revision name/number) 33 | up [revision] migrate up (stop at optional revision name/number) 34 | create [title] create a new migration file with optional [title] 35 | ``` 36 | 37 | ## Command-line usage 38 | NPM will install `mongodb-migrate` into a `node_modules` folder within the directory it is run. To use `mongodb-migrate` from the command line, you must always specify the relative path from your current directory to the `mongodb-migrate` directory for `node` to be able to find it. Such as: `node ./node_modules/mongodb-migrate -runmm create` (shown in examples below), or on *nix machines `node ./node_modules/mongodb-migrate -runmm create`. 39 | 40 | ## Creating Migrations 41 | To create a migration execute with `node ./node_modules/mongodb-migrate -runmm create` and optionally a title. mongodb-migrate will create a node module within `./migrations/` which contains the following two exports: 42 | ``` 43 | var mongodb = require('mongodb'); 44 | 45 | exports.up = function (db, next) { 46 | next(); 47 | }; 48 | 49 | exports.down = function (db, next) { 50 | next(); 51 | }; 52 | ``` 53 | 54 | All you have to do is populate these, invoking `next()` when complete, and you are ready to migrate! If you detect an error during the `exports.up` or `exports.down` pass next(err) and the migration will attempt to revert the opposite direction. If you're migrating up and error, it'll try to do that migration down. 55 | 56 | For example: 57 | 58 | ``` 59 | $ node ./node_modules/mongodb-migrate -runmm create add-pets 60 | $ node ./node_modules/mongodb-migrate -runmm create add-owners 61 | ``` 62 | 63 | The first call creates `./migrations/0005-add-pets.js`, which we can populate: 64 | ``` 65 | exports.up = function (db, next) { 66 | var pets = db.Collection('pets'); 67 | pets.insert({name: 'tobi'}, next); 68 | }; 69 | 70 | exports.down = function (db, next) { 71 | var pets = db.Collection('pets'); 72 | pets.findAndModify({name: 'tobi'}, [], {}, { remove: true }, next); 73 | }; 74 | ``` 75 | 76 | The second creates `./migrations/0010-add-owners.js`, which we can populate: 77 | ``` 78 | exports.up = function(db, next){ 79 | var owners = db.Collection('owners'); 80 | owners.insert({name: 'taylor'}, next); 81 | }; 82 | 83 | exports.down = function(db, next){ 84 | var owners = db.Collection('owners'); 85 | owners.findAndModify({name: 'taylor'}, [], {}, { remove: true }, next); 86 | }; 87 | ``` 88 | 89 | Note, for mongodb 2.x you need to use `db.collection('')` instead of `mongodb.Collection(db, '')`. 90 | 91 | ## Running Migrations 92 | When first running the migrations, all will be executed in sequence. 93 | 94 | ``` 95 | node ./node_modules/mongodb-migrate -runmm 96 | up : migrations/0005-add-pets.js 97 | up : migrations/0010-add-owners.js 98 | migration : complete 99 | ``` 100 | 101 | Subsequent attempts will simply output "complete", as they have already been executed on the given database. `mongodb-migrate` knows this because it stores migrations already run against the database in the `migrations` collection. 102 | ``` 103 | $ node mongodb-migrate -runmm 104 | migration : complete 105 | ``` 106 | 107 | If we were to create another migration using `node ./node_modules/mongodb-migrate -runmm create coolest-owner`, and then execute migrations again, we would execute only those not previously executed: 108 | ``` 109 | $ node ./node_modules/mongodb-migrate -runmm 110 | up : migrations/0015-coolest-owner 111 | ``` 112 | 113 | If we were to then migrate using `node ./node_modules/mongodb-migrate -runmm down 5`. This means to run from current revision, which in this case would be `0015-coolecst-owner`, down to revision number 5. Note that you can use either the revision number, or then full revision name `0005-add-pets` 114 | ``` 115 | $ node ./node_modules/mongodb-migrate -runmm down 5 116 | down : migrations/0015-coolest-owner 117 | down : migrations/0010-add-owners 118 | ``` 119 | 120 | ## Configuration 121 | ### JSON String 122 | This option allows you to pass in the database configuration on the command line, eliminating the need to store settings in a config file. The argument should be wrapped in single quotes, and all keys and string values must be in double quotes. Using this option overrides any of the other config options described below. The "port", "username", and "password" properties are optional. 123 | ``` 124 | $ node ./node_modules/mongodb-migrate -runmm -dbc '{ "host":"localhost","db":"mydbname","port":27017,"username":"myuser","password":"mypwd"}' up 125 | migration : complete 126 | ``` 127 | ### Working Directory 128 | The options for connecting to the database are read in from a file. You can configure where the file is read in from and where the migration directory root is by the `-c ` option. 129 | ``` 130 | $ node ./node_modules/mongodb-migrate -runmm -c ../.. up 131 | migration : complete 132 | ``` 133 | This would set the working directory two levels above the mongodb-migrate directory, such as if you included it into another project and it was nested in the node_modules folder. 134 | 135 | ### Config filename 136 | The default configuration filename is `default-config.json`. If you wish to use a different filename, use the `-cfg ` option: 137 | ``` 138 | $ node ./node_modules/mongodb-migrate -runmm -cfg my-config.json up 139 | migration : complete 140 | ``` 141 | 142 | ### Config file property name 143 | Inside the configuration file, mongodb-migrate expects the database connection information to be nested inside an object. The default object name is `mongoAppDb`. If you wish to change this you can use the `-dbn ` option: 144 | ``` 145 | $ node ./node_modules/mongodb-migrate -runmm -dbn dbSettings up 146 | migration : complete 147 | ``` 148 | This would tell mongodb-migrate your config file looks something like: 149 | ``` 150 | { 151 | "dbSettings": { 152 | "host": "localhost", 153 | "db": "myDatabaseName", 154 | //"port": 27017 //Specifying a port is optional 155 | } 156 | } 157 | ``` 158 | To connect to a replica set, use the `replicaSet` property: 159 | ``` 160 | { 161 | "dbSettings": { 162 | "replicaSet" : ["localhost:27017","localhost:27018","localhost:27019"], 163 | "db": "myDatabaseName", 164 | } 165 | } 166 | ``` 167 | or use `connectionString` property: 168 | ``` 169 | { 170 | "dbSettings": { 171 | "connectionString": "mongodb://user:password@mongo1.host.com:27018,mongo2.host.com:27018,mongo-arbiter.host.com:27018/?w=majority&wtimeoutMS=10000&journal=true" 172 | } 173 | } 174 | ``` 175 | `connectionString` has priority over the other properties 176 | 177 | All of these settings can be combined as desired, except for the up/down obviously ;) 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | # Licence 193 | 194 | (The MIT License) 195 | 196 | Copyright © 2017 Austin Floyd 197 | 198 | Permission is hereby granted, free of charge, to any person obtaining 199 | a copy of this software and associated documentation files (the 200 | 'Software'), to deal in the Software without restriction, including 201 | without limitation the rights to use, copy, modify, merge, publish, 202 | distribute, sublicense, and/or sell copies of the Software, and to 203 | permit persons to whom the Software is furnished to do so, subject to 204 | the following conditions: 205 | 206 | The above copyright notice and this permission notice shall be 207 | included in all copies or substantial portions of the Software. 208 | 209 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 210 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 211 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 212 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 213 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 214 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 215 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 216 | 217 | 218 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Arguments. 3 | */ 4 | var args = process.argv.slice(2); 5 | 6 | /** 7 | * Module dependencies. 8 | */ 9 | var migrate = require('./lib/migrate'), 10 | path = require('path'), 11 | join = path.join, 12 | fs = require('fs'), 13 | verror = require('verror'); 14 | 15 | /** 16 | * Option defaults. 17 | */ 18 | var options = { args: [] }; 19 | 20 | /** 21 | * Current working directory. 22 | */ 23 | var previousWorkingDirectory = process.cwd(); 24 | 25 | var configFileName = 'default-config.json', 26 | dbConfig = null, 27 | dbProperty = 'mongoAppDb'; 28 | 29 | /** 30 | * Usage information. 31 | */ 32 | var usage = [ 33 | '' 34 | , ' Usage: migrate [options] [command]' 35 | , '' 36 | , ' Options:' 37 | , '' 38 | , ' -runmm, --runMongoMigrate Run the migration from the command line' 39 | , ' -dbc, --dbConfig JSON string containing db settings (overrides -c, -cfg, & -dbn)' 40 | , ' -c, --chdir change the working directory' 41 | , ' -cfg, --config DB config file name' 42 | , ' -dbn, --dbPropName Property name for database connection in config file' 43 | , '' 44 | , ' Commands:' 45 | , '' 46 | , ' down [name] migrate down till given migration' 47 | , ' up [name] migrate up till given migration (the default command)' 48 | , ' create [title] create a new migration file with optional [title]' 49 | , '' 50 | ].join('\n'); 51 | 52 | /** 53 | * Migration template. 54 | */ 55 | 56 | var template = [ 57 | '' 58 | , 'var mongodb = require(\'mongodb\');' 59 | , '' 60 | , 'exports.up = function(db, next){' 61 | , ' next();' 62 | , '};' 63 | , '' 64 | , 'exports.down = function(db, next){' 65 | , ' next();' 66 | , '};' 67 | , '' 68 | ].join('\n'); 69 | 70 | /** 71 | * require an argument 72 | * @returns {*} 73 | */ 74 | function required() { 75 | if (args.length) return args.shift(); 76 | abort(arg + ' requires an argument'); 77 | } 78 | 79 | /** 80 | * abort with a message 81 | * @param msg 82 | */ 83 | function abort(msg) { 84 | console.error(' %s', msg); 85 | process.exit(1); 86 | } 87 | 88 | /** 89 | * Log a keyed message. 90 | */ 91 | function log(key, msg) { 92 | console.log(' \033[90m%s :\033[0m \033[36m%s\033[0m', key, msg); 93 | } 94 | 95 | /** 96 | * Slugify the given `str`. 97 | */ 98 | function slugify(str) { 99 | return str.replace(/\s+/g, '-'); 100 | } 101 | 102 | /** 103 | * Pad the given number. 104 | * 105 | * @param {Number} n 106 | * @return {String} 107 | */ 108 | function pad(n) { 109 | return Array(5 - n.toString().length).join('0') + n; 110 | } 111 | 112 | function runMongoMigrate(direction, migrationEnd, next) { 113 | if (direction) { 114 | options.command = direction; 115 | } 116 | 117 | if (migrationEnd) { 118 | options.args.push(migrationEnd); 119 | } 120 | 121 | if (next) { 122 | options.args.push(next); 123 | } 124 | 125 | /** 126 | * Load migrations. 127 | * @param {String} direction 128 | * @param {Number} lastMigrationNum 129 | * @param {Number} migrateTo 130 | */ 131 | function migrations(direction, lastMigrationNum, migrateTo) { 132 | var isDirectionUp = direction === 'up', 133 | hasMigrateTo = !!migrateTo, 134 | migrateToNum = hasMigrateTo ? parseInt(migrateTo, 10) : undefined, 135 | migrateToFound = !hasMigrateTo; 136 | 137 | var migrationsToRun = fs.readdirSync('migrations') 138 | .filter(function (file) { 139 | var formatCorrect = file.match(/^\d+.*\.js$/), 140 | migrationNum = formatCorrect && parseInt(file.match(/^\d+/)[0], 10), 141 | isRunnable = formatCorrect && isDirectionUp ? migrationNum > lastMigrationNum : migrationNum <= lastMigrationNum, 142 | isFile = fs.statSync(path.join('migrations', file)).isFile(); 143 | 144 | if (isFile && !formatCorrect) { 145 | console.log('"' + file + '" ignored. Does not match migration naming schema'); 146 | } 147 | 148 | return formatCorrect && isRunnable && isFile; 149 | }).sort(function (a, b) { 150 | var aMigrationNum = parseInt(a.match(/^\d+/)[0], 10), 151 | bMigrationNum = parseInt(b.match(/^\d+/)[0], 10); 152 | 153 | if (aMigrationNum > bMigrationNum) { 154 | return isDirectionUp ? 1 : -1; 155 | } 156 | if (aMigrationNum < bMigrationNum) { 157 | return isDirectionUp ? -1 : 1; 158 | } 159 | 160 | return 0; 161 | }).filter(function(file){ 162 | var formatCorrect = file.match(/^\d+.*\.js$/), 163 | migrationNum = formatCorrect && parseInt(file.match(/^\d+/)[0], 10), 164 | isRunnable = formatCorrect && isDirectionUp ? migrationNum > lastMigrationNum : migrationNum <= lastMigrationNum; 165 | 166 | if (hasMigrateTo) { 167 | if (migrateToNum === migrationNum) { 168 | migrateToFound = true; 169 | } 170 | 171 | if (isDirectionUp) { 172 | isRunnable = isRunnable && migrateToNum >= migrationNum; 173 | } else { 174 | isRunnable = isRunnable && migrateToNum < migrationNum; 175 | } 176 | } 177 | 178 | return formatCorrect && isRunnable; 179 | }).map(function(file){ 180 | return 'migrations/' + file; 181 | }); 182 | 183 | if (!migrateToFound) { 184 | return abort('migration `'+ migrateTo + '` not found!'); 185 | } 186 | 187 | return migrationsToRun; 188 | } 189 | 190 | // create ./migrations 191 | 192 | try { 193 | fs.mkdirSync('migrations', 0774); 194 | } catch (err) { 195 | // ignore 196 | } 197 | 198 | // commands 199 | 200 | var commands = { 201 | /** 202 | * up 203 | */ 204 | up: function(migrateTo, next){ 205 | performMigration('up', migrateTo, next); 206 | }, 207 | 208 | /** 209 | * down 210 | */ 211 | down: function(migrateTo, next){ 212 | performMigration('down', migrateTo, next); 213 | }, 214 | 215 | /** 216 | * create [title] 217 | */ 218 | create: function(){ 219 | var migrations = fs.readdirSync('migrations').filter(function(file){ 220 | return file.match(/^\d+/); 221 | }).map(function(file){ 222 | return parseInt(file.match(/^(\d+)/)[1], 10); 223 | }).sort(function(a, b){ 224 | return a - b; 225 | }); 226 | 227 | var curr = pad((migrations.pop() || 0) + 5), 228 | title = slugify([].slice.call(arguments).join(' ')); 229 | title = title ? curr + '-' + title : curr; 230 | create(title); 231 | } 232 | }; 233 | 234 | /** 235 | * Create a migration with the given `name`. 236 | * 237 | * @param {String} name 238 | */ 239 | function create(name) { 240 | var path = 'migrations/' + name + '.js'; 241 | log('create', join(process.cwd(), path)); 242 | fs.writeFileSync(path, template); 243 | } 244 | 245 | /** 246 | * Perform a migration in the given `direction`. 247 | * 248 | * @param {String} direction 249 | */ 250 | function performMigration(direction, migrateTo, next) { 251 | if (!next && 252 | Object.prototype.toString.call(migrateTo) === '[object Function]') { 253 | next = migrateTo; 254 | migrateTo = undefined; 255 | } 256 | 257 | if (!next) { 258 | next = function(err) { 259 | if (err) { 260 | console.error(err); 261 | process.exit(1); 262 | } else { 263 | process.exit(); 264 | } 265 | } 266 | } 267 | 268 | var db = require('./lib/db'); 269 | db.getConnection(dbConfig || require(process.cwd() + path.sep + configFileName)[dbProperty], function (err, db) { 270 | if (err) { 271 | return next(new verror.WError(err, 'Error connecting to database')); 272 | } 273 | var migrationCollection = db.migrationCollection, 274 | dbConnection = db.connection; 275 | 276 | migrationCollection.find({}).sort({num: -1}).limit(1).toArray(function (err, migrationsRun) { 277 | if (err) { 278 | return next(new verror.WError(err, 'Error querying migration collection')); 279 | } 280 | 281 | var lastMigration = migrationsRun[0], 282 | lastMigrationNum = lastMigration ? lastMigration.num : 0; 283 | 284 | migrate({ 285 | migrationTitle: 'migrations/.migrate', 286 | db: dbConnection, 287 | migrationCollection: migrationCollection 288 | }); 289 | migrations(direction, lastMigrationNum, migrateTo).forEach(function(path){ 290 | var mod = require(process.cwd() + '/' + path); 291 | migrate({ 292 | num: parseInt(path.split('/')[1].match(/^(\d+)/)[0], 10), 293 | title: path, 294 | up: mod.up, 295 | down: mod.down}); 296 | }); 297 | 298 | //Revert working directory to previous state 299 | process.chdir(previousWorkingDirectory); 300 | 301 | var set = migrate(); 302 | 303 | set.on('migration', function(migration, direction){ 304 | log(direction, migration.title); 305 | }); 306 | 307 | set.on('save', function(){ 308 | log('migration', 'complete'); 309 | return next(); 310 | }); 311 | 312 | set[direction](null, lastMigrationNum); 313 | }); 314 | }); 315 | } 316 | 317 | // invoke command 318 | var command = options.command || 'up'; 319 | if (!(command in commands)) abort('unknown command "' + command + '"'); 320 | command = commands[command]; 321 | command.apply(this, options.args); 322 | } 323 | 324 | function chdir(dir) { 325 | process.chdir(dir); 326 | } 327 | 328 | function setConfigFilename(filename) { 329 | configFileName = filename; 330 | } 331 | 332 | function setConfigFileProperty(propertyName) { 333 | dbProperty = propertyName; 334 | } 335 | 336 | function setDbConfig(conf) { 337 | dbConfig = JSON.parse(conf); 338 | } 339 | 340 | var runmmIdx = args.indexOf('-runmm'), 341 | runMongoMigrateIdx = args.indexOf('--runMongoMigrate'); 342 | if (runmmIdx > -1 || runMongoMigrateIdx > -1) { 343 | args.splice(runmmIdx > -1 ? runmmIdx : runMongoMigrateIdx, 1); 344 | 345 | // parse arguments 346 | var arg; 347 | while (args.length) { 348 | arg = args.shift(); 349 | switch (arg) { 350 | case '-h': 351 | case '--help': 352 | case 'help': 353 | console.log(usage); 354 | process.exit(); 355 | break; 356 | case '-dbc': 357 | case '--dbConfig': 358 | setDbConfig(required()); 359 | break; 360 | case '-c': 361 | case '--chdir': 362 | chdir(required()); 363 | break; 364 | case '-cfg': 365 | case '--config': 366 | setConfigFilename(required()); 367 | break; 368 | case '-dbn': 369 | case '--dbPropName': 370 | setConfigFileProperty(required()); 371 | break; 372 | default: 373 | if (options.command) { 374 | options.args.push(arg); 375 | } else { 376 | options.command = arg; 377 | } 378 | } 379 | } 380 | 381 | runMongoMigrate(); 382 | } else { 383 | module.exports = { 384 | run: runMongoMigrate, 385 | changeWorkingDirectory: chdir, 386 | setDbConfig: setDbConfig, 387 | setConfigFilename: setConfigFilename, 388 | setConfigFileProp: setConfigFileProperty 389 | }; 390 | } 391 | --------------------------------------------------------------------------------