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)
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

View File

@ -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"

View File

@ -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,

View File

@ -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 {

View File

@ -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"

View File

@ -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();
};

View File

@ -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") )

121
index.js
View File

@ -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;
}

View File

@ -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",