Help improved, exclusive push lock , check version, secure priv restore, README
This commit is contained in:
parent
801b6e9539
commit
381ada4411
55
README.md
55
README.md
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
function camelToSnake(str) {
|
||||||
|
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.camelToSnake = camelToSnake;
|
15
myvc-dump.js
15
myvc-dump.js
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
18
myvc-pull.js
18
myvc-pull.js
|
@ -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'));
|
||||||
|
|
109
myvc-push.js
109
myvc-push.js
|
@ -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)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
19
myvc-run.js
19
myvc-run.js
|
@ -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}`
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
208
myvc.js
|
@ -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;
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
-- Place your SQL code here
|
|
@ -1 +0,0 @@
|
||||||
SET @test = NULL;
|
|
Loading…
Reference in New Issue