Merge pull request 'refactor(server): dedupe shard-state shaping, upsert builder, and config DB models' (#89) from refactor/dedupe-shardstate-config-db into main
Reviewed-on: #89 Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
This commit is contained in:
@@ -1,28 +1,7 @@
|
|||||||
const { query } = require('../../utils/db')
|
const singletonConfigDb = require('../singletonConfigDb')
|
||||||
|
|
||||||
const COLS =
|
const COLS =
|
||||||
'id, guild_id, bot_token_enc, application_id, enabled, status, status_detail, last_connected_at, updated_by, created_at, updated_at'
|
'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.
|
// Singleton row (id = 1). See ../singletonConfigDb for the get/upsert contract.
|
||||||
async function get() {
|
module.exports = singletonConfigDb('bot_config', COLS)
|
||||||
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 }
|
|
||||||
|
|||||||
@@ -1,28 +1,7 @@
|
|||||||
const { query } = require('../../utils/db')
|
const singletonConfigDb = require('../singletonConfigDb')
|
||||||
|
|
||||||
const COLS =
|
const COLS =
|
||||||
'id, provider, enabled, sender_email, sender_name, refresh_token_enc, status, status_detail, last_verified_at, updated_by, created_at, updated_at'
|
'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.
|
// Singleton row (id = 1). See ../singletonConfigDb for the get/upsert contract.
|
||||||
async function get() {
|
module.exports = singletonConfigDb('email_config', COLS)
|
||||||
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 }
|
|
||||||
|
|||||||
@@ -1,26 +1,37 @@
|
|||||||
const { query } = require('../../utils/db')
|
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 ─────────────────────────────────────────────────────────
|
// ── Online players ─────────────────────────────────────────────────────────
|
||||||
const ONLINE_COLS =
|
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'
|
'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
|
// 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.
|
// columns it wants to write); serial is required and is the primary key.
|
||||||
async function upsertOnline(serial, fields) {
|
// COALESCE variant: a char.vitals frame that omits acct/name must not blank what
|
||||||
const cols = Object.keys(fields)
|
// mob.login set, so an incoming NULL keeps the prior column value.
|
||||||
const allCols = ['serial', ...cols]
|
const upsertOnline = (serial, fields) =>
|
||||||
const insertCols = allCols.map((c) => `\`${c}\``).join(', ')
|
upsertRow('shard_online', 'serial', serial, fields, { coalesce: true })
|
||||||
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])],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeOnline = (serial) => query('DELETE FROM shard_online WHERE serial = ?', [serial])
|
const removeOnline = (serial) => query('DELETE FROM shard_online WHERE serial = ?', [serial])
|
||||||
const clearOnline = () => query('DELETE FROM shard_online')
|
const clearOnline = () => query('DELETE FROM shard_online')
|
||||||
@@ -89,18 +100,7 @@ async function latestEconomy() {
|
|||||||
const HOUSE_COLS =
|
const HOUSE_COLS =
|
||||||
'serial, stage, map, x, y, z, region, name, owner_serial, owner_acct, built_on, last_refreshed, is_idoc, updated_at'
|
'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 upsertHouse = (serial, fields) => upsertRow('shard_houses', 'serial', 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 listIdocHouses = () =>
|
const listIdocHouses = () =>
|
||||||
query(`SELECT ${HOUSE_COLS} FROM shard_houses WHERE is_idoc = 1 ORDER BY updated_at DESC`)
|
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 =
|
const CHAMP_COLS =
|
||||||
'serial, category, type, name, status, active, map, x, y, z, boss_up, payload, t, updated_at'
|
'serial, category, type, name, status, active, map, x, y, z, boss_up, payload, t, updated_at'
|
||||||
|
|
||||||
async function upsertChamp(serial, fields) {
|
const upsertChamp = (serial, fields) => upsertRow('shard_champs', 'serial', 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 removeChamp = (serial) => query('DELETE FROM shard_champs WHERE serial = ?', [serial])
|
const removeChamp = (serial) => query('DELETE FROM shard_champs WHERE serial = ?', [serial])
|
||||||
const clearChamps = () => query('DELETE FROM shard_champs')
|
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 =
|
const GUILD_COLS =
|
||||||
'id, name, abbr, members, online, alliance, leader_serial, leader_name, leader_acct, leader_web_id, payload, t, updated_at'
|
'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 upsertGuild = (id, fields) => upsertRow('shard_guilds', 'id', 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 removeGuild = (id) => query('DELETE FROM shard_guilds WHERE id = ?', [id])
|
const removeGuild = (id) => query('DELETE FROM shard_guilds WHERE id = ?', [id])
|
||||||
const clearGuilds = () => query('DELETE FROM shard_guilds')
|
const clearGuilds = () => query('DELETE FROM shard_guilds')
|
||||||
@@ -219,18 +197,7 @@ const listGuildsLedByAccounts = (accounts) =>
|
|||||||
const GOV_COLS =
|
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'
|
'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 upsertGovernor = (city, fields) => upsertRow('shard_governors', 'city', 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 listGovernors = () => query(`SELECT ${GOV_COLS} FROM shard_governors ORDER BY city ASC`)
|
const listGovernors = () => query(`SELECT ${GOV_COLS} FROM shard_governors ORDER BY city ASC`)
|
||||||
|
|
||||||
|
|||||||
@@ -86,26 +86,7 @@ async function listOnlineLinked() {
|
|||||||
|
|
||||||
async function listOnline() {
|
async function listOnline() {
|
||||||
const rows = await db.listOnline()
|
const rows = await db.listOnline()
|
||||||
return rows.map((r) => ({
|
return rows.map(shapeOnline)
|
||||||
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,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append a gold-supply sample (economy.supply).
|
// Append a gold-supply sample (economy.supply).
|
||||||
|
|||||||
35
server/src/model/singletonConfigDb.js
Normal file
35
server/src/model/singletonConfigDb.js
Normal file
@@ -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
|
||||||
@@ -1,28 +1,7 @@
|
|||||||
const { query } = require('../../utils/db')
|
const singletonConfigDb = require('../singletonConfigDb')
|
||||||
|
|
||||||
const COLS =
|
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'
|
'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.
|
// Singleton row (id = 1). See ../singletonConfigDb for the get/upsert contract.
|
||||||
async function get() {
|
module.exports = singletonConfigDb('uo_link_config', COLS)
|
||||||
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 }
|
|
||||||
|
|||||||
Reference in New Issue
Block a user