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+ // Widget type configurations for long_tasks relationships
18+ const WIDGET_LONG_TASK_CONFIG = {
19+ 'drill' : {
20+ reportField : 'drill_report'
21+ } ,
22+ 'users' : {
23+ reportField : 'drill_report'
24+ } ,
25+ 'formulas' : {
26+ reportField : 'cmetrics'
27+ }
28+ } ;
29+
30+ /**
31+ * Deletes documents in batches to avoid oversized commands
32+ * @param {Object } db - MongoDB connection
33+ * @param {String } collection - Collection name
34+ * @param {Array } ids - List of document ids to delete
35+ */
36+ async function deleteByChunks ( db , collection , ids ) {
37+ let bucket = [ ] ;
38+
39+ for ( const id of ids ) {
40+ bucket . push ( id ) ;
41+
42+ if ( bucket . length === BATCH_SIZE ) {
43+ await runDelete ( bucket ) ;
44+ bucket = [ ] ;
45+ }
46+ }
47+
48+ if ( bucket . length ) {
49+ await runDelete ( bucket ) ;
50+ }
51+
52+ /**
53+ * Executes the delete operation for a batch of ids
54+ * @param {Array } batch - Array of document ids to delete
55+ * @returns {Promise<void> }
56+ * */
57+ async function runDelete ( batch ) {
58+ if ( DRY_RUN ) {
59+ console . log ( `[dry-run] ${ collection } : would delete ${ batch . length } ` ) ;
60+ }
61+ else {
62+ const res = await db . collection ( collection ) . deleteMany ( { _id : { $in : batch } } ) ;
63+ console . log ( `[deleted] ${ collection } : ${ res . deletedCount } ` ) ;
64+ }
65+ }
66+ }
67+
68+ /**
69+ * Counts references to reports and returns only unreferenced ones
70+ * @param {Object } db - MongoDB connection
71+ * @param {Array } reportIds - Report IDs to be checked
72+ * @param {Array } excludeWidgetIds - Widget IDs to exclude from reference check
73+ * @returns {Array } Unreferenced report IDs
74+ */
75+ async function getUnreferencedReports ( db , reportIds , excludeWidgetIds ) {
76+ if ( ! reportIds || ! reportIds . length ) {
77+ return [ ] ;
78+ }
79+
80+ let referencedReports = [ ] ;
81+
82+ // Check all widget types that can reference reports
83+ for ( const [ widgetType , config ] of Object . entries ( WIDGET_LONG_TASK_CONFIG ) ) {
84+ const query = {
85+ widget_type : widgetType ,
86+ [ config . reportField ] : { $in : reportIds }
87+ } ;
88+
89+ // Exclude orphan widgets from reference check
90+ if ( excludeWidgetIds . length ) {
91+ query . _id = { $nin : excludeWidgetIds } ;
92+ }
93+
94+ const widgets = await db . collection ( 'widgets' ) . find ( query , { [ config . reportField ] : 1 } ) . toArray ( ) ;
95+
96+ widgets . forEach ( widget => {
97+ const reports = widget [ config . reportField ] || [ ] ;
98+ referencedReports . push ( ...reports . map ( reportId => reportId . toString ( ) ) ) ;
99+ } ) ;
100+ }
101+
102+ // Return only those report IDs that are not referenced in any widget
103+ return reportIds . filter ( reportId => ! referencedReports . includes ( reportId . toString ( ) ) ) ;
104+ }
105+
106+ /**
107+ * Collects all linked long_task IDs from a widget based on its type
108+ * @param {Object } widget - Widget document
109+ * @returns {Array } Array of long_task IDs
110+ */
111+ function collectAllLinkedLongTasks ( widget ) {
112+ const config = WIDGET_LONG_TASK_CONFIG [ widget . widget_type ] ;
113+ if ( ! config ) {
114+ return [ ] ;
115+ }
116+
117+ const reportField = config . reportField ;
118+ return Array . isArray ( widget [ reportField ] ) ? widget [ reportField ] : [ ] ;
119+ }
120+
121+
122+ ( async ( ) => {
123+ const db = await pluginManager . dbConnection ( 'countly' ) ;
124+
125+ try {
126+ const dashboardWidgets = [ ] ;
127+
128+ const dashCursor = db . collection ( 'dashboards' ) . find ( { widgets : { $exists : true , $not : { $size : 0 } } } , { projection : { widgets : 1 } } ) ;
129+
130+ while ( await dashCursor . hasNext ( ) ) {
131+ const dash = await dashCursor . next ( ) ;
132+ for ( const w of dash . widgets ) {
133+ const idStr = ( w && w . $oid ) ? w . $oid : ( w + '' ) ;
134+ if ( idStr && ! dashboardWidgets . includes ( idStr ) ) {
135+ dashboardWidgets . push ( idStr ) ;
136+ }
137+ }
138+ }
139+
140+ await dashCursor . close ( ) ;
141+
142+ const orphanWidgetIds = [ ] ;
143+ const allLinkedLongTasks = [ ] ;
144+
145+ const widgetCursor = db . collection ( 'widgets' ) . find ( { } ) ;
146+
147+ while ( await widgetCursor . hasNext ( ) ) {
148+ const w = await widgetCursor . next ( ) ;
149+ if ( ! dashboardWidgets . includes ( String ( w . _id ) ) ) {
150+ orphanWidgetIds . push ( w . _id ) ;
151+
152+ // Find linked long_tasks based on widget type
153+ const linkedTasks = collectAllLinkedLongTasks ( w ) ;
154+ allLinkedLongTasks . push ( ...linkedTasks ) ;
155+ }
156+ }
157+ await widgetCursor . close ( ) ;
158+
159+ console . log ( `Orphan widgets found: ${ orphanWidgetIds . length } ` ) ;
160+ if ( DRY_RUN && orphanWidgetIds . length ) {
161+ console . log ( 'Orphan widget IDs to be deleted:' , orphanWidgetIds . map ( id => id . toString ( ) ) ) ;
162+ }
163+ await deleteByChunks ( db , 'widgets' , orphanWidgetIds ) ;
164+
165+ const unreferencedLongTasks = await getUnreferencedReports ( db , allLinkedLongTasks , orphanWidgetIds ) ;
166+ console . log ( `Unreferenced long_tasks to delete: ${ unreferencedLongTasks . length } ` ) ;
167+ if ( DRY_RUN && unreferencedLongTasks . length ) {
168+ console . log ( 'Unreferenced long_task IDs to be deleted:' , unreferencedLongTasks . map ( id => id . toString ( ) ) ) ;
169+ }
170+ await deleteByChunks ( db , 'long_tasks' , unreferencedLongTasks ) ;
171+
172+ console . log ( DRY_RUN ? 'Dry-run finished' : 'Cleanup completed.' ) ;
173+ }
174+ catch ( err ) {
175+ console . error ( err ) ;
176+ }
177+ finally {
178+ db . close ( ) ;
179+ }
180+ } ) ( ) ;
0 commit comments