Files
Module-uo/server/utils/announceLinks.js
wtclaude fe3251a543 feat(server): port the UO models, utils and schema fragment
The data half of the extraction: 8 model directories, 13 utils, the shard
stream catalog and the 27-table schema fragment with its purge.

server/core.js is what makes the port a one-line import change per file rather
than a signature change per function. Ported code requires its dependencies at
file scope -- `const { query } = require('../../core')` -- which runs before
register() has been called and before any ctx exists. So every member is a
stable function that resolves ctx when CALLED, and nothing may be destructured
off ctx at init either, because core is free to hand over a getter.

Two helpers are vendored rather than taken from ctx, and the line between them
is the point. utils/excerpt.js is core's deriveExcerpt -- nine lines of pure
text handling. Core's sanitiser next to it was NOT copied: a second copy of a
security control diverges silently the moment either is fixed. announceLinks.js
vendors legError and articleUrl the same way, but baseUrl could not be: core's
reads APP_BASE_URL, and §2.7 forbids a module reading core's environment, so it
comes off ctx.site.baseUrl.

The schema fragment is core's 27 shard_*/uo_link_* statements, verbs CREATE,
ALTER and UPDATE only, every CREATE TABLE guarded. Two of its tables carry a
foreign key INTO users, which is allowed and is why the replay order matters --
core's schema is in place before this runs. The reverse never occurs and must
not: it would make core unable to boot without a module installed.

One real port bug caught by the integration run, not by tests: the atlas art
map resolved `../../../db/data`, which pointed at core's tree when this file
lived there and points outside server/ now. A path that happens to resolve is
exactly what survives a green suite, because the absent-file branch returns {}
and looks like the normal case.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 12:06:26 -05:00

53 lines
2.2 KiB
JavaScript

// The three helpers the town-crier leg needs from core's announce pipeline.
//
// Core owns `announce_jobs`, the worker that drains it and the retry policy;
// this module owns one leg of it (MODULE_API.md §2.4). These three lived in
// core's `announceJobs.logic` and are reproduced here rather than added to
// `ctx`, because each is a few lines of pure string handling with no state and
// no policy — the kind of thing a contract member would only make harder to
// change on both sides.
//
// The one that could NOT be vendored is `baseUrl`. Core's version reads
// `process.env.APP_BASE_URL`, and §2.7 forbids a module reading core's
// environment — it is core's deployment fact, not the module's. So it comes off
// `ctx.site.baseUrl` (API 1.1.0), read per call rather than captured, which also
// means a module built before an env change keeps agreeing with core after it.
// NOT destructured. `core.baseUrl` is a getter that resolves `ctx`, so pulling
// it out here would run at require time — before `register()` — and throw. Read
// it inside the function, where `ctx` exists.
const core = require('../core')
/** Where this deployment is reachable, without a trailing slash. */
function baseUrl() {
return core.baseUrl
}
/**
* The public link that goes in an announcement.
*
* News has no per-post route — core's SPA has only the list — so this links the
* list, matching what the pre-pipeline Discord announce did. It names a CORE
* route on purpose: the news list is core's page and stays core's through the
* whole extraction, so this is a module linking to its host, not a leftover.
*/
function articleUrl(base) {
return `${String(base || '').replace(/\/+$/, '')}/site/news`
}
/**
* Squeeze a leg client's `{ ok, status, data, error }` into the one line stored
* in `announce_job_legs.last_error` and shown in the admin panel.
*/
function legError(result) {
if (!result) return 'no response'
if (result.status) {
return result.data && result.data.message
? `${result.status}: ${result.data.message}`
: result.error || `status ${result.status}`
}
return result.error || 'request failed'
}
module.exports = { baseUrl, articleUrl, legError }