Refactor and fixes

This commit is contained in:
Juan Ferrer 2020-12-04 17:30:26 +01:00
parent 0709670550
commit b08617d77e
13 changed files with 285 additions and 280 deletions

View File

@ -12,7 +12,7 @@ Any help is welcomed! Feel free to contribute.
* Node.js <= 12.0 * Node.js <= 12.0
* Git * Git
* Docker (Only to setup a local server) * Docker (Local server)
## Installation ## Installation
@ -35,19 +35,19 @@ $ npx myvc [command]
Execute *myvc* with the desired command. Execute *myvc* with the desired command.
```text ```text
$ myvc [-w|--workspace] [-e|--env] [-h|--help] command $ myvc [-w|--workspace] [-r|--remote] [-d|--debug] [-h|--help] command
``` ```
The default workspace directory is the current working directory and unless The default workspace directory is the current working directory and unless
otherwise indicated, the default environment is *local*. otherwise indicated, the default remote is *local*.
Commands for database versioning: Database versioning commands:
* **init**: Initialize an empty workspace. * **init**: Initialize an empty workspace.
* **pull**: Export database routines into workspace. * **pull**: Export database routines into workspace.
* **push**: Apply changes into database. * **push**: Apply changes into database.
Commands for local server management: Local server management commands:
* **dump**: Export database structure and fixtures from *production*. * **dump**: Export database structure and fixtures from *production*.
* **run**: Build and starts local database server container. * **run**: Build and starts local database server container.
@ -67,14 +67,14 @@ Now you can configure MyVC using *myvc.config.yml* file, located at the root of
your workspace. This file should include the project codename and schemas/tables your workspace. This file should include the project codename and schemas/tables
wich are exported when you use *pull* or *dump* commands. wich are exported when you use *pull* or *dump* commands.
### Environments ### Remotes
Create database connection configuration for each environment at *remotes* Create database connection configuration for each environment at *remotes*
folder using standard MySQL *ini* configuration files. The predefined folder using standard MySQL *ini* configuration files. The convention remote
environment names are *production* and *test*. names are *production* and *test*.
```text ```text
remotes/[environment].ini remotes/[remote].ini
``` ```
### Dumps ### Dumps

4
cli.js Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env node
const MyVC = require('./myvc');
new MyVC().run();

253
index.js
View File

@ -1,253 +0,0 @@
require('require-yaml');
require('colors');
const getopts = require('getopts');
const packageJson = require('./package.json');
const fs = require('fs-extra');
const ini = require('ini');
const path = require('path');
const mysql = require('mysql2/promise');
const nodegit = require('nodegit');
class MyVC {
async run(command) {
console.log(
'MyVC (MySQL Version Control)'.green,
`v${packageJson.version}`.magenta
);
const opts = {};
const argv = process.argv.slice(2);
const cliOpts = getopts(argv, {
alias: {
env: 'e',
workspace: 'w',
socket: 's',
debug: 'd',
version: 'v',
help: 'h'
},
default: {
workspace: process.cwd()
}
})
if (cliOpts.version)
process.exit(0);
try {
if (!command) {
const commandName = cliOpts._[0];
if (!commandName) {
console.log(
'Usage:'.gray,
'[npx] myvc'
+ '[-w|--workspace]'
+ '[-e|--env]'
+ '[-d|--debug]'
+ '[-h|--help]'
+ '[-v|--version]'
+ 'command'.blue
);
process.exit(0);
}
const commands = [
'init',
'pull',
'push',
'dump',
'start',
'run'
];
if (commands.indexOf(commandName) == -1)
throw new Error (`Unknown command '${commandName}'`);
const Klass = require(`./myvc-${commandName}`);
command = new Klass();
}
const commandOpts = getopts(argv, command.myOpts);
Object.assign(cliOpts, commandOpts);
for (const opt in cliOpts) {
if (opt.length > 1 || opt == '_')
opts[opt] = cliOpts[opt];
}
parameter('Workspace:', opts.workspace);
parameter('Environment:', opts.env);
await this.load(opts);
command.opts = opts;
await command.run(this, opts);
await this.unload();
} catch (err) {
if (err.name == 'Error' && !opts.debug)
console.error('Error:'.gray, err.message.red);
else
throw err;
}
function parameter(parameter, value) {
console.log(parameter.gray, (value || 'null').blue);
}
process.exit();
}
async load(opts) {
// Configuration file
const config = require(`${__dirname}/myvc.default.yml`);
const configFile = 'myvc.config.yml';
const configPath = path.join(opts.workspace, configFile);
if (await fs.pathExists(configPath))
Object.assign(config, require(configPath));
Object.assign(opts, config);
opts.configFile = configFile;
// Database configuration
let iniFile = 'db.ini';
let iniDir = __dirname;
if (opts.env) {
iniFile = `remotes/${opts.env}.ini`;
iniDir = opts.workspace;
}
const iniPath = path.join(iniDir, iniFile);
if (!await fs.pathExists(iniPath))
throw new Error(`Database config file not found: ${iniFile}`);
const iniConfig = ini.parse(await fs.readFile(iniPath, 'utf8')).client;
const dbConfig = {
host: iniConfig.host,
port: iniConfig.port,
user: iniConfig.user,
password: iniConfig.password,
database: opts.versionSchema,
authPlugins: {
mysql_clear_password() {
return () => iniConfig.password + '\0';
}
}
};
if (iniConfig.ssl_ca) {
dbConfig.ssl = {
ca: await fs.readFile(`${opts.workspace}/${iniConfig.ssl_ca}`),
rejectUnauthorized: iniConfig.ssl_verify_server_cert != undefined
}
}
if (opts.socket)
dbConfig.socketPath = '/var/run/mysqld/mysqld.sock';
Object.assign(opts, {
iniFile,
dbConfig
});
this.opts = opts;
}
async dbConnect() {
if (!this.conn)
this.conn = await this.createConnection();
return this.conn;
}
async createConnection() {
return await mysql.createConnection(this.opts.dbConfig);
}
async unload() {
if (this.conn)
await this.conn.end();
}
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]
);
return version;
}
async changedRoutines(commit) {
const repo = await nodegit.Repository.open(this.opts.workspace);
const from = await repo.getCommit(commit);
const fromTree = await from.getTree();
const to = await repo.getHeadCommit();
const toTree = await to.getTree();
const diff = await toTree.diff(fromTree);
const patches = await diff.patches();
const changes = [];
for (const patch of patches) {
const path = patch.newFile().path();
const match = path.match(/^routines\/(.+)\.sql$/);
if (!match) continue;
changes.push({
mark: patch.isDeleted() ? '-' : '+',
path: match[1]
});
}
return changes.sort(
(a, b) => b.mark == '-' && b.mark != a.mark ? 1 : -1
);
}
async cachedChanges() {
const changes = [];
const dumpDir = `${this.opts.workspace}/dump`;
const dumpChanges = `${dumpDir}/.changes`;
if (!await fs.pathExists(dumpChanges))
return null;
const readline = require('readline');
const rl = readline.createInterface({
input: fs.createReadStream(dumpChanges),
//output: process.stdout,
console: false
});
for await (const line of rl) {
changes.push({
mark: line.charAt(0),
path: line.substr(1)
});
}
return changes;
}
}
module.exports = MyVC;
if (require.main === module)
new MyVC().run();

View File

@ -1,5 +1,5 @@
const MyVC = require('./index'); const MyVC = require('./myvc');
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('path'); const path = require('path');
const docker = require('./docker'); const docker = require('./docker');
@ -11,10 +11,10 @@ class Dump {
get myOpts() { get myOpts() {
return { return {
alias: { alias: {
env: 'e' remote: 'r'
}, },
default: { default: {
env: 'production' remote: 'production'
} }
}; };
} }
@ -38,7 +38,7 @@ class Dump {
await docker.build(__dirname, { await docker.build(__dirname, {
tag: 'myvc/client', tag: 'myvc/client',
file: path.join(__dirname, 'Dockerfile.client') file: path.join(__dirname, 'server', 'Dockerfile')
}, opts.debug); }, opts.debug);
let dumpArgs = [ let dumpArgs = [
@ -84,7 +84,8 @@ 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.workspace}:/workspace`,
rm: true
}, execOptions); }, execOptions);
} }
} }

View File

@ -1,5 +1,5 @@
const MyVC = require('./index'); const MyVC = require('./myvc');
const fs = require('fs-extra'); const fs = require('fs-extra');
class Init { class Init {

View File

@ -1,5 +1,5 @@
const MyVC = require('./index'); const MyVC = require('./myvc');
const fs = require('fs-extra'); const fs = require('fs-extra');
const ejs = require('ejs'); const ejs = require('ejs');

View File

@ -1,5 +1,5 @@
const MyVC = require('./index'); const MyVC = require('./myvc');
const fs = require('fs-extra'); const fs = require('fs-extra');
const nodegit = require('nodegit'); const nodegit = require('nodegit');
@ -58,7 +58,7 @@ class Push {
version = userVersion; version = userVersion;
} }
if (opts.env == 'production') { if (opts.remote == 'production') {
console.log( console.log(
'\n ( ( ) ( ( ) ) ' '\n ( ( ) ( ( ) ) '
+ '\n )\\ ))\\ ) ( /( )\\ ) ( ))\\ ) ( /( ( /( ' + '\n )\\ ))\\ ) ( /( )\\ ) ( ))\\ ) ( /( ( /( '

View File

@ -1,6 +1,7 @@
const MyVC = require('./index'); const MyVC = require('./myvc');
const docker = require('./docker'); const docker = require('./docker');
const Container = require('./docker').Container;
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('path'); const path = require('path');
const Server = require('./server/server'); const Server = require('./server/server');
@ -37,7 +38,7 @@ class Run {
const changes = await myvc.changedRoutines(version.gitCommit); const changes = await myvc.changedRoutines(version.gitCommit);
let isEqual = false; let isEqual = false;
if (cache && changes && cache.length == changes.lenth) if (cache && changes && cache.length == changes.length)
for (let i = 0; i < changes.length; i++) { for (let i = 0; i < changes.length; i++) {
isEqual = cache[i].path == changes[i].path isEqual = cache[i].path == changes[i].path
&& cache[i].mark == changes[i].mark; && cache[i].mark == changes[i].mark;
@ -45,6 +46,7 @@ class Run {
} }
if (!isEqual) { if (!isEqual) {
console.log('not equal');
const fd = await fs.open(`${dumpDir}/.changes`, 'w+'); const fd = await fs.open(`${dumpDir}/.changes`, 'w+');
for (const change of changes) for (const change of changes)
fs.write(fd, change.mark + change.path + '\n'); fs.write(fd, change.mark + change.path + '\n');
@ -85,7 +87,8 @@ class Run {
publish: `3306:${dbConfig.port}` publish: `3306:${dbConfig.port}`
}; };
try { try {
await this.rm(); const server = new Server(new Container(opts.code));
await server.rm();
} catch (e) {} } catch (e) {}
} }

View File

@ -1,5 +1,5 @@
const MyVC = require('./index'); const MyVC = require('./myvc');
const Container = require('./docker').Container; const Container = require('./docker').Container;
const Server = require('./server/server'); const Server = require('./server/server');
const Run = require('./myvc-run'); const Run = require('./myvc-run');

255
myvc.js
View File

@ -1,4 +1,255 @@
#!/usr/bin/env node #!/usr/bin/env node
const MyVC = require('./'); require('require-yaml');
new MyVC().run(); require('colors');
const getopts = require('getopts');
const packageJson = require('./package.json');
const fs = require('fs-extra');
const ini = require('ini');
const path = require('path');
const mysql = require('mysql2/promise');
const nodegit = require('nodegit');
class MyVC {
async run(command) {
console.log(
'MyVC (MySQL Version Control)'.green,
`v${packageJson.version}`.magenta
);
const opts = {};
const argv = process.argv.slice(2);
const cliOpts = getopts(argv, {
alias: {
remote: 'r',
workspace: 'w',
socket: 's',
debug: 'd',
version: 'v',
help: 'h'
},
default: {
workspace: process.cwd()
}
})
if (cliOpts.version)
process.exit(0);
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 commands = [
'init',
'pull',
'push',
'dump',
'start',
'run'
];
if (commands.indexOf(commandName) == -1)
throw new Error (`Unknown command '${commandName}'`);
const Klass = require(`./myvc-${commandName}`);
command = new Klass();
}
const commandOpts = getopts(argv, command.myOpts);
Object.assign(cliOpts, commandOpts);
for (const opt in cliOpts) {
if (opt.length > 1 || opt == '_')
opts[opt] = cliOpts[opt];
}
parameter('Workspace:', opts.workspace);
parameter('Remote:', opts.remote || 'local');
await this.load(opts);
command.opts = opts;
await command.run(this, opts);
await this.unload();
} catch (err) {
if (err.name == 'Error' && !opts.debug)
console.error('Error:'.gray, err.message.red);
else
throw err;
}
function parameter(parameter, value) {
console.log(parameter.gray, (value || 'null').blue);
}
process.exit();
}
async load(opts) {
// Configuration file
const config = require(`${__dirname}/myvc.default.yml`);
const configFile = 'myvc.config.yml';
const configPath = path.join(opts.workspace, configFile);
if (await fs.pathExists(configPath))
Object.assign(config, require(configPath));
Object.assign(opts, config);
opts.configFile = configFile;
// Database configuration
let iniFile = 'db.ini';
let iniDir = __dirname;
if (opts.remote) {
iniFile = `remotes/${opts.remote}.ini`;
iniDir = opts.workspace;
}
const iniPath = path.join(iniDir, iniFile);
if (!await fs.pathExists(iniPath))
throw new Error(`Database config file not found: ${iniFile}`);
const iniConfig = ini.parse(await fs.readFile(iniPath, 'utf8')).client;
const dbConfig = {
host: iniConfig.host,
port: iniConfig.port,
user: iniConfig.user,
password: iniConfig.password,
database: opts.versionSchema,
authPlugins: {
mysql_clear_password() {
return () => iniConfig.password + '\0';
}
}
};
if (iniConfig.ssl_ca) {
dbConfig.ssl = {
ca: await fs.readFile(`${opts.workspace}/${iniConfig.ssl_ca}`),
rejectUnauthorized: iniConfig.ssl_verify_server_cert != undefined
}
}
if (opts.socket)
dbConfig.socketPath = '/var/run/mysqld/mysqld.sock';
Object.assign(opts, {
iniFile,
dbConfig
});
this.opts = opts;
}
async dbConnect() {
if (!this.conn)
this.conn = await this.createConnection();
return this.conn;
}
async createConnection() {
return await mysql.createConnection(this.opts.dbConfig);
}
async unload() {
if (this.conn)
await this.conn.end();
}
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]
);
return version;
}
async changedRoutines(commit) {
const repo = await nodegit.Repository.open(this.opts.workspace);
const from = await repo.getCommit(commit);
const fromTree = await from.getTree();
const to = await repo.getHeadCommit();
const toTree = await to.getTree();
const diff = await toTree.diff(fromTree);
const patches = await diff.patches();
const changes = [];
for (const patch of patches) {
const path = patch.newFile().path();
const match = path.match(/^routines\/(.+)\.sql$/);
if (!match) continue;
changes.push({
mark: patch.isDeleted() ? '-' : '+',
path: match[1]
});
}
return changes.sort(
(a, b) => b.mark == '-' && b.mark != a.mark ? 1 : -1
);
}
async cachedChanges() {
const changes = [];
const dumpDir = `${this.opts.workspace}/dump`;
const dumpChanges = `${dumpDir}/.changes`;
if (!await fs.pathExists(dumpChanges))
return null;
const readline = require('readline');
const rl = readline.createInterface({
input: fs.createReadStream(dumpChanges),
//output: process.stdout,
console: false
});
for await (const line of rl) {
changes.push({
mark: line.charAt(0),
path: line.substr(1)
});
}
return changes;
}
}
module.exports = MyVC;
if (require.main === module)
new MyVC().run();

View File

@ -1,10 +1,10 @@
{ {
"name": "myvc", "name": "myvc",
"version": "1.1.3", "version": "1.1.4",
"author": "Verdnatura Levante SL", "author": "Verdnatura Levante SL",
"description": "MySQL Version Control", "description": "MySQL Version Control",
"license": "GPL-3.0", "license": "GPL-3.0",
"bin": "myvc.js", "bin": "cli.js",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/verdnatura/myvc.git" "url": "https://github.com/verdnatura/myvc.git"

View File

@ -32,7 +32,6 @@ RUN npm install --only=prod
COPY \ COPY \
structure.sql \ structure.sql \
index.js \
myvc.js \ myvc.js \
myvc-push.js \ myvc-push.js \
myvc.default.yml \ myvc.default.yml \