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

View File

@ -1,5 +1,6 @@
const spawn = require('child_process').spawn; const spawn = require('child_process').spawn;
const execFile = require('child_process').execFile; const execFile = require('child_process').execFile;
const camelToSnake = require('./lib').camelToSnake;
const docker = { const docker = {
async run(image, commandArgs, options, execOptions) { 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 = docker;
module.exports.Container = Container; 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 path = require('path');
const docker = require('./docker'); const docker = require('./docker');
/**
* Dumps structure and fixtures from remote.
*/
class Dump { class Dump {
get usage() {
return {
description: 'Dumps structure and fixtures from remote',
operand: 'remote'
};
}
get localOpts() { get localOpts() {
return { return {
operand: 'remote',
default: { default: {
remote: 'production' remote: 'production'
} }
@ -20,7 +23,7 @@ class Dump {
async run(myvc, opts) { async run(myvc, opts) {
const conn = await myvc.dbConnect(); const conn = await myvc.dbConnect();
const dumpDir = `${opts.workspace}/dump`; const dumpDir = `${opts.myvcDir}/dump`;
if (!await fs.pathExists(dumpDir)) if (!await fs.pathExists(dumpDir))
await fs.mkdir(dumpDir); await fs.mkdir(dumpDir);
@ -82,7 +85,7 @@ class Dump {
async dockerRun(command, args, execOptions) { async dockerRun(command, args, execOptions) {
const commandArgs = [command].concat(args); const commandArgs = [command].concat(args);
await docker.run('myvc/client', commandArgs, { await docker.run('myvc/client', commandArgs, {
volume: `${this.opts.workspace}:/workspace`, volume: `${this.opts.myvcDir}:/workspace`,
rm: true rm: true
}, execOptions); }, execOptions);
} }

View File

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

View File

@ -6,10 +6,20 @@ const shajs = require('sha.js');
const nodegit = require('nodegit'); const nodegit = require('nodegit');
class Pull { 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() { get localOpts() {
return { return {
operand: 'remote', boolean: {
alias: {
force: 'f', force: 'f',
checkout: 'c' checkout: 'c'
} }
@ -80,7 +90,7 @@ class Pull {
for (const exporter of exporters) for (const exporter of exporters)
await exporter.init(); await exporter.init();
const exportDir = `${opts.workspace}/routines`; const exportDir = `${opts.myvcDir}/routines`;
if (!await fs.pathExists(exportDir)) if (!await fs.pathExists(exportDir))
await fs.mkdir(exportDir); await fs.mkdir(exportDir);
@ -88,7 +98,7 @@ class Pull {
let newShaSums = {}; let newShaSums = {};
let oldShaSums; let oldShaSums;
const shaFile = `${opts.workspace}/.shasums.json`; const shaFile = `${opts.myvcDir}/.shasums.json`;
if (await fs.pathExists(shaFile)) if (await fs.pathExists(shaFile))
oldShaSums = JSON.parse(await fs.readFile(shaFile, 'utf8')); oldShaSums = JSON.parse(await fs.readFile(shaFile, 'utf8'));

View File

@ -5,15 +5,22 @@ const nodegit = require('nodegit');
/** /**
* Pushes changes to remote. * Pushes changes to remote.
*
* @property {Boolean} force Answer yes to all questions
* @property {Boolean} user Whether to change current user version
*/ */
class Push { 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() { get localOpts() {
return { return {
operand: 'remote', boolean: {
alias: {
force: 'f', force: 'f',
user: 'u' user: 'u'
} }
@ -24,6 +31,25 @@ class Push {
const conn = await myvc.dbConnect(); const conn = await myvc.dbConnect();
this.conn = conn; 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() || {}; const version = await myvc.fetchDbVersion() || {};
console.log( console.log(
@ -33,7 +59,7 @@ class Push {
); );
if (!version.number) if (!version.number)
version.number = '00000'; version.number = String('0').padStart(opts.versionDigits, '0');
if (!/^[0-9]*$/.test(version.number)) if (!/^[0-9]*$/.test(version.number))
throw new Error('Wrong database version'); throw new Error('Wrong database version');
@ -59,6 +85,8 @@ class Push {
version.gitCommit = userVersion.gitCommit; version.gitCommit = userVersion.gitCommit;
} }
// Prevent push to production by mistake
if (opts.remote == 'production') { if (opts.remote == 'production') {
console.log( console.log(
'\n ( ( ) ( ( ) ) ' '\n ( ( ) ( ( ) ) '
@ -88,11 +116,13 @@ class Push {
} }
} }
// Apply versions
console.log('Applying versions.'); console.log('Applying versions.');
const pushConn = await myvc.createConnection(); const pushConn = await myvc.createConnection();
let nChanges = 0; let nChanges = 0;
const versionsDir = `${opts.workspace}/versions`; const versionsDir = `${opts.myvcDir}/versions`;
function logVersion(type, version, name) { function logVersion(type, version, name) {
console.log('', type.bold, `[${version.bold}]`, name); console.log('', type.bold, `[${version.bold}]`, name);
@ -105,21 +135,25 @@ class Push {
if (versionDir == 'README.md') if (versionDir == 'README.md')
continue; continue;
const match = versionDir.match(/^([0-9]{5})-([a-zA-Z0-9]+)?$/); const match = versionDir.match(/^([0-9])-([a-zA-Z0-9]+)?$/);
if (!match) { if (!match) {
logVersion('[W]'.yellow, '?????', versionDir); logVersion('[W]'.yellow, '?????', versionDir);
continue; continue;
} }
const dirVersion = match[1]; const versionNumber = match[1];
const versionName = match[2]; const versionName = match[2];
if (version.number >= dirVersion) { if (versionNumber.length != version.number.length) {
logVersion('[I]'.blue, dirVersion, versionName); logVersion('[W]'.yellow, '*****', versionDir);
continue;
}
if (version.number >= versionNumber) {
logVersion('[I]'.blue, versionNumber, versionName);
continue; continue;
} }
logVersion('[+]'.green, dirVersion, versionName); logVersion('[+]'.green, versionNumber, versionName);
const scriptsDir = `${versionsDir}/${versionDir}`; const scriptsDir = `${versionsDir}/${versionDir}`;
const scripts = await fs.readdir(scriptsDir); const scripts = await fs.readdir(scriptsDir);
@ -134,10 +168,12 @@ class Push {
nChanges++; nChanges++;
} }
await this.updateVersion(nChanges, 'number', dirVersion); await this.updateVersion(nChanges, 'number', versionNumber);
} }
} }
// Apply routines
console.log('Applying changed routines.'); console.log('Applying changed routines.');
let nRoutines = 0; let nRoutines = 0;
@ -146,12 +182,6 @@ class Push {
: await myvc.cachedChanges(); : await myvc.cachedChanges();
changes = this.parseChanges(changes); changes = this.parseChanges(changes);
await conn.query(
`CREATE TEMPORARY TABLE tProcsPriv
ENGINE = MEMORY
SELECT * FROM mysql.procs_priv LIMIT 0`
);
const routines = []; const routines = [];
for (const change of changes) for (const change of changes)
if (change.isRoutine) if (change.isRoutine)
@ -171,19 +201,32 @@ class Push {
} }
for (const change of changes) { 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 exists = await fs.pathExists(fullPath);
const actionMsg = exists ? '[+]'.green : '[-]'.red; const actionMsg = exists ? '[+]'.green : '[-]'.red;
const typeMsg = `[${change.type.abbr}]`[change.type.color]; const typeMsg = `[${change.type.abbr}]`[change.type.color];
console.log('', actionMsg.bold, typeMsg.bold, change.fullName); console.log('', actionMsg.bold, typeMsg.bold, change.fullName);
try { try {
const scapedSchema = pushConn.escapeId(change.schema, true); const scapedSchema = pushConn.escapeId(change.schema, true);
if (exists) { if (exists) {
await pushConn.query(`USE ${scapedSchema}`); if (change.type.name = 'VIEW')
await pushConn.query(`USE ${scapedSchema}`);
await this.queryFromFile(pushConn, `routines/${change.path}.sql`); 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 { } else {
const escapedName = const escapedName =
scapedSchema + '.' + scapedSchema + '.' +
@ -203,20 +246,15 @@ class Push {
} }
if (routines.length) { if (routines.length) {
await conn.query( await conn.query(`DROP TEMPORARY TABLE tProcsPriv`);
`INSERT IGNORE INTO mysql.procs_priv await conn.query('FLUSH PRIVILEGES');
SELECT * FROM tProcsPriv`
);
await conn.query(
`DROP TEMPORARY TABLE tProcsPriv`
);
} }
// Update and release
await pushConn.end(); await pushConn.end();
if (nRoutines > 0) { if (nRoutines > 0) {
await conn.query('FLUSH PRIVILEGES');
console.log(` -> ${nRoutines} routines have changed.`); console.log(` -> ${nRoutines} routines have changed.`);
} else } else
console.log(` -> No routines changed.`); console.log(` -> No routines changed.`);
@ -226,6 +264,8 @@ class Push {
if (version.gitCommit !== head.sha()) if (version.gitCommit !== head.sha())
await this.updateVersion(nRoutines, 'gitCommit', head.sha()); await this.updateVersion(nRoutines, 'gitCommit', head.sha());
await conn.query(`DO RELEASE_LOCK('myvc_push')`);
} }
parseChanges(changes) { parseChanges(changes) {
@ -395,6 +435,11 @@ const typeMap = {
}, },
}; };
const routineTypes = new Set([
'FUNCTION',
'PROCEDURE'
]);
class Routine { class Routine {
constructor(change) { constructor(change) {
const path = change.path; const path = change.path;
@ -411,7 +456,7 @@ class Routine {
schema, schema,
name, name,
fullName: `${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 when fixtures have been modified or when the day on which the
* image was built is different to today. Some workarounds have been used * image was built is different to today. Some workarounds have been used
* to avoid a bug with OverlayFS driver on MacOS. * 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 { 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() { get localOpts() {
return { return {
alias: { boolean: {
ci: 'c', ci: 'c',
random: 'r' random: 'r'
} }
@ -26,7 +33,7 @@ class Run {
} }
async run(myvc, opts) { async run(myvc, opts) {
const dumpDir = `${opts.workspace}/dump`; const dumpDir = `${opts.myvcDir}/dump`;
if (!await fs.pathExists(`${dumpDir}/.dump.sql`)) if (!await fs.pathExists(`${dumpDir}/.dump.sql`))
throw new Error('To run local database you have to create a dump first'); 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 day = pad(today.getDate());
const stamp = `${year}-${month}-${day}`; const stamp = `${year}-${month}-${day}`;
await docker.build(opts.workspace, { await docker.build(opts.myvcDir, {
tag: opts.code, tag: opts.code,
file: `${dockerfilePath}.dump`, file: `${dockerfilePath}.dump`,
buildArg: `STAMP=${stamp}` buildArg: `STAMP=${stamp}`

View File

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

View File

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

208
myvc.js
View File

@ -9,6 +9,7 @@ const ini = require('ini');
const path = require('path'); const path = require('path');
const mysql = require('mysql2/promise'); const mysql = require('mysql2/promise');
const nodegit = require('nodegit'); const nodegit = require('nodegit');
const camelToSnake = require('./lib').camelToSnake;
class MyVC { class MyVC {
async run(command) { async run(command) {
@ -17,12 +18,23 @@ class MyVC {
`v${packageJson.version}`.magenta `v${packageJson.version}`.magenta
); );
const opts = {}; const usage = {
const argv = process.argv.slice(2); description: 'Utility for database versioning',
const cliOpts = getopts(argv, { 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: { alias: {
remote: 'r', remote: 'r',
workspace: 'w', workspace: 'w'
},
boolean: {
socket: 's', socket: 's',
debug: 'd', debug: 'd',
version: 'v', version: 'v',
@ -31,28 +43,12 @@ class MyVC {
default: { default: {
workspace: process.cwd() workspace: process.cwd()
} }
}) };
const opts = this.getopts(baseOpts);
if (cliOpts.version)
process.exit(0);
try { try {
if (!command) { const commandName = opts._[0];
const commandName = cliOpts._[0]; if (!command && commandName) {
if (!commandName) {
console.log(
'Usage:'.gray,
'[npx] myvc'
+ '[-w|--workspace]'
+ '[-r|--remote]'
+ '[-d|--debug]'
+ '[-h|--help]'
+ '[-v|--version]'
+ 'command'.blue
);
process.exit(0);
}
const commands = [ const commands = [
'init', 'init',
'pull', 'pull',
@ -69,18 +65,54 @@ class MyVC {
const Klass = require(`./myvc-${commandName}`); const Klass = require(`./myvc-${commandName}`);
command = new Klass(); command = new Klass();
} }
const commandOpts = getopts(argv, command.localOpts);
Object.assign(cliOpts, commandOpts);
for (const opt in cliOpts)
if (opt.length > 1 || opt == '_')
opts[opt] = cliOpts[opt];
const operandToOpt = command.localOpts.operand; if (!command) {
this.showHelp(baseOpts, usage);
process.exit(0);
}
const commandOpts = this.getopts(command.localOpts);
Object.assign(opts, commandOpts);
const operandToOpt = command.usage.operand;
if (opts._.length >= 2 && operandToOpt) if (opts._.length >= 2 && operandToOpt)
opts[operandToOpt] = opts._[1]; 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('Workspace:', opts.workspace);
parameter('Remote:', opts.remote || 'local'); parameter('Remote:', opts.remote || 'local');
@ -115,19 +147,23 @@ class MyVC {
Object.assign(opts, config); Object.assign(opts, config);
opts.configFile = configFile; opts.configFile = configFile;
if (!opts.myvcDir)
opts.myvcDir = path.join(opts.workspace, opts.subdir || '');
// Database configuration // Database configuration
let iniFile = 'db.ini';
let iniDir = __dirname; let iniDir = __dirname;
let iniFile = 'db.ini';
if (opts.remote) { if (opts.remote) {
iniFile = `remotes/${opts.remote}.ini`; iniDir = `${opts.myvcDir}/remotes`;
iniDir = opts.workspace; iniFile = `${opts.remote}.ini`;
} }
const iniPath = path.join(iniDir, iniFile); const iniPath = path.join(iniDir, iniFile);
if (!await fs.pathExists(iniPath)) 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 iniConfig = ini.parse(await fs.readFile(iniPath, 'utf8')).client;
const dbConfig = { const dbConfig = {
@ -135,7 +171,6 @@ class MyVC {
port: iniConfig.port, port: iniConfig.port,
user: iniConfig.user, user: iniConfig.user,
password: iniConfig.password, password: iniConfig.password,
database: opts.versionSchema,
multipleStatements: true, multipleStatements: true,
authPlugins: { authPlugins: {
mysql_clear_password() { mysql_clear_password() {
@ -146,7 +181,7 @@ class MyVC {
if (iniConfig.ssl_ca) { if (iniConfig.ssl_ca) {
dbConfig.ssl = { 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 rejectUnauthorized: iniConfig.ssl_verify_server_cert != undefined
} }
} }
@ -165,9 +200,46 @@ class MyVC {
await this.conn.end(); 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() { async dbConnect() {
if (!this.conn) const {opts} = this;
this.conn = await this.createConnection();
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 = ?
AND TABLE_NAME = 'version'`,
[opts.versionSchema]
);
if (!res.tableExists) {
const structure = await fs.readFile(`${__dirname}/structure.sql`, 'utf8');
await conn.query(structure);
return null;
}
await conn.query(`USE ??`, [opts.versionSchema]);
}
return this.conn; return this.conn;
} }
@ -176,26 +248,10 @@ class MyVC {
} }
async fetchDbVersion() { async fetchDbVersion() {
const {opts} = this;
const [[res]] = await this.conn.query(
`SELECT COUNT(*) > 0 tableExists
FROM information_schema.tables
WHERE TABLE_SCHEMA = ?
AND TABLE_NAME = 'version'`,
[opts.versionSchema]
);
if (!res.tableExists) {
const structure = await fs.readFile(`${__dirname}/structure.sql`, 'utf8');
await this.conn.query(structure);
return null;
}
const [[version]] = await this.conn.query( const [[version]] = await this.conn.query(
`SELECT number, gitCommit `SELECT number, gitCommit
FROM version WHERE code = ?`, FROM version WHERE code = ?`,
[opts.code] [this.opts.code]
); );
return version; return version;
} }
@ -278,7 +334,7 @@ class MyVC {
} }
async cachedChanges() { async cachedChanges() {
const dumpDir = `${this.opts.workspace}/dump`; const dumpDir = `${this.opts.myvcDir}/dump`;
const dumpChanges = `${dumpDir}/.changes`; const dumpChanges = `${dumpDir}/.changes`;
if (!await fs.pathExists(dumpChanges)) if (!await fs.pathExists(dumpChanges))
@ -300,6 +356,40 @@ class MyVC {
} }
return changes; 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; module.exports = MyVC;

View File

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

View File

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

View File

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