diff --git a/README.md b/README.md index 00bc3aea..e052558a 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ You could also specify your database url by setting the environment variable `DA DATABASE_URL=postgres://postgres@localhost/name node-pg-migrate ``` +You can specify custom JSON file with config (format is same as for `db` entry of [config](https://www.npmjs.com/package/config) file) + If a .env file exists, it will be loaded using [dotenv](https://www.npmjs.com/package/dotenv) (if installed) when running the pg-migrate binary. Depending on your project's setup, it may make sense to write some custom grunt tasks that set this env var and run your migration commands. More on that below. @@ -65,6 +67,7 @@ Depending on your project's setup, it may make sense to write some custom grunt You can adjust defaults by passing arguments to `pg-migrate`: +* `config-file` (`f`) - The file with migration JSON config (defaults to undefined) * `schema` (`s`) - The schema on which migration will be run (defaults to `public`) * `database-url-var` (`d`) - Name of env variable with database url string (defaults to `DATABASE_URL`) * `migrations-dir` (`m`) - The directory containing your migration files (defaults to `migrations`) @@ -76,11 +79,26 @@ You can adjust defaults by passing arguments to `pg-migrate`: See all by running `pg-migrate --help`. -Most of configuration options can be also specified in `node-config` configuration file. +Most of configuration options can be also specified in [config](https://www.npmjs.com/package/config) file. For SSL connection to DB you can set `PGSSLMODE` environment variable to value from [list](https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNECT-SSLMODE) other then `disable`. e.g. `PGSSLMODE=require pg-migrate up` ([pg](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md#v260) will take it into account) +#### JSON Configuration + +You can use [config](https://www.npmjs.com/package/config) or your own json file with configuration (`config-file` command line option). + +Available options are: + +* `migrations-dir`, `migrations-schema`, `migrations-table`, `check-order` - same as above + +* either `url` or [`user`], [`password`], `host` (defaults to localhost), `port` (defaults to 5432), `name` - for connection details + +* `type-shorthands` - for column type shorthands + + You can specify custom types which will be expanded to column definition (e.g. for `module.exports = { "type-shorthands": { id: { type: 'uuid', primaryKey: true }, createdAt: { type: 'timestamp', notNull: true, default: new require('node-pg-migrate').PgLiteral('current_timestamp') } } }` + it will in `pgm.createTable('test', { id: 'id', createdAt: 'createdAt' });` produce SQL `CREATE TABLE "test" ("id" uuid PRIMARY KEY, "createdAt" timestamp DEFAULT current_timestamp NOT NULL);`). + ### Locking `pg-migrate` automatically checks if no other migration is running. To do so, it locks the migration table and enters comment there. diff --git a/bin/pg-migrate b/bin/pg-migrate index 90d1ac38..f2971ab3 100755 --- a/bin/pg-migrate +++ b/bin/pg-migrate @@ -2,9 +2,11 @@ 'use strict'; const util = require('util'); +const path = require('path'); const yargs = require('yargs'); const Migration = require('../dist/migration').default; // eslint-disable-line import/no-unresolved,import/extensions const runner = require('../dist/runner'); // eslint-disable-line import/no-unresolved,import/extensions + const migrationRunner = runner.default; const unlockRunner = runner.unlockRunner; @@ -28,6 +30,7 @@ const migrationsTable = 'migrations-table'; const migrationsSchema = 'migrations-schema'; const checkOrder = 'check-order'; const configValue = 'config-value'; +const configFile = 'config-file'; const argv = yargs .usage('Usage: db-migrate [up|down|create|unlock] [migrationName] [options]') @@ -87,12 +90,19 @@ const argv = yargs type: 'boolean', }) - .option('config-value', { + .option(configValue, { default: 'db', describe: 'Name of config section with db options', type: 'string', }) + .option('f', { + alias: configFile, + default: undefined, + describe: 'Name of config file with db options', + type: 'string', + }) + .help() .argv; @@ -112,35 +122,49 @@ let SCHEMA = argv[schema]; let MIGRATIONS_SCHEMA = argv[migrationsSchema]; let MIGRATIONS_TABLE = argv[migrationsTable]; let CHECK_ORDER = argv[checkOrder]; +let TYPE_SHORTHANDS = {}; + +function readJson(json) { + if (typeof json === 'object') { + SCHEMA = json[schema] || SCHEMA; + MIGRATIONS_DIR = json[migrationsDir] || MIGRATIONS_DIR; + MIGRATIONS_SCHEMA = json[migrationsSchema] || MIGRATIONS_SCHEMA; + MIGRATIONS_TABLE = json[migrationsTable] || MIGRATIONS_TABLE; + CHECK_ORDER = typeof json[checkOrder] !== 'undefined' ? json[checkOrder] : CHECK_ORDER; + TYPE_SHORTHANDS = json['type-shorthands'] || TYPE_SHORTHANDS; + if (json.url) { + DATABASE_URL = json.url; + } else if (json.host || json.port || json.name) { + const creds = `${json.user}${json.password ? `:${json.password}` : ''}`; + DATABASE_URL = `postgres://${creds ? `${creds}@` : ''}${json.host || 'localhost'}:${json.port || 5432}/${json.name}`; + } + } else { + DATABASE_URL = json || DATABASE_URL; + } +} try { // Load config (and suppress the no-config-warning) + const oldSuppressWarning = process.env.SUPPRESS_NO_CONFIG_WARNING; process.env.SUPPRESS_NO_CONFIG_WARNING = 1; const config = require('config'); // eslint-disable-line global-require,import/no-extraneous-dependencies if (config[argv[configValue]]) { const db = config[argv[configValue]]; - if (typeof db === 'object') { - SCHEMA = db[schema] || SCHEMA; - MIGRATIONS_DIR = db[migrationsDir] || MIGRATIONS_DIR; - MIGRATIONS_SCHEMA = db[migrationsSchema] || MIGRATIONS_SCHEMA; - MIGRATIONS_TABLE = db[migrationsTable] || MIGRATIONS_TABLE; - CHECK_ORDER = typeof db[checkOrder] !== 'undefined' ? db[checkOrder] : CHECK_ORDER; - if (db.url) { - DATABASE_URL = db.url; - } else if (db.host || db.port || db.name) { - const creds = `${db.user}${db.password ? `:${db.password}` : ''}`; - DATABASE_URL = `postgres://${creds ? `${creds}@` : ''}${db.host || 'localhost'}:${db.port || 5432}/${db.name}`; - } - } else { - DATABASE_URL = db; - } + readJson(db); } + process.env.SUPPRESS_NO_CONFIG_WARNING = oldSuppressWarning; } catch (err) { if (err.code !== 'MODULE_NOT_FOUND') { throw err; } } +const configFileName = argv[configFile]; +if (configFileName) { + const config = require(path.resolve(configFileName)); // eslint-disable-line global-require,import/no-dynamic-require + readJson(config); +} + const action = argv._.shift(); if (action === 'create') { @@ -210,6 +234,7 @@ if (action === 'create') { count: num_migrations, file: migration_name, checkOrder: CHECK_ORDER, + typeShorthands: TYPE_SHORTHANDS, }) .then(() => { console.log('Migrations complete!'); diff --git a/lib/db.js b/lib/db.js index 17724bab..b0fee07c 100644 --- a/lib/db.js +++ b/lib/db.js @@ -6,7 +6,7 @@ import pg from 'pg'; // or native libpq bindings // import pg from 'pg/native'; -export default (connection_string) => { +export default (connection_string, log = console.error) => { const client = new pg.Client(connection_string); let client_active = false; const beforeCloseListeners = []; @@ -17,7 +17,7 @@ export default (connection_string) => { ? resolve() : client.connect((err) => { if (err) { - console.error('could not connect to postgres', err); + log('could not connect to postgres', err); reject(err); } else { client_active = true; @@ -46,14 +46,14 @@ export default (connection_string) => { const stringEnd = string.substr(endLineWrapPos); const startLineWrapPos = stringStart.lastIndexOf('\n') + 1; const padding = ' '.repeat(position - startLineWrapPos - 1); - console.error(`Error executing: + log(`Error executing: ${stringStart} ${padding}^^^^${stringEnd} ${message} `); } else { - console.error(`Error executing: + log(`Error executing: ${string} ${err} `); @@ -82,11 +82,7 @@ ${err} promise .then(listener) .catch(err => - console.err( - err.stack - ? err.stack - : err - ) + log(err.stack || err) ), Promise.resolve() ) diff --git a/lib/migration-builder.js b/lib/migration-builder.js index 8a9e43f7..89a69b01 100644 --- a/lib/migration-builder.js +++ b/lib/migration-builder.js @@ -19,8 +19,7 @@ import * as tables from './operations/tables'; import * as other from './operations/other'; export default class MigrationBuilder { - - constructor() { + constructor(options = {}) { this._steps = []; this._REVERSE_MODE = false; // by default, all migrations are wrapped in a transaction @@ -46,11 +45,11 @@ export default class MigrationBuilder { this.dropExtension = wrap(extensions.drop); this.addExtension = this.createExtension; - this.createTable = wrap(tables.create); + this.createTable = wrap(tables.create(options.typeShorthands)); this.dropTable = wrap(tables.drop); this.renameTable = wrap(tables.renameTable); - this.addColumns = wrap(tables.addColumns); + this.addColumns = wrap(tables.addColumns(options.typeShorthands)); this.dropColumns = wrap(tables.dropColumns); this.renameColumn = wrap(tables.renameColumn); this.alterColumn = wrap(tables.alterColumn); diff --git a/lib/migration.js b/lib/migration.js index 19648b84..6f002e07 100644 --- a/lib/migration.js +++ b/lib/migration.js @@ -15,7 +15,6 @@ import MigrationBuilder from './migration-builder'; import { getMigrationTableSchema } from './utils'; class Migration { - // class method that creates a new migration file by cloning the migration template static create(name, directory) { // ensure the migrations directory exists @@ -89,13 +88,13 @@ class Migration { } applyUp() { - const pgm = new MigrationBuilder(); + const pgm = new MigrationBuilder(this.options); return this._apply(this.up, pgm); } applyDown() { - const pgm = new MigrationBuilder(); + const pgm = new MigrationBuilder(this.options); if (this.down === false) { return Promise.reject(`User has disabled down migration on file: ${this.name}`); diff --git a/lib/operations/tables.js b/lib/operations/tables.js index cbb61be7..69f117c9 100644 --- a/lib/operations/tables.js +++ b/lib/operations/tables.js @@ -10,17 +10,21 @@ const type_adapters = { bool: 'boolean', }; +const default_type_shorthands = { + id: { type: 'serial', primaryKey: true }, // convenience type for serial primary keys +}; + // some convenience adapters -- see above const applyTypeAdapters = type => (type_adapters[type] ? type_adapters[type] : type); const quote = array => array.map(item => template`"${item}"`); -function parseColumns(columns, table_name) { +function parseColumns(columns, table_name, extending_type_shorthands = {}) { + const type_shorthands = { ...default_type_shorthands, ...extending_type_shorthands }; let columnsWithOptions = _.mapValues(columns, (options = {}) => { if (typeof options === 'string') { - options = options === 'id' // eslint-disable-line no-param-reassign - // convenience type for serial primary keys - ? { type: 'serial', primaryKey: true } + options = type_shorthands[options] // eslint-disable-line no-param-reassign + ? type_shorthands[options] : { type: options }; } @@ -78,7 +82,7 @@ function parseColumns(columns, table_name) { .join(',\n'); } -export const create = (table_name, columns, options = {}) => { +export const create = type_shorthands => (table_name, columns, options = {}) => { /* columns - hash of columns @@ -87,7 +91,7 @@ export const create = (table_name, columns, options = {}) => { columns - see column options options.inherits - table to inherit from (optional) */ - const columnsString = parseColumns(columns, table_name).replace(/^/gm, ' '); + const columnsString = parseColumns(columns, table_name, type_shorthands).replace(/^/gm, ' '); const inherits = options.inherits ? ` INHERITS ${options.inherits}` : ''; return template`CREATE TABLE "${table_name}" (\n${columnsString}\n)${inherits};`; }; @@ -95,8 +99,8 @@ export const create = (table_name, columns, options = {}) => { export const drop = table_name => template`DROP TABLE "${table_name}";`; -export const addColumns = (table_name, columns) => - template`ALTER TABLE "${table_name}"\n${parseColumns(columns, table_name).replace(/^/gm, ' ADD ')};`; +export const addColumns = type_shorthands => (table_name, columns) => + template`ALTER TABLE "${table_name}"\n${parseColumns(columns, table_name, type_shorthands).replace(/^/gm, ' ADD ')};`; export const dropColumns = (table_name, columns) => { if (typeof columns === 'string') { diff --git a/lib/runner.js b/lib/runner.js index 9a44d9dd..157999d5 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -6,6 +6,8 @@ import Db from './db'; import Migration from './migration'; import { getMigrationTableSchema, finallyPromise } from './utils'; +export { PgLiteral } from './utils'; + const readdir = (...args) => new Promise((resolve, reject) => fs.readdir( @@ -55,7 +57,7 @@ const lock = (db, options) => { return null; }); - return db.query(`BEGIN`) + return db.query('BEGIN') .then(() => db.query(`LOCK "${schema}"."${options.migrations_table}" IN ACCESS EXCLUSIVE MODE`)) .then(getCurrentLockName) .then((currentLockName) => { @@ -64,7 +66,7 @@ const lock = (db, options) => { } }) .then(() => db.query(`COMMENT ON TABLE "${schema}"."${options.migrations_table}" IS '${lockName}'`)) - .then(() => db.query(`COMMIT`)); + .then(() => db.query('COMMIT')); }; const unlock = (db, options) => { diff --git a/lib/utils.js b/lib/utils.js index 04c46507..0b21c412 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -64,7 +64,7 @@ export const finallyPromise = func => [ func, (err) => { const errHandler = (innerErr) => { - console.err( + console.error( innerErr.stack ? innerErr.stack : innerErr diff --git a/package.json b/package.json index e39ae895..8e4ecf0d 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "bin": { "pg-migrate": "bin/pg-migrate" }, + "main": "dist/runner.js", "keywords": [ "db", "database", @@ -41,12 +42,12 @@ "babel-plugin-rewire": "^1.1.0", "babel-preset-env": "^1.4.0", "babel-preset-stage-3": "^6.24.0", - "chai": "^3.5.0", - "chai-as-promised": "^6.0.0", + "chai": "^4.0.0", + "chai-as-promised": "^7.0.0", "config": ">=1.0.0", "cross-env": "^5.0.0", "dotenv": ">=1.0.0", - "eslint": "^3.17.0", + "eslint": "^3.19.0", "eslint-config-airbnb-base": "11.2.0", "eslint-plugin-import": "^2.2.0", "mocha": "^3.4.1", @@ -66,6 +67,6 @@ "test": "cross-env NODE_ENV=test mocha --opts ./mocha.opts test", "lint": "eslint -c .eslintrc . bin/pg-migrate", "lintfix": "npm run lint -- --fix", - "prepublish": "npm run compile" + "prepare": "npm run compile" } } diff --git a/test/db-test.js b/test/db-test.js index 7f8cfce4..17e5ba51 100644 --- a/test/db-test.js +++ b/test/db-test.js @@ -6,6 +6,7 @@ import Db, { __RewireAPI__ as DbRewireAPI } from '../lib/db'; // eslint-disable- describe('lib/db', () => { let sandbox; const pgMock = {}; + const log = () => null; let client; before(() => { @@ -41,7 +42,7 @@ describe('lib/db', () => { describe('.query( query )', () => { let db; beforeEach(() => { - db = Db(); + db = Db(undefined, log); client.connect = sandbox.stub(); client.query = sandbox.stub(); }); @@ -111,8 +112,10 @@ describe('lib/db', () => { it('should call client.end', () => { client.end = sinon.spy(); - db.close(); - expect(client.end).to.be.calledOnce; + return db.close() + .then(() => + expect(client.end).to.be.calledOnce + ); }); }); }); diff --git a/test/tables-test.js b/test/tables-test.js index 257bdc23..e74f3c93 100644 --- a/test/tables-test.js +++ b/test/tables-test.js @@ -4,9 +4,23 @@ import * as Tables from '../lib/operations/tables'; describe('lib/operations/tables', () => { describe('.create', () => { it('check schemas can be used', () => { - const sql = Tables.create({ schema: 'my_schema', name: 'my_table_name' }, { id: 'serial' }); + const sql = Tables.create()({ schema: 'my_schema', name: 'my_table_name' }, { id: 'serial' }); expect(sql).to.equal(`CREATE TABLE "my_schema"."my_table_name" ( "id" serial +);`); + }); + + it('check shorthands work', () => { + const sql = Tables.create()('my_table_name', { id: 'id' }); + expect(sql).to.equal(`CREATE TABLE "my_table_name" ( + "id" serial PRIMARY KEY +);`); + }); + + it('check custom shorthands can be used', () => { + const sql = Tables.create({ id: { type: 'uuid', primaryKey: true } })('my_table_name', { id: 'id' }); + expect(sql).to.equal(`CREATE TABLE "my_table_name" ( + "id" uuid PRIMARY KEY );`); }); });