Files
onebox/ts/database/index.ts
Juergen Kunz 49998c4c32
Some checks failed
CI / Type Check & Lint (push) Failing after 36s
CI / Build Test (Current Platform) (push) Failing after 1m8s
CI / Build All Platforms (push) Successful in 8m29s
add migration
2026-03-15 12:45:13 +00:00

644 lines
18 KiB
TypeScript

/**
* 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<typeof plugins.sqlite.DB> | 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<void> {
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<void> {
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<T = Record<string, unknown>>(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<IService, 'id'>): Promise<IService> {
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<IService>): void {
this.serviceRepo.update(id, updates);
}
deleteService(id: number): void {
this.serviceRepo.delete(id);
}
// ============ Registries CRUD (delegated to repository) ============
async createRegistry(registry: Omit<IRegistry, 'id'>): Promise<IRegistry> {
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<string, string> {
return this.authRepo.getAllSettings();
}
// ============ Users CRUD (delegated to repository) ============
async createUser(user: Omit<IUser, 'id'>): Promise<IUser> {
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<IMetric, 'id'>): void {
this.metricsRepo.addMetric(metric);
}
getMetrics(serviceId: number, limit = 100): IMetric[] {
return this.metricsRepo.getMetrics(serviceId, limit);
}
// ============ Logs (delegated to repository) ============
addLog(log: Omit<ILogEntry, 'id'>): 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<ISslCertificate, 'id'>): Promise<ISslCertificate> {
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<ISslCertificate>): void {
this.certificateRepo.updateSSLCertificate(domain, updates);
}
deleteSSLCertificate(domain: string): void {
this.certificateRepo.deleteSSLCertificate(domain);
}
// ============ Domains (delegated to repository) ============
createDomain(domain: Omit<IDomain, 'id'>): 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<IDomain>): void {
this.certificateRepo.updateDomain(id, updates);
}
deleteDomain(id: number): void {
this.certificateRepo.deleteDomain(id);
}
// ============ Certificates (delegated to repository) ============
createCertificate(cert: Omit<ICertificate, 'id'>): 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<ICertificate>): void {
this.certificateRepo.updateCertificate(id, updates);
}
deleteCertificate(id: number): void {
this.certificateRepo.deleteCertificate(id);
}
// ============ Certificate Requirements (delegated to repository) ============
createCertRequirement(req: Omit<ICertRequirement, 'id'>): 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<ICertRequirement>): void {
this.certificateRepo.updateCertRequirement(id, updates);
}
deleteCertRequirement(id: number): void {
this.certificateRepo.deleteCertRequirement(id);
}
// ============ Registry Tokens (delegated to repository) ============
createRegistryToken(token: Omit<IRegistryToken, 'id'>): 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, 'id'>): 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<IPlatformService>): void {
this.platformRepo.updatePlatformService(id, updates);
}
deletePlatformService(id: number): void {
this.platformRepo.deletePlatformService(id);
}
// ============ Platform Resources (delegated to repository) ============
createPlatformResource(resource: Omit<IPlatformResource, 'id'>): 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, 'id'>): 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, 'id'>): 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);
}
}