From 2812d819790fc18379b550f42b450e301ebfb13d Mon Sep 17 00:00:00 2001 From: Aaron Wood Date: Sat, 11 Apr 2026 00:12:08 -0400 Subject: [PATCH] fix: add error handling and safe JSON parsing to route handlers Co-Authored-By: Claude Sonnet 4.6 --- server/src/routes/campaigns.ts | 86 +++++++++++++++++++------------ server/src/routes/game-items.ts | 25 +++++---- server/src/routes/game-talents.ts | 23 ++++++--- server/src/routes/rolls.ts | 38 +++++++++----- server/src/utils/parseJson.ts | 10 ++++ 5 files changed, 118 insertions(+), 64 deletions(-) create mode 100644 server/src/utils/parseJson.ts diff --git a/server/src/routes/campaigns.ts b/server/src/routes/campaigns.ts index 6a0189a..4bfad16 100644 --- a/server/src/routes/campaigns.ts +++ b/server/src/routes/campaigns.ts @@ -6,54 +6,74 @@ const router = Router(); // GET /api/campaigns router.get("/", async (_req, res) => { - const [rows] = await db.execute( - "SELECT * FROM campaigns ORDER BY created_at DESC" - ); - res.json(rows); + try { + const [rows] = await db.execute( + "SELECT * FROM campaigns ORDER BY created_at DESC" + ); + res.json(rows); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Internal server error" }); + } }); // POST /api/campaigns router.post("/", async (req, res) => { - const { name } = req.body; - if (!name?.trim()) { - res.status(400).json({ error: "Campaign name is required" }); - return; + try { + const { name } = req.body; + if (!name?.trim()) { + res.status(400).json({ error: "Campaign name is required" }); + return; + } + const [result] = await db.execute( + "INSERT INTO campaigns (name) VALUES (?)", + [name.trim()] + ); + const [rows] = await db.execute( + "SELECT * FROM campaigns WHERE id = ?", + [result.insertId] + ); + res.status(201).json(rows[0]); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Internal server error" }); } - const [result] = await db.execute( - "INSERT INTO campaigns (name) VALUES (?)", - [name.trim()] - ); - const [rows] = await db.execute( - "SELECT * FROM campaigns WHERE id = ?", - [result.insertId] - ); - res.status(201).json(rows[0]); }); // GET /api/campaigns/:id router.get("/:id", async (req, res) => { - const [rows] = await db.execute( - "SELECT * FROM campaigns WHERE id = ?", - [req.params.id] - ); - if (rows.length === 0) { - res.status(404).json({ error: "Campaign not found" }); - return; + try { + const [rows] = await db.execute( + "SELECT * FROM campaigns WHERE id = ?", + [req.params.id] + ); + if (rows.length === 0) { + res.status(404).json({ error: "Campaign not found" }); + return; + } + res.json(rows[0]); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Internal server error" }); } - res.json(rows[0]); }); // DELETE /api/campaigns/:id router.delete("/:id", async (req, res) => { - const [result] = await db.execute( - "DELETE FROM campaigns WHERE id = ?", - [req.params.id] - ); - if (result.affectedRows === 0) { - res.status(404).json({ error: "Campaign not found" }); - return; + try { + const [result] = await db.execute( + "DELETE FROM campaigns WHERE id = ?", + [req.params.id] + ); + if (result.affectedRows === 0) { + res.status(404).json({ error: "Campaign not found" }); + return; + } + res.status(204).end(); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Internal server error" }); } - res.status(204).end(); }); export default router; diff --git a/server/src/routes/game-items.ts b/server/src/routes/game-items.ts index e718d10..cb48c77 100644 --- a/server/src/routes/game-items.ts +++ b/server/src/routes/game-items.ts @@ -1,19 +1,26 @@ import { Router } from "express"; import type { RowDataPacket } from "mysql2"; import db from "../db.js"; +import { parseJson } from "../utils/parseJson.js"; const router = Router(); +// GET /api/game-items router.get("/", async (_req, res) => { - const [rows] = await db.execute( - "SELECT * FROM game_items ORDER BY type, name" - ); - const parsed = rows.map((item) => ({ - ...item, - effects: JSON.parse(item.effects as string), - properties: JSON.parse(item.properties as string), - })); - res.json(parsed); + try { + const [rows] = await db.execute( + "SELECT * FROM game_items ORDER BY type, name" + ); + const parsed = rows.map((item) => ({ + ...item, + effects: parseJson(item.effects), + properties: parseJson(item.properties), + })); + res.json(parsed); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Internal server error" }); + } }); export default router; diff --git a/server/src/routes/game-talents.ts b/server/src/routes/game-talents.ts index b90f724..a334540 100644 --- a/server/src/routes/game-talents.ts +++ b/server/src/routes/game-talents.ts @@ -1,18 +1,25 @@ import { Router } from "express"; import type { RowDataPacket } from "mysql2"; import db from "../db.js"; +import { parseJson } from "../utils/parseJson.js"; const router = Router(); +// GET /api/game-talents router.get("/", async (_req, res) => { - const [rows] = await db.execute( - "SELECT * FROM game_talents ORDER BY source, name" - ); - const parsed = rows.map((t) => ({ - ...t, - effect: JSON.parse(t.effect as string), - })); - res.json(parsed); + try { + const [rows] = await db.execute( + "SELECT * FROM game_talents ORDER BY source, name" + ); + const parsed = rows.map((t) => ({ + ...t, + effect: parseJson(t.effect), + })); + res.json(parsed); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Internal server error" }); + } }); export default router; diff --git a/server/src/routes/rolls.ts b/server/src/routes/rolls.ts index 6ae6717..add2df7 100644 --- a/server/src/routes/rolls.ts +++ b/server/src/routes/rolls.ts @@ -1,23 +1,33 @@ import { Router } from "express"; +import type { ParamsDictionary } from "express-serve-static-core"; import type { RowDataPacket } from "mysql2"; import db from "../db.js"; +import { parseJson } from "../utils/parseJson.js"; + +type CampaignParams = ParamsDictionary & { campaignId: string }; const router = Router({ mergeParams: true }); -router.get("/", async (req: any, res) => { - const { campaignId } = req.params as { campaignId: string }; - const [rows] = await db.execute( - "SELECT * FROM roll_log WHERE campaign_id = ? ORDER BY created_at DESC LIMIT 50", - [campaignId] - ); - const parsed = rows.map((r) => ({ - ...r, - rolls: JSON.parse(r.rolls as string), - advantage: r.advantage === 1, - disadvantage: r.disadvantage === 1, - nat20: r.nat20 === 1, - })); - res.json(parsed); +// GET /api/campaigns/:campaignId/rolls +router.get("/", async (req, res) => { + try { + const { campaignId } = req.params; + const [rows] = await db.execute( + "SELECT * FROM roll_log WHERE campaign_id = ? ORDER BY created_at DESC LIMIT 50", + [campaignId] + ); + const parsed = rows.map((r) => ({ + ...r, + rolls: (parseJson(r.rolls) as unknown) as unknown[], + advantage: r.advantage === 1, + disadvantage: r.disadvantage === 1, + nat20: r.nat20 === 1, + })); + res.json(parsed); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Internal server error" }); + } }); export default router; diff --git a/server/src/utils/parseJson.ts b/server/src/utils/parseJson.ts new file mode 100644 index 0000000..831aaf6 --- /dev/null +++ b/server/src/utils/parseJson.ts @@ -0,0 +1,10 @@ +export function parseJson(val: unknown): Record { + if (typeof val === "string") { + try { + return JSON.parse(val); + } catch { + return {}; + } + } + return (val as Record) ?? {}; +}