mysqlsyncr 1.1.12 → 1.2.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.
Files changed (62) hide show
  1. package/README.md +59 -0
  2. package/__tests__/acceptCurrent.test.js +117 -0
  3. package/__tests__/columnAttributes.test.js +101 -0
  4. package/__tests__/options.test.js +259 -0
  5. package/__tests__/parser.test.js +167 -166
  6. package/__tests__/parser2.test.js +155 -152
  7. package/__tests__/paths.test.js +21 -0
  8. package/__tests__/prompt.test.js +106 -0
  9. package/__tests__/sync.test.js +348 -0
  10. package/__tests__/templateDrift.test.js +72 -0
  11. package/__tests__/versions.test.js +138 -0
  12. package/dist/index.cjs +4059 -1733
  13. package/dist/index.js +4101 -1777
  14. package/package.json +66 -59
  15. package/rollup.config.js +2 -1
  16. package/src/DBStructureChecker.js +47 -0
  17. package/src/acceptCurrent.js +225 -0
  18. package/src/apply.js +254 -136
  19. package/src/cli.js +61 -34
  20. package/src/compare.js +106 -92
  21. package/src/differences.js +258 -212
  22. package/src/dump.js +63 -54
  23. package/src/getters.js +151 -139
  24. package/src/index.js +223 -169
  25. package/src/loggers.js +156 -139
  26. package/src/normalizers.js +28 -6
  27. package/src/options.js +133 -0
  28. package/src/parsers.js +82 -36
  29. package/src/paths.js +41 -0
  30. package/src/prompt.js +109 -0
  31. package/src/rebuild.js +106 -0
  32. package/src/rows.js +46 -0
  33. package/src/socketServer.js +106 -24
  34. package/src/templateDrift.js +93 -0
  35. package/src/versions.js +112 -0
  36. package/ui/README.md +55 -40
  37. package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-k1h7X_-h.woff +0 -0
  38. package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-B7du-70m.woff +0 -0
  39. package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-CoAZ_DKt.woff +0 -0
  40. package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-D0406B4n.woff +0 -0
  41. package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-CnAg2DeQ.woff +0 -0
  42. package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-BG9VWE5v.woff +0 -0
  43. package/ui/dist/spa/assets/MainLayout-DLH1Kfg-.js +1 -0
  44. package/ui/dist/spa/assets/axios-2hQ_j_Tz.js +1 -0
  45. package/ui/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-OUIwM9U8.woff +0 -0
  46. package/ui/dist/spa/assets/index-Cqrg985P.js +23 -0
  47. package/ui/dist/spa/assets/index-pOWtXlXQ.css +1 -0
  48. package/ui/dist/spa/index.html +2 -2
  49. package/ui/package.json +43 -42
  50. package/ui/src/components/DefinitionDiff.vue +169 -0
  51. package/ui/src/pages/MainComponent.vue +704 -78
  52. package/ui/dist/spa/assets/KFOkCnqEu92Fr1MmgVxIIzQ-Y2mpUUkw.woff +0 -0
  53. package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmEU9fBBc--ViM7Ag4Q.woff +0 -0
  54. package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmSU5fBBc--CrFzpgl6.woff +0 -0
  55. package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmWUlfBBc--CW3_XjG_.woff +0 -0
  56. package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmYUtfBBc--Dr8fnyGc.woff +0 -0
  57. package/ui/dist/spa/assets/KFOmCnqEu92Fr1Mu4mxM-DAYh6T4l.woff +0 -0
  58. package/ui/dist/spa/assets/MainLayout-CvQD3xKr.js +0 -1
  59. package/ui/dist/spa/assets/axios-C0XcclIb.js +0 -1
  60. package/ui/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
  61. package/ui/dist/spa/assets/index-CwWo8xmP.js +0 -40
  62. package/ui/dist/spa/assets/index-kXpA8n8r.css +0 -5
package/src/index.js CHANGED
@@ -1,169 +1,223 @@
1
- #!/usr/bin/env node
2
-
3
- import mysql from 'mysql2';
4
- import util from 'util';
5
- import fs from 'fs';
6
- import path from 'path';
7
- import { cli } from './cli.js';
8
- import { dumpAllDatabases, getDatabasesFromExistingDumps } from './dump.js';
9
- import { compareAllDatabases } from './compare.js';
10
- import { applyDifferences } from './apply.js';
11
- import { startApiServer } from './socketServer.js'; // Import the new socket server
12
- import open from 'open';
13
- import { fileURLToPath } from 'node:url';
14
-
15
- class DBStructureChecker {
16
- constructor(config, targetDatabase = null) {
17
- this.config = config;
18
- this.targetDatabase = targetDatabase;
19
- this.connection = mysql.createConnection(this.config)
20
- this.dumpAllDatabases = dumpAllDatabases;
21
- this.compareAllDatabases = compareAllDatabases;
22
- this.getDatabases = () => {
23
-
24
- fs.readFileSync(path.join(__dirname, 'databases.json'), 'utf8');
25
- }
26
- // this.connection.on('error', (err) => {
27
- // this.emit('error', err);
28
- // console.error('Database connection error:', err);
29
- // });
30
- this.connection.query = util.promisify(this.connection.query); // Promisify for async/await
31
-
32
- }
33
-
34
- async reconnect() {
35
- this.connection = mysql.createConnection(this.config);
36
- this.connection.query = util.promisify(this.connection.query); // Promisify for async/await
37
- await this.connection.connect(); // Connect to the database
38
- }
39
-
40
- async closeConnection() {
41
- if (this.connection.state !== 'disconnected') {
42
- await this.connection.end();
43
- }
44
- }
45
-
46
-
47
- }
48
-
49
- const compare = async (config, output) => {
50
- return new Promise(async (resolve, reject) => {
51
- let checker
52
- try {
53
- checker = new DBStructureChecker(config);
54
- checker.connection.on('connect', async () => {
55
- const diffs = await compareAllDatabases(checker.connection, output, false, 'compare', { output: output });
56
- resolve({ type: 'success', diffs });
57
- await checker.closeConnection();
58
- });
59
- } catch (e) {
60
- reject({ type: 'error', error: e.message });
61
- await checker.closeConnection();
62
- }
63
- });
64
- }
65
-
66
- const apply = async (config, database, diffs) => {
67
- return new Promise((resolve, reject) => {
68
- let checker;
69
- try {
70
- checker = new DBStructureChecker(config, database);
71
-
72
- checker.connection.on('error', async (err) => {
73
- console.error('Database connection error in route apply:', err);
74
- reject({ type: 'error', error: err.message });
75
- await checker.closeConnection();
76
- });
77
-
78
- checker.connection.on('connect', async () => {
79
- console.log('Connected to the database');
80
- checker.targetDatabase = database;
81
- checker.command = 'apply';
82
- checker.options = { config, database, diffs };
83
- try {
84
- console.log('Applying the database structure based on JSON dumps');
85
- await applyDifferences(checker.connection, database, diffs);
86
- resolve({ type: 'success', message: 'Apply completed successfully' });
87
- await checker.closeConnection();
88
- } catch (err) {
89
- reject({ type: 'error', error: err.message });
90
- await checker.closeConnection();
91
- }
92
- });
93
- } catch (e) {
94
- reject({ type: 'error', error: e.message });
95
- }
96
- });
97
- }
98
-
99
- export { DBStructureChecker, dumpAllDatabases, compare, startApiServer, apply, getDatabasesFromExistingDumps };
100
-
101
- // Only run this if this is the main module
102
- const isMain = (() => {
103
- try {
104
- const modulePath = fileURLToPath(import.meta.url);
105
- return process.argv[1] === modulePath;
106
-
107
- } catch (e) {
108
- return false;
109
- }
110
- })();
111
-
112
- if (isMain) {
113
- (async () => {
114
- const { command, config, argv } = cli();
115
- console.log("MySQLsyncr started via command line")
116
- if (command === 'ui') {
117
- const PORT = argv.uiPort
118
- startApiServer(PORT, argv); // Start the Socket.IO server
119
- console.log('Server started on port', PORT);
120
- console.log('Open http://localhost:' + PORT + ' in your browser to use the UI');
121
- if (argv.openUI && argv.openUI === true || argv.openUI === 'true') {
122
- await open(`http://localhost:${PORT}`);
123
- }
124
- } else {
125
- // Handle other commands (dump, compare, update) as before
126
- const checker = new DBStructureChecker(config, argv.database);
127
- checker.connection.on('connect', async () => {
128
- console.log('Connected to the database');
129
- checker.targetDatabase = argv.database;
130
- checker.command = command;
131
- checker.options = argv;
132
- try {
133
- if (command === 'dump') {
134
- console.log('Dumping the database structure to files');
135
- await dumpAllDatabases(checker.connection, command, argv);
136
- } else if (command === 'compare') {
137
- console.log('Comparing the database structure with JSON dumps (dry run)');
138
- let diffs = await compareAllDatabases(checker.connection, argv.output, false, command, argv); // Dry run
139
- let totalDiffs = Object.values(diffs).reduce((acc, arr) => acc + arr.length, 0);
140
- console.log('Total differences count:', totalDiffs);
141
- fs.writeFileSync(path.join(argv.output, 'diffs.json'), JSON.stringify(diffs, null, 4));
142
- } else if (command === 'update') {
143
- console.log('Updating the database structure based on JSON dumps');
144
- let totalDiffs = Infinity; // Initialize with a large number
145
- let previousDiffs = totalDiffs; // Store previous differences
146
- let runCount = 0; // Counter for the number of runs
147
- let maxRuns = 6;
148
- while (runCount < maxRuns && totalDiffs > 0 && (totalDiffs < previousDiffs || previousDiffs === Infinity)) {
149
- console.log('Running update command. Run count:', runCount);
150
- let diffs = await compareAllDatabases(checker.connection, argv.output, true, command, argv); // Apply updates
151
- previousDiffs = totalDiffs; // Update previous differences
152
- totalDiffs = Object.values(diffs).reduce((acc, arr) => acc + arr.length, 0);
153
- console.log('Total differences count:', totalDiffs);
154
- runCount++;
155
- if (totalDiffs > 0 && runCount < maxRuns) {
156
- console.log('Total differences count is still greater than 0. Waiting 1 second before next run. Run count:', runCount);
157
- await new Promise(resolve => setTimeout(resolve, 1000));
158
- }
159
- }
160
- }
161
- } catch (err) {
162
- console.error(err.message);
163
- } finally {
164
- await checker.closeConnection();
165
- }
166
- });
167
- }
168
- })();
169
- }
1
+ #!/usr/bin/env node
2
+
3
+ import { cli } from './cli.js';
4
+ import { DBStructureChecker } from './DBStructureChecker.js';
5
+ import { dumpAllDatabases, getDatabasesFromExistingDumps } from './dump.js';
6
+ import { compareAllDatabases } from './compare.js';
7
+ import { applyDifferences, describeFailures } from './apply.js';
8
+ import { startApiServer } from './socketServer.js'; // Import the new socket server
9
+ import { rebuildTemplate } from './rebuild.js';
10
+ import { assertOutsidePackage, templateFolderProblem } from './paths.js';
11
+ import { getVersionInfo } from './versions.js';
12
+ import { acceptCurrentIntoTemplate } from './acceptCurrent.js';
13
+ import open from 'open';
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+
19
+ const compare = async (config, output) => {
20
+ return new Promise(async (resolve, reject) => {
21
+ let checker
22
+ try {
23
+ checker = new DBStructureChecker(config);
24
+ // Without this a refused connection is an unhandled 'error' event and the promise never settles
25
+ checker.connection.on('error', (err) => {
26
+ reject({ type: 'error', error: err.message });
27
+ });
28
+ checker.connection.on('connect', async () => {
29
+ try {
30
+ const diffs = await compareAllDatabases(checker.connection, output, false, 'compare', { output: output });
31
+ resolve({ type: 'success', diffs });
32
+ } catch (e) {
33
+ // e.g. an invalid syncOptions.json
34
+ reject({ type: 'error', error: e.message });
35
+ } finally {
36
+ await checker.closeConnection();
37
+ }
38
+ });
39
+ } catch (e) {
40
+ reject({ type: 'error', error: e.message });
41
+ if (checker) await checker.closeConnection();
42
+ }
43
+ });
44
+ }
45
+
46
+ const apply = async (config, database, diffs) => {
47
+ return new Promise((resolve, reject) => {
48
+ let checker;
49
+ try {
50
+ checker = new DBStructureChecker(config, database);
51
+
52
+ checker.connection.on('error', async (err) => {
53
+ console.error('Database connection error in route apply:', err);
54
+ reject({ type: 'error', error: err.message });
55
+ await checker.closeConnection();
56
+ });
57
+
58
+ checker.connection.on('connect', async () => {
59
+ console.log('Connected to the database');
60
+ checker.targetDatabase = database;
61
+ checker.command = 'apply';
62
+ checker.options = { config, database, diffs };
63
+ try {
64
+ console.log('Applying the database structure based on JSON dumps');
65
+ const result = await applyDifferences(checker.connection, database, diffs);
66
+ if (result.failures.length > 0) {
67
+ // Callers treat a resolved apply as done, so anything that failed rejects
68
+ reject({ type: 'error', error: describeFailures(result), failures: result.failures, warnings: result.warnings });
69
+ } else {
70
+ resolve({ type: 'success', message: `Applied ${result.attempted} of ${result.attempted}`, warnings: result.warnings });
71
+ }
72
+ await checker.closeConnection();
73
+ } catch (err) {
74
+ reject({ type: 'error', error: err.message });
75
+ await checker.closeConnection();
76
+ }
77
+ });
78
+ } catch (e) {
79
+ reject({ type: 'error', error: e.message });
80
+ }
81
+ });
82
+ }
83
+
84
+ export { DBStructureChecker, dumpAllDatabases, compare, startApiServer, apply, getDatabasesFromExistingDumps, rebuildTemplate, acceptCurrentIntoTemplate };
85
+
86
+ // Show which MySQL versions are being compared, and what that means
87
+ const printVersions = async (connection, templateDir) => {
88
+ const { server, template, notes } = await getVersionInfo(connection, templateDir);
89
+ const templateText = template
90
+ ? `${template.version}${template.inferred ? ` (inferred: ${template.reason})` : ''}`
91
+ : 'unknown';
92
+ console.log(`MySQL server: ${server ?? 'unknown'}; template: ${templateText}`);
93
+ for (const note of notes) console.log(` Note: ${note}`);
94
+ }
95
+
96
+ // Only run this if this is the main module
97
+ const isMain = (() => {
98
+ // CommonJS build (dist/index.cjs, the mysqlsyncr bin)
99
+ if (typeof require !== 'undefined' && typeof module !== 'undefined') {
100
+ return require.main === module;
101
+ }
102
+ // ES module (node src/index.js): compare real paths, since the bin may be a symlink
103
+ try {
104
+ return !!process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
105
+ } catch (e) {
106
+ return false;
107
+ }
108
+ })();
109
+
110
+ if (isMain) {
111
+ (async () => {
112
+ const { command, config, argv } = await cli();
113
+ if (!command) {
114
+ process.exitCode = 1;
115
+ return;
116
+ }
117
+ // dump writes the template, compare writes diffs.json and rebuild --write rewrites the template
118
+ if (command === 'dump' || command === 'compare' || (command === 'rebuild' && argv.write)) {
119
+ try {
120
+ assertOutsidePackage(argv.output);
121
+ } catch (err) {
122
+ console.error(err.message);
123
+ process.exitCode = 1;
124
+ return;
125
+ }
126
+ }
127
+ // compare, update and rebuild read an existing template; an empty folder would compare nothing
128
+ if (command === 'compare' || command === 'update' || command === 'rebuild') {
129
+ const problem = templateFolderProblem(argv.output);
130
+ if (problem) {
131
+ console.error(`Template folder: ${problem}`);
132
+ process.exitCode = 1;
133
+ return;
134
+ }
135
+ }
136
+ if (command === 'dump' && !fs.existsSync(argv.output)) {
137
+ fs.mkdirSync(argv.output, { recursive: true });
138
+ console.log(`Created ${path.resolve(argv.output)}`);
139
+ }
140
+ console.log("MySQLsyncr started via command line")
141
+ if (command === 'ui') {
142
+ const PORT = argv.uiPort
143
+ startApiServer(PORT, argv); // Start the Socket.IO server
144
+ console.log('Server started on port', PORT);
145
+ console.log('Open http://localhost:' + PORT + ' in your browser to use the UI');
146
+ if (argv.openUI && argv.openUI === true || argv.openUI === 'true') {
147
+ await open(`http://localhost:${PORT}`);
148
+ }
149
+ } else if (command === 'rebuild') {
150
+ // Works on the files only; no database connection needed
151
+ try {
152
+ const { changes: report, notes } = await rebuildTemplate(argv.output, { write: argv.write });
153
+ for (const [table, changes] of Object.entries(report)) {
154
+ console.log(`\n${table}`);
155
+ for (const change of changes) console.log(` ${change}`);
156
+ }
157
+ const count = Object.keys(report).length;
158
+ console.log(`\n${count} table(s) ${argv.write ? 'rebuilt' : 'would change. Run again with --write to rebuild them'}`);
159
+ if (Object.keys(notes).length > 0) {
160
+ console.log('\nCheck by hand:');
161
+ for (const [table, note] of Object.entries(notes)) console.log(` ${table}: ${note}`);
162
+ }
163
+ } catch (err) {
164
+ console.error(err.message);
165
+ process.exitCode = 1;
166
+ }
167
+ } else {
168
+ // Handle other commands (dump, compare, update) as before
169
+ console.log(`Connecting to MySQL as ${config.user}@${config.host}:${config.port}${config.password ? '' : ' with no password'}`);
170
+ const checker = new DBStructureChecker(config, argv.database);
171
+ checker.connection.on('error', (err) => {
172
+ console.error(`MySQL connection error (${config.user}@${config.host}:${config.port}): ${err.message}`);
173
+ if (err.code === 'ER_ACCESS_DENIED_ERROR') {
174
+ // e.g. 'root'@'172.17.0.1' when MySQL runs in Docker
175
+ console.error(`The user and password were rejected. The address in MySQL's message is where the server saw the connection come from, not the host connected to.`);
176
+ }
177
+ process.exitCode = 1;
178
+ });
179
+ checker.connection.on('connect', async () => {
180
+ console.log('Connected to the database');
181
+ checker.targetDatabase = argv.database;
182
+ checker.command = command;
183
+ checker.options = argv;
184
+ try {
185
+ if (command === 'dump') {
186
+ console.log('Dumping the database structure to files');
187
+ await dumpAllDatabases(checker.connection, command, argv);
188
+ } else if (command === 'compare') {
189
+ await printVersions(checker.connection, argv.output);
190
+ console.log('Comparing the database structure with JSON dumps (dry run)');
191
+ let diffs = await compareAllDatabases(checker.connection, argv.output, false, command, argv); // Dry run
192
+ let totalDiffs = Object.values(diffs).reduce((acc, arr) => acc + arr.length, 0);
193
+ console.log('Total differences count:', totalDiffs);
194
+ fs.writeFileSync(path.join(argv.output, 'diffs.json'), JSON.stringify(diffs, null, 4));
195
+ } else if (command === 'update') {
196
+ await printVersions(checker.connection, argv.output);
197
+ console.log('Updating the database structure based on JSON dumps');
198
+ let totalDiffs = Infinity; // Initialize with a large number
199
+ let previousDiffs = totalDiffs; // Store previous differences
200
+ let runCount = 0; // Counter for the number of runs
201
+ let maxRuns = 6;
202
+ while (runCount < maxRuns && totalDiffs > 0 && (totalDiffs < previousDiffs || previousDiffs === Infinity)) {
203
+ console.log('Running update command. Run count:', runCount);
204
+ let diffs = await compareAllDatabases(checker.connection, argv.output, true, command, argv); // Apply updates
205
+ previousDiffs = totalDiffs; // Update previous differences
206
+ totalDiffs = Object.values(diffs).reduce((acc, arr) => acc + arr.length, 0);
207
+ console.log('Total differences count:', totalDiffs);
208
+ runCount++;
209
+ if (totalDiffs > 0 && runCount < maxRuns) {
210
+ console.log('Total differences count is still greater than 0. Waiting 1 second before next run. Run count:', runCount);
211
+ await new Promise(resolve => setTimeout(resolve, 1000));
212
+ }
213
+ }
214
+ }
215
+ } catch (err) {
216
+ console.error(err.message);
217
+ } finally {
218
+ await checker.closeConnection();
219
+ }
220
+ });
221
+ }
222
+ })();
223
+ }