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 ### run
Builds and starts local database server container. It only rebuilds the image 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 when dump have been modified.
is different to today.
```text ```text
$ myt run [-c|--ci] [-r|--random] $ myt run [-c|--ci] [-r|--random]

2
cli.js
View File

@ -1,4 +1,4 @@
#!/usr/bin/env node #!/usr/bin/env node
const Myt = require('./myt'); 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. * Base class for Myt commands.
*/ */
module.exports = class MytCommand { module.exports = class MytCommand extends EventEmitter {
constructor(myt, opts) { constructor(myt, opts) {
super();
this.myt = myt; this.myt = myt;
this.opts = opts; 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) { async run(myt, opts) {
throw new Error('run command not defined'); 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 port: dbConfig.port
}; };
console.log('Waiting for MySQL init process...');
async function checker() { async function checker() {
elapsedTime += interval; elapsedTime += interval;
let status; let status;
@ -46,10 +44,7 @@ module.exports = class Server {
conn.on('error', () => {}); conn.on('error', () => {});
conn.connect(err => { conn.connect(err => {
conn.destroy(); conn.destroy();
if (!err) { if (!err) return resolve();
console.log('MySQL process ready.');
return resolve();
}
if (elapsedTime >= maxInterval) if (elapsedTime >= maxInterval)
reject(new Error(`MySQL not initialized whithin ${elapsedTime / 1000} secs`)); 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) { async run(myt, opts) {
await myt.dbConnect(); await myt.dbConnect();
const version = await myt.fetchDbVersion() || {}; const version = await myt.fetchDbVersion() || {};
@ -46,13 +53,13 @@ class Clean extends Command {
path.join(archiveDir, oldVersion) path.join(archiveDir, oldVersion)
); );
console.log(`Old versions deleted: ${oldVersions.length}`); this.emit('versionsDeleted', oldVersions.length);
} else } else
console.log(`No versions to delete.`); this.emit('noVersionsDeleted');
} }
} }
module.exports = Clean; module.exports = Clean;
if (require.main === module) 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) { async run(myt, opts) {
const match = opts.name.match(/^(\w+)\.(\w+)$/); const match = opts.name.match(/^(\w+)\.(\w+)$/);
if (!match) if (!match)
@ -63,12 +68,10 @@ class Create extends Command {
const routineFile = `${routineDir}/${name}.sql`; const routineFile = `${routineDir}/${name}.sql`;
await fs.writeFile(routineFile, sql); await fs.writeFile(routineFile, sql);
console.log('Routine created.');
} }
} }
module.exports = Create; module.exports = Create;
if (require.main === module) 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.', dumpStructure: 'Dumping structure.',
dumpData: 'Dumping data.', dumpData: 'Dumping data.',
dumpPrivileges: 'Dumping privileges.', dumpPrivileges: 'Dumping privileges.',
@ -128,5 +128,4 @@ class Dump extends Command {
module.exports = Dump; module.exports = Dump;
if (require.main === module) 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; module.exports = Fixtures;
if (require.main === module) 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; module.exports = Init;
if (require.main === module) 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) { async run(myt, opts) {
const conn = await myt.dbConnect(); const conn = await myt.dbConnect();
const repo = await myt.openRepo(); const repo = await myt.openRepo();
@ -73,7 +80,7 @@ class Pull extends Command {
if (version && version.gitCommit) { if (version && version.gitCommit) {
const now = parseInt(new Date().toJSON()); const now = parseInt(new Date().toJSON());
const branchName = `myt-pull_${now}`; 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 commit = await repo.getCommit(version.gitCommit);
const branch = await nodegit.Branch.create(repo, const branch = await nodegit.Branch.create(repo,
`myt-pull_${now}`, commit, () => {}); `myt-pull_${now}`, commit, () => {});
@ -83,7 +90,7 @@ class Pull extends Command {
// Export routines to SQL files // Export routines to SQL files
console.log(`Incorporating routine changes.`); this.emit('routineChanges', branchName);
const engine = new ExporterEngine(conn, opts); const engine = new ExporterEngine(conn, opts);
await engine.init(); await engine.init();
@ -180,4 +187,4 @@ class Pull extends Command {
module.exports = Pull; module.exports = Pull;
if (require.main === module) 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) { async run(myt, opts) {
const conn = await myt.dbConnect(); const conn = await myt.dbConnect();
this.conn = conn; this.conn = conn;
@ -85,24 +167,7 @@ class Push extends Command {
await releaseLock(); await releaseLock();
} }
async push(myt, opts, conn) { async cli(myt, opts) {
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');
// Prevent push to production by mistake // Prevent push to production by mistake
if (opts.remote == 'production') { 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 // Apply versions
console.log('Applying versions.'); this.emit('applyingVersions');
let nChanges = 0; let nChanges = 0;
let silent = true; let silent = true;
const versionsDir = opts.versionsDir; 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) { function isUndoScript(script) {
return /\.undo\.sql$/.test(script); return /\.undo\.sql$/.test(script);
} }
@ -159,29 +234,28 @@ class Push extends Command {
if (await fs.pathExists(versionsDir)) { if (await fs.pathExists(versionsDir)) {
const versionDirs = await fs.readdir(versionsDir); const versionDirs = await fs.readdir(versionsDir);
const [[realm]] = await this.conn.query( const [[row]] = await this.conn.query(
`SELECT realm `SELECT realm FROM versionConfig`
FROM versionConfig`
); );
const realm = row?.realm;
for (const versionDir of versionDirs) { for (const versionDir of versionDirs) {
if (skipFiles.has(versionDir)) continue; if (skipFiles.has(versionDir)) continue;
const dirVersion = myt.parseVersionDir(versionDir); const dirVersion = myt.parseVersionDir(versionDir);
const versionData = {
version: dirVersion,
current: version
};
if (!dirVersion) { if (!dirVersion) {
logVersion('[?????]'.yellow, versionDir, this.emit('version', versionData, 'wrongDirectory');
`Wrong directory name.`
);
continue; continue;
} }
const versionNumber = dirVersion.number; const versionNumber = dirVersion.number;
const versionName = dirVersion.name;
if (versionNumber.length != version.number.length) { if (versionNumber.length != version.number.length) {
logVersion('[*****]'.gray, versionDir, this.emit('version', versionData, 'badVersion');
`Bad version length, should have ${version.number.length} characters.`
);
continue; continue;
} }
@ -204,18 +278,17 @@ class Push extends Command {
} }
if (silent) continue; if (silent) continue;
logVersion(`[${versionNumber}]`.cyan, versionName); this.emit('version', versionData, 'apply');
for (const script of scripts) { for (const script of scripts) {
const match = script.match(/^[0-9]{2}-[a-zA-Z0-9_]+(?:\.(?!undo)([a-zA-Z0-9_]+))?(?:\.undo)?\.sql$/); const match = script.match(/^[0-9]{2}-[a-zA-Z0-9_]+(?:\.(?!undo)([a-zA-Z0-9_]+))?(?:\.undo)?\.sql$/);
if (!match) { if (!match) {
logScript('[W]'.yellow, script, `Wrong file name.`); this.emit('logScript', script, 'warn', 'wrongFile');
continue; continue;
} }
const skipRealm = match[1] && match[1] !== realm; const skipRealm = match[1] && match[1] !== realm;
if (isUndoScript(script) || skipRealm) if (isUndoScript(script) || skipRealm)
continue; continue;
@ -231,9 +304,7 @@ class Push extends Command {
] ]
); );
const apply = !row || row.errorNumber; const apply = !row || row.errorNumber;
const actionMsg = apply ? '[+]'.green : '[I]'.blue; this.emit('logScript', script, apply ? 'apply' : 'ignore');
logScript(actionMsg, script);
if (!apply) continue; if (!apply) continue;
let err; let err;
@ -277,9 +348,7 @@ class Push extends Command {
// Apply routines // Apply routines
console.log('Applying changed routines.'); this.emit('applyingRoutines');
const gitExists = await fs.pathExists(`${opts.workspace}/.git`);
let nRoutines = 0; let nRoutines = 0;
const changes = await this.changedRoutines(version.gitCommit); const changes = await this.changedRoutines(version.gitCommit);
@ -343,26 +412,15 @@ class Push extends Command {
&& opts.mockFunctions.indexOf(name) !== -1; && opts.mockFunctions.indexOf(name) !== -1;
const ignore = newSql == oldSql || isMockFn; const ignore = newSql == oldSql || isMockFn;
let statusMsg; let status;
if (exists && !oldSql) if (exists && !oldSql)
statusMsg = '[+]'.green; status = 'added';
else if (!exists) else if (!exists)
statusMsg = '[-]'.red; status = 'deleted';
else else
statusMsg = '[·]'.yellow; status = 'modified';
let actionMsg; this.emit('change', status, ignore, change);
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
);
if (!ignore) { if (!ignore) {
const scapedSchema = SqlString.escapeId(schema, true); const scapedSchema = SqlString.escapeId(schema, true);
@ -410,10 +468,9 @@ class Push extends Command {
await finalize(); await finalize();
if (nRoutines > 0) { this.emit('routinesApplied', nRoutines);
console.log(` -> ${nRoutines} routines have changed.`);
} else const gitExists = await fs.pathExists(`${opts.workspace}/.git`);
console.log(` -> No routines changed.`);
if (gitExists && opts.commit) { if (gitExists && opts.commit) {
const repo = await nodegit.Repository.open(this.opts.workspace); const repo = await nodegit.Repository.open(this.opts.workspace);
@ -581,4 +638,4 @@ class Routine {
module.exports = Push; module.exports = Push;
if (require.main === module) 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 * 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 when dump have been modified. Some workarounds have been used to avoid
* image was built is different to today. Some workarounds have been used * a bug with OverlayFS driver on MacOS.
* to avoid a bug with OverlayFS driver on MacOS.
*/ */
class Run extends Command { class Run extends Command {
static usage = { 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) { async run(myt, opts) {
const dumpDir = opts.dumpDir; const dumpDir = opts.dumpDir;
const dumpDataDir = path.join(dumpDir, '.dump'); const dumpDataDir = path.join(dumpDir, '.dump');
@ -44,6 +52,8 @@ class Run extends Command {
// Build base image // Build base image
this.emit('buildingImage');
let basePath = dumpDir; let basePath = dumpDir;
let baseDockerfile = path.join(dumpDir, 'Dockerfile'); let baseDockerfile = path.join(dumpDir, 'Dockerfile');
@ -74,6 +84,8 @@ class Run extends Command {
// Run container // Run container
this.emit('runningContainer');
const isRandom = opts.random; const isRandom = opts.random;
const dbConfig = Object.assign({}, opts.dbConfig); const dbConfig = Object.assign({}, opts.dbConfig);
@ -118,12 +130,13 @@ class Run extends Command {
} }
} }
this.emit('waitingDb');
await server.wait(); await server.wait();
const conn = await myt.createConnection(); const conn = await myt.createConnection();
// Mock date functions // Mock date functions
console.log('Mocking date functions.'); this.emit('mockingDate');
const mockDateScript = path.join(dumpDir, 'mockDate.sql'); const mockDateScript = path.join(dumpDir, 'mockDate.sql');
if (opts.mockDate) { if (opts.mockDate) {
@ -144,11 +157,11 @@ class Run extends Command {
commit: true, commit: true,
dbConfig dbConfig
}); });
await myt.runCommand(Push, opts); await myt.run(Push, opts);
// Apply fixtures // Apply fixtures
console.log('Applying fixtures.'); this.emit('applyingFixtures');
const fixturesFiles = [ const fixturesFiles = [
'fixtures.before', 'fixtures.before',
'.fixtures', '.fixtures',
@ -167,7 +180,7 @@ class Run extends Command {
// Create triggers // Create triggers
if (!hasTriggers) { if (!hasTriggers) {
console.log('Creating triggers.'); this.emit('creatingTriggers');
for (const schema of opts.schemas) { for (const schema of opts.schemas) {
const triggersPath = `${opts.routinesDir}/${schema}/triggers`; const triggersPath = `${opts.routinesDir}/${schema}/triggers`;
@ -187,4 +200,4 @@ class Run extends Command {
module.exports = Run; module.exports = Run;
if (require.main === module) 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' description: 'Start local database server container'
}; };
static reporter = {
startingContainer: 'Starting container.'
};
async run(myt, opts) { async run(myt, opts) {
const ct = new Container(opts.code); const ct = new Container(opts.code);
let status; let status;
@ -27,7 +31,7 @@ class Start extends Command {
}); });
exists = true; exists = true;
} catch (err) { } catch (err) {
server = await myt.runCommand(Run, opts); server = await myt.run(Run, opts);
} }
if (exists) { if (exists) {
@ -35,6 +39,7 @@ class Start extends Command {
case 'running': case 'running':
break; break;
case 'exited': case 'exited':
this.emit('startingContainer');
await ct.start(); await ct.start();
server = new Server(ct, opts.dbConfig); server = new Server(ct, opts.dbConfig);
await server.wait(); await server.wait();
@ -51,4 +56,4 @@ class Start extends Command {
module.exports = Start; module.exports = Start;
if (require.main === module) 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) { async run(myt, opts) {
let newVersionDir; let newVersionDir;
@ -45,12 +58,7 @@ class Version extends Command {
); );
const number = row && row.number; const number = row && row.number;
const lastNumber = row && row.lastNumber; const lastNumber = row && row.lastNumber;
this.emit('dbInfo', number, lastNumber);
console.log(
`Database information:`
+ `\n -> Version: ${number}`
+ `\n -> Last version: ${lastNumber}`
);
let newVersion; let newVersion;
if (lastNumber) if (lastNumber)
@ -117,7 +125,7 @@ class Version extends Command {
`${newVersionDir}/00-firstScript.sql`, `${newVersionDir}/00-firstScript.sql`,
'-- Place your SQL code here\n' '-- Place your SQL code here\n'
); );
console.log(`New version created: ${versionFolder}`); this.emit('versionCreated', versionFolder);
await conn.query('COMMIT'); await conn.query('COMMIT');
} catch (err) { } catch (err) {
@ -215,4 +223,4 @@ const plants = [
module.exports = Version; module.exports = Version;
if (require.main === module) 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( console.log(
'Myt'.green, 'Myt'.green,
`v${packageJson.version}`.magenta `v${packageJson.version}`.magenta
@ -142,9 +144,9 @@ class Myt {
parameter('Workspace:', opts.workspace); parameter('Workspace:', opts.workspace);
parameter('Remote:', opts.remote || 'local'); parameter('Remote:', opts.remote || 'local');
await this.load(opts); await this.init(opts);
await this.runCommand(Command, opts); await this.run(Command, opts);
await this.unload(); await this.deinit();
} catch (err) { } catch (err) {
if (err.name == 'Error' && !opts.debug) { if (err.name == 'Error' && !opts.debug) {
console.error('Error:'.gray, err.message.red); console.error('Error:'.gray, err.message.red);
@ -162,12 +164,15 @@ class Myt {
process.exit(); process.exit();
} }
async runCommand(Command, opts) { async run(Command, opts) {
const command = new Command(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 // Configuration file
const defaultConfig = require(`${__dirname}/assets/myt.default.yml`); const defaultConfig = require(`${__dirname}/assets/myt.default.yml`);
@ -263,7 +268,7 @@ class Myt {
this.opts = opts; this.opts = opts;
} }
async unload() { async deinit() {
if (this.conn) if (this.conn)
await this.conn.end(); await this.conn.end();
} }
@ -375,4 +380,4 @@ class Myt {
module.exports = Myt; module.exports = Myt;
if (require.main === module) if (require.main === module)
new Myt().run(); new Myt().cli();

4
package-lock.json generated
View File

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

View File

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