diff --git a/README.md b/README.md index 8005b4b..e9b969d 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Any help is welcomed! Feel free to contribute. * Node.js <= 12.0 * Git -* Docker (Only to setup a local server) +* Docker (Local server) ## Installation @@ -35,19 +35,19 @@ $ npx myvc [command] Execute *myvc* with the desired command. ```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 -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. * **pull**: Export database routines into workspace. * **push**: Apply changes into database. -Commands for local server management: +Local server management commands: * **dump**: Export database structure and fixtures from *production*. * **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 wich are exported when you use *pull* or *dump* commands. -### Environments +### Remotes Create database connection configuration for each environment at *remotes* -folder using standard MySQL *ini* configuration files. The predefined -environment names are *production* and *test*. +folder using standard MySQL *ini* configuration files. The convention remote +names are *production* and *test*. ```text -remotes/[environment].ini +remotes/[remote].ini ``` ### Dumps diff --git a/cli.js b/cli.js new file mode 100755 index 0000000..7d4a369 --- /dev/null +++ b/cli.js @@ -0,0 +1,4 @@ +#!/usr/bin/env node + +const MyVC = require('./myvc'); +new MyVC().run(); diff --git a/index.js b/index.js deleted file mode 100644 index 3e337c5..0000000 --- a/index.js +++ /dev/null @@ -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(); diff --git a/myvc-dump.js b/myvc-dump.js index 3238a8a..2b5a4d1 100644 --- a/myvc-dump.js +++ b/myvc-dump.js @@ -1,5 +1,5 @@ -const MyVC = require('./index'); +const MyVC = require('./myvc'); const fs = require('fs-extra'); const path = require('path'); const docker = require('./docker'); @@ -11,10 +11,10 @@ class Dump { get myOpts() { return { alias: { - env: 'e' + remote: 'r' }, default: { - env: 'production' + remote: 'production' } }; } @@ -38,7 +38,7 @@ class Dump { await docker.build(__dirname, { tag: 'myvc/client', - file: path.join(__dirname, 'Dockerfile.client') + file: path.join(__dirname, 'server', 'Dockerfile') }, opts.debug); let dumpArgs = [ @@ -84,7 +84,8 @@ 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.workspace}:/workspace`, + rm: true }, execOptions); } } diff --git a/myvc-init.js b/myvc-init.js index 556b38a..03f86a0 100755 --- a/myvc-init.js +++ b/myvc-init.js @@ -1,5 +1,5 @@ -const MyVC = require('./index'); +const MyVC = require('./myvc'); const fs = require('fs-extra'); class Init { diff --git a/myvc-pull.js b/myvc-pull.js index 71cbb4e..9d565f3 100755 --- a/myvc-pull.js +++ b/myvc-pull.js @@ -1,5 +1,5 @@ -const MyVC = require('./index'); +const MyVC = require('./myvc'); const fs = require('fs-extra'); const ejs = require('ejs'); diff --git a/myvc-push.js b/myvc-push.js index 9fca4c5..11d6230 100644 --- a/myvc-push.js +++ b/myvc-push.js @@ -1,5 +1,5 @@ -const MyVC = require('./index'); +const MyVC = require('./myvc'); const fs = require('fs-extra'); const nodegit = require('nodegit'); @@ -58,7 +58,7 @@ class Push { version = userVersion; } - if (opts.env == 'production') { + if (opts.remote == 'production') { console.log( '\n ( ( ) ( ( ) ) ' + '\n )\\ ))\\ ) ( /( )\\ ) ( ))\\ ) ( /( ( /( ' diff --git a/myvc-run.js b/myvc-run.js index fa3cee1..8165a4a 100644 --- a/myvc-run.js +++ b/myvc-run.js @@ -1,6 +1,7 @@ -const MyVC = require('./index'); +const MyVC = require('./myvc'); const docker = require('./docker'); +const Container = require('./docker').Container; const fs = require('fs-extra'); const path = require('path'); const Server = require('./server/server'); @@ -37,7 +38,7 @@ class Run { const changes = await myvc.changedRoutines(version.gitCommit); 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++) { isEqual = cache[i].path == changes[i].path && cache[i].mark == changes[i].mark; @@ -45,6 +46,7 @@ class Run { } if (!isEqual) { + console.log('not equal'); const fd = await fs.open(`${dumpDir}/.changes`, 'w+'); for (const change of changes) fs.write(fd, change.mark + change.path + '\n'); @@ -85,7 +87,8 @@ class Run { publish: `3306:${dbConfig.port}` }; try { - await this.rm(); + const server = new Server(new Container(opts.code)); + await server.rm(); } catch (e) {} } diff --git a/myvc-start.js b/myvc-start.js index e590f27..e6ad9f0 100644 --- a/myvc-start.js +++ b/myvc-start.js @@ -1,5 +1,5 @@ -const MyVC = require('./index'); +const MyVC = require('./myvc'); const Container = require('./docker').Container; const Server = require('./server/server'); const Run = require('./myvc-run'); diff --git a/myvc.js b/myvc.js index 755a52e..bb3453a 100755 --- a/myvc.js +++ b/myvc.js @@ -1,4 +1,255 @@ #!/usr/bin/env node -const MyVC = require('./'); -new MyVC().run(); +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: { + 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(); diff --git a/package.json b/package.json index 7acbb6d..c44b9ee 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "myvc", - "version": "1.1.3", + "version": "1.1.4", "author": "Verdnatura Levante SL", "description": "MySQL Version Control", "license": "GPL-3.0", - "bin": "myvc.js", + "bin": "cli.js", "repository": { "type": "git", "url": "https://github.com/verdnatura/myvc.git" diff --git a/server/Dockerfile b/server/Dockerfile index 4b38305..21d693a 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -32,7 +32,6 @@ RUN npm install --only=prod COPY \ structure.sql \ - index.js \ myvc.js \ myvc-push.js \ myvc.default.yml \ diff --git a/Dockerfile.client b/server/Dockerfile.client similarity index 100% rename from Dockerfile.client rename to server/Dockerfile.client