3a7340975f
Campaign CRUD API with cascading delete across all campaign-scoped tables, a systems endpoint to seed the create-campaign form, active campaign state wired to the sidebar chaos factor, and a campaign management view for creating/switching/deleting campaigns.
165 lines
5.2 KiB
JavaScript
165 lines
5.2 KiB
JavaScript
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;
|