chore(quality): resolve SonarQube code smells across website
Clears the 124 CODE_SMELL findings from the SonarQube scan (server, client, and bot). All changes are behaviour-preserving refactors — no route, protocol, schema, or config changes — verified against the full server (381) and client (43) test suites plus a clean client build. By rule: - S3776 (20, cognitive complexity): extract helpers/handlers so each function drops under the threshold — shard model upsert builders, page/wiki update, block validation, notification stream mapping (dispatch table), SSO mobile login, shard ingest deps, uo-link socket backfill/connect, the bot slash- command dispatchers + discord manager, and the Shard/UserDetail/HeroEditor/ CharacterStats React components. - S4624 (34, nested template literals): pull inner templates into locals / a withQs() helper; rewrite shardEvents.describe() as a formatter table. - S3358 (35, nested ternaries): lift to if/else vars, lookup maps, small components, or guarded JSX expressions. - S6479 (12, array-index React keys): key by stable content instead of index (two in-editor lists left as-is; index matches their by-index edit model). - S6353 (6): [0-9]/[^0-9] -> \d/\D. S125 (5): reword state-shape comments that parsed as code. S3800/S3782 (botScore): JSDoc-type PATH_WEIGHTS tuples. - S6481 (2): memoize Auth/Site context values (and SiteContext brand). - S4144: dedupe HeroEditor upload handler into useImageUpload(). - S1126 (2), S6035, S5869 (redundant A-Z under /i), S5843 (town-name regex -> prefix list): assorted one-liners. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -130,35 +130,49 @@ function mapMetadata(metadata) {
|
||||
return cols
|
||||
}
|
||||
|
||||
// Per-setting validators — each throws a 400 or returns the accepted value. Split
|
||||
// out of mapSettings so that function stays a flat dispatch (keeps its cognitive
|
||||
// complexity low).
|
||||
function validLayout(v) {
|
||||
if (!LAYOUTS.includes(v)) {
|
||||
throw new PageError(400, 'invalid_settings', `layout must be one of ${LAYOUTS.join(', ')}.`)
|
||||
}
|
||||
return v
|
||||
}
|
||||
function validShowInNav(v) {
|
||||
if (typeof v !== 'boolean') {
|
||||
throw new PageError(400, 'invalid_settings', 'showInNav must be a boolean.')
|
||||
}
|
||||
return v ? 1 : 0
|
||||
}
|
||||
function validNavGroup(v) {
|
||||
if (v !== null && !NAV_GROUPS.includes(v)) {
|
||||
throw new PageError(400, 'invalid_settings', `navGroup must be null or one of ${NAV_GROUPS.join(', ')}.`)
|
||||
}
|
||||
return v
|
||||
}
|
||||
function validNavOrder(v) {
|
||||
if (v !== null && !Number.isInteger(v)) {
|
||||
throw new PageError(400, 'invalid_settings', 'navOrder must be an integer or null.')
|
||||
}
|
||||
return v
|
||||
}
|
||||
function validTitle(v) {
|
||||
if (typeof v !== 'string' || v.trim() === '' || v.length > 200) {
|
||||
throw new PageError(400, 'invalid_title', 'Title is required (max 200 characters).')
|
||||
}
|
||||
return v.trim()
|
||||
}
|
||||
|
||||
// Map the grouped `settings` object to DB columns (except `protected`, which is
|
||||
// handled by the caller so the unprotect rule stays centralized).
|
||||
function mapSettings(settings) {
|
||||
const cols = {}
|
||||
if (!settings || typeof settings !== 'object') return cols
|
||||
if ('layout' in settings) {
|
||||
if (!LAYOUTS.includes(settings.layout)) {
|
||||
throw new PageError(400, 'invalid_settings', `layout must be one of ${LAYOUTS.join(', ')}.`)
|
||||
}
|
||||
cols.layout = settings.layout
|
||||
}
|
||||
if ('showInNav' in settings) {
|
||||
if (typeof settings.showInNav !== 'boolean') {
|
||||
throw new PageError(400, 'invalid_settings', 'showInNav must be a boolean.')
|
||||
}
|
||||
cols.show_in_nav = settings.showInNav ? 1 : 0
|
||||
}
|
||||
if ('navGroup' in settings) {
|
||||
if (settings.navGroup !== null && !NAV_GROUPS.includes(settings.navGroup)) {
|
||||
throw new PageError(400, 'invalid_settings', `navGroup must be null or one of ${NAV_GROUPS.join(', ')}.`)
|
||||
}
|
||||
cols.nav_group = settings.navGroup
|
||||
}
|
||||
if ('navOrder' in settings) {
|
||||
if (settings.navOrder !== null && !Number.isInteger(settings.navOrder)) {
|
||||
throw new PageError(400, 'invalid_settings', 'navOrder must be an integer or null.')
|
||||
}
|
||||
cols.nav_order = settings.navOrder
|
||||
}
|
||||
if ('layout' in settings) cols.layout = validLayout(settings.layout)
|
||||
if ('showInNav' in settings) cols.show_in_nav = validShowInNav(settings.showInNav)
|
||||
if ('navGroup' in settings) cols.nav_group = validNavGroup(settings.navGroup)
|
||||
if ('navOrder' in settings) cols.nav_order = validNavOrder(settings.navOrder)
|
||||
return cols
|
||||
}
|
||||
|
||||
@@ -191,13 +205,10 @@ async function create(input, authorId) {
|
||||
const { slug, title, blocks = [], status = 'draft', metadata, settings } = input
|
||||
assertSlug(slug)
|
||||
assertStatus(status)
|
||||
if (typeof title !== 'string' || title.trim() === '' || title.length > 200) {
|
||||
throw new PageError(400, 'invalid_title', 'Title is required (max 200 characters).')
|
||||
}
|
||||
|
||||
const row = {
|
||||
slug,
|
||||
title: title.trim(),
|
||||
title: validTitle(title),
|
||||
blocks: buildBlocks(blocks),
|
||||
status,
|
||||
author_id: authorId,
|
||||
@@ -219,6 +230,26 @@ async function create(input, authorId) {
|
||||
return getById(id)
|
||||
}
|
||||
|
||||
// Apply the status patch, stamping published_at the first time a page publishes.
|
||||
function applyStatusPatch(fields, status, current) {
|
||||
assertStatus(status)
|
||||
fields.status = status
|
||||
if (status === 'published' && !current.published_at) fields.published_at = new Date()
|
||||
}
|
||||
|
||||
// Protected transitions: turning protection ON is allowed here; turning it OFF is
|
||||
// not (must go through the password-gated unprotect endpoint), regardless of the
|
||||
// request body. Turning OFF while already unprotected is a no-op.
|
||||
function applyProtectedPatch(fields, settings, current) {
|
||||
if (!settings || !('protected' in settings)) return
|
||||
const want = settings.protected
|
||||
if (want === true) {
|
||||
fields.protected = 1
|
||||
} else if (want === false && current.protected) {
|
||||
throw new PageError(403, 'unprotect_required', 'Disabling protection requires the unprotect endpoint.')
|
||||
}
|
||||
}
|
||||
|
||||
async function update(id, patch) {
|
||||
const current = await pagesDb.findById(id)
|
||||
if (!current) throw new PageError(404, 'not_found', 'Page not found.')
|
||||
@@ -230,41 +261,13 @@ async function update(id, patch) {
|
||||
}
|
||||
|
||||
const fields = {}
|
||||
|
||||
if (patch.title !== undefined) {
|
||||
if (typeof patch.title !== 'string' || patch.title.trim() === '' || patch.title.length > 200) {
|
||||
throw new PageError(400, 'invalid_title', 'Title is required (max 200 characters).')
|
||||
}
|
||||
fields.title = patch.title.trim()
|
||||
}
|
||||
|
||||
if (patch.blocks !== undefined) {
|
||||
fields.blocks = buildBlocks(patch.blocks)
|
||||
}
|
||||
|
||||
if (patch.status !== undefined) {
|
||||
assertStatus(patch.status)
|
||||
fields.status = patch.status
|
||||
// Stamp published_at the first time a page becomes published.
|
||||
if (patch.status === 'published' && !current.published_at) {
|
||||
fields.published_at = new Date()
|
||||
}
|
||||
}
|
||||
if (patch.title !== undefined) fields.title = validTitle(patch.title)
|
||||
if (patch.blocks !== undefined) fields.blocks = buildBlocks(patch.blocks)
|
||||
if (patch.status !== undefined) applyStatusPatch(fields, patch.status, current)
|
||||
|
||||
Object.assign(fields, mapMetadata(patch.metadata))
|
||||
Object.assign(fields, mapSettings(patch.settings))
|
||||
|
||||
// Protected transitions: ON is allowed here; OFF is not (must go through the
|
||||
// password-gated unprotect endpoint), regardless of the request body.
|
||||
if (patch.settings && 'protected' in patch.settings) {
|
||||
const want = patch.settings.protected
|
||||
if (want === true) {
|
||||
fields.protected = 1
|
||||
} else if (want === false && current.protected) {
|
||||
throw new PageError(403, 'unprotect_required', 'Disabling protection requires the unprotect endpoint.')
|
||||
}
|
||||
// want === false while already unprotected → no-op.
|
||||
}
|
||||
applyProtectedPatch(fields, patch.settings, current)
|
||||
|
||||
await pagesDb.update(id, fields)
|
||||
return getById(id)
|
||||
|
||||
Reference in New Issue
Block a user