├── .gitignore ├── template └── migration.js ├── package.json ├── LICENSE ├── README.md └── bin └── migrate.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log -------------------------------------------------------------------------------- /template/migration.js: -------------------------------------------------------------------------------- 1 | exports.up = function(next) { 2 | next(); 3 | }; 4 | 5 | exports.down = function(next) { 6 | next(); 7 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongoose-migration", 3 | "version": "0.2.0", 4 | "description": "Data migration tool for Mongoose", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "bin": { 10 | "migrate": "./bin/migrate.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/mccraveiro/mongoose-migration.git" 15 | }, 16 | "keywords": [ 17 | "mongoose", 18 | "migration", 19 | "migrate" 20 | ], 21 | "author": "Mateus Craveiro ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/mccraveiro/mongoose-migration/issues" 25 | }, 26 | "homepage": "https://github.com/mccraveiro/mongoose-migration", 27 | "dependencies": { 28 | "commander": "^2.5.0", 29 | "slug": "^0.8.0", 30 | "prompt": "^0.2.14", 31 | "colors": "^1.0.3", 32 | "mongoose": "^3.8.19" 33 | }, 34 | "devDependencies": { 35 | "jsup": "0.0.1", 36 | "semver": "^4.1.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 mccraveiro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mongoose-migration 2 | ================== 3 | 4 | Data migration tool for Mongoose 5 | 6 | ## Installation 7 | 8 | Run the following command to install it: 9 | 10 | ```console 11 | npm install mongoose-migration --save 12 | ``` 13 | 14 | ## Usage 15 | 16 | ### Init configuration 17 | 18 | The following command should be executed a single time on the project root directory. It will create the `.migrate.json` configuration file. 19 | 20 | ```console 21 | migrate init 22 | ``` 23 | 24 | After creating it you need to edit and add the path to your models. 25 | 26 | Example: 27 | ```json 28 | { 29 | "basepath": "migrations", 30 | "connection": "mongodb://localhost/db", 31 | "current_timestamp": 0, 32 | "models": { 33 | "User": "models/user.js" 34 | } 35 | } 36 | ``` 37 | 38 | Note that on the previous example `models/user.js` is the model definition file. This file should exports the mongoose model definition. 39 | 40 | `model/users.js` example: 41 | ```javascript 42 | ... 43 | module.exports = mongoose.model('User', schema); 44 | ``` 45 | 46 | ### Create a migration 47 | 48 | ```console 49 | migrate create 50 | ``` 51 | 52 | Example: 53 | ```console 54 | migrate create "Add createdAt field to users collection" 55 | ``` 56 | 57 | ### Edit Migration File 58 | 59 | Open up your migration file (created on the previous step). It should have a default `up` and `down` function. 60 | 61 | Example: 62 | ```javascript 63 | exports.up = function(next) { 64 | next(); 65 | }; 66 | 67 | exports.down = function(next) { 68 | next(); 69 | }; 70 | ``` 71 | 72 | Note: To load a mongoose model defined on your configuration file you should call `this.model()` 73 | 74 | Example: 75 | ```javascript 76 | exports.up = function(next) { 77 | this 78 | .model('User') 79 | .update( 80 | {}, 81 | { 82 | $set: { createdAt: Date.now() } 83 | }, 84 | { 85 | multi: true, 86 | strict: false 87 | }, 88 | function (error, numberAffected, raw) { 89 | if (error) { 90 | console.error(error); 91 | } 92 | console.log('The number of updated documents was %d', numberAffected); 93 | console.log('The raw response from Mongo was ', raw); 94 | next(); 95 | } 96 | ); 97 | }; 98 | 99 | exports.down = function(next) { 100 | this 101 | .model('User') 102 | .update( 103 | {}, 104 | { 105 | $unset: { createdAt: 1 } 106 | }, 107 | { 108 | multi: true, 109 | strict: false 110 | }, 111 | function (error, numberAffected, raw) { 112 | if (error) { 113 | console.error(error); 114 | } 115 | console.log('The number of updated documents was %d', numberAffected); 116 | console.log('The raw response from Mongo was ', raw); 117 | next(); 118 | } 119 | ); 120 | }; 121 | ``` 122 | 123 | ### Perform Migration 124 | 125 | ```console 126 | migrate 127 | ``` 128 | or 129 | ```console 130 | migrate up [number of migrations to perform] 131 | ``` 132 | 133 | Note: By default `migrate` will execute all migrations created until now. However `migrate up` will only execute one migration. 134 | 135 | ### Rollback Migration 136 | 137 | ```console 138 | migrate down 139 | ``` 140 | or 141 | ```console 142 | migrate down [number of migrations to rollback] 143 | ``` 144 | 145 | ### Help 146 | 147 | ```console 148 | migrate -h 149 | ``` 150 | 151 | ## Todo 152 | 153 | - Add environments (dev, production) on the configuration file 154 | - Add `migrate to [timestamp]` 155 | - Add tests 156 | 157 | # Contributing 158 | 159 | For contributing, [open an issue](https://github.com/mccraveiro/mongoose-migration/issues) and/or a [pull request](https://github.com/mccraveiro/mongoose-migration/pulls). 160 | 161 | ## License 162 | 163 | The MIT License (MIT) 164 | 165 | Copyright (c) 2014 mccraveiro 166 | 167 | Permission is hereby granted, free of charge, to any person obtaining a copy 168 | of this software and associated documentation files (the "Software"), to deal 169 | in the Software without restriction, including without limitation the rights 170 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 171 | copies of the Software, and to permit persons to whom the Software is 172 | furnished to do so, subject to the following conditions: 173 | 174 | The above copyright notice and this permission notice shall be included in all 175 | copies or substantial portions of the Software. 176 | 177 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 178 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 179 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 180 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 181 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 182 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 183 | SOFTWARE. -------------------------------------------------------------------------------- /bin/migrate.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var program = require('commander'); 6 | var prompt = require('prompt'); 7 | var colors = require('colors/safe'); 8 | var slug = require('slug'); 9 | var path = require('path'); 10 | var fs = require('fs'); 11 | 12 | var config_filename = '.migrate.json'; 13 | var config_path = process.cwd() + '/' + config_filename; 14 | var CONFIG; 15 | 16 | program 17 | .command('init') 18 | .description('Init migrations on current path') 19 | .action(init); 20 | 21 | program 22 | .command('create ') 23 | .description('Create Migration') 24 | .action(createMigration); 25 | 26 | program 27 | .command('down [number_of_migrations] (default = 1)') 28 | .description('Migrate down') 29 | .action(migrate.bind(null, 'down', process.exit)); 30 | 31 | program 32 | .command('up [number_of_migrations]') 33 | .description('Migrate up (default command)') 34 | .action(migrate.bind(null, 'up', process.exit)); 35 | 36 | program.version(require('../package.json').version); 37 | program.parse(process.argv); 38 | 39 | // Default command ? 40 | if (program.rawArgs.length < 3) { 41 | migrate('up', process.exit, Number.POSITIVE_INFINITY); 42 | } 43 | 44 | /* 45 | * Helpers 46 | */ 47 | 48 | function error(msg) { 49 | console.error(colors.red(msg)); 50 | process.exit(1); 51 | } 52 | 53 | function success(msg) { 54 | console.log(colors.green(msg)); 55 | } 56 | 57 | function loadConfiguration() { 58 | try { 59 | return require(config_path); 60 | } catch (e) { 61 | error('Missing ' + config_filename + ' file. Type `migrate init` to create.'); 62 | } 63 | } 64 | 65 | function updateTimestamp(timestamp, cb) { 66 | CONFIG.current_timestamp = timestamp; 67 | var data = JSON.stringify(CONFIG, null, 2); 68 | fs.writeFile(config_path, data, cb); 69 | } 70 | 71 | function init() { 72 | if (fs.existsSync(config_path)) { 73 | error(config_filename + ' already exists!'); 74 | } 75 | 76 | var schema = { 77 | properties: { 78 | basepath: { 79 | description: 'Enter migrations directory', 80 | type: 'string', 81 | default: 'migrations' 82 | }, 83 | connection: { 84 | description: 'Enter mongo connection string', 85 | type: 'string', 86 | required: true 87 | } 88 | } 89 | }; 90 | 91 | prompt.start(); 92 | prompt.get(schema, function (error, result) { 93 | CONFIG = { 94 | basepath: result.basepath, 95 | connection: result.connection, 96 | current_timestamp: 0, 97 | models: {} 98 | }; 99 | 100 | var data = JSON.stringify(CONFIG, null, 2); 101 | fs.writeFileSync(config_path, data); 102 | 103 | success(config_filename + ' file created!\nEdit it to include your models definitions'); 104 | process.exit(); 105 | }); 106 | } 107 | 108 | function createMigration(description) { 109 | 110 | CONFIG = loadConfiguration(); 111 | 112 | var timestamp = Date.now(); 113 | var migrationName = timestamp + '-' + slug(description) + '.js'; 114 | var template = path.normalize(__dirname + '/../template/migration.js'); 115 | var filename = path.normalize(CONFIG.basepath + '/' + migrationName); 116 | 117 | // create migrations directory 118 | if (!fs.existsSync(CONFIG.basepath)){ 119 | fs.mkdirSync(CONFIG.basepath); 120 | } 121 | 122 | var data = fs.readFileSync(template); 123 | fs.writeFileSync(filename, data); 124 | success('Created migration ' + migrationName); 125 | process.exit(); 126 | } 127 | 128 | function connnectDB() { 129 | // load local app mongoose instance 130 | var mongoose = require(process.cwd() + '/node_modules/mongoose'); 131 | mongoose.connect(CONFIG.connection); 132 | // mongoose.set('debug', true); 133 | } 134 | 135 | function loadModel(model_name) { 136 | return require(process.cwd() + '/' + CONFIG.models[model_name]); 137 | } 138 | 139 | function getTimestamp(name) { 140 | return parseInt((name.split('-'))[0]); 141 | } 142 | 143 | function migrate(direction, cb, number_of_migrations) { 144 | 145 | CONFIG = loadConfiguration(); 146 | 147 | if (!number_of_migrations) { 148 | number_of_migrations = 1; 149 | } 150 | 151 | if (direction == 'down') { 152 | number_of_migrations = -1 * number_of_migrations; 153 | } 154 | 155 | var migrations = fs.readdirSync(CONFIG.basepath); 156 | 157 | connnectDB(); 158 | 159 | migrations = migrations.filter(function (migration_name) { 160 | var timestamp = getTimestamp(migration_name); 161 | 162 | if (number_of_migrations > 0) { 163 | return timestamp > CONFIG.current_timestamp; 164 | } else if (number_of_migrations < 0) { 165 | return timestamp <= CONFIG.current_timestamp; 166 | } 167 | }); 168 | 169 | loopMigrations(number_of_migrations, migrations, cb); 170 | } 171 | 172 | function loopMigrations(direction, migrations, cb) { 173 | 174 | if (direction == 0 || migrations.length == 0) { 175 | return cb(); 176 | } 177 | 178 | if (direction > 0) { 179 | applyMigration('up', migrations.shift(), function () { 180 | direction--; 181 | loopMigrations(direction, migrations, cb); 182 | }); 183 | } else if (direction < 0) { 184 | applyMigration('down', migrations.pop(), function () { 185 | direction++; 186 | loopMigrations(direction, migrations, cb); 187 | }); 188 | } 189 | } 190 | 191 | function applyMigration(direction, name, cb) { 192 | var migration = require(process.cwd() + '/' + CONFIG.basepath + '/' + name); 193 | var timestamp = getTimestamp(name); 194 | 195 | success('Applying migration ' + name + ' - ' + direction); 196 | migration[direction].call({ 197 | model: loadModel 198 | }, callback); 199 | 200 | function callback() { 201 | 202 | if (direction == 'down') { 203 | timestamp--; 204 | } 205 | 206 | updateTimestamp(timestamp, cb); 207 | } 208 | } --------------------------------------------------------------------------------