refs #4036 Myt: class code clean/refactor, template fix

This commit is contained in:
Juan Ferrer 2022-12-29 10:15:02 +01:00
parent 0cb3ec8573
commit 9f4a01ad97
21 changed files with 407 additions and 383 deletions

View File

@ -2,12 +2,9 @@
* Base class for Myt commands. * Base class for Myt commands.
*/ */
module.exports = class MytCommand { module.exports = class MytCommand {
get usage() { constructor(myt, opts) {
return {}; this.myt = myt;
} this.opts = opts;
get localOpts() {
return {};
} }
async run(myt, opts) { async run(myt, opts) {

141
lib/conn.js Normal file
View File

@ -0,0 +1,141 @@
const fs = require('fs-extra');
/**
* Executes an SQL script.
*
* @param {Connection} conn MySQL connection object
* @returns {Array<Result>} The resultset
*/
async function queryFromFile(conn, file) {
const sql = await fs.readFile(file, 'utf8');
return await this.multiQuery(conn, sql);
}
/**
* Executes a multi-query string.
*
* @param {Connection} conn MySQL connection object
* @param {String} sql SQL multi-query string
* @returns {Array<Result>} The resultset
*/
async function multiQuery(conn, sql) {
let results = [];
const stmts = this.querySplit(sql);
for (const stmt of stmts)
results = results.concat(await conn.query(stmt));
return results;
}
/**
* Splits an SQL muti-query into a single-query array, it does an small
* parse to correctly handle the DELIMITER statement.
*
* @param {Array<String>} stmts The splitted SQL statements
*/
function querySplit(sql) {
const stmts = [];
let i,
char,
token,
escaped,
stmtStart;
let delimiter = ';';
const delimiterRe = /\s*delimiter\s+(\S+)[^\S\r\n]*(?:\r?\n|\r|$)/yi;
function begins(str) {
let j;
for (j = 0; j < str.length; j++)
if (sql[i + j] != str[j])
return false;
i += j;
return true;
}
for (i = 0; i < sql.length;) {
stmtStart = i;
delimiterRe.lastIndex = i;
const match = sql.match(delimiterRe);
if (match) {
delimiter = match[1];
i += match[0].length;
continue;
}
let delimiterFound = false;
while (i < sql.length) {
char = sql[i];
if (token) {
if (!escaped && begins(token.end))
token = null;
else {
escaped = !escaped && token.escape(char);
i++;
}
} else {
delimiterFound = begins(delimiter);
if (delimiterFound) break;
const tok = tokenIndex.get(char);
if (tok && begins(tok.start))
token = tok;
else
i++;
}
}
let len = i - stmtStart;
if (delimiterFound) len -= delimiter.length;
const stmt = sql.substr(stmtStart, len);
if (!/^\s*$/.test(stmt))
stmts.push(stmt);
}
return stmts;
}
const tokens = {
string: {
start: '\'',
end: '\'',
escape: char => char == '\'' || char == '\\'
},
quotedString: {
start: '"',
end: '"',
escape: char => char == '"' || char == '\\'
},
id: {
start: '`',
end: '`',
escape: char => char == '`'
},
multiComment: {
start: '/*',
end: '*/',
escape: () => false
},
singleComment: {
start: '-- ',
end: '\n',
escape: () => false
}
};
const tokenIndex = new Map();
for (const tokenId in tokens) {
const token = tokens[tokenId];
tokenIndex.set(token.start[0], token);
}
module.exports = {
queryFromFile,
multiQuery,
querySplit,
tokens
};

83
lib/dumper.js Normal file
View File

@ -0,0 +1,83 @@
const docker = require('./docker');
const fs = require('fs-extra');
const path = require('path');
module.exports = class Dumper {
constructor(opts) {
this.opts = opts;
}
async init(dumpFile) {
const dumpDir = this.opts.dumpDir;
if (!await fs.pathExists(dumpDir))
await fs.mkdir(dumpDir);
const dumpPath = path.join(dumpDir, dumpFile);
// FIXME: If it's called after docker.build() statement it creates an
// "invalid" WriteStream
const dumpStream = await fs.createWriteStream(dumpPath);
const buidDir = path.join(__dirname, '..',)
await docker.build(buidDir, {
tag: 'myt/client',
file: path.join(buidDir, 'server', 'Dockerfile.client')
}, this.opts.debug);
this.dumpStream = dumpStream;
}
async use(schema) {
const escapedSchema = '`'+ schema.replace('`', '``') +'`';
await this.dumpStream.write(
`USE ${escapedSchema};\n`,
'utf8'
);
}
async dumpFixtures(tables, replace) {
const fixturesArgs = [
'--no-create-info',
'--skip-triggers',
'--skip-extended-insert',
'--skip-disable-keys',
'--skip-add-locks',
'--skip-set-charset',
'--skip-comments',
'--skip-tz-utc'
];
if (replace)
fixturesArgs.push('--replace');
for (const schema in tables) {
await this.use(schema);
const args = fixturesArgs.concat([schema], tables[schema]);
await this.runDump('mysqldump', args, this.dumpStream);
}
}
async runDump(command, args) {
const iniPath = path.join(this.opts.subdir || '', 'remotes', this.opts.iniFile);
const myArgs = [
`--defaults-file=${iniPath}`
];
const execOptions = {
stdio: [
process.stdin,
this.dumpStream,
process.stderr
]
};
const commandArgs = [command].concat(myArgs, args);
await docker.run('myt/client', commandArgs, {
addHost: 'host.docker.internal:host-gateway',
volume: `${this.opts.mytDir}:/workspace`,
rm: true
}, execOptions);
}
async end() {
await this.dumpStream.end();
}
}

29
lib/repo.js Normal file
View File

@ -0,0 +1,29 @@
const nodegit = require('nodegit');
async function getStaged(repo) {
const head = await repo.getHeadCommit();
try {
const emptyTree = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
const headTree = await (head
? head.getTree()
: nodegit.Tree.lookup(repo, emptyTree)
);
return await nodegit.Diff.treeToIndex(repo, headTree, null);
} catch (err) {
console.warn('Cannot fetch staged changes:', err.message);
}
}
async function getUnstaged(repo) {
const Diff = nodegit.Diff;
return await Diff.indexToWorkdir(repo, null, {
flags: Diff.OPTION.SHOW_UNTRACKED_CONTENT
| Diff.OPTION.RECURSE_UNTRACKED_DIRS
});
}
module.exports = {
getStaged,
getUnstaged
};

View File

@ -1,6 +1,7 @@
function camelToSnake(str) { function camelToSnake(str) {
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`); return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
} }
module.exports.camelToSnake = camelToSnake; module.exports = {
camelToSnake
};

View File

@ -1,4 +1,3 @@
const Myt = require('./myt'); const Myt = require('./myt');
const Command = require('./lib/command'); const Command = require('./lib/command');
const fs = require('fs-extra'); const fs = require('fs-extra');
@ -11,7 +10,7 @@ class Clean extends Command {
description: 'Cleans old applied versions' description: 'Cleans old applied versions'
}; };
static localOpts = { static opts = {
default: { default: {
remote: 'production' remote: 'production'
} }

View File

@ -1,7 +1,7 @@
const Myt = require('./myt'); const Myt = require('./myt');
const Command = require('./lib/command'); const Command = require('./lib/command');
const fs = require('fs-extra'); const fs = require('fs-extra');
const Dumper = require('./lib/dumper');
class Dump extends Command { class Dump extends Command {
static usage = { static usage = {
@ -9,14 +9,15 @@ class Dump extends Command {
operand: 'remote' operand: 'remote'
}; };
static localOpts = { static opts = {
default: { default: {
remote: 'production' remote: 'production'
} }
}; };
async run(myt, opts) { async run(myt, opts) {
const dumpStream = await myt.initDump('.dump.sql'); const dumper = new Dumper(opts);
await dumper.init('.dump.sql');
console.log('Dumping structure.'); console.log('Dumping structure.');
let dumpArgs = [ let dumpArgs = [
@ -29,10 +30,10 @@ class Dump extends Command {
'--databases' '--databases'
]; ];
dumpArgs = dumpArgs.concat(opts.schemas); dumpArgs = dumpArgs.concat(opts.schemas);
await myt.runDump('docker-dump.sh', dumpArgs, dumpStream); await dumper.runDump('docker-dump.sh', dumpArgs);
console.log('Dumping fixtures.'); console.log('Dumping fixtures.');
await myt.dumpFixtures(dumpStream, opts.fixtures); await dumper.dumpFixtures(opts.fixtures);
console.log('Dumping privileges.'); console.log('Dumping privileges.');
const privs = opts.privileges; const privs = opts.privileges;
@ -46,11 +47,11 @@ class Dump extends Command {
if (privs.where) args.push('--where', privs.where); if (privs.where) args.push('--where', privs.where);
args = args.concat(['mysql'], privs.tables); args = args.concat(['mysql'], privs.tables);
await dumpStream.write('USE `mysql`;\n', 'utf8'); await dumper.use('mysql');
await myt.runDump('mysqldump', args, dumpStream); await dumper.runDump('mysqldump', args);
} }
await dumpStream.end(); await dumper.end();
console.log('Saving version.'); console.log('Saving version.');
await myt.dbConnect(); await myt.dbConnect();

View File

@ -1,6 +1,6 @@
const Myt = require('./myt'); const Myt = require('./myt');
const Command = require('./lib/command'); const Command = require('./lib/command');
const Dumper = require('./lib/dumper');
class Fixtures extends Command { class Fixtures extends Command {
static usage = { static usage = {
@ -8,16 +8,17 @@ class Fixtures extends Command {
operand: 'remote' operand: 'remote'
}; };
static localOpts = { static opts = {
default: { default: {
remote: 'docker' remote: 'docker'
} }
}; };
async run(myt, opts) { async run(myt, opts) {
const dumpStream = await myt.initDump('fixtures.sql'); const dumper = new Dumper(opts);
await myt.dumpFixtures(dumpStream, opts.localFixtures, true); await dumper.init('fixtures.sql');
await dumpStream.end(); await dumper.dumpFixtures(opts.localFixtures, true);
await dumper.end();
} }
} }

View File

@ -1,4 +1,3 @@
const Myt = require('./myt'); const Myt = require('./myt');
const Command = require('./lib/command'); const Command = require('./lib/command');
const fs = require('fs-extra'); const fs = require('fs-extra');

View File

@ -1,9 +1,9 @@
const Myt = require('./myt'); const Myt = require('./myt');
const Command = require('./lib/command'); const Command = require('./lib/command');
const fs = require('fs-extra'); const fs = require('fs-extra');
const nodegit = require('nodegit'); const nodegit = require('nodegit');
const ExporterEngine = require('./lib/exporter-engine'); const ExporterEngine = require('./lib/exporter-engine');
const repoExt = require('./lib/repo');
class Pull extends Command { class Pull extends Command {
static usage = { static usage = {
@ -17,7 +17,7 @@ class Pull extends Command {
operand: 'remote' operand: 'remote'
}; };
static localOpts = { static opts = {
alias: { alias: {
force: 'f', force: 'f',
checkout: 'c', checkout: 'c',
@ -52,14 +52,14 @@ class Pull extends Command {
// Check for unstaged changes // Check for unstaged changes
const unstagedDiff = await myt.getUnstaged(repo); const unstagedDiff = await repoExt.getUnstaged(repo);
if (await hasChanges(unstagedDiff)) if (await hasChanges(unstagedDiff))
throw new Error('You have unstaged changes, save them before pull'); throw new Error('You have unstaged changes, save them before pull');
// Check for staged changes // Check for staged changes
const stagedDiff = await myt.getStaged(repo); const stagedDiff = await repoExt.getStaged(repo);
if (await hasChanges(stagedDiff)) if (await hasChanges(stagedDiff))
throw new Error('You have staged changes, save them before pull'); throw new Error('You have staged changes, save them before pull');

View File

@ -1,9 +1,10 @@
const Myt = require('./myt'); const Myt = require('./myt');
const Command = require('./lib/command'); const Command = require('./lib/command');
const fs = require('fs-extra'); const fs = require('fs-extra');
const nodegit = require('nodegit'); const nodegit = require('nodegit');
const ExporterEngine = require('./lib/exporter-engine'); const ExporterEngine = require('./lib/exporter-engine');
const connExt = require('./lib/conn');
const repoExt = require('./lib/repo');
/** /**
* Pushes changes to remote. * Pushes changes to remote.
@ -20,7 +21,7 @@ class Push extends Command {
operand: 'remote' operand: 'remote'
}; };
static localOpts = { static opts = {
alias: { alias: {
force: 'f', force: 'f',
commit: 'c', commit: 'c',
@ -217,7 +218,7 @@ class Push extends Command {
let err; let err;
try { try {
await myt.queryFromFile(pushConn, await connExt.queryFromFile(pushConn,
`${scriptsDir}/${script}`); `${scriptsDir}/${script}`);
} catch (e) { } catch (e) {
err = e; err = e;
@ -261,7 +262,7 @@ class Push extends Command {
const gitExists = await fs.pathExists(`${opts.workspace}/.git`); const gitExists = await fs.pathExists(`${opts.workspace}/.git`);
let nRoutines = 0; let nRoutines = 0;
let changes = await myt.changedRoutines(version.gitCommit); let changes = await this.changedRoutines(version.gitCommit);
changes = this.parseChanges(changes); changes = this.parseChanges(changes);
const routines = []; const routines = [];
@ -334,7 +335,7 @@ class Push extends Command {
if (change.type.name === 'VIEW') if (change.type.name === 'VIEW')
await pushConn.query(`USE ${scapedSchema}`); await pushConn.query(`USE ${scapedSchema}`);
await myt.multiQuery(pushConn, newSql); await connExt.multiQuery(pushConn, newSql);
if (change.isRoutine) { if (change.isRoutine) {
await conn.query( await conn.query(
@ -416,6 +417,68 @@ class Push extends Command {
); );
} }
async changedRoutines(commitSha) {
const repo = await this.myt.openRepo();
const changes = [];
const changesMap = new Map();
async function pushChanges(diff) {
if (!diff) return;
const patches = await diff.patches();
for (const patch of patches) {
const path = patch.newFile().path();
const match = path.match(/^routines\/(.+)\.sql$/);
if (!match) continue;
let change = changesMap.get(match[1]);
if (!change) {
change = {path: match[1]};
changes.push(change);
changesMap.set(match[1], change);
}
change.mark = patch.isDeleted() ? '-' : '+';
}
}
const head = await repo.getHeadCommit();
if (head && commitSha) {
let commit;
let notFound;
try {
commit = await repo.getCommit(commitSha);
notFound = false;
} catch (err) {
if (err.errorFunction == 'Commit.lookup')
notFound = true;
else
throw err;
}
if (notFound) {
console.warn(`Database commit not found, trying git fetch`.yellow);
await repo.fetchAll();
commit = await repo.getCommit(commitSha);
}
const commitTree = await commit.getTree();
const headTree = await head.getTree();
const diff = await headTree.diff(commitTree);
await pushChanges(diff);
}
await pushChanges(await repoExt.getUnstaged(repo));
await pushChanges(await repoExt.getStaged(repo));
return changes.sort((a, b) => {
if (b.mark != a.mark)
return b.mark == '-' ? 1 : -1;
return a.path.localeCompare(b.path);
});
}
} }
const typeMap = { const typeMap = {

View File

@ -1,4 +1,3 @@
const Myt = require('./myt'); const Myt = require('./myt');
const Command = require('./lib/command'); const Command = require('./lib/command');
const Push = require('./myt-push'); const Push = require('./myt-push');
@ -6,6 +5,7 @@ const docker = require('./lib/docker');
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('path'); const path = require('path');
const Server = require('./lib/server'); const Server = require('./lib/server');
const connExt = require('./lib/conn');
/** /**
* Builds the database image and runs a container. It only rebuilds the * Builds the database image and runs a container. It only rebuilds the
@ -22,7 +22,7 @@ class Run extends Command {
} }
}; };
static localOpts = { static opts = {
alias: { alias: {
ci: 'c', ci: 'c',
random: 'r' random: 'r'
@ -144,7 +144,7 @@ class Run extends Command {
const triggersDir = await fs.readdir(triggersPath); const triggersDir = await fs.readdir(triggersPath);
for (const triggerFile of triggersDir) for (const triggerFile of triggersDir)
await myt.queryFromFile(conn, `${triggersPath}/${triggerFile}`); await connExt.queryFromFile(conn, `${triggersPath}/${triggerFile}`);
} }
return server; return server;

View File

@ -1,4 +1,3 @@
const Myt = require('./myt'); const Myt = require('./myt');
const Command = require('./lib/command'); const Command = require('./lib/command');
const Container = require('./lib/docker').Container; const Container = require('./lib/docker').Container;

View File

@ -1,4 +1,3 @@
const Myt = require('./myt'); const Myt = require('./myt');
const Command = require('./lib/command'); const Command = require('./lib/command');
const fs = require('fs-extra'); const fs = require('fs-extra');
@ -15,7 +14,7 @@ class Version extends Command {
operand: 'name' operand: 'name'
}; };
static localOpts = { static opts = {
alias: { alias: {
name: 'n' name: 'n'
}, },

332
myt.js
View File

@ -10,12 +10,9 @@ 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/util').camelToSnake; const camelToSnake = require('./lib/util').camelToSnake;
const docker = require('./lib/docker');
const Command = require('./lib/command');
class Myt { class Myt {
get usage() { static usage = {
return {
description: 'Utility for database versioning', description: 'Utility for database versioning',
params: { params: {
remote: 'Name of remote to use', remote: 'Name of remote to use',
@ -26,10 +23,8 @@ class Myt {
help: 'Display this help message' help: 'Display this help message'
} }
}; };
}
get localOpts() { static opts = {
return {
alias: { alias: {
remote: 'r', remote: 'r',
workspace: 'w', workspace: 'w',
@ -42,20 +37,19 @@ class Myt {
'debug', 'debug',
'version', 'version',
'help' 'help'
], ]
default: {
workspace: process.cwd()
}
}; };
}
async run(CommandClass) { async run(Command) {
console.log( console.log(
'Myt'.green, 'Myt'.green,
`v${packageJson.version}`.magenta `v${packageJson.version}`.magenta
); );
const baseOpts = this.localOpts; let baseOpts = this.constructor.opts;
baseOpts.default = Object.assign(baseOpts.default || {}, {
workspace: process.cwd()
});
const opts = this.getopts(baseOpts); const opts = this.getopts(baseOpts);
if (opts.debug) { if (opts.debug) {
@ -68,7 +62,7 @@ class Myt {
try { try {
const commandName = opts._[0]; const commandName = opts._[0];
if (!CommandClass && commandName) { if (!Command && commandName) {
if (!/^[a-z]+$/.test(commandName)) if (!/^[a-z]+$/.test(commandName))
throw new Error (`Invalid command name '${commandName}'`); throw new Error (`Invalid command name '${commandName}'`);
@ -76,20 +70,20 @@ class Myt {
if (!await fs.pathExists(commandFile)) if (!await fs.pathExists(commandFile))
throw new Error (`Unknown command '${commandName}'`); throw new Error (`Unknown command '${commandName}'`);
CommandClass = require(commandFile); Command = require(commandFile);
} }
if (!CommandClass) { if (!Command) {
this.showHelp(baseOpts, this.usage); this.showHelp(baseOpts, this.constructor.usage);
process.exit(0); process.exit(0);
} }
const allOpts = Object.assign({}, baseOpts); const allOpts = Object.assign({}, baseOpts);
if (CommandClass.localOpts) if (Command.opts)
for (const key in CommandClass.localOpts) { for (const key in Command.opts) {
const baseValue = baseOpts[key]; const baseValue = baseOpts[key];
const cmdValue = CommandClass.localOpts[key]; const cmdValue = Command.opts[key];
if (Array.isArray(baseValue)) if (Array.isArray(baseValue))
allOpts[key] = baseValue.concat(cmdValue); allOpts[key] = baseValue.concat(cmdValue);
else if (typeof baseValue == 'object') else if (typeof baseValue == 'object')
@ -103,7 +97,7 @@ class Myt {
console.log('Command options:'.magenta, commandOpts); console.log('Command options:'.magenta, commandOpts);
Object.assign(opts, commandOpts); Object.assign(opts, commandOpts);
const operandToOpt = CommandClass.usage.operand; const operandToOpt = Command.usage.operand;
if (opts._.length >= 2 && operandToOpt) if (opts._.length >= 2 && operandToOpt)
opts[operandToOpt] = opts._[1]; opts[operandToOpt] = opts._[1];
@ -111,7 +105,7 @@ class Myt {
console.log('Final options:'.magenta, opts); console.log('Final options:'.magenta, opts);
if (opts.help) { if (opts.help) {
this.showHelp(CommandClass.localOpts, CommandClass.usage, commandName); this.showHelp(Command.opts, Command.usage, commandName);
process.exit(0); process.exit(0);
} }
@ -150,7 +144,7 @@ class Myt {
parameter('Remote:', opts.remote || 'local'); parameter('Remote:', opts.remote || 'local');
await this.load(opts); await this.load(opts);
await this.runCommand(CommandClass, opts); await this.runCommand(Command, opts);
await this.unload(); await this.unload();
} catch (err) { } catch (err) {
if (err.name == 'Error' && !opts.debug) { if (err.name == 'Error' && !opts.debug) {
@ -169,9 +163,8 @@ class Myt {
process.exit(); process.exit();
} }
async runCommand(CommandClass, opts) { async runCommand(Command, opts) {
const command = new CommandClass(); const command = new Command(this, opts);
command.opts = opts;
return await command.run(this, opts); return await command.run(this, opts);
} }
@ -315,70 +308,6 @@ class Myt {
}; };
} }
async changedRoutines(commitSha) {
const repo = await this.openRepo();
const changes = [];
const changesMap = new Map();
async function pushChanges(diff) {
if (!diff) return;
const patches = await diff.patches();
for (const patch of patches) {
const path = patch.newFile().path();
const match = path.match(/^routines\/(.+)\.sql$/);
if (!match) continue;
let change = changesMap.get(match[1]);
if (!change) {
change = {path: match[1]};
changes.push(change);
changesMap.set(match[1], change);
}
change.mark = patch.isDeleted() ? '-' : '+';
}
}
const head = await repo.getHeadCommit();
if (head && commitSha) {
let commit;
let notFound;
try {
commit = await repo.getCommit(commitSha);
notFound = false;
} catch (err) {
if (err.errorFunction == 'Commit.lookup')
notFound = true;
else
throw err;
}
if (notFound) {
console.warn(`Database commit not found, trying git fetch`.yellow);
await repo.fetchAll();
commit = await repo.getCommit(commitSha);
}
const commitTree = await commit.getTree();
const headTree = await head.getTree();
const diff = await headTree.diff(commitTree);
await pushChanges(diff);
}
await pushChanges(await this.getUnstaged(repo));
await pushChanges(await this.getStaged(repo));
return changes.sort((a, b) => {
if (b.mark != a.mark)
return b.mark == '-' ? 1 : -1;
return a.path.localeCompare(b.path);
});
}
async openRepo() { async openRepo() {
const {opts} = this; const {opts} = this;
@ -388,94 +317,6 @@ class Myt {
return await nodegit.Repository.open(opts.workspace); return await nodegit.Repository.open(opts.workspace);
} }
async getStaged(repo) {
const head = await repo.getHeadCommit();
try {
const emptyTree = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
const headTree = await (head
? head.getTree()
: nodegit.Tree.lookup(repo, emptyTree)
);
return await nodegit.Diff.treeToIndex(repo, headTree, null);
} catch (err) {
console.warn('Cannot fetch staged changes:', err.message);
}
}
async getUnstaged(repo) {
return await nodegit.Diff.indexToWorkdir(repo, null, {
flags: nodegit.Diff.OPTION.SHOW_UNTRACKED_CONTENT
| nodegit.Diff.OPTION.RECURSE_UNTRACKED_DIRS
});
}
async initDump(dumpFile) {
const dumpDir = this.opts.dumpDir;
if (!await fs.pathExists(dumpDir))
await fs.mkdir(dumpDir);
const dumpPath = path.join(dumpDir, dumpFile);
// FIXME: If it's called after docker.build() statement it creates an
// "invalid" WriteStream
const dumpStream = await fs.createWriteStream(dumpPath);
await docker.build(__dirname, {
tag: 'myt/client',
file: path.join(__dirname, 'server', 'Dockerfile.client')
}, this.opts.debug);
return dumpStream;
}
async dumpFixtures(dumpStream, tables, replace) {
const fixturesArgs = [
'--no-create-info',
'--skip-triggers',
'--skip-extended-insert',
'--skip-disable-keys',
'--skip-add-locks',
'--skip-set-charset',
'--skip-comments',
'--skip-tz-utc'
];
if (replace)
fixturesArgs.push('--replace');
for (const schema in tables) {
const escapedSchema = '`'+ schema.replace('`', '``') +'`';
await dumpStream.write(
`USE ${escapedSchema};\n`,
'utf8'
);
const args = fixturesArgs.concat([schema], tables[schema]);
await this.runDump('mysqldump', args, dumpStream);
}
}
async runDump(command, args, dumpStream) {
const iniPath = path.join(this.opts.subdir || '', 'remotes', this.opts.iniFile);
const myArgs = [
`--defaults-file=${iniPath}`
];
const execOptions = {
stdio: [
process.stdin,
dumpStream,
process.stderr
]
};
const commandArgs = [command].concat(myArgs, args);
await docker.run('myt/client', commandArgs, {
addHost: 'host.docker.internal:host-gateway',
volume: `${this.opts.mytDir}:/workspace`,
rm: true
}, execOptions);
}
showHelp(opts, usage, command) { showHelp(opts, usage, command) {
const prefix = `${'Usage:'.gray} [npx] myt`; const prefix = `${'Usage:'.gray} [npx] myt`;
@ -505,139 +346,6 @@ class Myt {
} }
} }
} }
/**
* Executes an SQL script.
*
* @param {Connection} conn MySQL connection object
* @returns {Array<Result>} The resultset
*/
async queryFromFile(conn, file) {
const sql = await fs.readFile(file, 'utf8');
return await this.multiQuery(conn, sql);
}
/**
* Executes a multi-query string.
*
* @param {Connection} conn MySQL connection object
* @param {String} sql SQL multi-query string
* @returns {Array<Result>} The resultset
*/
async multiQuery(conn, sql) {
let results = [];
const stmts = this.querySplit(sql);
for (const stmt of stmts)
results = results.concat(await conn.query(stmt));
return results;
}
/**
* Splits an SQL muti-query into a single-query array, it does an small
* parse to correctly handle the DELIMITER statement.
*
* @param {Array<String>} stmts The splitted SQL statements
*/
querySplit(sql) {
const stmts = [];
let i,
char,
token,
escaped,
stmtStart;
let delimiter = ';';
const delimiterRe = /\s*delimiter\s+(\S+)[^\S\r\n]*(?:\r?\n|\r|$)/yi;
function begins(str) {
let j;
for (j = 0; j < str.length; j++)
if (sql[i + j] != str[j])
return false;
i += j;
return true;
}
for (i = 0; i < sql.length;) {
stmtStart = i;
delimiterRe.lastIndex = i;
const match = sql.match(delimiterRe);
if (match) {
delimiter = match[1];
i += match[0].length;
continue;
}
let delimiterFound = false;
while (i < sql.length) {
char = sql[i];
if (token) {
if (!escaped && begins(token.end))
token = null;
else {
escaped = !escaped && token.escape(char);
i++;
}
} else {
delimiterFound = begins(delimiter);
if (delimiterFound) break;
const tok = tokenIndex.get(char);
if (tok && begins(tok.start))
token = tok;
else
i++;
}
}
let len = i - stmtStart;
if (delimiterFound) len -= delimiter.length;
const stmt = sql.substr(stmtStart, len);
if (!/^\s*$/.test(stmt))
stmts.push(stmt);
}
return stmts;
}
}
const tokens = {
string: {
start: '\'',
end: '\'',
escape: char => char == '\'' || char == '\\'
},
quotedString: {
start: '"',
end: '"',
escape: char => char == '"' || char == '\\'
},
id: {
start: '`',
end: '`',
escape: char => char == '`'
},
multiComment: {
start: '/*',
end: '*/',
escape: () => false
},
singleComment: {
start: '-- ',
end: '\n',
escape: () => false
}
};
const tokenIndex = new Map();
for (const tokenId in tokens) {
const token = tokens[tokenId];
tokenIndex.set(token.start[0], token);
} }
module.exports = Myt; module.exports = Myt;

8
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "@verdnatura/myt", "name": "@verdnatura/myt",
"version": "1.5.6", "version": "1.5.7",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@verdnatura/myt", "name": "@verdnatura/myt",
"version": "1.5.6", "version": "1.5.7",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@sqltools/formatter": "^1.2.3", "@sqltools/formatter": "^1.2.3",
@ -21,9 +21,7 @@
"sha.js": "^2.4.11" "sha.js": "^2.4.11"
}, },
"bin": { "bin": {
"myt": "cli.js", "myt": "cli.js"
"myv": "cli.js",
"myvc": "cli.js"
} }
}, },
"node_modules/@sindresorhus/is": { "node_modules/@sindresorhus/is": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@verdnatura/myt", "name": "@verdnatura/myt",
"version": "1.5.6", "version": "1.5.7",
"author": "Verdnatura Levante SL", "author": "Verdnatura Levante SL",
"description": "MySQL version control", "description": "MySQL version control",
"license": "GPL-3.0", "license": "GPL-3.0",

1
template/.gitignore vendored
View File

@ -2,4 +2,5 @@
node_modules node_modules
remotes/*.ini remotes/*.ini
!remotes/local.ini !remotes/local.ini
!remotes/docker.ini
dump/.changes dump/.changes

View File

@ -8,6 +8,6 @@
"type": "git" "type": "git"
}, },
"dependencies": { "dependencies": {
"@verdnatura/myt": "^1.5.6" "@verdnatura/myt": "^1.5.7"
} }
} }

View File

@ -0,0 +1,5 @@
[client]
host = host.docker.internal
port = 3306
user = root
password = root