├── .gitignore ├── LICENSE ├── README.md ├── bin ├── db-create ├── db-migrate ├── db-rollback ├── db-status └── env.js ├── doc ├── CONFIGURATION.md ├── db-create.png ├── db-migrate.js └── db-status.png ├── lib ├── application │ └── service │ │ ├── migrator-service.js │ │ └── rollback-service.js ├── colors.js ├── create.js ├── domain │ ├── persister │ │ ├── mysql.js │ │ └── postgres.js │ ├── repository │ │ ├── script-repository.js │ │ └── version-repository.js │ └── service │ │ ├── script-service.js │ │ └── version-service.js ├── infrastructure │ └── messages.js ├── migrate.js ├── rollback.js └── status.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Aphel Cloud Solutions 4 | Copyright (c) 2015 Pavel Pokorny 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DB Migrator 2 | =========== 3 | 4 | The complete and easy to use database migration tool for Node.js projects. Supports PostgreSQL and MySQL. 5 | 6 | ## Features 7 | 8 | * Auto migration from scratch to up to date 9 | * Step by step forward migration 10 | * Step by step backward migration 11 | * Migrate to a specific version forward or backward 12 | * Subfolder deep search for migration scripts 13 | * All or nothing (transactional migration) 14 | 15 | ## Installation 16 | 17 | ``` 18 | npm install db-migrator --save 19 | # and one of 20 | npm install pg --save 21 | npm install promise-mysql --save 22 | ``` 23 | 24 | ## Quick Start 25 | 26 | Add following to your `package.json`: 27 | 28 | ``` 29 | "scripts": { 30 | "db-migrate": "db-migrate", 31 | "db-rollback": "db-rollback", 32 | "db-create": "db-create", 33 | "db-status": "db-status" 34 | } 35 | ``` 36 | 37 | Create `.npmrc` with a database connection string: 38 | 39 | ``` 40 | # For PostgreSQL it can look like this 41 | db_migrator_db_url=postgresql://mydatabase@localhost?ssl=false 42 | # For MySQL 43 | db_migrator_db_url=mysql://user:pass@host/db 44 | ``` 45 | 46 | Create folder for migration files, by default `migrations`. 47 | 48 | ``` 49 | mkdir migrations 50 | ``` 51 | 52 | At this point you should be able to run all following commands. 53 | 54 | ### db-create 55 | 56 | Run `npm run db-create description of the migration` to generate files for UP and DOWN migration and put your SQL to these files. Migration scripts name has to match pattern `timestamp-[UP|DOWN](-optional-description).sql`. 57 | 58 | ![Example migration scripts](https://raw.githubusercontent.com/Pajk/db-migrator/master/doc/db-create.png) 59 | 60 | ### db-migrate 61 | 62 | Run `npm run db-migrate` to migrate your database to the latest version. 63 | 64 | ### db-rollback 65 | 66 | Run `npm run db-rollback` to rollback one last migration. 67 | 68 | ### db-status 69 | 70 | Run `npm run db-status` to see the state of your database. 71 | 72 | ![Example output of db-status](https://raw.githubusercontent.com/Pajk/db-migrator/master/doc/db-status.png) 73 | 74 | ## Configuration 75 | 76 | There's not too much to set up but it's very common to have several deployment and testing environments for each project. There are several ways how to configure DB Migrator so everyone should find an easy way how to plug it in their stack. 77 | 78 | Check out [this document](doc/CONFIGURATION.md) to find out more. 79 | 80 | ## Common Pitfalls 81 | 82 | * All migration scripts are executed in the same transaction scope and totally roll back in case of fail so you shouldn't put any transaction statements in your scripts. 83 | * You should use a database user with sufficient permissions according to your script content. 84 | * Postgres ENUM type cannot be altered in a transaction so you have to change the columns type to varchar, recreate the enum and set the columns type back to use the enum. 85 | 86 | ## Credits 87 | 88 | This is a fork of [pg-migrator](https://github.com/aphel-bilisim-hizmetleri/pg-migrator) library with following differences features: 89 | 90 | * Timestamp-based version id 91 | * All version ids together with time of execution are stored in the database 92 | * It's possible to get a list of versions migrated in the database 93 | * Async/Await is used in the codebase so at least Node.js v7.6.0 is required 94 | * Migration file name can contain a short description of the migration 95 | * Favors npm scripts (and .npmrc config file) but supports also custom execution scripts 96 | * MySQL support 97 | -------------------------------------------------------------------------------- /bin/db-create: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const create = require('../lib/create') 4 | 5 | const titleParts = Array.prototype.slice.call(process.argv, 2) 6 | 7 | let scriptsPath = process.env.npm_package_config_db_migrator_directory || 8 | process.env.npm_config_db_migrator_directory || 9 | process.env.DB_MIGRATOR_DIR || 10 | './migrations' 11 | 12 | create({ 13 | path: scriptsPath, 14 | title: titleParts.join(' ') 15 | }) 16 | -------------------------------------------------------------------------------- /bin/db-migrate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const migrate = require('../lib/migrate') 4 | const config = require('./env').getConfig() 5 | 6 | migrate({ 7 | connectionString: config.connectionString, 8 | path: config.scriptsPath, 9 | schema: config.schema || 'public', 10 | tableName: config.tableName, 11 | targetVersion: config.targetVersion || 0, 12 | printStatus: true, 13 | dbDriver: config.dbDriver 14 | }) 15 | .then(function () { 16 | process.exit(0) 17 | }) 18 | .catch(function () { 19 | process.exit(1) 20 | }) 21 | -------------------------------------------------------------------------------- /bin/db-rollback: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | const rollback = require('../lib/rollback') 6 | const config = require('./env').getConfig() 7 | 8 | rollback({ 9 | connectionString: config.connectionString, 10 | path: config.scriptsPath, 11 | schema: config.schema || 'public', 12 | tableName: config.tableName, 13 | targetVersion: config.targetVersion || '-1', 14 | printStatus: true, 15 | dbDriver: config.dbDriver 16 | }) 17 | .then(function () { 18 | process.exit(0) 19 | }) 20 | .catch(function () { 21 | process.exit(1) 22 | }) 23 | -------------------------------------------------------------------------------- /bin/db-status: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | const status = require('../lib/status') 6 | const config = require('./env').getConfig() 7 | 8 | status({ 9 | connectionString: config.connectionString, 10 | path: config.scriptsPath, 11 | schema: config.schema || 'public', 12 | tableName: config.tableName, 13 | dbDriver: config.dbDriver 14 | }) 15 | .then(function () { 16 | process.exit(0) 17 | }) 18 | .catch(function () { 19 | process.exit(1) 20 | }) 21 | -------------------------------------------------------------------------------- /bin/env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | 5 | module.exports = { 6 | getConfig: function () { 7 | const config = {} 8 | 9 | const args = Array.prototype.slice.call(process.argv, 2) 10 | 11 | config.connectionString = args[0] || 12 | process.env.npm_package_config_db_migrator_db_url || 13 | process.env.npm_config_db_migrator_db_url || 14 | getDatabaseUrl() || 15 | process.env.DB_MIGRATOR_URL || 16 | 'postgresql://localhost' 17 | 18 | config.scriptsPath = args[1] || 19 | process.env.npm_package_config_db_migrator_directory || 20 | process.env.npm_config_db_migrator_directory || 21 | process.env.DB_MIGRATOR_DIR || 22 | './migrations' 23 | 24 | config.tableName = args[2] || 25 | process.env.npm_package_config_db_migrator_table_name || 26 | process.env.npm_config_db_migrator_table_name || 27 | process.env.DB_MIGRATOR_TABLE || 28 | 'migrations' 29 | 30 | config.targetVersion = args[3] || 31 | process.env.npm_config_db_migrator_target || 32 | process.env.DB_MIGRATOR_TARGET 33 | 34 | const dbms = config.connectionString.split(':')[0] 35 | 36 | switch (dbms.toLowerCase()) { 37 | case 'postgresql': 38 | case 'postgres': 39 | config.dbDriver = 'postgres' 40 | break 41 | case 'mysql': 42 | config.dbDriver = 'mysql' 43 | break 44 | default: 45 | throw new Error( 46 | 'The connection string format is not valid: ' + 47 | config.connectionString 48 | ) 49 | } 50 | 51 | return config 52 | } 53 | } 54 | 55 | function getDatabaseUrl() { 56 | if (process.env.DATABASE_URL) { 57 | return process.env.DATABASE_URL 58 | } 59 | 60 | if (process.env.DATABASE_URL_FILE) { 61 | return fs.readFileSync(process.env.DATABASE_URL_FILE, 'utf8').trim() 62 | } 63 | 64 | return null 65 | } -------------------------------------------------------------------------------- /doc/CONFIGURATION.md: -------------------------------------------------------------------------------- 1 | ## DB Migrator Configuration 2 | 3 | There are more ways how to tell DB Migrator where to look for migrations and what database to use. You can choose which one suits you most. 4 | 5 | ### Custom execution scripts 6 | 7 | My personal favorite because it gives you absolute freedom. There's not much to explain here, just take a look at [this example](db-migrate.js). 8 | 9 | ### Environment Variables 10 | 11 | Configuration with env variables is most useful when executing migrations on your CI (e.g. Travis) or on your servers. 12 | 13 | * `DATABASE_URL` or `DB_MIGRATOR_URL` - Database connection string. 14 | * `DATABASE_URL_FILE` - Path to file with the connection string. 15 | * `DB_MIGRATOR_DIR` - Name of the migration scripts folder (defaults to `./migrations`). 16 | * `DB_MIGRATOR_TABLE` - Name of the database table where to store the database state (defaults to `migrations`). 17 | * `DB_MIGRATOR_TARGET` - Target migration id (timestamp). 18 | * `DB_CONNECT_MAX_ATTEMPTS` - Default `25`. Maximum number of attemps if the database is not running yet (ECONNREFUSED). 19 | * `DB_CONNECT_NEXT_ATTEMPT_DELAY` - Default `1000`. How long should pg-migrator wait before next attempt to connect to the database. 20 | * `DB_MIGRATOR_ROLE` - Run migrations as a role (psql only) 21 | 22 | ### NPM Config Variables 23 | 24 | You can utilize [npm config variables](https://docs.npmjs.com/misc/config). It has higher priority than env variables. 25 | 26 | Available options are: 27 | 28 | * `db_url` - Database connection string. 29 | * `directory` - Name of the migration scripts folder (defaults to `./migrations`). 30 | * `table_name` - Name of the database table where to store database state (defaults to `migrations`). 31 | * `target` - Target migration id (timestamp). 32 | 33 | All npm config variables can be set in `package.json`, in `.npmrc` or as command line arguments. 34 | 35 | #### package.json 36 | 37 | ``` 38 | "config": { 39 | "db-migrator": { 40 | "directory": "./db-migrations" 41 | } 42 | } 43 | ``` 44 | 45 | #### .npmrc 46 | 47 | Use `db_migrator_` or `db-migrator-` prefix: 48 | ``` 49 | db_migrator_db_url=postgresql://pavel@localhost 50 | ``` 51 | 52 | #### Command Line Arguments 53 | 54 | Use `db_migrator_` or `db-migrator-` prefix: 55 | ``` 56 | npm run db-status --db_migrator_table_name=version 57 | npm run db-rollback --db-migrator-target=1452156800 58 | ``` 59 | -------------------------------------------------------------------------------- /doc/db-create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pajk/db-migrator/dbf2dec9708bb0debdb896e8a7214718c6e9887d/doc/db-create.png -------------------------------------------------------------------------------- /doc/db-migrate.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-disable no-console, no-process-exit, no-process-env */ 4 | 5 | const migrate = require('db-migrator/lib/migrate') 6 | const status = require('db-migrator/lib/status') 7 | const dotenv = require('dotenv') 8 | const path = require('path') 9 | 10 | const env = process.argv[2] || 'local' 11 | 12 | let envFile 13 | 14 | switch (env) { 15 | case 'staging': 16 | case 'production': 17 | case 'dev': 18 | envFile = `.env-heroku-${env}` 19 | break 20 | default: 21 | envFile = '.env' 22 | } 23 | 24 | dotenv.config({ 25 | path: path.join(__dirname, envFile), 26 | }) 27 | 28 | if (!process.env.DATABASE_URL) { 29 | console.error('DB connection string not defined.') 30 | process.exit(1) 31 | } 32 | 33 | const DB_URL = `${process.env.DATABASE_URL}?ssl=true` 34 | 35 | async function run() { 36 | await status({ 37 | connectionString: DB_URL, 38 | path: './migrations', 39 | tableName: 'migrations', 40 | }) 41 | await migrate({ 42 | connectionString: DB_URL, 43 | path: './migrations', 44 | tableName: 'migrations', 45 | }) 46 | console.log(envFile, 'migrated') 47 | } 48 | 49 | run() 50 | .then(() => process.exit(0)) 51 | .catch(() => process.exit(1)) 52 | -------------------------------------------------------------------------------- /doc/db-status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pajk/db-migrator/dbf2dec9708bb0debdb896e8a7214718c6e9887d/doc/db-status.png -------------------------------------------------------------------------------- /lib/application/service/migrator-service.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const colors = require('../../colors') 4 | const _ = require('lodash') 5 | 6 | const UP = 1 7 | 8 | class MigratorService { 9 | constructor (scriptService, versionService, messages) { 10 | this._scriptService = scriptService 11 | this._versionService = versionService 12 | this._messages = messages 13 | } 14 | 15 | async migrate (currentPath, targetVersion) { 16 | const scriptVersions = this._scriptService.getList(currentPath, UP) 17 | const dbVersions = await this._versionService.getAll() 18 | 19 | if (scriptVersions.length == 0) { 20 | throw this._messages.MIGRATION_SCRIPT_NOT_FOUND 21 | } 22 | 23 | const currentVersion = await this._versionService.getLastVersion() 24 | 25 | if (targetVersion == 0) { 26 | targetVersion = this._scriptService.getNewestVersion(scriptVersions) 27 | } else if (targetVersion === '+1') { 28 | targetVersion = this._scriptService.getNextVersion( 29 | scriptVersions, 30 | currentVersion 31 | ) 32 | } else { 33 | if ( 34 | this._scriptService.versionExists(scriptVersions, targetVersion) == 35 | false 36 | ) { 37 | throw this._messages.INVALID_TARGET_VERSION + targetVersion 38 | } 39 | 40 | if (currentVersion > targetVersion) { 41 | throw new Error( 42 | `Cannot migrate to older version. Current: ${currentVersion}, ` + 43 | `target: ${targetVersion}.` 44 | ) 45 | } 46 | } 47 | 48 | console.log( 49 | colors.verbose( 50 | this._messages.CURRENT_VERSION + 51 | (currentVersion ? currentVersion : this._messages.INITIAL_STATE) 52 | ) 53 | ) 54 | console.log( 55 | colors.verbose( 56 | this._messages.TARGET_VERSION + 57 | (targetVersion ? targetVersion : this._messages.INITIAL_STATE) 58 | ) 59 | ) 60 | 61 | const migrateVersions = _.filter(scriptVersions, function (v) { 62 | return typeof dbVersions[v.version] === 'undefined' && 63 | (v.version <= targetVersion || v.version <= currentVersion) 64 | }) 65 | 66 | if (migrateVersions.length == 0) { 67 | console.log(colors.warn(this._messages.ALREADY_MIGRATED)) 68 | return currentVersion 69 | } 70 | 71 | for (const v of migrateVersions) { 72 | await this._migrateVersion(v) 73 | } 74 | 75 | return targetVersion 76 | } 77 | 78 | async _migrateVersion (versionInfo) { 79 | const fileContent = this._scriptService.get(versionInfo.path) 80 | 81 | console.log( 82 | colors.grey('--------------------------------------------------') 83 | ) 84 | console.log(colors.white(fileContent)) 85 | 86 | await this._scriptService.execute(fileContent) 87 | 88 | console.log(colors.info(versionInfo.name + '.sql executed')) 89 | console.log( 90 | colors.grey('--------------------------------------------------') 91 | ) 92 | 93 | await this._versionService.addVersion( 94 | versionInfo.version, 95 | versionInfo.description 96 | ) 97 | } 98 | } 99 | 100 | module.exports = MigratorService 101 | -------------------------------------------------------------------------------- /lib/application/service/rollback-service.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const colors = require('../../colors') 4 | const _ = require('lodash') 5 | 6 | const DOWN = -1 7 | 8 | class RollbackService { 9 | constructor (scriptService, versionService, messages) { 10 | this._scriptService = scriptService 11 | this._versionService = versionService 12 | this._messages = messages 13 | } 14 | 15 | async rollback (currentPath, targetVersion) { 16 | const scriptVersions = this._scriptService.getList(currentPath, DOWN) 17 | const dbVersions = await this._versionService.getAll() 18 | 19 | if (scriptVersions.length == 0) { 20 | throw this._messages.MIGRATION_SCRIPT_NOT_FOUND 21 | } 22 | 23 | const currentVersion = await this._versionService.getLastVersion() 24 | 25 | if (targetVersion == 'initial') { 26 | targetVersion = null 27 | } else if (targetVersion == -1) { 28 | if (currentVersion == null) { 29 | throw this._messages.NO_MORE_ROLLBACK 30 | } 31 | 32 | targetVersion = this._scriptService.getPreviousVersion( 33 | scriptVersions, 34 | currentVersion 35 | ) 36 | } else { 37 | if ( 38 | this._scriptService.versionExists(scriptVersions, targetVersion) == 39 | false 40 | ) { 41 | throw this._messages.INVALID_TARGET_VERSION + targetVersion 42 | } 43 | 44 | if (currentVersion < targetVersion) { 45 | throw new Error( 46 | `Cannot rollback to newer version. Current: ${currentVersion}, ` + 47 | `target: ${targetVersion}.` 48 | ) 49 | } 50 | } 51 | 52 | console.log( 53 | colors.verbose( 54 | this._messages.CURRENT_VERSION + 55 | (currentVersion ? currentVersion : this._messages.INITIAL_STATE) 56 | ) 57 | ) 58 | console.log( 59 | colors.verbose( 60 | this._messages.TARGET_VERSION + 61 | (targetVersion ? targetVersion : this._messages.INITIAL_STATE) 62 | ) 63 | ) 64 | 65 | const rollbackVersions = _.filter(scriptVersions, function (v) { 66 | return dbVersions[v.version] && v.version > targetVersion 67 | }) 68 | 69 | if (rollbackVersions.length == 0) { 70 | console.log(colors.warn(this._messages.ALREADY_MIGRATED)) 71 | return currentVersion 72 | } 73 | 74 | for (const v of rollbackVersions) { 75 | await this._rollbackVersion(v) 76 | } 77 | 78 | return targetVersion 79 | } 80 | 81 | async _rollbackVersion (versionInfo) { 82 | const fileContent = this._scriptService.get(versionInfo.path) 83 | 84 | console.log( 85 | colors.grey('--------------------------------------------------') 86 | ) 87 | console.log(colors.white(fileContent)) 88 | 89 | await this._scriptService.execute(fileContent) 90 | 91 | console.log(colors.info(versionInfo.name + '.sql executed')) 92 | console.log( 93 | colors.grey('--------------------------------------------------') 94 | ) 95 | 96 | await this._versionService.removeVersion(versionInfo.version) 97 | } 98 | } 99 | 100 | module.exports = RollbackService 101 | -------------------------------------------------------------------------------- /lib/colors.js: -------------------------------------------------------------------------------- 1 | const colors = require('colors/safe') 2 | 3 | colors.setTheme({ 4 | verbose: 'cyan', 5 | info: 'green', 6 | warn: 'yellow', 7 | error: 'red' 8 | }) 9 | 10 | module.exports = colors 11 | -------------------------------------------------------------------------------- /lib/create.js: -------------------------------------------------------------------------------- 1 | const join = require('path').join 2 | const fs = require('fs') 3 | const colors = require('colors') 4 | 5 | function slugify (str) { 6 | return str.replace(/\s+/g, '-') 7 | } 8 | 9 | function create (options) { 10 | const currentPath = options.path || '.' 11 | const currentTimestamp = Math.floor(Date.now() / 1000) 12 | const title = options.title 13 | const slug = slugify(title) 14 | 15 | if (fs.existsSync(currentPath) == false) { 16 | fs.mkdirSync(currentPath) 17 | } 18 | 19 | ['UP', 'DOWN'].forEach(function (direction) { 20 | const fileName = slug 21 | ? [currentTimestamp, direction, slug].join('-') 22 | : [currentTimestamp, direction].join('-') 23 | 24 | const path = join(currentPath, fileName + '.sql') 25 | 26 | log('create', join(process.cwd(), path)) 27 | 28 | fs.writeFileSync( 29 | path, 30 | '-- ' + [currentTimestamp, direction, title].join(' ') 31 | ) 32 | }) 33 | } 34 | 35 | function log (key, msg) { 36 | console.log(colors.gray(` ${key} :`), colors.cyan(msg)) 37 | } 38 | 39 | module.exports = create 40 | -------------------------------------------------------------------------------- /lib/domain/persister/mysql.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | let mysqlLib 4 | 5 | try { 6 | mysqlLib = require('promise-mysql') 7 | } catch (er) { 8 | throw new Error( 9 | 'Add promise-mysql as a dependency to your project to use db-migrator with MySQL.' 10 | ) 11 | } 12 | 13 | const util = require('util') 14 | 15 | const CHECK_TABLE_SQL = 'SELECT EXISTS(SELECT * FROM information_schema.tables WHERE table_schema = ? AND table_name = ?) as value' 16 | const CREATE_TABLE_SQL = 'CREATE TABLE %s (id int, description VARCHAR(500), migrated_at DATETIME)' 17 | const LAST_VERSION_SQL = 'SELECT id AS value FROM %s ORDER BY id DESC LIMIT 1' 18 | const GET_ALL_SQL = 'SELECT id, description, migrated_at FROM %s ORDER BY id DESC' 19 | const ADD_VERSION_SQL = 'INSERT INTO %s (id, description, migrated_at) VALUES (?, ?, ?)' 20 | const REMOVE_VERSION_SQL = 'DELETE FROM %s WHERE id = ?' 21 | 22 | function createMysqlPersister (client, tableName, schema) { 23 | // MYSQL don't care about schema 24 | const schemaTable = tableName; 25 | 26 | const queryValue = (sql, params) => 27 | client 28 | .query(sql, params) 29 | .then(result => result.length > 0 ? result[0].value : null) 30 | 31 | const beginTransaction = () => client.query('SET AUTOCOMMIT = 0') 32 | .then(() => client.query('START TRANSACTION')) 33 | 34 | const commitTransaction = () => client.query('COMMIT') 35 | 36 | const checkTableExists = () => queryValue(CHECK_TABLE_SQL, [client.connection.config.database, tableName]) 37 | 38 | const createTable = () => 39 | client.query(util.format(CREATE_TABLE_SQL, schemaTable)) 40 | 41 | const getLastVersion = () => 42 | queryValue(util.format(LAST_VERSION_SQL, schemaTable)) 43 | 44 | const getAll = () => client.query(util.format(GET_ALL_SQL, schemaTable)) 45 | 46 | const addVersion = (version, description) => 47 | client.query(util.format(ADD_VERSION_SQL, schemaTable), [ 48 | version, 49 | description, 50 | new Date() 51 | ]) 52 | 53 | const removeVersion = version => 54 | client.query(util.format(REMOVE_VERSION_SQL, schemaTable), [version]) 55 | 56 | const executeRawQuery = async sql => { 57 | const queries = sql.split(';').filter(q => q.trim() != '') 58 | 59 | for (const query of queries) { 60 | await client.query(query) 61 | } 62 | } 63 | 64 | const done = () => {} 65 | 66 | return { 67 | beginTransaction, 68 | commitTransaction, 69 | checkTableExists, 70 | createTable, 71 | getLastVersion, 72 | getAll, 73 | addVersion, 74 | removeVersion, 75 | executeRawQuery, 76 | done 77 | } 78 | } 79 | 80 | module.exports = { 81 | create: async (connectionString, tableName, schema) => { 82 | let client 83 | const maxTries = (process.env.DB_CONNECT_MAX_ATTEMPTS || 25) - 1 84 | const waitMsec = process.env.DB_CONNECT_NEXT_ATTEMPT_DELAY || 1000 85 | 86 | for (let i = 0; !client; i++) { 87 | try { 88 | client = await mysqlLib.createConnection(connectionString) 89 | } catch(err) { 90 | if (err.code !== 'ECONNREFUSED' || i >= maxTries) { 91 | throw err 92 | } 93 | console.info('WAITING FOR DB...') 94 | await new Promise(resolve => setTimeout(resolve, waitMsec)) 95 | } 96 | } 97 | 98 | return createMysqlPersister(client, tableName, schema) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/domain/persister/postgres.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | let pg 4 | try { 5 | pg = require('pg') 6 | } catch (er) { 7 | throw new Error( 8 | 'Add `pg` as a dependency to your project to use db-migrator with PostgreSQL.' 9 | ) 10 | } 11 | 12 | const util = require('util') 13 | 14 | const CHECK_TABLE_SQL = 'SELECT EXISTS(SELECT * FROM information_schema.tables WHERE table_name = $1) as value' 15 | const CREATE_TABLE_SQL = 'CREATE TABLE %s (id int, description VARCHAR(500), migrated_at timestamptz DEFAULT NOW())' 16 | const LAST_VERSION_SQL = 'SELECT id AS value FROM %s ORDER BY id DESC LIMIT 1' 17 | const GET_ALL_SQL = 'SELECT id, description, migrated_at FROM %s ORDER BY id DESC' 18 | const ADD_VERSION_SQL = 'INSERT INTO %s (id, description) VALUES ($1, $2)' 19 | const REMOVE_VERSION_SQL = 'DELETE FROM %s WHERE id = $1' 20 | 21 | function createPostgresPersister (client, tableName, schema) { 22 | 23 | const schemaTable = schema === 'public' ? tableName : schema + '.' + tableName; 24 | 25 | const queryValue = (sql, params) => 26 | client 27 | .query(sql, params) 28 | .then(result => result.rows.length > 0 ? result.rows[0].value : null) 29 | 30 | const beginTransaction = () => client.query('BEGIN TRANSACTION') 31 | 32 | const commitTransaction = () => client.query('COMMIT') 33 | 34 | const checkTableExists = () => queryValue(CHECK_TABLE_SQL, [ tableName ]) 35 | 36 | const createTable = () => 37 | client.query(util.format(CREATE_TABLE_SQL, schemaTable)) 38 | 39 | const getLastVersion = () => 40 | queryValue(util.format(LAST_VERSION_SQL, schemaTable)) 41 | 42 | const getAll = () => 43 | client 44 | .query(util.format(GET_ALL_SQL, schemaTable)) 45 | .then(result => result.rows) 46 | 47 | const addVersion = (version, description) => 48 | client.query(util.format(ADD_VERSION_SQL, schemaTable), [ 49 | version, 50 | description 51 | ]) 52 | 53 | const removeVersion = version => 54 | client.query(util.format(REMOVE_VERSION_SQL, schemaTable), [version]) 55 | 56 | const executeRawQuery = async sql => 57 | client.query(sql).then(result => result.rows) 58 | 59 | const done = () => client && client.release() 60 | 61 | return { 62 | beginTransaction, 63 | commitTransaction, 64 | checkTableExists, 65 | createTable, 66 | getLastVersion, 67 | getAll, 68 | addVersion, 69 | removeVersion, 70 | executeRawQuery, 71 | done 72 | } 73 | } 74 | 75 | module.exports = { 76 | create: async (connectionString, tableName, schema) => { 77 | const pool = new pg.Pool({ 78 | connectionString: connectionString 79 | }) 80 | 81 | let client 82 | const maxTries = (process.env.DB_CONNECT_MAX_ATTEMPTS || 25) - 1 83 | const waitMsec = process.env.DB_CONNECT_NEXT_ATTEMPT_DELAY || 1000 84 | const role = process.env.DB_MIGRATOR_ROLE || process.env.npm_config_db_migrator_role || "" 85 | for (let i = 0; !client; i++) { 86 | try { 87 | client = await pool.connect() 88 | } catch(err) { 89 | if (err.code !== 'ECONNREFUSED' || i >= maxTries) { 90 | throw err 91 | } 92 | console.info('WAITING FOR DB...') 93 | await new Promise(resolve => setTimeout(resolve, waitMsec)) 94 | } 95 | } 96 | 97 | if (role !== "") { 98 | await client.query(`SET ROLE "${role}"`) 99 | } 100 | 101 | return createPostgresPersister(client, tableName, schema) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/domain/repository/script-repository.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class ScriptRepository { 4 | constructor (fs, persister) { 5 | this._fs = fs 6 | this._persister = persister 7 | } 8 | 9 | get (path) { 10 | return this._fs.readFileSync(path, 'utf8') 11 | } 12 | 13 | execute (query) { 14 | return this._persister.executeRawQuery(query) 15 | } 16 | 17 | getList (path) { 18 | return this._fs.readdirSync(path) 19 | } 20 | 21 | getStat (path) { 22 | return this._fs.statSync(path) 23 | } 24 | } 25 | 26 | module.exports = ScriptRepository 27 | -------------------------------------------------------------------------------- /lib/domain/repository/version-repository.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class VersionRepository { 4 | constructor (persister) { 5 | this._persister = persister 6 | } 7 | 8 | checkTable () { 9 | return this._persister.checkTableExists() 10 | } 11 | 12 | createTable () { 13 | return this._persister.createTable() 14 | } 15 | 16 | getLastVersion () { 17 | return this._persister.getLastVersion() 18 | } 19 | 20 | getAll () { 21 | return this._persister.getAll() 22 | } 23 | 24 | addVersion (version, description) { 25 | return this._persister.addVersion(version, description) 26 | } 27 | 28 | removeVersion (version) { 29 | return this._persister.removeVersion(version) 30 | } 31 | } 32 | 33 | module.exports = VersionRepository 34 | -------------------------------------------------------------------------------- /lib/domain/service/script-service.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _ = require('lodash') 4 | 5 | class ScriptService { 6 | constructor (scriptRepository, path) { 7 | this._scriptRepository = scriptRepository 8 | this._path = path 9 | } 10 | 11 | get (path) { 12 | return this._scriptRepository.get(path) 13 | } 14 | 15 | getList (currentPath, onlyDirection) { 16 | let sqlFiles = [] 17 | 18 | const files = this._scriptRepository.getList(currentPath) 19 | 20 | if (files) { 21 | files.sort(function (a, b) { 22 | if (onlyDirection == -1) { 23 | return parseInt(b) - parseInt(a) 24 | } else { 25 | return parseInt(a) - parseInt(b) 26 | } 27 | }) 28 | } 29 | 30 | // Looking for all files in the path directory and all sub directories recursively 31 | for (const file of files) { 32 | const fullPath = `${currentPath}/${file}` 33 | 34 | const stats = this._scriptRepository.getStat(fullPath) 35 | 36 | if (stats.isDirectory()) { 37 | sqlFiles = sqlFiles.concat(this.getList(fullPath)) 38 | } else if (stats.isFile()) { 39 | // Files must have an extension with ".sql" (case insensitive) 40 | // with and "ID-[UP|DOWN](-description).sql" format where ID must be a number 41 | // All other files will be ignored 42 | if (this._path.extname(fullPath).toUpperCase() == '.SQL') { 43 | const fileName = this._path.basename(fullPath, '.sql') 44 | 45 | if (fileName.indexOf('-') == -1) { 46 | continue 47 | } 48 | 49 | const parts = fileName.split('-') 50 | 51 | if (parts.length < 2) { 52 | continue 53 | } 54 | 55 | // "ID-DIRECTION(-description)" 56 | const version = parseInt(parts[0]) 57 | let direction = parts[1].toUpperCase() 58 | let description = null 59 | 60 | if (parts.length > 2) { 61 | description = parts 62 | .slice(2) 63 | .reduce((final, part) => `${final}${_.capitalize(part)} `, '') 64 | } 65 | 66 | if ( 67 | !version || 68 | isNaN(version) || 69 | (direction != 'DOWN' && direction != 'UP') 70 | ) { 71 | continue 72 | } 73 | 74 | direction = direction == 'UP' ? 1 : -1 75 | 76 | if ( 77 | typeof onlyDirection !== 'undefined' && direction != onlyDirection 78 | ) { 79 | continue 80 | } 81 | 82 | sqlFiles.push({ 83 | version, 84 | direction, 85 | path: fullPath, 86 | name: fileName, 87 | description 88 | }) 89 | } 90 | } 91 | } 92 | 93 | return sqlFiles 94 | } 95 | 96 | getNextVersion (fileList, currentVersion) { 97 | const versions = _.filter(fileList, function (sqlFile) { 98 | return sqlFile.version > currentVersion 99 | }) 100 | 101 | return versions.length > 0 ? _.head(versions).version : null 102 | } 103 | 104 | getPreviousVersion (fileList, currentVersion) { 105 | const versions = _.filter( 106 | fileList, 107 | sqlFile => sqlFile.version < currentVersion 108 | ) 109 | 110 | return versions.length > 0 ? _.head(versions).version : null 111 | } 112 | 113 | getNewestVersion (fileList) { 114 | return _.maxBy(fileList, item => item.version).version 115 | } 116 | 117 | versionExists (fileList, version) { 118 | return _.findIndex(fileList, {'version': parseInt(version)}) > -1 119 | } 120 | 121 | execute (query) { 122 | // Execute migration script 123 | return this._scriptRepository.execute(query) 124 | } 125 | } 126 | 127 | module.exports = ScriptService 128 | -------------------------------------------------------------------------------- /lib/domain/service/version-service.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const colors = require('colors/safe') 4 | 5 | class VersionService { 6 | constructor (versionRepository, messages) { 7 | this._versionRepository = versionRepository 8 | this._messages = messages 9 | } 10 | 11 | async getLastVersion () { 12 | let currentVersion = null 13 | 14 | const exists = await this._versionRepository.checkTable() 15 | 16 | if (!exists) { 17 | console.log(colors.warn(this._messages.FIRST_INITIALIZE)) 18 | await this._versionRepository.createTable() 19 | } else { 20 | currentVersion = await this._versionRepository.getLastVersion() 21 | } 22 | 23 | return currentVersion 24 | } 25 | 26 | async getAll () { 27 | const exists = await this._versionRepository.checkTable() 28 | const indexedVersions = {} 29 | 30 | if (!exists) { 31 | console.log(colors.warn(this._messages.FIRST_INITIALIZE)) 32 | await this._versionRepository.createTable() 33 | } else { 34 | const versions = await this._versionRepository.getAll() 35 | 36 | versions.forEach(v => { 37 | indexedVersions[v.id] = { 38 | version: v.id, 39 | migrated_at: v.migrated_at.toLocaleString(), 40 | description: v.description 41 | } 42 | }) 43 | } 44 | 45 | return indexedVersions 46 | } 47 | 48 | addVersion (version, description) { 49 | return this._versionRepository.addVersion(version, description) 50 | } 51 | 52 | removeVersion (version) { 53 | return this._versionRepository.removeVersion(version) 54 | } 55 | } 56 | 57 | module.exports = VersionService 58 | -------------------------------------------------------------------------------- /lib/infrastructure/messages.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | CURRENT_VERSION: 'Current Version : ', 5 | TARGET_VERSION: 'Target Version : ', 6 | FIRST_INITIALIZE: 'Versioning table does not exist, will be created for the first time', 7 | CONNECTION_STRING_MUST_BE_PROVIDED: 'ConnectionString must be provided', 8 | INVALID_TARGET_VERSION: 'Target version parameter must be:\n* a valid version id to migrate to\n* or +1 to roll one step forward\n* or -1 to roll one step back\n* or empty to migrate to the latest version available\nTarget version was set to: ', 9 | ALREADY_MIGRATED: 'Database is already migrated to the target version', 10 | MIGRATION_SCRIPT_NOT_FOUND: 'There is no migration script file in current folder and subfolders', 11 | FILE_NOT_FOUND: 'Migration script could not be found : ', 12 | NO_MORE_ROLLBACK: "Database at the initial state, can't roll back more", 13 | NO_MORE_MIGRATE: "Database at the latest version, can't find newer migrations", 14 | MIGRATION_ERROR: 'Migration could not be completed: ', 15 | MIGRATION_COMPLETED: 'Migration has been completed successfully\nCurrent database version : ', 16 | ROLLBACK_COMPLETED: 'Rollback has been completed successfully\nCurrent database version : ', 17 | INITIAL_STATE: 'initial state', 18 | NOT_MIGRATED: 'NOT MIGRATED' 19 | } 20 | -------------------------------------------------------------------------------- /lib/migrate.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const colors = require('./colors') 6 | const printStatus = require('./status') 7 | const messages = require('./infrastructure/messages') 8 | 9 | async function migrate (options) { 10 | const connectionString = options.connectionString 11 | const targetVersion = options.targetVersion || 0 12 | const schema = options.schema || 'public' 13 | const currentPath = options.path || '.' 14 | const tableName = options.tableName || 'migrations' 15 | const dbDriver = options.dbDriver || 'postgres' 16 | let persister 17 | 18 | try { 19 | const persisterProvider = require('./domain/persister/' + dbDriver) 20 | 21 | persister = await persisterProvider.create(connectionString, tableName, schema) 22 | 23 | await persister.beginTransaction() 24 | 25 | const migrationService = getMigrationService(persister) 26 | 27 | const currentVersion = await migrationService.migrate( 28 | currentPath, 29 | targetVersion 30 | ) 31 | 32 | await persister.commitTransaction() 33 | 34 | persister.done() 35 | 36 | console.log( 37 | colors.info('--------------------------------------------------') 38 | ) 39 | console.log( 40 | colors.info( 41 | messages.MIGRATION_COMPLETED + 42 | (currentVersion ? currentVersion : messages.INITIAL_STATE) 43 | ) 44 | ) 45 | } catch (error) { 46 | if (error) { 47 | console.error(colors.error(messages.MIGRATION_ERROR + error)) 48 | } 49 | 50 | if (persister) { 51 | persister.done() 52 | } 53 | 54 | throw error 55 | } 56 | 57 | if (options.printStatus) { 58 | await printStatus(options) 59 | } 60 | } 61 | 62 | function getMigrationService (persister) { 63 | const MigratorService = require('./application/service/migrator-service') 64 | const ScriptService = require('./domain/service/script-service') 65 | const VersionService = require('./domain/service/version-service') 66 | const ScriptRepository = require('./domain/repository/script-repository') 67 | const VersionRepository = require('./domain/repository/version-repository') 68 | 69 | return new MigratorService( 70 | new ScriptService(new ScriptRepository(fs, persister), path), 71 | new VersionService(new VersionRepository(persister), messages), 72 | messages 73 | ) 74 | } 75 | 76 | module.exports = migrate 77 | -------------------------------------------------------------------------------- /lib/rollback.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const colors = require('./colors') 6 | const printStatus = require('./status') 7 | const messages = require('./infrastructure/messages') 8 | 9 | async function rollback (options) { 10 | const connectionString = options.connectionString 11 | const targetVersion = options.targetVersion || -1 12 | const currentPath = options.path || '.' 13 | const schema = options.schema || 'public' 14 | const tableName = options.tableName || 'migrations' 15 | const dbDriver = options.dbDriver || 'postgres' 16 | let persister 17 | 18 | try { 19 | const persisterProvider = require('./domain/persister/' + dbDriver) 20 | 21 | persister = await persisterProvider.create(connectionString, tableName, schema) 22 | 23 | await persister.beginTransaction() 24 | 25 | const rollbackService = getRollbackService(persister) 26 | 27 | const currentVersion = await rollbackService.rollback( 28 | currentPath, 29 | targetVersion 30 | ) 31 | 32 | await persister.commitTransaction() 33 | 34 | persister.done() 35 | 36 | console.log( 37 | colors.info('--------------------------------------------------') 38 | ) 39 | console.log( 40 | colors.info( 41 | messages.ROLLBACK_COMPLETED + 42 | (currentVersion ? currentVersion : messages.INITIAL_STATE) 43 | ) 44 | ) 45 | } catch (error) { 46 | if (error) { 47 | console.error(colors.error(messages.MIGRATION_ERROR + error)) 48 | } 49 | 50 | if (persister) { 51 | persister.done() 52 | } 53 | 54 | throw error 55 | } 56 | 57 | if (options.printStatus) { 58 | await printStatus(options) 59 | } 60 | } 61 | 62 | function getRollbackService (persister) { 63 | const RollbackService = require('./application/service/rollback-service') 64 | const ScriptService = require('./domain/service/script-service') 65 | const VersionService = require('./domain/service/version-service') 66 | const ScriptRepository = require('./domain/repository/script-repository') 67 | const VersionRepository = require('./domain/repository/version-repository') 68 | 69 | return new RollbackService( 70 | new ScriptService(new ScriptRepository(fs, persister), path), 71 | new VersionService(new VersionRepository(persister), messages), 72 | messages 73 | ) 74 | } 75 | 76 | module.exports = rollback 77 | -------------------------------------------------------------------------------- /lib/status.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const _ = require('lodash') 5 | const path = require('path') 6 | const colors = require('./colors') 7 | const messages = require('./infrastructure/messages') 8 | const ScriptService = require('./domain/service/script-service') 9 | const VersionService = require('./domain/service/version-service') 10 | const ScriptRepository = require('./domain/repository/script-repository') 11 | const VersionRepository = require('./domain/repository/version-repository') 12 | 13 | async function status (options) { 14 | const connectionString = options.connectionString 15 | const currentPath = options.path || '.' 16 | const schema = options.schema || 'public' 17 | const tableName = options.tableName || 'version' 18 | const dbDriver = options.dbDriver || 'postgres' 19 | let persister 20 | 21 | try { 22 | const persisterProvider = require('./domain/persister/' + dbDriver) 23 | persister = await persisterProvider.create(connectionString, tableName, schema) 24 | 25 | const scriptService = new ScriptService( 26 | new ScriptRepository(fs, persister), 27 | path 28 | ) 29 | const versionService = new VersionService( 30 | new VersionRepository(persister), 31 | messages 32 | ) 33 | 34 | const scriptVersions = scriptService.getList(currentPath, 1) 35 | const dbVersions = await versionService.getAll() 36 | 37 | console.log( 38 | colors.grey('\n----------------- Database Status ----------------\n') 39 | ) 40 | console.log( 41 | colors.grey('Version | Migrated At | Description --------------\n') 42 | ) 43 | 44 | const middleColumnSize = new Date().toLocaleString().length 45 | const notMigratedMsg = _.pad(messages.NOT_MIGRATED, middleColumnSize) 46 | let line 47 | 48 | scriptVersions.forEach(function (v) { 49 | if (dbVersions[v.version]) { 50 | line = [ 51 | v.version, 52 | dbVersions[v.version].migrated_at, 53 | v.description 54 | ].join(' | ') 55 | delete dbVersions[v.version] 56 | console.log(colors.info(line)) 57 | } else { 58 | line = [v.version, notMigratedMsg, v.description].join(' | ') 59 | console.log(colors.warn(line)) 60 | } 61 | }) 62 | 63 | if (_.size(dbVersions) > 0) { 64 | console.log('\nMissing files ------------------------------------\n') 65 | 66 | _.forOwn(dbVersions, function (info) { 67 | line = [info.version, info.migrated_at, info.description].join(' | ') 68 | console.log(colors.error(line)) 69 | }) 70 | } 71 | console.log('') 72 | 73 | persister.done() 74 | } catch (error) { 75 | if (error) { 76 | console.log(error.stack) 77 | console.error(colors.error(messages.MIGRATION_ERROR + error)) 78 | } 79 | 80 | if (persister) { 81 | persister.done() 82 | } 83 | 84 | throw error 85 | } 86 | } 87 | 88 | module.exports = status 89 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "db-migrator", 3 | "version": "2.4.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "colors": { 8 | "version": "1.4.0", 9 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", 10 | "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" 11 | }, 12 | "lodash": { 13 | "version": "4.17.15", 14 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 15 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "db-migrator", 3 | "description": "The complete and easy to use database migration tool", 4 | "version": "2.4.0", 5 | "bin": { 6 | "db-migrate": "bin/db-migrate", 7 | "db-rollback": "bin/db-rollback", 8 | "db-create": "bin/db-create", 9 | "db-status": "bin/db-status" 10 | }, 11 | "homepage": "https://github.com/Pajk/db-migrator/", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/Pajk/db-migrator.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/Pajk/db-migrator/issues/" 18 | }, 19 | "author": { 20 | "name": "Pavel Pokorny", 21 | "email": "pajkycz@gmail.com" 22 | }, 23 | "contributors": [ 24 | { 25 | "name": "Aphel Cloud Solutions", 26 | "email": "lkaragol@aphel.com.tr", 27 | "url": "http://www.aphel.com.tr/" 28 | } 29 | ], 30 | "dependencies": { 31 | "colors": "^1.4.0", 32 | "lodash": "^4.17.15" 33 | }, 34 | "license": "MIT", 35 | "keywords": [ 36 | "pg", 37 | "postgres", 38 | "postgresql", 39 | "migrate", 40 | "migration", 41 | "migrator", 42 | "pg-migrate", 43 | "pg-migration", 44 | "pg-migrator", 45 | "db", 46 | "database", 47 | "database migration", 48 | "database migrator", 49 | "node-postgres", 50 | "node-postgresql" 51 | ], 52 | "engines": { 53 | "node": ">=7.6.0" 54 | } 55 | } 56 | --------------------------------------------------------------------------------