The Team provider's db layer built its query helper as `core.db.query(...)`. The
facade has no `db` member -- every other *.db.js in this module destructures
`query` from it directly -- so every call threw `Cannot read properties of
undefined (reading 'query')`.
The failure mode is the bad part. That throw is caught by the provider's own
error handling and turned into `{ ok: false, reason: 'roster unreadable: …' }`,
which is a perfectly valid refusal -- so core would have accepted it, held the
projection it had, and reported staleness. A provider that answers correctly and
never returns data, forever, with nothing in any log louder than a warning.
Invisible to the unit tests because they stub every db function, so the helper
was never called. Found by running a real roster frame through the ingest and
then asking the provider what it saw, against the real database.
Co-Authored-By: Claude <noreply@anthropic.com>
68 lines
2.9 KiB
JavaScript
68 lines
2.9 KiB
JavaScript
// SQL behind the Team provider — three questions core asks, answered from the
|
|
// guild board and the roster Protocol 4 put there.
|
|
//
|
|
// Every statement reads only THIS module's tables. Core's Team tables are
|
|
// core-internal (docs/website/TEAMS.md §10.3) and this module must never name
|
|
// one, even though it is what fills them.
|
|
|
|
// `query` is destructured from the core facade at require time, like every other
|
|
// *.db.js here. The facade resolves `ctx` per call, so taking it now is safe even
|
|
// though `ctx` does not exist yet when this file is first required.
|
|
const { query } = require('../../core')
|
|
|
|
/**
|
|
* The guild board — one row per guild the shard has told us about.
|
|
*
|
|
* `members`/`online` here are the COUNTS `guild.update` carries; the roster is a
|
|
* separate table (Protocol 4). Both are read, because a count is what the shard
|
|
* asserts and a roster is what it enumerated, and they can legitimately disagree
|
|
* for the moment between a membership change and the sweep that reports it.
|
|
*/
|
|
const listGuilds = () =>
|
|
query(
|
|
`SELECT id, name, abbr, alliance, members, online, leader_serial, leader_name, leader_acct
|
|
FROM shard_guilds ORDER BY name ASC`,
|
|
)
|
|
|
|
const findGuild = (id) =>
|
|
query(
|
|
`SELECT id, name, abbr, alliance, members, online, leader_serial, leader_name, leader_acct
|
|
FROM shard_guilds WHERE id = ? LIMIT 1`,
|
|
[id],
|
|
)
|
|
|
|
/**
|
|
* One guild's roster, with the site link and live presence folded in.
|
|
*
|
|
* Two LEFT JOINs, both deliberate:
|
|
*
|
|
* - `shard_account_links` resolves `user_id` HERE rather than in core, because
|
|
* this module owns that table and a core that read it would be core naming a
|
|
* module's table by name (§2.3). It is also why a freshly linked account
|
|
* appears as linked on the next reconcile rather than needing core to know
|
|
* anything about linking.
|
|
* - `shard_online` is how a member's `online` is answered at all. The roster
|
|
* frame does not carry it — the wire's member is the standard actor object
|
|
* (`serial`, `name`, `player`, `acct?`, `webId?`), and the board's `online` is
|
|
* a count, not a set. Presence therefore comes from the online table, which
|
|
* is the same source the public "who's online" surface already uses.
|
|
*
|
|
* `web_id` on the roster row is preferred over the link table when present: it is
|
|
* what the shard itself asserted at roster time, and the join is the fallback for
|
|
* a member whose row predates their link.
|
|
*/
|
|
const listGuildMembers = (guildId) =>
|
|
query(
|
|
`SELECT m.serial, m.name, m.acct, m.web_id, m.is_player,
|
|
l.user_id AS linked_user_id,
|
|
(o.serial IS NOT NULL) AS is_online
|
|
FROM shard_guild_members m
|
|
LEFT JOIN shard_account_links l ON l.account = m.acct
|
|
LEFT JOIN shard_online o ON o.serial = m.serial
|
|
WHERE m.guild_id = ?
|
|
ORDER BY m.name ASC`,
|
|
[guildId],
|
|
)
|
|
|
|
module.exports = { listGuilds, findGuild, listGuildMembers }
|