From 2f31d0f98e446b6213d199d446ccc8ba6dd898da Mon Sep 17 00:00:00 2001 From: Juan Ferrer Toribio Date: Thu, 29 Dec 2022 14:27:16 +0100 Subject: [PATCH] refs #4036 New create command, escapeId fixes, refactor --- README.md | 10 ++++++ exporters/event.ejs | 8 ++--- exporters/event.js | 6 ++++ exporters/function.ejs | 10 +++--- exporters/function.js | 4 +++ exporters/procedure.ejs | 8 ++--- exporters/trigger.js | 7 +++- exporters/view.ejs | 2 +- exporters/view.js | 4 +++ lib/dumper.js | 4 +-- lib/exporter-engine.js | 5 ++- lib/exporter.js | 76 ++++++----------------------------------- myt-create.js | 74 +++++++++++++++++++++++++++++++++++++++ myt-pull.js | 55 +++++++++++++++++++++++++++-- myt-push.js | 7 ++-- package-lock.json | 18 +++++----- package.json | 4 +-- template/package.json | 2 +- 18 files changed, 202 insertions(+), 102 deletions(-) create mode 100755 myt-create.js diff --git a/README.md b/README.md index 406ec96..e9982aa 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ Database versioning commands: * **pull**: Incorporate database routine changes into workspace. * **push**: Apply changes into database. * **version**: Creates a new version. + * **create**: Creates a new routine file. + * **clean**: Cleans old versions. Local server management commands: @@ -195,6 +197,14 @@ name mixing a color with a plant name. $ myt version [] ``` +### create + +Creates a new routine file with a default template. + +```text +$ myt create [-t ] . +``` + ### clean Cleans all already applied versions older than *maxOldVersions*. diff --git a/exporters/event.ejs b/exporters/event.ejs index 60ea855..b0a1e86 100755 --- a/exporters/event.ejs +++ b/exporters/event.ejs @@ -1,12 +1,12 @@ DROP EVENT IF EXISTS <%- schema %>.<%- name %>; DELIMITER $$ CREATE DEFINER=<%- definer %> EVENT <%- schema %>.<%- name %><% -if (type == 'RECURRING') { %> +if (locals.type == 'RECURRING') { %> ON SCHEDULE EVERY <%- intervalValue %> <%- intervalField %><% - if (starts) { %> + if (locals.starts) { %> STARTS <%- starts %><% } - if (ends) { %> + if (locals.ends) { %> ENDS <%- ends %><% } } else { %> @@ -14,7 +14,7 @@ if (type == 'RECURRING') { %> } %> ON COMPLETION <%- onCompletion %> <%- status %><% -if (comment) { %> +if (locals.comment) { %> COMMENT <%- comment %><% } %> DO <%- body %>$$ diff --git a/exporters/event.js b/exporters/event.js index 0217864..609580f 100644 --- a/exporters/event.js +++ b/exporters/event.js @@ -8,6 +8,12 @@ module.exports = { 'executeAt', 'comment' ], + defaults: { + type: 'RECURRING', + intervalValue: 1, + intervalField: 'DAY', + onCompletion: 'PRESERVE' + }, formatter(params) { let status; switch(params.status){ diff --git a/exporters/function.ejs b/exporters/function.ejs index 3fcc78c..7274cce 100755 --- a/exporters/function.ejs +++ b/exporters/function.ejs @@ -1,19 +1,19 @@ DROP FUNCTION IF EXISTS <%- schema %>.<%- name %>; DELIMITER $$ -CREATE DEFINER=<%- definer %> FUNCTION <%- schema %>.<%- name %>(<%- paramList %>) +CREATE DEFINER=<%- definer %> FUNCTION <%- schema %>.<%- name %>(<%- locals.paramList %>) RETURNS <%- returns %><% -if (isDeterministic == 'NO') { %> +if (locals.isDeterministic == 'NO') { %> NOT DETERMINISTIC<% } else { %> DETERMINISTIC<% } -if (dataAccess) { %> +if (locals.dataAccess) { %> <%- dataAccess %><% } -if (securityType == 'INVOKER') { %> +if (locals.securityType == 'INVOKER') { %> SQL SECURITY <%- securityType %><% } -if (comment) { %> +if (locals.comment) { %> COMMENT <%- comment %><% } %> <%- body %>$$ diff --git a/exporters/function.js b/exporters/function.js index 68cb15d..a18fe38 100644 --- a/exporters/function.js +++ b/exporters/function.js @@ -5,6 +5,10 @@ module.exports = { escapeCols: [ 'comment' ], + defaults: { + returns: 'INT', + isDeterministic: 'NO' + }, formatter(params) { let dataAccess; switch(params.dataAccess) { diff --git a/exporters/procedure.ejs b/exporters/procedure.ejs index 78793f1..3fe2e11 100755 --- a/exporters/procedure.ejs +++ b/exporters/procedure.ejs @@ -1,13 +1,13 @@ DROP PROCEDURE IF EXISTS <%- schema %>.<%- name %>; DELIMITER $$ -CREATE DEFINER=<%- definer %> PROCEDURE <%- schema %>.<%- name %>(<%- paramList %>)<% -if (dataAccess) { %> +CREATE DEFINER=<%- definer %> PROCEDURE <%- schema %>.<%- name %>(<%- locals.paramList %>)<% +if (locals.dataAccess) { %> <%- dataAccess %><% } -if (securityType == 'INVOKER') { %> +if (locals.securityType == 'INVOKER') { %> SQL SECURITY <%- securityType %><% } -if (comment) { %> +if (locals.comment) { %> COMMENT <%- comment %><% } %> <%- body %>$$ diff --git a/exporters/trigger.js b/exporters/trigger.js index 329bbfb..5d15861 100644 --- a/exporters/trigger.js +++ b/exporters/trigger.js @@ -1,5 +1,10 @@ module.exports = { schemaCol: 'TRIGGER_SCHEMA', - nameCol: 'TRIGGER_NAME' + nameCol: 'TRIGGER_NAME', + defaults: { + actionTiming: 'AFTER', + actionType: 'INSERT', + table: 'table' + } }; diff --git a/exporters/view.ejs b/exporters/view.ejs index 41115c7..6dff4e2 100755 --- a/exporters/view.ejs +++ b/exporters/view.ejs @@ -2,6 +2,6 @@ CREATE OR REPLACE DEFINER=<%- definer %> SQL SECURITY <%- securityType %> VIEW <%- schema %>.<%- name %> AS <%- definition %><% -if (checkOption != 'NONE') { %> +if (locals.checkOption != 'NONE') { %> WITH <%- checkOption %> CHECK OPTION<% } %> diff --git a/exporters/view.js b/exporters/view.js index e098fc3..35988ad 100644 --- a/exporters/view.js +++ b/exporters/view.js @@ -4,6 +4,10 @@ const sqlFormatter = require('@sqltools/formatter'); module.exports = { schemaCol: 'TABLE_SCHEMA', nameCol: 'TABLE_NAME', + defaults: { + securityType: 'DEFINER', + checkOption: 'NONE' + }, formatter(params) { params.definition = sqlFormatter.format(params.definition, { indent: '\t', diff --git a/lib/dumper.js b/lib/dumper.js index 0ca7476..143f8ce 100644 --- a/lib/dumper.js +++ b/lib/dumper.js @@ -1,6 +1,7 @@ const docker = require('./docker'); const fs = require('fs-extra'); const path = require('path'); +const SqlString = require('sqlstring'); module.exports = class Dumper { constructor(opts) { @@ -28,9 +29,8 @@ module.exports = class Dumper { } async use(schema) { - const escapedSchema = '`'+ schema.replace('`', '``') +'`'; await this.dumpStream.write( - `USE ${escapedSchema};\n`, + `USE ${SqlString.escapeId(schema, true)};\n`, 'utf8' ); } diff --git a/lib/exporter-engine.js b/lib/exporter-engine.js index 2c996c0..d2f81fb 100644 --- a/lib/exporter-engine.js +++ b/lib/exporter-engine.js @@ -1,4 +1,3 @@ - const shajs = require('sha.js'); const fs = require('fs-extra'); const Exporter = require('./exporter'); @@ -36,7 +35,7 @@ module.exports = class ExporterEngine { ]; for (const type of types) { - const exporter = new Exporter(this, type, this.conn); + const exporter = new Exporter(type); await exporter.init(); this.exporters.push(exporter); @@ -46,7 +45,7 @@ module.exports = class ExporterEngine { async fetchRoutine(type, schema, name) { const exporter = this.exporterMap[type]; - const [row] = await exporter.query(schema, name); + const [row] = await exporter.query(this.conn, schema, name); return row && exporter.format(row); } diff --git a/lib/exporter.js b/lib/exporter.js index 9f65bf3..a541573 100644 --- a/lib/exporter.js +++ b/lib/exporter.js @@ -1,13 +1,10 @@ - const ejs = require('ejs'); const fs = require('fs-extra'); +const SqlString = require('sqlstring'); module.exports = class Exporter { - constructor(engine, objectType, conn) { - this.engine = engine; + constructor(objectType) { this.objectType = objectType; - this.dstDir = `${objectType}s`; - this.conn = conn; } async init() { @@ -19,60 +16,7 @@ module.exports = class Exporter { this.attrs = require(`${templateDir}.js`); } - async export(exportDir, schema, update, saveSum) { - const res = await this.query(schema); - if (!res.length) return; - - const routineDir = `${exportDir}/${schema}/${this.dstDir}`; - if (!await fs.pathExists(routineDir)) - await fs.mkdir(routineDir); - - const routineSet = new Set(); - for (const params of res) - routineSet.add(params.name); - - const routines = await fs.readdir(routineDir); - for (const routineFile of routines) { - const match = routineFile.match(/^(.*)\.sql$/); - if (!match) continue; - const routine = match[1]; - if (!routineSet.has(routine)) - await fs.remove(`${routineDir}/${routine}.sql`); - } - - const engine = this.engine; - - for (const params of res) { - const routineName = params.name; - const sql = this.format(params); - const routineFile = `${routineDir}/${routineName}.sql`; - - const oldSum = engine.getShaSum(routineName); - if (oldSum || saveSum) { - const shaSum = engine.shaSum(sql); - if (oldSum !== shaSum) { - engine.setShaSum( - this.objectType, schema, routineName, shaSum); - update = true; - } - } else if (params.modified && engine.lastPull) { - if (params.modified > engine.lastPull) - update = true; - } else if (await fs.pathExists(routineFile)) { - const currentSql = await fs.readFile(routineFile, 'utf8'); - if (sql != currentSql) - update = true; - } else - update = true; - - if (update) - await fs.writeFile(routineFile, sql); - } - } - - async query(schema, name) { - const {conn} = this; - + async query(conn, schema, name) { const ops = []; function addOp(col, value) { ops.push(conn.format('?? = ?', [col, value])); @@ -93,22 +37,24 @@ module.exports = class Exporter { } format(params) { - const {conn, attrs} = this; + const {attrs} = this; + params = Object.assign({}, attrs.defaults, params); if (attrs.formatter) - attrs.formatter(params, conn); + attrs.formatter(params); if (attrs.escapeCols) for (const escapeCol of attrs.escapeCols) { if (params[escapeCol]) - params[escapeCol] = conn.escape(params[escapeCol]) + params[escapeCol] = SqlString.escape(params[escapeCol]) } const split = params.definer.split('@'); - params.schema = conn.escapeId(params.schema, true); - params.name = conn.escapeId(params.name, true); + params.schema = SqlString.escapeId(params.schema, true); + params.name = SqlString.escapeId(params.name, true); params.definer = - `${conn.escapeId(split[0], true)}@${conn.escapeId(split[1], true)}`; + SqlString.escapeId(split[0], true) + '@' + + SqlString.escapeId(split[1], true); return this.template(params); } diff --git a/myt-create.js b/myt-create.js new file mode 100755 index 0000000..2ac011a --- /dev/null +++ b/myt-create.js @@ -0,0 +1,74 @@ +const Myt = require('./myt'); +const Command = require('./lib/command'); +const Exporter = require('./lib/exporter'); +const fs = require('fs-extra'); + +class Create extends Command { + static usage = { + description: 'Creates a new DB object', + params: { + type: 'The object type', + name: 'The object name, including schema' + }, + operand: 'name' + }; + + static opts = { + alias: { + type: 't', + name: 'n' + }, + string: [ + 'type', + 'name' + ], + default: { + type: 'procedure' + } + }; + + async run(myt, opts) { + const match = opts.name.match(/^(\w+)\.(\w+)$/); + if (!match) + throw new Error('Invalid object name, should contain schema and routine name'); + + const schema = match[1]; + const name = match[2]; + + const params = { + schema, + name, + definer: 'root@localhost' + }; + + switch (opts.type) { + case 'event': + case 'function': + case 'procedure': + case 'trigger': + params.body = "BEGIN\n-- Your code goes here\nEND"; + break; + case 'view': + params.definition = "SELECT TRUE" + break; + } + + const exporter = new Exporter(opts.type); + await exporter.init(); + const sql = exporter.format(params); + + const routineDir = `${opts.routinesDir}/${schema}/${opts.type}s`; + if (!await fs.pathExists(routineDir)) + await fs.mkdir(routineDir); + + 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); diff --git a/myt-pull.js b/myt-pull.js index a542c6b..b2ef549 100755 --- a/myt-pull.js +++ b/myt-pull.js @@ -114,13 +114,64 @@ class Pull extends Command { await fs.mkdir(schemaDir); for (const exporter of engine.exporters) - await exporter.export(routinesDir, - schema, opts.update, opts.sums); + await this.export(conn, engine, exporter, schema); } await engine.refreshPullDate(); await engine.saveInfo(); } + + async export(conn, engine, exporter, schema) { + const {opts} = this; + + const res = await exporter.query(conn, schema); + if (!res.length) return; + + const routineDir = `${opts.routinesDir}/${schema}/${exporter.objectType}s`; + if (!await fs.pathExists(routineDir)) + await fs.mkdir(routineDir); + + const routineSet = new Set(); + for (const params of res) + routineSet.add(params.name); + + const routines = await fs.readdir(routineDir); + for (const routineFile of routines) { + const match = routineFile.match(/^(.*)\.sql$/); + if (!match) continue; + const routine = match[1]; + if (!routineSet.has(routine)) + await fs.remove(`${routineDir}/${routine}.sql`); + } + + for (const params of res) { + const routineName = params.name; + const sql = exporter.format(params); + const routineFile = `${routineDir}/${routineName}.sql`; + let update = opts.update; + + const oldSum = engine.getShaSum(routineName); + if (oldSum || opts.sums) { + const shaSum = engine.shaSum(sql); + if (oldSum !== shaSum) { + engine.setShaSum( + exporter.objectType, schema, routineName, shaSum); + update = true; + } + } else if (params.modified && engine.lastPull) { + if (params.modified > engine.lastPull) + update = true; + } else if (await fs.pathExists(routineFile)) { + const currentSql = await fs.readFile(routineFile, 'utf8'); + if (sql != currentSql) + update = true; + } else + update = true; + + if (update) + await fs.writeFile(routineFile, sql); + } + } } module.exports = Pull; diff --git a/myt-push.js b/myt-push.js index 2f64e3e..1d0ae6b 100644 --- a/myt-push.js +++ b/myt-push.js @@ -5,6 +5,7 @@ const nodegit = require('nodegit'); const ExporterEngine = require('./lib/exporter-engine'); const connExt = require('./lib/conn'); const repoExt = require('./lib/repo'); +const SqlString = require('sqlstring'); /** * Pushes changes to remote. @@ -329,7 +330,7 @@ class Push extends Command { console.log('', actionMsg.bold, typeMsg.bold, change.fullName); if (!isEqual) { - const scapedSchema = pushConn.escapeId(schema, true); + const scapedSchema = SqlString.escapeId(schema, true); if (exists) { if (change.type.name === 'VIEW') @@ -353,7 +354,7 @@ class Push extends Command { } else { const escapedName = scapedSchema + '.' + - pushConn.escapeId(name, true); + SqlString.escapeId(name, true); const query = `DROP ${change.type.name} IF EXISTS ${escapedName}`; await pushConn.query(query); @@ -401,7 +402,7 @@ class Push extends Command { } async updateVersion(column, value) { - column = this.conn.escapeId(column, true); + column = SqlString.escapeId(column, true); await this.conn.query( `INSERT INTO version SET code = ?, diff --git a/package-lock.json b/package-lock.json index 8b080d5..f8b7b18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@verdnatura/myt", - "version": "1.5.7", + "version": "1.5.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@verdnatura/myt", - "version": "1.5.7", + "version": "1.5.8", "license": "GPL-3.0", "dependencies": { - "@sqltools/formatter": "^1.2.3", + "@sqltools/formatter": "^1.2.5", "colors": "^1.4.0", "ejs": "^3.1.6", "fs-extra": "^8.1.0", @@ -36,9 +36,9 @@ } }, "node_modules/@sqltools/formatter": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.3.tgz", - "integrity": "sha512-O3uyB/JbkAEMZaP3YqyHH7TMnex7tWyCbCI4EfJdOCoN6HIhqdJBWTM6aCCiWQ/5f5wxjgU735QAIpJbjDvmzg==" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", @@ -1846,9 +1846,9 @@ "integrity": "sha512-/aPsuoj/1Dw/kzhkgz+ES6TxG0zfTMGLwuK2ZG00k/iJzYHTLCE8mVU8EPqEOp/lmxPoq1C1C9RYToRKb2KEfg==" }, "@sqltools/formatter": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.3.tgz", - "integrity": "sha512-O3uyB/JbkAEMZaP3YqyHH7TMnex7tWyCbCI4EfJdOCoN6HIhqdJBWTM6aCCiWQ/5f5wxjgU735QAIpJbjDvmzg==" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "@szmarczak/http-timer": { "version": "4.0.6", diff --git a/package.json b/package.json index 9994337..f74ade8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@verdnatura/myt", - "version": "1.5.7", + "version": "1.5.8", "author": "Verdnatura Levante SL", "description": "MySQL version control", "license": "GPL-3.0", @@ -12,7 +12,7 @@ "url": "https://github.com/verdnatura/myt.git" }, "dependencies": { - "@sqltools/formatter": "^1.2.3", + "@sqltools/formatter": "^1.2.5", "colors": "^1.4.0", "ejs": "^3.1.6", "fs-extra": "^8.1.0", diff --git a/template/package.json b/template/package.json index 9a05758..fd7065b 100644 --- a/template/package.json +++ b/template/package.json @@ -8,6 +8,6 @@ "type": "git" }, "dependencies": { - "@verdnatura/myt": "^1.5.7" + "@verdnatura/myt": "^1.5.8" } }