feat: refs #5483 Console logging via events

This commit is contained in:
Juan Ferrer 2024-01-04 13:02:40 +01:00
parent 19b368b219
commit 5f2e2c3933
17 changed files with 236 additions and 127 deletions

View File

@ -275,9 +275,8 @@ $ myt fixtures [<remote>]
### run
Builds and starts local database server container. It only rebuilds the image
when fixtures have been modified or when the day on which the image was built
is different to today.
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,30 @@
const EventEmitter = require('node:events');
/**
* Base class for Myt commands.
*/
module.exports = class MytCommand {
module.exports = class MytCommand extends EventEmitter {
constructor(myt, opts) {
super();
this.myt = myt;
this.opts = opts;
}
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]);
}
}

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

@ -17,6 +17,13 @@ class Clean extends Command {
}
};
static reporter = {
versionsDeleted: function(nVersions) {
console.log(`Old versions deleted: ${nVersions}`);
},
noVersionsDeleted: 'No versions to delete.'
};
async run(myt, opts) {
await myt.dbConnect();
const version = await myt.fetchDbVersion() || {};
@ -46,13 +53,13 @@ class Clean extends Command {
path.join(archiveDir, oldVersion)
);
console.log(`Old versions deleted: ${oldVersions.length}`);
this.emit('versionsDeleted', oldVersions.length);
} else
console.log(`No versions to delete.`);
this.emit('noVersionsDeleted');
}
}
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,88 @@ 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(data, action) {
let {version} = data;
let name = data.dir;
let num, color;
switch(action) {
case 'apply':
num = version.number;
name = version.name;
color = 'cyan';
break;
case 'badVersion':
num = '?????';
color = 'yellow';
break;
case 'wrongDirectory':
num = '*****';
color = 'gray';
break;
}
console.log('', `[${num[color].bold}]`, name);
},
logScript(script, action, error) {
let actionMsg;
switch(action) {
case 'apply':
actionMsg = '[+]'.green;
break;
case 'ignore':
actionMsg = '[I]'.blue;
break;
default:
actionMsg = '[W]'.yellow;
break;
}
console.log(' ', actionMsg.bold, script);
},
change(status, ignore, change) {
let statusMsg;
switch(status) {
case 'added':
statusMsg = '[+]'.green;
break;
case 'deleted':
statusMsg = '[-]'.red;
break;
case 'modified':
statusMsg = '[·]'.yellow;
break;
}
let actionMsg;
if (ignore)
actionMsg = '[I]'.blue;
else
actionMsg = '[A]'.green;
const typeMsg = `[${change.type.abbr}]`[change.type.color];
console.log('',
(statusMsg + actionMsg).bold,
typeMsg.bold,
change.fullName
);
},
routinesApplied: function(nRoutines) {
if (nRoutines > 0) {
console.log(` -> ${nRoutines} routines have changed.`);
} else {
console.log(` -> No routines changed.`);
}
}
};
async run(myt, opts) {
const conn = await myt.dbConnect();
this.conn = conn;
@ -85,24 +167,7 @@ class Push extends Command {
await releaseLock();
}
async push(myt, opts, conn) {
const pushConn = await myt.createConnection();
// Get database version
const version = await myt.fetchDbVersion() || {};
console.log(
`Database information:`
+ `\n -> Version: ${version.number}`
+ `\n -> Commit: ${version.gitCommit}`
);
if (!version.number)
version.number = String('0').padStart(opts.versionDigits, '0');
if (!/^[0-9]*$/.test(version.number))
throw new Error('Wrong database version');
async cli(myt, opts) {
// Prevent push to production by mistake
if (opts.remote == 'production') {
@ -134,20 +199,30 @@ class Push extends Command {
}
}
await super.cli(myt, opts);
}
async push(myt, opts, conn) {
const pushConn = await myt.createConnection();
// Get database version
const version = await myt.fetchDbVersion() || {};
this.emit('dbInfo', version);
if (!version.number)
version.number = String('0').padStart(opts.versionDigits, '0');
if (!/^[0-9]*$/.test(version.number))
throw new Error('Wrong database version');
// Apply versions
console.log('Applying versions.');
this.emit('applyingVersions');
let nChanges = 0;
let silent = true;
const versionsDir = opts.versionsDir;
function logVersion(version, name, error) {
console.log('', version.bold, name);
}
function logScript(type, message, error) {
console.log(' ', type.bold, message);
}
function isUndoScript(script) {
return /\.undo\.sql$/.test(script);
}
@ -159,29 +234,28 @@ class Push extends Command {
if (await fs.pathExists(versionsDir)) {
const versionDirs = await fs.readdir(versionsDir);
const [[realm]] = await this.conn.query(
`SELECT realm
FROM versionConfig`
const [[row]] = await this.conn.query(
`SELECT realm FROM versionConfig`
);
const realm = row?.realm;
for (const versionDir of versionDirs) {
if (skipFiles.has(versionDir)) continue;
const dirVersion = myt.parseVersionDir(versionDir);
const versionData = {
version: dirVersion,
current: version
};
if (!dirVersion) {
logVersion('[?????]'.yellow, versionDir,
`Wrong directory name.`
);
this.emit('version', versionData, 'wrongDirectory');
continue;
}
const versionNumber = dirVersion.number;
const versionName = dirVersion.name;
if (versionNumber.length != version.number.length) {
logVersion('[*****]'.gray, versionDir,
`Bad version length, should have ${version.number.length} characters.`
);
this.emit('version', versionData, 'badVersion');
continue;
}
@ -204,18 +278,17 @@ class Push extends Command {
}
if (silent) continue;
logVersion(`[${versionNumber}]`.cyan, versionName);
this.emit('version', versionData, 'apply');
for (const script of scripts) {
const match = script.match(/^[0-9]{2}-[a-zA-Z0-9_]+(?:\.(?!undo)([a-zA-Z0-9_]+))?(?:\.undo)?\.sql$/);
if (!match) {
logScript('[W]'.yellow, script, `Wrong file name.`);
this.emit('logScript', script, 'warn', 'wrongFile');
continue;
}
const skipRealm = match[1] && match[1] !== realm;
if (isUndoScript(script) || skipRealm)
continue;
@ -231,9 +304,7 @@ class Push extends Command {
]
);
const apply = !row || row.errorNumber;
const actionMsg = apply ? '[+]'.green : '[I]'.blue;
logScript(actionMsg, script);
this.emit('logScript', script, apply ? 'apply' : 'ignore');
if (!apply) continue;
let err;
@ -277,9 +348,7 @@ class Push extends Command {
// 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(version.gitCommit);
@ -343,26 +412,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('',
(statusMsg + actionMsg).bold,
typeMsg.bold,
change.fullName
);
this.emit('change', status, ignore, change);
if (!ignore) {
const scapedSchema = SqlString.escapeId(schema, true);
@ -410,10 +468,9 @@ class Push extends Command {
await finalize();
if (nRoutines > 0) {
console.log(` -> ${nRoutines} routines have 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);
@ -581,4 +638,4 @@ class Routine {
module.exports = Push;
if (require.main === module)
new Myt().run(Push);
new Myt().cli(Push);

View File

@ -10,9 +10,8 @@ const SqlString = require('sqlstring');
/**
* Builds the database image and runs a container. It only rebuilds the
* image when fixtures have been modified or when the day on which the
* image was built is different to today. Some workarounds have been used
* to avoid a bug with OverlayFS driver on MacOS.
* image when dump have been modified. Some workarounds have been used to avoid
* a bug with OverlayFS driver on MacOS.
*/
class Run extends Command {
static usage = {
@ -34,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');
@ -44,6 +52,8 @@ class Run extends Command {
// Build base image
this.emit('buildingImage');
let basePath = dumpDir;
let baseDockerfile = path.join(dumpDir, 'Dockerfile');
@ -74,6 +84,8 @@ class Run extends Command {
// Run container
this.emit('runningContainer');
const isRandom = opts.random;
const dbConfig = Object.assign({}, opts.dbConfig);
@ -118,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) {
@ -144,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',
@ -167,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`;
@ -187,4 +200,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);

23
myt.js
View File

@ -38,7 +38,9 @@ class Myt {
]
};
async run(Command) {
async cli(Command) {
this.cliMode = true;
console.log(
'Myt'.green,
`v${packageJson.version}`.magenta
@ -142,9 +144,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);
@ -162,12 +164,15 @@ class Myt {
process.exit();
}
async runCommand(Command, opts) {
async run(Command, 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`);
@ -263,7 +268,7 @@ class Myt {
this.opts = opts;
}
async unload() {
async deinit() {
if (this.conn)
await this.conn.end();
}
@ -375,4 +380,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.22",
"version": "1.5.23",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@verdnatura/myt",
"version": "1.5.22",
"version": "1.5.23",
"license": "GPL-3.0",
"dependencies": {
"@sqltools/formatter": "^1.2.5",

View File

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