Merge pull request 'feat: refs #5483 Split logging from logic' (!3) from 5483-unifyDb into master

Reviewed-on: #3
This commit is contained in:
Juan Ferrer 2024-01-19 10:54:25 +00:00
commit 44957a2275
17 changed files with 260 additions and 158 deletions

View File

@ -275,8 +275,8 @@ $ myt fixtures [<remote>]
### run
Builds and starts local database server container. It only rebuilds the image
dump has been modified.
Builds and starts local database server container. It only rebuilds the image
when dump have been modified.
```text
$ myt run [-c|--ci] [-r|--random]

2
cli.js
View File

@ -1,4 +1,4 @@
#!/usr/bin/env node
const Myt = require('./myt');
new Myt().run();
new Myt().cli();

View File

@ -1,19 +1,37 @@
/**
* Base class for Myt commands.
*/
module.exports = class MytCommand {
module.exports = class MytCommand{
constructor(myt, opts) {
this.myt = myt;
this.opts = opts;
this.handlers = {};
}
async cli(myt, opts) {
const reporter = this.constructor.reporter;
if (reporter)
for (const event in reporter) {
const handler = reporter[event];
if (typeof handler == 'string') {
this.on(event, () => console.log(handler));
} else if (handler instanceof Function)
this.on(event, handler);
}
await this.run(myt, opts);
}
async run(myt, opts) {
throw new Error('run command not defined');
}
emit(event) {
const messages = this.constructor.messages;
if (messages && messages[event])
console.log(messages[event]);
on(event, handler) {
this.handlers[event] = handler;
}
emit(event, ...args) {
const handler = this.handlers[event];
if (handler) handler (...args);
}
}

View File

@ -25,8 +25,6 @@ module.exports = class Server {
port: dbConfig.port
};
console.log('Waiting for MySQL init process...');
async function checker() {
elapsedTime += interval;
let status;
@ -46,10 +44,7 @@ module.exports = class Server {
conn.on('error', () => {});
conn.connect(err => {
conn.destroy();
if (!err) {
console.log('MySQL process ready.');
return resolve();
}
if (!err) return resolve();
if (elapsedTime >= maxInterval)
reject(new Error(`MySQL not initialized whithin ${elapsedTime / 1000} secs`));

View File

@ -26,6 +26,21 @@ class Clean extends Command {
}
};
static reporter = {
versionsArchived: function(nVersions) {
if (nVersions)
console.log(` -> ${oldVersions.length} versions archived.`);
else
console.log(` -> No versions archived.`);
},
versionLogPurged: function(nPurged) {
if (nPurged)
console.log(` -> ${nPurged} changes purged from log.`);
else
console.log(` -> No logs purged.`);
}
};
async run(myt, opts) {
const conn = await myt.dbConnect();
const archiveDir = path.join(opts.versionsDir, '.archive');
@ -71,9 +86,9 @@ class Clean extends Command {
await fs.rmdir(srcDir);
}
console.log(` -> ${oldVersions.length} versions archived.`);
this.emit('versionsArchived', oldVersions.length);
} else
console.log(` -> No versions archived.`);
this.emit('versionsArchived');
if (opts.purge) {
const versionDb = new VersionDb(myt, opts.versionsDir);
@ -103,11 +118,8 @@ class Clean extends Command {
nPurged++;
}
}
if (nPurged)
console.log(` -> ${nPurged} versions purged from log.`);
else
console.log(` -> No versions purged from log.`);
this.emit('versionLogPurged', nPurged);
}
}
}
@ -146,4 +158,4 @@ class VersionDb {
module.exports = Clean;
if (require.main === module)
new Myt().run(Clean);
new Myt().cli(Clean);

View File

@ -27,6 +27,11 @@ class Create extends Command {
}
};
async cli(myt, opts) {
await super.cli(myt, opts);
console.log('Routine created.');
}
async run(myt, opts) {
const match = opts.name.match(/^(\w+)\.(\w+)$/);
if (!match)
@ -63,12 +68,10 @@ class Create extends Command {
const routineFile = `${routineDir}/${name}.sql`;
await fs.writeFile(routineFile, sql);
console.log('Routine created.');
}
}
module.exports = Create;
if (require.main === module)
new Myt().run(Create);
new Myt().cli(Create);

View File

@ -28,7 +28,7 @@ class Dump extends Command {
]
};
static messages = {
static reporter = {
dumpStructure: 'Dumping structure.',
dumpData: 'Dumping data.',
dumpPrivileges: 'Dumping privileges.',
@ -128,5 +128,4 @@ class Dump extends Command {
module.exports = Dump;
if (require.main === module)
new Myt().run(Dump);
new Myt().cli(Dump);

View File

@ -25,4 +25,4 @@ class Fixtures extends Command {
module.exports = Fixtures;
if (require.main === module)
new Myt().run(Fixtures);
new Myt().cli(Fixtures);

View File

@ -33,4 +33,4 @@ class Init extends Command {
module.exports = Init;
if (require.main === module)
new Myt().run(Init);
new Myt().cli(Init);

View File

@ -32,6 +32,13 @@ class Pull extends Command {
]
};
static reporter = {
creatingBranch: function(branchName) {
console.log(`Creating branch '${branchName}' from database commit.`);
},
routineChanges: 'Incorporating routine changes.'
};
async run(myt, opts) {
const conn = await myt.dbConnect();
const repo = await myt.openRepo();
@ -73,7 +80,7 @@ class Pull extends Command {
if (version && version.gitCommit) {
const now = parseInt(new Date().toJSON());
const branchName = `myt-pull_${now}`;
console.log(`Creating branch '${branchName}' from database commit.`);
this.emit('creatingBranch', branchName);
const commit = await repo.getCommit(version.gitCommit);
const branch = await nodegit.Branch.create(repo,
`myt-pull_${now}`, commit, () => {});
@ -83,7 +90,7 @@ class Pull extends Command {
// Export routines to SQL files
console.log(`Incorporating routine changes.`);
this.emit('routineChanges', branchName);
const engine = new ExporterEngine(conn, opts);
await engine.init();
@ -180,4 +187,4 @@ class Pull extends Command {
module.exports = Pull;
if (require.main === module)
new Myt().run(Pull);
new Myt().cli(Pull);

View File

@ -37,6 +37,95 @@ class Push extends Command {
]
};
static reporter = {
applyingVersions: 'Applying versions.',
applyingRoutines: 'Applying changed routines.',
dbInfo: function(version) {
console.log(
`Database information:`
+ `\n -> Version: ${version.number}`
+ `\n -> Commit: ${version.gitCommit}`
);
},
version(version, error) {
let actionMsg;
let number, color;
if (!error) {
actionMsg = version.apply
? '[A]'.green
: '[I]'.blue;
number = version.number;
color = 'cyan';
} else {
actionMsg = '[W]'.yellow;
switch(action) {
case 'badVersion':
number = '?????';
color = 'yellow';
break;
case 'wrongDirectory':
number = '*****';
color = 'gray';
break;
}
}
const numberMsg = `[${number}]`[color];
console.log('', `${actionMsg}${numberMsg}`.bold, version.name);
},
logScript(script) {
let actionMsg;
if (script.apply)
actionMsg = '[+]'.green;
else if (!script.matchRegex)
actionMsg = '[W]'.yellow;
else
actionMsg = '[I]'.blue;
console.log(' ', actionMsg.bold, script.file);
},
change(status, ignore, change) {
let actionMsg;
if (ignore)
actionMsg = '[I]'.blue;
else
actionMsg = '[A]'.green;
let statusMsg;
switch(status) {
case 'added':
statusMsg = '[+]'.green;
break;
case 'deleted':
statusMsg = '[-]'.red;
break;
case 'modified':
statusMsg = '[·]'.yellow;
break;
}
const typeMsg = `[${change.type.abbr}]`[change.type.color];
console.log('',
(actionMsg + statusMsg).bold,
typeMsg.bold,
change.fullName
);
},
versionsApplied: function(nVersions, nChanges) {
if (nVersions) {
console.log(` -> ${nVersions} versions with ${nChanges} changes applied.`);
} else
console.log(` -> No versions applied.`);
},
routinesApplied: function(nRoutines) {
if (nRoutines) {
console.log(` -> ${nRoutines} routines changed.`);
} else
console.log(` -> No routines changed.`);
}
};
async run(myt, opts) {
const conn = await myt.dbConnect();
this.conn = conn;
@ -85,24 +174,7 @@ class Push extends Command {
await releaseLock();
}
async push(myt, opts, conn) {
const pushConn = await myt.createConnection();
// Get database version
const dbVersion = await myt.fetchDbVersion() || {};
console.log(
`Database information:`
+ `\n -> Version: ${dbVersion.number}`
+ `\n -> Commit: ${dbVersion.gitCommit}`
);
if (!dbVersion.number)
dbVersion.number = String('0').padStart(opts.versionDigits, '0');
if (!/^[0-9]*$/.test(dbVersion.number))
throw new Error('Wrong database version');
async cli(myt, opts) {
// Prevent push to production by mistake
if (opts.remote == 'production') {
@ -134,34 +206,31 @@ class Push extends Command {
}
}
await super.cli(myt, opts);
}
async push(myt, opts, conn) {
const pushConn = await myt.createConnection();
// Get database version
const dbVersion = await myt.fetchDbVersion() || {};
this.emit('dbInfo', dbVersion);
if (!dbVersion.number)
dbVersion.number = String('0').padStart(opts.versionDigits, '0');
if (!/^[0-9]*$/.test(dbVersion.number))
throw new Error('Wrong database version');
// Apply versions
console.log('Applying versions.');
this.emit('applyingVersions');
let nVersions = 0;
let nChanges = 0;
let silent = true;
let showLog = false;
const versionsDir = opts.versionsDir;
function logVersion(version, name, action, error) {
let actionMsg;
switch(action) {
case 'apply':
actionMsg = '[A]'.green;
break;
case 'ignore':
actionMsg = '[I]'.blue;
break;
default:
actionMsg = '[W]'.yellow;
}
console.log('', (actionMsg + version).bold, name);
}
function logScript(type, message, error) {
console.log(' ', type.bold, message);
}
const skipFiles = new Set([
'README.md',
'.archive'
@ -173,44 +242,27 @@ class Push extends Command {
if (skipFiles.has(versionDir)) continue;
const version = await myt.loadVersion(versionDir);
if (!version) {
logVersion('[?????]'.yellow, versionDir, 'warn',
`Wrong directory name.`
);
continue;
}
if (version.number.length != dbVersion.number.length) {
logVersion('[*****]'.gray, versionDir, 'warn'
`Bad version length, should have ${dbVersion.number.length} characters.`
);
continue;
}
let apply = false;
const {apply} = version;
if (apply) silent = false;
if (silent) continue;
if (!version)
this.emit('version', version, 'wrongDirectory');
else if (version.number.length != dbVersion.number.length)
this.emit('version', version, 'badVersion');
else
apply = version.apply;
const action = apply ? 'apply' : 'ignore';
logVersion(`[${version.number}]`.cyan, version.name, action);
if (apply) showLog = true;
if (showLog) this.emit('version', version);
if (!apply) continue;
for (const script of version.scripts) {
const scriptFile = script.file;
if (!script.matchRegex) {
logScript('[W]'.yellow, scriptFile, `Wrong file name.`);
continue;
}
const actionMsg = script.apply ? '[+]'.green : '[I]'.blue;
logScript(actionMsg, scriptFile);
this.emit('logScript', script);
if (!script.apply) continue;
let err;
try {
await connExt.queryFromFile(pushConn,
`${versionsDir}/${versionDir}/${scriptFile}`);
`${versionsDir}/${versionDir}/${script.file}`);
} catch (e) {
err = e;
}
@ -232,7 +284,7 @@ class Push extends Command {
[
opts.code,
version.number,
scriptFile,
script.file,
err && err.errno,
err && err.message
]
@ -247,16 +299,11 @@ class Push extends Command {
}
}
if (nVersions) {
console.log(` -> ${nVersions} versions with ${nChanges} changes applied.`);
} else
console.log(` -> No versions applied.`);
this.emit('versionsApplied', nVersions, nChanges);
// Apply routines
console.log('Applying changed routines.');
const gitExists = await fs.pathExists(`${opts.workspace}/.git`);
this.emit('applyingRoutines');
let nRoutines = 0;
const changes = await this.changedRoutines(dbVersion.gitCommit);
@ -320,26 +367,15 @@ class Push extends Command {
&& opts.mockFunctions.indexOf(name) !== -1;
const ignore = newSql == oldSql || isMockFn;
let statusMsg;
let status;
if (exists && !oldSql)
statusMsg = '[+]'.green;
status = 'added';
else if (!exists)
statusMsg = '[-]'.red;
status = 'deleted';
else
statusMsg = '[·]'.yellow;
status = 'modified';
let actionMsg;
if (ignore)
actionMsg = '[I]'.blue;
else
actionMsg = '[A]'.green;
const typeMsg = `[${change.type.abbr}]`[change.type.color];
console.log('',
(actionMsg + statusMsg).bold,
typeMsg.bold,
change.fullName
);
this.emit('change', status, ignore, change);
if (!ignore) {
const scapedSchema = SqlString.escapeId(schema, true);
@ -387,11 +423,9 @@ class Push extends Command {
await finalize();
if (nRoutines) {
console.log(` -> ${nRoutines} routines changed.`);
} else
console.log(` -> No routines changed.`);
this.emit('routinesApplied', nRoutines);
const gitExists = await fs.pathExists(`${opts.workspace}/.git`);
if (gitExists && opts.commit) {
const repo = await nodegit.Repository.open(this.opts.workspace);
const head = await repo.getHeadCommit();
@ -558,4 +592,4 @@ class Routine {
module.exports = Push;
if (require.main === module)
new Myt().run(Push);
new Myt().cli(Push);

View File

@ -10,7 +10,7 @@ const SqlString = require('sqlstring');
/**
* Builds the database image and runs a container. It only rebuilds the image
* when dump has been modified. Some workarounds have been used to avoid a bug
* when dump have been modified. Some workarounds have been used to avoid a bug
* with OverlayFS driver on MacOS.
*/
class Run extends Command {
@ -33,6 +33,15 @@ class Run extends Command {
]
};
static reporter = {
buildingImage: 'Building container image.',
runningContainer: 'Running container.',
waitingDb: 'Waiting for MySQL init process.',
mockingDate: 'Mocking date functions.',
applyingFixtures: 'Applying fixtures.',
creatingTriggers: 'Creating triggers.'
};
async run(myt, opts) {
const dumpDir = opts.dumpDir;
const dumpDataDir = path.join(dumpDir, '.dump');
@ -43,6 +52,8 @@ class Run extends Command {
// Build base image
this.emit('buildingImage');
let basePath = dumpDir;
let baseDockerfile = path.join(dumpDir, 'Dockerfile');
@ -73,8 +84,10 @@ class Run extends Command {
// Run container
this.emit('runningContainer');
const isRandom = opts.random;
const dbConfig = Object.assign({}, opts.dbConfig);
const dbConfig = opts.dbConfig;
let runOptions;
@ -117,12 +130,13 @@ class Run extends Command {
}
}
this.emit('waitingDb');
await server.wait();
const conn = await myt.createConnection();
// Mock date functions
console.log('Mocking date functions.');
this.emit('mockingDate');
const mockDateScript = path.join(dumpDir, 'mockDate.sql');
if (opts.mockDate) {
@ -143,11 +157,11 @@ class Run extends Command {
commit: true,
dbConfig
});
await myt.runCommand(Push, opts);
await myt.run(Push, opts);
// Apply fixtures
console.log('Applying fixtures.');
this.emit('applyingFixtures');
const fixturesFiles = [
'fixtures.before',
'.fixtures',
@ -166,7 +180,7 @@ class Run extends Command {
// Create triggers
if (!hasTriggers) {
console.log('Creating triggers.');
this.emit('creatingTriggers');
for (const schema of opts.schemas) {
const triggersPath = `${opts.routinesDir}/${schema}/triggers`;
@ -179,6 +193,7 @@ class Run extends Command {
}
}
await conn.end();
return server;
}
}
@ -186,4 +201,4 @@ class Run extends Command {
module.exports = Run;
if (require.main === module)
new Myt().run(Run);
new Myt().cli(Run);

View File

@ -15,6 +15,10 @@ class Start extends Command {
description: 'Start local database server container'
};
static reporter = {
startingContainer: 'Starting container.'
};
async run(myt, opts) {
const ct = new Container(opts.code);
let status;
@ -27,7 +31,7 @@ class Start extends Command {
});
exists = true;
} catch (err) {
server = await myt.runCommand(Run, opts);
server = await myt.run(Run, opts);
}
if (exists) {
@ -35,6 +39,7 @@ class Start extends Command {
case 'running':
break;
case 'exited':
this.emit('startingContainer');
await ct.start();
server = new Server(ct, opts.dbConfig);
await server.wait();
@ -51,4 +56,4 @@ class Start extends Command {
module.exports = Start;
if (require.main === module)
new Myt().run(Start);
new Myt().cli(Start);

View File

@ -26,6 +26,19 @@ class Version extends Command {
}
};
static reporter = {
dbInfo: function(number, lastNumber) {
console.log(
`Database information:`
+ `\n -> Version: ${number}`
+ `\n -> Last version: ${lastNumber}`
);
},
versionCreated: function(versionName) {
console.log(`New version created: ${versionName}`);
}
};
async run(myt, opts) {
let newVersionDir;
@ -45,12 +58,7 @@ class Version extends Command {
);
const number = row && row.number;
const lastNumber = row && row.lastNumber;
console.log(
`Database information:`
+ `\n -> Version: ${number}`
+ `\n -> Last version: ${lastNumber}`
);
this.emit('dbInfo', number, lastNumber);
let newVersion;
if (lastNumber)
@ -117,7 +125,7 @@ class Version extends Command {
`${newVersionDir}/00-firstScript.sql`,
'-- Place your SQL code here\n'
);
console.log(`New version created: ${versionFolder}`);
this.emit('versionCreated', versionFolder);
await conn.query('COMMIT');
} catch (err) {
@ -215,4 +223,4 @@ const plants = [
module.exports = Version;
if (require.main === module)
new Myt().run(Version);
new Myt().cli(Version);

24
myt.js
View File

@ -40,7 +40,9 @@ class Myt {
]
};
async run(Command) {
async cli(Command) {
this.cliMode = true;
console.log(
'Myt'.green,
`v${packageJson.version}`.magenta
@ -144,9 +146,9 @@ class Myt {
parameter('Workspace:', opts.workspace);
parameter('Remote:', opts.remote || 'local');
await this.load(opts);
await this.runCommand(Command, opts);
await this.unload();
await this.init(opts);
await this.run(Command, opts);
await this.deinit();
} catch (err) {
if (err.name == 'Error' && !opts.debug) {
console.error('Error:'.gray, err.message.red);
@ -164,12 +166,16 @@ class Myt {
process.exit();
}
async runCommand(Command, opts) {
async run(Command, opts) {
if (!opts) opts = this.opts;
const command = new Command(this, opts);
return await command.run(this, opts);
if (this.cliMode)
return await command.cli(this, opts);
else
return await command.run(this, opts);
}
async load(opts) {
async init(opts) {
// Configuration file
const defaultConfig = require(`${__dirname}/assets/myt.default.yml`);
@ -265,7 +271,7 @@ class Myt {
this.opts = opts;
}
async unload() {
async deinit() {
if (this.conn)
await this.conn.end();
}
@ -434,4 +440,4 @@ class Myt {
module.exports = Myt;
if (require.main === module)
new Myt().run();
new Myt().cli();

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "@verdnatura/myt",
"version": "1.5.27",
"version": "1.5.28",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@verdnatura/myt",
"version": "1.5.27",
"version": "1.5.28",
"license": "GPL-3.0",
"dependencies": {
"@sqltools/formatter": "^1.2.5",

View File

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