diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6148158 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[{*.js,*.json,*.yml,bin/*}] +indent_size = 2 +indent_style = space + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index e9af0b1..aa239b0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ node_modules *.sock .migrate - +*.db migrations/ +test/fixtures/tmp diff --git a/.npmignore b/.npmignore index f1250e5..8757baa 100644 --- a/.npmignore +++ b/.npmignore @@ -2,3 +2,5 @@ support test examples *.sock +.db +.migrate diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ed68bac --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2011-2017 TJ Holowaychuk +Copyright (c) 2017 Wes Todd + +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: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. diff --git a/Makefile b/Makefile deleted file mode 100644 index d260bf0..0000000 --- a/Makefile +++ /dev/null @@ -1,5 +0,0 @@ - -test: - @node test/test.migrate.js - -.PHONY: test \ No newline at end of file diff --git a/Readme.md b/Readme.md index 8e48beb..347f1ea 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,10 @@ -# migrate +# Migrate - Abstract migration framework for node +[![NPM Version](https://img.shields.io/npm/v/migrate.svg)](https://npmjs.org/package/migrate) +[![NPM Downloads](https://img.shields.io/npm/dm/migrate.svg)](https://npmjs.org/package/migrate) +[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) + +Abstract migration framework for node. *NOTE:* A large refactor is underway for the next major version of this package. Check out the `1.x` pull request tracking this work, or to explicitly opt in, install the `next` tag (`npm install migrate@next`). @@ -15,124 +19,221 @@ Usage: migrate [options] [command] Options: - -c, --chdir change the working directory - --state-file set path to state file (migrations/.migrate) - --template-file set path to template file to use for new migrations - --date-format set a date format to use for new migration filenames + -V, --version output the version number + -h, --help output usage information Commands: - down [name] migrate down till given migration - up [name] migrate up till given migration (the default command) - create [title] create a new migration file with optional [title] - + init Initalize the migrations tool in a project + list List migrations and their status + create Create a new migration + up [name] Migrate up to a give migration + down [name] Migrate down to a given migration + help [cmd] display help for [cmd] ``` +For help with the individual commands, see `migrate help [cmd]`. Each command has some helpful flags +for customising the behavior of the tool. + ## Programmatic usage ```javascript -var migrate = require('migrate'); -var set = migrate.load('migration/.migrate', 'migration'); - -set.up(function (err) { - if (err) throw err; - - console.log('Migration completed'); -}); +var migrate = require('migrate') + +migrate.load({ + stateStore: '.migrate' +}, function (err, set) { + if (err) { + throw err + } + set.up(function (err) { + if (err) { + throw err + } + console.log('migrations successfully ran') + }) +}) ``` ## Creating Migrations -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: +To create a migration, execute `migrate create ` with a title. By default, a file in `./migrations/` will be created with the following content: + +```javascript +'use strict' - exports.up = function(next){ - next(); - }; +module.exports.up = function (next) { + next() +} - exports.down = function(next){ - next(); - }; +module.exports.down = function (next) { + next() +} +``` All you have to do is populate these, invoking `next()` when complete, and you are ready to migrate! For example: - $ migrate create add-pets - $ migrate create add-owners +``` +$ migrate create add-pets +$ migrate create add-owners +``` The first call creates `./migrations/{timestamp in milliseconds}-add-pets.js`, which we can populate: - var db = require('./db'); +```javascript +// db is just an object shared between the migrations +var db = require('./db'); + +exports.up = function (next) { + db.pets = []; + db.pets.push('tobi') + db.pets.push('loki') + db.pets.push('jane') + next() +} + +exports.down = function (next) { + db.pets.pop('pets') + db.pets.pop('pets') + db.pets.pop('pets') + delete db.pets + next() +} +``` - exports.up = function(next){ - db.rpush('pets', 'tobi'); - db.rpush('pets', 'loki'); - db.rpush('pets', 'jane', next); - }; +The second creates `./migrations/{timestamp in milliseconds}-add-owners.js`, which we can populate: - exports.down = function(next){ - db.rpop('pets'); - db.rpop('pets'); - db.rpop('pets', next); - }; +```javascript +var db = require('./db'); + +exports.up = function (next) { + db.owners = []; + db.owners.push('taylor') + db.owners.push('tj', next) +} + +exports.down = function (next) { + db.owners.pop() + db.owners.pop() + delete db.owners + next() +} +``` -The second creates `./migrations/{timestamp in milliseconds}-add-owners.js`, which we can populate: +### Advanced migration creation - var db = require('./db'); +When creating migrations you have a bunch of other options to help you control how the migrations +are created. You can fully configure the way the migration is made with a `generator`, which is just a +function exported as a node module. A good example of a generator is the default one [shipped with +this package](https://github.com/tj/node-migrate/blob/b282cacbb4c0e73631d651394da52396131dd5de/lib/template-generator.js). - exports.up = function(next){ - db.rpush('owners', 'taylor'); - db.rpush('owners', 'tj', next); - }; +The `create` command accepts a flag for pointing the tool at a generator, for example: - exports.down = function(next){ - db.rpop('owners'); - db.rpop('owners', next); - }; +``` +$ migrate create --generator ./my-migrate-generator.js +``` + +A more simple and common thing you might want is to just change the default template file which is created. To do this, you +can simply pass the `template-file` flag: + +``` +$ migrate create --template-file ./my-migration-template.js +``` + +Lastly, if you want to use newer ECMAscript features, or language addons like TypeScript, for your migrations, you can +use the `comipler` flag. For example, to use babel with your migrations, you can do the following: + +``` +$ npm install --save babel-register +$ migrate create --compiler=".js:babel-register" foo +$ migrate up --compiler=".js:babel-register" +``` ## Running Migrations When first running the migrations, all will be executed in sequence. - $ migrate - up : migrations/1316027432511-add-pets.js - up : migrations/1316027432512-add-jane.js - up : migrations/1316027432575-add-owners.js - up : migrations/1316027433425-coolest-pet.js - migration : complete +``` +$ migrate + up : migrations/1316027432511-add-pets.js + up : migrations/1316027432512-add-jane.js + up : migrations/1316027432575-add-owners.js + up : migrations/1316027433425-coolest-pet.js + migration : complete +``` -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. +Subsequent attempts will simply output "complete", as they have already been executed. `migrate` knows this because it stores the current state in +`./.migrate` which is typically a file that SCMs like GIT should ignore. - $ migrate - migration : complete +``` +$ migrate + migration : complete +``` If we were to create another migration using `migrate create`, and then execute migrations again, we would execute only those not previously executed: - $ migrate - up : migrates/1316027433455-coolest-owner.js +``` +$ migrate + up : migrates/1316027433455-coolest-owner.js +``` You can also run migrations incrementally by specifying a migration. - $ migrate up 1316027433425-coolest-pet.js - up : migrations/1316027432511-add-pets.js - up : migrations/1316027432512-add-jane.js - up : migrations/1316027432575-add-owners.js - up : migrations/1316027433425-coolest-pet.js - migration : complete +``` +$ migrate up 1316027433425-coolest-pet.js + up : migrations/1316027432511-add-pets.js + up : migrations/1316027432512-add-jane.js + up : migrations/1316027432575-add-owners.js + up : migrations/1316027433425-coolest-pet.js + migration : complete +``` + +This will run up-migrations up to (and including) `1316027433425-coolest-pet.js`. Similarly you can run down-migrations up to (and including) a +specific migration, instead of migrating all the way down. + +``` +$ migrate down 1316027432512-add-jane.js + down : migrations/1316027432575-add-owners.js + down : migrations/1316027432512-add-jane.js + migration : complete +``` -This will run up-migrations upto (and including) `1316027433425-coolest-pet.js`. Similarly you can run down-migrations upto (and including) a specific migration, instead of migrating all the way down. +Any time you want to see the current state of the migrations, you can run `migrate list` to see an output like: + +``` +$ migrate list + 1316027432511-add-pets.js [2017-09-23] : <No Description> + 1316027432512-add-jane.js [2017-09-23] : <No Description> +``` - $ migrate down 1316027432512-add-jane.js - down : migrations/1316027432575-add-owners.js - down : migrations/1316027432512-add-jane.js - migration : complete +The description can be added by exporting a `description` field from the migration file. + +## Custom State Storage + +By default, `migrate` stores the state of the migrations which have been run in a file (`.migrate`). But you +can provide a custom storage engine if you would like to do something different, like storing them in your database of choice. +A storage engine has a simple interface of `load(fn)` and `save(set, fn)`. As long as what goes in as `set` comes out +the same on `load`, then you are good to go! + +If you are using the provided cli, you can specify the store implementation with the `--store` flag, which is be a `require`-able node module. For example: + +``` +$ migrate up --store="my-migration-store" +``` ## API -### `migrate.load(stateFile, migrationsDirectory)` +### `migrate.load(opts, cb)` + +Calls the callback with a `Set` based on the options passed. Options: -Returns a `Set` populated with migration scripts from the `migrationsDirectory` -and state loaded from `stateFile`. +- `set`: A set instance if you created your own +- `stateStore`: A store instance to load and store migration state, or a string which is a path to the migration state file +- `migrationsDirectory`: The path to the migrations directory +- `filterFunction`: A filter function which will be called for each file found in the migrations directory +- `sortFunction`: A sort function to ensure migration order ### `Set.up([migration, ]cb)` @@ -144,28 +245,3 @@ migration. Calls the callback `cb`, possibly with an error `err`, when done. Migrates down to the specified `migration` or, if none is specified, to the first migration. Calls the callback `cb`, possibly with an error `err`, when done. - -## License - -(The MIT License) - -Copyright (c) 2011 TJ Holowaychuk <tj@vision-media.ca> - -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: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -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. diff --git a/bin/migrate b/bin/migrate index f77fd15..77b2f76 100755 --- a/bin/migrate +++ b/bin/migrate @@ -1,222 +1,15 @@ #!/usr/bin/env node - -/** - * Module dependencies. - */ - -var migrate = require('../') - , join = require('path').join - , fs = require('fs') - , dateFormat = require('dateformat'); - -/** - * Arguments. - */ - -var args = process.argv.slice(2); - -/** - * Option defaults. - */ - -var options = { args: [] }; - -/** - * Current working directory. - */ - -var cwd; - -/** - * Usage information. - */ - -var usage = [ - '' - , ' Usage: migrate [options] [command]' - , '' - , ' Options:' - , '' - , ' -c, --chdir <path> change the working directory' - , ' --state-file <path> set path to state file (migrations/.migrate)' - , ' --template-file <path> set path to template file to use for new migrations' - , ' --date-format <format> set a date format to use for new migration filenames' - , ' Commands:' - , '' - , ' down [name] migrate down till given migration' - , ' up [name] migrate up till given migration (the default command)' - , ' create [title] create a new migration file with optional [title]' - , '' -].join('\n'); - -/** - * Migration template. - */ - -var template = [ - '\'use strict\'' - , '' - , 'exports.up = function(next) {' - , ' next();' - , '};' - , '' - , 'exports.down = function(next) {' - , ' next();' - , '};' - , '' -].join('\n'); - -// require an argument - -function required() { - if (args.length) return args.shift(); - abort(arg + ' requires an argument'); -} - -// abort with a message - -function abort(msg) { - console.error(' %s', msg); - process.exit(1); -} - -// parse arguments - -var arg; -while (args.length) { - arg = args.shift(); - switch (arg) { - case '-h': - case '--help': - case 'help': - console.log(usage); - process.exit(); - break; - case '-c': - case '--chdir': - process.chdir(cwd = required()); - break; - case '--state-file': - options.stateFile = required(); - break; - case '--template-file': - template = fs.readFileSync(required()); - break; - case '--date-format': - options.dateFormat = required(); - break; - default: - if (options.command) { - options.args.push(arg); - } else { - options.command = arg; - } - } -} - -/** - * Log a keyed message. - */ - -function log(key, msg) { - console.log(' \033[90m%s :\033[0m \033[36m%s\033[0m', key, msg); -} - -/** - * Slugify the given `str`. - */ - -function slugify(str) { - return str.replace(/\s+/g, '-'); -} - -// create ./migrations - -try { - fs.mkdirSync('migrations', 0774); -} catch (err) { - // ignore -} - -// commands - -var commands = { - - /** - * up [name] - */ - - up: function(migrationName){ - performMigration('up', migrationName); - }, - - /** - * down [name] - */ - - down: function(migrationName){ - performMigration('down', migrationName); - }, - - /** - * create [title] - */ - - create: function(){ - var curr = Date.now() - , title = slugify([].slice.call(arguments).join(' ')); - if (options.dateFormat) { - curr = dateFormat(curr, options.dateFormat); - } - title = title ? curr + '-' + title : curr; - create(title); - } -}; - -/** - * Create a migration with the given `name`. - * - * @param {String} name - */ - -function create(name) { - var path = join('migrations', name + '.js'); - log('create', join(process.cwd(), path)); - fs.writeFileSync(path, template); -} - -/** - * Perform a migration in the given `direction`. - * - * @param {Number} direction - */ - -function performMigration(direction, migrationName) { - var state = options.stateFile || join('migrations', '.migrate'); - var set = migrate.load(state, 'migrations'); - - set.on('migration', function(migration, direction){ - log(direction, migration.title); - }); - - var migrationPath = migrationName - ? join('migrations', migrationName) - : migrationName; - - set[direction](migrationName, function (err) { - if (err) { - log('error', err); - process.exit(1); - } - - log('migration', 'complete'); - process.exit(0); - }); -} - -// invoke command - -var command = options.command || 'up'; -if (!(command in commands)) abort('unknown command "' + command + '"'); -command = commands[command]; -command.apply(this, options.args); +// vim: set ft=javascript: +'use strict' + +var program = require('commander') +var pkg = require('../package.json') + +program + .version(pkg.version) + .command('init', 'Initalize the migrations tool in a project') + .command('list', 'List migrations and their status') + .command('create [name]', 'Create a new migration') + .command('up [name]', 'Migrate up to a give migration', {isDefault: true}) + .command('down [name]', 'Migrate down to a given migration') + .parse(process.argv) diff --git a/bin/migrate-create b/bin/migrate-create new file mode 100755 index 0000000..31ed414 --- /dev/null +++ b/bin/migrate-create @@ -0,0 +1,69 @@ +#!/usr/bin/env node +// vim: set ft=javascript: +'use strict' + +var program = require('commander') +var path = require('path') +var dotenv = require('dotenv') +var log = require('../lib/log') +var registerCompiler = require('../lib/register-compiler') +var pkg = require('../package.json') + +program + .version(pkg.version) + .option('-c, --chdir [dir]', 'Change the working directory', process.cwd()) + .option('--migrations-dir <dir>', 'Change the migrations directory name', 'migrations') + .option('--compiler <ext:module>', 'Use the given module to compile files') + .option('-d, --date-format [format]', 'Set a date format to use') + .option('-t, --template-file [filePath]', 'Set path to template file to use for new migrations', path.join(__dirname, '..', 'lib', 'template.js')) + .option('-e, --extention [extention]', 'Use the given extention to create the file', '.js') + .option('-g, --generator <name>', 'A template generator function', path.join(__dirname, '..', 'lib', 'template-generator')) + .option('--env [name]', 'Use dotenv to load an environment file') + .arguments('<name>') + .action(create) + .parse(process.argv) + +// Setup environment +if (program.env) { + var e = dotenv.config({ + path: typeof program.env === 'string' ? program.env : '.env' + }) + if (e && e.error instanceof Error) { + throw e.error + } +} + +var _name +function create (name) { + // Name provided? + _name = name + + // Change the working dir + process.chdir(program.chdir) + + // Load compiler + if (program.compiler) { + registerCompiler(program.compiler) + } + + // Load the template generator + var gen = require(program.generator) + gen({ + name: name, + dateFormat: program.dateFormat, + templateFile: program.templateFile, + migrationsDirectory: program.migrationsDir, + extention: program.extention + }, function (err, p) { + if (err) { + log.error('Template generation error', err.message) + process.exit(1) + } + log('create', p) + }) +} + +if (!_name) { + log.error('error', 'Migration name required') + log('usage', 'migrate create <name>') +} diff --git a/bin/migrate-down b/bin/migrate-down new file mode 100755 index 0000000..7ced975 --- /dev/null +++ b/bin/migrate-down @@ -0,0 +1,73 @@ +#!/usr/bin/env node +// vim: set ft=javascript: +'use strict' + +var program = require('commander') +var path = require('path') +var minimatch = require('minimatch') +var dotenv = require('dotenv') +var migrate = require('../') +var runMigrations = require('../lib/migrate') +var log = require('../lib/log') +var registerCompiler = require('../lib/register-compiler') +var pkg = require('../package.json') + +program + .version(pkg.version) + .usage('[options] <name>') + .option('-c, --chdir <dir>', 'Change the working directory', process.cwd()) + .option('-f, --state-file <path>', 'Set path to state file', '.migrate') + .option('-s, --store <store>', 'Set the migrations store', path.join(__dirname, '..', 'lib', 'file-store')) + .option('--migrations-dir <dir>', 'Change the migrations directory name', 'migrations') + .option('--matches <glob>', 'A glob pattern to filter migration files', '*') + .option('--compiler <ext:module>', 'Use the given module to compile files') + .option('--env [name]', 'Use dotenv to load an environment file') + .parse(process.argv) + +// Change the working dir +process.chdir(program.chdir) + +// Setup environment +if (program.env) { + var e = dotenv.config({ + path: typeof program.env === 'string' ? program.env : '.env' + }) + if (e && e.error instanceof Error) { + throw e.error + } +} + +// Load compiler +if (program.compiler) { + registerCompiler(program.compiler) +} + +// Setup store +var Store = require(program.store) +var store = new Store(program.stateFile) + +// Load in migrations +migrate.load({ + stateStore: store, + migrationsDirectory: program.migrationsDir, + filterFunction: minimatch.filter(program.matches) +}, function (err, set) { + if (err) { + log.error('error', err) + process.exit(1) + } + + set.on('migration', function (migration, direction) { + log(direction, migration.title) + }) + + runMigrations(set, 'down', program.args[0], function (err) { + if (err) { + log('error', err) + process.exit(1) + } + + log('migration', 'complete') + process.exit(0) + }) +}) diff --git a/bin/migrate-init b/bin/migrate-init new file mode 100755 index 0000000..b36d5cd --- /dev/null +++ b/bin/migrate-init @@ -0,0 +1,50 @@ +#!/usr/bin/env node +// vim: set ft=javascript: +'use strict' + +var program = require('commander') +var mkdirp = require('mkdirp') +var dotenv = require('dotenv') +var path = require('path') +var log = require('../lib/log') +var pkg = require('../package.json') + +program + .version(pkg.version) + .option('-f, --state-file <path>', 'Set path to state file', '.migrate') + .option('-s, --store <store>', 'Set the migrations store', path.join(__dirname, '..', 'lib', 'file-store')) + .option('--migrations-dir <dir>', 'Change the migrations directory name', 'migrations') + .option('-c, --chdir [dir]', 'Change the working directory', process.cwd()) + .option('--env [name]', 'Use dotenv to load an environment file') + .parse(process.argv) + +// Change the working dir +process.chdir(program.chdir) + +// Setup environment +if (program.env) { + var e = dotenv.config({ + path: typeof program.env === 'string' ? program.env : '.env' + }) + if (e && e.error instanceof Error) { + throw e.error + } +} + +// Setup store +var Store = require(program.store) +var store = new Store(program.stateFile) + +// Create migrations dir path +var p = path.join(process.cwd(), program.migrationsDir) + +log('migrations dir', p) +mkdirp.sync(p) + +// Call store init +if (typeof store.init === 'function') { + store.init(function (err) { + if (err) return log.error(err) + log('init') + }) +} diff --git a/bin/migrate-list b/bin/migrate-list new file mode 100755 index 0000000..a52eed6 --- /dev/null +++ b/bin/migrate-list @@ -0,0 +1,74 @@ +#!/usr/bin/env node +// vim: set ft=javascript: +'use strict' + +var program = require('commander') +var path = require('path') +var dateFormat = require('dateformat') +var minimatch = require('minimatch') +var dotenv = require('dotenv') +var migrate = require('../') +var log = require('../lib/log') +var registerCompiler = require('../lib/register-compiler') +var pkg = require('../package.json') + +program + .version(pkg.version) + .usage('[options] <name>') + .option('-c, --chdir <dir>', 'Change the working directory', process.cwd()) + .option('-f, --state-file <path>', 'Set path to state file', '.migrate') + .option('-s, --store <store>', 'Set the migrations store', path.join(__dirname, '..', 'lib', 'file-store')) + .option('-d, --date-format [format]', 'Set a date format to use', 'yyyy-mm-dd') + .option('--migrations-dir <dir>', 'Change the migrations directory name', 'migrations') + .option('--matches <glob>', 'A glob pattern to filter migration files', '*') + .option('--compiler <ext:module>', 'Use the given module to compile files') + .option('--env [name]', 'Use dotenv to load an environment file') + .parse(process.argv) + +// Check clean flag, exit if NODE_ENV === 'production' and force not specified +if (program.clean && process.env.NODE_ENV === 'production' && !program.force) { + log.error('error', 'Cowardly refusing to clean while node environment set to production, use --force to continue.') + process.exit(1) +} + +// Change the working dir +process.chdir(program.chdir) + +// Setup environment +if (program.env) { + var e = dotenv.config({ + path: typeof program.env === 'string' ? program.env : '.env' + }) + if (e && e.error instanceof Error) { + throw e.error + } +} + +// Load compiler +if (program.compiler) { + registerCompiler(program.compiler) +} + +// Setup store +var Store = require(program.store) +var store = new Store(program.stateFile) + +// Load in migrations +migrate.load({ + stateStore: store, + migrationsDirectory: program.migrationsDir, + filterFunction: minimatch.filter(program.matches) +}, function (err, set) { + if (err) { + log.error('error', err) + process.exit(1) + } + + if (set.migrations.length === 0) { + return log('list', 'No Migrations') + } + + set.migrations.forEach(function (migration) { + log(migration.title + (migration.timestamp ? ' [' + dateFormat(migration.timestamp, program.dateFormat) + ']' : ' [not run]'), migration.description || '<No Description>') + }) +}) diff --git a/bin/migrate-up b/bin/migrate-up new file mode 100755 index 0000000..71d67aa --- /dev/null +++ b/bin/migrate-up @@ -0,0 +1,122 @@ +#!/usr/bin/env node +// vim: set ft=javascript: +'use strict' + +var program = require('commander') +var path = require('path') +var minimatch = require('minimatch') +var dotenv = require('dotenv') +var migrate = require('../') +var runMigrations = require('../lib/migrate') +var log = require('../lib/log') +var registerCompiler = require('../lib/register-compiler') +var pkg = require('../package.json') + +program + .version(pkg.version) + .usage('[options] <name>') + .option('-c, --chdir <dir>', 'Change the working directory', process.cwd()) + .option('-f, --state-file <path>', 'Set path to state file', '.migrate') + .option('-s, --store <store>', 'Set the migrations store', path.join(__dirname, '..', 'lib', 'file-store')) + .option('--clean', 'Tears down the migration state before running up') + .option('--force', 'Force through the command, ignoring warnings') + .option('--init', 'Runs init for the store') + .option('--migrations-dir <dir>', 'Change the migrations directory name', 'migrations') + .option('--matches <glob>', 'A glob pattern to filter migration files', '*') + .option('--compiler <ext:module>', 'Use the given module to compile files') + .option('--env [name]', 'Use dotenv to load an environment file') + .parse(process.argv) + +// Change the working dir +process.chdir(program.chdir) + +// Setup environment +if (program.env) { + var e = dotenv.config({ + path: typeof program.env === 'string' ? program.env : '.env' + }) + if (e && e.error instanceof Error) { + throw e.error + } +} + +// Check clean flag, exit if NODE_ENV === 'production' and force not specified +if (program.clean && process.env.NODE_ENV === 'production' && !program.force) { + log.error('error', 'Cowardly refusing to clean while node environment set to production, use --force to continue.') + process.exit(1) +} + +// Check init flag, exit if NODE_ENV === 'production' and force not specified +if (program.init && process.env.NODE_ENV === 'production' && !program.force) { + log.error('error', 'Cowardly refusing to init while node environment set to production, use --force to continue.') + process.exit(1) +} + +// Load compiler +if (program.compiler) { + registerCompiler(program.compiler) +} + +// Setup store +var Store = require(program.store) +var store = new Store(program.stateFile) + +// Call store init +if (program.init && typeof store.init === 'function') { + store.init(function (err) { + if (err) return log.error(err) + loadAndGo() + }) +} else { + loadAndGo() +} + +// Load in migrations +function loadAndGo () { + migrate.load({ + stateStore: store, + migrationsDirectory: program.migrationsDir, + filterFunction: minimatch.filter(program.matches) + }, function (err, set) { + if (err) { + log.error('error', err) + process.exit(1) + } + + set.on('warning', function (msg) { + log('warning', msg) + }) + + set.on('migration', function (migration, direction) { + log(direction, migration.title) + }) + + // Run + ;(program.clean ? cleanUp : up)(set, function (err) { + if (err) { + log('error', err) + process.exit(1) + } + log('migration', 'complete') + process.exit(0) + }) + }) +} + +function cleanUp (set, fn) { + runMigrations(set, 'down', null, function (err) { + if (err) { + return fn(err) + } + up(set, fn) + }) +} + +function up (set, fn) { + runMigrations(set, 'up', program.args[0], function (err) { + if (err) { + return fn(err) + } + fn() + }) +} diff --git a/examples/env/.env b/examples/env/.env new file mode 100644 index 0000000..a71ce89 --- /dev/null +++ b/examples/env/.env @@ -0,0 +1 @@ +DB=contributors diff --git a/examples/env/.foo b/examples/env/.foo new file mode 100644 index 0000000..e9902e4 --- /dev/null +++ b/examples/env/.foo @@ -0,0 +1 @@ +DB=foo diff --git a/examples/env/README.md b/examples/env/README.md new file mode 100644 index 0000000..e91a95b --- /dev/null +++ b/examples/env/README.md @@ -0,0 +1,11 @@ +# Environment Example + +``` +$ migrate up --env +$ migrate down --env +$ cat .db # should see table of `contributors` + +$ migrate up --env .foo +$ migrate down --env .foo +$ cat .db # should see table of `foo` +``` diff --git a/examples/env/db.js b/examples/env/db.js new file mode 100644 index 0000000..b151444 --- /dev/null +++ b/examples/env/db.js @@ -0,0 +1,44 @@ +'use strict' +var fs = require('fs') + +module.exports = { + loaded: false, + tables: {}, + table: function (name) { + this.tables[name] = [] + this.save() + }, + removeTable: function (name) { + delete this.tables[name] + this.save() + }, + insert: function (table, value) { + this.tables[table].push(value) + this.save() + }, + remove: function (table, value) { + this.tables[table].splice(this.tables[table].indexOf(value), 1) + this.save() + }, + save: function () { + fs.writeFileSync('.db', JSON.stringify(this)) + }, + load: function () { + if (this.loaded) return this + var json + try { + json = JSON.parse(fs.readFileSync('.db', 'utf8')) + } catch (e) { + // ignore + return this + } + this.loaded = true + this.tables = json.tables + return this + }, + toJSON: function () { + return { + tables: this.tables + } + } +} diff --git a/examples/migrate.js b/examples/migrate.js index 7f48570..c6a56eb 100644 --- a/examples/migrate.js +++ b/examples/migrate.js @@ -4,52 +4,53 @@ // $ npm install redis // $ redis-server +var path = require('path') var migrate = require('../') - , redis = require('redis') - , db = redis.createClient(); - -migrate(__dirname + '/.migrate'); - -migrate('add pets', function(next){ - db.rpush('pets', 'tobi'); - db.rpush('pets', 'loki', next); -}, function(next){ - db.rpop('pets'); - db.rpop('pets', next); -}); - -migrate('add jane', function(next){ - db.rpush('pets', 'jane', next); -}, function(next){ - db.rpop('pets', next); -}); - -migrate('add owners', function(next){ - db.rpush('owners', 'taylor'); - db.rpush('owners', 'tj', next); -}, function(next){ - db.rpop('owners'); - db.rpop('owners', next); -}); - -migrate('coolest pet', function(next){ - db.set('pets:coolest', 'tobi', next); -}, function(next){ - db.del('pets:coolest', next); -}); - -var set = migrate(); - -console.log(); -set.on('save', function(){ - console.log(); -}); - -set.on('migration', function(migration, direction){ - console.log(' \033[90m%s\033[0m \033[36m%s\033[0m', direction, migration.title); -}); - -set.up(function(err){ - if (err) throw err; - process.exit(); -}); +var redis = require('redis') +var db = redis.createClient() + +migrate(path.join(__dirname, '.migrate')) + +migrate('add pets', function (next) { + db.rpush('pets', 'tobi') + db.rpush('pets', 'loki', next) +}, function (next) { + db.rpop('pets') + db.rpop('pets', next) +}) + +migrate('add jane', function (next) { + db.rpush('pets', 'jane', next) +}, function (next) { + db.rpop('pets', next) +}) + +migrate('add owners', function (next) { + db.rpush('owners', 'taylor') + db.rpush('owners', 'tj', next) +}, function (next) { + db.rpop('owners') + db.rpop('owners', next) +}) + +migrate('coolest pet', function (next) { + db.set('pets:coolest', 'tobi', next) +}, function (next) { + db.del('pets:coolest', next) +}) + +var set = migrate() + +console.log() +set.on('save', function () { + console.log() +}) + +set.on('migration', function (migration, direction) { + console.log(direction, migration.title) +}) + +set.up(function (err) { + if (err) throw err + process.exit() +}) diff --git a/index.js b/index.js index 7aebfd7..f52e191 100644 --- a/index.js +++ b/index.js @@ -1,2 +1,62 @@ +'use strict' -module.exports = require('./lib/migrate'); \ No newline at end of file +/*! + * migrate + * Copyright(c) 2011 TJ Holowaychuk <tj@vision-media.ca> + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var MigrationSet = require('./lib/set') +var FileStore = require('./lib/file-store') +var loadMigrationsIntoSet = require('./lib/load-migrations') + +/** + * Expose the migrate function. + */ + +exports = module.exports = migrate + +function migrate (title, up, down) { + // migration + if (typeof title === 'string' && up && down) { + migrate.set.addMigration(title, up, down) + // specify migration file + } else if (typeof title === 'string') { + migrate.set = exports.load(title) + // no migration path + } else if (!migrate.set) { + throw new Error('must invoke migrate(path) before running migrations') + // run migrations + } else { + return migrate.set + } +} + +/** + * Expose MigrationSet + */ +exports.MigrationSet = MigrationSet + +exports.load = function (options, fn) { + var opts = options || {} + + // Create default store + var store = (typeof opts.stateStore === 'string') ? new FileStore(opts.stateStore) : opts.stateStore + + // Create migration set + var set = new MigrationSet(store) + + loadMigrationsIntoSet({ + set: set, + store: store, + migrationsDirectory: opts.migrationsDirectory, + filterFunction: opts.filterFunction, + sortFunction: opts.sortFunction + }, function (err) { + fn(err, set) + }) +} diff --git a/lib/file-store.js b/lib/file-store.js new file mode 100644 index 0000000..210eb49 --- /dev/null +++ b/lib/file-store.js @@ -0,0 +1,42 @@ +var fs = require('fs') + +module.exports = FileStore + +function FileStore (path) { + this.path = path +} + +/** + * Save the migration data. + * + * @api public + */ + +FileStore.prototype.save = function (set, fn) { + fs.writeFile(this.path, JSON.stringify({ + lastRun: set.lastRun, + migrations: set.migrations + }, null, ' '), fn) +} + +/** + * Load the migration data and call `fn(err, obj)`. + * + * @param {Function} fn + * @return {Type} + * @api public + */ + +FileStore.prototype.load = function (fn) { + fs.readFile(this.path, 'utf8', function (err, json) { + if (err && err.code !== 'ENOENT') return fn(err) + if (!json || json === '') { + return fn(null, {}) + } + try { + fn(null, JSON.parse(json)) + } catch (err) { + fn(err) + } + }) +} diff --git a/lib/load-migrations.js b/lib/load-migrations.js new file mode 100644 index 0000000..4e3bec8 --- /dev/null +++ b/lib/load-migrations.js @@ -0,0 +1,72 @@ +'use strict' + +var path = require('path') +var fs = require('fs') +var Migration = require('./migration') + +module.exports = loadMigrationsIntoSet + +function loadMigrationsIntoSet (options, fn) { + // Process options, set and store are required, rest optional + var opts = options || {} + if (!opts.set || !opts.store) { + throw new TypeError((opts.set ? 'store' : 'set') + ' is required for loading migrations') + } + var set = opts.set + var store = opts.store + var migrationsDirectory = path.resolve(opts.migrationsDirectory || 'migrations') + var filterFn = opts.filterFunction || (() => true) + var sortFn = opts.sortFunction || function (m1, m2) { + return m1.title > m2.title ? 1 : (m1.title < m2.title ? -1 : 0) + } + + // Load from migrations store first up + store.load(function (err, state) { + if (err) return fn(err) + + // Set last run date on the set + set.lastRun = state.lastRun || null + + // Read migrations directory + fs.readdir(migrationsDirectory, function (err, files) { + if (err) return fn(err) + + // Filter out non-matching files + files = files.filter(filterFn) + + // Create migrations, keep a lookup map for the next step + var migMap = {} + var migrations = files.map(function (file) { + // Try to load the migrations file + var mod + try { + mod = require(path.join(migrationsDirectory, file)) + } catch (e) { + return fn(e) + } + + var migration = new Migration(file, mod.up, mod.down, mod.description) + migMap[file] = migration + return migration + }) + + // Fill in timestamp from state, or error if missing + state.migrations && state.migrations.forEach(function (m) { + if (!migMap[m.title]) { + // @TODO is this the best way to handle this? + return fn(new Error('Missing migration file: ' + m.title)) + } + migMap[m.title].timestamp = m.timestamp + }) + + // Sort the migrations by their title + migrations = migrations.sort(sortFn) + + // Add the migrations to the set + migrations.forEach(set.addMigration.bind(set)) + + // Successfully loaded + fn() + }) + }) +} diff --git a/lib/log.js b/lib/log.js new file mode 100644 index 0000000..25ed549 --- /dev/null +++ b/lib/log.js @@ -0,0 +1,10 @@ +'use strict' +var chalk = require('chalk') + +module.exports = function log (key, msg) { + console.log(' ' + chalk.grey(key) + ' : ' + chalk.cyan(msg)) +} + +module.exports.error = function log (key, msg) { + console.error(' ' + chalk.red(key) + ' : ' + chalk.white(msg)) +} diff --git a/lib/migrate.js b/lib/migrate.js index 04756e3..8c1b5ca 100644 --- a/lib/migrate.js +++ b/lib/migrate.js @@ -1,51 +1,116 @@ +'use strict' -/*! - * migrate - * Copyright(c) 2011 TJ Holowaychuk <tj@vision-media.ca> - * MIT Licensed - */ +module.exports = migrate -/** - * Module dependencies. - */ +function migrate (set, direction, migrationName, fn) { + var migrations = [] + var lastRunIndex + var toIndex + + if (!migrationName) { + toIndex = direction === 'up' ? set.migrations.length : 0 + } else if ((toIndex = positionOfMigration(set.migrations, migrationName)) === -1) { + return fn(new Error('Could not find migration: ' + migrationName)) + } -var Set = require('./set') - , path = require('path') - , fs = require('fs'); + lastRunIndex = positionOfMigration(set.migrations, set.lastRun) + migrations = (direction === 'up' ? upMigrations : downMigrations)(set, lastRunIndex, toIndex) -/** - * Expose the migrate function. - */ + function next (migration) { + // Done running migrations + if (!migration) return fn(null) + + // Missing direction method + if (typeof migration[direction] !== 'function') { + return fn(new TypeError('Migration ' + migration.title + ' does not have method ' + direction)) + } + + // Status for supporting promises and callbacks + var isPromise = false + + // Run the migration function + set.emit('migration', migration, direction) + var arity = migration[direction].length + var returnValue = migration[direction](function (err) { + if (isPromise) return set.emit('warning', 'if your migration returns a promise, do not call the done callback') + completeMigration(err) + }) + + // Is it a promise? + isPromise = typeof Promise !== 'undefined' && returnValue instanceof Promise + + // If not a promise and arity is not 1, warn + if (!isPromise && arity < 1) set.emit('warning', 'it looks like your migration did not take or callback or return a Promise, this might be an error') + + // Handle the promises + if (isPromise) { + returnValue + .then(completeMigration) + .catch(completeMigration) + } + + function completeMigration (err) { + if (err) return fn(err) + + // Set timestamp if running up, clear it if down + migration.timestamp = direction === 'up' ? Date.now() : null -exports = module.exports = migrate; - -function migrate(title, up, down) { - // migration - if ('string' == typeof title && up && down) { - migrate.set.addMigration(title, up, down); - // specify migration file - } else if ('string' == typeof title) { - migrate.set = new Set(title); - // no migration path - } else if (!migrate.set) { - throw new Error('must invoke migrate(path) before running migrations'); - // run migrations - } else { - return migrate.set; + // Decrement last run index + lastRunIndex-- + + set.lastRun = direction === 'up' ? migration.title : set.migrations[lastRunIndex] && set.migrations[lastRunIndex].title + set.save(function (err) { + if (err) return fn(err) + + next(migrations.shift()) + }) + } } + + next(migrations.shift()) +} + +function upMigrations (set, lastRunIndex, toIndex) { + return set.migrations.reduce(function (arr, migration, index) { + if (index > toIndex) { + return arr + } + + if (index < lastRunIndex && !migration.timestamp) { + set.emit('warning', 'migrations running out of order') + } + + if (!migration.timestamp) { + arr.push(migration) + } + + return arr + }, []) } -exports.load = function (stateFile, migrationsDirectory) { +function downMigrations (set, lastRunIndex, toIndex) { + return set.migrations.reduce(function (arr, migration, index) { + if (index < toIndex || index > lastRunIndex) { + return arr + } - var set = new Set(stateFile); - var dir = path.resolve(migrationsDirectory); + if (migration.timestamp) { + arr.push(migration) + } - fs.readdirSync(dir).filter(function(file){ - return file.match(/^\d+.*\.js$/); - }).sort().forEach(function (file) { - var mod = require(path.join(dir, file)); - set.addMigration(file, mod.up, mod.down); - }); + return arr + }, []).reverse() +} - return set; -}; +/** + * Get index of given migration in list of migrations + * + * @api private + */ + +function positionOfMigration (migrations, title) { + for (var i = 0; i < migrations.length; ++i) { + if (migrations[i].title === title) return i + } + return -1 +} diff --git a/lib/migration.js b/lib/migration.js index 361e828..02a408c 100644 --- a/lib/migration.js +++ b/lib/migration.js @@ -1,3 +1,4 @@ +'use strict' /*! * migrate - Migration @@ -9,10 +10,12 @@ * Expose `Migration`. */ -module.exports = Migration; +module.exports = Migration -function Migration(title, up, down) { - this.title = title; - this.up = up; - this.down = down; -} \ No newline at end of file +function Migration (title, up, down, description) { + this.title = title + this.up = up + this.down = down + this.description = description + this.timestamp = null +} diff --git a/lib/register-compiler.js b/lib/register-compiler.js new file mode 100644 index 0000000..8294383 --- /dev/null +++ b/lib/register-compiler.js @@ -0,0 +1,15 @@ +'use strict' +var path = require('path') + +module.exports = registerCompiler + +function registerCompiler (c) { + var compiler = c.split(':') + var ext = compiler[0] + var mod = compiler[1] + + if (mod[0] === '.') mod = path.join(process.cwd(), mod) + require(mod)({ + extensions: ['.' + ext] + }) +} diff --git a/lib/set.js b/lib/set.js index 1bf3947..4c8c3cc 100644 --- a/lib/set.js +++ b/lib/set.js @@ -1,3 +1,4 @@ +'use strict' /*! * migrate - Set @@ -9,15 +10,16 @@ * Module dependencies. */ -var EventEmitter = require('events').EventEmitter - , Migration = require('./migration') - , fs = require('fs'); +var EventEmitter = require('events') +var Migration = require('./migration') +var migrate = require('./migrate') +var inherits = require('inherits') /** * Expose `Set`. */ -module.exports = Set; +module.exports = MigrationSet /** * Initialize a new migration `Set` with the given `path` @@ -27,17 +29,18 @@ module.exports = Set; * @api private */ -function Set(path) { - this.migrations = []; - this.path = path; - this.pos = 0; +function MigrationSet (store) { + this.store = store + this.migrations = [] + this.map = {} + this.lastRun = null }; /** * Inherit from `EventEmitter.prototype`. */ -Set.prototype.__proto__ = EventEmitter.prototype; +inherits(MigrationSet, EventEmitter) /** * Add a migration. @@ -48,46 +51,39 @@ Set.prototype.__proto__ = EventEmitter.prototype; * @api public */ -Set.prototype.addMigration = function(title, up, down){ - this.migrations.push(new Migration(title, up, down)); -}; - -/** - * Save the migration data. - * - * @api public - */ +MigrationSet.prototype.addMigration = function (title, up, down) { + var migration + if (!(title instanceof Migration)) { + migration = new Migration(title, up, down) + } else { + migration = title + } -Set.prototype.save = function(fn){ - var self = this - , json = JSON.stringify(this); - fs.writeFile(this.path, json, function(err){ - if (err) return fn(err); + // Only add the migration once, but update + if (this.map[migration.title]) { + this.map[migration.title].up = migration.up + this.map[migration.title].down = migration.down + this.map[migration.title].description = migration.description + return + } - self.emit('save'); - fn(null); - }); -}; + this.migrations.push(migration) + this.map[migration.title] = migration +} /** - * Load the migration data and call `fn(err, obj)`. + * Save the migration data. * - * @param {Function} fn - * @return {Type} * @api public */ -Set.prototype.load = function(fn){ - this.emit('load'); - fs.readFile(this.path, 'utf8', function(err, json){ - if (err) return fn(err); - try { - fn(null, JSON.parse(json)); - } catch (err) { - fn(err); - } - }); -}; +MigrationSet.prototype.save = function (fn) { + this.store.save(this, (err) => { + if (err) return fn(err) + this.emit('save') + fn(null) + }) +} /** * Run down migrations and call `fn(err)`. @@ -96,9 +92,9 @@ Set.prototype.load = function(fn){ * @api public */ -Set.prototype.down = function(migrationName, fn){ - this.migrate('down', migrationName, fn); -}; +MigrationSet.prototype.down = function (migrationName, fn) { + this.migrate('down', migrationName, fn) +} /** * Run up migrations and call `fn(err)`. @@ -107,9 +103,9 @@ Set.prototype.down = function(migrationName, fn){ * @api public */ -Set.prototype.up = function(migrationName, fn){ - this.migrate('up', migrationName, fn); -}; +MigrationSet.prototype.up = function (migrationName, fn) { + this.migrate('up', migrationName, fn) +} /** * Migrate in the given `direction`, calling `fn(err)`. @@ -119,76 +115,10 @@ Set.prototype.up = function(migrationName, fn){ * @api public */ -Set.prototype.migrate = function(direction, migrationName, fn){ +MigrationSet.prototype.migrate = function (direction, migrationName, fn) { if (typeof migrationName === 'function') { - fn = migrationName; - migrationName = null; + fn = migrationName + migrationName = null } - var self = this; - this.load(function(err, obj){ - if (err) { - if ('ENOENT' != err.code) return fn(err); - } else { - self.pos = obj.pos; - } - self._migrate(direction, migrationName, fn); - }); -}; - -/** - * Get index of given migration in list of migrations - * - * @api private - */ - - function positionOfMigration(migrations, filename) { - for(var i=0; i < migrations.length; ++i) { - if (migrations[i].title == filename) return i; - } - return -1; - } - -/** - * Perform migration. - * - * @api private - */ - -Set.prototype._migrate = function(direction, migrationName, fn){ - var self = this - , migrations - , migrationPos; - - if (!migrationName) { - migrationPos = direction == 'up' ? this.migrations.length : 0; - } else if ((migrationPos = positionOfMigration(this.migrations, migrationName)) == -1) { - return fn(new Error("Could not find migration: " + migrationName)); - } - - switch (direction) { - case 'up': - migrations = this.migrations.slice(this.pos, migrationPos+1); - break; - case 'down': - migrations = this.migrations.slice(migrationPos, this.pos).reverse(); - break; - } - - function next(migration) { - if (!migration) return fn(null); - - self.emit('migration', migration, direction); - migration[direction](function(err){ - if (err) return fn(err); - - self.pos += (direction === 'up' ? 1 : -1); - self.save(function (err) { - if (err) return fn(err); - - next(migrations.shift()) - }); - }); - } - - next(migrations.shift()); -}; + migrate(this, direction, migrationName, fn) +} diff --git a/lib/template-generator.js b/lib/template-generator.js new file mode 100644 index 0000000..cedf4eb --- /dev/null +++ b/lib/template-generator.js @@ -0,0 +1,51 @@ +'use strict' +var path = require('path') +var fs = require('fs') +var slug = require('slug') +var formatDate = require('dateformat') +var mkdirp = require('mkdirp') + +module.exports = function templateGenerator (opts, cb) { + // Setup default options + opts = opts || {} + var name = opts.name + var dateFormat = opts.dateFormat + var templateFile = opts.templateFile || path.join(__dirname, 'template.js') + var migrationsDirectory = opts.migrationsDirectory || 'migrations' + var extention = opts.extention || '.js' + + loadTemplate(templateFile, function (err, template) { + if (err) return cb(err) + + // Ensure migrations directory exists + mkdirp(migrationsDirectory, function (err) { + if (err) return cb(err) + + // Create date string + var formattedDate = dateFormat ? formatDate(new Date(), dateFormat) : Date.now() + + // Fix up file path + var p = path.join(process.cwd(), migrationsDirectory, slug(formattedDate + (name ? '-' + name : '')) + extention) + + // Write the template file + fs.writeFile(p, template, function (err) { + if (err) return cb(err) + cb(null, p) + }) + }) + }) +} + +var _templateCache = {} +function loadTemplate (tmpl, cb) { + if (_templateCache[tmpl]) { + return cb(null, _templateCache) + } + fs.readFile(tmpl, { + encoding: 'utf8' + }, function (err, content) { + if (err) return cb(err) + _templateCache[tmpl] = content + cb(null, content) + }) +} diff --git a/lib/template.js b/lib/template.js new file mode 100644 index 0000000..f158549 --- /dev/null +++ b/lib/template.js @@ -0,0 +1,9 @@ +'use strict' + +module.exports.up = function (next) { + next() +} + +module.exports.down = function (next) { + next() +} diff --git a/package.json b/package.json index d6f8489..4ea54ef 100644 --- a/package.json +++ b/package.json @@ -9,20 +9,34 @@ "author": "TJ Holowaychuk <tj@vision-media.ca>", "repository": "git://github.com/visionmedia/node-migrate", "bin": { - "migrate": "./bin/migrate" + "migrate": "./bin/migrate", + "migrate-init": "./bin/migrate-init", + "migrate-list": "./bin/migrate-list", + "migrate-create": "./bin/migrate-create", + "migrate-up": "./bin/migrate-up", + "migrate-down": "./bin/migrate-down" }, "devDependencies": { - "mocha": "^2.2.1" + "mocha": "^3.2.0", + "rimraf": "^2.6.1", + "standard": "^10.0.1" }, "main": "index", "engines": { "node": ">= 0.4.x" }, "scripts": { - "test": "mocha" + "test": "standard && standard ./bin/* && mocha" }, "license": "MIT", "dependencies": { - "dateformat": "^1.0.12" + "chalk": "^1.1.3", + "commander": "^2.9.0", + "dateformat": "^2.0.0", + "dotenv": "^4.0.0", + "inherits": "^2.0.3", + "minimatch": "^3.0.3", + "mkdirp": "^0.5.1", + "slug": "^0.9.1" } } diff --git a/test/basic.js b/test/basic.js index d2a1e10..824ef0b 100644 --- a/test/basic.js +++ b/test/basic.js @@ -1,195 +1,190 @@ +/* global describe, it, beforeEach, afterEach */ -var fs = require('fs'); -var path = require('path'); -var assert = require('assert'); +var rimraf = require('rimraf') +var path = require('path') +var assert = require('assert') -var migrate = require('../'); -var db = require('./fixtures/db'); +var migrate = require('../') +var db = require('./util/db') -var BASE = path.join(__dirname, 'fixtures', 'basic'); -var STATE = path.join(BASE, '.migrate'); +var BASE = path.join(__dirname, 'fixtures', 'basic') +var STATE = path.join(BASE, '.migrate') -describe('migrate', function () { +describe('migration set', function () { + var set - var set; - - function assertNoPets() { - assert.equal(db.pets.length, 0); - assert.equal(set.pos, 0); + function assertNoPets () { + assert.equal(db.pets.length, 0) } - function assertPets() { - assert.equal(db.pets.length, 3); - assert.equal(db.pets[0].name, 'tobi'); - assert.equal(db.pets[0].email, 'tobi@learnboost.com'); - assert.equal(set.pos, 3); + function assertPets () { + assert.equal(db.pets.length, 3) + assert.equal(db.pets[0].name, 'tobi') + assert.equal(db.pets[0].email, 'tobi@learnboost.com') } - function assertPetsWithDogs() { - assert.equal(db.pets.length, 5); - assert.equal(db.pets[0].name, 'tobi'); - assert.equal(db.pets[0].email, 'tobi@learnboost.com'); - assert.equal(db.pets[4].name, 'suki'); + function assertPetsWithDogs () { + assert.equal(db.pets.length, 5) + assert.equal(db.pets[0].name, 'tobi') + assert.equal(db.pets[0].email, 'tobi@learnboost.com') + assert.equal(db.pets[4].name, 'suki') }; - function assertFirstMigration() { - assert.equal(db.pets.length, 2); - assert.equal(db.pets[0].name, 'tobi'); - assert.equal(db.pets[1].name, 'loki'); - assert.equal(set.pos, 1); + function assertFirstMigration () { + assert.equal(db.pets.length, 2) + assert.equal(db.pets[0].name, 'tobi') + assert.equal(db.pets[1].name, 'loki') } - function assertSecondMigration() { - assert.equal(db.pets.length, 3); - assert.equal(db.pets[0].name, 'tobi'); - assert.equal(db.pets[1].name, 'loki'); - assert.equal(db.pets[2].name, 'jane'); - assert.equal(set.pos, 2); + function assertSecondMigration () { + assert.equal(db.pets.length, 3) + assert.equal(db.pets[0].name, 'tobi') + assert.equal(db.pets[1].name, 'loki') + assert.equal(db.pets[2].name, 'jane') } - beforeEach(function () { - set = migrate.load(STATE, BASE); - }); + beforeEach(function (done) { + migrate.load({ + stateStore: STATE, + migrationsDirectory: BASE + }, function (err, s) { + set = s + done(err) + }) + }) it('should handle basic migration', function (done) { - set.up(function (err) { - assert.ifError(err); - assertPets(); + assert.ifError(err) + assertPets() set.up(function (err) { - assert.ifError(err); - assertPets(); + assert.ifError(err) + assertPets() set.down(function (err) { - assert.ifError(err); - assertNoPets(); + assert.ifError(err) + assertNoPets() set.down(function (err) { - assert.ifError(err); - assertNoPets(); + assert.ifError(err) + assertNoPets() set.up(function (err) { - assert.ifError(err); - assertPets(); - done(); - }); - }); - }); - }); - }); - - }); + assert.ifError(err) + assertPets() + done() + }) + }) + }) + }) + }) + }) it('should add a new migration', function (done) { - set.addMigration('add dogs', function (next) { - db.pets.push({ name: 'simon' }); - db.pets.push({ name: 'suki' }); - next(); + db.pets.push({ name: 'simon' }) + db.pets.push({ name: 'suki' }) + next() }, function (next) { - db.pets.pop(); - db.pets.pop(); - next(); - }); + db.pets.pop() + db.pets.pop() + next() + }) set.up(function (err) { - assert.ifError(err); - assertPetsWithDogs(); + assert.ifError(err) + assertPetsWithDogs() set.up(function (err) { - assert.ifError(err); - assertPetsWithDogs(); + assert.ifError(err) + assertPetsWithDogs() set.down(function (err) { - assert.ifError(err); - assertNoPets(); - done(); - }); - }); - }); - - }); + assert.ifError(err) + assertNoPets() + done() + }) + }) + }) + }) it('should emit events', function (done) { - set.addMigration('4-adjust-emails.js', function (next) { db.pets.forEach(function (pet) { - if (pet.email) - pet.email = pet.email.replace('learnboost.com', 'lb.com'); - }); - next(); + if (pet.email) { pet.email = pet.email.replace('learnboost.com', 'lb.com') } + }) + next() }, function (next) { db.pets.forEach(function (pet) { - if (pet.email) - pet.email = pet.email.replace('lb.com', 'learnboost.com'); - }); - next(); - }); - - var saved = 0; - var migrations = []; + if (pet.email) { pet.email = pet.email.replace('lb.com', 'learnboost.com') } + }) + next() + }) + + var saved = 0 + var migrations = [] var expectedMigrations = [ '1-add-guy-ferrets.js', '2-add-girl-ferrets.js', '3-add-emails.js', '4-adjust-emails.js' - ]; + ] set.on('save', function () { - saved++; - }); + saved++ + }) set.on('migration', function (migration, direction) { - migrations.push(migration.title); - assert.equal(typeof direction, 'string'); - }); + migrations.push(migration.title) + assert.equal(typeof direction, 'string') + }) set.up(function (err) { - assert.ifError(err); - assert.equal(saved, 4); - assert.equal(db.pets[0].email, 'tobi@lb.com'); - assert.deepEqual(migrations, expectedMigrations); + assert.ifError(err) + assert.equal(saved, 4) + assert.equal(db.pets[0].email, 'tobi@lb.com') + assert.deepEqual(migrations, expectedMigrations) - migrations = []; - expectedMigrations = expectedMigrations.reverse(); + migrations = [] + expectedMigrations = expectedMigrations.reverse() set.down(function (err) { - assert.ifError(err); - assert.equal(saved, 8); - assert.deepEqual(migrations, expectedMigrations); - assertNoPets(); - done(); - }); - - }); - - }); + assert.ifError(err) + assert.equal(saved, 8) + assert.deepEqual(migrations, expectedMigrations) + assertNoPets() + done() + }) + }) + }) it('should migrate to named migration', function (done) { - - assertNoPets(); + assertNoPets() set.up('1-add-guy-ferrets.js', function (err) { - assert.ifError(err); - assertFirstMigration(); + assert.ifError(err) + assertFirstMigration() set.up('2-add-girl-ferrets.js', function (err) { - assert.ifError(err); - assertSecondMigration(); + assert.ifError(err) + assertSecondMigration() set.down('2-add-girl-ferrets.js', function (err) { - assert.ifError(err); - assertFirstMigration(); + assert.ifError(err) + assertFirstMigration() set.up('2-add-girl-ferrets.js', function (err) { - assert.ifError(err); - assertSecondMigration(); + assert.ifError(err) + assertSecondMigration() + assert.equal(set.lastRun, '2-add-girl-ferrets.js') set.down('2-add-girl-ferrets.js', function (err) { - assert.ifError(err); - assert.equal(set.pos, 1); - done(); - }); - }); - }); - }); - }); - - }); + assert.ifError(err) + assert.equal(set.lastRun, '1-add-guy-ferrets.js') + done() + }) + }) + }) + }) + }) + }) + + it('should load migration descriptions', function () { + assert.equal(set.migrations[0].description, 'Adds two pets') + }) afterEach(function (done) { - db.nuke(); - fs.unlink(STATE, done); - }); - -}); + db.nuke() + rimraf(STATE, done) + }) +}) diff --git a/test/cli.js b/test/cli.js new file mode 100644 index 0000000..26593a0 --- /dev/null +++ b/test/cli.js @@ -0,0 +1,226 @@ +/* global describe, it, beforeEach, afterEach */ +const path = require('path') +const fs = require('fs') +const assert = require('assert') +const rimraf = require('rimraf') +const mkdirp = require('mkdirp') +const formatDate = require('dateformat') +const db = require('./util/db') +const run = require('./util/run') + +// Paths +const FIX_DIR = path.join(__dirname, 'fixtures', 'numbers') +const TMP_DIR = path.join(__dirname, 'fixtures', 'tmp') +const UP = path.join(__dirname, '..', 'bin', 'migrate-up') +const DOWN = path.join(__dirname, '..', 'bin', 'migrate-down') +const CREATE = path.join(__dirname, '..', 'bin', 'migrate-create') +const INIT = path.join(__dirname, '..', 'bin', 'migrate-init') +const LIST = path.join(__dirname, '..', 'bin', 'migrate-list') + +// Run helper +const up = run.bind(null, UP, FIX_DIR) +const down = run.bind(null, DOWN, FIX_DIR) +const create = run.bind(null, CREATE, TMP_DIR) +const init = run.bind(null, INIT, TMP_DIR) +const list = run.bind(null, LIST, FIX_DIR) + +function reset () { + rimraf.sync(path.join(FIX_DIR, '.migrate')) + rimraf.sync(TMP_DIR) + db.nuke() +} + +describe('$ migrate', function () { + beforeEach(reset) + afterEach(reset) + + describe('init', function () { + beforeEach(mkdirp.bind(mkdirp, TMP_DIR)) + + it('should create a migrations directory', function (done) { + init([], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + assert.doesNotThrow(() => { + fs.accessSync(path.join(TMP_DIR, 'migrations')) + }) + done() + }) + }) + }) // end init + + describe('create', function () { + beforeEach(mkdirp.bind(mkdirp, TMP_DIR)) + + it('should create a fixture file', function (done) { + create(['test'], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + var file = out.split(':')[1].trim() + var content = fs.readFileSync(file, { + encoding: 'utf8' + }) + assert(content) + assert(content.indexOf('module.exports.up') !== -1) + assert(content.indexOf('module.exports.down') !== -1) + done() + }) + }) + + it('should respect the --date-format', function (done) { + var name = 'test' + var fmt = 'yyyy-mm-dd' + var now = formatDate(new Date(), fmt) + + create([name, '-d', fmt], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + assert.doesNotThrow(() => { + fs.accessSync(path.join(TMP_DIR, 'migrations', now + '-' + name + '.js')) + }) + done() + }) + }) + + it('should respect the --extention', function (done) { + var name = 'test' + var fmt = 'yyyy-mm-dd' + var ext = '.mjs' + var now = formatDate(new Date(), fmt) + + create([name, '-d', fmt, '-e', ext], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + assert.doesNotThrow(() => { + fs.accessSync(path.join(TMP_DIR, 'migrations', now + '-' + name + ext)) + }) + done() + }) + }) + + it('should fail with non-zero and a helpful message when template is unreadable', function (done) { + create(['test', '-t', 'fake'], function (err, out, code) { + assert(!err) + assert.equal(code, 1) + assert(out.indexOf('fake') !== -1) + done() + }) + }) + }) // end create + + describe('up', function () { + it('should run up on multiple migrations', function (done) { + up([], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + db.load() + assert(out.indexOf('up') !== -1) + assert.equal(db.numbers.length, 2) + assert(db.numbers.indexOf(1) !== -1) + assert(db.numbers.indexOf(2) !== -1) + done() + }) + }) + + it('should run up to a specified migration', function (done) { + up(['1-one.js'], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + db.load() + assert(out.indexOf('up') !== -1) + assert.equal(db.numbers.length, 1) + assert(db.numbers.indexOf(1) !== -1) + assert(db.numbers.indexOf(2) === -1) + done() + }) + }) + + it('should run up multiple times', function (done) { + up([], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + db.load() + assert(out.indexOf('up') !== -1) + up([], function (err, out) { + assert(!err) + assert(out.indexOf('up') === -1) + assert.equal(db.numbers.length, 2) + done() + }) + }) + }) + + it('should run down when passed --clean', function (done) { + up([], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + up(['--clean'], function (err, out) { + assert(!err) + db.load() + assert(out.indexOf('down') !== -1) + assert(out.indexOf('up') !== -1) + assert.equal(db.numbers.length, 2) + done() + }) + }) + }) + }) // end up + + describe('down', function () { + beforeEach(function (done) { + up([], done) + }) + it('should run down on multiple migrations', function (done) { + down([], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + db.load() + assert(out.indexOf('down') !== -1) + assert.equal(db.numbers.length, 0) + assert(db.numbers.indexOf(1) === -1) + assert(db.numbers.indexOf(2) === -1) + done() + }) + }) + + it('should run down to a specified migration', function (done) { + down(['2-two.js'], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + db.load() + assert(out.indexOf('down') !== -1) + assert.equal(db.numbers.length, 1) + assert(db.numbers.indexOf(1) !== -1) + assert(db.numbers.indexOf(2) === -1) + done() + }) + }) + + it('should run down multiple times', function (done) { + down([], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + assert(out.indexOf('down') !== -1) + db.load() + down([], function (err, out) { + assert(!err) + assert(out.indexOf('down') === -1) + assert.equal(db.numbers.length, 0) + done() + }) + }) + }) + }) // end down + + describe('list', function () { + it('should list available migrations', function (done) { + list([], function (err, out, code) { + assert(!err) + assert.equal(code, 0, out) + assert(out.indexOf('1-one.js') !== -1) + assert(out.indexOf('2-two.js') !== -1) + done() + }) + }) + }) // end init +}) diff --git a/test/fixtures/basic/1-add-guy-ferrets.js b/test/fixtures/basic/1-add-guy-ferrets.js index 0f4c750..d328485 100644 --- a/test/fixtures/basic/1-add-guy-ferrets.js +++ b/test/fixtures/basic/1-add-guy-ferrets.js @@ -1,14 +1,16 @@ -var db = require('../db'); +var db = require('../../util/db') + +exports.description = 'Adds two pets' exports.up = function (next) { - db.pets.push({ name: 'tobi' }); - db.pets.push({ name: 'loki' }); - next(); -}; + db.pets.push({ name: 'tobi' }) + db.pets.push({ name: 'loki' }) + next() +} exports.down = function (next) { - db.pets.pop(); - db.pets.pop(); - next(); -}; + db.pets.pop() + db.pets.pop() + next() +} diff --git a/test/fixtures/basic/2-add-girl-ferrets.js b/test/fixtures/basic/2-add-girl-ferrets.js index 61610fa..a096e31 100644 --- a/test/fixtures/basic/2-add-girl-ferrets.js +++ b/test/fixtures/basic/2-add-girl-ferrets.js @@ -1,12 +1,12 @@ -var db = require('../db'); +var db = require('../../util/db') exports.up = function (next) { - db.pets.push({ name: 'jane' }); - next(); -}; + db.pets.push({ name: 'jane' }) + next() +} exports.down = function (next) { - db.pets.pop(); - next(); -}; + db.pets.pop() + next() +} diff --git a/test/fixtures/basic/3-add-emails.js b/test/fixtures/basic/3-add-emails.js index 4553a87..4e89607 100644 --- a/test/fixtures/basic/3-add-emails.js +++ b/test/fixtures/basic/3-add-emails.js @@ -1,16 +1,16 @@ -var db = require('../db'); +var db = require('../../util/db') exports.up = function (next) { db.pets.forEach(function (pet) { - pet.email = pet.name + '@learnboost.com'; - }); - next(); -}; + pet.email = pet.name + '@learnboost.com' + }) + next() +} exports.down = function (next) { db.pets.forEach(function (pet) { - delete pet.email; - }); - next(); -}; + delete pet.email + }) + next() +} diff --git a/test/fixtures/db.js b/test/fixtures/db.js deleted file mode 100644 index e19497c..0000000 --- a/test/fixtures/db.js +++ /dev/null @@ -1,9 +0,0 @@ - -function nuke() { - exports.pets = []; - exports.issue33 = []; -} - -exports.nuke = nuke; - -nuke(); diff --git a/test/fixtures/env/env b/test/fixtures/env/env new file mode 100644 index 0000000..8a6696f --- /dev/null +++ b/test/fixtures/env/env @@ -0,0 +1 @@ +DB=pets diff --git a/test/fixtures/issue-33/1-migration.js b/test/fixtures/issue-33/1-migration.js index beb3741..6d866d3 100644 --- a/test/fixtures/issue-33/1-migration.js +++ b/test/fixtures/issue-33/1-migration.js @@ -1,12 +1,12 @@ -var db = require('../db'); +var db = require('../../util/db') exports.up = function (next) { - db.issue33.push('1-up'); - next(); -}; + db.issue33.push('1-up') + next() +} exports.down = function (next) { - db.issue33.push('1-down'); - next(); -}; + db.issue33.push('1-down') + next() +} diff --git a/test/fixtures/issue-33/2-migration.js b/test/fixtures/issue-33/2-migration.js index a5f529e..159c3d5 100644 --- a/test/fixtures/issue-33/2-migration.js +++ b/test/fixtures/issue-33/2-migration.js @@ -1,12 +1,12 @@ -var db = require('../db'); +var db = require('../../util/db') exports.up = function (next) { - db.issue33.push('2-up'); - next(); -}; + db.issue33.push('2-up') + next() +} exports.down = function (next) { - db.issue33.push('2-down'); - next(); -}; + db.issue33.push('2-down') + next() +} diff --git a/test/fixtures/issue-33/3-migration.js b/test/fixtures/issue-33/3-migration.js index f9ad2aa..b94ad4c 100644 --- a/test/fixtures/issue-33/3-migration.js +++ b/test/fixtures/issue-33/3-migration.js @@ -1,12 +1,12 @@ -var db = require('../db'); +var db = require('../../util/db') exports.up = function (next) { - db.issue33.push('3-up'); - next(); -}; + db.issue33.push('3-up') + next() +} exports.down = function (next) { - db.issue33.push('3-down'); - next(); -}; + db.issue33.push('3-down') + next() +} diff --git a/test/fixtures/numbers/.gitkeep b/test/fixtures/numbers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/promises/1-callback-test.js b/test/fixtures/promises/1-callback-test.js new file mode 100644 index 0000000..a43e85c --- /dev/null +++ b/test/fixtures/promises/1-callback-test.js @@ -0,0 +1,13 @@ +'use strict' + +module.exports.up = function (next) { + setTimeout(function () { + next() + }, 1) +} + +module.exports.down = function (next) { + setTimeout(function () { + next() + }, 1) +} diff --git a/test/fixtures/promises/2-promise-test.js b/test/fixtures/promises/2-promise-test.js new file mode 100644 index 0000000..2954828 --- /dev/null +++ b/test/fixtures/promises/2-promise-test.js @@ -0,0 +1,17 @@ +'use strict' + +module.exports.up = function () { + return new Promise(function (resolve, reject) { + setTimeout(function () { + resolve() + }, 1) + }) +} + +module.exports.down = function () { + return new Promise(function (resolve, reject) { + setTimeout(function () { + resolve() + }, 1) + }) +} diff --git a/test/fixtures/promises/3-callback-promise-test.js b/test/fixtures/promises/3-callback-promise-test.js new file mode 100644 index 0000000..142c96c --- /dev/null +++ b/test/fixtures/promises/3-callback-promise-test.js @@ -0,0 +1,19 @@ +'use strict' + +module.exports.up = function (next) { + return new Promise(function (resolve, reject) { + setTimeout(function () { + next() + resolve() + }, 1) + }) +} + +module.exports.down = function (next) { + return new Promise(function (resolve, reject) { + setTimeout(function () { + next() + resolve() + }, 1) + }) +} diff --git a/test/fixtures/promises/4-neither-test.js b/test/fixtures/promises/4-neither-test.js new file mode 100644 index 0000000..980cef4 --- /dev/null +++ b/test/fixtures/promises/4-neither-test.js @@ -0,0 +1,9 @@ +'use strict' + +module.exports.up = function () { + arguments[0]() +} + +module.exports.down = function () { + arguments[0]() +} diff --git a/test/fixtures/promises/99-failure-test.js b/test/fixtures/promises/99-failure-test.js new file mode 100644 index 0000000..d0d3315 --- /dev/null +++ b/test/fixtures/promises/99-failure-test.js @@ -0,0 +1,17 @@ +'use strict' + +module.exports.up = function () { + return new Promise(function (resolve, reject) { + setTimeout(function () { + reject(new Error('foo')) + }, 1) + }) +} + +module.exports.down = function () { + return new Promise(function (resolve, reject) { + setTimeout(function () { + reject(new Error('foo')) + }, 1) + }) +} diff --git a/test/integration.js b/test/integration.js new file mode 100644 index 0000000..2965e42 --- /dev/null +++ b/test/integration.js @@ -0,0 +1,116 @@ +/* global describe, it, beforeEach, afterEach */ +const path = require('path') +const assert = require('assert') +const rimraf = require('rimraf') +const mkdirp = require('mkdirp') +const run = require('./util/run') +const db = require('./util/db') + +// Paths +const TMP_DIR = path.join(__dirname, 'fixtures', 'tmp') +const ENV_DIR = path.join(__dirname, 'fixtures', 'env') + +function reset () { + rimraf.sync(path.join(ENV_DIR, '.migrate')) + rimraf.sync(TMP_DIR) + mkdirp.sync(TMP_DIR) + db.nuke() +} + +describe('integration tests', function () { + beforeEach(reset) + afterEach(reset) + + it('should warn when the migrations are run out of order', function (done) { + run.init(TMP_DIR, [], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + + run.create(TMP_DIR, ['1-one', '-d', 'W'], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + + run.create(TMP_DIR, ['3-three', '-d', 'W'], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + + run.up(TMP_DIR, [], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + + run.create(TMP_DIR, ['2-two', '-d', 'W'], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + + run.up(TMP_DIR, [], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + + // A warning should log, and the process not exit with 0 + // because migration 2 should come before migration 3, + // but migration 3 was already run from the previous + // state + assert(out.indexOf('warn') !== -1) + done() + }) + }) + }) + }) + }) + }) + }) + + it('should error when migrations are present in the state file, but not loadable', function (done) { + run.init(TMP_DIR, [], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + + run.create(TMP_DIR, ['1-one', '-d', 'W'], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + + run.create(TMP_DIR, ['3-three', '-d', 'W'], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + + // Keep migration filename to remove + var filename = out.split(' : ')[1].trim() + + run.up(TMP_DIR, [], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + + // Remove the three migration + rimraf.sync(filename) + + run.create(TMP_DIR, ['2-two', '-d', 'W'], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + + run.up(TMP_DIR, [], function (err, out, code) { + assert(!err) + assert.equal(code, 1) + assert(out.indexOf('error') !== -1) + done() + }) + }) + }) + }) + }) + }) + }) + + it('should load the enviroment file when passed --env', function (done) { + run.up(ENV_DIR, ['--env', 'env'], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + assert(out.indexOf('error') === -1) + run.down(ENV_DIR, ['--env', 'env'], function (err, out, code) { + assert(!err) + assert.equal(code, 0) + assert(out.indexOf('error') === -1) + done() + }) + }) + }) +}) diff --git a/test/issue-33.js b/test/issue-33.js index bf6b22d..db86079 100644 --- a/test/issue-33.js +++ b/test/issue-33.js @@ -1,54 +1,57 @@ +/* global describe, it, beforeEach, afterEach */ -var fs = require('fs'); -var path = require('path'); -var assert = require('assert'); +var fs = require('fs') +var path = require('path') +var assert = require('assert') -var migrate = require('../'); -var db = require('./fixtures/db'); +var migrate = require('../') +var db = require('./util/db') -var BASE = path.join(__dirname, 'fixtures', 'issue-33'); -var STATE = path.join(BASE, '.migrate'); +var BASE = path.join(__dirname, 'fixtures', 'issue-33') +var STATE = path.join(BASE, '.migrate') -var A1 = ['1-up', '2-up', '3-up']; -var A2 = A1.concat(['3-down', '2-down', '1-down']); +var A1 = ['1-up', '2-up', '3-up'] +var A2 = A1.concat(['3-down', '2-down', '1-down']) describe('issue #33', function () { - - var set; - - beforeEach(function () { - set = migrate.load(STATE, BASE); - }); + var set + + beforeEach(function (done) { + migrate.load({ + stateStore: STATE, + migrationsDirectory: BASE + }, function (err, s) { + set = s + done(err) + }) + }) it('should run migrations in the correct order', function (done) { - set.up(function (err) { - assert.ifError(err); - assert.deepEqual(db.issue33, A1); + assert.ifError(err) + assert.deepEqual(db.issue33, A1) set.up(function (err) { - assert.ifError(err); - assert.deepEqual(db.issue33, A1); + assert.ifError(err) + assert.deepEqual(db.issue33, A1) set.down(function (err) { - assert.ifError(err); - assert.deepEqual(db.issue33, A2); + assert.ifError(err) + assert.deepEqual(db.issue33, A2) set.down(function (err) { - assert.ifError(err); - assert.deepEqual(db.issue33, A2); + assert.ifError(err) + assert.deepEqual(db.issue33, A2) - done(); - }); - }); - }); - }); - - }); + done() + }) + }) + }) + }) + }) afterEach(function (done) { - db.nuke(); - fs.unlink(STATE, done); - }); - -}); + db.nuke() + fs.unlink(STATE, done) + }) +}) diff --git a/test/promises.js b/test/promises.js new file mode 100644 index 0000000..a1e0481 --- /dev/null +++ b/test/promises.js @@ -0,0 +1,76 @@ +/* global describe, it, beforeEach, afterEach */ + +var rimraf = require('rimraf') +var path = require('path') +var assert = require('assert') + +var migrate = require('../') + +var BASE = path.join(__dirname, 'fixtures', 'promises') +var STATE = path.join(__dirname, 'fixtures', '.migrate') + +describe('Promise migrations', function () { + var set + + beforeEach(function (done) { + migrate.load({ + stateStore: STATE, + migrationsDirectory: BASE + }, function (err, s) { + set = s + done(err) + }) + }) + + afterEach(function (done) { + rimraf(STATE, done) + }) + + it('should handle callback migration', function (done) { + set.up('1-callback-test.js', function (err) { + assert.ifError(err) + done() + }) + }) + + it('should handle promise migration', function (done) { + set.up('2-promise-test.js', function (err) { + assert.ifError(err) + done() + }) + }) + + it('should warn when using promise but still calling callback', function (done) { + var warned = false + set.on('warning', function (msg) { + assert(msg) + warned = true + }) + set.up('3-callback-promise-test.js', function () { + assert(warned) + done() + }) + }) + + it('should warn with no promise or callback', function (done) { + set.up('3-callback-promise-test.js', function () { + var warned = false + set.on('warning', function (msg) { + assert(msg) + warned = true + }) + set.up('4-neither-test.js', function () { + assert(warned) + done() + }) + }) + }) + + it('should error with rejected promises', function (done) { + set.up('99-failure-test.js', function (err) { + assert(err) + assert.equal(err.message, 'foo') + done() + }) + }) +}) diff --git a/test/util/db.js b/test/util/db.js new file mode 100644 index 0000000..82a261b --- /dev/null +++ b/test/util/db.js @@ -0,0 +1,38 @@ +var fs = require('fs') +var path = require('path') +var rimraf = require('rimraf') + +var DB_PATH = path.join(__dirname, 'test.db') + +function init () { + exports.pets = [] + exports.issue33 = [] + exports.numbers = [] +} + +function nuke () { + init() + rimraf.sync(DB_PATH) +} + +function load () { + try { + var c = fs.readFileSync(DB_PATH, 'utf8') + } catch (e) { + return + } + var j = JSON.parse(c) + Object.keys(j).forEach(function (k) { + exports[k] = j[k] + }) +} + +function persist () { + fs.writeFileSync(DB_PATH, JSON.stringify(exports)) +} + +exports.nuke = nuke +exports.persist = persist +exports.load = load + +init() diff --git a/test/util/run.js b/test/util/run.js new file mode 100644 index 0000000..e7c1c73 --- /dev/null +++ b/test/util/run.js @@ -0,0 +1,25 @@ +'use strict' +const path = require('path') +const spawn = require('child_process').spawn + +var run = module.exports = function run (cmd, dir, args, done) { + var p = spawn(cmd, ['-c', dir, ...args]) + var out = '' + p.stdout.on('data', function (d) { + out += d.toString('utf8') + }) + p.stderr.on('data', function (d) { + out += d.toString('utf8') + }) + p.on('error', done) + p.on('close', function (code) { + done(null, out, code) + }) +} + +// Run specific commands +module.exports.up = run.bind(null, path.join(__dirname, '..', '..', 'bin', 'migrate-up')) +module.exports.down = run.bind(null, path.join(__dirname, '..', '..', 'bin', 'migrate-down')) +module.exports.create = run.bind(null, path.join(__dirname, '..', '..', 'bin', 'migrate-create')) +module.exports.init = run.bind(null, path.join(__dirname, '..', '..', 'bin', 'migrate-init')) +module.exports.list = run.bind(null, path.join(__dirname, '..', '..', 'bin', 'migrate-list'))