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

@ -276,7 +276,7 @@ $ myt fixtures [<remote>]
### run
Builds and starts local database server container. It only rebuilds the image
dump has been modified.
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);
@ -104,10 +119,7 @@ class Clean extends Command {
}
}
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",