/** * Data Retention Cleanup Service * Automatically removes old data based on tenant retention policies */ const cron = require('node-cron'); const { Op } = require('sequelize'); class DataRetentionService { constructor() { this.isRunning = false; this.lastCleanup = null; // Run cleanup daily at 2 AM this.scheduleCleanup(); } /** * Schedule automatic cleanup */ scheduleCleanup() { // Run at 2:00 AM every day cron.schedule('0 2 * * *', async () => { console.log('๐Ÿ—‘๏ธ Starting scheduled data retention cleanup...'); await this.runCleanup(); }); console.log('๐Ÿ“… Data retention cleanup scheduled for 2:00 AM daily'); } /** * Run cleanup for all tenants */ async runCleanup() { if (this.isRunning) { console.log('โš ๏ธ Data retention cleanup already running, skipping...'); return; } try { this.isRunning = true; const startTime = Date.now(); console.log('๐Ÿ—‘๏ธ Starting data retention cleanup...'); const { Tenant } = require('../models'); const tenants = await Tenant.findAll({ where: { is_active: true } }); let totalCleaned = { detections: 0, heartbeats: 0, logs: 0, sessions: 0 }; for (const tenant of tenants) { const retentionDays = tenant.features?.data_retention_days; // Skip tenants with unlimited retention (-1) if (retentionDays === -1) { console.log(`โญ๏ธ Skipping tenant ${tenant.slug} - unlimited retention`); continue; } console.log(`๐Ÿงน Cleaning data for tenant ${tenant.slug} (${retentionDays} days retention)`); const cleanupResult = await this.cleanupTenantData(tenant.id, retentionDays); totalCleaned.detections += cleanupResult.detections; totalCleaned.heartbeats += cleanupResult.heartbeats; totalCleaned.logs += cleanupResult.logs; totalCleaned.sessions += cleanupResult.sessions; } const duration = Date.now() - startTime; this.lastCleanup = new Date(); console.log(`โœ… Data retention cleanup completed in ${duration}ms`); console.log(`๐Ÿ“Š Cleaned up:`, totalCleaned); } catch (error) { console.error('โŒ Error during data retention cleanup:', error); } finally { this.isRunning = false; } } /** * Clean up data for a specific tenant */ async cleanupTenantData(tenantId, retentionDays) { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - retentionDays); console.log(`๐Ÿ—‘๏ธ Cleaning data older than ${cutoffDate.toISOString()} for tenant ${tenantId}`); const { DroneDetection, Heartbeat, ApiLog, Session } = require('../models'); const cleanupResults = { detections: 0, heartbeats: 0, logs: 0, sessions: 0 }; try { // Clean up drone detections const deletedDetections = await DroneDetection.destroy({ where: { tenant_id: tenantId, timestamp: { [Op.lt]: cutoffDate } } }); cleanupResults.detections = deletedDetections; // Clean up heartbeats const deletedHeartbeats = await Heartbeat.destroy({ where: { tenant_id: tenantId, timestamp: { [Op.lt]: cutoffDate } } }); cleanupResults.heartbeats = deletedHeartbeats; // Clean up API logs (if exists) try { const deletedLogs = await ApiLog.destroy({ where: { tenant_id: tenantId, created_at: { [Op.lt]: cutoffDate } } }); cleanupResults.logs = deletedLogs; } catch (error) { // ApiLog table might not exist, skip silently console.log(`โญ๏ธ Skipping API logs cleanup for tenant ${tenantId} (table might not exist)`); } // Clean up old sessions try { const deletedSessions = await Session.destroy({ where: { tenant_id: tenantId, updated_at: { [Op.lt]: cutoffDate } } }); cleanupResults.sessions = deletedSessions; } catch (error) { // Session table might not exist, skip silently console.log(`โญ๏ธ Skipping sessions cleanup for tenant ${tenantId} (table might not exist)`); } console.log(`โœ… Tenant ${tenantId} cleanup:`, cleanupResults); } catch (error) { console.error(`โŒ Error cleaning data for tenant ${tenantId}:`, error); } return cleanupResults; } /** * Manual cleanup for a specific tenant */ async manualCleanup(tenantId, retentionDays = null) { const { Tenant } = require('../models'); const tenant = await Tenant.findByPk(tenantId); if (!tenant) { throw new Error('Tenant not found'); } const days = retentionDays || tenant.features?.data_retention_days; if (days === -1) { throw new Error('Tenant has unlimited retention, manual cleanup requires explicit retention days'); } console.log(`๐Ÿงน Manual cleanup for tenant ${tenant.slug} (${days} days retention)`); return await this.cleanupTenantData(tenantId, days); } /** * Get cleanup statistics */ async getCleanupStats() { const { Tenant, DroneDetection, Heartbeat } = require('../models'); const tenants = await Tenant.findAll({ where: { is_active: true } }); const stats = []; for (const tenant of tenants) { const retentionDays = tenant.features?.data_retention_days; if (retentionDays === -1) { stats.push({ tenant_id: tenant.id, tenant_slug: tenant.slug, retention_days: 'unlimited', old_detections: 0, old_heartbeats: 0, next_cleanup: 'never' }); continue; } const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - retentionDays); const [oldDetections, oldHeartbeats] = await Promise.all([ DroneDetection.count({ where: { tenant_id: tenant.id, timestamp: { [Op.lt]: cutoffDate } } }), Heartbeat.count({ where: { tenant_id: tenant.id, timestamp: { [Op.lt]: cutoffDate } } }) ]); stats.push({ tenant_id: tenant.id, tenant_slug: tenant.slug, retention_days: retentionDays, old_detections: oldDetections, old_heartbeats: oldHeartbeats, next_cleanup: this.getNextCleanupTime() }); } return { last_cleanup: this.lastCleanup, next_cleanup: this.getNextCleanupTime(), is_running: this.isRunning, tenant_stats: stats }; } /** * Get next scheduled cleanup time */ getNextCleanupTime() { const now = new Date(); const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(2, 0, 0, 0); return tomorrow; } /** * Force immediate cleanup (for testing/admin use) */ async forceCleanup() { console.log('๐Ÿšจ Force cleanup initiated'); await this.runCleanup(); } } // Create singleton instance const dataRetentionService = new DataRetentionService(); module.exports = dataRetentionService;