From 97a3d196f20f0e337ed2d0cc45cb4a9a28e68f8c Mon Sep 17 00:00:00 2001 From: Juan Ferrer Toribio Date: Sun, 15 Nov 2020 19:24:25 +0100 Subject: [PATCH] Code and documentation fixes --- README.md | 71 +++++++++++++++----------- apply-changes.sh | 22 +++++--- docker-run.js | 10 ++-- docker.js | 10 ++-- export-fixtures.sh | 9 +--- export-routines.js | 44 ++++------------ export-structure.sh | 9 +--- index.js | 121 +++++++++++++++++++++++++++++++++----------- package.json | 2 +- 9 files changed, 173 insertions(+), 125 deletions(-) diff --git a/README.md b/README.md index 338db7e..210efe7 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,74 @@ # MyVC (MySQL Version Control) -Utilities to ease the maintenance of MySQL database versioning using a Git -repository. +Utilities to ease the maintenance of MySQL or MariaDB database versioning using +a Git repository. + +This project is just to bring an idea to life and is still in an early stage of +development, so it may not be fully functional. + +Any help is welcomed! Feel free to contribute. ## Prerequisites Required applications. -* Git * Node.js = 12.17.0 LTS +* Git * Docker ## Installation It's recommended to install the package globally. -``` +```text # npm install -g myvc ``` +You can also install locally and use the *npx* command to execute it. +```text +$ npm install myvc +$ npx myvc [action] +``` + ## How to use -Export structure (uses production configuration). -``` -$ myvc structure +Execute *myvc* with the desired action. +```text +$ myvc [-w|--workdir] [-e|--env] [-h|--help] action ``` +The default working directory is the current one and unless otherwise indicated, +the default environment is *production*. -Export fixtures (uses production configuration). -``` -$ myvc fixtures -``` +Available actions are: + * **structure**: Export the database structure. + * **fixtures**: Export the database structure. + * **routines**: Export database routines. + * **apply**: Apply changes into database, uses *local* environment by default. + * **run**: Builds and starts local database server container. + * **start**: Starts local database server container. -Export routines. -``` -$ myvc routines [environment] -``` - -Apply changes into database. -``` -$ myvc apply [-f] [-u] [environment] -``` +Each action can have its own specific commandline options. ## Basic information Create database connection configuration files for each environment at main -project folder using the standard MySQL parameters. The predefined environment -names are *production* and *testing*. -``` +project folder using the standard MySQL *.ini* parameters. The predefined +environment names are *production* and *testing*. +```text db.[environment].ini ``` -Structure and fixture dumps are located inside *dump* folder. +Structure and fixture dumps will be created inside *dump* folder. * *structure.sql* * *fixtures.sql* * *fixtures.local.sql* -Routines are located inside *routines* folder. It includes procedures, -functions, triggers, views and events with the following structure. -``` +### Routines + +Routines should be placed inside *routines* folder. All objects that have +PL/SQL code are considered routines. It includes functions, triggers, views and +events with the following structure. +```text routines `- schema |- events @@ -72,10 +83,10 @@ functions, triggers, views and events with the following structure. `- viewName.sql ``` -## Versions +### Versions -Place your versions inside *changes* folder with the following structure. -``` +Versions should be placed inside *changes* folder with the following structure. +```text changes |- 00001-firstVersionCodeName | |- 00-firstExecutedScript.sql diff --git a/apply-changes.sh b/apply-changes.sh index 2e01d02..b72fb0b 100755 --- a/apply-changes.sh +++ b/apply-changes.sh @@ -4,11 +4,11 @@ FORCE=FALSE IS_USER=FALSE usage() { - echo "[ERROR] Usage: $0 [-f] [-u] [environment]" + echo "[ERROR] Usage: $0 [-f] [-u] [-e environment]" exit 1 } -while getopts ":fu" option +while getopts ":fue:" option do case $option in f) @@ -17,6 +17,9 @@ do u) IS_USER=TRUE ;; + e) + ENV="$OPTARG" + ;; \?|:) usage ;; @@ -24,12 +27,11 @@ do done shift $(($OPTIND - 1)) -ENV=$1 CONFIG_FILE="myvc.config.json" if [ ! -f "$CONFIG_FILE" ]; then - echo "[ERROR] Config file not found in working directory." + echo "[ERROR] Config file not found: $CONFIG_FILE" exit 2 fi @@ -69,12 +71,18 @@ else fi if [ ! -f "$INI_FILE" ]; then - echo "[ERROR] DB config file doesn't exists: $INI_FILE" + echo "[ERROR] Database config file not found: $INI_FILE" exit 2 fi echo "[INFO] Using config file: $INI_FILE" +echo "SELECT 1;" | mysql --defaults-file="$INI_FILE" >> /dev/null + +if [ "$?" -ne "0" ]; then + exit 3 +fi + # Query functions dbQuery() { @@ -107,7 +115,7 @@ echo "[INFO] -> Commit: $DB_COMMIT" if [[ ! "$DB_VERSION" =~ ^[0-9]*$ ]]; then echo "[ERROR] Wrong database version." - exit 3 + exit 4 fi if [[ -z "$DB_VERSION" ]]; then DB_VERSION=10000 @@ -232,7 +240,7 @@ applyRoutines() { ACTION="DROP" fi - echo "[INFO] -> $ROUTINE_TYPE $ROUTINE_NAME: $ACTION" + echo "[INFO] -> $ACTION: $ROUTINE_TYPE $ROUTINE_NAME" if [ "$ACTION" == "REPLACE" ]; then dbExecFromFile "$FILE_PATH" "$SCHEMA" diff --git a/docker-run.js b/docker-run.js index 2b59fa7..aee5711 100644 --- a/docker-run.js +++ b/docker-run.js @@ -2,7 +2,7 @@ const execFileSync = require('child_process').execFileSync; const spawn = require('child_process').spawn; -module.exports = function(command) { +module.exports = function(command, workdir, ...args) { const buildArgs = [ 'build', '-t', 'myvc/client', @@ -11,15 +11,15 @@ module.exports = function(command) { ]; execFileSync('docker', buildArgs); - let args = [ + let runArgs = [ 'run', - '-v', `${process.cwd()}:/workdir`, + '-v', `${workdir}:/workdir`, 'myvc/client', command ]; - args = args.concat(process.argv.slice(2)); + runArgs = runArgs.concat(args); - const child = spawn('docker', args, { + const child = spawn('docker', runArgs, { stdio: [ process.stdin, process.stdout, diff --git a/docker.js b/docker.js index 4c44a83..8c51e79 100644 --- a/docker.js +++ b/docker.js @@ -6,7 +6,7 @@ const path = require('path'); const serverImage = require(`${cwd}/myvc.config.json`).serverImage; module.exports = class Docker { - constructor(name) { + constructor(name, context) { Object.assign(this, { id: name, name, @@ -16,7 +16,9 @@ module.exports = class Docker { port: '3306', username: 'root', password: 'root' - } + }, + imageTag: name || 'myvc/dump', + context }); } @@ -35,7 +37,7 @@ module.exports = class Docker { let d = new Date(); let pad = v => v < 10 ? '0' + v : v; let stamp = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; - await this.execP(`docker build --build-arg STAMP=${stamp} -f ${dockerfilePath}.dump -t ${serverImage} ${cwd}`); + await this.execP(`docker build --build-arg STAMP=${stamp} -f ${dockerfilePath}.dump -t ${this.serverImage} ${this.context}`); let dockerArgs; @@ -50,7 +52,7 @@ module.exports = class Docker { let runChown = process.platform != 'linux'; - const container = await this.execP(`docker run --env RUN_CHOWN=${runChown} -d ${dockerArgs} ${serverImage}`); + const container = await this.execP(`docker run --env RUN_CHOWN=${runChown} -d ${dockerArgs} ${this.serverImage}`); this.id = container.stdout.trim(); try { diff --git a/export-fixtures.sh b/export-fixtures.sh index 4123e38..e94ea67 100755 --- a/export-fixtures.sh +++ b/export-fixtures.sh @@ -1,14 +1,9 @@ #!/bin/bash set -e -CONFIG_FILE="myvc.config.json" +CONFIG_FILE=$1 +INI_FILE=$2 DUMP_FILE="dump/fixtures.sql" -INI_FILE="db.production.ini" - -if [ ! -f "$CONFIG_FILE" ]; then - echo "Config file not found in working directory." - exit 1 -fi echo "SELECT 1;" | mysql --defaults-file="$INI_FILE" >> /dev/null echo "" > "$DUMP_FILE" diff --git a/export-routines.js b/export-routines.js index 9b103f7..5de138f 100755 --- a/export-routines.js +++ b/export-routines.js @@ -1,26 +1,18 @@ #!/usr/bin/node const fs = require('fs-extra'); -const ini = require('ini'); const mysql = require('mysql2/promise'); const ejs = require('ejs'); -let cwd = process.cwd(); -let env = process.argv[2]; -let iniFile = env ? `db.${env}.ini` : `${__dirname}/db.ini`; -let dbConf = ini.parse(fs.readFileSync(iniFile, 'utf8')).client; -let exportDir = `${cwd}/routines`; -let config = require(`${cwd}/myvc.config.json`); - class Exporter { constructor(objectName, callback) { this.objectName = objectName; this.callback = callback; this.dstDir = `${objectName}s`; - let templateDir = `${__dirname}/templates/${objectName}`; + const templateDir = `${__dirname}/templates/${objectName}`; this.query = fs.readFileSync(`${templateDir}.sql`, 'utf8'); - let templateFile = fs.readFileSync(`${templateDir}.ejs`, 'utf8'); + const templateFile = fs.readFileSync(`${templateDir}.ejs`, 'utf8'); this.template = ejs.compile(templateFile); if (fs.existsSync(`${templateDir}.js`)) @@ -28,10 +20,10 @@ class Exporter { } async export(conn, exportDir, schema) { - let res = await conn.execute(this.query, [schema]); + const res = await conn.execute(this.query, [schema]); if (!res[0].length) return; - let routineDir = `${exportDir}/${schema}/${this.dstDir}`; + const routineDir = `${exportDir}/${schema}/${this.dstDir}`; if (!fs.existsSync(routineDir)) fs.mkdirSync(routineDir); @@ -46,7 +38,7 @@ class Exporter { } } -let exporters = [ +const exporters = [ new Exporter('function'), new Exporter('procedure'), new Exporter('view'), @@ -56,27 +48,10 @@ let exporters = [ // Exports objects for all schemas -async function main() { - let ssl; - if (dbConf.ssl_ca) { - ssl = { - ca: fs.readFileSync(`${cwd}/${dbConf.ssl_ca}`), - rejectUnauthorized: dbConf.ssl_verify_server_cert != undefined - } - } +module.exports = async function main(opts, config, dbConf) { + const exportDir = `${opts.workdir}/routines`; - let conn = await mysql.createConnection({ - host: !env ? 'localhost' : dbConf.host, - port: dbConf.port, - user: dbConf.user, - password: dbConf.password, - authPlugins: { - mysql_clear_password() { - return () => dbConf.password + '\0'; - } - }, - ssl - }); + const conn = await mysql.createConnection(dbConf); conn.queryFromFile = function(file, params) { return this.execute( fs.readFileSync(`${file}.sql`, 'utf8'), @@ -104,5 +79,4 @@ async function main() { } finally { await conn.end(); } -} -main(); +}; diff --git a/export-structure.sh b/export-structure.sh index 41a55d7..19bc5d6 100755 --- a/export-structure.sh +++ b/export-structure.sh @@ -1,14 +1,9 @@ #!/bin/bash set -e -CONFIG_FILE="myvc.config.json" +CONFIG_FILE=$1 +INI_FILE=$2 DUMP_FILE="dump/structure.sql" -INI_FILE="db.production.ini" - -if [ ! -f "$CONFIG_FILE" ]; then - echo "Config file found in working directory." - exit 1 -fi SCHEMAS=( $(jq -r ".structure[]" "$CONFIG_FILE") ) diff --git a/index.js b/index.js index ba63c7a..93bbbdb 100644 --- a/index.js +++ b/index.js @@ -4,65 +4,128 @@ const getopts = require('getopts'); const package = require('./package.json'); const dockerRun = require('./docker-run'); const fs = require('fs-extra'); +const path = require('path'); +const ini = require('ini'); -console.log('MyVC (MySQL Version Control)'.green, `v${package.version}`.blue); +console.log('MyVC (MySQL Version Control)'.green, `v${package.version}`.magenta); -const options = getopts(process.argv.slice(2), { +const argv = process.argv.slice(2); +const opts = getopts(argv, { alias: { - dir: 'd', env: 'e', + workdir: 'w', help: 'h', version: 'v' }, - default: {} + default: { + workdir: process.cwd(), + env: 'production' + } }) +if (opts.version) + process.exit(0); + function usage() { - console.log('Usage:'.gray, 'myvc [-d|--dir] [-e|--env] [-h|--help] action'.magenta); + console.log('Usage:'.gray, 'myvc [-w|--workdir] [-e|--env] [-h|--help] action'.magenta); process.exit(0); } - -if (options.help) usage(); -if (options.version) process.exit(0); - -let config; -let container; - -let action = options._[0]; -if (action) { - console.log('Action:'.gray, action.magenta); - - const configFile = 'myvc.config.json'; - if (!fs.existsSync(configFile)) { - console.error('Error:'.gray, `Config file '${configFile}' not found in working directory`.red); - process.exit(1); - } - - config = require(`${process.cwd()}/${configFile}`); +function error(message) { + console.error('Error:'.gray, message.red); + process.exit(1); } +function parameter(parameter, value) { + console.log(parameter.gray, value.blue); +} + +const action = opts._[0]; +if (!action) usage(); + +const actionArgs = { + apply: { + alias: { + force: 'f', + user: 'u' + }, + default: { + force: false, + user: false, + env: 'test' + } + } +}; +const actionOpts = getopts(argv, actionArgs[action]); +Object.assign(opts, actionOpts); + +parameter('Environment:', opts.env); +parameter('Workdir:', opts.workdir); +parameter('Action:', action); + +// Configuration file + +const configFile = 'myvc.config.json'; +const configPath = path.join(opts.workdir, configFile); +if (!fs.existsSync(configPath)) + error(`Config file not found: ${configFile}`); +const config = require(configPath); + +// Database configuration + +let iniFile = 'db.ini'; +let iniDir = __dirname; +if (opts.env) { + iniFile = `db.${opts.env}.ini`; + iniDir = opts.workdir; +} +const iniPath = path.join(iniDir, iniFile); + +if (!fs.existsSync(iniPath)) + error(`Database config file not found: ${iniFile}`); + +const iniConfig = ini.parse(fs.readFileSync(iniPath, 'utf8')).client; +const dbConfig = { + host: !opts.env ? 'localhost' : iniConfig.host, + port: iniConfig.port, + user: iniConfig.user, + password: iniConfig.password, + authPlugins: { + mysql_clear_password() { + return () => iniConfig.password + '\0'; + } + } +}; + +if (iniConfig.ssl_ca) { + dbConfig.ssl = { + ca: fs.readFileSync(`${opts.workdir}/${iniConfig.ssl_ca}`), + rejectUnauthorized: iniConfig.ssl_verify_server_cert != undefined + } +} + +// Actions switch (action) { case 'structure': - dockerRun('export-structure.sh'); + dockerRun('export-structure.sh', opts.workdir, configFile, iniFile); break; case 'fixtures': - dockerRun('export-fixtures.sh'); + dockerRun('export-fixtures.sh', opts.workdir, configFile, iniFile); break; case 'routines': - require('./export-routines'); + require('./export-routines')(opts, config, dbConfig); break; case 'apply': - dockerRun('apply-changes.sh'); + dockerRun('apply-changes.sh', opts.workdir, ...argv); break; case 'run': { const Docker = require('./docker'); - container = new Docker(); + const container = new Docker(config.code, opts.workdir); container.run(); break; } case 'start': { const Docker = require('./docker'); - container = new Docker(); + const container = new Docker(config.code, opts.workdir); container.start(); break; } diff --git a/package.json b/package.json index 96348c8..00845a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "myvc", - "version": "1.0.3", + "version": "1.0.4", "author": "Verdnatura Levante SL", "description": "MySQL Version Control", "license": "GPL-3.0",