Files
website/modules/uo/server/model/shardLinks/shardLinks.db.js
wtclaude bf470c7658 spike(modules): carry /public/atlas/* behind the proposed module surface
THROWAWAY BRANCH — evidence for the Phase 1 contract, never merged. See
modules/uo/SPIKE.md and docs/website/MODULE_API.md Part 7.

The six public spawn-atlas routes now live in modules/uo/, reached only through
the ctx/register surface, with the client half loading as a prebuilt ESM chunk.
All three exit criteria met:

  • zero internal-file imports from the module into core; the built chunk has
    zero bare import specifiers and bundles no React
  • routes.manifest.json AND routes.guards.json are byte-identical
  • /uo/atlas renders from /modules/uo/entry.js under script-src 'self' with
    zero CSP violation reports

729 core tests and 81 module tests pass. Verified end to end against the real
database: the schema fragment replays after core's, onBoot runs the atlas
refresh, and the six API URLs answer unchanged.

Two things the spike changed in the contract:

  • ctx.express / ctx.validator. A module lives outside server/, so Node never
    reaches server/node_modules and require('express') fails outright — the
    server-side twin of the one-React rule, which §2.6 had only for the client.
  • window.__rg.jsxRuntime, so a module can build with the automatic JSX
    runtime its tooling already assumes rather than being forced to classic.

And it confirmed §6.1 empirically: regenerating the OpenAPI spec silently
deleted all 361 lines of the atlas paths with "Swagger-autogen: Success", while
the route manifest kept all six in the same run. That is exactly the
static-analysis-vs-runtime split the fragment merge exists to prevent.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-10 05:29:35 -05:00

43 lines
1.6 KiB
JavaScript

const { query } = require('../../core')
const COLS = 'account, user_id, char_name, linked_at'
// Upsert a link. account is the PK, so a re-link moves the account to the new
// user (the sidecar already treats /link/confirm as authoritative).
async function upsert({ account, userId, charName }) {
await query(
`INSERT INTO shard_account_links (account, user_id, char_name)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE user_id = VALUES(user_id), char_name = VALUES(char_name)`,
[account, userId, charName || null],
)
return getByAccount(account)
}
async function getByAccount(account) {
const rows = await query(`SELECT ${COLS} FROM shard_account_links WHERE account = ? LIMIT 1`, [account])
return rows[0] || null
}
const listByUser = (userId) =>
query(`SELECT ${COLS} FROM shard_account_links WHERE user_id = ? ORDER BY linked_at DESC`, [userId])
async function isOwnedBy(account, userId) {
const rows = await query(
'SELECT 1 FROM shard_account_links WHERE account = ? AND user_id = ? LIMIT 1',
[account, userId],
)
return rows.length > 0
}
const remove = (account, userId) =>
query('DELETE FROM shard_account_links WHERE account = ? AND user_id = ?', [account, userId])
// Drop the mirror for an account regardless of which user held it — used to
// reconcile when the tie is severed at the source (an in-game [unlink →
// account.unlinked event, or a site-side DELETE /link/{account}).
const removeByAccount = (account) =>
query('DELETE FROM shard_account_links WHERE account = ?', [account])
module.exports = { upsert, getByAccount, listByUser, isOwnedBy, remove, removeByAccount }