|
| 1 | +/** |
| 2 | + * Description : Remove orphan widget and long_tasks records left behind after a dashboard is deleted |
| 3 | + * Server : countly |
| 4 | + * Path : $(countly dir)/bin/scripts/fix-data |
| 5 | + * Command : node delete_widgets_of_deleted_dashboards.js [--dry-run] |
| 6 | + * Usage : |
| 7 | + * # Preview only |
| 8 | + * node delete_widgets_of_deleted_dashboards.js --dry-run |
| 9 | + * # Actual deletion |
| 10 | + * node delete_widgets_of_deleted_dashboards.js |
| 11 | + */ |
| 12 | +const DRY_RUN = process.argv.includes('--dry-run'); |
| 13 | + |
| 14 | +const BATCH_SIZE = 1000; |
| 15 | +const pluginManager = require('../../../plugins/pluginManager.js'); |
| 16 | + |
| 17 | +/** |
| 18 | + * Deletes documents in batches to avoid oversized commands |
| 19 | + * @param {Object} db - MongoDB connection |
| 20 | + * @param {String} collection - Collection name |
| 21 | + * @param {Array} ids - List of document ids to delete |
| 22 | + */ |
| 23 | +async function deleteByChunks(db, collection, ids) { |
| 24 | + let bucket = []; |
| 25 | + |
| 26 | + for (const id of ids) { |
| 27 | + bucket.push(id); |
| 28 | + |
| 29 | + if (bucket.length === BATCH_SIZE) { |
| 30 | + await runDelete(bucket); |
| 31 | + bucket = []; |
| 32 | + } |
| 33 | + } |
| 34 | + |
| 35 | + if (bucket.length) { |
| 36 | + await runDelete(bucket); |
| 37 | + } |
| 38 | + |
| 39 | + /** |
| 40 | + * Executes the delete operation for a batch of ids |
| 41 | + * @param {Array} batch - Array of document ids to delete |
| 42 | + * @returns {Promise<void>} |
| 43 | + * */ |
| 44 | + async function runDelete(batch) { |
| 45 | + if (DRY_RUN) { |
| 46 | + console.log(`[dry-run] ${collection}: would delete ${batch.length}`); |
| 47 | + } |
| 48 | + else { |
| 49 | + const res = await db.collection(collection).deleteMany({ _id: { $in: batch } }); |
| 50 | + console.log(`[deleted] ${collection}: ${res.deletedCount}`); |
| 51 | + } |
| 52 | + } |
| 53 | +} |
| 54 | + |
| 55 | +(async() => { |
| 56 | + const db = await pluginManager.dbConnection('countly'); |
| 57 | + |
| 58 | + try { |
| 59 | + const dashboardWidgets = []; |
| 60 | + |
| 61 | + const dashCursor = db.collection('dashboards').find({widgets: {$exists: true, $not: {$size: 0}}}, {projection: {widgets: 1}}); |
| 62 | + |
| 63 | + while (await dashCursor.hasNext()) { |
| 64 | + const dash = await dashCursor.next(); |
| 65 | + for (const w of dash.widgets) { |
| 66 | + const idStr = (w && w.$oid) ? w.$oid : (w + ''); |
| 67 | + if (idStr && !dashboardWidgets.includes(idStr)) { |
| 68 | + dashboardWidgets.push(idStr); |
| 69 | + } |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + await dashCursor.close(); |
| 74 | + |
| 75 | + const orphanWidgetIds = []; |
| 76 | + const orphanLongTaskIds = []; |
| 77 | + |
| 78 | + const widgetCursor = db.collection('widgets').find({}, {projection: {_id: 1, drill_report: 1}}); |
| 79 | + |
| 80 | + while (await widgetCursor.hasNext()) { |
| 81 | + const w = await widgetCursor.next(); |
| 82 | + if (!dashboardWidgets.includes(String(w._id))) { |
| 83 | + orphanWidgetIds.push(w._id); |
| 84 | + if (Array.isArray(w.drill_report)) { |
| 85 | + orphanLongTaskIds.push(...w.drill_report); |
| 86 | + } |
| 87 | + } |
| 88 | + } |
| 89 | + await widgetCursor.close(); |
| 90 | + |
| 91 | + console.log(`Orphan widgets found: ${orphanWidgetIds.length}`); |
| 92 | + console.log(`Linked long_tasks to drop: ${orphanLongTaskIds.length}`); |
| 93 | + |
| 94 | + await deleteByChunks(db, 'widgets', orphanWidgetIds); |
| 95 | + await deleteByChunks(db, 'long_tasks', orphanLongTaskIds); |
| 96 | + |
| 97 | + |
| 98 | + console.log(DRY_RUN ? 'Dry-run finished' : 'Cleanup completed.'); |
| 99 | + } |
| 100 | + catch (err) { |
| 101 | + console.error(err); |
| 102 | + } |
| 103 | + finally { |
| 104 | + db.close(); |
| 105 | + } |
| 106 | +})(); |
0 commit comments