From 610c153e258a3df1d488b114723eeccb2f7f9b9f Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Mon, 2 Mar 2026 22:52:55 +0100 Subject: [PATCH] sb --- src/App/Config.php | 6 +++ src/App/Database.php | 122 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/src/App/Config.php b/src/App/Config.php index 93847dd..6f27a04 100755 --- a/src/App/Config.php +++ b/src/App/Config.php @@ -6,12 +6,18 @@ namespace App; class Config { public string $assetVersion; + public bool $dbAutoInit; + public ?string $dbInitScript; + public ?string $dbInitCmd; public function __construct( public array $db, public bool $dbEnabled = true ) { $this->assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : ''; + $this->dbAutoInit = defined('APP_DB_AUTO_INIT') ? (bool)APP_DB_AUTO_INIT : false; + $this->dbInitScript = defined('APP_DB_INIT_SCRIPT') ? (string)APP_DB_INIT_SCRIPT : null; + $this->dbInitCmd = defined('APP_DB_INIT_CMD') ? (string)APP_DB_INIT_CMD : null; } public function primaryUrl(): string diff --git a/src/App/Database.php b/src/App/Database.php index 678f849..d3f8486 100755 --- a/src/App/Database.php +++ b/src/App/Database.php @@ -43,6 +43,8 @@ final class Database } } + self::ensureSchema($pdo, $config); + return $pdo; } catch (\PDOException $e) { http_response_code(500); @@ -61,6 +63,126 @@ final class Database } } + private static function ensureSchema(\PDO $pdo, Config $config): void + { + $driver = (string)$pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); + if ($driver !== 'pgsql') { + return; + } + + if (!self::tableExists($pdo, 'hosts')) { + if ($config->dbAutoInit) { + self::initKeaSchema($pdo, $config); + } + } + + // After init, ensure our metadata table exists (non-invasive) + if (self::tableExists($pdo, 'hosts')) { + self::ensureNexusTables($pdo); + } + } + + private static function tableExists(\PDO $pdo, string $table): bool + { + $stmt = $pdo->prepare( + "SELECT 1 + FROM information_schema.tables + WHERE table_schema = current_schema() + AND table_name = :table + LIMIT 1" + ); + $stmt->execute(['table' => $table]); + return (bool)$stmt->fetchColumn(); + } + + private static function initKeaSchema(\PDO $pdo, Config $config): void + { + if ($config->dbInitCmd) { + self::runInitCommand($config->dbInitCmd); + return; + } + + if ($config->dbInitScript) { + self::execSqlFile($pdo, $config->dbInitScript); + return; + } + + throw new \RuntimeException( + 'DB auto-init enabled but no APP_DB_INIT_CMD or APP_DB_INIT_SCRIPT configured.' + ); + } + + private static function runInitCommand(string $cmd): void + { + $descriptor = [ + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $process = proc_open($cmd, $descriptor, $pipes); + if (!is_resource($process)) { + throw new \RuntimeException('Failed to start DB init command.'); + } + + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + foreach ($pipes as $pipe) { + fclose($pipe); + } + $code = proc_close($process); + + if ($code !== 0) { + throw new \RuntimeException('DB init command failed: ' . trim($stderr ?: $stdout)); + } + } + + private static function execSqlFile(\PDO $pdo, string $path): void + { + if (!is_readable($path)) { + throw new \RuntimeException('DB init script not readable: ' . $path); + } + + $sql = file_get_contents($path); + if ($sql === false) { + throw new \RuntimeException('Failed to read DB init script: ' . $path); + } + + // Strip psql meta-commands (lines starting with backslash) + $sql = preg_replace('/^\\\\.+$/m', '', $sql); + + $pdo->exec($sql); + } + + private static function ensureNexusTables(\PDO $pdo): void + { + $pdo->exec( + "CREATE TABLE IF NOT EXISTS nexus_host_meta ( + host_id BIGINT PRIMARY KEY, + location TEXT, + device_type TEXT, + owner TEXT, + tags JSONB, + notes TEXT, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + )" + ); + + $pdo->exec( + "DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'fk_nexus_host_meta_host' + ) THEN + ALTER TABLE nexus_host_meta + ADD CONSTRAINT fk_nexus_host_meta_host + FOREIGN KEY (host_id) + REFERENCES hosts(host_id) + ON DELETE CASCADE; + END IF; + END $$;" + ); + } + private static function buildMysqlDsn(array $db): string { if (empty($db['dbname'])) {