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.
|
||||
|
||||
```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
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
18
myvc-pull.js
18
myvc-pull.js
|
@ -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'));
|
||||
|
|
109
myvc-push.js
109
myvc-push.js
|
@ -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) {
|
||||
await pushConn.query(`USE ${scapedSchema}`);
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
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 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}`
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
208
myvc.js
208
myvc.js
|
@ -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',
|
||||
|
@ -69,18 +65,54 @@ class MyVC {
|
|||
const Klass = require(`./myvc-${commandName}`);
|
||||
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)
|
||||
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');
|
||||
|
||||
|
@ -115,19 +147,23 @@ 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,9 +200,46 @@ 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();
|
||||
const {opts} = this;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -176,26 +248,10 @@ class MyVC {
|
|||
}
|
||||
|
||||
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(
|
||||
`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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
-- Place your SQL code here
|
|
@ -1 +0,0 @@
|
|||
SET @test = NULL;
|
Loading…
Reference in New Issue