├── .editorconfig ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── .npmignore ├── .npmrc ├── .travis.yml ├── History.md ├── LICENSE ├── Readme.md ├── bin ├── migrate ├── migrate-create ├── migrate-down ├── migrate-init ├── migrate-list └── migrate-up ├── examples ├── cli │ └── migrations │ │ ├── 1316027432511-add-pets.js │ │ ├── 1316027432512-add-jane.js │ │ ├── 1316027432575-add-owners.js │ │ ├── 1316027433425-coolest-pet.js │ │ ├── db.js │ │ └── template.js ├── custom-state-storage │ ├── custom-state-storage.js │ └── mongo-state-storage.js ├── env │ ├── .env │ ├── .foo │ ├── README.md │ └── db.js └── migrate.js ├── index.d.ts ├── index.js ├── lib ├── file-store.js ├── load-migrations.js ├── log.js ├── migrate.js ├── migration.js ├── register-compiler.js ├── set.js ├── template-generator.js └── template.js ├── package.json └── test ├── basic.js ├── cli.js ├── file-store.js ├── fixtures ├── basic │ ├── 1-add-guy-ferrets.js │ ├── 2-add-girl-ferrets.js │ └── 3-add-emails.js ├── env │ ├── env │ └── migrations │ │ ├── 1-add-guy-ferrets.js │ │ └── 2-add-girl-ferrets.js ├── file-store │ ├── bad-store │ ├── good-store │ ├── invalid-store │ └── old-store ├── issue-33 │ ├── 1-migration.js │ ├── 2-migration.js │ └── 3-migration.js ├── issue-92 │ └── migrations │ │ ├── 1-one.js │ │ └── 2-two.js ├── numbers │ ├── .gitkeep │ └── migrations │ │ ├── 1-one.js │ │ └── 2-two.js └── promises │ ├── 1-callback-test.js │ ├── 2-promise-test.js │ ├── 3-callback-promise-test.js │ ├── 4-neither-test.js │ ├── 5-resolve-test.js │ └── 99-failure-test.js ├── integration.js ├── issue-148.js ├── issue-33.js ├── issue-92.js ├── promises.js └── util ├── db.js ├── run.js ├── tmpl.js └── tmpl.mjs /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [{*.js,*.json,*.yml,bin/*}] 11 | indent_size = 2 12 | indent_style = space 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: push 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | node-version: [14.x, 16.x, 18.x, 20.x] 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Use Node.js ${{ matrix.node-version }} 12 | uses: actions/setup-node@v4 13 | with: 14 | node-version: ${{ matrix.node-version }} 15 | - name: npm install and test 16 | run: npm it 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .migrate 4 | test/fixtures/tmp 5 | *.swp 6 | yarn-error.log 7 | yarn.lock 8 | package-lock.json 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "support/should"] 2 | path = support/should 3 | url = git://github.com/visionmedia/should.js.git 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | support 2 | test 3 | examples 4 | *.sock 5 | .db 6 | .migrate 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 1.8.0 / 2022-03-22 2 | ================== 3 | 4 | * fix(devDeps): updated standard & mocha 5 | * fix: updated deps which don't break 6 | * feat(docs): added custom state store example for mongodb closes #162 7 | * feat: add TypeScript types closes #178 8 | 9 | 1.7.0 / 2020-06-29 10 | ================== 11 | 12 | * Docs, deps and maintence 13 | * Resolve absolute and `node_module` specifiers for path options 14 | 15 | 1.3.0 / 2018-03-15 16 | ================== 17 | 18 | * Added `--extension` 19 | * `--extention` deprecation 20 | * Bug fixes 21 | 22 | 1.2.0 / 2018-01-27 23 | ================== 24 | 25 | * Bug fixes for promise rejections 26 | 27 | 1.1.1 / 2018-01-02 28 | ================== 29 | 30 | * Bug Fix 31 | 32 | 1.1.0 / 2017-12-20 33 | ================== 34 | 35 | * Fixes state store functionality issues 36 | 37 | 1.0.1 / 2017-12-19 38 | ================== 39 | 40 | * Bug fix 41 | 42 | 1.0.0 / 2017-11-30 43 | ================== 44 | 45 | * This was a major refactor effecting most of the codebase 46 | * Track migration status on a per migration basis as opposed to with an array index 47 | * Moved to commander for cli interface 48 | * Compiler support 49 | * State store support 50 | * `dotenv` support 51 | * See: https://github.com/tj/node-migrate/pull/77 52 | 53 | 0.2.3 / 2016-07-05 54 | ================== 55 | 56 | * Add date-format option for new migrations (#66) 57 | * Update readme: Usage section (#65) 58 | * Add missing license field to package.json (#64) 59 | 60 | 0.2.2 / 2015-07-07 61 | ================== 62 | 63 | * Fixed migration to specific point by name 64 | 65 | 0.2.1 / 2015-04-24 66 | ================== 67 | 68 | * Ability to use a custom template file 69 | * Expose easy api for programmatic use 70 | * State is now saved after each successful migration 71 | * Proper tests with `mocha` 72 | * Add `use strict` to the template 73 | 74 | 0.2.0 / 2014-12-26 75 | ================== 76 | 77 | * Report errors on migration next 78 | * The name format of migrations is now timestamp-based instead of numerical 79 | * Remove inner library version 80 | 81 | 0.1.6 / 2014-10-28 82 | ================== 83 | 84 | * Fix paths for windows users 85 | * Added command line option --state-file (to set the location of the migration state file) 86 | 87 | 0.1.3 / 2012-06-25 88 | ================== 89 | 90 | * Update migrate to support v0.8.x 91 | 92 | 0.1.2 / 2012-03-15 93 | ================== 94 | 95 | * 0.7.x support 96 | 97 | 0.1.1 / 2011-12-04 98 | ================== 99 | 100 | * Fixed a typo [kishorenc] 101 | 102 | 0.1.0 / 2011-12-03 103 | ================== 104 | 105 | * Added support for incremental migrations. [Kishore Nallan] 106 | 107 | 0.0.5 / 2011-11-07 108 | ================== 109 | 110 | * 0.6.0 support 111 | 112 | 0.0.4 / 2011-09-12 113 | ================== 114 | 115 | * Fixed: load js files only [aheckmann] 116 | 117 | 0.0.3 / 2011-09-09 118 | ================== 119 | 120 | * Fixed initial `create` support 121 | 122 | 0.0.2 / 2011-09-09 123 | ================== 124 | 125 | * Fixed `make test` 126 | 127 | 0.0.1 / 2011-04-24 128 | ================== 129 | 130 | * Initial release 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2017 TJ Holowaychuk 2 | Copyright (c) 2017 Wes Todd 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | 'Software'), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 19 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 21 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Migrate 2 | 3 | [![NPM Version](https://img.shields.io/npm/v/migrate.svg)](https://npmjs.org/package/migrate) 4 | [![NPM Downloads](https://img.shields.io/npm/dm/migrate.svg)](https://npmjs.org/package/migrate) 5 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 6 | [![Build Status](https://travis-ci.org/tj/node-migrate.svg?branch=master)](https://travis-ci.org/tj/node-migrate) 7 | 8 | Abstract migration framework for node. 9 | 10 | ## Installation 11 | 12 | $ npm install migrate 13 | 14 | ## Usage 15 | 16 | ``` 17 | Usage: migrate [options] [command] 18 | 19 | Options: 20 | 21 | -V, --version output the version number 22 | -h, --help output usage information 23 | 24 | Commands: 25 | 26 | init Initalize the migrations tool in a project 27 | list List migrations and their status 28 | create Create a new migration 29 | up [name] Migrate up to a given migration 30 | down [name] Migrate down to a given migration 31 | help [cmd] display help for [cmd] 32 | ``` 33 | 34 | For help with the individual commands, see `migrate help [cmd]`. Each command has some helpful flags 35 | for customising the behavior of the tool. 36 | 37 | ## Programmatic usage 38 | 39 | ```javascript 40 | var migrate = require('migrate') 41 | 42 | migrate.load({ 43 | stateStore: '.migrate' 44 | }, function (err, set) { 45 | if (err) { 46 | throw err 47 | } 48 | set.up(function (err) { 49 | if (err) { 50 | throw err 51 | } 52 | console.log('migrations successfully ran') 53 | }) 54 | }) 55 | ``` 56 | 57 | ## Creating Migrations 58 | 59 | To create a migration, execute `migrate create ` with a title. By default, a file in `./migrations/` will be created with the following content: 60 | 61 | ```javascript 62 | 'use strict' 63 | 64 | module.exports.up = function (next) { 65 | next() 66 | } 67 | 68 | module.exports.down = function (next) { 69 | next() 70 | } 71 | ``` 72 | 73 | All you have to do is populate these, invoking `next()` when complete (no need to call `next()` if up/down functions are `async`), and you are ready to migrate! 74 | 75 | For example: 76 | 77 | ``` 78 | $ migrate create add-pets 79 | $ migrate create add-owners 80 | ``` 81 | 82 | The first call creates `./migrations/{timestamp in milliseconds}-add-pets.js`, which we can populate: 83 | 84 | ```javascript 85 | // db is just an object shared between the migrations 86 | var db = require('./db'); 87 | 88 | exports.up = function (next) { 89 | db.pets = []; 90 | db.pets.push('tobi') 91 | db.pets.push('loki') 92 | db.pets.push('jane') 93 | next() 94 | } 95 | 96 | exports.down = function (next) { 97 | db.pets.pop('pets') 98 | db.pets.pop('pets') 99 | db.pets.pop('pets') 100 | delete db.pets 101 | next() 102 | } 103 | ``` 104 | 105 | The second creates `./migrations/{timestamp in milliseconds}-add-owners.js`, which we can populate: 106 | 107 | ```javascript 108 | var db = require('./db'); 109 | 110 | exports.up = function (next) { 111 | db.owners = []; 112 | db.owners.push('taylor') 113 | db.owners.push('tj', next) 114 | } 115 | 116 | exports.down = function (next) { 117 | db.owners.pop() 118 | db.owners.pop() 119 | delete db.owners 120 | next() 121 | } 122 | ``` 123 | 124 | ### Advanced migration creation 125 | 126 | When creating migrations you have a bunch of other options to help you control how the migrations 127 | are created. You can fully configure the way the migration is made with a `generator`, which is just a 128 | function exported as a node module. A good example of a generator is the default one [shipped with 129 | this package](https://github.com/tj/node-migrate/blob/b282cacbb4c0e73631d651394da52396131dd5de/lib/template-generator.js). 130 | 131 | The `create` command accepts a flag for pointing the tool at a generator, for example: 132 | 133 | ``` 134 | $ migrate create --generator ./my-migrate-generator.js 135 | ``` 136 | 137 | A more simple and common thing you might want is to just change the default template file which is created. To do this, you 138 | can simply pass the `template-file` flag: 139 | 140 | ``` 141 | $ migrate create --template-file ./my-migration-template.js 142 | ``` 143 | 144 | Lastly, if you want to use newer ECMAscript features, or language addons like TypeScript, for your migrations, you can 145 | use the `compiler` flag. For example, to use babel with your migrations, you can do the following: 146 | 147 | ``` 148 | $ npm install --save babel-register 149 | $ migrate create --compiler="js:babel-register" foo 150 | $ migrate up --compiler="js:babel-register" 151 | ``` 152 | 153 | ## Running Migrations 154 | 155 | When first running the migrations, all will be executed in sequence. 156 | 157 | ``` 158 | $ migrate 159 | up : migrations/1316027432511-add-pets.js 160 | up : migrations/1316027432512-add-jane.js 161 | up : migrations/1316027432575-add-owners.js 162 | up : migrations/1316027433425-coolest-pet.js 163 | migration : complete 164 | ``` 165 | 166 | Subsequent attempts will simply output "complete", as they have already been executed. `migrate` knows this because it stores the current state in 167 | `./.migrate` which is typically a file that SCMs like GIT should ignore. 168 | 169 | ``` 170 | $ migrate 171 | migration : complete 172 | ``` 173 | 174 | If we were to create another migration using `migrate create`, and then execute migrations again, we would execute only those not previously executed: 175 | 176 | ``` 177 | $ migrate 178 | up : migrates/1316027433455-coolest-owner.js 179 | ``` 180 | 181 | You can also run migrations incrementally by specifying a migration. 182 | 183 | ``` 184 | $ migrate up 1316027433425-coolest-pet.js 185 | up : migrations/1316027432511-add-pets.js 186 | up : migrations/1316027432512-add-jane.js 187 | up : migrations/1316027432575-add-owners.js 188 | up : migrations/1316027433425-coolest-pet.js 189 | migration : complete 190 | ``` 191 | 192 | This will run up-migrations up to (and including) `1316027433425-coolest-pet.js`. Similarly you can run down-migrations up to (and including) a 193 | specific migration, instead of migrating all the way down. 194 | 195 | ``` 196 | $ migrate down 1316027432512-add-jane.js 197 | down : migrations/1316027432575-add-owners.js 198 | down : migrations/1316027432512-add-jane.js 199 | migration : complete 200 | ``` 201 | 202 | Any time you want to see the current state of the migrations, you can run `migrate list` to see an output like: 203 | 204 | ``` 205 | $ migrate list 206 | 1316027432511-add-pets.js [2017-09-23] : <No Description> 207 | 1316027432512-add-jane.js [2017-09-23] : <No Description> 208 | ``` 209 | 210 | The description can be added by exporting a `description` field from the migration file. 211 | 212 | ## Custom State Storage 213 | 214 | By default, `migrate` stores the state of the migrations which have been run in a file (`.migrate`). But you 215 | can provide a custom storage engine if you would like to do something different, like storing them in your database of choice. 216 | A storage engine has a simple interface of `load(fn)` and `save(set, fn)`. As long as what goes in as `set` comes out 217 | the same on `load`, then you are good to go! 218 | 219 | If you are using the provided cli, you can specify the store implementation with the `--store` flag, which should be a `require`-able node module. For example: 220 | 221 | ``` 222 | $ migrate up --store="my-migration-store" 223 | ``` 224 | 225 | ## API 226 | 227 | ### `migrate.load(opts, cb)` 228 | 229 | Calls the callback with a `Set` based on the options passed. Options: 230 | 231 | - `set`: A set instance if you created your own 232 | - `stateStore`: A store instance to load and store migration state, or a string which is a path to the migration state file 233 | - `migrations`: An object where keys are migration names and values are migration objects 234 | - `migrationsDirectory`: The path to the migrations directory 235 | - `filterFunction`: A filter function which will be called for each file found in the migrations directory 236 | - `sortFunction`: A sort function to ensure migration order 237 | 238 | ### `Set.up([migration, ]cb)` 239 | 240 | Migrates up to the specified `migration` or, if none is specified, to the latest 241 | migration. Calls the callback `cb`, possibly with an error `err`, when done. 242 | 243 | ### `Set.down([migration, ]cb)` 244 | 245 | Migrates down to the specified `migration` or, if none is specified, to the 246 | first migration. Calls the callback `cb`, possibly with an error `err`, when 247 | done. 248 | -------------------------------------------------------------------------------- /bin/migrate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // vim: set ft=javascript: 3 | 'use strict' 4 | 5 | const program = require('commander') 6 | const pkg = require('../package.json') 7 | 8 | program 9 | .version(pkg.version) 10 | .command('init', 'Initalize the migrations tool in a project') 11 | .command('list', 'List migrations and their status') 12 | .command('create [name]', 'Create a new migration') 13 | .command('up [name]', 'Migrate up to a given migration', { isDefault: true }) 14 | .command('down [name]', 'Migrate down to a given migration') 15 | .parse(process.argv) 16 | -------------------------------------------------------------------------------- /bin/migrate-create: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // vim: set ft=javascript: 3 | 'use strict' 4 | 5 | const program = require('commander') 6 | const path = require('path') 7 | const dotenv = require('dotenv') 8 | const log = require('../lib/log') 9 | const registerCompiler = require('../lib/register-compiler') 10 | const pkg = require('../package.json') 11 | 12 | program 13 | .version(pkg.version) 14 | .option('-c, --chdir [dir]', 'Change the working directory', process.cwd()) 15 | .option('--migrations-dir <dir>', 'Change the migrations directory name', 'migrations') 16 | .option('--compiler <ext:module>', 'Use the given module to compile files') 17 | .option('-d, --date-format [format]', 'Set a date format to use') 18 | .option('-t, --template-file [filePath]', 'Set path to template file to use for new migrations', path.join(__dirname, '..', 'lib', 'template.js')) 19 | .option('-e, --extension [extension]', 'Use the given extension to create the file') 20 | .option('--extention [extension]', '. Use the given extension to create the file. Deprecated as of v1.2. Replace with -e or --extension.') 21 | .option('-g, --generator <name>', 'A template generator function', path.join(__dirname, '..', 'lib', 'template-generator')) 22 | .option('--env [name]', 'Use dotenv to load an environment file') 23 | .arguments('<name>') 24 | .action(create) 25 | .parse(process.argv) 26 | 27 | if (program.extention) { 28 | console.log('create', '"--extention" argument is deprecated. Use "--extension" instead') 29 | } 30 | 31 | // Setup environment 32 | if (program.env) { 33 | const e = dotenv.config({ 34 | path: typeof program.env === 'string' ? program.env : '.env' 35 | }) 36 | if (e && e.error instanceof Error) { 37 | throw e.error 38 | } 39 | } 40 | 41 | // eslint-disable-next-line no-var 42 | var _name 43 | function create (name) { 44 | // Name provided? 45 | _name = name 46 | 47 | // Change the working dir 48 | process.chdir(program.chdir) 49 | 50 | // Load compiler 51 | if (program.compiler) { 52 | registerCompiler(program.compiler) 53 | } 54 | 55 | // Load the template generator 56 | let gen 57 | try { 58 | gen = require(program.generator) 59 | } catch (e) { 60 | gen = require(path.resolve(program.generator)) 61 | } 62 | gen({ 63 | name, 64 | dateFormat: program.dateFormat, 65 | templateFile: program.templateFile, 66 | migrationsDirectory: program.migrationsDir, 67 | extension: program.extension || path.extname(program.templateFile) 68 | }, function (err, p) { 69 | if (err) { 70 | log.error('Template generation error', err.message) 71 | process.exit(1) 72 | } 73 | log('create', p) 74 | }) 75 | } 76 | 77 | if (!_name) { 78 | log.error('error', 'Migration name required') 79 | log('usage', 'migrate create <name>') 80 | } 81 | -------------------------------------------------------------------------------- /bin/migrate-down: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // vim: set ft=javascript: 3 | 'use strict' 4 | 5 | const program = require('commander') 6 | const path = require('path') 7 | const minimatch = require('minimatch') 8 | const dotenv = require('dotenv') 9 | const migrate = require('../') 10 | const runMigrations = require('../lib/migrate') 11 | const log = require('../lib/log') 12 | const registerCompiler = require('../lib/register-compiler') 13 | const pkg = require('../package.json') 14 | 15 | program 16 | .version(pkg.version) 17 | .usage('[options] <name>') 18 | .option('-c, --chdir <dir>', 'Change the working directory', process.cwd()) 19 | .option('-f, --state-file <path>', 'Set path to state file', '.migrate') 20 | .option('-s, --store <store>', 'Set the migrations store', path.join(__dirname, '..', 'lib', 'file-store')) 21 | .option('--migrations-dir <dir>', 'Change the migrations directory name', 'migrations') 22 | .option('--matches <glob>', 'A glob pattern to filter migration files', '*') 23 | .option('--compiler <ext:module>', 'Use the given module to compile files') 24 | .option('--env [name]', 'Use dotenv to load an environment file') 25 | .option('-F, --force', 'Force through the command, ignoring warnings') 26 | .parse(process.argv) 27 | 28 | // Change the working dir 29 | process.chdir(program.chdir) 30 | 31 | // Setup environment 32 | if (program.env) { 33 | const e = dotenv.config({ 34 | path: typeof program.env === 'string' ? program.env : '.env' 35 | }) 36 | if (e && e.error instanceof Error) { 37 | throw e.error 38 | } 39 | } 40 | 41 | // Load compiler 42 | if (program.compiler) { 43 | registerCompiler(program.compiler) 44 | } 45 | 46 | // Setup store 47 | if (program.store[0] === '.') program.store = path.join(process.cwd(), program.store) 48 | 49 | const StoreImport = require(program.store) 50 | const Store = StoreImport.default || StoreImport 51 | const store = new Store(program.stateFile) 52 | 53 | // Load in migrations 54 | migrate.load({ 55 | stateStore: store, 56 | migrationsDirectory: program.migrationsDir, 57 | filterFunction: minimatch.filter(program.matches), 58 | ignoreMissing: program.force 59 | }, function (err, set) { 60 | if (err) { 61 | log.error('error', err) 62 | process.exit(1) 63 | } 64 | 65 | set.on('migration', function (migration, direction) { 66 | log(direction, migration.title) 67 | }) 68 | 69 | runMigrations(set, 'down', program.args[0], function (err) { 70 | if (err) { 71 | log.error('error', err) 72 | process.exit(1) 73 | } 74 | 75 | log('migration', 'complete') 76 | process.exit(0) 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /bin/migrate-init: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // vim: set ft=javascript: 3 | 'use strict' 4 | 5 | const program = require('commander') 6 | const mkdirp = require('mkdirp') 7 | const dotenv = require('dotenv') 8 | const path = require('path') 9 | const log = require('../lib/log') 10 | const pkg = require('../package.json') 11 | const registerCompiler = require('../lib/register-compiler') 12 | 13 | program 14 | .version(pkg.version) 15 | .option('-f, --state-file <path>', 'Set path to state file', '.migrate') 16 | .option('-s, --store <store>', 'Set the migrations store', path.join(__dirname, '..', 'lib', 'file-store')) 17 | .option('--migrations-dir <dir>', 'Change the migrations directory name', 'migrations') 18 | .option('--compiler <ext:module>', 'Use the given module to compile files') 19 | .option('-c, --chdir [dir]', 'Change the working directory', process.cwd()) 20 | .option('--env [name]', 'Use dotenv to load an environment file') 21 | .parse(process.argv) 22 | 23 | // Change the working dir 24 | process.chdir(program.chdir) 25 | 26 | // Setup environment 27 | if (program.env) { 28 | const e = dotenv.config({ 29 | path: typeof program.env === 'string' ? program.env : '.env' 30 | }) 31 | if (e && e.error instanceof Error) { 32 | throw e.error 33 | } 34 | } 35 | 36 | // Load compiler 37 | if (program.compiler) { 38 | registerCompiler(program.compiler) 39 | } 40 | 41 | // Setup store 42 | if (program.store[0] === '.') program.store = path.join(process.cwd(), program.store) 43 | 44 | const StoreImport = require(program.store) 45 | const Store = StoreImport.default || StoreImport 46 | const store = new Store(program.stateFile) 47 | 48 | // Create migrations dir path 49 | const p = path.resolve(process.cwd(), program.migrationsDir) 50 | 51 | log('migrations dir', p) 52 | mkdirp.sync(p) 53 | 54 | // Call store init 55 | if (typeof store.init === 'function') { 56 | store.init(function (err) { 57 | if (err) return log.error(err) 58 | log('init') 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /bin/migrate-list: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // vim: set ft=javascript: 3 | 'use strict' 4 | 5 | const program = require('commander') 6 | const path = require('path') 7 | const dateFormat = require('dateformat') 8 | const minimatch = require('minimatch') 9 | const dotenv = require('dotenv') 10 | const migrate = require('../') 11 | const log = require('../lib/log') 12 | const registerCompiler = require('../lib/register-compiler') 13 | const pkg = require('../package.json') 14 | 15 | program 16 | .version(pkg.version) 17 | .usage('[options] <name>') 18 | .option('-c, --chdir <dir>', 'Change the working directory', process.cwd()) 19 | .option('-f, --state-file <path>', 'Set path to state file', '.migrate') 20 | .option('-s, --store <store>', 'Set the migrations store', path.join(__dirname, '..', 'lib', 'file-store')) 21 | .option('-d, --date-format [format]', 'Set a date format to use', 'yyyy-mm-dd') 22 | .option('--migrations-dir <dir>', 'Change the migrations directory name', 'migrations') 23 | .option('--matches <glob>', 'A glob pattern to filter migration files', '*') 24 | .option('--compiler <ext:module>', 'Use the given module to compile files') 25 | .option('--env [name]', 'Use dotenv to load an environment file') 26 | .parse(process.argv) 27 | 28 | // Check clean flag, exit if NODE_ENV === 'production' and force not specified 29 | if (program.clean && process.env.NODE_ENV === 'production' && !program.force) { 30 | log.error('error', 'Cowardly refusing to clean while node environment set to production, use --force to continue.') 31 | process.exit(1) 32 | } 33 | 34 | // Change the working dir 35 | process.chdir(program.chdir) 36 | 37 | // Setup environment 38 | if (program.env) { 39 | const e = dotenv.config({ 40 | path: typeof program.env === 'string' ? program.env : '.env' 41 | }) 42 | if (e && e.error instanceof Error) { 43 | throw e.error 44 | } 45 | } 46 | 47 | // Load compiler 48 | if (program.compiler) { 49 | registerCompiler(program.compiler) 50 | } 51 | 52 | // Setup store 53 | if (program.store[0] === '.') program.store = path.join(process.cwd(), program.store) 54 | 55 | const StoreImport = require(program.store) 56 | const Store = StoreImport.default || StoreImport 57 | const store = new Store(program.stateFile) 58 | 59 | // Load in migrations 60 | migrate.load({ 61 | stateStore: store, 62 | migrationsDirectory: program.migrationsDir, 63 | filterFunction: minimatch.filter(program.matches) 64 | }, function (err, set) { 65 | if (err) { 66 | log.error('error', err) 67 | process.exit(1) 68 | } 69 | 70 | if (set.migrations.length === 0) { 71 | return log('list', 'No Migrations') 72 | } 73 | 74 | set.migrations.forEach(function (migration) { 75 | log(migration.title + (migration.timestamp ? ' [' + dateFormat(migration.timestamp, program.dateFormat) + ']' : ' [not run]'), migration.description || '<No Description>') 76 | }) 77 | 78 | process.exit(0) 79 | }) 80 | -------------------------------------------------------------------------------- /bin/migrate-up: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // vim: set ft=javascript: 3 | 'use strict' 4 | 5 | const program = require('commander') 6 | const path = require('path') 7 | const minimatch = require('minimatch') 8 | const dotenv = require('dotenv') 9 | const migrate = require('../') 10 | const runMigrations = require('../lib/migrate') 11 | const log = require('../lib/log') 12 | const registerCompiler = require('../lib/register-compiler') 13 | const pkg = require('../package.json') 14 | 15 | program 16 | .version(pkg.version) 17 | .usage('[options] <name>') 18 | .option('-c, --chdir <dir>', 'Change the working directory', process.cwd()) 19 | .option('-f, --state-file <path>', 'Set path to state file', '.migrate') 20 | .option('-s, --store <store>', 'Set the migrations store', path.join(__dirname, '..', 'lib', 'file-store')) 21 | .option('--clean', 'Tears down the migration state before running up') 22 | .option('-F, --force', 'Force through the command, ignoring warnings') 23 | .option('--init', 'Runs init for the store') 24 | .option('--migrations-dir <dir>', 'Change the migrations directory name', 'migrations') 25 | .option('--matches <glob>', 'A glob pattern to filter migration files', '*') 26 | .option('--compiler <ext:module>', 'Use the given module to compile files') 27 | .option('--env [name]', 'Use dotenv to load an environment file') 28 | .parse(process.argv) 29 | 30 | // Change the working dir 31 | process.chdir(program.chdir) 32 | 33 | // Setup environment 34 | if (program.env) { 35 | const e = dotenv.config({ 36 | path: typeof program.env === 'string' ? program.env : '.env' 37 | }) 38 | if (e && e.error instanceof Error) { 39 | throw e.error 40 | } 41 | } 42 | 43 | // Check clean flag, exit if NODE_ENV === 'production' and force not specified 44 | if (program.clean && process.env.NODE_ENV === 'production' && !program.force) { 45 | log.error('error', 'Cowardly refusing to clean while node environment set to production, use --force to continue.') 46 | process.exit(1) 47 | } 48 | 49 | // Check init flag, exit if NODE_ENV === 'production' and force not specified 50 | if (program.init && process.env.NODE_ENV === 'production' && !program.force) { 51 | log.error('error', 'Cowardly refusing to init while node environment set to production, use --force to continue.') 52 | process.exit(1) 53 | } 54 | 55 | // Load compiler 56 | if (program.compiler) { 57 | registerCompiler(program.compiler) 58 | } 59 | 60 | // Setup store 61 | if (program.store[0] === '.') program.store = path.join(process.cwd(), program.store) 62 | 63 | const StoreImport = require(program.store) 64 | const Store = StoreImport.default || StoreImport 65 | const store = new Store(program.stateFile) 66 | 67 | // Call store init 68 | if (program.init && typeof store.init === 'function') { 69 | store.init(function (err) { 70 | if (err) return log.error(err) 71 | loadAndGo() 72 | }) 73 | } else { 74 | loadAndGo() 75 | } 76 | 77 | // Load in migrations 78 | function loadAndGo () { 79 | migrate.load({ 80 | stateStore: store, 81 | migrationsDirectory: program.migrationsDir, 82 | filterFunction: minimatch.filter(program.matches), 83 | ignoreMissing: program.force 84 | }, function (err, set) { 85 | if (err) { 86 | log.error('error', err) 87 | process.exit(1) 88 | } 89 | 90 | set.on('warning', function (msg) { 91 | log('warning', msg) 92 | }) 93 | 94 | set.on('migration', function (migration, direction) { 95 | log(direction, migration.title) 96 | }) 97 | 98 | // Run 99 | ;(program.clean ? cleanUp : up)(set, function (err) { 100 | if (err) { 101 | log.error('error', err) 102 | process.exit(1) 103 | } 104 | log('migration', 'complete') 105 | process.exit(0) 106 | }) 107 | }) 108 | } 109 | 110 | function cleanUp (set, fn) { 111 | runMigrations(set, 'down', null, function (err) { 112 | if (err) { 113 | return fn(err) 114 | } 115 | up(set, fn) 116 | }) 117 | } 118 | 119 | function up (set, fn) { 120 | runMigrations(set, 'up', program.args[0], function (err) { 121 | if (err) { 122 | return fn(err) 123 | } 124 | fn() 125 | }) 126 | } 127 | -------------------------------------------------------------------------------- /examples/cli/migrations/1316027432511-add-pets.js: -------------------------------------------------------------------------------- 1 | const db = require('./db') 2 | 3 | exports.up = function (next) { 4 | db.rpush('pets', 'tobi') 5 | db.rpush('pets', 'loki', next) 6 | } 7 | 8 | exports.down = function (next) { 9 | db.rpop('pets') 10 | db.rpop('pets', next) 11 | } 12 | -------------------------------------------------------------------------------- /examples/cli/migrations/1316027432512-add-jane.js: -------------------------------------------------------------------------------- 1 | const db = require('./db') 2 | 3 | exports.up = function (next) { 4 | db.rpush('pets', 'jane', next) 5 | } 6 | 7 | exports.down = function (next) { 8 | db.rpop('pets', next) 9 | } 10 | -------------------------------------------------------------------------------- /examples/cli/migrations/1316027432575-add-owners.js: -------------------------------------------------------------------------------- 1 | const db = require('./db') 2 | 3 | exports.up = function (next) { 4 | db.rpush('owners', 'taylor') 5 | db.rpush('owners', 'tj', next) 6 | } 7 | 8 | exports.down = function (next) { 9 | db.rpop('owners') 10 | db.rpop('owners', next) 11 | } 12 | -------------------------------------------------------------------------------- /examples/cli/migrations/1316027433425-coolest-pet.js: -------------------------------------------------------------------------------- 1 | const db = require('./db') 2 | 3 | exports.up = function (next) { 4 | db.set('pets:coolest', 'tobi', next) 5 | } 6 | 7 | exports.down = function (next) { 8 | db.del('pets:coolest', next) 9 | } 10 | -------------------------------------------------------------------------------- /examples/cli/migrations/db.js: -------------------------------------------------------------------------------- 1 | // bad example, but you get the point ;) 2 | 3 | // $ npm install redis 4 | // $ redis-server 5 | 6 | const redis = require('redis') 7 | const db = redis.createClient() 8 | 9 | module.exports = db 10 | -------------------------------------------------------------------------------- /examples/cli/migrations/template.js: -------------------------------------------------------------------------------- 1 | // use `migrate create add-pets --template-file migrations/template.js` 2 | 3 | 'use strict' 4 | 5 | var db = require('./db') // eslint-disable-line 6 | 7 | exports.up = function (next) { 8 | next() 9 | } 10 | 11 | exports.down = function (next) { 12 | next() 13 | } 14 | -------------------------------------------------------------------------------- /examples/custom-state-storage/custom-state-storage.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const migrate = require('migrate') 3 | const { Client } = require('pg') 4 | const pg = new Client() 5 | 6 | /** 7 | * Stores and loads the executed migrations in the database. The table 8 | * migrations is only one row and stores a JSON of the data that the 9 | * migrate package uses to know which migrations have been executed. 10 | */ 11 | const customStateStorage = { 12 | load: async function (fn) { 13 | await pg.connect() 14 | 15 | // Load the single row of migration data from the database 16 | const { rows } = await pg.query('SELECT data FROM schema.migrations') 17 | 18 | if (rows.length !== 1) { 19 | console.log('Cannot read migrations from database. If this is the first time you run migrations, then this is normal.') 20 | return fn(null, {}) 21 | } 22 | 23 | // Call callback with new migration data object 24 | await pg.end() 25 | fn(null, rows[0].data) 26 | }, 27 | 28 | save: async function (set, fn) { 29 | await pg.connect() 30 | 31 | // Check if table 'migrations' exists and if not, create it. 32 | await pg.query('CREATE TABLE IF NOT EXISTS schema.migrations (id integer PRIMARY KEY, data jsonb NOT NULL)') 33 | 34 | await pg.query(` 35 | INSERT INTO schema.migrations (id, data) 36 | VALUES (1, $1) 37 | ON CONFLICT (id) DO UPDATE SET data = $1 38 | `, [{ 39 | lastRun: set.lastRun, 40 | migrations: set.migrations 41 | }]) 42 | 43 | await pg.end() 44 | fn() 45 | } 46 | } 47 | 48 | /** 49 | * Main application code 50 | */ 51 | migrate.load({ 52 | // Set class as custom stateStore 53 | stateStore: customStateStorage 54 | }, function (err, set) { 55 | if (err) { 56 | throw err 57 | } 58 | 59 | set.up((err2) => { 60 | if (err2) { 61 | throw err2 62 | } 63 | 64 | console.log('Migrations successfully ran') 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /examples/custom-state-storage/mongo-state-storage.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const migrate = require('migrate') 3 | const MongoClient = require('mongodb').MongoClient 4 | const url = 'mongodb://localhost/test' 5 | 6 | class MongoDbStore { 7 | async load (fn) { 8 | let client = null 9 | let data = null 10 | try { 11 | client = await MongoClient.connect(url) 12 | const db = client.db() 13 | data = await db.collection('db_migrations').find().toArray() 14 | if (data.length !== 1) { 15 | console.log('Cannot read migrations from database. If this is the first time you run migrations, then this is normal.') 16 | return fn(null, {}) 17 | } 18 | } catch (err) { 19 | console.log(err) 20 | throw err 21 | } finally { 22 | client.close() 23 | } 24 | return fn(null, data[0]) 25 | }; 26 | 27 | async save (set, fn) { 28 | let client = null 29 | let result = null 30 | try { 31 | client = await MongoClient.connect(url) 32 | const db = client.db() 33 | result = await db.collection('db_migrations') 34 | .update({}, { 35 | $set: { 36 | lastRun: set.lastRun 37 | }, 38 | $push: { 39 | migrations: { $each: set.migrations } 40 | } 41 | }, { upsert: true }) 42 | } catch (err) { 43 | console.log(err) 44 | throw err 45 | } finally { 46 | client.close() 47 | } 48 | 49 | return fn(null, result) 50 | } 51 | } 52 | 53 | /** 54 | * Main application code 55 | */ 56 | migrate.load({ 57 | // Set class as custom stateStore 58 | stateStore: new MongoDbStore() 59 | }, function (err, set) { 60 | if (err) { 61 | throw err 62 | } 63 | 64 | set.up((err2) => { 65 | if (err2) { 66 | throw err2 67 | } 68 | console.log('Migrations successfully ran') 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /examples/env/.env: -------------------------------------------------------------------------------- 1 | DB=contributors 2 | -------------------------------------------------------------------------------- /examples/env/.foo: -------------------------------------------------------------------------------- 1 | DB=foo 2 | -------------------------------------------------------------------------------- /examples/env/README.md: -------------------------------------------------------------------------------- 1 | # Environment Example 2 | 3 | ``` 4 | $ migrate up --env 5 | $ migrate down --env 6 | $ cat .db # should see table of `contributors` 7 | 8 | $ migrate up --env .foo 9 | $ migrate down --env .foo 10 | $ cat .db # should see table of `foo` 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/env/db.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | 4 | module.exports = { 5 | loaded: false, 6 | tables: {}, 7 | table: function (name) { 8 | this.tables[name] = [] 9 | this.save() 10 | }, 11 | removeTable: function (name) { 12 | delete this.tables[name] 13 | this.save() 14 | }, 15 | insert: function (table, value) { 16 | this.tables[table].push(value) 17 | this.save() 18 | }, 19 | remove: function (table, value) { 20 | this.tables[table].splice(this.tables[table].indexOf(value), 1) 21 | this.save() 22 | }, 23 | save: function () { 24 | fs.writeFileSync('.db', JSON.stringify(this)) 25 | }, 26 | load: function () { 27 | if (this.loaded) return this 28 | let json 29 | try { 30 | json = JSON.parse(fs.readFileSync('.db', 'utf8')) 31 | } catch (e) { 32 | // ignore 33 | return this 34 | } 35 | this.loaded = true 36 | this.tables = json.tables 37 | return this 38 | }, 39 | toJSON: function () { 40 | return { 41 | tables: this.tables 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/migrate.js: -------------------------------------------------------------------------------- 1 | // bad example, but you get the point ;) 2 | 3 | // $ npm install redis 4 | // $ redis-server 5 | 6 | const path = require('path') 7 | const migrate = require('../') 8 | const redis = require('redis') 9 | const db = redis.createClient() 10 | 11 | migrate(path.join(__dirname, '.migrate')) 12 | 13 | migrate('add pets', function (next) { 14 | db.rpush('pets', 'tobi') 15 | db.rpush('pets', 'loki', next) 16 | }, function (next) { 17 | db.rpop('pets') 18 | db.rpop('pets', next) 19 | }) 20 | 21 | migrate('add jane', function (next) { 22 | db.rpush('pets', 'jane', next) 23 | }, function (next) { 24 | db.rpop('pets', next) 25 | }) 26 | 27 | migrate('add owners', function (next) { 28 | db.rpush('owners', 'taylor') 29 | db.rpush('owners', 'tj', next) 30 | }, function (next) { 31 | db.rpop('owners') 32 | db.rpop('owners', next) 33 | }) 34 | 35 | migrate('coolest pet', function (next) { 36 | db.set('pets:coolest', 'tobi', next) 37 | }, function (next) { 38 | db.del('pets:coolest', next) 39 | }) 40 | 41 | const set = migrate() 42 | 43 | console.log() 44 | set.on('save', function () { 45 | console.log() 46 | }) 47 | 48 | set.on('migration', function (migration, direction) { 49 | console.log(direction, migration.title) 50 | }) 51 | 52 | set.up(function (err) { 53 | if (err) throw err 54 | process.exit() 55 | }) 56 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import type { EventEmitter } from "events"; 2 | 3 | type MigrationOptions = { 4 | set?: MigrationSet; 5 | stateStore?: string | FileStore; 6 | migrations?: { [key: string]: { up: Function, down: Function } }; 7 | migrationsDirectory?: string; 8 | ignoreMissing?: boolean; 9 | filterFunction?: (migration: string) => boolean; 10 | sortFunction?: (migration1: Migration, migration2: Migration) => boolean; 11 | }; 12 | 13 | type NextFunction = () => void; 14 | /** 15 | * Callback to continue migration. 16 | * Result should be an `Error` if the method failed 17 | * Result should be `null` if the method was successful 18 | */ 19 | type Callback = (result: Error | null) => void; 20 | 21 | export default function migrate( 22 | title: string, 23 | up: (next: NextFunction) => void, 24 | down: (next: NextFunction) => void 25 | ): void; 26 | 27 | export function load( 28 | opts: MigrationOptions, 29 | cb: (err: any, set: MigrationSet) => void 30 | ): void; 31 | 32 | declare class Migration { 33 | constructor( 34 | title: string, 35 | up: (next: NextFunction) => void, 36 | down: (next: NextFunction) => void, 37 | description: string 38 | ); 39 | title: string; 40 | up: (next: NextFunction) => void; 41 | down: (next: NextFunction) => void; 42 | description: string; 43 | timestamp: number | null; 44 | } 45 | 46 | export class MigrationSet extends EventEmitter { 47 | constructor(store: FileStore); 48 | addMigration( 49 | title: string, 50 | up: (next: NextFunction) => void, 51 | down: (next: NextFunction) => void 52 | ): void; 53 | addMigration(migration: Migration): void; 54 | save(cb: Callback): void; 55 | down(migrationName: string, cb: Callback): void; 56 | down(cb: Callback): void; 57 | up(migrationName: string, cb: Callback): void; 58 | up(cb: Callback): void; 59 | migrate( 60 | direction: "up" | "down", 61 | migrationName: string, 62 | cb: Callback 63 | ): void; 64 | migrate(direction: "up" | "down", cb: Callback): void; 65 | migrations: Migration[]; 66 | map: { [title: string]: Migration }; 67 | lastRun: string | null; 68 | } 69 | 70 | /** 71 | * Callback to invoke after loading migration state from filestore 72 | * If loading failed, just the error should be passed as `err` 73 | * If loading succeeded, `err` should be null and `store` should be the migration state that was loaded 74 | */ 75 | type FileStoreLoadCallback = ((err: Error) => void) & ((err: null, store: { 76 | lastRun?: string; 77 | migrations: Pick<Migration, "title" | "description" | "timestamp">[]; 78 | }) => void); 79 | 80 | declare class FileStore { 81 | constructor(path: string); 82 | save(set: MigrationSet, cb: Callback): void; 83 | load(cb: FileStoreLoadCallback): void; 84 | } 85 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /*! 4 | * migrate 5 | * Copyright(c) 2011 TJ Holowaychuk <tj@vision-media.ca> 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | const MigrationSet = require('./lib/set') 14 | const FileStore = require('./lib/file-store') 15 | const loadMigrationsIntoSet = require('./lib/load-migrations') 16 | 17 | /** 18 | * Expose the migrate function. 19 | */ 20 | 21 | exports = module.exports = migrate 22 | 23 | function migrate (title, up, down) { 24 | // migration 25 | if (typeof title === 'string' && up && down) { 26 | migrate.set.addMigration(title, up, down) 27 | // specify migration file 28 | } else if (typeof title === 'string') { 29 | migrate.set = exports.load(title) 30 | // no migration path 31 | } else if (!migrate.set) { 32 | throw new Error('must invoke migrate(path) before running migrations') 33 | // run migrations 34 | } else { 35 | return migrate.set 36 | } 37 | } 38 | 39 | /** 40 | * Expose MigrationSet 41 | */ 42 | exports.MigrationSet = MigrationSet 43 | 44 | exports.load = function (options, fn) { 45 | const opts = options || {} 46 | 47 | // Create default store 48 | const store = (typeof opts.stateStore === 'string') ? new FileStore(opts.stateStore) : opts.stateStore 49 | 50 | // Create migration set 51 | const set = new MigrationSet(store) 52 | 53 | loadMigrationsIntoSet({ 54 | set, 55 | store, 56 | migrations: opts.migrations, 57 | migrationsDirectory: opts.migrationsDirectory, 58 | filterFunction: opts.filterFunction, 59 | sortFunction: opts.sortFunction, 60 | ignoreMissing: opts.ignoreMissing 61 | }, function (err) { 62 | fn(err, set) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /lib/file-store.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const hasOwnProperty = Object.prototype.hasOwnProperty 5 | 6 | module.exports = FileStore 7 | 8 | function FileStore (path) { 9 | this.path = path 10 | } 11 | 12 | /** 13 | * Save the migration data. 14 | * 15 | * @api public 16 | */ 17 | 18 | FileStore.prototype.save = function (set, fn) { 19 | fs.writeFile(this.path, JSON.stringify({ 20 | lastRun: set.lastRun, 21 | migrations: set.migrations 22 | }, null, ' '), fn) 23 | } 24 | 25 | /** 26 | * Load the migration data and call `fn(err, obj)`. 27 | * 28 | * @param {Function} fn 29 | * @return {Type} 30 | * @api public 31 | */ 32 | 33 | FileStore.prototype.load = function (fn) { 34 | fs.readFile(this.path, 'utf8', function (err, json) { 35 | if (err && err.code !== 'ENOENT') return fn(err) 36 | if (!json || json === '') { 37 | return fn(null, {}) 38 | } 39 | 40 | let store 41 | try { 42 | store = JSON.parse(json) 43 | } catch (err) { 44 | return fn(err) 45 | } 46 | 47 | // Check if old format and convert if needed 48 | if (!hasOwnProperty.call(store, 'lastRun') && hasOwnProperty.call(store, 'pos')) { 49 | if (store.pos === 0) { 50 | store.lastRun = null 51 | } else { 52 | if (store.pos > store.migrations.length) { 53 | return fn(new Error('Store file contains invalid pos property')) 54 | } 55 | 56 | store.lastRun = store.migrations[store.pos - 1].title 57 | } 58 | 59 | // In-place mutate the migrations in the array 60 | store.migrations.forEach(function (migration, index) { 61 | if (index < store.pos) { 62 | migration.timestamp = Date.now() 63 | } 64 | }) 65 | } 66 | 67 | // Check if does not have required properties 68 | if (!hasOwnProperty.call(store, 'lastRun') || !hasOwnProperty.call(store, 'migrations')) { 69 | return fn(new Error('Invalid store file')) 70 | } 71 | 72 | return fn(null, store) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /lib/load-migrations.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const fs = require('fs').promises 5 | const url = require('node:url') 6 | const Migration = require('./migration') 7 | 8 | module.exports = loadMigrationsIntoSet 9 | 10 | async function loadFromMigrationsDirectory (migrationsDirectory, filterFn) { 11 | // Read migrations directory 12 | let files = await fs.readdir(migrationsDirectory) 13 | 14 | // Filter out non-matching files 15 | files = files.filter(filterFn) 16 | 17 | const migMap = {} 18 | const promises = files.map(async function (file) { 19 | // Try to load the migrations file 20 | const filepath = path.join(migrationsDirectory, file) 21 | let mod 22 | try { 23 | mod = require(filepath) 24 | } catch (e) { 25 | if (e.code === 'ERR_REQUIRE_ESM') { 26 | mod = await import(url.pathToFileURL(filepath)) 27 | } else { 28 | throw e 29 | } 30 | } 31 | 32 | const migration = new Migration(file, mod.up, mod.down, mod.description) 33 | migMap[file] = migration 34 | return migration 35 | }) 36 | await Promise.all(promises) 37 | return migMap 38 | } 39 | 40 | function loadFromMigrations (migrations, filterFn) { 41 | return Object 42 | .keys(migrations) 43 | .filter(filterFn) 44 | .reduce((migMap, migrationName) => { 45 | const mod = migrations[migrationName] 46 | migMap[migrationName] = new Migration(migrationName, mod.up, mod.down, mod.description) 47 | return migMap 48 | }, {}) 49 | } 50 | 51 | function loadMigrationsIntoSet (options, fn) { 52 | // Process options, set and store are required, rest optional 53 | const opts = options || {} 54 | if (!opts.set || !opts.store) { 55 | throw new TypeError((opts.set ? 'store' : 'set') + ' is required for loading migrations') 56 | } 57 | const set = opts.set 58 | const store = opts.store 59 | const ignoreMissing = !!opts.ignoreMissing 60 | const migrations = opts.migrations 61 | const migrationsDirectory = path.resolve(opts.migrationsDirectory || 'migrations') 62 | const filterFn = opts.filterFunction || (() => true) 63 | const sortFn = opts.sortFunction || function (m1, m2) { 64 | return m1.title > m2.title ? 1 : (m1.title < m2.title ? -1 : 0) 65 | } 66 | 67 | // Load from migrations store first up 68 | store.load(async function (err, state) { 69 | if (err) return fn(err) 70 | 71 | try { 72 | // Set last run date on the set 73 | set.lastRun = state.lastRun || null 74 | 75 | // Create migrations, keep a lookup map for the next step 76 | const migMap = (migrations) 77 | ? loadFromMigrations(migrations, filterFn) 78 | : await loadFromMigrationsDirectory(migrationsDirectory, filterFn) 79 | 80 | // Fill in timestamp from state, or error if missing 81 | state.migrations && state.migrations.forEach(function (m) { 82 | if (m.timestamp !== null && !migMap[m.title]) { 83 | return ignoreMissing ? null : fn(new Error('Missing migration file: ' + m.title)) 84 | } else if (!migMap[m.title]) { 85 | // Migration existed in state file, but was not run and not loadable 86 | return 87 | } 88 | migMap[m.title].timestamp = m.timestamp 89 | }) 90 | 91 | Object 92 | .values(migMap) 93 | .sort(sortFn) 94 | .forEach(set.addMigration.bind(set)) 95 | 96 | // Successfully loaded 97 | fn() 98 | } catch (e) { 99 | fn(e) 100 | } 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /lib/log.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | 4 | module.exports = function log (key, msg) { 5 | console.log(' ' + chalk.grey(key) + ' : ' + chalk.cyan(msg)) 6 | } 7 | 8 | module.exports.error = function log (key, msg) { 9 | console.error(' ' + chalk.red(key) + ' : ' + chalk.white(msg)) 10 | if (msg instanceof Error) { 11 | console.error(msg) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/migrate.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = migrate 4 | 5 | function migrate (set, direction, migrationName, fn) { 6 | let migrations = [] 7 | let lastRunIndex 8 | let toIndex 9 | 10 | if (!migrationName) { 11 | toIndex = direction === 'up' ? set.migrations.length : 0 12 | } else if ((toIndex = positionOfMigration(set.migrations, migrationName)) === -1) { 13 | return fn(new Error('Could not find migration: ' + migrationName)) 14 | } 15 | 16 | lastRunIndex = positionOfMigration(set.migrations, set.lastRun) 17 | 18 | migrations = (direction === 'up' ? upMigrations : downMigrations)(set, lastRunIndex, toIndex) 19 | 20 | function next (migration) { 21 | // Done running migrations 22 | if (!migration) return fn(null) 23 | 24 | // Missing direction method 25 | if (typeof migration[direction] !== 'function') { 26 | return fn(new TypeError('Migration ' + migration.title + ' does not have method ' + direction)) 27 | } 28 | 29 | // Status for supporting promises and callbacks 30 | let isPromise = false 31 | 32 | // Run the migration function 33 | set.emit('migration', migration, direction) 34 | const arity = migration[direction].length 35 | const returnValue = migration[direction](function (err) { 36 | if (isPromise) return set.emit('warning', 'if your migration returns a promise, do not call the done callback') 37 | completeMigration(err) 38 | }) 39 | 40 | // Is it a promise? 41 | isPromise = typeof Promise !== 'undefined' && returnValue instanceof Promise 42 | 43 | // If not a promise and arity is not 1, warn 44 | 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') 45 | 46 | // Handle the promises 47 | if (isPromise) { 48 | returnValue 49 | .then(() => completeMigration(null)) 50 | .catch(fn) 51 | } 52 | 53 | function completeMigration (err) { 54 | if (err) return fn(err) 55 | 56 | // Set timestamp if running up, clear it if down 57 | migration.timestamp = direction === 'up' ? Date.now() : null 58 | 59 | // Decrement last run index 60 | lastRunIndex-- 61 | 62 | if (direction === 'up') { 63 | set.lastRun = migration.title 64 | } else { 65 | set.lastRun = set.migrations[lastRunIndex] ? set.migrations[lastRunIndex].title : null 66 | } 67 | 68 | set.save(function (err) { 69 | if (err) return fn(err) 70 | 71 | next(migrations.shift()) 72 | }) 73 | } 74 | } 75 | 76 | next(migrations.shift()) 77 | } 78 | 79 | function upMigrations (set, lastRunIndex, toIndex) { 80 | return set.migrations.reduce(function (arr, migration, index) { 81 | if (index > toIndex) { 82 | return arr 83 | } 84 | 85 | if (index < lastRunIndex && !migration.timestamp) { 86 | set.emit('warning', 'migrations running out of order') 87 | } 88 | 89 | if (!migration.timestamp) { 90 | arr.push(migration) 91 | } 92 | 93 | return arr 94 | }, []) 95 | } 96 | 97 | function downMigrations (set, lastRunIndex, toIndex) { 98 | return set.migrations.reduce(function (arr, migration, index) { 99 | if (index < toIndex || index > lastRunIndex) { 100 | return arr 101 | } 102 | 103 | if (migration.timestamp) { 104 | arr.push(migration) 105 | } 106 | 107 | return arr 108 | }, []).reverse() 109 | } 110 | 111 | /** 112 | * Get index of given migration in list of migrations 113 | * 114 | * @api private 115 | */ 116 | 117 | function positionOfMigration (migrations, title) { 118 | let lastTimestamp 119 | for (let i = 0; i < migrations.length; ++i) { 120 | lastTimestamp = migrations[i].timestamp ? i : lastTimestamp 121 | if (migrations[i].title === title) return i 122 | } 123 | 124 | // If titled migration was missing use last timestamped 125 | return lastTimestamp 126 | } 127 | -------------------------------------------------------------------------------- /lib/migration.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /*! 4 | * migrate - Migration 5 | * Copyright (c) 2010 TJ Holowaychuk <tj@vision-media.ca> 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Expose `Migration`. 11 | */ 12 | 13 | module.exports = Migration 14 | 15 | function Migration (title, up, down, description) { 16 | this.title = title 17 | this.up = up 18 | this.down = down 19 | this.description = description 20 | this.timestamp = null 21 | } 22 | -------------------------------------------------------------------------------- /lib/register-compiler.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | 4 | module.exports = registerCompiler 5 | 6 | function registerCompiler (c) { 7 | const compiler = c.split(':') 8 | const ext = compiler[0] 9 | let mod = compiler[1] 10 | 11 | if (mod[0] === '.') mod = path.join(process.cwd(), mod) 12 | require(mod)({ 13 | extensions: ['.' + ext] 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /lib/set.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /*! 4 | * migrate - Set 5 | * Copyright (c) 2010 TJ Holowaychuk <tj@vision-media.ca> 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | const EventEmitter = require('events') 14 | const Migration = require('./migration') 15 | const migrate = require('./migrate') 16 | const inherits = require('inherits') 17 | 18 | /** 19 | * Expose `Set`. 20 | */ 21 | 22 | module.exports = MigrationSet 23 | 24 | /** 25 | * Initialize a new migration `Set` with the given `path` 26 | * which is used to store data between migrations. 27 | * 28 | * @param {String} path 29 | * @api private 30 | */ 31 | 32 | function MigrationSet (store) { 33 | this.store = store 34 | this.migrations = [] 35 | this.map = {} 36 | this.lastRun = null 37 | }; 38 | 39 | /** 40 | * Inherit from `EventEmitter.prototype`. 41 | */ 42 | 43 | inherits(MigrationSet, EventEmitter) 44 | 45 | /** 46 | * Add a migration. 47 | * 48 | * @param {String} title 49 | * @param {Function} up 50 | * @param {Function} down 51 | * @api public 52 | */ 53 | 54 | MigrationSet.prototype.addMigration = function (title, up, down) { 55 | let migration 56 | if (!(title instanceof Migration)) { 57 | migration = new Migration(title, up, down) 58 | } else { 59 | migration = title 60 | } 61 | 62 | // Only add the migration once, but update 63 | if (this.map[migration.title]) { 64 | this.map[migration.title].up = migration.up 65 | this.map[migration.title].down = migration.down 66 | this.map[migration.title].description = migration.description 67 | return 68 | } 69 | 70 | this.migrations.push(migration) 71 | this.map[migration.title] = migration 72 | } 73 | 74 | /** 75 | * Save the migration data. 76 | * 77 | * @api public 78 | */ 79 | 80 | MigrationSet.prototype.save = function (fn) { 81 | this.store.save(this, (err) => { 82 | if (err) return fn(err) 83 | this.emit('save') 84 | fn(null) 85 | }) 86 | } 87 | 88 | /** 89 | * Run down migrations and call `fn(err)`. 90 | * 91 | * @param {Function} fn 92 | * @api public 93 | */ 94 | 95 | MigrationSet.prototype.down = function (migrationName, fn) { 96 | this.migrate('down', migrationName, fn) 97 | } 98 | 99 | /** 100 | * Run up migrations and call `fn(err)`. 101 | * 102 | * @param {Function} fn 103 | * @api public 104 | */ 105 | 106 | MigrationSet.prototype.up = function (migrationName, fn) { 107 | this.migrate('up', migrationName, fn) 108 | } 109 | 110 | /** 111 | * Migrate in the given `direction`, calling `fn(err)`. 112 | * 113 | * @param {String} direction 114 | * @param {Function} fn 115 | * @api public 116 | */ 117 | 118 | MigrationSet.prototype.migrate = function (direction, migrationName, fn) { 119 | if (typeof migrationName === 'function') { 120 | fn = migrationName 121 | migrationName = null 122 | } 123 | migrate(this, direction, migrationName, fn) 124 | } 125 | -------------------------------------------------------------------------------- /lib/template-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const fs = require('fs') 4 | const slug = require('slug') 5 | const formatDate = require('dateformat') 6 | const { mkdirp } = require('mkdirp') 7 | 8 | module.exports = function templateGenerator (opts, cb) { 9 | // Setup default options 10 | opts = opts || {} 11 | const name = opts.name 12 | const dateFormat = opts.dateFormat 13 | const templateFile = opts.templateFile || path.join(__dirname, 'template.js') 14 | const migrationsDirectory = opts.migrationsDirectory || 'migrations' 15 | const extension = opts.extension 16 | 17 | loadTemplate(templateFile, function (err, template) { 18 | if (err) return cb(err) 19 | 20 | // Ensure migrations directory exists 21 | mkdirp(migrationsDirectory).then(function () { 22 | // Create date string 23 | const formattedDate = dateFormat ? formatDate(new Date(), dateFormat) : Date.now() 24 | 25 | // Fix up file path 26 | const p = path.join(path.resolve(migrationsDirectory), slug(formattedDate + (name ? '-' + name : '')) + extension) 27 | 28 | // Write the template file 29 | fs.writeFile(p, template, function (err) { 30 | if (err) return cb(err) 31 | cb(null, p) 32 | }) 33 | }).catch(cb) 34 | }) 35 | } 36 | 37 | const _templateCache = {} 38 | function loadTemplate (tmpl, cb) { 39 | if (_templateCache[tmpl]) { 40 | return cb(null, _templateCache) 41 | } 42 | fs.readFile(tmpl, { 43 | encoding: 'utf8' 44 | }, function (err, content) { 45 | if (err) return cb(err) 46 | _templateCache[tmpl] = content 47 | cb(null, content) 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /lib/template.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports.up = function (next) { 4 | next() 5 | } 6 | 7 | module.exports.down = function (next) { 8 | next() 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "migrate", 3 | "version": "2.1.0", 4 | "description": "Abstract migration framework for node", 5 | "keywords": [ 6 | "migrate", 7 | "migrations" 8 | ], 9 | "author": "TJ Holowaychuk <tj@vision-media.ca>", 10 | "repository": "git://github.com/tj/node-migrate", 11 | "bin": { 12 | "migrate": "./bin/migrate", 13 | "migrate-init": "./bin/migrate-init", 14 | "migrate-list": "./bin/migrate-list", 15 | "migrate-create": "./bin/migrate-create", 16 | "migrate-up": "./bin/migrate-up", 17 | "migrate-down": "./bin/migrate-down" 18 | }, 19 | "devDependencies": { 20 | "mocha": "^10.2.0", 21 | "rimraf": "^5.0.1", 22 | "standard": "^17.0.0" 23 | }, 24 | "main": "index", 25 | "engines": { 26 | "node": ">= 14.0.0" 27 | }, 28 | "types": "index.d.ts", 29 | "scripts": { 30 | "test": "standard && standard ./bin/* && mocha", 31 | "prepublishOnly": "npm t", 32 | "postpublish": "git push && git push --tags" 33 | }, 34 | "license": "MIT", 35 | "dependencies": { 36 | "chalk": "^4.1.2", 37 | "commander": "^2.20.3", 38 | "dateformat": "^4.6.3", 39 | "dotenv": "^16.0.0", 40 | "inherits": "^2.0.3", 41 | "minimatch": "^9.0.1", 42 | "mkdirp": "^3.0.1", 43 | "slug": "^8.2.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | const { rimraf } = require('rimraf') 4 | const path = require('path') 5 | const assert = require('assert') 6 | const migrate = require('../') 7 | const db = require('./util/db') 8 | 9 | const BASE = path.join(__dirname, 'fixtures', 'basic') 10 | const STATE = path.join(BASE, '.migrate') 11 | 12 | describe('migration set', function () { 13 | let set 14 | 15 | function assertNoPets () { 16 | assert.strictEqual(db.pets.length, 0) 17 | } 18 | 19 | function assertPets () { 20 | assert.strictEqual(db.pets.length, 3) 21 | assert.strictEqual(db.pets[0].name, 'tobi') 22 | assert.strictEqual(db.pets[0].email, 'tobi@learnboost.com') 23 | } 24 | 25 | function assertPetsWithDogs () { 26 | assert.strictEqual(db.pets.length, 5) 27 | assert.strictEqual(db.pets[0].name, 'tobi') 28 | assert.strictEqual(db.pets[0].email, 'tobi@learnboost.com') 29 | assert.strictEqual(db.pets[4].name, 'suki') 30 | } 31 | 32 | function assertFirstMigration () { 33 | assert.strictEqual(db.pets.length, 2) 34 | assert.strictEqual(db.pets[0].name, 'tobi') 35 | assert.strictEqual(db.pets[1].name, 'loki') 36 | } 37 | 38 | function assertSecondMigration () { 39 | assert.strictEqual(db.pets.length, 3) 40 | assert.strictEqual(db.pets[0].name, 'tobi') 41 | assert.strictEqual(db.pets[1].name, 'loki') 42 | assert.strictEqual(db.pets[2].name, 'jane') 43 | } 44 | 45 | beforeEach(function (done) { 46 | migrate.load( 47 | { 48 | stateStore: STATE, 49 | migrationsDirectory: BASE 50 | }, 51 | function (err, s) { 52 | set = s 53 | done(err) 54 | } 55 | ) 56 | }) 57 | 58 | it('should handle basic migration', function (done) { 59 | set.up(function (err) { 60 | assert.ifError(err) 61 | assertPets() 62 | set.up(function (err) { 63 | assert.ifError(err) 64 | assertPets() 65 | set.down(function (err) { 66 | assert.ifError(err) 67 | assertNoPets() 68 | set.down(function (err) { 69 | assert.ifError(err) 70 | assertNoPets() 71 | set.up(function (err) { 72 | assert.ifError(err) 73 | assertPets() 74 | done() 75 | }) 76 | }) 77 | }) 78 | }) 79 | }) 80 | }) 81 | 82 | it('should add a new migration', function (done) { 83 | set.addMigration( 84 | 'add dogs', 85 | function (next) { 86 | db.pets.push({ name: 'simon' }) 87 | db.pets.push({ name: 'suki' }) 88 | next() 89 | }, 90 | function (next) { 91 | db.pets.pop() 92 | db.pets.pop() 93 | next() 94 | } 95 | ) 96 | 97 | set.up(function (err) { 98 | assert.ifError(err) 99 | assertPetsWithDogs() 100 | set.up(function (err) { 101 | assert.ifError(err) 102 | assertPetsWithDogs() 103 | set.down(function (err) { 104 | assert.ifError(err) 105 | assertNoPets() 106 | done() 107 | }) 108 | }) 109 | }) 110 | }) 111 | 112 | it('should emit events', function (done) { 113 | set.addMigration( 114 | '4-adjust-emails.js', 115 | function (next) { 116 | db.pets.forEach(function (pet) { 117 | if (pet.email) { 118 | pet.email = pet.email.replace('learnboost.com', 'lb.com') 119 | } 120 | }) 121 | next() 122 | }, 123 | function (next) { 124 | db.pets.forEach(function (pet) { 125 | if (pet.email) { 126 | pet.email = pet.email.replace('lb.com', 'learnboost.com') 127 | } 128 | }) 129 | next() 130 | } 131 | ) 132 | 133 | let saved = 0 134 | let migrations = [] 135 | let expectedMigrations = [ 136 | '1-add-guy-ferrets.js', 137 | '2-add-girl-ferrets.js', 138 | '3-add-emails.js', 139 | '4-adjust-emails.js' 140 | ] 141 | 142 | set.on('save', function () { 143 | saved++ 144 | }) 145 | 146 | set.on('migration', function (migration, direction) { 147 | migrations.push(migration.title) 148 | assert.strictEqual(typeof direction, 'string') 149 | }) 150 | 151 | set.up(function (err) { 152 | assert.ifError(err) 153 | assert.strictEqual(saved, 4) 154 | assert.strictEqual(db.pets[0].email, 'tobi@lb.com') 155 | assert.deepStrictEqual(migrations, expectedMigrations) 156 | 157 | migrations = [] 158 | expectedMigrations = expectedMigrations.reverse() 159 | 160 | set.down(function (err) { 161 | assert.ifError(err) 162 | assert.strictEqual(saved, 8) 163 | assert.deepStrictEqual(migrations, expectedMigrations) 164 | assertNoPets() 165 | done() 166 | }) 167 | }) 168 | }) 169 | 170 | it('should migrate to named migration', function (done) { 171 | assertNoPets() 172 | set.up('1-add-guy-ferrets.js', function (err) { 173 | assert.ifError(err) 174 | assertFirstMigration() 175 | set.up('2-add-girl-ferrets.js', function (err) { 176 | assert.ifError(err) 177 | assertSecondMigration() 178 | set.down('2-add-girl-ferrets.js', function (err) { 179 | assert.ifError(err) 180 | assertFirstMigration() 181 | set.up('2-add-girl-ferrets.js', function (err) { 182 | assert.ifError(err) 183 | assertSecondMigration() 184 | assert.strictEqual(set.lastRun, '2-add-girl-ferrets.js') 185 | set.down('2-add-girl-ferrets.js', function (err) { 186 | assert.ifError(err) 187 | assert.strictEqual(set.lastRun, '1-add-guy-ferrets.js') 188 | done() 189 | }) 190 | }) 191 | }) 192 | }) 193 | }) 194 | }) 195 | 196 | it('should load migration descriptions', function () { 197 | assert.strictEqual(set.migrations[0].description, 'Adds two pets') 198 | }) 199 | 200 | afterEach(function () { 201 | db.nuke() 202 | return rimraf(STATE) 203 | }) 204 | }) 205 | -------------------------------------------------------------------------------- /test/cli.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | const path = require('path') 4 | const fs = require('fs') 5 | const assert = require('assert') 6 | const rimraf = require('rimraf') 7 | const { mkdirp } = require('mkdirp') 8 | const formatDate = require('dateformat') 9 | const db = require('./util/db') 10 | const run = require('./util/run') 11 | 12 | // Paths 13 | const FIX_DIR = path.join(__dirname, 'fixtures', 'numbers') 14 | const TMP_DIR = path.join(__dirname, 'fixtures', 'tmp') 15 | const UP = path.join(__dirname, '..', 'bin', 'migrate-up') 16 | const DOWN = path.join(__dirname, '..', 'bin', 'migrate-down') 17 | const CREATE = path.join(__dirname, '..', 'bin', 'migrate-create') 18 | const INIT = path.join(__dirname, '..', 'bin', 'migrate-init') 19 | const LIST = path.join(__dirname, '..', 'bin', 'migrate-list') 20 | 21 | // Run helper 22 | const up = run.bind(null, UP, FIX_DIR) 23 | const down = run.bind(null, DOWN, FIX_DIR) 24 | const create = run.bind(null, CREATE, TMP_DIR) 25 | const init = run.bind(null, INIT, TMP_DIR) 26 | const list = run.bind(null, LIST, FIX_DIR) 27 | 28 | function reset () { 29 | rimraf.sync(path.join(FIX_DIR, '.migrate')) 30 | rimraf.sync(TMP_DIR) 31 | db.nuke() 32 | } 33 | 34 | describe('$ migrate', function () { 35 | beforeEach(reset) 36 | afterEach(reset) 37 | 38 | describe('init', function () { 39 | beforeEach(() => { 40 | return mkdirp(TMP_DIR) 41 | }) 42 | 43 | it('should create a migrations directory', function (done) { 44 | init([], function (err, out, code) { 45 | assert(!err) 46 | assert.strictEqual(code, 0) 47 | assert.doesNotThrow(() => { 48 | fs.accessSync(path.join(TMP_DIR, 'migrations')) 49 | }) 50 | done() 51 | }) 52 | }) 53 | }) // end init 54 | 55 | describe('create', function () { 56 | beforeEach(() => { 57 | return mkdirp(TMP_DIR) 58 | }) 59 | 60 | it('should create a fixture file', function (done) { 61 | create(['test'], function (err, out, code) { 62 | assert(!err) 63 | assert.strictEqual(code, 0) 64 | const file = out.split(':')[1].trim() 65 | const content = fs.readFileSync(file, { 66 | encoding: 'utf8' 67 | }) 68 | assert(content) 69 | assert(content.indexOf('module.exports.up') !== -1) 70 | assert(content.indexOf('module.exports.down') !== -1) 71 | done() 72 | }) 73 | }) 74 | 75 | it('should respect the --date-format', function (done) { 76 | const name = 'test' 77 | const fmt = 'yyyy-mm-dd' 78 | const now = formatDate(new Date(), fmt) 79 | 80 | create([name, '-d', fmt], function (err, out, code) { 81 | assert(!err) 82 | assert.strictEqual(code, 0) 83 | assert.doesNotThrow(() => { 84 | fs.accessSync(path.join(TMP_DIR, 'migrations', now + '-' + name + '.js')) 85 | }) 86 | done() 87 | }) 88 | }) 89 | 90 | it('should respect the --extension', function (done) { 91 | const name = 'test' 92 | const fmt = 'yyyy-mm-dd' 93 | const ext = '.mjs' 94 | const now = formatDate(new Date(), fmt) 95 | 96 | create([name, '-d', fmt, '-e', ext], function (err, out, code) { 97 | assert(!err) 98 | assert.strictEqual(code, 0) 99 | assert.doesNotThrow(() => { 100 | fs.accessSync(path.join(TMP_DIR, 'migrations', now + '-' + name + ext)) 101 | }) 102 | done() 103 | }) 104 | }) 105 | 106 | it('should default the extension to the template file extension', function (done) { 107 | const name = 'test' 108 | const fmt = 'yyyy-mm-dd' 109 | const ext = '.mjs' 110 | const now = formatDate(new Date(), fmt) 111 | 112 | create([name, '-d', fmt, '-t', path.join(__dirname, 'util', 'tmpl' + ext)], function (err, out, code) { 113 | assert(!err) 114 | assert.strictEqual(code, 0) 115 | assert.doesNotThrow(() => { 116 | fs.accessSync(path.join(TMP_DIR, 'migrations', now + '-' + name + ext)) 117 | }) 118 | done() 119 | }) 120 | }) 121 | 122 | it('should use the --template-file flag', function (done) { 123 | create(['test', '-t', path.join(__dirname, 'util', 'tmpl.js')], function (err, out, code) { 124 | assert(!err) 125 | assert.strictEqual(code, 0, out) 126 | assert(out.indexOf('create') !== -1) 127 | const file = out.split(':')[1].trim() 128 | const content = fs.readFileSync(file, { 129 | encoding: 'utf8' 130 | }) 131 | assert(content.indexOf('test') !== -1) 132 | done() 133 | }) 134 | }) 135 | 136 | it('should fail with non-zero and a helpful message when template is unreadable', function (done) { 137 | create(['test', '-t', 'fake'], function (err, out, code) { 138 | assert(!err) 139 | assert.strictEqual(code, 1) 140 | assert(out.indexOf('fake') !== -1) 141 | done() 142 | }) 143 | }) 144 | }) // end create 145 | 146 | describe('up', function () { 147 | it('should run up on multiple migrations', function (done) { 148 | up([], function (err, out, code) { 149 | assert(!err) 150 | assert.strictEqual(code, 0) 151 | db.load() 152 | assert(out.indexOf('up') !== -1) 153 | assert.strictEqual(db.numbers.length, 2) 154 | assert(db.numbers.indexOf(1) !== -1) 155 | assert(db.numbers.indexOf(2) !== -1) 156 | done() 157 | }) 158 | }) 159 | 160 | it('should run up to a specified migration', function (done) { 161 | up(['1-one.js'], function (err, out, code) { 162 | assert(!err) 163 | assert.strictEqual(code, 0) 164 | db.load() 165 | assert(out.indexOf('up') !== -1) 166 | assert.strictEqual(db.numbers.length, 1) 167 | assert(db.numbers.indexOf(1) !== -1) 168 | assert(db.numbers.indexOf(2) === -1) 169 | done() 170 | }) 171 | }) 172 | 173 | it('should run up multiple times', function (done) { 174 | up([], function (err, out, code) { 175 | assert(!err) 176 | assert.strictEqual(code, 0) 177 | db.load() 178 | assert(out.indexOf('up') !== -1) 179 | up([], function (err, out) { 180 | assert(!err) 181 | assert(out.indexOf('up') === -1) 182 | assert.strictEqual(db.numbers.length, 2) 183 | done() 184 | }) 185 | }) 186 | }) 187 | 188 | it('should run down when passed --clean', function (done) { 189 | up([], function (err, out, code) { 190 | assert(!err) 191 | assert.strictEqual(code, 0) 192 | up(['--clean'], function (err, out) { 193 | assert(!err) 194 | db.load() 195 | assert(out.indexOf('down') !== -1) 196 | assert(out.indexOf('up') !== -1) 197 | assert.strictEqual(db.numbers.length, 2) 198 | done() 199 | }) 200 | }) 201 | }) 202 | }) // end up 203 | 204 | describe('down', function () { 205 | beforeEach(function (done) { 206 | up([], done) 207 | }) 208 | it('should run down on multiple migrations', function (done) { 209 | down([], function (err, out, code) { 210 | assert(!err) 211 | assert.strictEqual(code, 0) 212 | db.load() 213 | assert(out.indexOf('down') !== -1) 214 | assert.strictEqual(db.numbers.length, 0) 215 | assert(db.numbers.indexOf(1) === -1) 216 | assert(db.numbers.indexOf(2) === -1) 217 | done() 218 | }) 219 | }) 220 | 221 | it('should run down to a specified migration', function (done) { 222 | down(['2-two.js'], function (err, out, code) { 223 | assert(!err) 224 | assert.strictEqual(code, 0) 225 | db.load() 226 | assert(out.indexOf('down') !== -1) 227 | assert.strictEqual(db.numbers.length, 1) 228 | assert(db.numbers.indexOf(1) !== -1) 229 | assert(db.numbers.indexOf(2) === -1) 230 | done() 231 | }) 232 | }) 233 | 234 | it('should run down multiple times', function (done) { 235 | down([], function (err, out, code) { 236 | assert(!err) 237 | assert.strictEqual(code, 0) 238 | assert(out.indexOf('down') !== -1) 239 | db.load() 240 | down([], function (err, out) { 241 | assert(!err) 242 | assert(out.indexOf('down') === -1) 243 | assert.strictEqual(db.numbers.length, 0) 244 | done() 245 | }) 246 | }) 247 | }) 248 | }) // end down 249 | 250 | describe('list', function () { 251 | it('should list available migrations', function (done) { 252 | list([], function (err, out, code) { 253 | assert(!err) 254 | assert.strictEqual(code, 0, out) 255 | assert(out.indexOf('1-one.js') !== -1) 256 | assert(out.indexOf('2-two.js') !== -1) 257 | done() 258 | }) 259 | }) 260 | }) // end init 261 | }) 262 | -------------------------------------------------------------------------------- /test/file-store.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | const path = require('path') 4 | const assert = require('assert') 5 | const FileStore = require('../lib/file-store') 6 | 7 | const BASE = path.join(__dirname, 'fixtures', 'file-store') 8 | const MODERN_STORE_FILE = path.join(BASE, 'good-store') 9 | const OLD_STORE_FILE = path.join(BASE, 'old-store') 10 | const BAD_STORE_FILE = path.join(BASE, 'bad-store') 11 | const INVALID_STORE_FILE = path.join(BASE, 'invalid-store') 12 | 13 | describe('FileStore tests', function () { 14 | it('should load store file', function (done) { 15 | const store = new FileStore(MODERN_STORE_FILE) 16 | store.load(function (err, store) { 17 | if (err) { 18 | return done(err) 19 | } 20 | 21 | assert.strictEqual(store.lastRun, '1480449051248-farnsworth.js') 22 | assert.strictEqual(store.migrations.length, 2) 23 | 24 | return done() 25 | }) 26 | }) 27 | 28 | it('should convert pre-v1 store file format', function (done) { 29 | const store = new FileStore(OLD_STORE_FILE) 30 | store.load(function (err, store) { 31 | if (err) { 32 | return done(err) 33 | } 34 | 35 | assert.strictEqual(store.lastRun, '1480449051248-farnsworth.js') 36 | assert.strictEqual(store.migrations.length, 2) 37 | 38 | store.migrations.forEach(function (migration) { 39 | assert.strictEqual(typeof migration.title, 'string') 40 | assert.strictEqual(typeof migration.timestamp, 'number') 41 | }) 42 | 43 | return done() 44 | }) 45 | }) 46 | 47 | it('should error with invalid store file format', function (done) { 48 | const store = new FileStore(BAD_STORE_FILE) 49 | store.load(function (err, store) { 50 | if (!err) { 51 | return done(new Error('Error expected')) 52 | } 53 | 54 | assert.strictEqual(err.message, 'Invalid store file') 55 | 56 | return done() 57 | }) 58 | }) 59 | 60 | it('should error with invalid pos', function (done) { 61 | const store = new FileStore(INVALID_STORE_FILE) 62 | store.load(function (err, store) { 63 | if (!err) { 64 | return done(new Error('Error expected')) 65 | } 66 | 67 | assert.strictEqual(err.message, 'Store file contains invalid pos property') 68 | 69 | return done() 70 | }) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /test/fixtures/basic/1-add-guy-ferrets.js: -------------------------------------------------------------------------------- 1 | const db = require('../../util/db') 2 | 3 | exports.description = 'Adds two pets' 4 | 5 | exports.up = function (next) { 6 | db.pets.push({ name: 'tobi' }) 7 | db.pets.push({ name: 'loki' }) 8 | next() 9 | } 10 | 11 | exports.down = function (next) { 12 | db.pets.pop() 13 | db.pets.pop() 14 | next() 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/basic/2-add-girl-ferrets.js: -------------------------------------------------------------------------------- 1 | const db = require('../../util/db') 2 | 3 | exports.up = function (next) { 4 | db.pets.push({ name: 'jane' }) 5 | next() 6 | } 7 | 8 | exports.down = function (next) { 9 | db.pets.pop() 10 | next() 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/basic/3-add-emails.js: -------------------------------------------------------------------------------- 1 | const db = require('../../util/db') 2 | 3 | exports.up = function (next) { 4 | db.pets.forEach(function (pet) { 5 | pet.email = pet.name + '@learnboost.com' 6 | }) 7 | next() 8 | } 9 | 10 | exports.down = function (next) { 11 | db.pets.forEach(function (pet) { 12 | delete pet.email 13 | }) 14 | next() 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/env/env: -------------------------------------------------------------------------------- 1 | DB=pets 2 | -------------------------------------------------------------------------------- /test/fixtures/env/migrations/1-add-guy-ferrets.js: -------------------------------------------------------------------------------- 1 | const db = require('../../../util/db') 2 | 3 | exports.up = function (next) { 4 | db[process.env.DB].push({ name: 'tobi' }) 5 | db[process.env.DB].push({ name: 'loki' }) 6 | db.persist() 7 | next() 8 | } 9 | 10 | exports.down = function (next) { 11 | db[process.env.DB].pop() 12 | db[process.env.DB].pop() 13 | db.persist() 14 | next() 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/env/migrations/2-add-girl-ferrets.js: -------------------------------------------------------------------------------- 1 | const db = require('../../../util/db') 2 | 3 | exports.up = function (next) { 4 | db[process.env.DB].push({ name: 'jane' }) 5 | db.persist() 6 | next() 7 | } 8 | 9 | exports.down = function (next) { 10 | db[process.env.DB].pop() 11 | db.persist() 12 | next() 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/file-store/bad-store: -------------------------------------------------------------------------------- 1 | { 2 | "notareal":"store" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/file-store/good-store: -------------------------------------------------------------------------------- 1 | { 2 | "lastRun": "1480449051248-farnsworth.js", 3 | "migrations": [ 4 | { 5 | "title": "1480357772278-fozrick.js", 6 | "timestamp": 1513724986512 7 | }, 8 | { 9 | "title": "1480449051248-farnsworth.js", 10 | "timestamp": 1513724986524 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/file-store/invalid-store: -------------------------------------------------------------------------------- 1 | { 2 | "migrations": [], 3 | "pos": 1 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/file-store/old-store: -------------------------------------------------------------------------------- 1 | { 2 | "migrations": [ 3 | { 4 | "title": "1480357772278-fozrick.js" 5 | }, 6 | { 7 | "title": "1480449051248-farnsworth.js" 8 | } 9 | ], 10 | "path": "test/fixtures/file-store/old-store", 11 | "pos": 2 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/issue-33/1-migration.js: -------------------------------------------------------------------------------- 1 | const db = require('../../util/db') 2 | 3 | exports.up = function (next) { 4 | db.issue33.push('1-up') 5 | next() 6 | } 7 | 8 | exports.down = function (next) { 9 | db.issue33.push('1-down') 10 | next() 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/issue-33/2-migration.js: -------------------------------------------------------------------------------- 1 | const db = require('../../util/db') 2 | 3 | exports.up = function (next) { 4 | db.issue33.push('2-up') 5 | next() 6 | } 7 | 8 | exports.down = function (next) { 9 | db.issue33.push('2-down') 10 | next() 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/issue-33/3-migration.js: -------------------------------------------------------------------------------- 1 | const db = require('../../util/db') 2 | 3 | exports.up = function (next) { 4 | db.issue33.push('3-up') 5 | next() 6 | } 7 | 8 | exports.down = function (next) { 9 | db.issue33.push('3-down') 10 | next() 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/issue-92/migrations/1-one.js: -------------------------------------------------------------------------------- 1 | exports.up = function (next) { 2 | next() 3 | } 4 | 5 | exports.down = function (next) { 6 | next() 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/issue-92/migrations/2-two.js: -------------------------------------------------------------------------------- 1 | exports.up = function (next) { 2 | next() 3 | } 4 | 5 | exports.down = function (next) { 6 | next() 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/numbers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tj/node-migrate/6a8e5994018e6a9ab7da4079dbe20fdd3de68d68/test/fixtures/numbers/.gitkeep -------------------------------------------------------------------------------- /test/fixtures/numbers/migrations/1-one.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const db = require('../../../util/db') 3 | 4 | exports.up = function (next) { 5 | db.load() 6 | db.numbers.push(1) 7 | db.persist() 8 | next() 9 | } 10 | 11 | exports.down = function (next) { 12 | db.load() 13 | db.numbers.pop() 14 | db.persist() 15 | next() 16 | } 17 | 18 | exports.test = function (next) { 19 | exports.up(function (err) { 20 | if (err) return next(err) 21 | exports.verify(function (err) { 22 | if (err) return next(err) 23 | exports.down(next) 24 | }) 25 | }) 26 | } 27 | 28 | exports.verify = function (next) { 29 | assert.strictEqual(db.numbers.indexOf(1), 0) 30 | next() 31 | } 32 | -------------------------------------------------------------------------------- /test/fixtures/numbers/migrations/2-two.js: -------------------------------------------------------------------------------- 1 | const db = require('../../../util/db') 2 | 3 | exports.up = function (next) { 4 | db.load() 5 | db.numbers.push(2) 6 | db.persist() 7 | next() 8 | } 9 | 10 | exports.down = function (next) { 11 | db.load() 12 | db.numbers.pop() 13 | db.persist() 14 | next() 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/promises/1-callback-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports.up = function (next) { 4 | setTimeout(function () { 5 | next() 6 | }, 1) 7 | } 8 | 9 | module.exports.down = function (next) { 10 | setTimeout(function () { 11 | next() 12 | }, 1) 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/promises/2-promise-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports.up = function () { 4 | return new Promise(function (resolve, reject) { 5 | setTimeout(function () { 6 | resolve() 7 | }, 1) 8 | }) 9 | } 10 | 11 | module.exports.down = function () { 12 | return new Promise(function (resolve, reject) { 13 | setTimeout(function () { 14 | resolve() 15 | }, 1) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/promises/3-callback-promise-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports.up = function (next) { 4 | return new Promise(function (resolve, reject) { 5 | setTimeout(function () { 6 | next() 7 | resolve() 8 | }, 1) 9 | }) 10 | } 11 | 12 | module.exports.down = function (next) { 13 | return new Promise(function (resolve, reject) { 14 | setTimeout(function () { 15 | next() 16 | resolve() 17 | }, 1) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/promises/4-neither-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports.up = function () { 4 | arguments[0]() 5 | } 6 | 7 | module.exports.down = function () { 8 | arguments[0]() 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/promises/5-resolve-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports.up = function () { 4 | return new Promise(function (resolve, reject) { 5 | setTimeout(function () { 6 | resolve('foo') 7 | }, 1) 8 | }) 9 | } 10 | 11 | module.exports.down = function () { 12 | return new Promise(function (resolve, reject) { 13 | setTimeout(function () { 14 | resolve('foo') 15 | }, 1) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/promises/99-failure-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports.up = function () { 4 | return new Promise(function (resolve, reject) { 5 | setTimeout(function () { 6 | reject(new Error('foo')) 7 | }, 1) 8 | }) 9 | } 10 | 11 | module.exports.down = function () { 12 | return new Promise(function (resolve, reject) { 13 | setTimeout(function () { 14 | reject(new Error('foo')) 15 | }, 1) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /test/integration.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | const path = require('path') 4 | const assert = require('assert') 5 | const rimraf = require('rimraf') 6 | const mkdirp = require('mkdirp') 7 | const run = require('./util/run') 8 | const db = require('./util/db') 9 | 10 | // Paths 11 | const TMP_DIR = path.join(__dirname, 'fixtures', 'tmp') 12 | const ENV_DIR = path.join(__dirname, 'fixtures', 'env') 13 | 14 | function reset () { 15 | rimraf.sync(path.join(ENV_DIR, '.migrate')) 16 | rimraf.sync(TMP_DIR) 17 | mkdirp.sync(TMP_DIR) 18 | db.nuke() 19 | } 20 | 21 | describe('integration tests', function () { 22 | beforeEach(reset) 23 | afterEach(reset) 24 | 25 | it('should warn when the migrations are run out of order', function (done) { 26 | run.init(TMP_DIR, [], function (err, out, code) { 27 | assert(!err) 28 | assert.strictEqual(code, 0) 29 | 30 | run.create(TMP_DIR, ['1-one', '-d', 'W'], function (err, out, code) { 31 | assert(!err) 32 | assert.strictEqual(code, 0) 33 | 34 | run.create(TMP_DIR, ['3-three', '-d', 'W'], function (err, out, code) { 35 | assert(!err) 36 | assert.strictEqual(code, 0) 37 | 38 | run.up(TMP_DIR, [], function (err, out, code) { 39 | assert(!err) 40 | assert.strictEqual(code, 0) 41 | 42 | run.create(TMP_DIR, ['2-two', '-d', 'W'], function (err, out, code) { 43 | assert(!err) 44 | assert.strictEqual(code, 0) 45 | 46 | run.up(TMP_DIR, [], function (err, out, code) { 47 | assert(!err) 48 | assert.strictEqual(code, 0) 49 | 50 | // A warning should log, and the process not exit with 0 51 | // because migration 2 should come before migration 3, 52 | // but migration 3 was already run from the previous 53 | // state 54 | assert(out.indexOf('warn') !== -1) 55 | done() 56 | }) 57 | }) 58 | }) 59 | }) 60 | }) 61 | }) 62 | }) 63 | 64 | it('should error when migrations are present in the state file, but not loadable', function (done) { 65 | run.init(TMP_DIR, [], function (err, out, code) { 66 | assert(!err) 67 | assert.strictEqual(code, 0) 68 | 69 | run.create(TMP_DIR, ['1-one', '-d', 'W'], function (err, out, code) { 70 | assert(!err) 71 | assert.strictEqual(code, 0) 72 | 73 | run.create(TMP_DIR, ['3-three', '-d', 'W'], function (err, out, code) { 74 | assert(!err) 75 | assert.strictEqual(code, 0) 76 | 77 | // Keep migration filename to remove 78 | const filename = out.split(' : ')[1].trim() 79 | 80 | run.up(TMP_DIR, [], function (err, out, code) { 81 | assert(!err) 82 | assert.strictEqual(code, 0) 83 | 84 | // Remove the three migration 85 | rimraf.sync(filename) 86 | 87 | run.create(TMP_DIR, ['2-two', '-d', 'W'], function (err, out, code) { 88 | assert(!err) 89 | assert.strictEqual(code, 0) 90 | 91 | run.up(TMP_DIR, [], function (err, out, code) { 92 | assert(!err) 93 | assert.strictEqual(code, 1) 94 | assert(out.indexOf('error') !== -1) 95 | done() 96 | }) 97 | }) 98 | }) 99 | }) 100 | }) 101 | }) 102 | }) 103 | 104 | it('should not error when migrations are present in the state file, not loadable but not run', function (done) { 105 | run.init(TMP_DIR, [], function (err, out, code) { 106 | assert(!err) 107 | assert.strictEqual(code, 0) 108 | 109 | run.create(TMP_DIR, ['1-one', '-d', 'W'], function (err, out, code) { 110 | assert(!err) 111 | assert.strictEqual(code, 0) 112 | 113 | run.create(TMP_DIR, ['2-two', '-d', 'W'], function (err, out, code) { 114 | assert(!err) 115 | assert.strictEqual(code, 0) 116 | 117 | // Keep migration filename to remove 118 | const filename = out.split(' : ')[1].trim() 119 | 120 | run.up(TMP_DIR, [], function (err, out, code) { 121 | assert(!err) 122 | assert.strictEqual(code, 0) 123 | 124 | run.down(TMP_DIR, [], function (err, out, code) { 125 | assert(!err) 126 | assert.strictEqual(code, 0) 127 | 128 | // Remove the three migration 129 | rimraf.sync(filename) 130 | 131 | run.up(TMP_DIR, [], function (err, out, code) { 132 | assert(!err) 133 | assert.strictEqual(code, 0, out) 134 | done() 135 | }) 136 | }) 137 | }) 138 | }) 139 | }) 140 | }) 141 | }) 142 | 143 | it('should load the enviroment file when passed --env', function (done) { 144 | run.up(ENV_DIR, ['--env', 'env'], function (err, out, code) { 145 | assert(!err) 146 | assert.strictEqual(code, 0) 147 | assert(out.indexOf('error') === -1) 148 | run.down(ENV_DIR, ['--env', 'env'], function (err, out, code) { 149 | assert(!err) 150 | assert.strictEqual(code, 0) 151 | assert(out.indexOf('error') === -1) 152 | done() 153 | }) 154 | }) 155 | }) 156 | }) 157 | -------------------------------------------------------------------------------- /test/issue-148.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | const path = require('path') 4 | const assert = require('assert') 5 | const rimraf = require('rimraf') 6 | const { mkdirp } = require('mkdirp') 7 | const fs = require('fs') 8 | const { create, up, down, init } = require('./util/run') 9 | 10 | const TMP_DIR = path.join(__dirname, 'fixtures', 'tmp') 11 | 12 | describe('issue #148 - `migrate create --migrations-dir=... foo` should allow an absolute path', () => { 13 | function reset () { 14 | rimraf.sync(TMP_DIR) 15 | return mkdirp(TMP_DIR) 16 | } 17 | 18 | beforeEach(reset) 19 | afterEach(reset) 20 | 21 | it('should allow an absolute path', (done) => { 22 | init(TMP_DIR, ['--migrations-dir', path.join(TMP_DIR, 'other')], (err, out, code) => { 23 | assert.ifError(err) 24 | assert.strictEqual(code, 0, out) 25 | assert.doesNotThrow(() => { 26 | fs.accessSync(path.join(TMP_DIR, 'other')) 27 | }) 28 | 29 | create(TMP_DIR, ['--migrations-dir', path.join(TMP_DIR, 'other'), 'foo'], (err, out, code) => { 30 | assert.ifError(err) 31 | assert.strictEqual(code, 0, out) 32 | 33 | up(TMP_DIR, ['--migrations-dir', path.join(TMP_DIR, 'other')], (err, out, code) => { 34 | assert.ifError(err) 35 | assert.strictEqual(code, 0, out) 36 | 37 | down(TMP_DIR, ['--migrations-dir', path.join(TMP_DIR, 'other')], (err, out, code) => { 38 | assert.ifError(err) 39 | assert.strictEqual(code, 0, out) 40 | 41 | done() 42 | }) 43 | }) 44 | }) 45 | }) 46 | }) 47 | 48 | it('should still allow a relative path', (done) => { 49 | init(TMP_DIR, ['--migrations-dir', 'other'], (err, out, code) => { 50 | assert.ifError(err) 51 | assert.strictEqual(code, 0, out) 52 | assert.doesNotThrow(() => { 53 | fs.accessSync(path.join(TMP_DIR, 'other')) 54 | }) 55 | 56 | create(TMP_DIR, ['--migrations-dir', 'other', 'foo'], (err, out, code) => { 57 | assert.ifError(err) 58 | assert.strictEqual(code, 0) 59 | 60 | up(TMP_DIR, ['--migrations-dir', 'other'], (err, out, code) => { 61 | assert.ifError(err) 62 | assert.strictEqual(code, 0, out) 63 | 64 | down(TMP_DIR, ['--migrations-dir', 'other'], (err, out, code) => { 65 | assert.ifError(err) 66 | assert.strictEqual(code, 0, out) 67 | 68 | done() 69 | }) 70 | }) 71 | }) 72 | }) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /test/issue-33.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | const fs = require('fs') 4 | const path = require('path') 5 | const assert = require('assert') 6 | const migrate = require('../') 7 | const db = require('./util/db') 8 | 9 | const BASE = path.join(__dirname, 'fixtures', 'issue-33') 10 | const STATE = path.join(BASE, '.migrate') 11 | 12 | const A1 = ['1-up', '2-up', '3-up'] 13 | const A2 = A1.concat(['3-down', '2-down', '1-down']) 14 | 15 | describe('issue #33', function () { 16 | let set 17 | 18 | beforeEach(function (done) { 19 | migrate.load({ 20 | stateStore: STATE, 21 | migrationsDirectory: BASE 22 | }, function (err, s) { 23 | set = s 24 | done(err) 25 | }) 26 | }) 27 | 28 | it('should run migrations in the correct order', function (done) { 29 | set.up(function (err) { 30 | assert.ifError(err) 31 | assert.deepStrictEqual(db.issue33, A1) 32 | 33 | set.up(function (err) { 34 | assert.ifError(err) 35 | assert.deepStrictEqual(db.issue33, A1) 36 | 37 | set.down(function (err) { 38 | assert.ifError(err) 39 | assert.deepStrictEqual(db.issue33, A2) 40 | 41 | set.down(function (err) { 42 | assert.ifError(err) 43 | assert.deepStrictEqual(db.issue33, A2) 44 | 45 | done() 46 | }) 47 | }) 48 | }) 49 | }) 50 | }) 51 | 52 | afterEach(function (done) { 53 | db.nuke() 54 | fs.unlink(STATE, done) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /test/issue-92.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | const path = require('path') 4 | const assert = require('assert') 5 | const rimraf = require('rimraf') 6 | const db = require('./util/db') 7 | const run = require('./util/run') 8 | 9 | // Paths 10 | const FIX_DIR = path.join(__dirname, 'fixtures', 'issue-92') 11 | const UP = path.join(__dirname, '..', 'bin', 'migrate-up') 12 | const DOWN = path.join(__dirname, '..', 'bin', 'migrate-down') 13 | 14 | // Run helper 15 | const up = run.bind(null, UP, FIX_DIR) 16 | const down = run.bind(null, DOWN, FIX_DIR) 17 | 18 | function reset () { 19 | rimraf.sync(path.join(FIX_DIR, '.migrate')) 20 | db.nuke() 21 | } 22 | 23 | describe('issue #92', function () { 24 | beforeEach(reset) 25 | afterEach(reset) 26 | 27 | it('shouldn\'t throw error after migrate down to initial state', function (done) { 28 | up([], function (err, out, code) { 29 | assert.ifError(err) 30 | assert.strictEqual(code, 0) 31 | 32 | down([], function (err, out, code) { 33 | assert.ifError(err) 34 | assert.strictEqual(code, 0) 35 | 36 | up([], function (err, out, code) { 37 | assert.ifError(err) 38 | assert.strictEqual(code, 0) 39 | 40 | done() 41 | }) 42 | }) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /test/promises.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | const { rimraf } = require('rimraf') 4 | const path = require('path') 5 | const assert = require('assert') 6 | const migrate = require('../') 7 | 8 | const BASE = path.join(__dirname, 'fixtures', 'promises') 9 | const STATE = path.join(__dirname, 'fixtures', '.migrate') 10 | 11 | describe('Promise migrations', function () { 12 | let set 13 | 14 | beforeEach(function (done) { 15 | migrate.load({ 16 | stateStore: STATE, 17 | migrationsDirectory: BASE 18 | }, function (err, s) { 19 | set = s 20 | done(err) 21 | }) 22 | }) 23 | 24 | afterEach(function () { 25 | return rimraf(STATE) 26 | }) 27 | 28 | it('should handle callback migration', function (done) { 29 | set.up('1-callback-test.js', function (err) { 30 | assert.ifError(err) 31 | done() 32 | }) 33 | }) 34 | 35 | it('should handle promise migration', function (done) { 36 | set.up('2-promise-test.js', function (err) { 37 | assert.ifError(err) 38 | done() 39 | }) 40 | }) 41 | 42 | it('should warn when using promise but still calling callback', function (done) { 43 | let warned = false 44 | set.on('warning', function (msg) { 45 | assert(msg) 46 | warned = true 47 | }) 48 | set.up('3-callback-promise-test.js', function () { 49 | assert(warned) 50 | done() 51 | }) 52 | }) 53 | 54 | it('should warn with no promise or callback', function (done) { 55 | set.up('3-callback-promise-test.js', function () { 56 | let warned = false 57 | set.on('warning', function (msg) { 58 | assert(msg) 59 | warned = true 60 | }) 61 | set.up('4-neither-test.js', function () { 62 | assert(warned) 63 | done() 64 | }) 65 | }) 66 | }) 67 | 68 | it("shouldn't error with resolved promises", function (done) { 69 | set.up('5-resolve-test.js', function (err) { 70 | assert(!err) 71 | done() 72 | }) 73 | }) 74 | 75 | it('should error with rejected promises', function (done) { 76 | set.up('99-failure-test.js', function (err) { 77 | assert(err) 78 | assert.strictEqual(err.message, 'foo') 79 | done() 80 | }) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /test/util/db.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const rimraf = require('rimraf') 4 | 5 | const DB_PATH = path.join(__dirname, 'test.db') 6 | 7 | function init () { 8 | exports.pets = [] 9 | exports.issue33 = [] 10 | exports.numbers = [] 11 | } 12 | 13 | function nuke () { 14 | init() 15 | rimraf.sync(DB_PATH) 16 | } 17 | 18 | function load () { 19 | let c 20 | try { 21 | c = fs.readFileSync(DB_PATH, 'utf8') 22 | } catch (e) { 23 | return 24 | } 25 | const j = JSON.parse(c) 26 | Object.keys(j).forEach(function (k) { 27 | exports[k] = j[k] 28 | }) 29 | } 30 | 31 | function persist () { 32 | fs.writeFileSync(DB_PATH, JSON.stringify(exports)) 33 | } 34 | 35 | exports.nuke = nuke 36 | exports.persist = persist 37 | exports.load = load 38 | 39 | init() 40 | -------------------------------------------------------------------------------- /test/util/run.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const spawn = require('child_process').spawn 4 | 5 | const run = module.exports = function run (cmd, dir, args, done) { 6 | const p = spawn(cmd, ['-c', dir, ...args]) 7 | let out = '' 8 | p.stdout.on('data', function (d) { 9 | out += d.toString('utf8') 10 | }) 11 | p.stderr.on('data', function (d) { 12 | out += d.toString('utf8') 13 | }) 14 | p.on('error', done) 15 | p.on('close', function (code) { 16 | done(null, out, code) 17 | }) 18 | } 19 | 20 | // Run specific commands 21 | module.exports.up = run.bind(null, path.join(__dirname, '..', '..', 'bin', 'migrate-up')) 22 | module.exports.down = run.bind(null, path.join(__dirname, '..', '..', 'bin', 'migrate-down')) 23 | module.exports.create = run.bind(null, path.join(__dirname, '..', '..', 'bin', 'migrate-create')) 24 | module.exports.init = run.bind(null, path.join(__dirname, '..', '..', 'bin', 'migrate-init')) 25 | module.exports.list = run.bind(null, path.join(__dirname, '..', '..', 'bin', 'migrate-list')) 26 | -------------------------------------------------------------------------------- /test/util/tmpl.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports.description = 'test' 3 | 4 | module.exports.up = function (next) { 5 | next() 6 | } 7 | 8 | module.exports.down = function (next) { 9 | next() 10 | } 11 | -------------------------------------------------------------------------------- /test/util/tmpl.mjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | export const description = 'test' 3 | 4 | export const up = function (next) { 5 | next() 6 | } 7 | 8 | export const down = function (next) { 9 | next() 10 | } 11 | --------------------------------------------------------------------------------