diff --git a/README.md b/README.md index 02e7718..882d35e 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Operation | Command | Description ---|---|--- create | `rethinkdb-migrate create ` | Creates a migration with the given name up | `rethinkdb-migrate up` | Runs all un-executed migrations up -down | `rethinkdb-migrate down` | Runs all executed migrations down +down | `rethinkdb-migrate down` | Runs all executed migrations down (unless `step` option specified) ### Create @@ -118,7 +118,19 @@ $ rethinkdb-migrate down --db=mydb ``` This command will run all `down` steps from migrations that have been run -previously. See [Options](#options) section to configure this task. +previously. + +> *Caution: this refreshes the database to before the first migration +> (potentially deleting data added since). Be very cautious about running +> this command in a production environment.* + +To rollback just a subset of migrations, use the `step` option: + +```shell +$ rethinkdb-migrate down --step=2 --db=mydb +``` + +See [Options](#options) section to further configure this task. ### Options @@ -139,6 +151,7 @@ discovery | `false` | Whether or not the driver should try to keep a list of upd pool | `false` | Whether or not to use a connection pool when using `rethinkdbdash` driver. cursor | `true` | If true, cursors will not be automatically converted to arrays when using `rethinkdbdash`. servers | undefined | Array of `{ host, port }` of servers to connect to. Only available when using `rethinkdbdash` +step | none | Number of migrations to execute or rollback. If omitted, all migrations will be executed or rolled back, potentially refreshing the db to its initial state and resulting in data loss. migrationsDirectory | `migrations` | Directory where migration files will be saved relativeTo | `process.cwd()` | Root path from which migration directory will be searched or created (if inexistent)' migrationsTable | `_migrations` | Table where meta information about migrations will be saved. This should only be changed if you need a \_migrations table in your application diff --git a/bin/rethinkdb-migrate b/bin/rethinkdb-migrate index 43bbe6f..272dac3 100755 --- a/bin/rethinkdb-migrate +++ b/bin/rethinkdb-migrate @@ -64,13 +64,18 @@ require('yargs') handler: runMigrations.bind(null, 'up') }) .example('$0 up', 'Runs all new migrations up') + .example('$0 up --step=', 'Run just this number of new migrations up') .command({ command: 'down', - desc: 'Runs all down migrations', + desc: 'Rollback all previously executed migrations', builder: internals.migrateOptions, handler: runMigrations.bind(null, 'down') }) + .number('step') + .nargs('step', 1) + .describe('step', 'Number of migrations to run, if omitted run or rollback all migrations') .example('$0 down', 'Runs all migrations down') + .example('$0 down --step=', 'Rollback just this number of previously executed migration files') .alias('f', 'file') .nargs('f', 1) .describe('f', 'Uses the provided file as the options to be used when migration is run') @@ -115,7 +120,7 @@ function logger (emitter) { * an option present in the config file and also a environment variable, and so on */ function buildOptions (argv) { - const optionsMask = 'name,migrationsDirectory,relativeTo,migrationsTable,host,port,db,user,username,password,authKey,driver,discovery,pool,cursor,servers,ssl,i,ignoreTimestamp' + const optionsMask = 'name,migrationsDirectory,relativeTo,migrationsTable,host,port,db,user,username,password,authKey,driver,discovery,pool,cursor,servers,ssl,i,ignoreTimestamp,step' const envVars = Mask(process.env, optionsMask) const file = Mask(readOptionsFile(argv), optionsMask) const args = Mask(argv, optionsMask) diff --git a/lib/migrate.js b/lib/migrate.js index 41901db..41c20d9 100644 --- a/lib/migrate.js +++ b/lib/migrate.js @@ -31,6 +31,8 @@ function validateOptions (options) { const schema = Joi.object().keys({ op: Joi.string().valid('up', 'down').required() .description('Migration command'), + step: Joi.number().min(1) + .description('Number of migrations to perform (migrations are counted as each migration file)'), driver: Joi.string().valid('rethinkdb', 'rethinkdbdash').default('rethinkdb') .description('Rethinkdb javascript driver'), migrationsTable: Joi.string().default('_migrations') @@ -163,25 +165,53 @@ function executeMigration (options) { } function migrateUp (options) { + let steps return getLatestMigrationExecuted(options) .then(latest => getUnExecutedMigrations(latest, options)) - .then(newerMigrations => runMigrations('up', newerMigrations, options)) - .then(emit('info', 'Saving metada')) + .then(newerMigrations => { + const migrationSteps = limitToSteps(newerMigrations, options) + steps = migrationSteps.length + return migrationSteps + }) + .then(migrationSteps => runMigrations('up', migrationSteps, options)) + .then(emit('info', 'Saving metadata')) .then(executedMigrations => saveExecutedMigrationsMetadata(executedMigrations, options)) + .then(() => { + const migrationMessage = steps + ? `Executed ${steps} migration${steps > 1 ? 's' : ''}.` + : `No migrations executed.` + emit('info', migrationMessage)() + }) .then(() => options) } function migrateDown (options) { + let steps return getExecutedMigrations(options) - .then(migrations => runMigrations('down', migrations, options)) - .then(migrations => clearMigrationsTable(migrations, options)) - .then(emit('info', 'Cleared migrations table')) + .then(migrations => loadMigrationsCode(migrations, options)) + .then(migrations => { + const migrationSteps = limitToSteps(migrations, options) + steps = migrationSteps.length + return migrationSteps + }) + .then(migrationSteps => runMigrations('down', migrationSteps, options)) + .then(rolledBackMigrations => clearMigrationsTable(rolledBackMigrations, options)) + .then(() => { + const migrationMessage = steps + ? `Cleared ${steps} migration${steps > 1 ? 's' : ''} from table.` + : 'Migrations table already clear.' + emit('info', migrationMessage)() + }) .then(() => options) } +function limitToSteps (migrations, options) { + return options.step ? migrations.slice(0, options.step) : migrations +} + function getLatestMigrationExecuted (options) { return ensureMigrationsTable(options) - .then(() => getAllMigrationsExecuted(options)) + .then(() => getExecutedMigrations(options)) .then(migrations => { if (!migrations.length) { return { @@ -206,20 +236,6 @@ function ensureMigrationsTable (options) { }) } -function getAllMigrationsExecuted (options) { - const { r, conn, migrationsTable } = options - - return ensureMigrationsTable(options) - .then(() => r.table(migrationsTable) - .orderBy({ index: r.desc('timestamp') }) - .run(conn) - .then(toArray) - ) - .then(migrations => migrations.map(migration => Object.assign({}, migration, { - timestamp: Moment.utc(migration.timestamp) - }))) -} - function getMigrationsFromPath (options) { const { migrationsDirectory, relativeTo } = options const path = Path.resolve(relativeTo, migrationsDirectory) @@ -239,9 +255,17 @@ function getMigrationsFromPath (options) { } function getExecutedMigrations (options) { - return getMigrationsFromPath(options) - .then(migrations => sortMigrations(migrations, true)) - .then(migrations => loadMigrationsCode(migrations, options)) + const { r, conn, migrationsTable } = options + + return ensureMigrationsTable(options) + .then(() => r.table(migrationsTable) + .orderBy({ index: r.desc('timestamp') }) + .run(conn) + .then(toArray) + ) + .then(migrations => migrations.map(migration => Object.assign({}, migration, { + timestamp: Moment.utc(migration.timestamp) + }))) } function getUnExecutedMigrations (latestExecutedMigration, options) {