mysqlsyncr 1.1.10 → 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 +246 -186
  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 +4070 -1730
  13. package/dist/index.js +4106 -1768
  14. package/package.json +66 -59
  15. package/rollup.config.js +25 -24
  16. package/src/DBStructureChecker.js +47 -0
  17. package/src/acceptCurrent.js +225 -0
  18. package/src/apply.js +481 -348
  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 -170
  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 +197 -145
  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 +311 -229
  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/loggers.js CHANGED
@@ -1,140 +1,157 @@
1
- let isVerbose = false; // Global variable to manage verbosity
2
-
3
- export const setVerbose = (verbose) => {
4
- isVerbose = verbose; // Function to set the verbosity level
5
- };
6
-
7
- export const logger = (...args) => {
8
- if (isVerbose) {
9
- console.log(...args); // Log all arguments if verbose is enabled
10
- }
11
- };
12
-
13
- export const logDifferences = (differences, database, dryRun) => {
14
- const formattedDifferences = differences.map(diff => {
15
- switch (diff.type) {
16
- case 'missing_procedure':
17
- return {
18
- Type: diff.type,
19
- Name: diff.Name,
20
- Details: `Procedure ${diff.Name} is missing`,
21
- Action: 'Procedure is missing',
22
- };
23
- case 'mismatched_procedure':
24
-
25
- return {
26
- Type: diff.type,
27
- Name: diff.Name,
28
- Details: `Mismatched procedure ${diff.Name}`,
29
- Action: 'Mismatched procedure',
30
- };
31
- case 'missing_field':
32
- return {
33
- Type: diff.type,
34
- Name: diff.tableName,
35
- Details: `Field ${diff.field.Field} is missing`,
36
- Action: 'Field is missing',
37
- };
38
- case 'missing_table':
39
- return {
40
- Type: diff.type,
41
- Name: diff.tableName,
42
- Details: `Table ${diff.tableName} is missing`,
43
- Action: 'Table is missing',
44
- };
45
- case 'extra_field':
46
- return {
47
- Type: diff.type,
48
- Name: diff.tableName,
49
- Details: `Field ${diff.field.Field} is extra`,
50
- Action: 'Field is extra',
51
- };
52
- case 'extra_table':
53
- return {
54
- Type: diff.type,
55
- Name: diff.tableName,
56
- Details: `Table ${diff.tableName} is extra`,
57
- Action: 'Table is extra',
58
- };
59
- case 'missing_view':
60
- return {
61
- Type: diff.type,
62
- Name: diff.viewName,
63
- Details: `View ${diff.viewName} is missing`,
64
- Action: 'View is missing',
65
- };
66
- case 'extra_index':
67
- return {
68
- Type: diff.type,
69
- Name: diff.tableName,
70
- Details: `Index ${diff.index.Name} is extra`,
71
- Action: 'Index is extra',
72
- };
73
- case 'missing_index':
74
- return {
75
- Type: diff.type,
76
- TableName: diff.tableName,
77
- Name: diff.index.Name,
78
- Details: `Index ${diff.index.Name} is missing`,
79
- Action: 'Index is missing',
80
- };
81
-
82
- case 'mismatched_index':
83
- return {
84
- Type: diff.type,
85
- TableName: diff.tableName,
86
- Name: diff.index.Name,
87
- From: diff.from,
88
- Details: `Index ${diff.index.Name} is mismatched`,
89
- Action: 'Index is mismatched',
90
- };
91
-
92
- case 'extra_trigger':
93
- return {
94
- Type: diff.type,
95
- Name: diff.trigger.Name,
96
- Details: `Trigger ${diff.trigger.Name} is extra`,
97
- Action: 'Trigger is extra',
98
- };
99
- case 'mismatched_trigger':
100
- return {
101
- Type: diff.type,
102
- Name: diff.trigger.Name,
103
- Details: `Trigger ${diff.trigger.Name} is mismatched`,
104
- Action: 'Trigger is mismatched',
105
- };
106
- case 'missing_trigger':
107
- console.log('missing_trigger', diff)
108
- return {
109
- Type: diff.type,
110
- Name: diff.trigger.Name,
111
- Details: `Trigger ${diff.trigger.Name} is missing`,
112
- Action: 'Trigger is missing',
113
- };
114
- default:
115
- return {
116
- Type: diff.type,
117
- Details: `Unhandled difference type: ${diff.type}`,
118
- };
119
- }
120
- });
121
-
122
- // Filter out any undefined or null entries
123
- const filteredDifferences = formattedDifferences.filter(diff => diff);
124
-
125
- // Log the differences in a table format
126
- if (filteredDifferences.length > 0) {
127
- if (dryRun) {
128
- logger(`Dry Run: Differences for database ${database}:`);
129
- } else {
130
- logger(`Differences applied for database ${database}:`);
131
- }
132
- console.table(filteredDifferences);
133
- } else {
134
- if (dryRun) {
135
- logger(`Dry Run: No differences found. Database: ${database}`);
136
- } else {
137
- logger(`No differences found. Database: ${database}`);
138
- }
139
- }
1
+ let isVerbose = false; // Global variable to manage verbosity
2
+
3
+ export const setVerbose = (verbose) => {
4
+ isVerbose = verbose; // Function to set the verbosity level
5
+ };
6
+
7
+ export const logger = (...args) => {
8
+ if (isVerbose) {
9
+ console.log(...args); // Log all arguments if verbose is enabled
10
+ }
11
+ };
12
+
13
+ export const logDifferences = (differences, database, dryRun) => {
14
+ const formattedDifferences = differences.map(diff => {
15
+ switch (diff.type) {
16
+ case 'missing_procedure':
17
+ return {
18
+ Type: diff.type,
19
+ Name: diff.Name,
20
+ Details: `Procedure ${diff.Name} is missing`,
21
+ Action: 'Procedure is missing',
22
+ };
23
+ case 'mismatched_procedure':
24
+
25
+ return {
26
+ Type: diff.type,
27
+ Name: diff.Name,
28
+ Details: `Mismatched procedure ${diff.Name}`,
29
+ Action: 'Mismatched procedure',
30
+ };
31
+ case 'missing_field':
32
+ return {
33
+ Type: diff.type,
34
+ Name: diff.tableName,
35
+ Details: `Field ${diff.field.Field} is missing`,
36
+ Action: 'Field is missing',
37
+ };
38
+ case 'mismatched_field': {
39
+ // `info` names the check that failed; Null is stored as NotNull.
40
+ const property = diff.info === 'Null' ? 'NotNull' : diff.info;
41
+ return {
42
+ Type: diff.type,
43
+ Name: diff.tableName,
44
+ Details: `Field ${diff.field.Field} ${diff.info} is ${JSON.stringify(diff.currentField?.[property])}, expected ${JSON.stringify(diff.field?.[property])}`,
45
+ Action: 'Field is mismatched',
46
+ };
47
+ }
48
+ case 'missing_table':
49
+ return {
50
+ Type: diff.type,
51
+ Name: diff.tableName,
52
+ Details: `Table ${diff.tableName} is missing`,
53
+ Action: 'Table is missing',
54
+ };
55
+ case 'extra_field':
56
+ return {
57
+ Type: diff.type,
58
+ Name: diff.tableName,
59
+ Details: `Field ${diff.field.Field} is extra`,
60
+ Action: 'Field is extra',
61
+ };
62
+ case 'extra_table':
63
+ return {
64
+ Type: diff.type,
65
+ Name: diff.tableName,
66
+ Details: `Table ${diff.tableName} is extra`,
67
+ Action: 'Table is extra',
68
+ };
69
+ case 'missing_view':
70
+ return {
71
+ Type: diff.type,
72
+ Name: diff.viewName,
73
+ Details: `View ${diff.viewName} is missing`,
74
+ Action: 'View is missing',
75
+ };
76
+ case 'extra_index':
77
+ return {
78
+ Type: diff.type,
79
+ Name: diff.tableName,
80
+ Details: `Index ${diff.index.Name} is extra`,
81
+ Action: 'Index is extra',
82
+ };
83
+ case 'missing_index':
84
+ return {
85
+ Type: diff.type,
86
+ TableName: diff.tableName,
87
+ Name: diff.index.Name,
88
+ Details: `Index ${diff.index.Name} is missing`,
89
+ Action: 'Index is missing',
90
+ };
91
+
92
+ case 'mismatched_index':
93
+ return {
94
+ Type: diff.type,
95
+ TableName: diff.tableName,
96
+ Name: diff.index.Name,
97
+ From: diff.from,
98
+ Details: `Index ${diff.index.Name} is mismatched`,
99
+ Action: 'Index is mismatched',
100
+ };
101
+
102
+ case 'extra_trigger':
103
+ return {
104
+ Type: diff.type,
105
+ Name: diff.trigger.Name,
106
+ Details: `Trigger ${diff.trigger.Name} is extra`,
107
+ Action: 'Trigger is extra',
108
+ };
109
+ case 'mismatched_trigger':
110
+ return {
111
+ Type: diff.type,
112
+ Name: diff.trigger.Name,
113
+ Details: `Trigger ${diff.trigger.Name} is mismatched`,
114
+ Action: 'Trigger is mismatched',
115
+ };
116
+ case 'missing_trigger':
117
+ logger('missing_trigger', diff)
118
+ return {
119
+ Type: diff.type,
120
+ Name: diff.trigger.Name,
121
+ Details: `Trigger ${diff.trigger.Name} is missing`,
122
+ Action: 'Trigger is missing',
123
+ };
124
+ case 'missing_row':
125
+ return {
126
+ Type: diff.type,
127
+ Name: diff.tableName,
128
+ Details: `Row ${JSON.stringify(diff.key)} is missing`,
129
+ Action: 'Row is missing',
130
+ };
131
+ default:
132
+ return {
133
+ Type: diff.type,
134
+ Details: `Unhandled difference type: ${diff.type}`,
135
+ };
136
+ }
137
+ });
138
+
139
+ // Filter out any undefined or null entries
140
+ const filteredDifferences = formattedDifferences.filter(diff => diff);
141
+
142
+ // Log the differences in a table format
143
+ if (filteredDifferences.length > 0) {
144
+ if (dryRun) {
145
+ logger(`Dry Run: Differences for database ${database}:`);
146
+ } else {
147
+ logger(`Differences applied for database ${database}:`);
148
+ }
149
+ console.table(filteredDifferences);
150
+ } else {
151
+ if (dryRun) {
152
+ logger(`Dry Run: No differences found. Database: ${database}`);
153
+ } else {
154
+ logger(`No differences found. Database: ${database}`);
155
+ }
156
+ }
140
157
  }
@@ -1,6 +1,28 @@
1
- export const normalizeSQLDefinition = (definition) => {
2
- return definition
3
- .replace(/\s+/g, ' ') // Replace multiple whitespace with a single space
4
- .replace(/'/g, "\\'") // Escape single quotes
5
- .trim(); // Trim leading and trailing whitespace
6
- }
1
+ /**
2
+ * Undo formatting that differs between MySQL versions but doesn't change the
3
+ * structure, so a template dumped from one version compares cleanly with
4
+ * another:
5
+ * - integer display widths: int(11) in 5.7 is int from 8.0.19, except
6
+ * tinyint(1) and ZEROFILL types, which keep them
7
+ * - year(4) in 5.7 is year from 8.0.19
8
+ * - utf8 is shown as utf8mb3 from 8.0.24 (fully from 8.0.30)
9
+ * Applies to column types and to procedure, trigger and view definitions.
10
+ */
11
+ export const normalizeVersionFormatting = (sql) => {
12
+ if (typeof sql !== 'string') return sql;
13
+ return sql
14
+ .replace(/\b(tinyint|smallint|mediumint|int|integer|bigint)\((\d+)\)(\s+unsigned)?(?!\s+(?:unsigned\s+)?zerofill)/gi,
15
+ (match, type, width, unsigned = '') => type.toLowerCase() === 'tinyint' && width === '1' && !unsigned ? match : `${type}${unsigned}`)
16
+ .replace(/\byear\(4\)/gi, 'year')
17
+ .replace(/\butf8mb3(?=\b|_)/gi, 'utf8');
18
+ }
19
+
20
+ // A column type compared across MySQL versions
21
+ export const normalizeColumnType = (type) => normalizeVersionFormatting(type);
22
+
23
+ export const normalizeSQLDefinition = (definition) => {
24
+ return normalizeVersionFormatting(definition)
25
+ .replace(/\s+/g, ' ') // Replace multiple whitespace with a single space
26
+ .replace(/'/g, "\\'") // Escape single quotes
27
+ .trim(); // Trim leading and trailing whitespace
28
+ }
package/src/options.js ADDED
@@ -0,0 +1,133 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Optional file in the dump/template directory:
6
+ *
7
+ * {
8
+ * "definer": "root@%", // DEFINER for every procedure, view and trigger
9
+ * "autoIncrement": false, // default for every table
10
+ * "databases": {
11
+ * "Alerts": {
12
+ * "AlarmRules": {
13
+ * "autoIncrement": 1000, // overrides the default for this table
14
+ * "rowKey": ["idAlarmRule"], // columns that identify a row; defaults to the primary key
15
+ * "rows": [{ "idAlarmRule": 1, "name": "Default" }]
16
+ * }
17
+ * }
18
+ * }
19
+ * }
20
+ *
21
+ * autoIncrement controls the table's AUTO_INCREMENT=N counter in CREATE TABLE
22
+ * (not the AUTO_INCREMENT column attribute):
23
+ * true keep the counter as dumped (the default)
24
+ * false remove it, so new tables start at 1
25
+ * N start new tables at N
26
+ *
27
+ * rows are inserted when no row with the same rowKey values exists; existing rows are never changed.
28
+ *
29
+ * definer replaces the DEFINER of every procedure, view and trigger in the
30
+ * template, so one whose database definer differs is reported and recreated
31
+ * with it. Written as root@%, `root`@`%` or 'root'@'%'.
32
+ */
33
+ export const OPTIONS_FILE = 'syncOptions.json';
34
+
35
+ export const loadSyncOptions = async (dir) => {
36
+ const filePath = path.join(dir, OPTIONS_FILE);
37
+ let content;
38
+ try {
39
+ content = await fs.readFile(filePath, 'utf-8');
40
+ } catch (err) {
41
+ if (err.code === 'ENOENT') return {};
42
+ throw err;
43
+ }
44
+ let options;
45
+ try {
46
+ options = JSON.parse(content);
47
+ } catch (err) {
48
+ throw new Error(`Invalid JSON in ${filePath}: ${err.message}`);
49
+ }
50
+ validateSyncOptions(options, filePath);
51
+ return options;
52
+ }
53
+
54
+ const isValidAutoIncrement = (value) => value === undefined || typeof value === 'boolean' || (Number.isInteger(value) && value > 0);
55
+
56
+ const validateSyncOptions = (options, filePath) => {
57
+ if (options.definer !== undefined && formatDefiner(options.definer) === null) {
58
+ throw new Error(`${filePath}: definer must look like root@% or \`root\`@\`%\``);
59
+ }
60
+ if (!isValidAutoIncrement(options.autoIncrement)) {
61
+ throw new Error(`${filePath}: autoIncrement must be true, false or a positive integer`);
62
+ }
63
+ for (const [database, tables] of Object.entries(options.databases || {})) {
64
+ for (const [table, tableOptions] of Object.entries(tables || {})) {
65
+ const where = `${filePath}: ${database}.${table}`;
66
+ if (!isValidAutoIncrement(tableOptions.autoIncrement)) {
67
+ throw new Error(`${where}.autoIncrement must be true, false or a positive integer`);
68
+ }
69
+ if (tableOptions.rows !== undefined && (!Array.isArray(tableOptions.rows) || tableOptions.rows.some(row => !row || typeof row !== 'object' || Array.isArray(row)))) {
70
+ throw new Error(`${where}.rows must be an array of objects`);
71
+ }
72
+ if (tableOptions.rowKey !== undefined && (!Array.isArray(tableOptions.rowKey) || tableOptions.rowKey.length === 0)) {
73
+ throw new Error(`${where}.rowKey must be a non-empty array of column names`);
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ export const getTableOptions = (syncOptions, database, table) => ({
80
+ autoIncrement: syncOptions?.autoIncrement ?? true,
81
+ ...syncOptions?.databases?.[database]?.[table]
82
+ });
83
+
84
+ export const removeAutoIncrementCounter = (createSQL) => createSQL.replace(/\s+AUTO_INCREMENT=\d+/i, '');
85
+
86
+ // Apply a table's autoIncrement option to its CREATE TABLE statement
87
+ export const applyAutoIncrementOption = (createSQL, autoIncrement = true) => {
88
+ if (!createSQL || autoIncrement === true) return createSQL;
89
+ const withoutCounter = removeAutoIncrementCounter(createSQL);
90
+ if (autoIncrement === false) return withoutCounter;
91
+ if (/\)\s*ENGINE=\w+/i.test(withoutCounter)) {
92
+ return withoutCounter.replace(/(\)\s*ENGINE=\w+)/i, `$1 AUTO_INCREMENT=${autoIncrement}`);
93
+ }
94
+ return withoutCounter.replace(/\)(\s*;?\s*)$/, `) AUTO_INCREMENT=${autoIncrement}$1`);
95
+ }
96
+
97
+ // root@%, `root`@`%`, 'root'@'%' or DEFINER=... -> `root`@`%`; null if it isn't a user@host
98
+ export const formatDefiner = (definer) => {
99
+ if (typeof definer !== 'string') return null;
100
+ const match = definer.trim().replace(/^DEFINER\s*=\s*/i, '')
101
+ .match(/^(?:`([^`]+)`|'([^']+)'|([^@`'\s]+))@(?:`([^`]+)`|'([^']+)'|([^@`'\s]+))$/);
102
+ if (!match) return null;
103
+ const user = match[1] ?? match[2] ?? match[3];
104
+ const host = match[4] ?? match[5] ?? match[6];
105
+ return `\`${user}\`@\`${host}\``;
106
+ }
107
+
108
+ // The DEFINER clause in a CREATE PROCEDURE/VIEW/TRIGGER header
109
+ const definerClauseRegex = /DEFINER\s*=\s*(?:`[^`]*`|'[^']*'|[^\s@]+)@(?:`[^`]*`|'[^']*'|[^\s]+)/i;
110
+
111
+ // Replace the DEFINER in one definition; unchanged when definer isn't set or the definition has none
112
+ export const applyDefiner = (definition, definer) => {
113
+ const formatted = definer ? formatDefiner(definer) : null;
114
+ if (!formatted || typeof definition !== 'string') return definition;
115
+ return definition.replace(definerClauseRegex, `DEFINER=${formatted}`);
116
+ }
117
+
118
+ // A copy of a database structure with the definer option applied to its procedures, views and triggers
119
+ export const applyDefinerOption = (structure, definer) => {
120
+ if (!definer || !structure) return structure;
121
+ const tables = Object.fromEntries(Object.entries(structure.tables || {}).map(([name, table]) => [name, {
122
+ ...table,
123
+ triggers: (table.triggers || []).map(trigger => ({ ...trigger, Definition: applyDefiner(trigger.Definition, definer) }))
124
+ }]));
125
+ const result = { ...structure, tables };
126
+ if (structure.procedures) {
127
+ result.procedures = structure.procedures.map(procedure => ({ ...procedure, Definition: applyDefiner(procedure.Definition, definer) }));
128
+ }
129
+ if (structure.views) {
130
+ result.views = Object.fromEntries(Object.entries(structure.views).map(([name, view]) => [name, applyDefiner(view, definer)]));
131
+ }
132
+ return result;
133
+ }