282 lines
7.3 KiB
JavaScript
282 lines
7.3 KiB
JavaScript
/**
|
|
* 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; |