/** * Backup Repository * Handles CRUD operations for backups and backup_schedules tables */ import { BaseRepository } from '../base.repository.ts'; import type { IBackup, IBackupSchedule, IBackupScheduleUpdate, TPlatformServiceType, TBackupScheduleScope, IRetentionPolicy, } from '../../types.ts'; export class BackupRepository extends BaseRepository { // ============ Backup CRUD ============ create(backup: Omit): IBackup { this.query( `INSERT INTO backups ( service_id, service_name, filename, size_bytes, created_at, includes_image, platform_resources, checksum, schedule_id ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ backup.serviceId, backup.serviceName, backup.filename, backup.sizeBytes, backup.createdAt, backup.includesImage ? 1 : 0, JSON.stringify(backup.platformResources), backup.checksum, backup.scheduleId ?? null, ] ); // Get the created backup by looking for the most recent one with matching filename const rows = this.query( 'SELECT * FROM backups WHERE filename = ? ORDER BY id DESC LIMIT 1', [backup.filename] ); return this.rowToBackup(rows[0]); } getById(id: number): IBackup | null { const rows = this.query('SELECT * FROM backups WHERE id = ?', [id]); return rows.length > 0 ? this.rowToBackup(rows[0]) : null; } getByService(serviceId: number): IBackup[] { const rows = this.query( 'SELECT * FROM backups WHERE service_id = ? ORDER BY created_at DESC', [serviceId] ); return rows.map((row) => this.rowToBackup(row)); } getAll(): IBackup[] { const rows = this.query('SELECT * FROM backups ORDER BY created_at DESC'); return rows.map((row) => this.rowToBackup(row)); } delete(id: number): void { this.query('DELETE FROM backups WHERE id = ?', [id]); } deleteByService(serviceId: number): void { this.query('DELETE FROM backups WHERE service_id = ?', [serviceId]); } getBySchedule(scheduleId: number): IBackup[] { const rows = this.query( 'SELECT * FROM backups WHERE schedule_id = ? ORDER BY created_at DESC', [scheduleId] ); return rows.map((row) => this.rowToBackup(row)); } private rowToBackup(row: any): IBackup { let platformResources: TPlatformServiceType[] = []; const platformResourcesRaw = row.platform_resources; if (platformResourcesRaw) { try { platformResources = JSON.parse(String(platformResourcesRaw)); } catch { platformResources = []; } } return { id: Number(row.id), serviceId: Number(row.service_id), serviceName: String(row.service_name), filename: String(row.filename), sizeBytes: Number(row.size_bytes), createdAt: Number(row.created_at), includesImage: Boolean(row.includes_image), platformResources, checksum: String(row.checksum), scheduleId: row.schedule_id ? Number(row.schedule_id) : undefined, }; } // ============ Backup Schedule CRUD ============ createSchedule(schedule: Omit): IBackupSchedule { const now = Date.now(); this.query( `INSERT INTO backup_schedules ( scope_type, scope_pattern, service_id, service_name, cron_expression, retention_hourly, retention_daily, retention_weekly, retention_monthly, enabled, last_run_at, next_run_at, last_status, last_error, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ schedule.scopeType, schedule.scopePattern ?? null, schedule.serviceId ?? null, schedule.serviceName ?? null, schedule.cronExpression, schedule.retention.hourly, schedule.retention.daily, schedule.retention.weekly, schedule.retention.monthly, schedule.enabled ? 1 : 0, schedule.lastRunAt, schedule.nextRunAt, schedule.lastStatus, schedule.lastError, now, now, ] ); // Get the created schedule by looking for the most recent one with matching scope const rows = this.query( 'SELECT * FROM backup_schedules WHERE scope_type = ? AND cron_expression = ? ORDER BY id DESC LIMIT 1', [schedule.scopeType, schedule.cronExpression] ); return this.rowToSchedule(rows[0]); } getScheduleById(id: number): IBackupSchedule | null { const rows = this.query('SELECT * FROM backup_schedules WHERE id = ?', [id]); return rows.length > 0 ? this.rowToSchedule(rows[0]) : null; } getSchedulesByService(serviceId: number): IBackupSchedule[] { const rows = this.query( 'SELECT * FROM backup_schedules WHERE service_id = ? ORDER BY created_at DESC', [serviceId] ); return rows.map((row) => this.rowToSchedule(row)); } getEnabledSchedules(): IBackupSchedule[] { const rows = this.query( 'SELECT * FROM backup_schedules WHERE enabled = 1 ORDER BY next_run_at ASC' ); return rows.map((row) => this.rowToSchedule(row)); } getAllSchedules(): IBackupSchedule[] { const rows = this.query('SELECT * FROM backup_schedules ORDER BY created_at DESC'); return rows.map((row) => this.rowToSchedule(row)); } updateSchedule(id: number, updates: IBackupScheduleUpdate & { lastRunAt?: number; nextRunAt?: number; lastStatus?: 'success' | 'failed' | null; lastError?: string | null }): void { const setClauses: string[] = []; const params: (string | number | null)[] = []; if (updates.cronExpression !== undefined) { setClauses.push('cron_expression = ?'); params.push(updates.cronExpression); } if (updates.retention !== undefined) { setClauses.push('retention_hourly = ?'); params.push(updates.retention.hourly); setClauses.push('retention_daily = ?'); params.push(updates.retention.daily); setClauses.push('retention_weekly = ?'); params.push(updates.retention.weekly); setClauses.push('retention_monthly = ?'); params.push(updates.retention.monthly); } if (updates.enabled !== undefined) { setClauses.push('enabled = ?'); params.push(updates.enabled ? 1 : 0); } if (updates.lastRunAt !== undefined) { setClauses.push('last_run_at = ?'); params.push(updates.lastRunAt); } if (updates.nextRunAt !== undefined) { setClauses.push('next_run_at = ?'); params.push(updates.nextRunAt); } if (updates.lastStatus !== undefined) { setClauses.push('last_status = ?'); params.push(updates.lastStatus); } if (updates.lastError !== undefined) { setClauses.push('last_error = ?'); params.push(updates.lastError); } if (setClauses.length === 0) return; setClauses.push('updated_at = ?'); params.push(Date.now()); params.push(id); this.query(`UPDATE backup_schedules SET ${setClauses.join(', ')} WHERE id = ?`, params); } deleteSchedule(id: number): void { this.query('DELETE FROM backup_schedules WHERE id = ?', [id]); } deleteSchedulesByService(serviceId: number): void { this.query('DELETE FROM backup_schedules WHERE service_id = ?', [serviceId]); } private rowToSchedule(row: any): IBackupSchedule { return { id: Number(row.id), scopeType: (String(row.scope_type) || 'service') as TBackupScheduleScope, scopePattern: row.scope_pattern ? String(row.scope_pattern) : undefined, serviceId: row.service_id ? Number(row.service_id) : undefined, serviceName: row.service_name ? String(row.service_name) : undefined, cronExpression: String(row.cron_expression), retention: { hourly: Number(row.retention_hourly ?? 0), daily: Number(row.retention_daily ?? 7), weekly: Number(row.retention_weekly ?? 4), monthly: Number(row.retention_monthly ?? 12), } as IRetentionPolicy, enabled: Boolean(row.enabled), lastRunAt: row.last_run_at ? Number(row.last_run_at) : null, nextRunAt: row.next_run_at ? Number(row.next_run_at) : null, lastStatus: row.last_status ? (String(row.last_status) as 'success' | 'failed') : null, lastError: row.last_error ? String(row.last_error) : null, createdAt: Number(row.created_at), updatedAt: Number(row.updated_at), }; } }