This commit is contained in:
2026-03-02 22:52:55 +01:00
parent 98866fd19b
commit 610c153e25
2 changed files with 128 additions and 0 deletions

View File

@@ -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

View File

@@ -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'])) {