mysqlsyncr 1.0.4 → 1.0.7

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 (59) hide show
  1. package/package.json +7 -2
  2. package/src/apply.js +16 -11
  3. package/src/cli.js +3 -2
  4. package/src/differences.js +45 -22
  5. package/src/dump.js +37 -1
  6. package/src/index.js +58 -40
  7. package/src/parsers.js +2 -1
  8. package/src/socketServer.js +96 -0
  9. package/ui/.editorconfig +7 -0
  10. package/ui/.prettierrc.json +6 -0
  11. package/ui/README.md +40 -0
  12. package/ui/dist/spa/assets/KFOkCnqEu92Fr1MmgVxIIzQ-Y2mpUUkw.woff +0 -0
  13. package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmEU9fBBc--ViM7Ag4Q.woff +0 -0
  14. package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmSU5fBBc--CrFzpgl6.woff +0 -0
  15. package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmWUlfBBc--CW3_XjG_.woff +0 -0
  16. package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmYUtfBBc--Dr8fnyGc.woff +0 -0
  17. package/ui/dist/spa/assets/KFOmCnqEu92Fr1Mu4mxM-DAYh6T4l.woff +0 -0
  18. package/ui/dist/spa/assets/MainLayout-C27l9N4F.js +1 -0
  19. package/ui/dist/spa/assets/axios-Y2H2Igoj.js +1 -0
  20. package/ui/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
  21. package/ui/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ-D-x-0Q06.woff2 +0 -0
  22. package/ui/dist/spa/assets/index-DKK0oGss.js +30 -0
  23. package/ui/dist/spa/assets/index-DME6fUxJ.css +5 -0
  24. package/ui/dist/spa/favicon.ico +0 -0
  25. package/ui/dist/spa/icons/favicon-128x128.png +0 -0
  26. package/ui/dist/spa/icons/favicon-16x16.png +0 -0
  27. package/ui/dist/spa/icons/favicon-32x32.png +0 -0
  28. package/ui/dist/spa/icons/favicon-96x96.png +0 -0
  29. package/ui/dist/spa/index.html +3 -0
  30. package/ui/eslint.config.js +73 -0
  31. package/ui/index.html +21 -0
  32. package/ui/jsconfig.json +3 -0
  33. package/ui/package.json +42 -0
  34. package/ui/postcss.config.js +29 -0
  35. package/ui/public/favicon.ico +0 -0
  36. package/ui/public/icons/favicon-128x128.png +0 -0
  37. package/ui/public/icons/favicon-16x16.png +0 -0
  38. package/ui/public/icons/favicon-32x32.png +0 -0
  39. package/ui/public/icons/favicon-96x96.png +0 -0
  40. package/ui/quasar.config.js +215 -0
  41. package/ui/src/App.vue +7 -0
  42. package/ui/src/assets/quasar-logo-vertical.svg +15 -0
  43. package/ui/src/boot/.gitkeep +0 -0
  44. package/ui/src/boot/axios.js +24 -0
  45. package/ui/src/components/EssentialLink.vue +44 -0
  46. package/ui/src/css/app.scss +1 -0
  47. package/ui/src/css/quasar.variables.scss +25 -0
  48. package/ui/src/layouts/MainLayout.vue +13 -0
  49. package/ui/src/pages/ApplyChanges.vue +46 -0
  50. package/ui/src/pages/CompareDatabase.vue +57 -0
  51. package/ui/src/pages/DatabaseManager.vue +42 -0
  52. package/ui/src/pages/DumpDatabase.vue +41 -0
  53. package/ui/src/pages/ErrorNotFound.vue +27 -0
  54. package/ui/src/pages/MainComponent copy.vue +322 -0
  55. package/ui/src/pages/MainComponent.vue +894 -0
  56. package/ui/src/router/index.js +30 -0
  57. package/ui/src/router/routes.js +20 -0
  58. package/ui/src/stores/example-store.js +21 -0
  59. package/ui/src/stores/index.js +20 -0
@@ -0,0 +1,894 @@
1
+ <template>
2
+ <q-page>
3
+ <div class="q-mb-md">
4
+ <q-btn @click="openSettings" label="Settings" color="primary" />
5
+ <q-btn
6
+ @click="
7
+ Object.keys(existingDatabases).length === 0
8
+ ? createNewDump()
9
+ : createNewDump(true)
10
+ "
11
+ :label="
12
+ Object.keys(existingDatabases).length === 0 ? 'Get New Dump' : 'Refresh Dump'
13
+ "
14
+ color="secondary"
15
+ />
16
+ </div>
17
+
18
+ <q-card v-if="!isDbSetup" class="q-mb-md">
19
+ <q-card-section>
20
+ <div class="text-h6">Database Not Set Up</div>
21
+ <div>Please configure your database settings.</div>
22
+ <q-btn @click="openSettings" label="Open Settings" color="primary" />
23
+ </q-card-section>
24
+ </q-card>
25
+
26
+ <q-card v-if="Object.keys(existingDatabases).length > 0" class="q-mb-md">
27
+ <q-card-section>
28
+ <div class="text-h6">Database Structure</div>
29
+ <q-tree
30
+ :nodes="databaseTree"
31
+ node-key="id"
32
+ v-model:expanded="expandedNodes"
33
+ @lazy-load="onLazyLoad"
34
+ >
35
+ <template v-slot:default-header="prop">
36
+ <div class="row items-center full-width">
37
+ <div
38
+ class="row items-center flex-grow-1"
39
+ @click.stop="
40
+ !['category', 'database', 'subcategory'].includes(
41
+ prop.node.type,
42
+ ) &&
43
+ prop.node.diffCount &&
44
+ showIssueDetails(prop.node.issueData, prop.node)
45
+ "
46
+ >
47
+ <q-icon
48
+ v-if="prop.node.icon"
49
+ :name="prop.node.icon"
50
+ :color="prop.node.iconColor"
51
+ size="sm"
52
+ class="q-mr-sm"
53
+ />
54
+ <div>{{ prop.node.label }}</div>
55
+ <q-badge
56
+ v-if="prop.node.diffCount"
57
+ color="negative"
58
+ class="q-ml-sm"
59
+ >
60
+ {{ prop.node.diffCount }}
61
+ </q-badge>
62
+ </div>
63
+
64
+ <!-- Fix button with better spacing -->
65
+ <q-btn
66
+ v-if="
67
+ ['issue-group', 'issues', 'category', 'database'].includes(
68
+ prop.node.type,
69
+ ) && prop.node.diffCount
70
+ "
71
+ flat
72
+ dense
73
+ color="primary"
74
+ :loading="isNodeFixing[prop.node.id]"
75
+ @click.stop="fixNodeIssues(prop.node)"
76
+ icon="build"
77
+ size="sm"
78
+ class="q-ml-md"
79
+ >
80
+ <q-tooltip>Fix All Issues</q-tooltip>
81
+ </q-btn>
82
+ </div>
83
+ </template>
84
+ </q-tree>
85
+ </q-card-section>
86
+ </q-card>
87
+
88
+ <q-dialog v-model="settingsDialog" persistent>
89
+ <q-card>
90
+ <q-card-section>
91
+ <div class="text-h6">Database Settings</div>
92
+ <q-input v-model="host" label="Database Host" required />
93
+ <q-input v-model="port" label="Database Port" type="number" required />
94
+ <q-input v-model="user" label="Database User" required />
95
+ <q-input
96
+ v-model="password"
97
+ label="Database Password"
98
+ type="password"
99
+ required
100
+ autocomplete="off"
101
+ />
102
+ <q-input v-model="database" label="Database Name" required />
103
+ <q-input v-model="dbFolder" label="Database Folder" required />
104
+ </q-card-section>
105
+ <q-card-actions>
106
+ <q-btn @click="saveSettings" label="Save" color="primary" />
107
+ <q-btn @click="closeSettings" label="Cancel" color="secondary" />
108
+ </q-card-actions>
109
+ </q-card>
110
+ </q-dialog>
111
+
112
+ <!-- Issue Details Dialog -->
113
+ <q-dialog v-model="issueDialog">
114
+ <q-card style="min-width: 800px; width: 90vw; max-width: 1200px">
115
+ <q-card-section>
116
+ <div class="text-h6">Issue Details</div>
117
+ </q-card-section>
118
+
119
+ <!-- Single Issue View -->
120
+ <template v-if="selectedIssues.length === 1">
121
+ <q-card-section class="q-pt-none">
122
+ <div class="text-subtitle2 q-mb-sm">
123
+ Issue Type:
124
+ <q-badge color="negative">{{
125
+ selectedIssues[0].type?.replace(/_/g, ' ')
126
+ }}</q-badge>
127
+ </div>
128
+ <div class="text-subtitle2 q-mb-sm">Name: {{ selectedIssues[0].Name }}</div>
129
+
130
+ <div v-if="selectedIssues[0].tableName" class="q-mb-sm">
131
+ <strong>Table:</strong> {{ selectedIssues[0].tableName }}
132
+ </div>
133
+
134
+ <div v-if="selectedIssues[0].createSQL" class="q-mt-md">
135
+ <div class="text-subtitle2 q-mb-sm">SQL Definition:</div>
136
+ <q-card class="bg-grey-2">
137
+ <q-card-section class="q-pa-md">
138
+ <pre class="sql-code">{{ selectedIssues[0].createSQL }}</pre>
139
+ </q-card-section>
140
+ </q-card>
141
+ </div>
142
+
143
+ <div v-if="selectedIssues[0].Definition" class="q-mt-md">
144
+ <div class="text-subtitle2 q-mb-sm">Definition:</div>
145
+ <q-card class="bg-grey-2">
146
+ <q-card-section class="q-pa-md">
147
+ <pre class="sql-code">{{ selectedIssues[0].Definition }}</pre>
148
+ </q-card-section>
149
+ </q-card>
150
+ </div>
151
+
152
+ <div v-if="selectedIssues[0].currentField" class="q-mt-md">
153
+ <div class="text-subtitle2 q-mb-sm">Field Comparison:</div>
154
+ <q-markup-table flat bordered>
155
+ <thead>
156
+ <tr>
157
+ <th class="text-left">Property</th>
158
+ <th class="text-left">Current</th>
159
+ <th class="text-left">Expected</th>
160
+ </tr>
161
+ </thead>
162
+ <tbody>
163
+ <tr v-for="(value, key) in selectedIssues[0].currentField" :key="key">
164
+ <td>{{ key }}</td>
165
+ <td>{{ value }}</td>
166
+ <td :class="{ 'text-negative': value !== selectedIssues[0].field?.[key] }">
167
+ {{ selectedIssues[0].field?.[key] }}
168
+ </td>
169
+ </tr>
170
+ </tbody>
171
+ </q-markup-table>
172
+ </div>
173
+
174
+ <q-btn
175
+ @click="fixNodeIssues(this.selectedNode)"
176
+ :loading="isNodeFixing['single-issue']"
177
+ color="primary"
178
+ class="q-mt-md"
179
+ label="Fix Issue"
180
+ />
181
+ </q-card-section>
182
+ </template>
183
+
184
+ <!-- Multiple Issues View -->
185
+ <template v-else>
186
+ <q-card-section class="q-pt-none">
187
+ <q-table
188
+ :rows="selectedIssues"
189
+ :columns="[
190
+ {
191
+ name: 'type',
192
+ label: 'Type',
193
+ field: (row) => row.type?.replace(/_/g, ' '),
194
+ },
195
+ { name: 'tableName', label: 'Table', field: 'tableName' },
196
+ { name: 'actions', label: 'Actions', field: 'actions' },
197
+ ]"
198
+ row-key="id"
199
+ dense
200
+ >
201
+ <template v-slot:body="props">
202
+ <q-tr :props="props">
203
+ <q-td key="type">{{ props.row.type?.replace(/_/g, ' ') }}</q-td>
204
+ <q-td key="tableName">{{ props.row.tableName }}</q-td>
205
+ <q-td key="actions">
206
+ <q-btn
207
+ flat
208
+ dense
209
+ color="primary"
210
+ icon="visibility"
211
+ @click="showSingleIssue(props.row)"
212
+ >
213
+ <q-tooltip>View Details</q-tooltip>
214
+ </q-btn>
215
+ <q-btn
216
+ flat
217
+ dense
218
+ color="positive"
219
+ icon="build"
220
+ :loading="isNodeFixing[`issue-${props.rowIndex}`]"
221
+ @click="
222
+ fixNodeIssues({
223
+ type: 'issue',
224
+ id: this.selectedNode.id,
225
+ issueData: props.row,
226
+ })
227
+ "
228
+ >
229
+ <q-tooltip>Fix Issue</q-tooltip>
230
+ </q-btn>
231
+ </q-td>
232
+ </q-tr>
233
+ </template>
234
+ </q-table>
235
+
236
+ <q-btn
237
+ @click="fixNodeIssues({ type: 'issues', children: selectedIssues })"
238
+ :loading="isNodeFixing['all-issues']"
239
+ color="primary"
240
+ class="q-mt-md"
241
+ label="Fix All Issues"
242
+ />
243
+ </q-card-section>
244
+ </template>
245
+
246
+ <q-card-actions align="right">
247
+ <q-btn flat label="Close" color="primary" v-close-popup />
248
+ </q-card-actions>
249
+ </q-card>
250
+ </q-dialog>
251
+ </q-page>
252
+ </template>
253
+
254
+ <script>
255
+ import axios from 'axios'
256
+
257
+ export default {
258
+ data() {
259
+ return {
260
+ isDbSetup: false,
261
+ differences: {},
262
+ columns: [
263
+ { name: 'table', label: 'Table', align: 'left', field: 'table' },
264
+ { name: 'difference', label: 'Difference', align: 'left', field: 'difference' },
265
+ ],
266
+ settingsDialog: false,
267
+ host: '127.0.0.1',
268
+ port: 3306,
269
+ user: 'root',
270
+ password: '',
271
+ database: '',
272
+ dbFolder: 'db-dump',
273
+ existingDatabases: [],
274
+ databaseColumns: [
275
+ { name: 'name', label: 'Database Name', align: 'left', field: 'name' },
276
+ { name: 'size', label: 'Size', align: 'left', field: 'size' },
277
+ ],
278
+ databaseTree: [],
279
+ expandedNodes: [],
280
+ issueDialog: false,
281
+ selectedIssues: [],
282
+ selectedNode: null,
283
+ isNodeFixing: {}, // Tracks loading state for each node
284
+ }
285
+ },
286
+ methods: {
287
+ openSettings() {
288
+ this.settingsDialog = true
289
+ },
290
+ closeSettings() {
291
+ this.settingsDialog = false
292
+ },
293
+ saveSettings() {
294
+ const dbConfig = {
295
+ host: this.host,
296
+ port: this.port,
297
+ user: this.user,
298
+ password: this.password,
299
+ database: this.database,
300
+ output: this.dbFolder,
301
+ }
302
+ localStorage.setItem('dbConfig', JSON.stringify(dbConfig))
303
+ this.isDbSetup = true
304
+ this.closeSettings()
305
+ this.refreshDifferences()
306
+ },
307
+ async refreshDifferences() {
308
+ if (!this.isDbSetup) return
309
+ try {
310
+ const response = await axios.post('http://localhost:3000/api/compare', {
311
+ config: {
312
+ host: this.host,
313
+ port: this.port,
314
+ user: this.user,
315
+ password: this.password,
316
+ output: this.dbFolder,
317
+ database: this.database,
318
+ },
319
+ })
320
+ this.differences = response.data
321
+ } catch (error) {
322
+ console.error('Error fetching differences:', error)
323
+ }
324
+ },
325
+ async getDatabasesFromExistingDumps() {
326
+ try {
327
+ const response = await axios.post(
328
+ `http://localhost:3000/api/getDatabasesFromExistingDumps`,
329
+ {
330
+ config: {
331
+ host: this.host,
332
+ port: this.port,
333
+ user: this.user,
334
+ password: this.password,
335
+ output: this.dbFolder,
336
+ },
337
+ },
338
+ )
339
+ if (response.data && typeof response.data === 'object') {
340
+ this.existingDatabases = response.data
341
+ this.buildDatabaseTree()
342
+ }
343
+ } catch (error) {
344
+ console.error('Error fetching databases from existing dumps:', error)
345
+ }
346
+ },
347
+ async buildDatabaseTree() {
348
+ if (Object.keys(this.differences).length === 0) {
349
+ await this.fetchAllDifferences()
350
+ }
351
+ this.databaseTree = Object.entries(this.existingDatabases).map(([dbName, db]) => {
352
+ console.log('Building database tree for:', dbName, this.differences)
353
+ const dbDiffs = this.differences[dbName] || []
354
+ const totalIssues = dbDiffs.length
355
+ console.log(`Database ${dbName} differences ${totalIssues}:`, dbDiffs)
356
+ return {
357
+ id: `db-${dbName}`,
358
+ label: dbName,
359
+ type: 'database',
360
+ lazy: true,
361
+ icon: totalIssues > 0 ? 'warning' : 'check_circle',
362
+ iconColor: totalIssues > 0 ? 'negative' : 'positive',
363
+ diffCount: totalIssues || null,
364
+ dbData: db,
365
+ dbName: dbName,
366
+ }
367
+ })
368
+
369
+ console.log('Database tree:', this.databaseTree)
370
+ this.expandedNodes = []
371
+ },
372
+ onLazyLoad({ node, done, fail }) {
373
+ console.log('Lazy loading node:', node)
374
+ try {
375
+ if (node.type === 'database') {
376
+ console.log('Loading database children for:', node.dbName)
377
+ const db = node.dbData
378
+ const dbDiffs = this.differences[node.dbName] || []
379
+
380
+ // Count issues by main category
381
+ const tableIssues = dbDiffs.filter(
382
+ (d) =>
383
+ d.type.includes('table') ||
384
+ d.type.includes('field') ||
385
+ d.type.includes('index') ||
386
+ d.type.includes('trigger'),
387
+ )
388
+ console.log('Table issues:', tableIssues)
389
+
390
+ const procedureIssues = dbDiffs.filter((d) => d.type.includes('procedure'))
391
+ const viewIssues = dbDiffs.filter((d) => d.type.includes('view'))
392
+
393
+ const children = [
394
+ {
395
+ id: `${node.dbName}-tables`,
396
+ label: 'Tables',
397
+ type: 'category',
398
+ lazy: true,
399
+ icon: tableIssues.length > 0 ? 'warning' : 'check_circle',
400
+ iconColor: tableIssues.length > 0 ? 'negative' : 'positive',
401
+ diffCount: tableIssues.length || null,
402
+ },
403
+ db.procedures && {
404
+ id: `${node.dbName}-procedures`,
405
+ label: 'Procedures',
406
+ type: 'category',
407
+ lazy: true,
408
+ icon: procedureIssues.length > 0 ? 'warning' : 'check_circle',
409
+ iconColor: procedureIssues.length > 0 ? 'negative' : 'positive',
410
+ diffCount: procedureIssues.length || null,
411
+ },
412
+ db.views && {
413
+ id: `${node.dbName}-views`,
414
+ label: 'Views',
415
+ type: 'category',
416
+ lazy: true,
417
+ icon: viewIssues.length > 0 ? 'warning' : 'check_circle',
418
+ iconColor: viewIssues.length > 0 ? 'negative' : 'positive',
419
+ diffCount: viewIssues.length || null,
420
+ },
421
+ ].filter(Boolean)
422
+
423
+ console.log('Database children:', children)
424
+ done(children)
425
+ } else if (node.type === 'category') {
426
+ console.log('Loading category children for:', node.id)
427
+ const dbName = node.id.split('-')[0]
428
+ const dbDiffs = this.differences[dbName] || []
429
+
430
+ if (node.id.includes('-tables')) {
431
+ const tableIssues = dbDiffs.filter(
432
+ (d) =>
433
+ d.type.includes('table') ||
434
+ d.type.includes('field') ||
435
+ d.type.includes('index') ||
436
+ d.type.includes('trigger'),
437
+ )
438
+ console.log('Table category issues:', tableIssues)
439
+
440
+ const children = []
441
+
442
+ // // Add issues section if there are any
443
+ // if (tableIssues.length > 0) {
444
+ // children.push({
445
+ // id: `${dbName}-tables-issues`,
446
+ // label: 'Issues',
447
+ // type: 'issues',
448
+ // icon: 'warning',
449
+ // iconColor: 'negative',
450
+ // diffCount: tableIssues.length,
451
+ // children: this.createTableIssueGroups(dbName, tableIssues),
452
+ // })
453
+ // }
454
+
455
+ // Add actual tables
456
+ const tables = this.existingDatabases[dbName].tables
457
+ console.log('Tables:', tables)
458
+
459
+ children.push(
460
+ ...Object.entries(tables).map(([tableName, table]) => {
461
+ const specificTableIssues = tableIssues.filter(
462
+ (d) => d.tableName === tableName,
463
+ )
464
+ console.log(`Issues for table ${tableName}:`, specificTableIssues)
465
+ const hasIssues = specificTableIssues.length > 0
466
+
467
+ return {
468
+ id: `${dbName}-table-${tableName}`,
469
+ label: tableName,
470
+ type: 'table',
471
+ lazy: true,
472
+ icon: hasIssues ? 'warning' : 'check_circle',
473
+ iconColor: hasIssues ? 'negative' : 'positive',
474
+ diffCount: hasIssues ? specificTableIssues.length : null,
475
+ tableData: table,
476
+ issueData: specificTableIssues,
477
+ }
478
+ }),
479
+ )
480
+
481
+ console.log('Category children:', children)
482
+ done(children)
483
+ } else if (node.id.includes('-procedures')) {
484
+ const children = []
485
+ const procedures = this.existingDatabases[dbName].procedures
486
+ children.push(
487
+ ...Object.entries(procedures).map(([index, procedure]) => {
488
+ const procedureName = procedure.Name
489
+ const specificProcedureIssues = dbDiffs.filter(
490
+ (d) => d.type.includes('procedure') && d.Name === procedureName,
491
+ )
492
+ console.log(
493
+ `Issues for procedure ${procedureName}:`,
494
+ specificProcedureIssues,
495
+ )
496
+ const hasIssues = specificProcedureIssues.length > 0
497
+ return {
498
+ id: `${dbName}-procedure-${procedureName}`,
499
+ label: procedureName,
500
+ index: index,
501
+ type: 'procedure',
502
+ lazy: false,
503
+ icon: hasIssues ? 'warning' : 'check_circle',
504
+ iconColor: hasIssues ? 'negative' : 'positive',
505
+ diffCount: hasIssues ? specificProcedureIssues.length : null,
506
+ procedureData: procedure,
507
+ issueData: specificProcedureIssues,
508
+ }
509
+ }),
510
+ )
511
+ done(children)
512
+ } else if (node.id.includes('-views')) {
513
+ const children = []
514
+ const views = this.existingDatabases[dbName].views
515
+ console.log('Views:', views)
516
+ children.push(
517
+ ...Object.entries(views).map(([name, view]) => ({
518
+ id: `${dbName}-view-${name}`,
519
+ label: name,
520
+ type: 'view',
521
+ lazy: false,
522
+ view: view,
523
+ })),
524
+ )
525
+ done(children)
526
+ }
527
+ } else if (node.type === 'table') {
528
+ console.log('Loading table children for:', node.id)
529
+ const table = node.tableData
530
+ console.log('Table data:', table)
531
+
532
+ if (!table) {
533
+ console.error('No table data found for node:', node)
534
+ done([])
535
+ return
536
+ }
537
+
538
+ const children = []
539
+
540
+ if (table.columns?.length > 0) {
541
+ children.push({
542
+ id: `${node.id}-columns`,
543
+ label: `Columns (${table.columns.length})`,
544
+ type: 'subcategory',
545
+ lazy: false,
546
+ children: table.columns.map(column => {
547
+ let details = [];
548
+ if (column.AutoIncrement) details.push('AUTO_INCREMENT');
549
+ if (column.NotNull) details.push('NOT NULL');
550
+ if (column.Default !== undefined) details.push(`DEFAULT ${column.Default}`);
551
+
552
+ return {
553
+ id: `${node.id}-column-${column.Field}`,
554
+ label: `${column.Field} (${column.Type})${details.length ? ' ' + details.join(', ') : ''}`,
555
+ type: 'column',
556
+ expandable: false,
557
+ columnData: column
558
+ };
559
+ })
560
+ })
561
+ }
562
+
563
+ if (table.indexes?.length > 0) {
564
+ children.push({
565
+ id: `${node.id}-indexes`,
566
+ label: `Indexes (${table.indexes.length})`,
567
+ type: 'subcategory',
568
+ lazy: false,
569
+ children: table.indexes.map(index => ({
570
+ id: `${node.id}-index-${index.Name}`,
571
+ label: `${index.Name}${index.Primary ? ' (PRIMARY)' : ''} [${index.ColumnName.join(', ')}]`,
572
+ type: 'index',
573
+ expandable: false,
574
+ indexData: index
575
+ }))
576
+ })
577
+ }
578
+
579
+ console.log('Table children:', children)
580
+ done(children)
581
+ }
582
+ } catch (error) {
583
+ console.error('Error in lazy loading:', error)
584
+ fail()
585
+ }
586
+ },
587
+ createTableIssueGroups(dbName, issues) {
588
+ const groupedIssues = issues.reduce((acc, issue) => {
589
+ const type = issue.type
590
+ acc[type] = acc[type] || { count: 0, items: [] }
591
+ acc[type].count++
592
+ acc[type].items.push(issue)
593
+ return acc
594
+ }, {})
595
+
596
+ return Object.entries(groupedIssues).map(([type, group]) => ({
597
+ id: `${dbName}-tables-${type}`,
598
+ label: type
599
+ .split('_')
600
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
601
+ .join(' '),
602
+ type: 'issue-group',
603
+ icon: 'warning',
604
+ iconColor: 'negative',
605
+ diffCount: group.count,
606
+ children: group.items.map((issue) => ({
607
+ id: `${dbName}-tables-${type}-${issue.tableName || issue.Name}`,
608
+ label: issue.tableName || issue.Name,
609
+ type: 'issue',
610
+ icon: 'warning',
611
+ iconColor: 'negative',
612
+ expandable: false,
613
+ issueData: issue,
614
+ })),
615
+ }))
616
+ },
617
+ async fetchAllDifferences() {
618
+ const response = await axios.post('http://localhost:3000/api/compare', {
619
+ config: {
620
+ host: this.host,
621
+ port: this.port,
622
+ user: this.user,
623
+ password: this.password,
624
+ output: this.dbFolder,
625
+ },
626
+ })
627
+ this.differences = response.data
628
+ console.log('Differences in fetchAllDifferences:', this.differences)
629
+ },
630
+ async fetchDifferencesForSelectedDb() {
631
+ if (Object.keys(this.existingDatabases).length > 0) {
632
+ const selectedDbName = Object.keys(this.existingDatabases)[0]
633
+ const response = await axios.post('http://localhost:3000/api/compare', {
634
+ config: {
635
+ host: this.host,
636
+ port: this.port,
637
+ user: this.user,
638
+ password: this.password,
639
+ output: this.dbFolder,
640
+ database: selectedDbName,
641
+ },
642
+ })
643
+ this.differences = response.data
644
+ }
645
+ },
646
+ async createNewDump(overwrite = false) {
647
+ console.log('Creating a new dump...')
648
+ if(overwrite){
649
+ this.$q.dialog({
650
+ title: 'Overwrite',
651
+ message: 'Are you sure you want to overwrite the existing dump?',
652
+ ok: 'Overwrite',
653
+ cancel: 'Cancel',
654
+ }).onOk(async () => {
655
+ console.log('Overwriting existing dump')
656
+ await this.createNewDump(false)
657
+ })
658
+ return
659
+ }
660
+
661
+ let response = await axios
662
+ .post('http://localhost:3000/api/dump', {
663
+ config: {
664
+ host: this.host,
665
+ port: this.port,
666
+ user: this.user,
667
+ password: this.password,
668
+ output: this.dbFolder,
669
+ // database: this.database,
670
+ },
671
+ })
672
+ .catch((error) => {
673
+ if (error.response.data.message) {
674
+ this.$q.dialog({
675
+ title: 'Error',
676
+ message: error.response.data.message,
677
+ })
678
+ }
679
+ console.error('Error creating new dump:', error)
680
+ return
681
+ })
682
+
683
+ if (!response) return
684
+ if (response.data.message) {
685
+ this.$q.dialog({
686
+ title: 'Error',
687
+ message: response.data.message,
688
+ })
689
+ }
690
+ this.differences = await this.fetchAllDifferences()
691
+ console.log('Differences in createNewDump:', this.differences)
692
+ if (
693
+ response.data &&
694
+ typeof response.data === 'object' &&
695
+ Object.keys(response.data).length > 0
696
+ ) {
697
+ this.existingDatabases = response.data
698
+ this.buildDatabaseTree()
699
+ } else {
700
+ this.existingDatabases = {}
701
+ this.$q.notify({
702
+ message: 'Error creating new dump',
703
+ color: 'red',
704
+ })
705
+ }
706
+ },
707
+ showIssueDetails(issue, node) {
708
+ console.log('Showing issue details:', issue, "from node:", node)
709
+ this.selectedIssues = Array.isArray(issue) ? issue : [issue]
710
+ this.selectedNode = node;
711
+ this.issueDialog = true
712
+ },
713
+ showSingleIssue(issue, dbName) {
714
+ this.selectedIssues = [issue]
715
+ this.selectedIssueDbName = dbName;
716
+ this.issueDialog = true
717
+ },
718
+ async fixNodeIssues(node) {
719
+ if (!node) return
720
+ try {
721
+ let issues = []
722
+ let dbName = null;
723
+ // Set loading state for this node
724
+ this.isNodeFixing[node.id] = true
725
+ this.isNodeFixing = { ...this.isNodeFixing } // Trigger reactivity
726
+
727
+ if(node.issueData){
728
+ issues = Array.isArray(node.issueData) ? node.issueData : [node.issueData];
729
+ }else{
730
+ issues = await this.collectIssuesForNode(node)
731
+ }
732
+ dbName = this.getDbNameFromNode(node)
733
+
734
+ if (!issues.length){
735
+ console.error('No issues found to fix', node);
736
+ return;
737
+ }
738
+
739
+
740
+ const response = await axios.post('http://localhost:3000/api/apply', {
741
+ config: {
742
+ host: this.host,
743
+ port: this.port,
744
+ user: this.user,
745
+ password: this.password,
746
+ output: this.dbFolder,
747
+ },
748
+ database: dbName,
749
+ diffs: issues,
750
+ })
751
+
752
+ if (response.data.message) {
753
+ this.$q.notify({
754
+ type: 'positive',
755
+ message: `Fixed ${issues.length} issue(s) successfully`,
756
+ position: 'top',
757
+ })
758
+ // Refresh the differences to update the UI
759
+
760
+ await this.fetchAllDifferences()
761
+ await this.buildDatabaseTree()
762
+ }
763
+ } catch (error) {
764
+ console.error('Error fixing issues:', error)
765
+ this.$q.notify({
766
+ type: 'negative',
767
+ message: error.response?.data?.message || 'Error fixing issues',
768
+ position: 'top',
769
+ })
770
+ } finally {
771
+ this.isNodeFixing[node.id] = false
772
+ this.isNodeFixing = { ...this.isNodeFixing } // Trigger reactivity
773
+ }
774
+ },
775
+ getDbNameFromNode(node) {
776
+ // Extract database name from node ID (assumes format: "dbName-something-etc")
777
+ if(node.id.startsWith('db-')){
778
+ return node.id.split('-')[1]
779
+ }
780
+ if (!node.id) return null, console.error('Node ID is required to get db name', node);
781
+ console.log('Node ID:', node.id)
782
+ return node.id.split('-')[0]
783
+ },
784
+ async collectIssuesForNode(node) {
785
+ const issues = []
786
+ console.log('Collecting issues for node:', node)
787
+ switch (node.type) {
788
+ case 'database':
789
+ //collect issues from the differences for the selected db
790
+ console.log('Collecting issues for database:', this.getDbNameFromNode(node))
791
+ issues.push(...this.differences[this.getDbNameFromNode(node)])
792
+ break
793
+ case 'issue':
794
+ if (node.issueData) issues.push(node.issueData)
795
+ break
796
+
797
+ case 'issue-group':
798
+ // Collect all issues in this group
799
+ if (node.children) {
800
+ issues.push(
801
+ ...node.children
802
+ .filter((child) => child.type === 'issue')
803
+ .map((child) => child.issueData)
804
+ .filter(Boolean),
805
+ )
806
+ }
807
+ break
808
+
809
+ case 'issues':
810
+ case 'category':
811
+ // For categories or issues sections, collect all nested issues
812
+ if (node.children) {
813
+ for (const child of node.children) {
814
+ if (child.type === 'issue-group' && child.children) {
815
+ issues.push(
816
+ ...child.children
817
+ .filter((grandChild) => grandChild.type === 'issue')
818
+ .map((grandChild) => grandChild.issueData)
819
+ .filter(Boolean),
820
+ )
821
+ }else if(child.issueData){
822
+ if(Array.isArray(child.issueData)){
823
+ issues.push(...child.issueData)
824
+ }else if (child.issueData.type){
825
+ issues.push(child.issueData)
826
+ }else{
827
+ console.error('Unknown issue data:', child.issueData)
828
+ }
829
+ }
830
+ }
831
+ }else{
832
+ //need to collect issues from the differences for the selected db and type
833
+ const selectedDbName = this.getDbNameFromNode(node)
834
+ console.log('Selected DB Name:', selectedDbName)
835
+ let type = node.id.split('-')[1]
836
+ const dbDifferences = this.differences[selectedDbName]
837
+ console.log('DB Differences:', dbDifferences)
838
+ if(type === 'tables'){
839
+ issues.push(...dbDifferences.filter(diff => diff.type.includes('table')))
840
+ }else if(type === 'views'){
841
+ issues.push(...dbDifferences.filter(diff => diff.type.includes('view')))
842
+ }else if(type === 'procedures'){
843
+ issues.push(...dbDifferences.filter(diff => diff.type.includes('procedure')))
844
+ }else{
845
+ console.error('Unknown type:', type)
846
+ }
847
+
848
+ }
849
+ break
850
+ }
851
+
852
+ return issues
853
+ },
854
+ async applyFix() {
855
+ if (!this.selectedIssues.length) return
856
+
857
+ try {
858
+ this.isApplying = true
859
+ await this.fixNodeIssues(this.selectedNode)
860
+ this.issueDialog = false
861
+ } finally {
862
+ this.isApplying = false
863
+ }
864
+ },
865
+ },
866
+ mounted() {
867
+ const dbConfig = localStorage.getItem('dbConfig')
868
+ if (dbConfig) {
869
+ const config = JSON.parse(dbConfig)
870
+ this.host = config.host
871
+ this.port = config.port
872
+ this.user = config.user
873
+ this.password = config.password
874
+ this.database = config.database
875
+ this.dbFolder = config.output
876
+ this.isDbSetup = true
877
+ this.getDatabasesFromExistingDumps()
878
+ this.fetchAllDifferences()
879
+ }
880
+ },
881
+ }
882
+ </script>
883
+
884
+ <style>
885
+ .sql-code {
886
+ white-space: pre-wrap;
887
+ word-wrap: break-word;
888
+ font-family: monospace;
889
+ margin: 0;
890
+ font-size: 14px;
891
+ line-height: 1.5;
892
+ overflow-x: auto;
893
+ }
894
+ </style>