@verdnatura/myt 1.5.19 → 1.5.21

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.
Files changed (69) hide show
  1. package/.dockerignore +2 -2
  2. package/LICENSE +17 -17
  3. package/README.md +284 -278
  4. package/assets/db.ini +5 -5
  5. package/assets/myt.default.yml +28 -11
  6. package/assets/structure.sql +24 -24
  7. package/cli.js +4 -4
  8. package/exporters/event.ejs +18 -18
  9. package/exporters/event.js +31 -31
  10. package/exporters/event.sql +19 -19
  11. package/exporters/function.ejs +17 -17
  12. package/exporters/function.js +27 -27
  13. package/exporters/function.sql +16 -16
  14. package/exporters/procedure.ejs +11 -11
  15. package/exporters/procedure.js +23 -23
  16. package/exporters/procedure.sql +14 -14
  17. package/exporters/trigger.ejs +4 -4
  18. package/exporters/trigger.js +10 -10
  19. package/exporters/trigger.sql +13 -13
  20. package/exporters/view.ejs +7 -7
  21. package/exporters/view.js +29 -29
  22. package/exporters/view.sql +12 -12
  23. package/helpers/local-fixtures.js +79 -0
  24. package/lib/command.js +19 -13
  25. package/lib/conn.js +141 -141
  26. package/lib/docker.js +133 -133
  27. package/lib/dumper.js +104 -83
  28. package/lib/exporter-engine.js +112 -112
  29. package/lib/exporter.js +86 -86
  30. package/lib/repo.js +29 -29
  31. package/lib/server.js +71 -71
  32. package/lib/util.js +7 -7
  33. package/myt-clean.js +58 -58
  34. package/myt-create.js +74 -74
  35. package/myt-dump.js +134 -75
  36. package/myt-fixtures.js +28 -29
  37. package/myt-init.js +36 -36
  38. package/myt-pull.js +183 -183
  39. package/myt-push.js +575 -568
  40. package/myt-run.js +190 -177
  41. package/myt-start.js +54 -54
  42. package/myt-version.js +218 -218
  43. package/myt.js +378 -361
  44. package/package.json +41 -41
  45. package/server/Dockerfile.base +1 -1
  46. package/server/Dockerfile.client +14 -14
  47. package/server/Dockerfile.dump +13 -16
  48. package/server/Dockerfile.server +25 -25
  49. package/server/docker-dump.sh +4 -4
  50. package/server/docker-fixtures.sh +5 -4
  51. package/server/docker-import.sh +10 -6
  52. package/server/docker-init.sh +23 -20
  53. package/server/docker-start.sh +10 -10
  54. package/server/docker.cnf +9 -9
  55. package/template/dump/Dockerfile.base +1 -1
  56. package/template/dump/dump.after.sql +1 -0
  57. package/template/dump/dump.before.sql +1 -0
  58. package/template/dump/fixtures.after.sql +1 -0
  59. package/template/dump/fixtures.before.sql +1 -0
  60. package/template/myt.config.yml +11 -11
  61. package/template/package.json +13 -13
  62. package/template/remotes/docker.ini +4 -4
  63. package/template/remotes/local.ini +5 -5
  64. package/template/routines/README.md +1 -1
  65. package/template/versions/00001-firstVersion/00-firstScript.sql +1 -1
  66. package/template/versions/README.md +1 -1
  67. package/template/dump/afterDump.sql +0 -1
  68. package/template/dump/beforeDump.sql +0 -1
  69. package/template/dump/fixtures.sql +0 -1
package/myt-push.js CHANGED
@@ -1,568 +1,575 @@
1
- const Myt = require('./myt');
2
- const Command = require('./lib/command');
3
- const fs = require('fs-extra');
4
- const nodegit = require('nodegit');
5
- const ExporterEngine = require('./lib/exporter-engine');
6
- const connExt = require('./lib/conn');
7
- const repoExt = require('./lib/repo');
8
- const SqlString = require('sqlstring');
9
-
10
- /**
11
- * Pushes changes to remote.
12
- */
13
- class Push extends Command {
14
- static usage = {
15
- description: 'Apply changes into database',
16
- params: {
17
- force: 'Answer yes to all questions',
18
- commit: 'Wether to save the commit SHA into database',
19
- sums: 'Save SHA sums of pushed objects',
20
- triggers: 'Wether to exclude triggers, used to generate local DB'
21
- },
22
- operand: 'remote'
23
- };
24
-
25
- static opts = {
26
- alias: {
27
- force: 'f',
28
- commit: 'c',
29
- sums: 's',
30
- triggers: 't'
31
- },
32
- boolean: [
33
- 'force',
34
- 'commit',
35
- 'sums',
36
- 'triggers'
37
- ]
38
- };
39
-
40
- async run(myt, opts) {
41
- const conn = await myt.dbConnect();
42
- this.conn = conn;
43
-
44
- if (opts.remote == 'local')
45
- opts.commit = true;
46
-
47
- // Obtain exclusive lock
48
-
49
- const [[row]] = await conn.query(
50
- `SELECT GET_LOCK('myt_push', 30) getLock`);
51
-
52
- const pingTimeout = setInterval(async() => {
53
- try {
54
- await conn.ping()
55
- } catch (e) {}
56
- }, 60 * 1000);
57
-
58
- if (!row.getLock) {
59
- let isUsed = 0;
60
-
61
- if (row.getLock == 0) {
62
- const [[row]] = await conn.query(
63
- `SELECT IS_USED_LOCK('myt_push') isUsed`);
64
- isUsed = row.isUsed;
65
- }
66
-
67
- throw new Error(`Cannot obtain exclusive lock, used by connection ${isUsed}`);
68
- }
69
-
70
- async function releaseLock() {
71
- await conn.query(`DO RELEASE_LOCK('myt_push')`);
72
- }
73
-
74
- try {
75
- await this.push(myt, opts, conn);
76
- } catch(err) {
77
- try {
78
- await releaseLock();
79
- } catch (e) {}
80
- throw err;
81
- } finally {
82
- clearInterval(pingTimeout);
83
- }
84
-
85
- await releaseLock();
86
- }
87
-
88
- async push(myt, opts, conn) {
89
- const pushConn = await myt.createConnection();
90
-
91
- // Get database version
92
-
93
- const version = await myt.fetchDbVersion() || {};
94
-
95
- console.log(
96
- `Database information:`
97
- + `\n -> Version: ${version.number}`
98
- + `\n -> Commit: ${version.gitCommit}`
99
- );
100
-
101
- if (!version.number)
102
- version.number = String('0').padStart(opts.versionDigits, '0');
103
- if (!/^[0-9]*$/.test(version.number))
104
- throw new Error('Wrong database version');
105
-
106
- // Prevent push to production by mistake
107
-
108
- if (opts.remote == 'production') {
109
- console.log(
110
- '\n ( ( ) ( ( ) ) '
111
- + '\n )\\ ))\\ ) ( /( )\\ ) ( ))\\ ) ( /( ( /( '
112
- + '\n(()/(()/( )\\()|()/( ( )\\ ) /(()/( )\\()) )\\())'
113
- + '\n /(_))(_)|(_)\\ /(_)) )\\ (((_) ( )(_))(_)|(_)\\ ((_)\\ '
114
- + '\n(_))(_)) ((_|_))_ _ ((_))\\___(_(_()|__)) ((_) _((_)'
115
- + '\n| _ \\ _ \\ / _ \\| \\| | | ((/ __|_ _|_ _| / _ \\| \\| |'
116
- + '\n| _/ /| (_) | |) | |_| || (__ | | | | | (_) | . |'
117
- + '\n|_| |_|_\\ \\___/|___/ \\___/ \\___| |_| |___| \\___/|_|\\_|'
118
- + '\n'
119
- );
120
-
121
- if (!opts.force) {
122
- const readline = require('readline');
123
- const rl = readline.createInterface({
124
- input: process.stdin,
125
- output: process.stdout
126
- });
127
- const answer = await new Promise(resolve => {
128
- rl.question('Are you sure? (Default: no) [yes|no] ', resolve);
129
- });
130
- rl.close();
131
-
132
- if (answer !== 'yes')
133
- throw new Error('Changes aborted');
134
- }
135
- }
136
-
137
- // Apply versions
138
-
139
- console.log('Applying versions.');
140
-
141
- let nChanges = 0;
142
- let silent = true;
143
- const versionsDir = opts.versionsDir;
144
-
145
- function logVersion(version, name, error) {
146
- console.log('', version.bold, name);
147
- }
148
- function logScript(type, message, error) {
149
- console.log(' ', type.bold, message);
150
- }
151
- function isUndoScript(script) {
152
- return /\.undo\.sql$/.test(script);
153
- }
154
-
155
- const skipFiles = new Set([
156
- 'README.md',
157
- '.archive'
158
- ]);
159
-
160
- if (await fs.pathExists(versionsDir)) {
161
- const versionDirs = await fs.readdir(versionsDir);
162
-
163
- for (const versionDir of versionDirs) {
164
- if (skipFiles.has(versionDir)) continue;
165
-
166
- const dirVersion = myt.parseVersionDir(versionDir);
167
- if (!dirVersion) {
168
- logVersion('[?????]'.yellow, versionDir,
169
- `Wrong directory name.`
170
- );
171
- continue;
172
- }
173
-
174
- const versionNumber = dirVersion.number;
175
- const versionName = dirVersion.name;
176
-
177
- if (versionNumber.length != version.number.length) {
178
- logVersion('[*****]'.gray, versionDir,
179
- `Bad version length, should have ${version.number.length} characters.`
180
- );
181
- continue;
182
- }
183
-
184
- const scriptsDir = `${versionsDir}/${versionDir}`;
185
- const scripts = await fs.readdir(scriptsDir);
186
-
187
- const [versionLog] = await conn.query(
188
- `SELECT file FROM versionLog
189
- WHERE code = ?
190
- AND number = ?
191
- AND errorNumber IS NULL`,
192
- [opts.code, versionNumber]
193
- );
194
-
195
- for (const script of scripts)
196
- if (!isUndoScript(script)
197
- && versionLog.findIndex(x => x.file == script) === -1) {
198
- silent = false;
199
- break;
200
- }
201
-
202
- if (silent) continue;
203
- logVersion(`[${versionNumber}]`.cyan, versionName);
204
-
205
- for (const script of scripts) {
206
- if (!/^[0-9]{2}-[a-zA-Z0-9_]+(.undo)?\.sql$/.test(script)) {
207
- logScript('[W]'.yellow, script, `Wrong file name.`);
208
- continue;
209
- }
210
- if (isUndoScript(script))
211
- continue;
212
-
213
- const [[row]] = await conn.query(
214
- `SELECT errorNumber FROM versionLog
215
- WHERE code = ?
216
- AND number = ?
217
- AND file = ?`,
218
- [
219
- opts.code,
220
- versionNumber,
221
- script
222
- ]
223
- );
224
- const apply = !row || row.errorNumber;
225
- const actionMsg = apply ? '[+]'.green : '[I]'.blue;
226
-
227
- logScript(actionMsg, script);
228
- if (!apply) continue;
229
-
230
- let err;
231
- try {
232
- await connExt.queryFromFile(pushConn,
233
- `${scriptsDir}/${script}`);
234
- } catch (e) {
235
- err = e;
236
- }
237
-
238
- await conn.query(
239
- `INSERT INTO versionLog
240
- SET code = ?,
241
- number = ?,
242
- file = ?,
243
- user = USER(),
244
- updated = NOW(),
245
- errorNumber = ?,
246
- errorMessage = ?
247
- ON DUPLICATE KEY UPDATE
248
- updated = VALUES(updated),
249
- user = VALUES(user),
250
- errorNumber = VALUES(errorNumber),
251
- errorMessage = VALUES(errorMessage)`,
252
- [
253
- opts.code,
254
- versionNumber,
255
- script,
256
- err && err.errno,
257
- err && err.message
258
- ]
259
- );
260
-
261
- if (err) throw err;
262
- nChanges++;
263
- }
264
-
265
- await this.updateVersion('number', versionNumber);
266
- }
267
- }
268
-
269
- // Apply routines
270
-
271
- console.log('Applying changed routines.');
272
-
273
- const gitExists = await fs.pathExists(`${opts.workspace}/.git`);
274
-
275
- let nRoutines = 0;
276
- const changes = await this.changedRoutines(version.gitCommit);
277
-
278
- const routines = [];
279
- for (const change of changes)
280
- if (change.isRoutine)
281
- routines.push([
282
- change.schema,
283
- change.name,
284
- change.type.name
285
- ]);
286
-
287
- if (routines.length) {
288
- await conn.query(
289
- `DROP TEMPORARY TABLE IF EXISTS tProcsPriv`
290
- );
291
- await conn.query(
292
- `CREATE TEMPORARY TABLE tProcsPriv
293
- ENGINE = MEMORY
294
- SELECT * FROM mysql.procs_priv
295
- WHERE (Db, Routine_name, Routine_type) IN (?)`,
296
- [routines]
297
- );
298
- }
299
-
300
- const engine = new ExporterEngine(conn, opts);
301
- await engine.init();
302
-
303
- async function finalize() {
304
- await engine.saveInfo();
305
-
306
- if (routines.length) {
307
- await conn.query('FLUSH PRIVILEGES');
308
- await conn.query(`DROP TEMPORARY TABLE tProcsPriv`);
309
- }
310
- }
311
-
312
- for (const change of changes)
313
- try {
314
- if (opts.trigger && change.type.name === 'TRIGGER')
315
- continue;
316
-
317
- const schema = change.schema;
318
- const name = change.name;
319
- const type = change.type.name.toLowerCase();
320
- const fullPath = `${opts.routinesDir}/${change.path}.sql`;
321
- const exists = await fs.pathExists(fullPath);
322
-
323
- let newSql;
324
- if (exists)
325
- newSql = await fs.readFile(fullPath, 'utf8');
326
- const oldSql = await engine.fetchRoutine(type, schema, name);
327
- const oldSum = engine.getShaSum(type, schema, name);
328
- const isEqual = newSql == oldSql;
329
-
330
- let statusMsg;
331
- if (exists && !oldSql)
332
- statusMsg = '[+]'.green;
333
- else if (!exists)
334
- statusMsg = '[-]'.red;
335
- else
336
- statusMsg = '[·]'.yellow;
337
-
338
- let actionMsg;
339
- if (isEqual)
340
- actionMsg = '[I]'.blue;
341
- else
342
- actionMsg = '[A]'.green;
343
-
344
- const typeMsg = `[${change.type.abbr}]`[change.type.color];
345
- console.log('',
346
- (statusMsg + actionMsg).bold,
347
- typeMsg.bold,
348
- change.fullName
349
- );
350
-
351
- if (!isEqual) {
352
- const scapedSchema = SqlString.escapeId(schema, true);
353
-
354
- if (exists) {
355
- if (change.type.name === 'VIEW')
356
- await pushConn.query(`USE ${scapedSchema}`);
357
-
358
- await connExt.multiQuery(pushConn, newSql);
359
-
360
- if (change.isRoutine) {
361
- await conn.query(
362
- `INSERT IGNORE INTO mysql.procs_priv
363
- SELECT * FROM tProcsPriv
364
- WHERE Db = ?
365
- AND Routine_name = ?
366
- AND Routine_type = ?`,
367
- [schema, name, change.type.name]
368
- );
369
- }
370
-
371
- if (opts.sums || oldSum || (opts.sumViews && type === 'view'))
372
- await engine.fetchShaSum(type, schema, name);
373
- } else {
374
- const escapedName =
375
- scapedSchema + '.' +
376
- SqlString.escapeId(name, true);
377
-
378
- const query = `DROP ${change.type.name} IF EXISTS ${escapedName}`;
379
- await pushConn.query(query);
380
-
381
- engine.deleteShaSum(type, schema, name);
382
- }
383
-
384
- nRoutines++;
385
- }
386
- } catch (err) {
387
- try {
388
- await finalize();
389
- } catch (e) {
390
- console.error(e);
391
- }
392
- throw err;
393
- }
394
-
395
- await finalize();
396
-
397
- if (nRoutines > 0) {
398
- console.log(` -> ${nRoutines} routines have changed.`);
399
- } else
400
- console.log(` -> No routines changed.`);
401
-
402
- if (gitExists && opts.commit) {
403
- const repo = await nodegit.Repository.open(this.opts.workspace);
404
- const head = await repo.getHeadCommit();
405
-
406
- if (head && version.gitCommit !== head.sha())
407
- await this.updateVersion('gitCommit', head.sha());
408
- }
409
-
410
- // End
411
-
412
- await pushConn.end();
413
- }
414
-
415
- async updateVersion(column, value) {
416
- column = SqlString.escapeId(column, true);
417
- await this.conn.query(
418
- `INSERT INTO version
419
- SET code = ?,
420
- ${column} = ?,
421
- updated = NOW()
422
- ON DUPLICATE KEY UPDATE
423
- ${column} = VALUES(${column}),
424
- updated = VALUES(updated)`,
425
- [
426
- this.opts.code,
427
- value
428
- ]
429
- );
430
- }
431
-
432
- async changedRoutines(commitSha) {
433
- const repo = await this.myt.openRepo();
434
- const changes = [];
435
- const changesMap = new Map();
436
-
437
- async function pushChanges(diff) {
438
- if (!diff) return;
439
- const patches = await diff.patches();
440
-
441
- for (const patch of patches) {
442
- const path = patch.newFile().path();
443
- const match = path.match(/^routines\/(.+)\.sql$/);
444
- if (!match) continue;
445
-
446
- let change = changesMap.get(match[1]);
447
- if (!change) {
448
- change = {path: match[1]};
449
- changes.push(change);
450
- changesMap.set(match[1], change);
451
- }
452
- change.mark = patch.isDeleted() ? '-' : '+';
453
- }
454
- }
455
-
456
- const head = await repo.getHeadCommit();
457
-
458
- if (head && commitSha) {
459
- let commit;
460
- let notFound;
461
-
462
- try {
463
- commit = await repo.getCommit(commitSha);
464
- notFound = false;
465
- } catch (err) {
466
- if (err.errorFunction == 'Commit.lookup')
467
- notFound = true;
468
- else
469
- throw err;
470
- }
471
-
472
- if (notFound) {
473
- console.warn(`Database commit not found, trying git fetch`.yellow);
474
- await repo.fetchAll();
475
- commit = await repo.getCommit(commitSha);
476
- }
477
-
478
- const commitTree = await commit.getTree();
479
-
480
- const headTree = await head.getTree();
481
- const diff = await headTree.diff(commitTree);
482
- await pushChanges(diff);
483
- }
484
-
485
- await pushChanges(await repoExt.getUnstaged(repo));
486
- await pushChanges(await repoExt.getStaged(repo));
487
-
488
- const routines = [];
489
- for (const change of changes)
490
- routines.push(new Routine(change));
491
-
492
- return routines.sort((a, b) => {
493
- if (b.mark != a.mark)
494
- return b.mark == '-' ? -1 : 1;
495
-
496
- if (b.type.name !== a.type.name) {
497
- if (b.type.name == 'VIEW')
498
- return -1;
499
- if (a.type.name == 'VIEW')
500
- return 1;
501
- }
502
-
503
- return a.path.localeCompare(b.path);
504
- });
505
- }
506
- }
507
-
508
- const typeMap = {
509
- events: {
510
- name: 'EVENT',
511
- abbr: 'EVNT',
512
- color: 'cyan'
513
- },
514
- functions: {
515
- name: 'FUNCTION',
516
- abbr: 'FUNC',
517
- color: 'cyan'
518
- },
519
- procedures: {
520
- name: 'PROCEDURE',
521
- abbr: 'PROC',
522
- color: 'yellow'
523
- },
524
- triggers: {
525
- name: 'TRIGGER',
526
- abbr: 'TRIG',
527
- color: 'blue'
528
- },
529
- views: {
530
- name: 'VIEW',
531
- abbr: 'VIEW',
532
- color: 'magenta'
533
- },
534
- };
535
-
536
- const routineTypes = new Set([
537
- 'FUNCTION',
538
- 'PROCEDURE'
539
- ]);
540
-
541
- class Routine {
542
- constructor(change) {
543
- const path = change.path;
544
- const split = path.split('/');
545
-
546
- const schema = split[0];
547
- const type = typeMap[split[1]];
548
- const name = split[2];
549
-
550
- if (split.length !== 3 || !type)
551
- throw new Error(`Wrong routine path for '${path}', check that the sql file is located in the correct directory`);
552
-
553
- Object.assign(this, {
554
- path,
555
- mark: change.mark,
556
- type,
557
- schema,
558
- name,
559
- fullName: `${schema}.${name}`,
560
- isRoutine: routineTypes.has(type.name)
561
- });
562
- }
563
- }
564
-
565
- module.exports = Push;
566
-
567
- if (require.main === module)
568
- new Myt().run(Push);
1
+ const Myt = require('./myt');
2
+ const Command = require('./lib/command');
3
+ const fs = require('fs-extra');
4
+ const nodegit = require('nodegit');
5
+ const ExporterEngine = require('./lib/exporter-engine');
6
+ const connExt = require('./lib/conn');
7
+ const repoExt = require('./lib/repo');
8
+ const SqlString = require('sqlstring');
9
+
10
+ /**
11
+ * Pushes changes to remote.
12
+ */
13
+ class Push extends Command {
14
+ static usage = {
15
+ description: 'Apply changes into database',
16
+ params: {
17
+ force: 'Answer yes to all questions',
18
+ commit: 'Wether to save the commit SHA into database',
19
+ sums: 'Save SHA sums of pushed objects',
20
+ triggers: 'Wether to exclude triggers, used to generate local DB'
21
+ },
22
+ operand: 'remote'
23
+ };
24
+
25
+ static opts = {
26
+ alias: {
27
+ force: 'f',
28
+ commit: 'c',
29
+ sums: 's',
30
+ triggers: 't'
31
+ },
32
+ boolean: [
33
+ 'force',
34
+ 'commit',
35
+ 'sums',
36
+ 'triggers'
37
+ ]
38
+ };
39
+
40
+ async run(myt, opts) {
41
+ const conn = await myt.dbConnect();
42
+ this.conn = conn;
43
+
44
+ if (opts.remote == 'local')
45
+ opts.commit = true;
46
+
47
+ // Obtain exclusive lock
48
+
49
+ const [[row]] = await conn.query(
50
+ `SELECT GET_LOCK('myt_push', 30) getLock`);
51
+
52
+ const pingTimeout = setInterval(async() => {
53
+ try {
54
+ await conn.ping()
55
+ } catch (e) {}
56
+ }, 60 * 1000);
57
+
58
+ if (!row.getLock) {
59
+ let isUsed = 0;
60
+
61
+ if (row.getLock == 0) {
62
+ const [[row]] = await conn.query(
63
+ `SELECT IS_USED_LOCK('myt_push') isUsed`);
64
+ isUsed = row.isUsed;
65
+ }
66
+
67
+ throw new Error(`Cannot obtain exclusive lock, used by connection ${isUsed}`);
68
+ }
69
+
70
+ async function releaseLock() {
71
+ await conn.query(`DO RELEASE_LOCK('myt_push')`);
72
+ }
73
+
74
+ try {
75
+ await this.push(myt, opts, conn);
76
+ } catch(err) {
77
+ try {
78
+ await releaseLock();
79
+ } catch (e) {}
80
+ throw err;
81
+ } finally {
82
+ clearInterval(pingTimeout);
83
+ }
84
+
85
+ await releaseLock();
86
+ }
87
+
88
+ async push(myt, opts, conn) {
89
+ const pushConn = await myt.createConnection();
90
+
91
+ // Get database version
92
+
93
+ const version = await myt.fetchDbVersion() || {};
94
+
95
+ console.log(
96
+ `Database information:`
97
+ + `\n -> Version: ${version.number}`
98
+ + `\n -> Commit: ${version.gitCommit}`
99
+ );
100
+
101
+ if (!version.number)
102
+ version.number = String('0').padStart(opts.versionDigits, '0');
103
+ if (!/^[0-9]*$/.test(version.number))
104
+ throw new Error('Wrong database version');
105
+
106
+ // Prevent push to production by mistake
107
+
108
+ if (opts.remote == 'production') {
109
+ console.log(
110
+ '\n ( ( ) ( ( ) ) '
111
+ + '\n )\\ ))\\ ) ( /( )\\ ) ( ))\\ ) ( /( ( /( '
112
+ + '\n(()/(()/( )\\()|()/( ( )\\ ) /(()/( )\\()) )\\())'
113
+ + '\n /(_))(_)|(_)\\ /(_)) )\\ (((_) ( )(_))(_)|(_)\\ ((_)\\ '
114
+ + '\n(_))(_)) ((_|_))_ _ ((_))\\___(_(_()|__)) ((_) _((_)'
115
+ + '\n| _ \\ _ \\ / _ \\| \\| | | ((/ __|_ _|_ _| / _ \\| \\| |'
116
+ + '\n| _/ /| (_) | |) | |_| || (__ | | | | | (_) | . |'
117
+ + '\n|_| |_|_\\ \\___/|___/ \\___/ \\___| |_| |___| \\___/|_|\\_|'
118
+ + '\n'
119
+ );
120
+
121
+ if (!opts.force) {
122
+ const readline = require('readline');
123
+ const rl = readline.createInterface({
124
+ input: process.stdin,
125
+ output: process.stdout
126
+ });
127
+ const answer = await new Promise(resolve => {
128
+ rl.question('Are you sure? (Default: no) [yes|no] ', resolve);
129
+ });
130
+ rl.close();
131
+
132
+ if (answer !== 'yes')
133
+ throw new Error('Changes aborted');
134
+ }
135
+ }
136
+
137
+ // Apply versions
138
+
139
+ console.log('Applying versions.');
140
+
141
+ let nChanges = 0;
142
+ let silent = true;
143
+ const versionsDir = opts.versionsDir;
144
+
145
+ function logVersion(version, name, error) {
146
+ console.log('', version.bold, name);
147
+ }
148
+ function logScript(type, message, error) {
149
+ console.log(' ', type.bold, message);
150
+ }
151
+ function isUndoScript(script) {
152
+ return /\.undo\.sql$/.test(script);
153
+ }
154
+
155
+ const skipFiles = new Set([
156
+ 'README.md',
157
+ '.archive'
158
+ ]);
159
+
160
+ if (await fs.pathExists(versionsDir)) {
161
+ const versionDirs = await fs.readdir(versionsDir);
162
+
163
+ for (const versionDir of versionDirs) {
164
+ if (skipFiles.has(versionDir)) continue;
165
+
166
+ const dirVersion = myt.parseVersionDir(versionDir);
167
+ if (!dirVersion) {
168
+ logVersion('[?????]'.yellow, versionDir,
169
+ `Wrong directory name.`
170
+ );
171
+ continue;
172
+ }
173
+
174
+ const versionNumber = dirVersion.number;
175
+ const versionName = dirVersion.name;
176
+
177
+ if (versionNumber.length != version.number.length) {
178
+ logVersion('[*****]'.gray, versionDir,
179
+ `Bad version length, should have ${version.number.length} characters.`
180
+ );
181
+ continue;
182
+ }
183
+
184
+ const scriptsDir = `${versionsDir}/${versionDir}`;
185
+ const scripts = await fs.readdir(scriptsDir);
186
+
187
+ const [versionLog] = await conn.query(
188
+ `SELECT file FROM versionLog
189
+ WHERE code = ?
190
+ AND number = ?
191
+ AND errorNumber IS NULL`,
192
+ [opts.code, versionNumber]
193
+ );
194
+
195
+ for (const script of scripts)
196
+ if (!isUndoScript(script)
197
+ && versionLog.findIndex(x => x.file == script) === -1) {
198
+ silent = false;
199
+ break;
200
+ }
201
+
202
+ if (silent) continue;
203
+ logVersion(`[${versionNumber}]`.cyan, versionName);
204
+
205
+ for (const script of scripts) {
206
+ if (!/^[0-9]{2}-[a-zA-Z0-9_]+(.undo)?\.sql$/.test(script)) {
207
+ logScript('[W]'.yellow, script, `Wrong file name.`);
208
+ continue;
209
+ }
210
+ if (isUndoScript(script))
211
+ continue;
212
+
213
+ const [[row]] = await conn.query(
214
+ `SELECT errorNumber FROM versionLog
215
+ WHERE code = ?
216
+ AND number = ?
217
+ AND file = ?`,
218
+ [
219
+ opts.code,
220
+ versionNumber,
221
+ script
222
+ ]
223
+ );
224
+ const apply = !row || row.errorNumber;
225
+ const actionMsg = apply ? '[+]'.green : '[I]'.blue;
226
+
227
+ logScript(actionMsg, script);
228
+ if (!apply) continue;
229
+
230
+ let err;
231
+ try {
232
+ await connExt.queryFromFile(pushConn,
233
+ `${scriptsDir}/${script}`);
234
+ } catch (e) {
235
+ err = e;
236
+ }
237
+
238
+ await conn.query(
239
+ `INSERT INTO versionLog
240
+ SET code = ?,
241
+ number = ?,
242
+ file = ?,
243
+ user = USER(),
244
+ updated = NOW(),
245
+ errorNumber = ?,
246
+ errorMessage = ?
247
+ ON DUPLICATE KEY UPDATE
248
+ updated = VALUES(updated),
249
+ user = VALUES(user),
250
+ errorNumber = VALUES(errorNumber),
251
+ errorMessage = VALUES(errorMessage)`,
252
+ [
253
+ opts.code,
254
+ versionNumber,
255
+ script,
256
+ err && err.errno,
257
+ err && err.message
258
+ ]
259
+ );
260
+
261
+ if (err) throw err;
262
+ nChanges++;
263
+ }
264
+
265
+ await this.updateVersion('number', versionNumber);
266
+ }
267
+ }
268
+
269
+ // Apply routines
270
+
271
+ console.log('Applying changed routines.');
272
+
273
+ const gitExists = await fs.pathExists(`${opts.workspace}/.git`);
274
+
275
+ let nRoutines = 0;
276
+ const changes = await this.changedRoutines(version.gitCommit);
277
+
278
+ const routines = [];
279
+ for (const change of changes)
280
+ if (change.isRoutine)
281
+ routines.push([
282
+ change.schema,
283
+ change.name,
284
+ change.type.name
285
+ ]);
286
+
287
+ if (routines.length) {
288
+ await conn.query(
289
+ `DROP TEMPORARY TABLE IF EXISTS tProcsPriv`
290
+ );
291
+ await conn.query(
292
+ `CREATE TEMPORARY TABLE tProcsPriv
293
+ ENGINE = MEMORY
294
+ SELECT * FROM mysql.procs_priv
295
+ WHERE (Db, Routine_name, Routine_type) IN (?)`,
296
+ [routines]
297
+ );
298
+ }
299
+
300
+ const engine = new ExporterEngine(conn, opts);
301
+ await engine.init();
302
+
303
+ async function finalize() {
304
+ await engine.saveInfo();
305
+
306
+ if (routines.length) {
307
+ await conn.query('FLUSH PRIVILEGES');
308
+ await conn.query(`DROP TEMPORARY TABLE tProcsPriv`);
309
+ }
310
+ }
311
+
312
+ for (const change of changes)
313
+ try {
314
+ if (opts.triggers && change.type.name === 'TRIGGER')
315
+ continue;
316
+
317
+ const schema = change.schema;
318
+ const name = change.name;
319
+ const type = change.type.name.toLowerCase();
320
+ const fullPath = `${opts.routinesDir}/${change.path}.sql`;
321
+ const exists = await fs.pathExists(fullPath);
322
+
323
+ let newSql;
324
+ if (exists)
325
+ newSql = await fs.readFile(fullPath, 'utf8');
326
+ const oldSql = await engine.fetchRoutine(type, schema, name);
327
+ const oldSum = engine.getShaSum(type, schema, name);
328
+
329
+ const isMockFn = type == 'function'
330
+ && schema == opts.versionSchema
331
+ && opts.remote == 'local'
332
+ && opts.mockDate
333
+ && opts.mockFunctions
334
+ && opts.mockFunctions.indexOf(name) !== -1;
335
+ const ignore = newSql == oldSql || isMockFn;
336
+
337
+ let statusMsg;
338
+ if (exists && !oldSql)
339
+ statusMsg = '[+]'.green;
340
+ else if (!exists)
341
+ statusMsg = '[-]'.red;
342
+ else
343
+ statusMsg = '[·]'.yellow;
344
+
345
+ let actionMsg;
346
+ if (ignore)
347
+ actionMsg = '[I]'.blue;
348
+ else
349
+ actionMsg = '[A]'.green;
350
+
351
+ const typeMsg = `[${change.type.abbr}]`[change.type.color];
352
+ console.log('',
353
+ (statusMsg + actionMsg).bold,
354
+ typeMsg.bold,
355
+ change.fullName
356
+ );
357
+
358
+ if (!ignore) {
359
+ const scapedSchema = SqlString.escapeId(schema, true);
360
+
361
+ if (exists) {
362
+ if (change.type.name === 'VIEW')
363
+ await pushConn.query(`USE ${scapedSchema}`);
364
+
365
+ await connExt.multiQuery(pushConn, newSql);
366
+
367
+ if (change.isRoutine) {
368
+ await conn.query(
369
+ `INSERT IGNORE INTO mysql.procs_priv
370
+ SELECT * FROM tProcsPriv
371
+ WHERE Db = ?
372
+ AND Routine_name = ?
373
+ AND Routine_type = ?`,
374
+ [schema, name, change.type.name]
375
+ );
376
+ }
377
+
378
+ if (opts.sums || oldSum || (opts.sumViews && type === 'view'))
379
+ await engine.fetchShaSum(type, schema, name);
380
+ } else {
381
+ const escapedName =
382
+ scapedSchema + '.' +
383
+ SqlString.escapeId(name, true);
384
+
385
+ const query = `DROP ${change.type.name} IF EXISTS ${escapedName}`;
386
+ await pushConn.query(query);
387
+
388
+ engine.deleteShaSum(type, schema, name);
389
+ }
390
+
391
+ nRoutines++;
392
+ }
393
+ } catch (err) {
394
+ try {
395
+ await finalize();
396
+ } catch (e) {
397
+ console.error(e);
398
+ }
399
+ throw err;
400
+ }
401
+
402
+ await finalize();
403
+
404
+ if (nRoutines > 0) {
405
+ console.log(` -> ${nRoutines} routines have changed.`);
406
+ } else
407
+ console.log(` -> No routines changed.`);
408
+
409
+ if (gitExists && opts.commit) {
410
+ const repo = await nodegit.Repository.open(this.opts.workspace);
411
+ const head = await repo.getHeadCommit();
412
+
413
+ if (head && version.gitCommit !== head.sha())
414
+ await this.updateVersion('gitCommit', head.sha());
415
+ }
416
+
417
+ // End
418
+
419
+ await pushConn.end();
420
+ }
421
+
422
+ async updateVersion(column, value) {
423
+ column = SqlString.escapeId(column, true);
424
+ await this.conn.query(
425
+ `INSERT INTO version
426
+ SET code = ?,
427
+ ${column} = ?,
428
+ updated = NOW()
429
+ ON DUPLICATE KEY UPDATE
430
+ ${column} = VALUES(${column}),
431
+ updated = VALUES(updated)`,
432
+ [
433
+ this.opts.code,
434
+ value
435
+ ]
436
+ );
437
+ }
438
+
439
+ async changedRoutines(commitSha) {
440
+ const repo = await this.myt.openRepo();
441
+ const changes = [];
442
+ const changesMap = new Map();
443
+
444
+ async function pushChanges(diff) {
445
+ if (!diff) return;
446
+ const patches = await diff.patches();
447
+
448
+ for (const patch of patches) {
449
+ const path = patch.newFile().path();
450
+ const match = path.match(/^routines\/(.+)\.sql$/);
451
+ if (!match) continue;
452
+
453
+ let change = changesMap.get(match[1]);
454
+ if (!change) {
455
+ change = {path: match[1]};
456
+ changes.push(change);
457
+ changesMap.set(match[1], change);
458
+ }
459
+ change.mark = patch.isDeleted() ? '-' : '+';
460
+ }
461
+ }
462
+
463
+ const head = await repo.getHeadCommit();
464
+
465
+ if (head && commitSha) {
466
+ let commit;
467
+ let notFound;
468
+
469
+ try {
470
+ commit = await repo.getCommit(commitSha);
471
+ notFound = false;
472
+ } catch (err) {
473
+ if (err.errorFunction == 'Commit.lookup')
474
+ notFound = true;
475
+ else
476
+ throw err;
477
+ }
478
+
479
+ if (notFound) {
480
+ console.warn(`Database commit not found, trying git fetch`.yellow);
481
+ await repo.fetchAll();
482
+ commit = await repo.getCommit(commitSha);
483
+ }
484
+
485
+ const commitTree = await commit.getTree();
486
+
487
+ const headTree = await head.getTree();
488
+ const diff = await headTree.diff(commitTree);
489
+ await pushChanges(diff);
490
+ }
491
+
492
+ await pushChanges(await repoExt.getUnstaged(repo));
493
+ await pushChanges(await repoExt.getStaged(repo));
494
+
495
+ const routines = [];
496
+ for (const change of changes)
497
+ routines.push(new Routine(change));
498
+
499
+ return routines.sort((a, b) => {
500
+ if (b.mark != a.mark)
501
+ return b.mark == '-' ? -1 : 1;
502
+
503
+ if (b.type.name !== a.type.name) {
504
+ if (b.type.name == 'VIEW')
505
+ return -1;
506
+ if (a.type.name == 'VIEW')
507
+ return 1;
508
+ }
509
+
510
+ return a.path.localeCompare(b.path);
511
+ });
512
+ }
513
+ }
514
+
515
+ const typeMap = {
516
+ events: {
517
+ name: 'EVENT',
518
+ abbr: 'EVNT',
519
+ color: 'cyan'
520
+ },
521
+ functions: {
522
+ name: 'FUNCTION',
523
+ abbr: 'FUNC',
524
+ color: 'cyan'
525
+ },
526
+ procedures: {
527
+ name: 'PROCEDURE',
528
+ abbr: 'PROC',
529
+ color: 'yellow'
530
+ },
531
+ triggers: {
532
+ name: 'TRIGGER',
533
+ abbr: 'TRIG',
534
+ color: 'blue'
535
+ },
536
+ views: {
537
+ name: 'VIEW',
538
+ abbr: 'VIEW',
539
+ color: 'magenta'
540
+ },
541
+ };
542
+
543
+ const routineTypes = new Set([
544
+ 'FUNCTION',
545
+ 'PROCEDURE'
546
+ ]);
547
+
548
+ class Routine {
549
+ constructor(change) {
550
+ const path = change.path;
551
+ const split = path.split('/');
552
+
553
+ const schema = split[0];
554
+ const type = typeMap[split[1]];
555
+ const name = split[2];
556
+
557
+ if (split.length !== 3 || !type)
558
+ throw new Error(`Wrong routine path for '${path}', check that the sql file is located in the correct directory`);
559
+
560
+ Object.assign(this, {
561
+ path,
562
+ mark: change.mark,
563
+ type,
564
+ schema,
565
+ name,
566
+ fullName: `${schema}.${name}`,
567
+ isRoutine: routineTypes.has(type.name)
568
+ });
569
+ }
570
+ }
571
+
572
+ module.exports = Push;
573
+
574
+ if (require.main === module)
575
+ new Myt().run(Push);