Help improved, exclusive push lock , check version, secure priv restore, README

This commit is contained in:
Juan Ferrer 2021-10-25 15:38:07 +02:00
parent 801b6e9539
commit 381ada4411
14 changed files with 357 additions and 159 deletions

View File

@ -33,7 +33,7 @@ $ npx myvc [command]
Execute *myvc* with the desired command.
```text
$ myvc [-w|--workspace] [-r|--remote] [-d|--debug] [-h|--help] command [args]
$ [npx] myvc [-w|--workspace <string>] [-r|--remote <string>] [-d|--debug] [-h|--help] <command> [<args>]
```
The default workspace directory is the current working directory and unless
@ -48,8 +48,8 @@ Database versioning commands:
Local server management commands:
* **dump**: Export database structure and fixtures from *production*.
* **run**: Build and starts local database server container.
* **dump**: Export database structure and fixtures.
* **run**: Build and start local database server container.
* **start**: Start local database server container.
Each command can have its own specific commandline options.
@ -72,32 +72,26 @@ Incorporates database routine changes into workspace.
$ myvc pull [remote] [-f|--force] [-c|--checkout]
```
When *checkout* option is provided, it does the following steps:
When *checkout* option is provided, it does the following before export:
1. Saves the current HEAD.
2. Check out to the last database push commit (saved in versioning tables).
3. Creates and checkout to a new branch.
4. Exports database routines.
5. Commits the new changes.
6. Checkout to the original HEAD.
7. Merge the new branch into.
8. Let the user deal with merge conflicts.
1. Get the last database push commit (saved in versioning tables).
2. Creates and checkout to a new branch based in database commit.
### push
Applies versions and routine changes into database.
```text
$ myvc push [remote] [-f|--force] [-u|--user]
$ myvc push [<remote>] [-f|--force] [-u|--user]
```
### version
Creates a new version folder, when name is not specified it generates a random
name mixing color with plant name.
name mixing a color with a plant name.
```text
$ myvc version [name]
$ myvc version [<name>]
```
## Local server commands
@ -105,10 +99,10 @@ $ myvc version [name]
### dump
Exports database structure and fixtures from remote into hidden files located
in *dump* folder.
in *dump* folder. If no remote is specified *production* is used.
```text
$ myvc dump [remote]
$ myvc dump [<remote>]
```
### run
@ -134,7 +128,7 @@ $ myvc start
## Basic information
First of all you have to initalize your workspace.
First of all you have to initalize the workspace.
```text
$ myvc init
@ -156,9 +150,9 @@ remotes/[remote].ini
### Routines
Routines should be placed inside *routines* folder. All objects that have
PL/SQL code are considered routines. It includes events, functions, procedures,
triggers and views with the following structure.
Routines are placed inside *routines* folder. All objects that have PL/SQL code
are considered routines. It includes events, functions, procedures, triggers
and views with the following structure.
```text
routines
@ -193,10 +187,11 @@ Don't place your PL/SQL objects here, use the routines folder!
### Local server
The local server is created as a MariaDB Docker container using the base dump created with the *dump* command plus pushing local versions and changed
The local server is created as a MariaDB Docker container using the base dump
created with the *dump* command plus pushing local versions and changed
routines.
## Dumps
### Dumps
You can create your local fixture and structure files.
@ -206,18 +201,18 @@ You can create your local fixture and structure files.
## Why
The main reason for starting this project it's because there are no fully free
and opensource migration tools available that allow versioning database routines
with an standard CVS system as if they were normal application code.
and open source migration tools available that allow versioning database
routines with an standard CVS system as if they were normal application code.
Also, the existing tools are too complex and require too much knowledge to start
a small project.
Also, the existing tools are too complex and require too much knowledge to
start a small project.
## Todo
* Improve *help* option for commands.
* Allow to specify a custom workspace subdirectory inside project directory.
* Create a lock during push to avoid collisions.
* Update routines shasum when push.
* Use a custom *Dockerfile* for local database container.
* Console logging via events.
* Lock version table row when pushing.
## Built With

View File

@ -1,5 +1,6 @@
const spawn = require('child_process').spawn;
const execFile = require('child_process').execFile;
const camelToSnake = require('./lib').camelToSnake;
const docker = {
async run(image, commandArgs, options, execOptions) {
@ -120,9 +121,5 @@ class Container {
}
}
function camelToSnake(str) {
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
}
module.exports = docker;
module.exports.Container = Container;

6
lib.js Normal file
View File

@ -0,0 +1,6 @@
function camelToSnake(str) {
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
}
module.exports.camelToSnake = camelToSnake;

View File

@ -4,13 +4,16 @@ const fs = require('fs-extra');
const path = require('path');
const docker = require('./docker');
/**
* Dumps structure and fixtures from remote.
*/
class Dump {
get usage() {
return {
description: 'Dumps structure and fixtures from remote',
operand: 'remote'
};
}
get localOpts() {
return {
operand: 'remote',
default: {
remote: 'production'
}
@ -20,7 +23,7 @@ class Dump {
async run(myvc, opts) {
const conn = await myvc.dbConnect();
const dumpDir = `${opts.workspace}/dump`;
const dumpDir = `${opts.myvcDir}/dump`;
if (!await fs.pathExists(dumpDir))
await fs.mkdir(dumpDir);
@ -82,7 +85,7 @@ class Dump {
async dockerRun(command, args, execOptions) {
const commandArgs = [command].concat(args);
await docker.run('myvc/client', commandArgs, {
volume: `${this.opts.workspace}:/workspace`,
volume: `${this.opts.myvcDir}:/workspace`,
rm: true
}, execOptions);
}

View File

@ -3,11 +3,17 @@ const MyVC = require('./myvc');
const fs = require('fs-extra');
class Init {
get usage() {
return {
description: 'Initialize an empty workspace'
};
}
async run(myvc, opts) {
const templateDir = `${__dirname}/template`;
const templates = await fs.readdir(templateDir);
for (let template of templates) {
const dst = `${opts.workspace}/${template}`;
const dst = `${opts.myvcDir}/${template}`;
if (!await fs.pathExists(dst))
await fs.copy(`${templateDir}/${template}`, dst);
}

View File

@ -6,10 +6,20 @@ const shajs = require('sha.js');
const nodegit = require('nodegit');
class Pull {
get usage() {
return {
description: 'Incorporate database routine changes into workspace',
params: {
force: 'Do it even if there are local changes',
checkout: 'Move to same database commit before pull'
},
operand: 'remote'
};
}
get localOpts() {
return {
operand: 'remote',
alias: {
boolean: {
force: 'f',
checkout: 'c'
}
@ -80,7 +90,7 @@ class Pull {
for (const exporter of exporters)
await exporter.init();
const exportDir = `${opts.workspace}/routines`;
const exportDir = `${opts.myvcDir}/routines`;
if (!await fs.pathExists(exportDir))
await fs.mkdir(exportDir);
@ -88,7 +98,7 @@ class Pull {
let newShaSums = {};
let oldShaSums;
const shaFile = `${opts.workspace}/.shasums.json`;
const shaFile = `${opts.myvcDir}/.shasums.json`;
if (await fs.pathExists(shaFile))
oldShaSums = JSON.parse(await fs.readFile(shaFile, 'utf8'));

View File

@ -5,15 +5,22 @@ const nodegit = require('nodegit');
/**
* Pushes changes to remote.
*
* @property {Boolean} force Answer yes to all questions
* @property {Boolean} user Whether to change current user version
*/
class Push {
get usage() {
return {
description: 'Apply changes into database',
params: {
force: 'Answer yes to all questions',
user: 'Update the user version instead, for shared databases'
},
operand: 'remote'
};
}
get localOpts() {
return {
operand: 'remote',
alias: {
boolean: {
force: 'f',
user: 'u'
}
@ -24,6 +31,25 @@ class Push {
const conn = await myvc.dbConnect();
this.conn = conn;
// Obtain exclusive lock
const [[row]] = await conn.query(
`SELECT GET_LOCK('myvc_push', 30) getLock`);
if (!row.getLock) {
let isUsed = 0;
if (row.getLock == 0) {
const [[row]] = await conn.query(
`SELECT IS_USED_LOCK('myvc_push') isUsed`);
isUsed = row.isUsed;
}
throw new Error(`Cannot obtain exclusive lock, used by connection ${isUsed}`);
}
// Get database version
const version = await myvc.fetchDbVersion() || {};
console.log(
@ -33,7 +59,7 @@ class Push {
);
if (!version.number)
version.number = '00000';
version.number = String('0').padStart(opts.versionDigits, '0');
if (!/^[0-9]*$/.test(version.number))
throw new Error('Wrong database version');
@ -59,6 +85,8 @@ class Push {
version.gitCommit = userVersion.gitCommit;
}
// Prevent push to production by mistake
if (opts.remote == 'production') {
console.log(
'\n ( ( ) ( ( ) ) '
@ -88,11 +116,13 @@ class Push {
}
}
// Apply versions
console.log('Applying versions.');
const pushConn = await myvc.createConnection();
let nChanges = 0;
const versionsDir = `${opts.workspace}/versions`;
const versionsDir = `${opts.myvcDir}/versions`;
function logVersion(type, version, name) {
console.log('', type.bold, `[${version.bold}]`, name);
@ -105,21 +135,25 @@ class Push {
if (versionDir == 'README.md')
continue;
const match = versionDir.match(/^([0-9]{5})-([a-zA-Z0-9]+)?$/);
const match = versionDir.match(/^([0-9])-([a-zA-Z0-9]+)?$/);
if (!match) {
logVersion('[W]'.yellow, '?????', versionDir);
continue;
}
const dirVersion = match[1];
const versionNumber = match[1];
const versionName = match[2];
if (version.number >= dirVersion) {
logVersion('[I]'.blue, dirVersion, versionName);
if (versionNumber.length != version.number.length) {
logVersion('[W]'.yellow, '*****', versionDir);
continue;
}
if (version.number >= versionNumber) {
logVersion('[I]'.blue, versionNumber, versionName);
continue;
}
logVersion('[+]'.green, dirVersion, versionName);
logVersion('[+]'.green, versionNumber, versionName);
const scriptsDir = `${versionsDir}/${versionDir}`;
const scripts = await fs.readdir(scriptsDir);
@ -134,10 +168,12 @@ class Push {
nChanges++;
}
await this.updateVersion(nChanges, 'number', dirVersion);
await this.updateVersion(nChanges, 'number', versionNumber);
}
}
// Apply routines
console.log('Applying changed routines.');
let nRoutines = 0;
@ -146,12 +182,6 @@ class Push {
: await myvc.cachedChanges();
changes = this.parseChanges(changes);
await conn.query(
`CREATE TEMPORARY TABLE tProcsPriv
ENGINE = MEMORY
SELECT * FROM mysql.procs_priv LIMIT 0`
);
const routines = [];
for (const change of changes)
if (change.isRoutine)
@ -171,19 +201,32 @@ class Push {
}
for (const change of changes) {
const fullPath = `${opts.workspace}/routines/${change.path}.sql`;
const fullPath = `${opts.myvcDir}/routines/${change.path}.sql`;
const exists = await fs.pathExists(fullPath);
const actionMsg = exists ? '[+]'.green : '[-]'.red;
const typeMsg = `[${change.type.abbr}]`[change.type.color];
console.log('', actionMsg.bold, typeMsg.bold, change.fullName);
try {
const scapedSchema = pushConn.escapeId(change.schema, true);
if (exists) {
if (change.type.name = 'VIEW')
await pushConn.query(`USE ${scapedSchema}`);
await this.queryFromFile(pushConn, `routines/${change.path}.sql`);
if (change.isRoutine) {
await conn.query(
`INSERT IGNORE INTO mysql.procs_priv
SELECT * FROM tProcsPriv
WHERE Db = ?
AND Routine_name = ?
AND Routine_type = ?`,
[change.schema, change.name, change.type.name]
);
}
} else {
const escapedName =
scapedSchema + '.' +
@ -203,20 +246,15 @@ class Push {
}
if (routines.length) {
await conn.query(
`INSERT IGNORE INTO mysql.procs_priv
SELECT * FROM tProcsPriv`
);
await conn.query(
`DROP TEMPORARY TABLE tProcsPriv`
);
await conn.query(`DROP TEMPORARY TABLE tProcsPriv`);
await conn.query('FLUSH PRIVILEGES');
}
// Update and release
await pushConn.end();
if (nRoutines > 0) {
await conn.query('FLUSH PRIVILEGES');
console.log(` -> ${nRoutines} routines have changed.`);
} else
console.log(` -> No routines changed.`);
@ -226,6 +264,8 @@ class Push {
if (version.gitCommit !== head.sha())
await this.updateVersion(nRoutines, 'gitCommit', head.sha());
await conn.query(`DO RELEASE_LOCK('myvc_push')`);
}
parseChanges(changes) {
@ -395,6 +435,11 @@ const typeMap = {
},
};
const routineTypes = new Set([
'FUNCTION',
'PROCEDURE'
]);
class Routine {
constructor(change) {
const path = change.path;
@ -411,7 +456,7 @@ class Routine {
schema,
name,
fullName: `${schema}.${name}`,
isRoutine: ['FUNC', 'PROC'].indexOf(type.abbr) !== -1
isRoutine: routineTypes.has(type.abbr)
});
}
}

View File

@ -11,14 +11,21 @@ const Server = require('./server/server');
* image when fixtures have been modified or when the day on which the
* image was built is different to today. Some workarounds have been used
* to avoid a bug with OverlayFS driver on MacOS.
*
* @property {Boolean} ci Continuous integration environment
* @property {Boolean} random Whether to use a random container name
*/
class Run {
get usage() {
return {
description: 'Build and start local database server container',
params: {
ci: 'Workaround for continuous integration system',
random: 'Whether to use a random container name or port'
}
};
}
get localOpts() {
return {
alias: {
boolean: {
ci: 'c',
random: 'r'
}
@ -26,7 +33,7 @@ class Run {
}
async run(myvc, opts) {
const dumpDir = `${opts.workspace}/dump`;
const dumpDir = `${opts.myvcDir}/dump`;
if (!await fs.pathExists(`${dumpDir}/.dump.sql`))
throw new Error('To run local database you have to create a dump first');
@ -71,7 +78,7 @@ class Run {
const day = pad(today.getDate());
const stamp = `${year}-${month}-${day}`;
await docker.build(opts.workspace, {
await docker.build(opts.myvcDir, {
tag: opts.code,
file: `${dockerfilePath}.dump`,
buildArg: `STAMP=${stamp}`

View File

@ -11,6 +11,12 @@ const Run = require('./myvc-run');
* version of it.
*/
class Start {
get usage() {
return {
description: 'Start local database server container'
};
}
async run(myvc, opts) {
const ct = new Container(opts.code);
let status;

View File

@ -6,11 +6,19 @@ const fs = require('fs-extra');
* Creates a new version.
*/
class Version {
mainOpt = 'name';
get usage() {
return {
description: 'Creates a new version',
params: {
name: 'Name for the new version'
},
operand: 'name'
};
}
get localOpts() {
return {
operand: 'name',
name: {
string: {
name: 'n'
},
default: {
@ -20,35 +28,52 @@ class Version {
}
async run(myvc, opts) {
const verionsDir =`${opts.workspace}/versions`;
let versionDir;
let versionName = opts.name;
// Fetch last version number
const conn = await myvc.dbConnect();
const version = await myvc.fetchDbVersion() || {};
try {
await conn.query('START TRANSACTION');
const [[row]] = await conn.query(
`SELECT lastNumber FROM version WHERE code = ? FOR UPDATE`,
`SELECT number, lastNumber
FROM version
WHERE code = ?
FOR UPDATE`,
[opts.code]
);
const lastVersion = row && row.lastNumber;
const number = row && row.number;
const lastNumber = row && row.lastNumber;
console.log(
`Database information:`
+ `\n -> Version: ${version.number}`
+ `\n -> Last version: ${lastVersion}`
+ `\n -> Version: ${number}`
+ `\n -> Last version: ${lastNumber}`
);
let newVersion = lastVersion ? parseInt(lastVersion) + 1 : 1;
newVersion = String(newVersion).padStart(opts.versionDigits, '0');
let newVersion;
if (lastNumber)
newVersion = Math.max(
parseInt(number),
parseInt(lastNumber)
) + 1;
else
newVersion = 1;
const versionDigits = number
? number.length
: opts.versionDigits;
newVersion = String(newVersion).padStart(versionDigits, '0');
// Get version name
let versionName = opts.name;
const verionsDir =`${opts.myvcDir}/versions`;
const versionNames = new Set();
const versionDirs = await fs.readdir(verionsDir);
for (const versionNameDir of versionDirs) {
@ -83,11 +108,19 @@ class Version {
versionDir = `${verionsDir}/${versionFolder}`;
await conn.query(
`UPDATE version SET lastNumber = ? WHERE code = ?`,
[newVersion, opts.code]
`INSERT INTO version
SET code = ?,
lastNumber = ?
ON DUPLICATE KEY UPDATE
lastNumber = VALUES(lastNumber)`,
[opts.code, newVersion]
);
await fs.mkdir(versionDir);
console.log(`New version folder created: ${versionFolder}`);
await fs.writeFile(
`${versionDir}/00-firstScript.sql`,
'--Place your SQL code here\n'
);
console.log(`New version created: ${versionFolder}`);
await conn.query('COMMIT');
} catch (err) {

190
myvc.js
View File

@ -9,6 +9,7 @@ const ini = require('ini');
const path = require('path');
const mysql = require('mysql2/promise');
const nodegit = require('nodegit');
const camelToSnake = require('./lib').camelToSnake;
class MyVC {
async run(command) {
@ -17,12 +18,23 @@ class MyVC {
`v${packageJson.version}`.magenta
);
const opts = {};
const argv = process.argv.slice(2);
const cliOpts = getopts(argv, {
const usage = {
description: 'Utility for database versioning',
params: {
remote: 'Name of remote to use',
workspace: 'The base directory of the project',
socket: 'Wether to connect to database via socket',
debug: 'Wether to enable debug mode',
version: 'Display the version number and exit',
help: 'Display this help message'
}
};
const baseOpts = {
alias: {
remote: 'r',
workspace: 'w',
workspace: 'w'
},
boolean: {
socket: 's',
debug: 'd',
version: 'v',
@ -31,28 +43,12 @@ class MyVC {
default: {
workspace: process.cwd()
}
})
if (cliOpts.version)
process.exit(0);
};
const opts = this.getopts(baseOpts);
try {
if (!command) {
const commandName = cliOpts._[0];
if (!commandName) {
console.log(
'Usage:'.gray,
'[npx] myvc'
+ '[-w|--workspace]'
+ '[-r|--remote]'
+ '[-d|--debug]'
+ '[-h|--help]'
+ '[-v|--version]'
+ 'command'.blue
);
process.exit(0);
}
const commandName = opts._[0];
if (!command && commandName) {
const commands = [
'init',
'pull',
@ -70,17 +66,53 @@ class MyVC {
command = new Klass();
}
const commandOpts = getopts(argv, command.localOpts);
Object.assign(cliOpts, commandOpts);
if (!command) {
this.showHelp(baseOpts, usage);
process.exit(0);
}
for (const opt in cliOpts)
if (opt.length > 1 || opt == '_')
opts[opt] = cliOpts[opt];
const commandOpts = this.getopts(command.localOpts);
Object.assign(opts, commandOpts);
const operandToOpt = command.localOpts.operand;
const operandToOpt = command.usage.operand;
if (opts._.length >= 2 && operandToOpt)
opts[operandToOpt] = opts._[1];
if (opts.version)
process.exit(0);
if (opts.help) {
this.showHelp(command.localOpts, command.usage, commandName);
process.exit(0);
}
// Check version
let depVersion;
const versionRegex = /^[^~]?([0-9]+)\.([0-9]+).([0-9]+)$/;
const wsPackageFile = path.join(opts.workspace, 'package.json');
if (await fs.pathExists(wsPackageFile)) {
const wsPackageJson = require(wsPackageFile);
try {
depVersion = wsPackageJson
.dependencies
.myvc.match(versionRegex);
} catch (e) {}
}
if (depVersion) {
const myVersion = packageJson.version.match(versionRegex);
const isSameVersion =
depVersion[1] === myVersion[1] &&
depVersion[2] === myVersion[2];
if (!isSameVersion)
throw new Error(`This version of MyVC differs from your package.json`)
}
// Load method
parameter('Workspace:', opts.workspace);
parameter('Remote:', opts.remote || 'local');
@ -116,18 +148,22 @@ class MyVC {
Object.assign(opts, config);
opts.configFile = configFile;
if (!opts.myvcDir)
opts.myvcDir = path.join(opts.workspace, opts.subdir || '');
// Database configuration
let iniFile = 'db.ini';
let iniDir = __dirname;
let iniFile = 'db.ini';
if (opts.remote) {
iniFile = `remotes/${opts.remote}.ini`;
iniDir = opts.workspace;
iniDir = `${opts.myvcDir}/remotes`;
iniFile = `${opts.remote}.ini`;
}
const iniPath = path.join(iniDir, iniFile);
if (!await fs.pathExists(iniPath))
throw new Error(`Database config file not found: ${iniFile}`);
throw new Error(`Database config file not found: ${iniPath}`);
const iniConfig = ini.parse(await fs.readFile(iniPath, 'utf8')).client;
const dbConfig = {
@ -135,7 +171,6 @@ class MyVC {
port: iniConfig.port,
user: iniConfig.user,
password: iniConfig.password,
database: opts.versionSchema,
multipleStatements: true,
authPlugins: {
mysql_clear_password() {
@ -146,7 +181,7 @@ class MyVC {
if (iniConfig.ssl_ca) {
dbConfig.ssl = {
ca: await fs.readFile(`${opts.workspace}/${iniConfig.ssl_ca}`),
ca: await fs.readFile(`${opts.myvcDir}/${iniConfig.ssl_ca}`),
rejectUnauthorized: iniConfig.ssl_verify_server_cert != undefined
}
}
@ -165,20 +200,30 @@ class MyVC {
await this.conn.end();
}
getopts(opts) {
const argv = process.argv.slice(2);
const values = getopts(argv, opts);
const cleanValues = {};
for (const opt in values)
if (opt.length > 1 || opt == '_')
cleanValues[opt] = values[opt];
return cleanValues;
}
async dbConnect() {
if (!this.conn)
this.conn = await this.createConnection();
return this.conn;
}
async createConnection() {
return await mysql.createConnection(this.opts.dbConfig);
}
async fetchDbVersion() {
const {opts} = this;
const [[res]] = await this.conn.query(
if (!this.conn) {
const conn = this.conn = await this.createConnection();
const [[schema]] = await conn.query(
`SHOW DATABASES LIKE ?`, [opts.versionSchema]
);
if (!schema)
await conn.query(`CREATE DATABASE ??`, [opts.versionSchema]);
const [[res]] = await conn.query(
`SELECT COUNT(*) > 0 tableExists
FROM information_schema.tables
WHERE TABLE_SCHEMA = ?
@ -188,14 +233,25 @@ class MyVC {
if (!res.tableExists) {
const structure = await fs.readFile(`${__dirname}/structure.sql`, 'utf8');
await this.conn.query(structure);
await conn.query(structure);
return null;
}
await conn.query(`USE ??`, [opts.versionSchema]);
}
return this.conn;
}
async createConnection() {
return await mysql.createConnection(this.opts.dbConfig);
}
async fetchDbVersion() {
const [[version]] = await this.conn.query(
`SELECT number, gitCommit
FROM version WHERE code = ?`,
[opts.code]
[this.opts.code]
);
return version;
}
@ -278,7 +334,7 @@ class MyVC {
}
async cachedChanges() {
const dumpDir = `${this.opts.workspace}/dump`;
const dumpDir = `${this.opts.myvcDir}/dump`;
const dumpChanges = `${dumpDir}/.changes`;
if (!await fs.pathExists(dumpChanges))
@ -300,6 +356,40 @@ class MyVC {
}
return changes;
}
showHelp(opts, usage, command) {
const prefix = `${'Usage:'.gray} [npx] myvc`;
if (command) {
let log = [prefix, command.blue];
if (usage.operand) log.push(`[<${usage.operand}>]`);
if (opts) log.push('[<options>]');
console.log(log.join(' '))
} else
console.log(`${prefix} [<options>] ${'<command>'.blue} [<args>]`);
if (usage.description)
console.log(`${'Description:'.gray} ${usage.description}`);
if (opts) {
console.log('Options:'.gray);
this.printOpts(opts, usage, 'alias');
this.printOpts(opts, usage, 'boolean');
this.printOpts(opts, usage, 'string');
}
}
printOpts(opts, usage, group) {
const optGroup = opts[group];
if (optGroup)
for (const opt in optGroup) {
const paramDescription = usage.params[opt] || '';
let longOpt = opt;
if (group !== 'boolean') longOpt += ` <string>`;
longOpt = camelToSnake(longOpt).padEnd(22, ' ')
console.log(` -${optGroup[opt]}, --${longOpt} ${paramDescription}`);
}
}
}
module.exports = MyVC;

View File

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

View File

@ -0,0 +1 @@
-- Place your SQL code here

View File

@ -1 +0,0 @@
SET @test = NULL;