const express = require('express'); const db = require('../db'); const router = express.Router(); const CHAOS_MIN = 1; const CHAOS_MAX = 9; const listCampaigns = db.prepare(` SELECT campaigns.*, systems.name AS system_name, systems.slug AS system_slug FROM campaigns JOIN systems ON systems.id = campaigns.system_id ORDER BY campaigns.created_at DESC `); const getCampaignById = db.prepare(` SELECT campaigns.*, systems.name AS system_name, systems.slug AS system_slug FROM campaigns JOIN systems ON systems.id = campaigns.system_id WHERE campaigns.id = ? `); const getSystemById = db.prepare('SELECT id FROM systems WHERE id = ?'); const insertCampaign = db.prepare(` INSERT INTO campaigns (name, system_id) VALUES (@name, @system_id) `); const updateCampaignStmt = db.prepare(` UPDATE campaigns SET name = @name, chaos_factor = @chaos_factor, updated_at = datetime('now') WHERE id = @id `); const deleteCampaignCascade = db.transaction((id) => { const cairnCharacterIds = db .prepare('SELECT id FROM characters_cairn WHERE campaign_id = ?') .all(id) .map((row) => row.id); for (const characterId of cairnCharacterIds) { db.prepare('DELETE FROM cairn_inventory WHERE character_id = ?').run(characterId); } const ironswornCharacterIds = db .prepare('SELECT id FROM characters_ironsworn WHERE campaign_id = ?') .all(id) .map((row) => row.id); for (const characterId of ironswornCharacterIds) { db.prepare('DELETE FROM ironsworn_assets WHERE character_id = ?').run(characterId); } const shadowrunCharacterIds = db .prepare('SELECT id FROM characters_shadowrun WHERE campaign_id = ?') .all(id) .map((row) => row.id); for (const characterId of shadowrunCharacterIds) { db.prepare('DELETE FROM shadowrun_skills WHERE character_id = ?').run(characterId); db.prepare('DELETE FROM shadowrun_contacts WHERE character_id = ?').run(characterId); db.prepare('DELETE FROM shadowrun_qualities WHERE character_id = ?').run(characterId); } db.prepare('DELETE FROM characters_dnd5e WHERE campaign_id = ?').run(id); db.prepare('DELETE FROM characters_morkborg WHERE campaign_id = ?').run(id); db.prepare('DELETE FROM characters_cairn WHERE campaign_id = ?').run(id); db.prepare('DELETE FROM characters_chaalt WHERE campaign_id = ?').run(id); db.prepare('DELETE FROM characters_ironsworn WHERE campaign_id = ?').run(id); db.prepare('DELETE FROM characters_shadowrun WHERE campaign_id = ?').run(id); db.prepare('DELETE FROM ironsworn_vows WHERE campaign_id = ?').run(id); db.prepare('DELETE FROM threads WHERE campaign_id = ?').run(id); db.prepare('DELETE FROM npcs WHERE campaign_id = ?').run(id); db.prepare('DELETE FROM session_logs WHERE campaign_id = ?').run(id); db.prepare('DELETE FROM campaign_docs WHERE campaign_id = ?').run(id); db.prepare('DELETE FROM custom_tables WHERE campaign_id = ?').run(id); db.prepare('DELETE FROM campaigns WHERE id = ?').run(id); }); function parseId(rawId, res) { const id = Number(rawId); if (!Number.isInteger(id)) { res.status(400).json({ error: 'id must be an integer' }); return null; } return id; } router.get('/', (req, res) => { res.json(listCampaigns.all()); }); router.post('/', (req, res) => { const { name, system_id: systemId } = req.body; if (typeof name !== 'string' || !name.trim()) { return res.status(400).json({ error: 'name is required' }); } if (!Number.isInteger(systemId)) { return res.status(400).json({ error: 'system_id is required' }); } if (!getSystemById.get(systemId)) { return res.status(400).json({ error: 'system_id does not exist' }); } const result = insertCampaign.run({ name: name.trim(), system_id: systemId }); res.status(201).json(getCampaignById.get(result.lastInsertRowid)); }); router.get('/:id', (req, res) => { const id = parseId(req.params.id, res); if (id === null) return; const campaign = getCampaignById.get(id); if (!campaign) { return res.status(404).json({ error: 'campaign not found' }); } res.json(campaign); }); router.patch('/:id', (req, res) => { const id = parseId(req.params.id, res); if (id === null) return; const existing = getCampaignById.get(id); if (!existing) { return res.status(404).json({ error: 'campaign not found' }); } let name = existing.name; if (req.body.name !== undefined) { name = String(req.body.name).trim(); if (!name) { return res.status(400).json({ error: 'name cannot be empty' }); } } let chaosFactor = existing.chaos_factor; if (req.body.chaos_factor !== undefined) { const parsed = Number(req.body.chaos_factor); if (!Number.isInteger(parsed)) { return res.status(400).json({ error: 'chaos_factor must be an integer' }); } chaosFactor = Math.min(CHAOS_MAX, Math.max(CHAOS_MIN, parsed)); } updateCampaignStmt.run({ id, name, chaos_factor: chaosFactor }); res.json(getCampaignById.get(id)); }); router.delete('/:id', (req, res) => { const id = parseId(req.params.id, res); if (id === null) return; const existing = getCampaignById.get(id); if (!existing) { return res.status(404).json({ error: 'campaign not found' }); } deleteCampaignCascade(id); res.status(204).end(); }); module.exports = router;