// ── SQL for the clan tables ─────────────────────────────────────────────── // // The same `.db.js` / `.model.js` split as `model/worldStatus/`, for the same // reason: 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. // // Everything here reads this module's OWN tables. **Nothing in a module ever // reads or writes `teams`, `team_members`, `team_forum_*` or any other core // table** — core owns the Team, this module owns the clan, and the whole of the // traffic between them is the provider next door answering three questions // (MODULE_API.md §2.6's prefix rule, and §2.7). const core = require('../../core') const CLANS = 'examplegame_clans' const MEMBERS = 'examplegame_clan_members' /** Every clan the game has told us about. */ async function listClans() { return core.query( `SELECT external_id AS externalId, name, abbr, member_count AS memberCount FROM ${CLANS} ORDER BY name`, ) } /** One clan, or `undefined`. */ async function findClan(externalId) { const rows = await core.query( `SELECT external_id AS externalId, name, abbr, member_count AS memberCount FROM ${CLANS} WHERE external_id = ?`, [externalId], ) return rows[0] } /** * One clan's roster. * * Ordered so that a page rendering it directly does not have to sort: leaders * first, then by name. Ordering in SQL rather than in the model is a judgement * call and this is the case for it — the database is doing it on an index, and * the alternative is every caller remembering to. */ async function listMembers(clanId) { return core.query( `SELECT member_key AS memberKey, display_name AS displayName, rank_label AS rankLabel, is_leader AS isLeader, is_online AS isOnline, user_id AS userId FROM ${MEMBERS} WHERE clan_id = ? ORDER BY is_leader DESC, display_name`, [clanId], ) } /** * Replace what we know about one clan, in one transaction-shaped pair of writes. * * Called by whatever ingests from your sidecar; here, by `boot.js`. Delete-then- * insert rather than an upsert, because a roster is a SET and the members who * left are as much a part of the update as the ones who joined — an upsert leaves * departed characters on the roster forever, and core would keep syncing them * into a Team as present members. */ async function replaceClan({ externalId, name, abbr, memberCount, members }) { await core.query( `INSERT INTO ${CLANS} (external_id, name, abbr, member_count, updated_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) ON DUPLICATE KEY UPDATE name = VALUES(name), abbr = VALUES(abbr), member_count = VALUES(member_count), updated_at = CURRENT_TIMESTAMP`, [externalId, name, abbr || null, memberCount], ) await core.query(`DELETE FROM ${MEMBERS} WHERE clan_id = ?`, [externalId]) for (const m of members) { await core.query( `INSERT INTO ${MEMBERS} (clan_id, member_key, display_name, rank_label, is_leader, is_online, user_id) VALUES (?, ?, ?, ?, ?, ?, ?)`, [externalId, m.memberKey, m.displayName || null, m.rankLabel || null, m.leader ? 1 : 0, m.online ? 1 : 0, m.userId || null], ) } } /** * The site accounts behind one clan's roster — the whole of an audience resolver. * * `user_id` is NULL for most characters, and the filter is the point: an audience * resolves to PEOPLE WITH ACCOUNTS, and a character nobody has linked is not one. * Returning its NULL would hand core a hole in an array it is about to mail. * * DISTINCT because one person may hold several characters in the same clan, and * the resolver's contract is a set of users rather than a list of characters. * Without it a three-character player is told three times. */ async function listMemberUserIds(clanId) { const rows = await core.query( `SELECT DISTINCT user_id AS userId FROM ${MEMBERS} WHERE clan_id = ? AND user_id IS NOT NULL`, [clanId], ) return rows.map((r) => r.userId) } module.exports = { listClans, findClan, listMembers, listMemberUserIds, replaceClan, CLANS, MEMBERS }