Code and documentation fixes

This commit is contained in:
Juan Ferrer 2020-11-15 19:24:25 +01:00
parent 6a42f2c887
commit 97a3d196f2
9 changed files with 173 additions and 125 deletions

View File

@ -1,63 +1,74 @@
# MyVC (MySQL Version Control) # MyVC (MySQL Version Control)
Utilities to ease the maintenance of MySQL database versioning using a Git Utilities to ease the maintenance of MySQL or MariaDB database versioning using
repository. 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 ## Prerequisites
Required applications. Required applications.
* Git
* Node.js = 12.17.0 LTS * Node.js = 12.17.0 LTS
* Git
* Docker * Docker
## Installation ## Installation
It's recommended to install the package globally. It's recommended to install the package globally.
``` ```text
# npm install -g myvc # 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 ## How to use
Export structure (uses production configuration). Execute *myvc* with the desired action.
``` ```text
$ myvc structure $ 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). Available actions are:
``` * **structure**: Export the database structure.
$ myvc fixtures * **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. Each action can have its own specific commandline options.
```
$ myvc routines [environment]
```
Apply changes into database.
```
$ myvc apply [-f] [-u] [environment]
```
## Basic information ## Basic information
Create database connection configuration files for each environment at main Create database connection configuration files for each environment at main
project folder using the standard MySQL parameters. The predefined environment project folder using the standard MySQL *.ini* parameters. The predefined
names are *production* and *testing*. environment names are *production* and *testing*.
``` ```text
db.[environment].ini db.[environment].ini
``` ```
Structure and fixture dumps are located inside *dump* folder. Structure and fixture dumps will be created inside *dump* folder.
* *structure.sql* * *structure.sql*
* *fixtures.sql* * *fixtures.sql*
* *fixtures.local.sql* * *fixtures.local.sql*
Routines are located inside *routines* folder. It includes procedures, ### Routines
functions, triggers, views and events with the following structure.
``` 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 routines
`- schema `- schema
|- events |- events
@ -72,10 +83,10 @@ functions, triggers, views and events with the following structure.
`- viewName.sql `- 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 changes
|- 00001-firstVersionCodeName |- 00001-firstVersionCodeName
| |- 00-firstExecutedScript.sql | |- 00-firstExecutedScript.sql

View File

@ -4,11 +4,11 @@ FORCE=FALSE
IS_USER=FALSE IS_USER=FALSE
usage() { usage() {
echo "[ERROR] Usage: $0 [-f] [-u] [environment]" echo "[ERROR] Usage: $0 [-f] [-u] [-e environment]"
exit 1 exit 1
} }
while getopts ":fu" option while getopts ":fue:" option
do do
case $option in case $option in
f) f)
@ -17,6 +17,9 @@ do
u) u)
IS_USER=TRUE IS_USER=TRUE
;; ;;
e)
ENV="$OPTARG"
;;
\?|:) \?|:)
usage usage
;; ;;
@ -24,12 +27,11 @@ do
done done
shift $(($OPTIND - 1)) shift $(($OPTIND - 1))
ENV=$1
CONFIG_FILE="myvc.config.json" CONFIG_FILE="myvc.config.json"
if [ ! -f "$CONFIG_FILE" ]; then if [ ! -f "$CONFIG_FILE" ]; then
echo "[ERROR] Config file not found in working directory." echo "[ERROR] Config file not found: $CONFIG_FILE"
exit 2 exit 2
fi fi
@ -69,12 +71,18 @@ else
fi fi
if [ ! -f "$INI_FILE" ]; then 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 exit 2
fi fi
echo "[INFO] Using config file: $INI_FILE" 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 # Query functions
dbQuery() { dbQuery() {
@ -107,7 +115,7 @@ echo "[INFO] -> Commit: $DB_COMMIT"
if [[ ! "$DB_VERSION" =~ ^[0-9]*$ ]]; then if [[ ! "$DB_VERSION" =~ ^[0-9]*$ ]]; then
echo "[ERROR] Wrong database version." echo "[ERROR] Wrong database version."
exit 3 exit 4
fi fi
if [[ -z "$DB_VERSION" ]]; then if [[ -z "$DB_VERSION" ]]; then
DB_VERSION=10000 DB_VERSION=10000
@ -232,7 +240,7 @@ applyRoutines() {
ACTION="DROP" ACTION="DROP"
fi fi
echo "[INFO] -> $ROUTINE_TYPE $ROUTINE_NAME: $ACTION" echo "[INFO] -> $ACTION: $ROUTINE_TYPE $ROUTINE_NAME"
if [ "$ACTION" == "REPLACE" ]; then if [ "$ACTION" == "REPLACE" ]; then
dbExecFromFile "$FILE_PATH" "$SCHEMA" dbExecFromFile "$FILE_PATH" "$SCHEMA"

View File

@ -2,7 +2,7 @@
const execFileSync = require('child_process').execFileSync; const execFileSync = require('child_process').execFileSync;
const spawn = require('child_process').spawn; const spawn = require('child_process').spawn;
module.exports = function(command) { module.exports = function(command, workdir, ...args) {
const buildArgs = [ const buildArgs = [
'build', 'build',
'-t', 'myvc/client', '-t', 'myvc/client',
@ -11,15 +11,15 @@ module.exports = function(command) {
]; ];
execFileSync('docker', buildArgs); execFileSync('docker', buildArgs);
let args = [ let runArgs = [
'run', 'run',
'-v', `${process.cwd()}:/workdir`, '-v', `${workdir}:/workdir`,
'myvc/client', 'myvc/client',
command command
]; ];
args = args.concat(process.argv.slice(2)); runArgs = runArgs.concat(args);
const child = spawn('docker', args, { const child = spawn('docker', runArgs, {
stdio: [ stdio: [
process.stdin, process.stdin,
process.stdout, process.stdout,

View File

@ -6,7 +6,7 @@ const path = require('path');
const serverImage = require(`${cwd}/myvc.config.json`).serverImage; const serverImage = require(`${cwd}/myvc.config.json`).serverImage;
module.exports = class Docker { module.exports = class Docker {
constructor(name) { constructor(name, context) {
Object.assign(this, { Object.assign(this, {
id: name, id: name,
name, name,
@ -16,7 +16,9 @@ module.exports = class Docker {
port: '3306', port: '3306',
username: 'root', username: 'root',
password: 'root' password: 'root'
} },
imageTag: name || 'myvc/dump',
context
}); });
} }
@ -35,7 +37,7 @@ module.exports = class Docker {
let d = new Date(); let d = new Date();
let pad = v => v < 10 ? '0' + v : v; let pad = v => v < 10 ? '0' + v : v;
let stamp = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; 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; let dockerArgs;
@ -50,7 +52,7 @@ module.exports = class Docker {
let runChown = process.platform != 'linux'; 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(); this.id = container.stdout.trim();
try { try {

View File

@ -1,14 +1,9 @@
#!/bin/bash #!/bin/bash
set -e set -e
CONFIG_FILE="myvc.config.json" CONFIG_FILE=$1
INI_FILE=$2
DUMP_FILE="dump/fixtures.sql" 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 "SELECT 1;" | mysql --defaults-file="$INI_FILE" >> /dev/null
echo "" > "$DUMP_FILE" echo "" > "$DUMP_FILE"

View File

@ -1,26 +1,18 @@
#!/usr/bin/node #!/usr/bin/node
const fs = require('fs-extra'); const fs = require('fs-extra');
const ini = require('ini');
const mysql = require('mysql2/promise'); const mysql = require('mysql2/promise');
const ejs = require('ejs'); 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 { class Exporter {
constructor(objectName, callback) { constructor(objectName, callback) {
this.objectName = objectName; this.objectName = objectName;
this.callback = callback; this.callback = callback;
this.dstDir = `${objectName}s`; this.dstDir = `${objectName}s`;
let templateDir = `${__dirname}/templates/${objectName}`; const templateDir = `${__dirname}/templates/${objectName}`;
this.query = fs.readFileSync(`${templateDir}.sql`, 'utf8'); 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); this.template = ejs.compile(templateFile);
if (fs.existsSync(`${templateDir}.js`)) if (fs.existsSync(`${templateDir}.js`))
@ -28,10 +20,10 @@ class Exporter {
} }
async export(conn, exportDir, schema) { 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; if (!res[0].length) return;
let routineDir = `${exportDir}/${schema}/${this.dstDir}`; const routineDir = `${exportDir}/${schema}/${this.dstDir}`;
if (!fs.existsSync(routineDir)) if (!fs.existsSync(routineDir))
fs.mkdirSync(routineDir); fs.mkdirSync(routineDir);
@ -46,7 +38,7 @@ class Exporter {
} }
} }
let exporters = [ const exporters = [
new Exporter('function'), new Exporter('function'),
new Exporter('procedure'), new Exporter('procedure'),
new Exporter('view'), new Exporter('view'),
@ -56,27 +48,10 @@ let exporters = [
// Exports objects for all schemas // Exports objects for all schemas
async function main() { module.exports = async function main(opts, config, dbConf) {
let ssl; const exportDir = `${opts.workdir}/routines`;
if (dbConf.ssl_ca) {
ssl = {
ca: fs.readFileSync(`${cwd}/${dbConf.ssl_ca}`),
rejectUnauthorized: dbConf.ssl_verify_server_cert != undefined
}
}
let conn = await mysql.createConnection({ const conn = await mysql.createConnection(dbConf);
host: !env ? 'localhost' : dbConf.host,
port: dbConf.port,
user: dbConf.user,
password: dbConf.password,
authPlugins: {
mysql_clear_password() {
return () => dbConf.password + '\0';
}
},
ssl
});
conn.queryFromFile = function(file, params) { conn.queryFromFile = function(file, params) {
return this.execute( return this.execute(
fs.readFileSync(`${file}.sql`, 'utf8'), fs.readFileSync(`${file}.sql`, 'utf8'),
@ -104,5 +79,4 @@ async function main() {
} finally { } finally {
await conn.end(); await conn.end();
} }
} };
main();

View File

@ -1,14 +1,9 @@
#!/bin/bash #!/bin/bash
set -e set -e
CONFIG_FILE="myvc.config.json" CONFIG_FILE=$1
INI_FILE=$2
DUMP_FILE="dump/structure.sql" 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") ) SCHEMAS=( $(jq -r ".structure[]" "$CONFIG_FILE") )

121
index.js
View File

@ -4,65 +4,128 @@ const getopts = require('getopts');
const package = require('./package.json'); const package = require('./package.json');
const dockerRun = require('./docker-run'); const dockerRun = require('./docker-run');
const fs = require('fs-extra'); 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: { alias: {
dir: 'd',
env: 'e', env: 'e',
workdir: 'w',
help: 'h', help: 'h',
version: 'v' version: 'v'
}, },
default: {} default: {
workdir: process.cwd(),
env: 'production'
}
}) })
if (opts.version)
process.exit(0);
function usage() { 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); process.exit(0);
} }
function error(message) {
if (options.help) usage(); console.error('Error:'.gray, message.red);
if (options.version) process.exit(0); process.exit(1);
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 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) { switch (action) {
case 'structure': case 'structure':
dockerRun('export-structure.sh'); dockerRun('export-structure.sh', opts.workdir, configFile, iniFile);
break; break;
case 'fixtures': case 'fixtures':
dockerRun('export-fixtures.sh'); dockerRun('export-fixtures.sh', opts.workdir, configFile, iniFile);
break; break;
case 'routines': case 'routines':
require('./export-routines'); require('./export-routines')(opts, config, dbConfig);
break; break;
case 'apply': case 'apply':
dockerRun('apply-changes.sh'); dockerRun('apply-changes.sh', opts.workdir, ...argv);
break; break;
case 'run': { case 'run': {
const Docker = require('./docker'); const Docker = require('./docker');
container = new Docker(); const container = new Docker(config.code, opts.workdir);
container.run(); container.run();
break; break;
} }
case 'start': { case 'start': {
const Docker = require('./docker'); const Docker = require('./docker');
container = new Docker(); const container = new Docker(config.code, opts.workdir);
container.start(); container.start();
break; break;
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "myvc", "name": "myvc",
"version": "1.0.3", "version": "1.0.4",
"author": "Verdnatura Levante SL", "author": "Verdnatura Levante SL",
"description": "MySQL Version Control", "description": "MySQL Version Control",
"license": "GPL-3.0", "license": "GPL-3.0",