@verdnatura/myt 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +2 -0
- package/LICENSE +17 -0
- package/README.md +268 -0
- package/assets/db.ini +5 -0
- package/assets/myvc.default.yml +9 -0
- package/assets/structure.sql +24 -0
- package/cli.js +4 -0
- package/exporters/event.ejs +21 -0
- package/exporters/event.js +25 -0
- package/exporters/event.sql +19 -0
- package/exporters/function.ejs +20 -0
- package/exporters/function.js +23 -0
- package/exporters/function.sql +16 -0
- package/exporters/procedure.ejs +14 -0
- package/exporters/procedure.js +23 -0
- package/exporters/procedure.sql +14 -0
- package/exporters/trigger.ejs +7 -0
- package/exporters/trigger.js +5 -0
- package/exporters/trigger.sql +13 -0
- package/exporters/view.ejs +7 -0
- package/exporters/view.js +25 -0
- package/exporters/view.sql +12 -0
- package/lib/command.js +16 -0
- package/lib/docker.js +133 -0
- package/lib/exporter-engine.js +112 -0
- package/lib/exporter.js +115 -0
- package/lib/server.js +71 -0
- package/lib/util.js +6 -0
- package/myt-clean.js +52 -0
- package/myt-dump.js +71 -0
- package/myt-fixtures.js +28 -0
- package/myt-init.js +25 -0
- package/myt-pull.js +129 -0
- package/myt-push.js +475 -0
- package/myt-run.js +160 -0
- package/myt-start.js +51 -0
- package/myt-version.js +219 -0
- package/myt.js +646 -0
- package/package.json +43 -0
- package/server/Dockerfile.base +1 -0
- package/server/Dockerfile.client +14 -0
- package/server/Dockerfile.dump +16 -0
- package/server/Dockerfile.server +25 -0
- package/server/docker-dump.sh +4 -0
- package/server/docker-fixtures.sh +4 -0
- package/server/docker-import.sh +6 -0
- package/server/docker-init.sh +20 -0
- package/server/docker-start.sh +10 -0
- package/server/docker.cnf +9 -0
- package/template/dump/fixtures.sql +1 -0
- package/template/dump/structure.sql +1 -0
- package/template/myvc.config.yml +11 -0
- package/template/package.json +13 -0
- package/template/remotes/local.ini +5 -0
- package/template/routines/README.md +1 -0
- package/template/versions/00001-firstVersion/00-firstScript.sql +1 -0
- package/template/versions/README.md +1 -0
package/myt.js
ADDED
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
require('require-yaml');
|
|
4
|
+
require('colors');
|
|
5
|
+
const getopts = require('getopts');
|
|
6
|
+
const packageJson = require('./package.json');
|
|
7
|
+
const fs = require('fs-extra');
|
|
8
|
+
const ini = require('ini');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const mysql = require('mysql2/promise');
|
|
11
|
+
const nodegit = require('nodegit');
|
|
12
|
+
const camelToSnake = require('./lib/util').camelToSnake;
|
|
13
|
+
const docker = require('./lib/docker');
|
|
14
|
+
const Command = require('./lib/command');
|
|
15
|
+
|
|
16
|
+
class Myt {
|
|
17
|
+
get usage() {
|
|
18
|
+
return {
|
|
19
|
+
description: 'Utility for database versioning',
|
|
20
|
+
params: {
|
|
21
|
+
remote: 'Name of remote to use',
|
|
22
|
+
workspace: 'The base directory of the project',
|
|
23
|
+
socket: 'Wether to connect to database via socket',
|
|
24
|
+
debug: 'Wether to enable debug mode',
|
|
25
|
+
version: 'Display the version number and exit',
|
|
26
|
+
help: 'Display this help message'
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get localOpts() {
|
|
32
|
+
return {
|
|
33
|
+
alias: {
|
|
34
|
+
remote: 'r',
|
|
35
|
+
workspace: 'w',
|
|
36
|
+
socket: 'k',
|
|
37
|
+
debug: 'd',
|
|
38
|
+
version: 'v',
|
|
39
|
+
help: 'h'
|
|
40
|
+
},
|
|
41
|
+
boolean: [
|
|
42
|
+
'debug',
|
|
43
|
+
'version',
|
|
44
|
+
'help'
|
|
45
|
+
],
|
|
46
|
+
default: {
|
|
47
|
+
workspace: process.cwd()
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async run(CommandClass) {
|
|
53
|
+
console.log(
|
|
54
|
+
'Myt (MySQL Version Control)'.green,
|
|
55
|
+
`v${packageJson.version}`.magenta
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const baseOpts = this.localOpts;
|
|
59
|
+
const opts = this.getopts(baseOpts);
|
|
60
|
+
|
|
61
|
+
if (opts.debug) {
|
|
62
|
+
console.warn('Debug mode enabled.'.yellow);
|
|
63
|
+
console.log('Global options:'.magenta, opts);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (opts.version)
|
|
67
|
+
process.exit(0);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const commandName = opts._[0];
|
|
71
|
+
if (!CommandClass && commandName) {
|
|
72
|
+
if (!/^[a-z]+$/.test(commandName))
|
|
73
|
+
throw new Error (`Invalid command name '${commandName}'`);
|
|
74
|
+
|
|
75
|
+
const commandFile = path.join(__dirname, `myt-${commandName}.js`);
|
|
76
|
+
|
|
77
|
+
if (!await fs.pathExists(commandFile))
|
|
78
|
+
throw new Error (`Unknown command '${commandName}'`);
|
|
79
|
+
CommandClass = require(commandFile);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!CommandClass) {
|
|
83
|
+
this.showHelp(baseOpts, this.usage);
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const allOpts = Object.assign({}, baseOpts);
|
|
88
|
+
|
|
89
|
+
if (CommandClass.localOpts)
|
|
90
|
+
for (const key in CommandClass.localOpts) {
|
|
91
|
+
const baseValue = baseOpts[key];
|
|
92
|
+
const cmdValue = CommandClass.localOpts[key];
|
|
93
|
+
if (Array.isArray(baseValue))
|
|
94
|
+
allOpts[key] = baseValue.concat(cmdValue);
|
|
95
|
+
else if (typeof baseValue == 'object')
|
|
96
|
+
allOpts[key] = Object.assign({}, baseValue, cmdValue);
|
|
97
|
+
else
|
|
98
|
+
allOpts[key] = cmdValue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const commandOpts = this.getopts(allOpts);
|
|
102
|
+
if (opts.debug)
|
|
103
|
+
console.log('Command options:'.magenta, commandOpts);
|
|
104
|
+
Object.assign(opts, commandOpts);
|
|
105
|
+
|
|
106
|
+
const operandToOpt = CommandClass.usage.operand;
|
|
107
|
+
if (opts._.length >= 2 && operandToOpt)
|
|
108
|
+
opts[operandToOpt] = opts._[1];
|
|
109
|
+
|
|
110
|
+
if (opts.debug)
|
|
111
|
+
console.log('Final options:'.magenta, opts);
|
|
112
|
+
|
|
113
|
+
if (opts.help) {
|
|
114
|
+
this.showHelp(CommandClass.localOpts, CommandClass.usage, commandName);
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check version
|
|
119
|
+
|
|
120
|
+
let depVersion;
|
|
121
|
+
const versionRegex = /^[^~]?([0-9]+)\.([0-9]+).([0-9]+)$/;
|
|
122
|
+
const wsPackageFile = path.join(opts.workspace, 'package.json');
|
|
123
|
+
|
|
124
|
+
if (await fs.pathExists(wsPackageFile)) {
|
|
125
|
+
const wsPackageJson = require(wsPackageFile);
|
|
126
|
+
try {
|
|
127
|
+
depVersion = wsPackageJson
|
|
128
|
+
.dependencies
|
|
129
|
+
.myt.match(versionRegex);
|
|
130
|
+
} catch (e) {}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (depVersion) {
|
|
134
|
+
const myVersion = packageJson.version.match(versionRegex);
|
|
135
|
+
|
|
136
|
+
const isSameVersion =
|
|
137
|
+
depVersion[1] === myVersion[1] &&
|
|
138
|
+
depVersion[2] === myVersion[2];
|
|
139
|
+
if (!isSameVersion)
|
|
140
|
+
throw new Error(`Myt version differs a lot from package.json, please run 'npm i' first to install the proper version.`);
|
|
141
|
+
|
|
142
|
+
const isSameMinor = depVersion[3] === myVersion[3];
|
|
143
|
+
if (!isSameMinor)
|
|
144
|
+
console.warn(`Warning! Myt minor version differs from package.json, maybe you shoud run 'npm i' to install the proper version.`.yellow);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Load method
|
|
148
|
+
|
|
149
|
+
parameter('Workspace:', opts.workspace);
|
|
150
|
+
parameter('Remote:', opts.remote || 'local');
|
|
151
|
+
|
|
152
|
+
await this.load(opts);
|
|
153
|
+
await this.runCommand(CommandClass, opts);
|
|
154
|
+
await this.unload();
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (err.name == 'Error' && !opts.debug) {
|
|
157
|
+
console.error('Error:'.gray, err.message.red);
|
|
158
|
+
console.log(`You can get more details about the error by passing the 'debug' option.`.yellow);
|
|
159
|
+
} else
|
|
160
|
+
console.log(err.stack.magenta);
|
|
161
|
+
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parameter(parameter, value) {
|
|
166
|
+
console.log(parameter.gray, (value || 'null').blue);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
process.exit();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async runCommand(CommandClass, opts) {
|
|
173
|
+
const command = new CommandClass();
|
|
174
|
+
command.opts = opts;
|
|
175
|
+
await command.run(this, opts);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async load(opts) {
|
|
179
|
+
// Configuration file
|
|
180
|
+
|
|
181
|
+
const config = require(`${__dirname}/assets/myt.default.yml`);
|
|
182
|
+
|
|
183
|
+
const configFile = 'myt.config.yml';
|
|
184
|
+
const configPath = path.join(opts.workspace, configFile);
|
|
185
|
+
if (await fs.pathExists(configPath))
|
|
186
|
+
Object.assign(config, require(configPath));
|
|
187
|
+
|
|
188
|
+
Object.assign(opts, config);
|
|
189
|
+
opts.configFile = configFile;
|
|
190
|
+
|
|
191
|
+
if (!opts.mytDir)
|
|
192
|
+
opts.mytDir = path.join(opts.workspace, opts.subdir || '');
|
|
193
|
+
|
|
194
|
+
opts.routinesDir = path.join(opts.mytDir, 'routines');
|
|
195
|
+
opts.versionsDir = path.join(opts.mytDir, 'versions');
|
|
196
|
+
opts.dumpDir = path.join(opts.mytDir, 'dump');
|
|
197
|
+
|
|
198
|
+
// Database configuration
|
|
199
|
+
|
|
200
|
+
let iniDir = path.join(__dirname, 'assets');
|
|
201
|
+
let iniFile = 'db.ini';
|
|
202
|
+
|
|
203
|
+
if (opts.remote) {
|
|
204
|
+
iniDir = `${opts.mytDir}/remotes`;
|
|
205
|
+
iniFile = `${opts.remote}.ini`;
|
|
206
|
+
}
|
|
207
|
+
const iniPath = path.join(iniDir, iniFile);
|
|
208
|
+
|
|
209
|
+
if (!await fs.pathExists(iniPath))
|
|
210
|
+
throw new Error(`Database config file not found: ${iniPath}`);
|
|
211
|
+
|
|
212
|
+
let dbConfig;
|
|
213
|
+
try {
|
|
214
|
+
const iniConfig = ini.parse(await fs.readFile(iniPath, 'utf8')).client;
|
|
215
|
+
dbConfig = {
|
|
216
|
+
host: iniConfig.host,
|
|
217
|
+
port: iniConfig.port,
|
|
218
|
+
user: iniConfig.user,
|
|
219
|
+
password: iniConfig.password,
|
|
220
|
+
multipleStatements: true,
|
|
221
|
+
authPlugins: {
|
|
222
|
+
mysql_clear_password() {
|
|
223
|
+
return () => iniConfig.password + '\0';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
if (iniConfig.ssl_ca) {
|
|
228
|
+
dbConfig.ssl = {
|
|
229
|
+
ca: await fs.readFile(`${opts.mytDir}/${iniConfig.ssl_ca}`),
|
|
230
|
+
rejectUnauthorized: iniConfig.ssl_verify_server_cert != undefined
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} catch(err) {
|
|
234
|
+
const newErr = Error(`Cannot process the ini file, check that the syntax is correct: ${iniPath}`);
|
|
235
|
+
newErr.stack += `\nCaused by: ${err.stack}`;
|
|
236
|
+
throw newErr;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (opts.socket)
|
|
240
|
+
dbConfig.socketPath = '/var/run/mysqld/mysqld.sock';
|
|
241
|
+
|
|
242
|
+
Object.assign(opts, {
|
|
243
|
+
iniFile,
|
|
244
|
+
dbConfig
|
|
245
|
+
});
|
|
246
|
+
this.opts = opts;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async unload() {
|
|
250
|
+
if (this.conn)
|
|
251
|
+
await this.conn.end();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
getopts(opts) {
|
|
255
|
+
const argv = process.argv.slice(2);
|
|
256
|
+
const values = getopts(argv, opts);
|
|
257
|
+
const cleanValues = {};
|
|
258
|
+
for (const opt in values)
|
|
259
|
+
if (opt.length > 1 || opt == '_')
|
|
260
|
+
cleanValues[opt] = values[opt];
|
|
261
|
+
return cleanValues;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async dbConnect() {
|
|
265
|
+
const {opts} = this;
|
|
266
|
+
|
|
267
|
+
if (!this.conn) {
|
|
268
|
+
const conn = this.conn = await this.createConnection();
|
|
269
|
+
|
|
270
|
+
const [[schema]] = await conn.query(
|
|
271
|
+
`SHOW DATABASES LIKE ?`, [opts.versionSchema]
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
if (!schema)
|
|
275
|
+
await conn.query(`CREATE DATABASE ??`, [opts.versionSchema]);
|
|
276
|
+
await conn.query(`USE ??`, [opts.versionSchema]);
|
|
277
|
+
|
|
278
|
+
const [[res]] = await conn.query(
|
|
279
|
+
`SELECT COUNT(*) > 0 tableExists
|
|
280
|
+
FROM information_schema.tables
|
|
281
|
+
WHERE TABLE_SCHEMA = ?
|
|
282
|
+
AND TABLE_NAME = 'version'`,
|
|
283
|
+
[opts.versionSchema]
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
if (!res.tableExists) {
|
|
287
|
+
const structure = await fs.readFile(
|
|
288
|
+
`${__dirname}/assets/structure.sql`, 'utf8');
|
|
289
|
+
await conn.query(structure);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return this.conn;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async createConnection() {
|
|
297
|
+
return await mysql.createConnection(this.opts.dbConfig);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async fetchDbVersion() {
|
|
301
|
+
const [[version]] = await this.conn.query(
|
|
302
|
+
`SELECT number, gitCommit
|
|
303
|
+
FROM version WHERE code = ?`,
|
|
304
|
+
[this.opts.code]
|
|
305
|
+
);
|
|
306
|
+
return version;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
parseVersionDir(versionDir) {
|
|
310
|
+
const match = versionDir.match(/^([0-9]+)-([a-zA-Z0-9]+)?$/);
|
|
311
|
+
if (!match) return null;
|
|
312
|
+
return {
|
|
313
|
+
number: match[1],
|
|
314
|
+
name: match[2]
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async changedRoutines(commitSha) {
|
|
319
|
+
const repo = await this.openRepo();
|
|
320
|
+
const changes = [];
|
|
321
|
+
const changesMap = new Map();
|
|
322
|
+
|
|
323
|
+
async function pushChanges(diff) {
|
|
324
|
+
if (!diff) return;
|
|
325
|
+
const patches = await diff.patches();
|
|
326
|
+
|
|
327
|
+
for (const patch of patches) {
|
|
328
|
+
const path = patch.newFile().path();
|
|
329
|
+
const match = path.match(/^routines\/(.+)\.sql$/);
|
|
330
|
+
if (!match) continue;
|
|
331
|
+
|
|
332
|
+
let change = changesMap.get(match[1]);
|
|
333
|
+
if (!change) {
|
|
334
|
+
change = {path: match[1]};
|
|
335
|
+
changes.push(change);
|
|
336
|
+
changesMap.set(match[1], change);
|
|
337
|
+
}
|
|
338
|
+
change.mark = patch.isDeleted() ? '-' : '+';
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const head = await repo.getHeadCommit();
|
|
343
|
+
|
|
344
|
+
if (head && commitSha) {
|
|
345
|
+
let commit;
|
|
346
|
+
let notFound;
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
commit = await repo.getCommit(commitSha);
|
|
350
|
+
notFound = false;
|
|
351
|
+
} catch (err) {
|
|
352
|
+
if (err.errorFunction == 'Commit.lookup')
|
|
353
|
+
notFound = true;
|
|
354
|
+
else
|
|
355
|
+
throw err;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (notFound) {
|
|
359
|
+
console.warn(`Database commit not found, trying git fetch`.yellow);
|
|
360
|
+
await repo.fetchAll();
|
|
361
|
+
commit = await repo.getCommit(commitSha);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const commitTree = await commit.getTree();
|
|
365
|
+
|
|
366
|
+
const headTree = await head.getTree();
|
|
367
|
+
const diff = await headTree.diff(commitTree);
|
|
368
|
+
await pushChanges(diff);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
await pushChanges(await this.getUnstaged(repo));
|
|
372
|
+
await pushChanges(await this.getStaged(repo));
|
|
373
|
+
|
|
374
|
+
return changes.sort((a, b) => {
|
|
375
|
+
if (b.mark != a.mark)
|
|
376
|
+
return b.mark == '-' ? 1 : -1;
|
|
377
|
+
return a.path.localeCompare(b.path);
|
|
378
|
+
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async openRepo() {
|
|
383
|
+
const {opts} = this;
|
|
384
|
+
|
|
385
|
+
if (!await fs.pathExists(`${opts.workspace}/.git`))
|
|
386
|
+
throw new Error ('Git not initialized');
|
|
387
|
+
|
|
388
|
+
return await nodegit.Repository.open(opts.workspace);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async getStaged(repo) {
|
|
392
|
+
const head = await repo.getHeadCommit();
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const emptyTree = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
396
|
+
const headTree = await (head
|
|
397
|
+
? head.getTree()
|
|
398
|
+
: nodegit.Tree.lookup(repo, emptyTree)
|
|
399
|
+
);
|
|
400
|
+
return await nodegit.Diff.treeToIndex(repo, headTree, null);
|
|
401
|
+
} catch (err) {
|
|
402
|
+
console.warn('Cannot fetch staged changes:', err.message);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async getUnstaged(repo) {
|
|
407
|
+
return await nodegit.Diff.indexToWorkdir(repo, null, {
|
|
408
|
+
flags: nodegit.Diff.OPTION.SHOW_UNTRACKED_CONTENT
|
|
409
|
+
| nodegit.Diff.OPTION.RECURSE_UNTRACKED_DIRS
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async initDump(dumpFile) {
|
|
414
|
+
const dumpDir = this.opts.dumpDir;
|
|
415
|
+
if (!await fs.pathExists(dumpDir))
|
|
416
|
+
await fs.mkdir(dumpDir);
|
|
417
|
+
|
|
418
|
+
const dumpPath = path.join(dumpDir, dumpFile);
|
|
419
|
+
|
|
420
|
+
// FIXME: If it's called after docker.build() statement it creates an
|
|
421
|
+
// "invalid" WriteStream
|
|
422
|
+
const dumpStream = await fs.createWriteStream(dumpPath);
|
|
423
|
+
|
|
424
|
+
await docker.build(__dirname, {
|
|
425
|
+
tag: 'myt/client',
|
|
426
|
+
file: path.join(__dirname, 'server', 'Dockerfile.client')
|
|
427
|
+
}, this.opts.debug);
|
|
428
|
+
|
|
429
|
+
return dumpStream;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async dumpFixtures(dumpStream, tables, replace) {
|
|
433
|
+
const fixturesArgs = [
|
|
434
|
+
'--no-create-info',
|
|
435
|
+
'--skip-triggers',
|
|
436
|
+
'--skip-extended-insert',
|
|
437
|
+
'--skip-disable-keys',
|
|
438
|
+
'--skip-add-locks',
|
|
439
|
+
'--skip-set-charset',
|
|
440
|
+
'--skip-comments',
|
|
441
|
+
'--skip-tz-utc'
|
|
442
|
+
];
|
|
443
|
+
|
|
444
|
+
if (replace)
|
|
445
|
+
fixturesArgs.push('--replace');
|
|
446
|
+
|
|
447
|
+
for (const schema in tables) {
|
|
448
|
+
const escapedSchema = '`'+ schema.replace('`', '``') +'`';
|
|
449
|
+
await dumpStream.write(
|
|
450
|
+
`USE ${escapedSchema};\n`,
|
|
451
|
+
'utf8'
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
const args = fixturesArgs.concat([schema], tables[schema]);
|
|
455
|
+
await this.runDump('mysqldump', args, dumpStream);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async runDump(command, args, dumpStream) {
|
|
460
|
+
const iniPath = path.join(this.opts.subdir || '', 'remotes', this.opts.iniFile);
|
|
461
|
+
const myArgs = [
|
|
462
|
+
`--defaults-file=${iniPath}`
|
|
463
|
+
];
|
|
464
|
+
const execOptions = {
|
|
465
|
+
stdio: [
|
|
466
|
+
process.stdin,
|
|
467
|
+
dumpStream,
|
|
468
|
+
process.stderr
|
|
469
|
+
]
|
|
470
|
+
};
|
|
471
|
+
const commandArgs = [command].concat(myArgs, args);
|
|
472
|
+
await docker.run('myt/client', commandArgs, {
|
|
473
|
+
addHost: 'host.docker.internal:host-gateway',
|
|
474
|
+
volume: `${this.opts.mytDir}:/workspace`,
|
|
475
|
+
rm: true
|
|
476
|
+
}, execOptions);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
showHelp(opts, usage, command) {
|
|
480
|
+
const prefix = `${'Usage:'.gray} [npx] myt`;
|
|
481
|
+
|
|
482
|
+
if (command) {
|
|
483
|
+
let log = [prefix, command.blue];
|
|
484
|
+
if (usage.operand) log.push(`[<${usage.operand}>]`);
|
|
485
|
+
if (opts) log.push('[<options>]');
|
|
486
|
+
console.log(log.join(' '))
|
|
487
|
+
} else
|
|
488
|
+
console.log(`${prefix} [<options>] ${'<command>'.blue} [<args>]`);
|
|
489
|
+
|
|
490
|
+
if (usage.description)
|
|
491
|
+
console.log(`${'Description:'.gray} ${usage.description}`);
|
|
492
|
+
|
|
493
|
+
if (opts && opts.alias) {
|
|
494
|
+
const alias = opts.alias;
|
|
495
|
+
const boolean = opts.boolean || [];
|
|
496
|
+
|
|
497
|
+
console.log('Options:'.gray);
|
|
498
|
+
for (const opt in alias) {
|
|
499
|
+
const paramDescription = usage.params[opt] || '';
|
|
500
|
+
let longOpt = opt;
|
|
501
|
+
if (boolean.indexOf(longOpt) === -1)
|
|
502
|
+
longOpt += ` <string>`;
|
|
503
|
+
longOpt = camelToSnake(longOpt).padEnd(22, ' ')
|
|
504
|
+
console.log(` -${alias[opt]}, --${longOpt} ${paramDescription}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Executes an SQL script.
|
|
511
|
+
*
|
|
512
|
+
* @param {Connection} conn MySQL connection object
|
|
513
|
+
* @returns {Array<Result>} The resultset
|
|
514
|
+
*/
|
|
515
|
+
async queryFromFile(conn, file) {
|
|
516
|
+
const sql = await fs.readFile(file, 'utf8');
|
|
517
|
+
return await this.multiQuery(conn, sql);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Executes a multi-query string.
|
|
522
|
+
*
|
|
523
|
+
* @param {Connection} conn MySQL connection object
|
|
524
|
+
* @param {String} sql SQL multi-query string
|
|
525
|
+
* @returns {Array<Result>} The resultset
|
|
526
|
+
*/
|
|
527
|
+
async multiQuery(conn, sql) {
|
|
528
|
+
let results = [];
|
|
529
|
+
const stmts = this.querySplit(sql);
|
|
530
|
+
|
|
531
|
+
for (const stmt of stmts)
|
|
532
|
+
results = results.concat(await conn.query(stmt));
|
|
533
|
+
|
|
534
|
+
return results;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Splits an SQL muti-query into a single-query array, it does an small
|
|
539
|
+
* parse to correctly handle the DELIMITER statement.
|
|
540
|
+
*
|
|
541
|
+
* @param {Array<String>} stmts The splitted SQL statements
|
|
542
|
+
*/
|
|
543
|
+
querySplit(sql) {
|
|
544
|
+
const stmts = [];
|
|
545
|
+
let i,
|
|
546
|
+
char,
|
|
547
|
+
token,
|
|
548
|
+
escaped,
|
|
549
|
+
stmtStart;
|
|
550
|
+
|
|
551
|
+
let delimiter = ';';
|
|
552
|
+
const delimiterRe = /\s*delimiter\s+(\S+)[^\S\r\n]*(?:\r?\n|\r|$)/yi;
|
|
553
|
+
|
|
554
|
+
function begins(str) {
|
|
555
|
+
let j;
|
|
556
|
+
for (j = 0; j < str.length; j++)
|
|
557
|
+
if (sql[i + j] != str[j])
|
|
558
|
+
return false;
|
|
559
|
+
i += j;
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
for (i = 0; i < sql.length;) {
|
|
564
|
+
stmtStart = i;
|
|
565
|
+
|
|
566
|
+
delimiterRe.lastIndex = i;
|
|
567
|
+
const match = sql.match(delimiterRe);
|
|
568
|
+
if (match) {
|
|
569
|
+
delimiter = match[1];
|
|
570
|
+
i += match[0].length;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
let delimiterFound = false;
|
|
575
|
+
while (i < sql.length) {
|
|
576
|
+
char = sql[i];
|
|
577
|
+
|
|
578
|
+
if (token) {
|
|
579
|
+
if (!escaped && begins(token.end))
|
|
580
|
+
token = null;
|
|
581
|
+
else {
|
|
582
|
+
escaped = !escaped && token.escape(char);
|
|
583
|
+
i++;
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
delimiterFound = begins(delimiter);
|
|
587
|
+
if (delimiterFound) break;
|
|
588
|
+
|
|
589
|
+
const tok = tokenIndex.get(char);
|
|
590
|
+
if (tok && begins(tok.start))
|
|
591
|
+
token = tok;
|
|
592
|
+
else
|
|
593
|
+
i++;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
let len = i - stmtStart;
|
|
598
|
+
if (delimiterFound) len -= delimiter.length;
|
|
599
|
+
const stmt = sql.substr(stmtStart, len);
|
|
600
|
+
|
|
601
|
+
if (!/^\s*$/.test(stmt))
|
|
602
|
+
stmts.push(stmt);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return stmts;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const tokens = {
|
|
610
|
+
string: {
|
|
611
|
+
start: '\'',
|
|
612
|
+
end: '\'',
|
|
613
|
+
escape: char => char == '\'' || char == '\\'
|
|
614
|
+
},
|
|
615
|
+
quotedString: {
|
|
616
|
+
start: '"',
|
|
617
|
+
end: '"',
|
|
618
|
+
escape: char => char == '"' || char == '\\'
|
|
619
|
+
},
|
|
620
|
+
id: {
|
|
621
|
+
start: '`',
|
|
622
|
+
end: '`',
|
|
623
|
+
escape: char => char == '`'
|
|
624
|
+
},
|
|
625
|
+
multiComment: {
|
|
626
|
+
start: '/*',
|
|
627
|
+
end: '*/',
|
|
628
|
+
escape: () => false
|
|
629
|
+
},
|
|
630
|
+
singleComment: {
|
|
631
|
+
start: '-- ',
|
|
632
|
+
end: '\n',
|
|
633
|
+
escape: () => false
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const tokenIndex = new Map();
|
|
638
|
+
for (const tokenId in tokens) {
|
|
639
|
+
const token = tokens[tokenId];
|
|
640
|
+
tokenIndex.set(token.start[0], token);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
module.exports = Myt;
|
|
644
|
+
|
|
645
|
+
if (require.main === module)
|
|
646
|
+
new Myt().run();
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@verdnatura/myt",
|
|
3
|
+
"version": "1.5.1",
|
|
4
|
+
"author": "Verdnatura Levante SL",
|
|
5
|
+
"description": "MySQL Version Control",
|
|
6
|
+
"license": "GPL-3.0",
|
|
7
|
+
"bin": {
|
|
8
|
+
"myt": "cli.js",
|
|
9
|
+
"myvc": "cli.js",
|
|
10
|
+
"myv": "cli.js"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/verdnatura/myt.git"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@sqltools/formatter": "^1.2.3",
|
|
18
|
+
"colors": "^1.4.0",
|
|
19
|
+
"ejs": "^3.1.6",
|
|
20
|
+
"fs-extra": "^8.1.0",
|
|
21
|
+
"getopts": "^2.3.0",
|
|
22
|
+
"ini": "^1.3.8",
|
|
23
|
+
"mysql2": "^2.3.3",
|
|
24
|
+
"nodegit": "^0.27.0",
|
|
25
|
+
"require-yaml": "0.0.1",
|
|
26
|
+
"sha.js": "^2.4.11"
|
|
27
|
+
},
|
|
28
|
+
"main": "index.js",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"mysql",
|
|
34
|
+
"mariadb",
|
|
35
|
+
"migrations",
|
|
36
|
+
"git",
|
|
37
|
+
"vcs",
|
|
38
|
+
"database",
|
|
39
|
+
"version",
|
|
40
|
+
"control",
|
|
41
|
+
"sql"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
FROM mysql:5.6
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
FROM debian:bullseye-slim
|
|
2
|
+
|
|
3
|
+
ARG DEBIAN_FRONTEND=noninteractive
|
|
4
|
+
RUN apt-get update \
|
|
5
|
+
&& apt-get install -y --no-install-recommends \
|
|
6
|
+
mariadb-client \
|
|
7
|
+
libmariadb3 \
|
|
8
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
9
|
+
|
|
10
|
+
COPY \
|
|
11
|
+
server/docker-dump.sh \
|
|
12
|
+
server/docker-fixtures.sh \
|
|
13
|
+
/usr/local/bin/
|
|
14
|
+
WORKDIR /workspace
|