mwalajs 1.1.17 → 1.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/bin/mwala.mjs +95 -33
- package/package.json +1 -1
- package/runMigrations.mjs +143 -0
package/LICENSE
CHANGED
package/bin/mwala.mjs
CHANGED
|
@@ -39,7 +39,24 @@ const __dirname = path.dirname(__filename);
|
|
|
39
39
|
// ────────────────────────────────────────────────
|
|
40
40
|
|
|
41
41
|
let imports = {};
|
|
42
|
-
let setupMwalajs, createProject, dropAllTables, getDbConnection;
|
|
42
|
+
// let setupMwalajs, createProject, dropAllTables, getDbConnection;
|
|
43
|
+
let {
|
|
44
|
+
setupMwalajs,
|
|
45
|
+
createProject,
|
|
46
|
+
dropAllTables,
|
|
47
|
+
getDbConnection,
|
|
48
|
+
listTables,
|
|
49
|
+
createTable,
|
|
50
|
+
dropTable,
|
|
51
|
+
migrateAll,
|
|
52
|
+
rollbackLastMigration,
|
|
53
|
+
showDatabaseSize,
|
|
54
|
+
listIndexes,
|
|
55
|
+
analyzeTable,
|
|
56
|
+
vacuumDatabase,
|
|
57
|
+
showConnections,
|
|
58
|
+
killConnections,
|
|
59
|
+
} = imports;
|
|
43
60
|
|
|
44
61
|
try {
|
|
45
62
|
const [
|
|
@@ -48,6 +65,7 @@ try {
|
|
|
48
65
|
setup,
|
|
49
66
|
proj,
|
|
50
67
|
dbUtilsRaw,
|
|
68
|
+
maintenance,
|
|
51
69
|
] = await Promise.all([
|
|
52
70
|
import(pathToFileURL(path.join(__dirname, '../config/createdatabase.mjs')).href),
|
|
53
71
|
import(pathToFileURL(path.join(__dirname, '../runMigrations.mjs')).href),
|
|
@@ -64,16 +82,31 @@ try {
|
|
|
64
82
|
...normalize(setup),
|
|
65
83
|
...normalize(proj),
|
|
66
84
|
...normalize(dbUtilsRaw),
|
|
85
|
+
...normalize(maintenance), // ← important
|
|
67
86
|
};
|
|
68
87
|
|
|
69
88
|
// 🔥 IMPORTANT FIX
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
({
|
|
90
|
+
setupMwalajs,
|
|
91
|
+
createProject,
|
|
92
|
+
dropAllTables,
|
|
93
|
+
getDbConnection,
|
|
94
|
+
listTables,
|
|
95
|
+
createTable,
|
|
96
|
+
dropTable,
|
|
97
|
+
migrateAll,
|
|
98
|
+
rollbackLastMigration,
|
|
99
|
+
// ongeza hizi zingine ukizihitaji baadaye
|
|
100
|
+
showDatabaseSize,
|
|
101
|
+
listIndexes,
|
|
102
|
+
analyzeTable,
|
|
103
|
+
vacuumDatabase,
|
|
104
|
+
showConnections,
|
|
105
|
+
killConnections,
|
|
106
|
+
// n.k.
|
|
107
|
+
} = imports);
|
|
108
|
+
|
|
109
|
+
|
|
77
110
|
} catch (err) {
|
|
78
111
|
error(`Failed to load required modules:\n${err.stack || err.message}`);
|
|
79
112
|
process.exit(1);
|
|
@@ -596,38 +629,67 @@ case 'db:restore': {
|
|
|
596
629
|
}
|
|
597
630
|
|
|
598
631
|
|
|
632
|
+
// case 'db:size':
|
|
633
|
+
// await runSafe(showDatabaseSize, 'Database size shown');
|
|
634
|
+
// break;
|
|
635
|
+
|
|
636
|
+
// case 'db:indexes':
|
|
637
|
+
// if (!args[1]) return error('Table name required');
|
|
638
|
+
// await runSafe(() => listIndexes(args[1]), `Indexes for ${args[1]}`);
|
|
639
|
+
// break;
|
|
640
|
+
|
|
641
|
+
// case 'db:analyze':
|
|
642
|
+
// if (!args[1]) return error('Table name required');
|
|
643
|
+
// await runSafe(() => analyzeTable(args[1]), `Table ${args[1]} analyzed`);
|
|
644
|
+
// break;
|
|
645
|
+
|
|
646
|
+
// case 'db:reindex':
|
|
647
|
+
// if (!args[1]) return error('Table name required');
|
|
648
|
+
// await runSafe(() => reindexTable(args[1]), `Table ${args[1]} reindexed`);
|
|
649
|
+
// break;
|
|
650
|
+
|
|
651
|
+
// case 'db:vacuum':
|
|
652
|
+
// await runSafe(vacuumDatabase, 'Database vacuumed');
|
|
653
|
+
// break;
|
|
654
|
+
|
|
655
|
+
// case 'db:connections':
|
|
656
|
+
// await runSafe(showConnections, 'Active connections shown');
|
|
657
|
+
// break;
|
|
658
|
+
|
|
659
|
+
|
|
599
660
|
case 'db:size':
|
|
600
|
-
|
|
601
|
-
|
|
661
|
+
await runSafe(() => imports.showDatabaseSize(), 'Database size shown');
|
|
662
|
+
break;
|
|
602
663
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
664
|
+
case 'db:indexes':
|
|
665
|
+
if (!args[1]) return error('Table name required: mwala db:indexes <table>');
|
|
666
|
+
await runSafe(() => imports.listIndexes(args[1]), `Indexes for ${args[1]}`);
|
|
667
|
+
break;
|
|
607
668
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
669
|
+
case 'db:analyze':
|
|
670
|
+
if (!args[1]) return error('Table name required');
|
|
671
|
+
await runSafe(() => imports.analyzeTable(args[1]), `Table ${args[1]} analyzed`);
|
|
672
|
+
break;
|
|
612
673
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
break;
|
|
674
|
+
case 'db:vacuum':
|
|
675
|
+
await runSafe(() => imports.vacuumDatabase(), 'Database vacuumed');
|
|
676
|
+
break;
|
|
617
677
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
678
|
+
case 'db:connections':
|
|
679
|
+
await runSafe(() => imports.showConnections(), 'Active connections shown');
|
|
680
|
+
break;
|
|
621
681
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
682
|
+
case 'db:kill-connections':
|
|
683
|
+
if (readlineSync.keyInYNStrict('⚠️ Kill ALL other database connections? (hatari!)')) {
|
|
684
|
+
await runSafe(() => imports.killConnections(), 'Other connections killed');
|
|
685
|
+
}
|
|
686
|
+
break;
|
|
625
687
|
|
|
626
|
-
case 'db:kill-connections':
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
688
|
+
// case 'db:kill-connections':
|
|
689
|
+
// if (readlineSync.keyInYNStrict('⚠️ Kill ALL other database connections?')) {
|
|
690
|
+
// await runSafe(killConnections, 'Other connections killed');
|
|
691
|
+
// }
|
|
692
|
+
// break;
|
|
631
693
|
|
|
632
694
|
case 'db:drop-all-tables':
|
|
633
695
|
if (readlineSync.keyInYNStrict('⚠️⚠️ THIS WILL DROP **ALL** TABLES! Continue?')) {
|
package/package.json
CHANGED
package/runMigrations.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// runMigrations.mjs
|
|
2
|
+
|
|
1
3
|
import fs from 'fs';
|
|
2
4
|
import path from 'path';
|
|
3
5
|
import readline from 'readline';
|
|
@@ -134,6 +136,38 @@ export const listTables = async () => {
|
|
|
134
136
|
}
|
|
135
137
|
};
|
|
136
138
|
|
|
139
|
+
|
|
140
|
+
export const dropAllTables = async () => {
|
|
141
|
+
try {
|
|
142
|
+
const tables = await sequelize.getQueryInterface().showAllTables();
|
|
143
|
+
|
|
144
|
+
if (tables.length === 0) {
|
|
145
|
+
console.log('No tables exist in the database.');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log(`Dropping ${tables.length} table(s): ${tables.join(', ')}`);
|
|
150
|
+
|
|
151
|
+
// Drop in reverse order to respect foreign key dependencies if any
|
|
152
|
+
for (const table of tables.reverse()) {
|
|
153
|
+
await sequelize.getQueryInterface().dropTable(table, { cascade: true });
|
|
154
|
+
console.log(` Dropped: ${table}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Clear the migration tracking file so future migrates start clean
|
|
158
|
+
fs.writeFileSync(migrationLog, JSON.stringify([]));
|
|
159
|
+
console.log('Migration log cleared – ready for fresh migrations.');
|
|
160
|
+
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('Error while dropping all tables:', error.message);
|
|
163
|
+
if (error.stack) console.error(error.stack);
|
|
164
|
+
throw error; // let runSafe show the full error
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
137
171
|
const askUser = (question) => {
|
|
138
172
|
return new Promise((resolve) => {
|
|
139
173
|
const rl = readline.createInterface({
|
|
@@ -146,3 +180,112 @@ const askUser = (question) => {
|
|
|
146
180
|
});
|
|
147
181
|
});
|
|
148
182
|
};
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
// ────────────────────────────────────────────────
|
|
187
|
+
// MAINTENANCE TOOLS (MariaDB / MySQL compatible)
|
|
188
|
+
// ────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
export const showDatabaseSize = async () => {
|
|
191
|
+
const dbName = sequelize.getDatabaseName(); // au tumia sequelize.config.database kama haifanyi kazi
|
|
192
|
+
const [results] = await sequelize.query(`
|
|
193
|
+
SELECT
|
|
194
|
+
ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS size_mb
|
|
195
|
+
FROM information_schema.tables
|
|
196
|
+
WHERE table_schema = '${dbName}'
|
|
197
|
+
`);
|
|
198
|
+
|
|
199
|
+
const size = results[0]?.size_mb || '0.00';
|
|
200
|
+
console.log(`Database size: ${size} MB`);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export const listIndexes = async (tableName) => {
|
|
204
|
+
const dbName = sequelize.getDatabaseName();
|
|
205
|
+
const [results] = await sequelize.query(`
|
|
206
|
+
SHOW INDEXES FROM \`${tableName}\`
|
|
207
|
+
`);
|
|
208
|
+
|
|
209
|
+
if (results.length === 0) {
|
|
210
|
+
console.log(`Hakuna indexes kwenye table "${tableName}".`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
console.log(`Indexes kwenye "${tableName}":`);
|
|
215
|
+
results.forEach(row => {
|
|
216
|
+
const unique = row.Non_unique === 0 ? 'UNIQUE' : 'NON-UNIQUE';
|
|
217
|
+
console.log(` • ${row.Key_name.padEnd(35)} → ${unique} | Column: ${row.Column_name} | Type: ${row.Index_type}`);
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export const analyzeTable = async (tableName) => {
|
|
222
|
+
await sequelize.query(`ANALYZE TABLE \`${tableName}\`;`);
|
|
223
|
+
console.log(`Table "${tableName}" ime-analyze (statistics zime-update).`);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export const vacuumDatabase = async () => {
|
|
227
|
+
// MySQL/MariaDB haina VACUUM kama PostgreSQL, lakini tunaweza optimize tables zote
|
|
228
|
+
const dbName = sequelize.getDatabaseName();
|
|
229
|
+
const [tables] = await sequelize.query(`
|
|
230
|
+
SELECT table_name
|
|
231
|
+
FROM information_schema.tables
|
|
232
|
+
WHERE table_schema = '${dbName}'
|
|
233
|
+
AND table_type = 'BASE TABLE'
|
|
234
|
+
`);
|
|
235
|
+
|
|
236
|
+
if (tables.length === 0) {
|
|
237
|
+
console.log('Hakuna tables za ku-optimize.');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log(`Optimizing ${tables.length} table(s)...`);
|
|
242
|
+
for (const row of tables) {
|
|
243
|
+
const table = row.table_name;
|
|
244
|
+
await sequelize.query(`OPTIMIZE TABLE \`${table}\`;`);
|
|
245
|
+
console.log(` Optimized: ${table}`);
|
|
246
|
+
}
|
|
247
|
+
console.log('Optimization imekamilika (space reclaimed na stats updated).');
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export const showConnections = async () => {
|
|
251
|
+
const [results] = await sequelize.query(`
|
|
252
|
+
SHOW PROCESSLIST
|
|
253
|
+
`);
|
|
254
|
+
|
|
255
|
+
const active = results.filter(row => row.Command !== 'Sleep' && row.Id !== 0); // exclude idle + our connection
|
|
256
|
+
|
|
257
|
+
if (active.length === 0) {
|
|
258
|
+
console.log('Hakuna connections zingine active (isipokuwa yako).');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
console.log(`Active connections (${active.length}):`);
|
|
263
|
+
active.forEach(r => {
|
|
264
|
+
const querySnippet = r.Info ? r.Info.substring(0, 60) + '...' : '(idle)';
|
|
265
|
+
console.log(` ID ${r.Id} | User: ${r.User} | Host: ${r.Host} | State: ${r.State} | Query: ${querySnippet}`);
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export const killConnections = async () => {
|
|
270
|
+
const [results] = await sequelize.query(`
|
|
271
|
+
SHOW PROCESSLIST
|
|
272
|
+
`);
|
|
273
|
+
|
|
274
|
+
const toKill = results.filter(row => row.Id !== 0 && row.Command !== 'Sleep'); // exclude our connection + idle
|
|
275
|
+
|
|
276
|
+
if (toKill.length === 0) {
|
|
277
|
+
console.log('Hakuna connections za ku-kill.');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.log(`Killing ${toKill.length} connection(s)...`);
|
|
282
|
+
for (const row of toKill) {
|
|
283
|
+
try {
|
|
284
|
+
await sequelize.query(`KILL ${row.Id};`);
|
|
285
|
+
console.log(` Killed ID ${row.Id}`);
|
|
286
|
+
} catch (e) {
|
|
287
|
+
console.warn(` Failed to kill ${row.Id}: ${e.message}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
console.log('Kill operation imekamilika.');
|
|
291
|
+
};
|