From 24b35e2e9078c0d421140a7989416c8dd2d815ee Mon Sep 17 00:00:00 2001 From: Juan Ferrer Toribio Date: Sat, 23 Oct 2021 15:20:35 +0200 Subject: [PATCH] Version command, operand to option, hashsum fixes --- README.md | 1 + myvc-dump.js | 3 +- myvc-pull.js | 72 +++++++++--------- myvc-push.js | 15 ++-- myvc-run.js | 2 +- myvc-version.js | 189 +++++++++++++++++++++++++++++++++++++++++++++++ myvc.default.yml | 1 + myvc.js | 25 ++++--- package.json | 2 +- structure.sql | 19 ++--- 10 files changed, 265 insertions(+), 64 deletions(-) create mode 100644 myvc-version.js diff --git a/README.md b/README.md index b73755e..5558772 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Database versioning commands: * **init**: Initialize an empty workspace. * **pull**: Incorporates database routines changes into workspace. * **push**: Apply changes into database. + * **version**: Creates a new version. Local server management commands: diff --git a/myvc-dump.js b/myvc-dump.js index b8cc9fd..378ab44 100644 --- a/myvc-dump.js +++ b/myvc-dump.js @@ -8,8 +8,9 @@ const docker = require('./docker'); * Dumps structure and fixtures from remote. */ class Dump { - get myOpts() { + get localOpts() { return { + operand: 'remote', alias: { remote: 'r' }, diff --git a/myvc-pull.js b/myvc-pull.js index add27ae..774aef2 100755 --- a/myvc-pull.js +++ b/myvc-pull.js @@ -6,8 +6,9 @@ const shajs = require('sha.js'); const nodegit = require('nodegit'); class Pull { - get myOpts() { + get localOpts() { return { + operand: 'remote', alias: { force: 'f', checkout: 'c' @@ -68,6 +69,14 @@ class Pull { console.log(`Incorporating routine changes.`); + const exporters = [ + new Exporter('function'), + new Exporter('procedure'), + new Exporter('view'), + new Exporter('trigger'), + new Exporter('event') + ]; + for (const exporter of exporters) await exporter.init(); @@ -75,40 +84,47 @@ class Pull { if (!await fs.pathExists(exportDir)) await fs.mkdir(exportDir); + // Initialize SHA data + + let newShaSums = {}; + let oldShaSums; + const shaFile = `${opts.workspace}/.shasums.json`; + + if (await fs.pathExists(shaFile)) + oldShaSums = JSON.parse(await fs.readFile(shaFile, 'utf8')); + + // Delete old schemas + const schemas = await fs.readdir(exportDir); for (const schema of schemas) { if (opts.schemas.indexOf(schema) == -1) await fs.remove(`${exportDir}/${schema}`, {recursive: true}); } - let shaSums; - const shaFile = `${opts.workspace}/.shasums.json`; - - if (await fs.pathExists(shaFile)) - shaSums = JSON.parse(await fs.readFile(shaFile, 'utf8')); - else - shaSums = {}; + // Export objects to SQL files for (const schema of opts.schemas) { - let schemaDir = `${exportDir}/${schema}`; + newShaSums[schema] = {}; + let schemaDir = `${exportDir}/${schema}`; if (!await fs.pathExists(schemaDir)) await fs.mkdir(schemaDir); - let schemaSums = shaSums[schema]; - if (!schemaSums) schemaSums = shaSums[schema] = {}; - for (const exporter of exporters) { const objectType = exporter.objectType; + const newSums = newShaSums[schema][objectType] = {}; + let oldSums = {}; + try { + oldSums = oldShaSums[schema][objectType]; + } catch (e) {} - let objectSums = schemaSums[objectType]; - if (!objectSums) objectSums = schemaSums[objectType] = {}; - - await exporter.export(conn, exportDir, schema, objectSums); + await exporter.export(conn, exportDir, schema, newSums, oldSums); } } - await fs.writeFile(shaFile, JSON.stringify(shaSums, null, ' ')); + // Save SHA data + + await fs.writeFile(shaFile, JSON.stringify(newShaSums, null, ' ')); } } @@ -129,7 +145,7 @@ class Exporter { this.formatter = require(`${templateDir}.js`); } - async export(conn, exportDir, schema, shaSums) { + async export(conn, exportDir, schema, newSums, oldSums) { const [res] = await conn.query(this.query, [schema]); if (!res.length) return; @@ -167,30 +183,14 @@ class Exporter { const shaSum = shajs('sha256') .update(JSON.stringify(sql)) .digest('hex'); - shaSums[routineName] = shaSum; + newSums[routineName] = shaSum; - let changed = true; - - if (await fs.pathExists(routineFile)) { - const currentSql = await fs.readFile(routineFile, 'utf8'); - changed = shaSums[routineName] !== shaSum;; - } - if (changed) { + if (oldSums[routineName] !== shaSum) await fs.writeFile(routineFile, sql); - shaSums[routineName] = shaSum; - } } } } -const exporters = [ - new Exporter('function'), - new Exporter('procedure'), - new Exporter('view'), - new Exporter('trigger'), - new Exporter('event') -]; - module.exports = Pull; if (require.main === module) diff --git a/myvc-push.js b/myvc-push.js index d61278d..7bddcf5 100644 --- a/myvc-push.js +++ b/myvc-push.js @@ -10,8 +10,9 @@ const nodegit = require('nodegit'); * @property {Boolean} user Whether to change current user version */ class Push { - get myOpts() { + get localOpts() { return { + operand: 'remote', alias: { force: 'f', user: 'u' @@ -252,9 +253,11 @@ class Push { `INSERT INTO versionUser SET code = ?, user = ?, - ${column} = ? + ${column} = ?, + updated = NOW() ON DUPLICATE KEY UPDATE - ${column} = VALUES(${column})`, + ${column} = VALUES(${column}), + updated = VALUES(updated)`, [ opts.code, user, @@ -265,9 +268,11 @@ class Push { await this.conn.query( `INSERT INTO version SET code = ?, - ${column} = ? + ${column} = ?, + updated = NOW() ON DUPLICATE KEY UPDATE - ${column} = VALUES(${column})`, + ${column} = VALUES(${column}), + updated = VALUES(updated)`, [ opts.code, value diff --git a/myvc-run.js b/myvc-run.js index 9b0b840..5da76ed 100644 --- a/myvc-run.js +++ b/myvc-run.js @@ -16,7 +16,7 @@ const Server = require('./server/server'); * @property {Boolean} random Whether to use a random container name */ class Run { - get myOpts() { + get localOpts() { return { alias: { ci: 'c', diff --git a/myvc-version.js b/myvc-version.js new file mode 100644 index 0000000..2f97da5 --- /dev/null +++ b/myvc-version.js @@ -0,0 +1,189 @@ + +const MyVC = require('./myvc'); +const fs = require('fs-extra'); + +/** + * Creates a new version. + */ +class Version { + mainOpt = 'name'; + get localOpts() { + return { + operand: 'name', + name: { + name: 'n' + }, + default: { + remote: 'production' + } + }; + } + + async run(myvc, opts) { + const verionsDir =`${opts.workspace}/versions`; + let versionDir; + let versionName = opts.name; + + // Fetch last version number + + const conn = await myvc.dbConnect(); + const version = await myvc.fetchDbVersion() || {}; + + try { + await conn.query('START TRANSACTION'); + + const [[row]] = await conn.query( + `SELECT lastNumber FROM version WHERE code = ? FOR UPDATE`, + [opts.code] + ); + const lastVersion = row && row.lastNumber; + + console.log( + `Database information:` + + `\n -> Version: ${version.number}` + + `\n -> Last version: ${lastVersion}` + ); + + let newVersion = lastVersion ? parseInt(lastVersion) + 1 : 1; + newVersion = String(newVersion).padStart(opts.versionDigits, '0'); + + // Get version name + + const versionNames = new Set(); + const versionDirs = await fs.readdir(verionsDir); + for (const versionNameDir of versionDirs) { + const split = versionNameDir.split('-'); + const versionName = split[1]; + if (versionName) versionNames.add(versionName); + } + + if (!versionName) { + let attempts; + const maxAttempts = 1000; + + for (attempts = 0; attempts < maxAttempts; attempts++) { + versionName = randomName(); + if (!versionNames.has(versionName)) break; + } + + if (attempts === maxAttempts) + throw new Error(`Cannot create a unique version name after ${attempts} attempts`); + } else { + const isNameValid = typeof versionName === 'string' + && /^[a-zA-Z0-9]+$/.test(versionName); + if (!isNameValid) + throw new Error('Version name can only contain letters or numbers'); + if (versionNames.has(versionName)) + throw new Error('Version with same name already exists'); + + } + + // Create version + + const versionFolder = `${newVersion}-${versionName}`; + versionDir = `${verionsDir}/${versionFolder}`; + + await conn.query( + `UPDATE version SET lastNumber = ? WHERE code = ?`, + [newVersion, opts.code] + ); + await fs.mkdir(versionDir); + console.log(`New version folder created: ${versionFolder}`); + + await conn.query('COMMIT'); + } catch (err) { + await conn.query('ROLLBACK'); + if (versionDir && await fs.pathExists(versionDir)) + await fs.remove(versionDir, {recursive: true}); + throw err; + } + } +} + +function randomName() { + const color = random(colors); + let plant = random(plants); + plant = plant.charAt(0).toUpperCase() + plant.slice(1); + return color + plant; +} + +function random(array) { + return array[Math.floor(Math.random() * array.length)]; +} + +const colors = [ + 'aqua', + 'azure', + 'black', + 'blue', + 'bronze', + 'brown', + 'chocolate', + 'crimson', + 'golden', + 'gray', + 'green', + 'lime', + 'maroon', + 'navy', + 'orange', + 'pink', + 'purple', + 'red', + 'salmon', + 'silver', + 'teal', + 'turquoise', + 'yellow', + 'wheat', + 'white' +]; + +const plants = [ + 'anthurium', + 'aralia', + 'arborvitae', + 'asparagus', + 'aspidistra', + 'bamboo', + 'birch', + 'carnation', + 'camellia', + 'cataractarum', + 'chico', + 'chrysanthemum', + 'cordyline', + 'cyca', + 'cymbidium', + 'dendro', + 'dracena', + 'erica', + 'eucalyptus', + 'fern', + 'galax', + 'gerbera', + 'hydrangea', + 'ivy', + 'laurel', + 'lilium', + 'mastic', + 'medeola', + 'monstera', + 'moss', + 'oak', + 'orchid', + 'palmetto', + 'paniculata', + 'phormium', + 'raphis', + 'roebelini', + 'rose', + 'ruscus', + 'salal', + 'tulip' +]; + +module.exports = Version; + +if (require.main === module) + new MyVC().run(Version); diff --git a/myvc.default.yml b/myvc.default.yml index 6191bea..505d473 100755 --- a/myvc.default.yml +++ b/myvc.default.yml @@ -1,4 +1,5 @@ versionSchema: myvc +versionDigits: 5 schemas: - myvc fixtures: diff --git a/myvc.js b/myvc.js index f70a9af..c289ca4 100755 --- a/myvc.js +++ b/myvc.js @@ -57,6 +57,7 @@ class MyVC { 'init', 'pull', 'push', + 'version', 'dump', 'start', 'run' @@ -69,13 +70,16 @@ class MyVC { command = new Klass(); } - const commandOpts = getopts(argv, command.myOpts); + const commandOpts = getopts(argv, command.localOpts); Object.assign(cliOpts, commandOpts); - for (const opt in cliOpts) { + for (const opt in cliOpts) if (opt.length > 1 || opt == '_') opts[opt] = cliOpts[opt]; - } + + const operandToOpt = command.localOpts.operand; + if (opts._.length >= 2 && operandToOpt) + opts[operandToOpt] = opts._[1]; parameter('Workspace:', opts.workspace); parameter('Remote:', opts.remote || 'local'); @@ -155,6 +159,11 @@ class MyVC { this.opts = opts; } + async unload() { + if (this.conn) + await this.conn.end(); + } + async dbConnect() { if (!this.conn) this.conn = await this.createConnection(); @@ -165,11 +174,6 @@ class MyVC { return await mysql.createConnection(this.opts.dbConfig); } - async unload() { - if (this.conn) - await this.conn.end(); - } - async fetchDbVersion() { const {opts} = this; @@ -201,6 +205,7 @@ class MyVC { const changesMap = new Map(); async function pushChanges(diff) { + if (!diff) return; const patches = await diff.patches(); for (const patch of patches) { @@ -230,9 +235,7 @@ class MyVC { } await pushChanges(await this.getUnstaged(repo)); - - const stagedDiff = await this.getStaged(repo); - if (stagedDiff) await pushChanges(stagedDiff); + await pushChanges(await this.getStaged(repo)); return changes.sort((a, b) => { if (b.mark != a.mark) diff --git a/package.json b/package.json index 01f9a5e..3c214d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "myvc", - "version": "1.1.11", + "version": "1.1.12", "author": "Verdnatura Levante SL", "description": "MySQL Version Control", "license": "GPL-3.0", diff --git a/structure.sql b/structure.sql index d148535..c78b864 100644 --- a/structure.sql +++ b/structure.sql @@ -1,20 +1,21 @@ CREATE TABLE `version` ( - `code` varchar(255) NOT NULL, - `number` char(11) NULL DEFAULT NULL, - `gitCommit` varchar(255) NULL DEFAULT NULL, - `updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + `code` VARCHAR(255) NOT NULL, + `number` CHAR(11) NULL DEFAULT NULL, + `gitCommit` VARCHAR(255) NULL DEFAULT NULL, + `updated` DATETIME NOT NULL DEFAULT NULL ) ENGINE=InnoDB; ALTER TABLE `version` ADD PRIMARY KEY (`code`); CREATE TABLE `versionUser` ( - `code` varchar(255) NOT NULL, - `user` varchar(255) NOT NULL, - `number` char(11) NULL DEFAULT NULL, - `gitCommit` varchar(255) NULL DEFAULT NULL, - `updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + `code` VARCHAR(255) NOT NULL, + `user` VARCHAR(255) NOT NULL, + `number` CHAR(11) NULL DEFAULT NULL, + `gitCommit` VARCHAR(255) NULL DEFAULT NULL, + `updated` DATETIME NOT NULL DEFAULT NULL, + `lastNumber` CHAR(11) NULL DEFAULT NULL, ) ENGINE=InnoDB; ALTER TABLE `versionUser`