diff --git a/server/src/model/botConfig/botConfig.db.js b/server/src/model/botConfig/botConfig.db.js index 007e426..7fcf3c6 100644 --- a/server/src/model/botConfig/botConfig.db.js +++ b/server/src/model/botConfig/botConfig.db.js @@ -1,28 +1,7 @@ -const { query } = require('../../utils/db') +const singletonConfigDb = require('../singletonConfigDb') const COLS = 'id, guild_id, bot_token_enc, application_id, enabled, status, status_detail, last_connected_at, updated_by, created_at, updated_at' -// Singleton row (id = 1). Returns null until the admin saves it for the first time. -async function get() { - const rows = await query(`SELECT ${COLS} FROM bot_config WHERE id = 1 LIMIT 1`) - return rows[0] || null -} - -// Upsert the singleton row. `fields` are column values already prepared by the -// model (token pre-encrypted). Only the provided columns are written/updated. -async function upsert(fields) { - const cols = Object.keys(fields) - const vals = cols.map((c) => fields[c]) - const insertCols = ['id', ...cols].map((c) => `\`${c}\``).join(', ') - const placeholders = ['1', ...cols.map(() => '?')].join(', ') - const updates = cols.map((c) => `\`${c}\` = VALUES(\`${c}\`)`).join(', ') - await query( - `INSERT INTO bot_config (${insertCols}) VALUES (${placeholders}) - ON DUPLICATE KEY UPDATE ${updates}`, - vals, - ) - return get() -} - -module.exports = { get, upsert } +// Singleton row (id = 1). See ../singletonConfigDb for the get/upsert contract. +module.exports = singletonConfigDb('bot_config', COLS) diff --git a/server/src/model/emailConfig/emailConfig.db.js b/server/src/model/emailConfig/emailConfig.db.js index 648be48..5927241 100644 --- a/server/src/model/emailConfig/emailConfig.db.js +++ b/server/src/model/emailConfig/emailConfig.db.js @@ -1,28 +1,7 @@ -const { query } = require('../../utils/db') +const singletonConfigDb = require('../singletonConfigDb') const COLS = 'id, provider, enabled, sender_email, sender_name, refresh_token_enc, status, status_detail, last_verified_at, updated_by, created_at, updated_at' -// Singleton row (id = 1). Returns null until the admin connects Gmail for the first time. -async function get() { - const rows = await query(`SELECT ${COLS} FROM email_config WHERE id = 1 LIMIT 1`) - return rows[0] || null -} - -// Upsert the singleton row. `fields` are column values already prepared by the -// model (refresh token pre-encrypted). Only the provided columns are written/updated. -async function upsert(fields) { - const cols = Object.keys(fields) - const vals = cols.map((c) => fields[c]) - const insertCols = ['id', ...cols].map((c) => `\`${c}\``).join(', ') - const placeholders = ['1', ...cols.map(() => '?')].join(', ') - const updates = cols.map((c) => `\`${c}\` = VALUES(\`${c}\`)`).join(', ') - await query( - `INSERT INTO email_config (${insertCols}) VALUES (${placeholders}) - ON DUPLICATE KEY UPDATE ${updates}`, - vals, - ) - return get() -} - -module.exports = { get, upsert } +// Singleton row (id = 1). See ../singletonConfigDb for the get/upsert contract. +module.exports = singletonConfigDb('email_config', COLS) diff --git a/server/src/model/shardState/shardState.db.js b/server/src/model/shardState/shardState.db.js index ac11fdd..c834bb6 100644 --- a/server/src/model/shardState/shardState.db.js +++ b/server/src/model/shardState/shardState.db.js @@ -1,26 +1,37 @@ const { query } = require('../../utils/db') +// Shared upsert builder for the shard-state tables. Each is keyed on a single +// primary column (`pkCol` = pk); `fields` carries only the columns the model +// wants to write, so a partial refresh touches nothing else. `coalesce` keeps +// the prior column value when the incoming one is NULL (used by shard_online so a +// vitals frame that omits acct/name doesn't blank what mob.login set); otherwise +// the incoming value wins (VALUES()). +function upsertRow(table, pkCol, pk, fields, { coalesce = false } = {}) { + const cols = Object.keys(fields) + const allCols = [pkCol, ...cols] + const insertCols = allCols.map((c) => `\`${c}\``).join(', ') + const placeholders = allCols.map(() => '?').join(', ') + const rhs = coalesce + ? (c) => `\`${c}\` = COALESCE(VALUES(\`${c}\`), \`${c}\`)` + : (c) => `\`${c}\` = VALUES(\`${c}\`)` + const updates = cols.map(rhs).join(', ') + return query( + `INSERT INTO ${table} (${insertCols}) VALUES (${placeholders}) + ON DUPLICATE KEY UPDATE ${updates}`, + [pk, ...cols.map((c) => fields[c])], + ) +} + // ── Online players ───────────────────────────────────────────────────────── const ONLINE_COLS = 'serial, name, acct, web_id, map, x, y, z, hits, hits_max, mana, mana_max, stam, stam_max, str, dex, `int`, updated_at' // Upsert one online player. `fields` already prepared by the model (only the // columns it wants to write); serial is required and is the primary key. -async function upsertOnline(serial, fields) { - const cols = Object.keys(fields) - const allCols = ['serial', ...cols] - const insertCols = allCols.map((c) => `\`${c}\``).join(', ') - const placeholders = allCols.map(() => '?').join(', ') - // Never overwrite an existing column with NULL on refresh (a char.vitals frame - // that omits acct/name shouldn't blank what mob.login set) — COALESCE keeps the - // prior value when the incoming one is NULL. - const updates = cols.map((c) => `\`${c}\` = COALESCE(VALUES(\`${c}\`), \`${c}\`)`).join(', ') - await query( - `INSERT INTO shard_online (${insertCols}) VALUES (${placeholders}) - ON DUPLICATE KEY UPDATE ${updates}`, - [serial, ...cols.map((c) => fields[c])], - ) -} +// COALESCE variant: a char.vitals frame that omits acct/name must not blank what +// mob.login set, so an incoming NULL keeps the prior column value. +const upsertOnline = (serial, fields) => + upsertRow('shard_online', 'serial', serial, fields, { coalesce: true }) const removeOnline = (serial) => query('DELETE FROM shard_online WHERE serial = ?', [serial]) const clearOnline = () => query('DELETE FROM shard_online') @@ -89,18 +100,7 @@ async function latestEconomy() { const HOUSE_COLS = 'serial, stage, map, x, y, z, region, name, owner_serial, owner_acct, built_on, last_refreshed, is_idoc, updated_at' -async function upsertHouse(serial, fields) { - const cols = Object.keys(fields) - const allCols = ['serial', ...cols] - const insertCols = allCols.map((c) => `\`${c}\``).join(', ') - const placeholders = allCols.map(() => '?').join(', ') - const updates = cols.map((c) => `\`${c}\` = VALUES(\`${c}\`)`).join(', ') - await query( - `INSERT INTO shard_houses (${insertCols}) VALUES (${placeholders}) - ON DUPLICATE KEY UPDATE ${updates}`, - [serial, ...cols.map((c) => fields[c])], - ) -} +const upsertHouse = (serial, fields) => upsertRow('shard_houses', 'serial', serial, fields) const listIdocHouses = () => query(`SELECT ${HOUSE_COLS} FROM shard_houses WHERE is_idoc = 1 ORDER BY updated_at DESC`) @@ -132,18 +132,7 @@ const listRegistryHouses = () => const CHAMP_COLS = 'serial, category, type, name, status, active, map, x, y, z, boss_up, payload, t, updated_at' -async function upsertChamp(serial, fields) { - const cols = Object.keys(fields) - const allCols = ['serial', ...cols] - const insertCols = allCols.map((c) => `\`${c}\``).join(', ') - const placeholders = allCols.map(() => '?').join(', ') - const updates = cols.map((c) => `\`${c}\` = VALUES(\`${c}\`)`).join(', ') - await query( - `INSERT INTO shard_champs (${insertCols}) VALUES (${placeholders}) - ON DUPLICATE KEY UPDATE ${updates}`, - [serial, ...cols.map((c) => fields[c])], - ) -} +const upsertChamp = (serial, fields) => upsertRow('shard_champs', 'serial', serial, fields) const removeChamp = (serial) => query('DELETE FROM shard_champs WHERE serial = ?', [serial]) const clearChamps = () => query('DELETE FROM shard_champs') @@ -176,18 +165,7 @@ const listPages = () => query(`SELECT ${PAGE_COLS} FROM shard_pages ORDER BY sen const GUILD_COLS = 'id, name, abbr, members, online, alliance, leader_serial, leader_name, leader_acct, leader_web_id, payload, t, updated_at' -async function upsertGuild(id, fields) { - const cols = Object.keys(fields) - const allCols = ['id', ...cols] - const insertCols = allCols.map((c) => `\`${c}\``).join(', ') - const placeholders = allCols.map(() => '?').join(', ') - const updates = cols.map((c) => `\`${c}\` = VALUES(\`${c}\`)`).join(', ') - await query( - `INSERT INTO shard_guilds (${insertCols}) VALUES (${placeholders}) - ON DUPLICATE KEY UPDATE ${updates}`, - [id, ...cols.map((c) => fields[c])], - ) -} +const upsertGuild = (id, fields) => upsertRow('shard_guilds', 'id', id, fields) const removeGuild = (id) => query('DELETE FROM shard_guilds WHERE id = ?', [id]) const clearGuilds = () => query('DELETE FROM shard_guilds') @@ -219,18 +197,7 @@ const listGuildsLedByAccounts = (accounts) => const GOV_COLS = 'city, governor_serial, governor_name, governor_acct, governor_web_id, elect_serial, elect_name, elect_acct, election_phase, candidates, auto_pick_at, payload, t, updated_at' -async function upsertGovernor(city, fields) { - const cols = Object.keys(fields) - const allCols = ['city', ...cols] - const insertCols = allCols.map((c) => `\`${c}\``).join(', ') - const placeholders = allCols.map(() => '?').join(', ') - const updates = cols.map((c) => `\`${c}\` = VALUES(\`${c}\`)`).join(', ') - await query( - `INSERT INTO shard_governors (${insertCols}) VALUES (${placeholders}) - ON DUPLICATE KEY UPDATE ${updates}`, - [city, ...cols.map((c) => fields[c])], - ) -} +const upsertGovernor = (city, fields) => upsertRow('shard_governors', 'city', city, fields) const listGovernors = () => query(`SELECT ${GOV_COLS} FROM shard_governors ORDER BY city ASC`) diff --git a/server/src/model/shardState/shardState.model.js b/server/src/model/shardState/shardState.model.js index e68917a..1cce781 100644 --- a/server/src/model/shardState/shardState.model.js +++ b/server/src/model/shardState/shardState.model.js @@ -86,26 +86,7 @@ async function listOnlineLinked() { async function listOnline() { const rows = await db.listOnline() - return rows.map((r) => ({ - serial: r.serial, - name: r.name, - acct: r.acct, - webId: r.web_id, - map: r.map, - x: r.x, - y: r.y, - z: r.z, - hits: r.hits, - hitsMax: r.hits_max, - mana: r.mana, - manaMax: r.mana_max, - stam: r.stam, - stamMax: r.stam_max, - str: r.str, - dex: r.dex, - int: r.int, - updatedAt: r.updated_at, - })) + return rows.map(shapeOnline) } // Append a gold-supply sample (economy.supply). diff --git a/server/src/model/singletonConfigDb.js b/server/src/model/singletonConfigDb.js new file mode 100644 index 0000000..9bafbb0 --- /dev/null +++ b/server/src/model/singletonConfigDb.js @@ -0,0 +1,35 @@ +const { query } = require('../utils/db') + +// Factory for the singleton config tables (bot_config, email_config, +// uo_link_config). Each is a one-row table keyed on id = 1: `get()` returns the +// row (or null before the admin first saves it), and `upsert()` writes only the +// columns the model prepared, leaving the rest untouched. The three tables share +// this shape exactly, so the DB layer is generated rather than copy-pasted — +// only the table name and column list differ. +// +// `fields` are already prepared by the model (secrets pre-encrypted); ordering +// and encryption stay a model-layer concern. +function singletonConfigDb(table, cols) { + async function get() { + const rows = await query(`SELECT ${cols} FROM ${table} WHERE id = 1 LIMIT 1`) + return rows[0] || null + } + + async function upsert(fields) { + const columns = Object.keys(fields) + const vals = columns.map((c) => fields[c]) + const insertCols = ['id', ...columns].map((c) => `\`${c}\``).join(', ') + const placeholders = ['1', ...columns.map(() => '?')].join(', ') + const updates = columns.map((c) => `\`${c}\` = VALUES(\`${c}\`)`).join(', ') + await query( + `INSERT INTO ${table} (${insertCols}) VALUES (${placeholders}) + ON DUPLICATE KEY UPDATE ${updates}`, + vals, + ) + return get() + } + + return { get, upsert } +} + +module.exports = singletonConfigDb diff --git a/server/src/model/uoLinkConfig/uoLinkConfig.db.js b/server/src/model/uoLinkConfig/uoLinkConfig.db.js index 63f66be..a27d54d 100644 --- a/server/src/model/uoLinkConfig/uoLinkConfig.db.js +++ b/server/src/model/uoLinkConfig/uoLinkConfig.db.js @@ -1,28 +1,7 @@ -const { query } = require('../../utils/db') +const singletonConfigDb = require('../singletonConfigDb') const COLS = 'id, base_url, ws_url, auth_token_enc, protocol, enabled, status, status_detail, plugin_connected, last_event_at, boot_id, updated_by, created_at, updated_at' -// Singleton row (id = 1). Returns null until the admin saves it for the first time. -async function get() { - const rows = await query(`SELECT ${COLS} FROM uo_link_config WHERE id = 1 LIMIT 1`) - return rows[0] || null -} - -// Upsert the singleton row. `fields` are column values already prepared by the -// model (token pre-encrypted). Only the provided columns are written/updated. -async function upsert(fields) { - const cols = Object.keys(fields) - const vals = cols.map((c) => fields[c]) - const insertCols = ['id', ...cols].map((c) => `\`${c}\``).join(', ') - const placeholders = ['1', ...cols.map(() => '?')].join(', ') - const updates = cols.map((c) => `\`${c}\` = VALUES(\`${c}\`)`).join(', ') - await query( - `INSERT INTO uo_link_config (${insertCols}) VALUES (${placeholders}) - ON DUPLICATE KEY UPDATE ${updates}`, - vals, - ) - return get() -} - -module.exports = { get, upsert } +// Singleton row (id = 1). See ../singletonConfigDb for the get/upsert contract. +module.exports = singletonConfigDb('uo_link_config', COLS)