// ── The module's own view of its clans ──────────────────────────────────── // // What `/api/v1/public/clans` serves. Separate from `clanProvider.model.js` // because the two answer to different consumers: the provider answers CORE, in // core's vocabulary, under core's envelope contract; this answers this module's // own page, in the game's vocabulary, under the ordinary rules of an HTTP route. // // **They share the audience rule and nothing else.** `rosterVisibleTo` lives in // the provider and is imported here, because a second copy is a copy that drifts // — and it drifts in the direction that matters, this page publishing a roster // core is withholding. const clanProvider = require('./clanProvider.model') const db = require('./clanProvider.db') /** * Every clan, with its size and nothing else. * * **A list is not a sync, so this does not refuse.** The provider's guard exists * because core makes destructive decisions from a complete answer; a page makes * none. An unreachable game here means the list is as old as it is, and saying so * is `stale` — the same shape `worldStatus` already answers with, for the same * reason. */ async function listPublic() { const [rows, reachable] = await Promise.all([db.listClans(), clanProvider.gameIsReachable()]) return { stale: !reachable.ok, clans: rows.map((row) => ({ externalId: String(row.externalId), name: row.name, abbr: row.abbr || null, memberCount: Number(row.memberCount) || 0, })), } } /** * One clan and its roster, or `null`. * * **What is deliberately not here: `memberKey` and `userId`.** Both are in the * tables and both go to core on the provider's envelope, because core needs an * identity to reconcile against and an account to notify. Neither belongs on a * public page: the member key is the game's internal handle for a character, and * the account id maps a character to a person. Core's own public roster withholds * both whatever `projectRoster` answers — a module route that published them * would route around its own visibility rules while looking like it respected * them. */ async function getPublic(externalId, viewer = null) { const clan = await db.findClan(externalId) if (!clan) return null let members = [] let projected = true try { if (await clanProvider.rosterVisibleTo(externalId, viewer)) { members = (await db.listMembers(externalId)).map((row) => ({ displayName: row.displayName || null, rankLabel: row.rankLabel || null, leader: Boolean(row.isLeader), online: Boolean(row.isOnline), })) } } catch { // Withhold, exactly as the provider does. `projected: false` says which of // the three reasons an empty roster has — no members, an audience that // excludes you, or a question nobody could answer — and a page that cannot // tell them apart will report the last as the first. projected = false } return { externalId: String(clan.externalId), name: clan.name, abbr: clan.abbr || null, memberCount: Number(clan.memberCount) || 0, projected, members, } } module.exports = { listPublic, getPublic }