// ── SQL, and nothing else ───────────────────────────────────────────────── // // Core's own backend is layered `router → controller → model → db`, with models // in pairs: a `.db.js` holding the SQL and a `.model.js` holding the logic that // calls it. The split earns its keep here for the same reason it does in core — // the file with the queries in it has no branching to test, and the file with the // branching in it has no database to stand up. // // Raw parameterised SQL through `core.query`, no ORM. Placeholders always. const core = require('../../core') const SERVERS = 'rust_servers' const STATE = 'rust_server_state' /** * Every configured server, in the operator's own order. * * **The encrypted token comes back on this read and is never returned to a * client.** Decryption happens in the model, one layer up; this file's job is to * fetch a column, not to decide who may see it. */ async function listServers({ enabledOnly = false } = {}) { return core.query( `SELECT id, name, sidecar_base_url AS sidecarBaseUrl, sidecar_token_enc AS sidecarTokenEnc, protocol, enabled, sort_order AS sortOrder, created_at AS createdAt, updated_at AS updatedAt FROM ${SERVERS} ${enabledOnly ? 'WHERE enabled = 1' : ''} ORDER BY sort_order ASC, id ASC`, ) } async function getServer(id) { const rows = await core.query( `SELECT id, name, sidecar_base_url AS sidecarBaseUrl, sidecar_token_enc AS sidecarTokenEnc, protocol, enabled, sort_order AS sortOrder, created_at AS createdAt, updated_at AS updatedAt FROM ${SERVERS} WHERE id = ?`, [id], ) return rows[0] || null } /** * Create or replace a server row. * * **`sidecar_token_enc` is only written when a value is supplied.** An admin form * that shows a blank token field — which is the only thing it can show, since the * token is write-only — posts an empty string on every save that did not intend * to change it. Writing that through would erase the credential every time an * operator renamed a server, and the failure would present as the bridge going * down for no reason an hour after an unrelated edit. */ async function upsertServer({ id, name, sidecarBaseUrl, sidecarTokenEnc, protocol, enabled, sortOrder }) { const setToken = sidecarTokenEnc !== null && sidecarTokenEnc !== undefined await core.query( `INSERT INTO ${SERVERS} (id, name, sidecar_base_url, sidecar_token_enc, protocol, enabled, sort_order, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON DUPLICATE KEY UPDATE name = VALUES(name), sidecar_base_url = VALUES(sidecar_base_url), ${setToken ? 'sidecar_token_enc = VALUES(sidecar_token_enc),' : ''} protocol = VALUES(protocol), enabled = VALUES(enabled), sort_order = VALUES(sort_order), updated_at = CURRENT_TIMESTAMP`, [id, name, sidecarBaseUrl, setToken ? sidecarTokenEnc : null, protocol, enabled ? 1 : 0, sortOrder], ) } async function deleteServer(id) { await core.query(`DELETE FROM ${SERVERS} WHERE id = ?`, [id]) } /** The last thing each server said about itself, keyed by server id. */ async function listState() { return core.query( `SELECT server_id AS serverId, reachable, online, players, max_players AS maxPlayers, hostname, level, seed, world_size AS worldSize, boot_id AS bootId, save_created_at AS saveCreatedAt, protocol, updated_at AS updatedAt FROM ${STATE}`, ) } /** * Replace one server's observed state. * * **`updated_at` is set explicitly, and it has to be.** MariaDB's * `ON UPDATE CURRENT_TIMESTAMP` fires only when an UPDATE actually CHANGES a * value, so an update writing the same numbers back — exactly what a quiet * server looks like — leaves the timestamp where it was. The row would then * cross the freshness window and the page would report the server offline while * it was up and reporting normally. That is invisible to every test and shows up * as a page that was right when you looked at it and wrong an hour later. */ async function putState(state) { await core.query( `INSERT INTO ${STATE} (server_id, reachable, online, players, max_players, hostname, level, seed, world_size, boot_id, save_created_at, protocol, raw, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON DUPLICATE KEY UPDATE reachable = VALUES(reachable), online = VALUES(online), players = VALUES(players), max_players = VALUES(max_players), hostname = VALUES(hostname), level = VALUES(level), seed = VALUES(seed), world_size = VALUES(world_size), boot_id = VALUES(boot_id), save_created_at = VALUES(save_created_at), protocol = VALUES(protocol), raw = VALUES(raw), updated_at = CURRENT_TIMESTAMP`, [ state.serverId, state.reachable ? 1 : 0, state.online ? 1 : 0, state.players || 0, state.maxPlayers || 0, state.hostname || null, state.level || null, state.seed === undefined ? null : state.seed, state.worldSize === undefined ? null : state.worldSize, state.bootId || null, state.saveCreatedAt || null, state.protocol === undefined ? null : state.protocol, state.raw ? JSON.stringify(state.raw) : null, ], ) } module.exports = { SERVERS, STATE, listServers, getServer, upsertServer, deleteServer, listState, putState, }