fix: add error handling and safe JSON parsing to route handlers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aaron Wood 2026-04-11 00:12:08 -04:00
parent 268997a009
commit 2812d81979
5 changed files with 118 additions and 64 deletions

View file

@ -6,54 +6,74 @@ const router = Router();
// GET /api/campaigns // GET /api/campaigns
router.get("/", async (_req, res) => { router.get("/", async (_req, res) => {
const [rows] = await db.execute<RowDataPacket[]>( try {
"SELECT * FROM campaigns ORDER BY created_at DESC" const [rows] = await db.execute<RowDataPacket[]>(
); "SELECT * FROM campaigns ORDER BY created_at DESC"
res.json(rows); );
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Internal server error" });
}
}); });
// POST /api/campaigns // POST /api/campaigns
router.post("/", async (req, res) => { router.post("/", async (req, res) => {
const { name } = req.body; try {
if (!name?.trim()) { const { name } = req.body;
res.status(400).json({ error: "Campaign name is required" }); if (!name?.trim()) {
return; res.status(400).json({ error: "Campaign name is required" });
return;
}
const [result] = await db.execute<ResultSetHeader>(
"INSERT INTO campaigns (name) VALUES (?)",
[name.trim()]
);
const [rows] = await db.execute<RowDataPacket[]>(
"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<ResultSetHeader>(
"INSERT INTO campaigns (name) VALUES (?)",
[name.trim()]
);
const [rows] = await db.execute<RowDataPacket[]>(
"SELECT * FROM campaigns WHERE id = ?",
[result.insertId]
);
res.status(201).json(rows[0]);
}); });
// GET /api/campaigns/:id // GET /api/campaigns/:id
router.get("/:id", async (req, res) => { router.get("/:id", async (req, res) => {
const [rows] = await db.execute<RowDataPacket[]>( try {
"SELECT * FROM campaigns WHERE id = ?", const [rows] = await db.execute<RowDataPacket[]>(
[req.params.id] "SELECT * FROM campaigns WHERE id = ?",
); [req.params.id]
if (rows.length === 0) { );
res.status(404).json({ error: "Campaign not found" }); if (rows.length === 0) {
return; 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 // DELETE /api/campaigns/:id
router.delete("/:id", async (req, res) => { router.delete("/:id", async (req, res) => {
const [result] = await db.execute<ResultSetHeader>( try {
"DELETE FROM campaigns WHERE id = ?", const [result] = await db.execute<ResultSetHeader>(
[req.params.id] "DELETE FROM campaigns WHERE id = ?",
); [req.params.id]
if (result.affectedRows === 0) { );
res.status(404).json({ error: "Campaign not found" }); if (result.affectedRows === 0) {
return; 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; export default router;

View file

@ -1,19 +1,26 @@
import { Router } from "express"; import { Router } from "express";
import type { RowDataPacket } from "mysql2"; import type { RowDataPacket } from "mysql2";
import db from "../db.js"; import db from "../db.js";
import { parseJson } from "../utils/parseJson.js";
const router = Router(); const router = Router();
// GET /api/game-items
router.get("/", async (_req, res) => { router.get("/", async (_req, res) => {
const [rows] = await db.execute<RowDataPacket[]>( try {
"SELECT * FROM game_items ORDER BY type, name" const [rows] = await db.execute<RowDataPacket[]>(
); "SELECT * FROM game_items ORDER BY type, name"
const parsed = rows.map((item) => ({ );
...item, const parsed = rows.map((item) => ({
effects: JSON.parse(item.effects as string), ...item,
properties: JSON.parse(item.properties as string), effects: parseJson(item.effects),
})); properties: parseJson(item.properties),
res.json(parsed); }));
res.json(parsed);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Internal server error" });
}
}); });
export default router; export default router;

View file

@ -1,18 +1,25 @@
import { Router } from "express"; import { Router } from "express";
import type { RowDataPacket } from "mysql2"; import type { RowDataPacket } from "mysql2";
import db from "../db.js"; import db from "../db.js";
import { parseJson } from "../utils/parseJson.js";
const router = Router(); const router = Router();
// GET /api/game-talents
router.get("/", async (_req, res) => { router.get("/", async (_req, res) => {
const [rows] = await db.execute<RowDataPacket[]>( try {
"SELECT * FROM game_talents ORDER BY source, name" const [rows] = await db.execute<RowDataPacket[]>(
); "SELECT * FROM game_talents ORDER BY source, name"
const parsed = rows.map((t) => ({ );
...t, const parsed = rows.map((t) => ({
effect: JSON.parse(t.effect as string), ...t,
})); effect: parseJson(t.effect),
res.json(parsed); }));
res.json(parsed);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Internal server error" });
}
}); });
export default router; export default router;

View file

@ -1,23 +1,33 @@
import { Router } from "express"; import { Router } from "express";
import type { ParamsDictionary } from "express-serve-static-core";
import type { RowDataPacket } from "mysql2"; import type { RowDataPacket } from "mysql2";
import db from "../db.js"; import db from "../db.js";
import { parseJson } from "../utils/parseJson.js";
type CampaignParams = ParamsDictionary & { campaignId: string };
const router = Router({ mergeParams: true }); const router = Router({ mergeParams: true });
router.get("/", async (req: any, res) => { // GET /api/campaigns/:campaignId/rolls
const { campaignId } = req.params as { campaignId: string }; router.get<CampaignParams>("/", async (req, res) => {
const [rows] = await db.execute<RowDataPacket[]>( try {
"SELECT * FROM roll_log WHERE campaign_id = ? ORDER BY created_at DESC LIMIT 50", const { campaignId } = req.params;
[campaignId] const [rows] = await db.execute<RowDataPacket[]>(
); "SELECT * FROM roll_log WHERE campaign_id = ? ORDER BY created_at DESC LIMIT 50",
const parsed = rows.map((r) => ({ [campaignId]
...r, );
rolls: JSON.parse(r.rolls as string), const parsed = rows.map((r) => ({
advantage: r.advantage === 1, ...r,
disadvantage: r.disadvantage === 1, rolls: (parseJson(r.rolls) as unknown) as unknown[],
nat20: r.nat20 === 1, advantage: r.advantage === 1,
})); disadvantage: r.disadvantage === 1,
res.json(parsed); nat20: r.nat20 === 1,
}));
res.json(parsed);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Internal server error" });
}
}); });
export default router; export default router;

View file

@ -0,0 +1,10 @@
export function parseJson(val: unknown): Record<string, unknown> {
if (typeof val === "string") {
try {
return JSON.parse(val);
} catch {
return {};
}
}
return (val as Record<string, unknown>) ?? {};
}