Files
onebox/ts/classes/database.ts

687 lines
20 KiB
TypeScript
Raw Normal View History

/**
* Database layer for Onebox using SQLite
*/
2025-11-18 00:03:24 +00:00
import * as plugins from '../plugins.ts';
import type {
IService,
IRegistry,
INginxConfig,
ISslCertificate,
IDnsRecord,
IMetric,
ILogEntry,
IUser,
ISetting,
2025-11-18 00:03:24 +00:00
} from '../types.ts';
import { logger } from '../logging.ts';
export class OneboxDatabase {
private db: plugins.sqlite.DB | null = null;
private dbPath: string;
2025-11-18 00:03:24 +00:00
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
await this.runMigrations();
} catch (error) {
logger.error(`Failed to initialize database: ${error.message}`);
throw error;
}
}
/**
* Create all database tables
*/
private async createTables(): Promise<void> {
if (!this.db) throw new Error('Database not initialized');
// Services table
2025-11-18 00:03:24 +00:00
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
2025-11-18 00:03:24 +00:00
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
2025-11-18 00:03:24 +00:00
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
2025-11-18 00:03:24 +00:00
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
2025-11-18 00:03:24 +00:00
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,
2025-11-18 00:36:23 +00:00
zone_id TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Metrics table
2025-11-18 00:03:24 +00:00
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
2025-11-18 00:03:24 +00:00
this.query(`
CREATE INDEX IF NOT EXISTS idx_metrics_service_timestamp
ON metrics(service_id, timestamp DESC)
`);
// Logs table
2025-11-18 00:03:24 +00:00
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
2025-11-18 00:03:24 +00:00
this.query(`
CREATE INDEX IF NOT EXISTS idx_logs_service_timestamp
ON logs(service_id, timestamp DESC)
`);
// Users table
2025-11-18 00:03:24 +00:00
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
2025-11-18 00:03:24 +00:00
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
2025-11-18 00:03:24 +00:00
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
*/
private async runMigrations(): Promise<void> {
if (!this.db) throw new Error('Database not initialized');
const currentVersion = this.getMigrationVersion();
logger.debug(`Current database version: ${currentVersion}`);
// Add migration logic here as needed
// For now, just set version to 1
if (currentVersion === 0) {
this.setMigrationVersion(1);
}
}
/**
* Get current migration version
*/
private getMigrationVersion(): number {
if (!this.db) throw new Error('Database not initialized');
try {
2025-11-18 00:03:24 +00:00
const result = this.query('SELECT MAX(version) as version FROM migrations');
return result.length > 0 && result[0][0] !== null ? Number(result[0][0]) : 0;
} catch {
return 0;
}
}
/**
* Set migration version
*/
private setMigrationVersion(version: number): void {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
this.query('INSERT INTO migrations (version, applied_at) VALUES (?, ?)', [
version,
Date.now(),
]);
logger.debug(`Migration version set to ${version}`);
}
/**
* 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 = unknown[]>(sql: string, params: unknown[] = []): T[] {
2025-11-18 00:03:24 +00:00
if (!this.db) {
const error = new Error('Database not initialized');
console.error('Database access before initialization!');
console.error('Stack trace:', error.stack);
throw error;
}
// For queries without parameters, use exec for better performance
if (params.length === 0 && !sql.trim().toUpperCase().startsWith('SELECT')) {
this.db.exec(sql);
return [] as T[];
}
// For SELECT queries or statements with parameters, use prepare().all()
const stmt = this.db.prepare(sql);
try {
const result = stmt.all(...params);
return result as T[];
} finally {
stmt.finalize();
}
}
// ============ Services CRUD ============
async createService(service: Omit<IService, 'id'>): Promise<IService> {
if (!this.db) throw new Error('Database not initialized');
const now = Date.now();
2025-11-18 00:03:24 +00:00
this.query(
`INSERT INTO services (name, image, registry, env_vars, port, domain, container_id, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
service.name,
service.image,
service.registry || null,
JSON.stringify(service.envVars),
service.port,
service.domain || null,
service.containerID || null,
service.status,
now,
now,
]
);
return this.getServiceByName(service.name)!;
}
getServiceByName(name: string): IService | null {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
const rows = this.query('SELECT * FROM services WHERE name = ?', [name]);
return rows.length > 0 ? this.rowToService(rows[0]) : null;
}
getServiceByID(id: number): IService | null {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
const rows = this.query('SELECT * FROM services WHERE id = ?', [id]);
return rows.length > 0 ? this.rowToService(rows[0]) : null;
}
getAllServices(): IService[] {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
const rows = this.query('SELECT * FROM services ORDER BY created_at DESC');
return rows.map((row) => this.rowToService(row));
}
updateService(id: number, updates: Partial<IService>): void {
if (!this.db) throw new Error('Database not initialized');
const fields: string[] = [];
const values: unknown[] = [];
if (updates.image !== undefined) {
fields.push('image = ?');
values.push(updates.image);
}
if (updates.registry !== undefined) {
fields.push('registry = ?');
values.push(updates.registry);
}
if (updates.envVars !== undefined) {
fields.push('env_vars = ?');
values.push(JSON.stringify(updates.envVars));
}
if (updates.port !== undefined) {
fields.push('port = ?');
values.push(updates.port);
}
if (updates.domain !== undefined) {
fields.push('domain = ?');
values.push(updates.domain);
}
if (updates.containerID !== undefined) {
fields.push('container_id = ?');
values.push(updates.containerID);
}
if (updates.status !== undefined) {
fields.push('status = ?');
values.push(updates.status);
}
fields.push('updated_at = ?');
values.push(Date.now());
values.push(id);
2025-11-18 00:03:24 +00:00
this.query(`UPDATE services SET ${fields.join(', ')} WHERE id = ?`, values);
}
deleteService(id: number): void {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
this.query('DELETE FROM services WHERE id = ?', [id]);
}
private rowToService(row: unknown[]): IService {
return {
id: Number(row[0]),
name: String(row[1]),
image: String(row[2]),
registry: row[3] ? String(row[3]) : undefined,
envVars: JSON.parse(String(row[4])),
port: Number(row[5]),
domain: row[6] ? String(row[6]) : undefined,
containerID: row[7] ? String(row[7]) : undefined,
status: String(row[8]) as IService['status'],
createdAt: Number(row[9]),
updatedAt: Number(row[10]),
};
}
// ============ Registries CRUD ============
async createRegistry(registry: Omit<IRegistry, 'id'>): Promise<IRegistry> {
if (!this.db) throw new Error('Database not initialized');
const now = Date.now();
2025-11-18 00:03:24 +00:00
this.query(
'INSERT INTO registries (url, username, password_encrypted, created_at) VALUES (?, ?, ?, ?)',
[registry.url, registry.username, registry.passwordEncrypted, now]
);
return this.getRegistryByURL(registry.url)!;
}
getRegistryByURL(url: string): IRegistry | null {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
const rows = this.query('SELECT * FROM registries WHERE url = ?', [url]);
return rows.length > 0 ? this.rowToRegistry(rows[0]) : null;
}
getAllRegistries(): IRegistry[] {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
const rows = this.query('SELECT * FROM registries ORDER BY created_at DESC');
return rows.map((row) => this.rowToRegistry(row));
}
deleteRegistry(url: string): void {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
this.query('DELETE FROM registries WHERE url = ?', [url]);
}
private rowToRegistry(row: unknown[]): IRegistry {
return {
id: Number(row[0]),
url: String(row[1]),
username: String(row[2]),
passwordEncrypted: String(row[3]),
createdAt: Number(row[4]),
};
}
// ============ Settings CRUD ============
getSetting(key: string): string | null {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
const rows = this.query('SELECT value FROM settings WHERE key = ?', [key]);
2025-11-18 00:36:23 +00:00
if (rows.length === 0) return null;
// @db/sqlite returns rows as objects with column names as keys
const value = (rows[0] as any).value || rows[0][0];
return value ? String(value) : null;
}
setSetting(key: string, value: string): void {
if (!this.db) throw new Error('Database not initialized');
const now = Date.now();
2025-11-18 00:03:24 +00:00
this.query(
'INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)',
[key, value, now]
);
}
getAllSettings(): Record<string, string> {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
const rows = this.query('SELECT key, value FROM settings');
const settings: Record<string, string> = {};
for (const row of rows) {
2025-11-18 00:03:24 +00:00
// @db/sqlite returns rows as objects with column names as keys
const key = (row as any).key || row[0];
const value = (row as any).value || row[1];
settings[String(key)] = String(value);
}
return settings;
}
// ============ Users CRUD ============
async createUser(user: Omit<IUser, 'id'>): Promise<IUser> {
if (!this.db) throw new Error('Database not initialized');
const now = Date.now();
2025-11-18 00:03:24 +00:00
this.query(
'INSERT INTO users (username, password_hash, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?)',
[user.username, user.passwordHash, user.role, now, now]
);
return this.getUserByUsername(user.username)!;
}
getUserByUsername(username: string): IUser | null {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
const rows = this.query('SELECT * FROM users WHERE username = ?', [username]);
return rows.length > 0 ? this.rowToUser(rows[0]) : null;
}
getAllUsers(): IUser[] {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
const rows = this.query('SELECT * FROM users ORDER BY created_at DESC');
return rows.map((row) => this.rowToUser(row));
}
updateUserPassword(username: string, passwordHash: string): void {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
this.query('UPDATE users SET password_hash = ?, updated_at = ? WHERE username = ?', [
passwordHash,
Date.now(),
username,
]);
}
deleteUser(username: string): void {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
this.query('DELETE FROM users WHERE username = ?', [username]);
}
2025-11-18 00:03:24 +00:00
private rowToUser(row: any): IUser {
return {
2025-11-18 00:03:24 +00:00
id: Number(row.id || row[0]),
username: String(row.username || row[1]),
passwordHash: String(row.password_hash || row[2]),
role: String(row.role || row[3]) as IUser['role'],
createdAt: Number(row.created_at || row[4]),
updatedAt: Number(row.updated_at || row[5]),
};
}
// ============ Metrics ============
addMetric(metric: Omit<IMetric, 'id'>): void {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
this.query(
`INSERT INTO metrics (service_id, timestamp, cpu_percent, memory_used, memory_limit, network_rx_bytes, network_tx_bytes)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
metric.serviceId,
metric.timestamp,
metric.cpuPercent,
metric.memoryUsed,
metric.memoryLimit,
metric.networkRxBytes,
metric.networkTxBytes,
]
);
}
getMetrics(serviceId: number, limit = 100): IMetric[] {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
const rows = this.query(
'SELECT * FROM metrics WHERE service_id = ? ORDER BY timestamp DESC LIMIT ?',
[serviceId, limit]
);
return rows.map((row) => this.rowToMetric(row));
}
private rowToMetric(row: unknown[]): IMetric {
return {
id: Number(row[0]),
serviceId: Number(row[1]),
timestamp: Number(row[2]),
cpuPercent: Number(row[3]),
memoryUsed: Number(row[4]),
memoryLimit: Number(row[5]),
networkRxBytes: Number(row[6]),
networkTxBytes: Number(row[7]),
};
}
// ============ Logs ============
addLog(log: Omit<ILogEntry, 'id'>): void {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
this.query(
'INSERT INTO logs (service_id, timestamp, message, level, source) VALUES (?, ?, ?, ?, ?)',
[log.serviceId, log.timestamp, log.message, log.level, log.source]
);
}
getLogs(serviceId: number, limit = 1000): ILogEntry[] {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
const rows = this.query(
'SELECT * FROM logs WHERE service_id = ? ORDER BY timestamp DESC LIMIT ?',
[serviceId, limit]
);
return rows.map((row) => this.rowToLog(row));
}
private rowToLog(row: unknown[]): ILogEntry {
return {
id: Number(row[0]),
serviceId: Number(row[1]),
timestamp: Number(row[2]),
message: String(row[3]),
level: String(row[4]) as ILogEntry['level'],
source: String(row[5]) as ILogEntry['source'],
};
}
// ============ SSL Certificates ============
async createSSLCertificate(cert: Omit<ISslCertificate, 'id'>): Promise<ISslCertificate> {
if (!this.db) throw new Error('Database not initialized');
const now = Date.now();
2025-11-18 00:03:24 +00:00
this.query(
`INSERT INTO ssl_certificates (domain, cert_path, key_path, full_chain_path, expiry_date, issuer, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
cert.domain,
cert.certPath,
cert.keyPath,
cert.fullChainPath,
cert.expiryDate,
cert.issuer,
now,
now,
]
);
return this.getSSLCertificate(cert.domain)!;
}
getSSLCertificate(domain: string): ISslCertificate | null {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
const rows = this.query('SELECT * FROM ssl_certificates WHERE domain = ?', [domain]);
return rows.length > 0 ? this.rowToSSLCert(rows[0]) : null;
}
getAllSSLCertificates(): ISslCertificate[] {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
const rows = this.query('SELECT * FROM ssl_certificates ORDER BY expiry_date ASC');
return rows.map((row) => this.rowToSSLCert(row));
}
updateSSLCertificate(domain: string, updates: Partial<ISslCertificate>): void {
if (!this.db) throw new Error('Database not initialized');
const fields: string[] = [];
const values: unknown[] = [];
if (updates.certPath) {
fields.push('cert_path = ?');
values.push(updates.certPath);
}
if (updates.keyPath) {
fields.push('key_path = ?');
values.push(updates.keyPath);
}
if (updates.fullChainPath) {
fields.push('full_chain_path = ?');
values.push(updates.fullChainPath);
}
if (updates.expiryDate) {
fields.push('expiry_date = ?');
values.push(updates.expiryDate);
}
fields.push('updated_at = ?');
values.push(Date.now());
values.push(domain);
2025-11-18 00:03:24 +00:00
this.query(`UPDATE ssl_certificates SET ${fields.join(', ')} WHERE domain = ?`, values);
}
deleteSSLCertificate(domain: string): void {
if (!this.db) throw new Error('Database not initialized');
2025-11-18 00:03:24 +00:00
this.query('DELETE FROM ssl_certificates WHERE domain = ?', [domain]);
}
private rowToSSLCert(row: unknown[]): ISslCertificate {
return {
id: Number(row[0]),
domain: String(row[1]),
certPath: String(row[2]),
keyPath: String(row[3]),
fullChainPath: String(row[4]),
expiryDate: Number(row[5]),
issuer: String(row[6]),
createdAt: Number(row[7]),
updatedAt: Number(row[8]),
};
}
}