const express = require('express'); const db = require('../db'); const router = express.Router({ mergeParams: true }); const STATUSES = ['active', 'resolved', 'suspended', 'complicated']; const OPTIONAL_TEXT_FIELDS = [ 'notes', 'related_npcs', 'related_location', 'origin', 'stakes', 'last_development', 'next_beat', 'suspected_resolution', ]; const getCampaignByIdStmt = db.prepare('SELECT id FROM campaigns WHERE id = ?'); const listThreadsStmt = db.prepare('SELECT * FROM threads WHERE campaign_id = ? ORDER BY created_at DESC'); const getThreadStmt = db.prepare('SELECT * FROM threads WHERE id = ? AND campaign_id = ?'); const insertThreadStmt = db.prepare(` INSERT INTO threads ( campaign_id, title, status, notes, related_npcs, related_location, origin, stakes, last_development, next_beat, suspected_resolution ) VALUES ( @campaign_id, @title, @status, @notes, @related_npcs, @related_location, @origin, @stakes, @last_development, @next_beat, @suspected_resolution ) `); const updateThreadStmt = db.prepare(` UPDATE threads SET title = @title, status = @status, notes = @notes, related_npcs = @related_npcs, related_location = @related_location, origin = @origin, stakes = @stakes, last_development = @last_development, next_beat = @next_beat, suspected_resolution = @suspected_resolution WHERE id = @id AND campaign_id = @campaign_id `); const deleteThreadStmt = db.prepare('DELETE FROM threads WHERE id = ? AND campaign_id = ?'); function parsePositiveInt(raw, res, label) { const value = Number(raw); if (!Number.isInteger(value) || value <= 0) { res.status(400).json({ error: `${label} must be a positive integer` }); return null; } return value; } function loadCampaignId(req, res) { const campaignId = parsePositiveInt(req.params.campaignId, res, 'campaign_id'); if (campaignId === null) return null; if (!getCampaignByIdStmt.get(campaignId)) { res.status(404).json({ error: 'campaign not found' }); return null; } return campaignId; } // Shared by create and update — PUT sends the full form, so both treat // title/status the same way rather than supporting partial patches. function parseThreadPayload(body, res) { const title = typeof body.title === 'string' ? body.title.trim() : ''; if (!title) { res.status(400).json({ error: 'title is required' }); return null; } const status = body.status === undefined ? 'active' : body.status; if (!STATUSES.includes(status)) { res.status(400).json({ error: `status must be one of: ${STATUSES.join(', ')}` }); return null; } const payload = { title, status }; for (const field of OPTIONAL_TEXT_FIELDS) { const value = body[field]; if (value === undefined || value === null) { payload[field] = null; } else if (typeof value === 'string') { payload[field] = value; } else { res.status(400).json({ error: `${field} must be a string or null` }); return null; } } return payload; } function withErrorHandling(handler) { return (req, res) => { try { handler(req, res); } catch (err) { console.error(err); res.status(500).json({ error: 'unexpected server error' }); } }; } router.get('/', withErrorHandling((req, res) => { const campaignId = loadCampaignId(req, res); if (campaignId === null) return; res.json({ data: listThreadsStmt.all(campaignId) }); })); router.get('/:id', withErrorHandling((req, res) => { const campaignId = loadCampaignId(req, res); if (campaignId === null) return; const id = parsePositiveInt(req.params.id, res, 'id'); if (id === null) return; const thread = getThreadStmt.get(id, campaignId); if (!thread) { return res.status(404).json({ error: 'thread not found' }); } res.json({ data: thread }); })); router.post('/', withErrorHandling((req, res) => { const campaignId = loadCampaignId(req, res); if (campaignId === null) return; const payload = parseThreadPayload(req.body, res); if (payload === null) return; const result = insertThreadStmt.run({ ...payload, campaign_id: campaignId }); res.status(201).json({ data: getThreadStmt.get(result.lastInsertRowid, campaignId) }); })); router.put('/:id', withErrorHandling((req, res) => { const campaignId = loadCampaignId(req, res); if (campaignId === null) return; const id = parsePositiveInt(req.params.id, res, 'id'); if (id === null) return; if (!getThreadStmt.get(id, campaignId)) { return res.status(404).json({ error: 'thread not found' }); } const payload = parseThreadPayload(req.body, res); if (payload === null) return; updateThreadStmt.run({ ...payload, id, campaign_id: campaignId }); res.json({ data: getThreadStmt.get(id, campaignId) }); })); router.delete('/:id', withErrorHandling((req, res) => { const campaignId = loadCampaignId(req, res); if (campaignId === null) return; const id = parsePositiveInt(req.params.id, res, 'id'); if (id === null) return; if (!getThreadStmt.get(id, campaignId)) { return res.status(404).json({ error: 'thread not found' }); } deleteThreadStmt.run(id, campaignId); res.json({ data: { id } }); })); module.exports = router;