/** * Database layer for Onebox using SQLite * Refactored into repository pattern */ import * as plugins from '../plugins.ts'; import type { IService, IRegistry, IRegistryToken, ISslCertificate, IMetric, ILogEntry, IUser, IPlatformService, IPlatformResource, TPlatformServiceType, IDomain, ICertificate, ICertRequirement, IBackup, IBackupSchedule, IBackupScheduleUpdate, } from '../types.ts'; import type { TBindValue } from './types.ts'; import { logger } from '../logging.ts'; import { getErrorMessage } from '../utils/error.ts'; import { MigrationRunner } from './migrations/index.ts'; // Import repositories import { ServiceRepository, RegistryRepository, CertificateRepository, AuthRepository, MetricsRepository, PlatformRepository, BackupRepository, } from './repositories/index.ts'; export class OneboxDatabase { private db: InstanceType | null = null; private dbPath: string; // Repositories private serviceRepo!: ServiceRepository; private registryRepo!: RegistryRepository; private certificateRepo!: CertificateRepository; private authRepo!: AuthRepository; private metricsRepo!: MetricsRepository; private platformRepo!: PlatformRepository; private backupRepo!: BackupRepository; constructor(dbPath = './.nogit/onebox.db') { this.dbPath = dbPath; } /** * Initialize database connection and create tables */ async init(): Promise { try { // Ensure data directory exists const dbDir = plugins.path.dirname(this.dbPath); await Deno.mkdir(dbDir, { recursive: true }); // Open database this.db = new plugins.sqlite.DB(this.dbPath); logger.info(`Database initialized at ${this.dbPath}`); // Create tables await this.createTables(); // Run migrations if needed const runner = new MigrationRunner(this.query.bind(this)); runner.run(); // Initialize repositories with bound query function const queryFn = this.query.bind(this); this.serviceRepo = new ServiceRepository(queryFn); this.registryRepo = new RegistryRepository(queryFn); this.certificateRepo = new CertificateRepository(queryFn); this.authRepo = new AuthRepository(queryFn); this.metricsRepo = new MetricsRepository(queryFn); this.platformRepo = new PlatformRepository(queryFn); this.backupRepo = new BackupRepository(queryFn); } catch (error) { logger.error(`Failed to initialize database: ${getErrorMessage(error)}`); throw error; } } /** * Create all database tables */ private async createTables(): Promise { if (!this.db) throw new Error('Database not initialized'); // Services table this.query(` CREATE TABLE IF NOT EXISTS services ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, image TEXT NOT NULL, registry TEXT, env_vars TEXT NOT NULL, port INTEGER NOT NULL, domain TEXT, container_id TEXT, status TEXT NOT NULL DEFAULT 'stopped', created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ) `); // Registries table this.query(` CREATE TABLE IF NOT EXISTS registries ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL UNIQUE, username TEXT NOT NULL, password_encrypted TEXT NOT NULL, created_at INTEGER NOT NULL ) `); // Nginx configs table this.query(` CREATE TABLE IF NOT EXISTS nginx_configs ( id INTEGER PRIMARY KEY AUTOINCREMENT, service_id INTEGER NOT NULL, domain TEXT NOT NULL, port INTEGER NOT NULL, ssl_enabled INTEGER NOT NULL DEFAULT 0, config_template TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE ) `); // SSL certificates table this.query(` CREATE TABLE IF NOT EXISTS ssl_certificates ( id INTEGER PRIMARY KEY AUTOINCREMENT, domain TEXT NOT NULL UNIQUE, cert_path TEXT NOT NULL, key_path TEXT NOT NULL, full_chain_path TEXT NOT NULL, expiry_date INTEGER NOT NULL, issuer TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ) `); // DNS records table this.query(` CREATE TABLE IF NOT EXISTS dns_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, domain TEXT NOT NULL UNIQUE, type TEXT NOT NULL, value TEXT NOT NULL, cloudflare_id TEXT, zone_id TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ) `); // Metrics table this.query(` CREATE TABLE IF NOT EXISTS metrics ( id INTEGER PRIMARY KEY AUTOINCREMENT, service_id INTEGER NOT NULL, timestamp INTEGER NOT NULL, cpu_percent REAL NOT NULL, memory_used INTEGER NOT NULL, memory_limit INTEGER NOT NULL, network_rx_bytes INTEGER NOT NULL, network_tx_bytes INTEGER NOT NULL, FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE ) `); // Create index for metrics queries this.query(` CREATE INDEX IF NOT EXISTS idx_metrics_service_timestamp ON metrics(service_id, timestamp DESC) `); // Logs table this.query(` CREATE TABLE IF NOT EXISTS logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, service_id INTEGER NOT NULL, timestamp INTEGER NOT NULL, message TEXT NOT NULL, level TEXT NOT NULL, source TEXT NOT NULL, FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE ) `); // Create index for logs queries this.query(` CREATE INDEX IF NOT EXISTS idx_logs_service_timestamp ON logs(service_id, timestamp DESC) `); // Users table this.query(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'user', created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ) `); // Settings table this.query(` CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL ) `); // Version table for migrations this.query(` CREATE TABLE IF NOT EXISTS migrations ( version INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL ) `); logger.debug('Database tables created successfully'); } /** * Run database migrations */ /** * Close database connection */ close(): void { if (this.db) { this.db.close(); this.db = null; logger.debug('Database connection closed'); } } /** * Execute a raw query */ query>(sql: string, params: TBindValue[] = []): T[] { if (!this.db) { const error = new Error('Database not initialized'); console.error('Database access before initialization!'); console.error('Stack trace:', error.stack); throw error; } if (params.length === 0 && !sql.trim().toUpperCase().startsWith('SELECT')) { this.db.exec(sql); return [] as T[]; } const stmt = this.db.prepare(sql); try { const result = stmt.all(...params); return result as T[]; } finally { stmt.finalize(); } } // ============ Services CRUD (delegated to repository) ============ async createService(service: Omit): Promise { return this.serviceRepo.create(service); } getServiceByName(name: string): IService | null { return this.serviceRepo.getByName(name); } getServiceByID(id: number): IService | null { return this.serviceRepo.getById(id); } getAllServices(): IService[] { return this.serviceRepo.getAll(); } updateService(id: number, updates: Partial): void { this.serviceRepo.update(id, updates); } deleteService(id: number): void { this.serviceRepo.delete(id); } // ============ Registries CRUD (delegated to repository) ============ async createRegistry(registry: Omit): Promise { return this.registryRepo.createRegistry(registry); } getRegistryByURL(url: string): IRegistry | null { return this.registryRepo.getRegistryByURL(url); } getAllRegistries(): IRegistry[] { return this.registryRepo.getAllRegistries(); } deleteRegistry(url: string): void { this.registryRepo.deleteRegistry(url); } // ============ Settings CRUD (delegated to repository) ============ getSetting(key: string): string | null { return this.authRepo.getSetting(key); } setSetting(key: string, value: string): void { this.authRepo.setSetting(key, value); } getAllSettings(): Record { return this.authRepo.getAllSettings(); } // ============ Users CRUD (delegated to repository) ============ async createUser(user: Omit): Promise { return this.authRepo.createUser(user); } getUserByUsername(username: string): IUser | null { return this.authRepo.getUserByUsername(username); } getAllUsers(): IUser[] { return this.authRepo.getAllUsers(); } updateUserPassword(username: string, passwordHash: string): void { this.authRepo.updateUserPassword(username, passwordHash); } deleteUser(username: string): void { this.authRepo.deleteUser(username); } // ============ Metrics (delegated to repository) ============ addMetric(metric: Omit): void { this.metricsRepo.addMetric(metric); } getMetrics(serviceId: number, limit = 100): IMetric[] { return this.metricsRepo.getMetrics(serviceId, limit); } // ============ Logs (delegated to repository) ============ addLog(log: Omit): void { this.metricsRepo.addLog(log); } getLogs(serviceId: number, limit = 1000): ILogEntry[] { return this.metricsRepo.getLogs(serviceId, limit); } // ============ SSL Certificates Legacy API (delegated to repository) ============ async createSSLCertificate(cert: Omit): Promise { return this.certificateRepo.createSSLCertificate(cert); } getSSLCertificate(domain: string): ISslCertificate | null { return this.certificateRepo.getSSLCertificate(domain); } getAllSSLCertificates(): ISslCertificate[] { return this.certificateRepo.getAllSSLCertificates(); } updateSSLCertificate(domain: string, updates: Partial): void { this.certificateRepo.updateSSLCertificate(domain, updates); } deleteSSLCertificate(domain: string): void { this.certificateRepo.deleteSSLCertificate(domain); } // ============ Domains (delegated to repository) ============ createDomain(domain: Omit): IDomain { return this.certificateRepo.createDomain(domain); } getDomainByName(domain: string): IDomain | null { return this.certificateRepo.getDomainByName(domain); } getDomainById(id: number): IDomain | null { return this.certificateRepo.getDomainById(id); } getAllDomains(): IDomain[] { return this.certificateRepo.getAllDomains(); } getDomainsByProvider(provider: 'cloudflare' | 'manual'): IDomain[] { return this.certificateRepo.getDomainsByProvider(provider); } updateDomain(id: number, updates: Partial): void { this.certificateRepo.updateDomain(id, updates); } deleteDomain(id: number): void { this.certificateRepo.deleteDomain(id); } // ============ Certificates (delegated to repository) ============ createCertificate(cert: Omit): ICertificate { return this.certificateRepo.createCertificate(cert); } getCertificateById(id: number): ICertificate | null { return this.certificateRepo.getCertificateById(id); } getCertificatesByDomain(domainId: number): ICertificate[] { return this.certificateRepo.getCertificatesByDomain(domainId); } getAllCertificates(): ICertificate[] { return this.certificateRepo.getAllCertificates(); } updateCertificate(id: number, updates: Partial): void { this.certificateRepo.updateCertificate(id, updates); } deleteCertificate(id: number): void { this.certificateRepo.deleteCertificate(id); } // ============ Certificate Requirements (delegated to repository) ============ createCertRequirement(req: Omit): ICertRequirement { return this.certificateRepo.createCertRequirement(req); } getCertRequirementById(id: number): ICertRequirement | null { return this.certificateRepo.getCertRequirementById(id); } getCertRequirementsByService(serviceId: number): ICertRequirement[] { return this.certificateRepo.getCertRequirementsByService(serviceId); } getCertRequirementsByDomain(domainId: number): ICertRequirement[] { return this.certificateRepo.getCertRequirementsByDomain(domainId); } getAllCertRequirements(): ICertRequirement[] { return this.certificateRepo.getAllCertRequirements(); } updateCertRequirement(id: number, updates: Partial): void { this.certificateRepo.updateCertRequirement(id, updates); } deleteCertRequirement(id: number): void { this.certificateRepo.deleteCertRequirement(id); } // ============ Registry Tokens (delegated to repository) ============ createRegistryToken(token: Omit): IRegistryToken { return this.registryRepo.createToken(token); } getRegistryTokenById(id: number): IRegistryToken | null { return this.registryRepo.getTokenById(id); } getRegistryTokenByHash(tokenHash: string): IRegistryToken | null { return this.registryRepo.getTokenByHash(tokenHash); } getAllRegistryTokens(): IRegistryToken[] { return this.registryRepo.getAllTokens(); } getRegistryTokensByType(type: 'global' | 'ci'): IRegistryToken[] { return this.registryRepo.getTokensByType(type); } updateRegistryTokenLastUsed(id: number): void { this.registryRepo.updateTokenLastUsed(id); } deleteRegistryToken(id: number): void { this.registryRepo.deleteToken(id); } // ============ Platform Services (delegated to repository) ============ createPlatformService(service: Omit): IPlatformService { return this.platformRepo.createPlatformService(service); } getPlatformServiceByName(name: string): IPlatformService | null { return this.platformRepo.getPlatformServiceByName(name); } getPlatformServiceById(id: number): IPlatformService | null { return this.platformRepo.getPlatformServiceById(id); } getPlatformServiceByType(type: TPlatformServiceType): IPlatformService | null { return this.platformRepo.getPlatformServiceByType(type); } getAllPlatformServices(): IPlatformService[] { return this.platformRepo.getAllPlatformServices(); } updatePlatformService(id: number, updates: Partial): void { this.platformRepo.updatePlatformService(id, updates); } deletePlatformService(id: number): void { this.platformRepo.deletePlatformService(id); } // ============ Platform Resources (delegated to repository) ============ createPlatformResource(resource: Omit): IPlatformResource { return this.platformRepo.createPlatformResource(resource); } getPlatformResourceById(id: number): IPlatformResource | null { return this.platformRepo.getPlatformResourceById(id); } getPlatformResourcesByService(serviceId: number): IPlatformResource[] { return this.platformRepo.getPlatformResourcesByService(serviceId); } getPlatformResourcesByPlatformService(platformServiceId: number): IPlatformResource[] { return this.platformRepo.getPlatformResourcesByPlatformService(platformServiceId); } getAllPlatformResources(): IPlatformResource[] { return this.platformRepo.getAllPlatformResources(); } deletePlatformResource(id: number): void { this.platformRepo.deletePlatformResource(id); } deletePlatformResourcesByService(serviceId: number): void { this.platformRepo.deletePlatformResourcesByService(serviceId); } // ============ Backups (delegated to repository) ============ createBackup(backup: Omit): IBackup { return this.backupRepo.create(backup); } getBackupById(id: number): IBackup | null { return this.backupRepo.getById(id); } getBackupsByService(serviceId: number): IBackup[] { return this.backupRepo.getByService(serviceId); } getAllBackups(): IBackup[] { return this.backupRepo.getAll(); } deleteBackup(id: number): void { this.backupRepo.delete(id); } deleteBackupsByService(serviceId: number): void { this.backupRepo.deleteByService(serviceId); } getBackupsBySchedule(scheduleId: number): IBackup[] { return this.backupRepo.getBySchedule(scheduleId); } // ============ Backup Schedules (delegated to repository) ============ createBackupSchedule(schedule: Omit): IBackupSchedule { return this.backupRepo.createSchedule(schedule); } getBackupScheduleById(id: number): IBackupSchedule | null { return this.backupRepo.getScheduleById(id); } getBackupSchedulesByService(serviceId: number): IBackupSchedule[] { return this.backupRepo.getSchedulesByService(serviceId); } getEnabledBackupSchedules(): IBackupSchedule[] { return this.backupRepo.getEnabledSchedules(); } getAllBackupSchedules(): IBackupSchedule[] { return this.backupRepo.getAllSchedules(); } updateBackupSchedule(id: number, updates: IBackupScheduleUpdate & { lastRunAt?: number; nextRunAt?: number; lastStatus?: 'success' | 'failed' | null; lastError?: string | null }): void { this.backupRepo.updateSchedule(id, updates); } deleteBackupSchedule(id: number): void { this.backupRepo.deleteSchedule(id); } deleteBackupSchedulesByService(serviceId: number): void { this.backupRepo.deleteSchedulesByService(serviceId); } }