diff --git a/api.js b/api.js index 2576bd4c..ccf3e973 100644 --- a/api.js +++ b/api.js @@ -227,6 +227,31 @@ dbmigrate.prototype = { ).asCallback(callback); }, + check: function (specification, opts, callback) { + var executeCheck = load('check'); + + if (arguments.length > 0) { + if (typeof specification === 'number') { + this.internals.argv.count = arguments[0]; + } else if (typeof specification === 'function') { + callback = specification; + } + + if (typeof opts === 'string') { + this.internals.migrationMode = opts; + this.internals.matching = opts; + } else if (typeof opts === 'function') { + callback = opts; + } + } + + return Promise.fromCallback( + function (callback) { + executeCheck(this.internals, this.config, callback); + }.bind(this) + ).asCallback(callback); + }, + /** * Executes up a given number of migrations or a specific one. * diff --git a/lib/commands/check.js b/lib/commands/check.js new file mode 100644 index 00000000..63891ff6 --- /dev/null +++ b/lib/commands/check.js @@ -0,0 +1,37 @@ +var path = require('path'); +var log = require('db-migrate-shared').log; +var assert = require('./helper/assert.js'); +var migrationHook = require('./helper/migration-hook.js'); + +module.exports = function (internals, config, callback) { + migrationHook(internals) + .then(function () { + var Migrator = require('../migrator.js'); + var index = require('../../connect'); + + if (!internals.argv.count) { + internals.argv.count = Number.MAX_VALUE; + } + index.connect({ + config: config.getCurrent().settings, + internals: internals + }, Migrator, function (err, migrator) { + if (!assert(err, callback)) return; + + if (internals.locTitle) { + migrator.migrationsDir = path.resolve(internals.argv['migrations-dir'], + internals.locTitle); + } else { migrator.migrationsDir = path.resolve(internals.argv['migrations-dir']); } + + internals.migrationsDir = migrator.migrationsDir; + + migrator.driver.createMigrationsTable(function (err) { + if (!assert(err, callback)) return; + log.verbose('migration table created'); + + migrator.check(internals.argv, internals.onComplete.bind(this, + migrator, internals, callback)); + }); + }); + }); +}; diff --git a/lib/commands/run.js b/lib/commands/run.js index 6d182cc4..40bffeb5 100644 --- a/lib/commands/run.js +++ b/lib/commands/run.js @@ -42,6 +42,7 @@ function run (internals, config) { break; case 'up': case 'down': + case 'check': case 'reset': if (action === 'reset') internals.argv.count = Number.MAX_VALUE; @@ -62,9 +63,12 @@ function run (internals, config) { if (action === 'up') { var executeUp = load('up'); executeUp(internals, config); - } else { + } else if (action === 'down') { var executeDown = load('down'); executeDown(internals, config); + } else { + var executeCheck = load('check'); + executeCheck(internals, config); } break; @@ -104,7 +108,7 @@ function run (internals, config) { ); } else { log.error( - 'Invalid Action: Must be [up|down|create|reset|sync|' + + 'Invalid Action: Must be [up|down|check|create|reset|sync|' + 'db|transition].' ); optimist.showHelp(); diff --git a/lib/commands/set-default-argv.js b/lib/commands/set-default-argv.js index 616750a5..bd89db37 100644 --- a/lib/commands/set-default-argv.js +++ b/lib/commands/set-default-argv.js @@ -22,7 +22,7 @@ module.exports = function (internals, isModule) { internals.argv = optimist .default(defaultConfig) .usage( - 'Usage: db-migrate [up|down|reset|sync|create|db|transition] ' + + 'Usage: db-migrate [up|down|check|reset|sync|create|db|transition] ' + '[[dbname/]migrationName|all] [options]' ) .describe( @@ -42,6 +42,8 @@ module.exports = function (internals, isModule) { .string('c') .describe('dry-run', "Prints the SQL but doesn't run it.") .boolean('dry-run') + .describe('check', 'Prints the migrations to be run without running them.') + .boolean('check') .describe( 'force-exit', 'Forcibly exit the migration process on completion.' @@ -145,8 +147,12 @@ module.exports = function (internals, isModule) { internals.notransactions = internals.argv['non-transactional']; internals.dryRun = internals.argv['dry-run']; global.dryRun = internals.dryRun; + internals.check = internals.argv['check']; if (internals.dryRun) { log.info('dry run'); } + if (internals.check) { + log.info('check'); + } }; diff --git a/lib/migrator.js b/lib/migrator.js index 846440cb..02a62909 100644 --- a/lib/migrator.js +++ b/lib/migrator.js @@ -119,6 +119,40 @@ Migrator.prototype = { } }, + check: function (funcOrOpts, callback) { + var self = this; + Migration.loadFromFilesystem(self.migrationsDir, self.internals, function ( + err, + allMigrations + ) { + if (err) { + callback(err); + return; + } + + Migration.loadFromDatabase( + self.migrationsDir, + self._driver, + self.internals, + function (err, completedMigrations) { + if (err) { + callback(err); + return; + } + + // Requires pr to export filterCompleted from db-migrate-shared + var toRun = dbmUtil.filterCompleted( + allMigrations, + completedMigrations + ); + + log.info('Migrations to run:', toRun.map(migration => migration.name)); + callback(null, toRun); + } + ); + }); + }, + sync: function (funcOrOpts, callback) { var self = this; @@ -180,6 +214,13 @@ Migrator.prototype = { return; } + if (self.internals.check) { + var toRunNames = toRun.map(migration => migration.name); + log.info('Migrations to run:', toRunNames); + callback(null, toRunNames); + return; + } + return Promise.resolve(toRun) .each(function (migration) { log.verbose('preparing to run up migration:', migration.name); @@ -234,6 +275,13 @@ Migrator.prototype = { return; } + if (self.internals.check) { + var toRunNames = toRun.map(migration => migration.name); + log.info('Migrations to run:', toRunNames); + callback(null, toRunNames); + return; + } + return Promise.resolve(toRun) .each(function (migration) { log.verbose('preparing to run down migration:', migration.name); diff --git a/test/integration/api_test.js b/test/integration/api_test.js index e997e04d..14524e53 100644 --- a/test/integration/api_test.js +++ b/test/integration/api_test.js @@ -168,6 +168,23 @@ lab.experiment('api', function () { } ); + lab.test( + 'should handle all check parameter variations properly', + + function () { + return Promise.resolve([ + [], // promise + [sinon.spy()], + [1, sinon.spy()], // targeted migration + [1], // promise targeted migration + [1, 'testscope', sinon.spy()], // scoped target + [1, 'testscope'] // promise scope target + ]) + .each(defaultExecParams('check')) + .each(spyCallback); + } + ); + lab.test( 'should handle all reset parameter variations properly', diff --git a/test/migrator_test.js b/test/migrator_test.js new file mode 100644 index 00000000..3ac29fc7 --- /dev/null +++ b/test/migrator_test.js @@ -0,0 +1,32 @@ +var Code = require('code'); +var Lab = require('lab'); +var proxyquire = require('proxyquire').noPreserveCache(); +var lab = (exports.lab = Lab.script()); + +lab.experiment('migrators', function () { + lab.experiment('check', function () { + lab.test('should return the migrations to be run', function (done) { + var completedMigration = { + name: '20180330020329-thisMigrationIsCompleted' + }; + var uncompletedMigration = { + name: '20180330020330-thisMigrationIsNotCompleted' + }; + var Migrator = proxyquire('../lib/migrator.js', { + './migration': { + loadFromFilesystem: (migrationsDir, internals, cb) => { + return cb(null, [completedMigration, uncompletedMigration]); + }, + loadFromDatabase: (migrationsDir, driver, internals, cb) => { + return cb(null, [completedMigration]); + } + } + }); + Migrator.prototype.check(null, function (err, res) { + Code.expect(res.length).to.equal(1); + Code.expect(res[0].name).to.equal(uncompletedMigration.name); + done(err, res); + }); + }); + }); +});