UNPKG

@whi/dbv

Version:

An agnostic database versioning tool designed for predictable upgrading/downgrading of database schema and content

419 lines (347 loc) 12.8 kB
#!/usr/bin/env node // -*- mode: javascript -*- const path = require('path'); const log = require('@whi/stdlog')(path.basename( __filename ), { level: process.env.DEBUG_LEVEL || 'fatal', }); const fs = require('fs'); const compareVersions = require('compare-versions'); const commander = require('commander'); const print = require('@whi/printf').colorAlways(); const prompter = require('@whi/prompter'); process.on('unhandledRejection', (reason, p) => { console.error( reason, p ); log.fatal('Unhandled Rejection at Promise %s for reason: %s', p, reason); process.exit(1); }); const cwd = process.cwd(); let db, config, context; async function loadConfig( configFile = './dbv-config.js' ) { configFile = configFile[0] === '/' ? configFile : cwd + '/' + configFile; log.debug("Loading config file: %s", configFile); config = require( configFile ); log.debug("Getting db context"); context = await config.context(); contextID = await config.contextID(); log.debug("Aquired context: %s", typeof context ); return configFile; } async function exit(n) { log.debug("Finished, running config.teardown"); config.teardown(); process.exit(n); } function versionSort(packA, packB) { return compareVersions( packA.version.name, packB.version.name ); } function getVersionPacks( startVersion, endVersion ) { log.info("Collecting vpacks for %s-%s", startVersion, endVersion); return new Promise(function (f,r) { let vpacks = []; fs.readdir( cwd + '/versions', (err, files) => { files.forEach(file => { if ( file.slice(-3) !== '.js' ) return; try { let [ v, ...name ] = file.split('-'); let module = require( cwd + '/versions/' + file ); let version = v.split('.').map( n => parseInt(n) ); name = name.join(' ').slice(0,-3); // remove '.js' if ( ( startVersion !== undefined && endVersion !== undefined ) && ( compareVersions( v, startVersion ) !== 1 || compareVersions( v, endVersion ) === 1 )) { log.debug("Skipping version %-8.8s (%s) because is outside range %s-%s", v, name, startVersion, endVersion); return; } vpacks.push({ name, description: module.description || null, path: cwd + '/versions/' + file, version: { name: v, major: version[0], minor: version[1], patch: version[2], }, module, }); } catch (err) { log.error("Failed to load vpack: %s", err); console.error( err ); } }); vpacks.sort( versionSort ); f( vpacks ); }); }); } async function main ( argv ) { function increaseVerbosity(v, total) { return total + 1; } function addRelative(v) { if ( v[0] !== '/' && v.slice(0,2) !== './' ) return './' + v; } async function runCommand(command, args, cmdopts, opts) { // Set logging verbosity for console transport log.transports[0].setLevel( opts.verbose ); if ( process.env.DEBUG_LEVEL ) print("Log level set to %d:%s", opts.verbose || 0, log.transports[0].level); const configPath = await loadConfig( opts.config ); print("Using context %s", contextID); await config.isInstalled(); // Handle install differently than other commands if ( command !== 'install' ) { log.debug("Check installed"); if ( ! await config.isInstalled() ) { print("dbv is not installed. Must run 'dbv install' first."); exit( 1 ); } } else { try { if ( await config.isInstalled() ) { print("dbv is already installed"); exit( 0 ); } await config.install(); print("Successfully installed dbv requirements based on config '%s'", configPath); } catch (err) { console.log( err ); print("Failed to install dbv using config '%s'", configPath); exit( 2 ); } exit( 0 ); } async function getCurrentVersion() { let cv; if ( opts.override ) { cv = opts.override; log.debug("Version override is set to %s", cv); } else { cv = await config.currentVersion(); log.debug("Current version is %s", cv ); } return cv; } log.debug("Running subcommand %s", command); let vpacks, version, currentVersion = await getCurrentVersion(); try { switch( command ) { case 'version': print("Current version is %s", currentVersion); break; case 'list': vpacks = await getVersionPacks(); print("Migration package list:"); for (var i=0; i < vpacks.length; i++ ) { let pack = vpacks[i]; print(" %-8.8s %s", pack.version.name, pack.name ); if ( typeof pack.description === 'string' ) print("%-12.12s - %s", "", pack.description ); } break; case 'update': version = args[0]; if ( ! await prompter.confirm("Are you sure you want to update the current version to "+version+"?") ) break; await config.setVersion( version ); print("Version is now set to %s", await config.currentVersion()); break; case 'uninstall': try { await config.uninstall(); print("Successfully uninstalled dbv requirements based on config '%s'", configPath); } catch (err) { console.log( err ); print("Failed to uninstall dbv using config '%s'", configPath); exit( 2 ); } break; case 'upgrade': dryRun = cmdopts.dryRun; version = args[0]; currentVersion = await getCurrentVersion(); if ( compareVersions( currentVersion, version ) !== -1 ) { print("Unable to upgrade. Current version (%s) is not lower than given version (%s)", currentVersion, version); break; } vpacks = await getVersionPacks( currentVersion, version ); if ( vpacks.length === 0 ) { print("No packages between versions %s-%s. Exiting with nothing to do", currentVersion, version); exit( 0 ); } print("Preview list of packages that will or will NOT run (packs: %d):", vpacks.length); for (var i=0; i < vpacks.length; i++ ) { let pack = vpacks[i]; let passed = await pack.module.check.call( pack, context ); print(" - %12.12s upgrade for package %-8.8s (%s)", !passed ? 'Will run' : 'Will NOT run', pack.version.name, pack.name ); } if ( !(dryRun || cmdopts.yes) ) if ( ! await prompter.confirm("Proceed with upgrade for context " + contextID + "?", "n") ) break; for (var i=0; i < vpacks.length; i++ ) { let pack = vpacks[i]; let passed = await pack.module.check.call( pack, context ); log.debug("Version %-10.10s result: %5.5s (dry run: %s)", pack.version.name, passed, dryRun); if ( ! passed ) { if ( dryRun ) print("Would have run upgrade for version %-8.8s (%s)", pack.version.name, pack.name); else { try { log.info("Running upgrade %s", pack.version.name); await pack.module.upgrade.call( pack, context ); passed = await pack.module.check.call( pack, context ); if ( !passed ) { print(" Failed to upgrade to version %-8.8s (%s)", pack.version.name, pack.name) throw new Error("Failed to pass pack.check(). Either your upgrade is incomplete or your check is misconfigured."); } print("Successfully upgraded to version %-8.8s (%s)", pack.version.name, pack.name); if ( config.packComplete ) await config.packComplete( pack, 'upgrade', pack.version.name ); await config.setVersion( pack.version.name ); } catch (err) { console.log( err ); log.fatal("Upgrade failed, running downgrade to rollback changes"); await pack.module.downgrade.call( pack, context ); } } } } print("Version is now set to %s", await config.currentVersion()); break; case 'downgrade': dryRun = cmdopts.dryRun; version = args[0]; currentVersion = await getCurrentVersion(); if ( compareVersions( currentVersion, version ) !== 1 ) { print("Unable to downgrade. Current version (%s) is not higher than given version (%s)", currentVersion, version); break; } vpacks = await getVersionPacks( version, currentVersion ); if ( vpacks.length === 0 ) { print("No packages between versions %s-%s. Exiting with nothing to do", version, currentVersion ); exit( 0 ); } print("Preview list of packages that will or will NOT run (packs: %d):", vpacks.length); for (var i=vpacks.length-1; i >= 0; i-- ) { let pack = vpacks[i]; let passed = await pack.module.check.call( pack, context ); print(" - %12.12s downgrade for package %-8.8s (%s)", passed ? 'Will run' : 'Will NOT run', pack.version.name, pack.name ); } if ( !(dryRun || cmdopts.yes) ) if ( ! await prompter.confirm("Proceed with downgrade for context " + contextID + "?", "n") ) break; for (var i=vpacks.length-1; i >= 0; i-- ) { let pack = vpacks[i]; let passed = await pack.module.check.call( pack, context ); log.debug("Version %-10.10s result: %5.5s (dry run: %s)", pack.version.name, passed, dryRun); if ( passed ) { if ( dryRun ) print("Would have run downgrade for version %-8.8s (%s)", pack.version.name, pack.name); else { await pack.module.downgrade.call( pack, context ); passed = await pack.module.check.call( pack, context ); if ( passed ) { print(" Failed to downgrade version %-8.8s (%s)", pack.version.name, pack.name) throw new Error("Failed to pass pack.check(). Either your downgrade is incomplete or your check is misconfigured."); } print("Successfully downgraded version %-8.8s (%s)", pack.version.name, pack.name); let newVersion = vpacks[i-1] ? vpacks[i-1].version.name : version; if ( config.packComplete ) await config.packComplete( pack, 'downgrade', newVersion ); await config.setVersion( newVersion ); } } } print("Version is now set to %s", await config.currentVersion()); break; } log.silly("%s", JSON.stringify( vpacks, null, 4 ) ); } catch (err) { console.error( err ); exit( 1 ); } exit( 0 ); } commander .version('1.0.0') .option('-v, --verbose', 'Increase logging verbosity', increaseVerbosity, 0) .option('-c, --config [path]', 'Configuration file for database connection', addRelative) .option('-o, --override [version]', 'Manually override the current version'); commander .command('version') .description("Get the database current version") .action(async function () { await runCommand('version', [], this, this.parent); }); commander .command('list') .description("List available migration scripts") .action(async function () { await runCommand('list', [], this, this.parent); }); commander .command('update <version>') .description("Update current version to given version") .action(async function ( version ) { await runCommand('update', [ version ], this, this.parent); }); commander .command('install') .description("Install version tracking requirements. Calls 'config.install'") .action(async function () { await runCommand('install', [], this, this.parent); }); commander .command('uninstall') .description("Uninstall version tracking requirements. Calls 'config.uninstall'") .action(async function () { await runCommand('uninstall', [], this, this.parent); }); commander .command('upgrade <version>') .option('-n, --dry-run', 'See which versions would have been run') .option('-y, --yes', 'Answer yes to all prompts') .description("Run upgrade scripts between current version and given version") .action(async function ( version ) { await runCommand('upgrade', [ version ], this, this.parent); }); commander .command('downgrade <version>') .option('-n, --dry-run', 'See which versions would have been run') .option('-y, --yes', 'Answer yes to all prompts') .description("Run downgrade scripts between current version and given version") .action(async function ( version ) { await runCommand('downgrade', [ version ], this, this.parent); }); commander.parse( argv ); // console.log( commander ); function help_and_exit() { commander.help(); exit(); } // Catch undefined subcommands if ( typeof commander.args[commander.args.length-1] === 'string' ) { print( `Error: Unknown subcommand '${commander.args[0]}'` ); help_and_exit() } // Display help and exit if no subcommands where given // if ( commander.args.length === 0 ) { // print( `Error: no input` ) // help_and_exit() // } } if ( typeof require != 'undefined' && require.main == module ) { main( process.argv ); } else { module.exports = main; }