feat: convert characters routes to async mysql2
Replace synchronous better-sqlite3 calls with async mysql2 db.execute(), import parseJson from shared utility, and add try/catch error handling throughout all character, gear, talent, and stat endpoints. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2812d81979
commit
385d9b6e9e
1 changed files with 343 additions and 333 deletions
|
|
@ -1,8 +1,10 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import type { ParamsDictionary } from "express-serve-static-core";
|
import type { ParamsDictionary } from "express-serve-static-core";
|
||||||
|
import type { RowDataPacket, ResultSetHeader } from "mysql2";
|
||||||
import type { Server } from "socket.io";
|
import type { Server } from "socket.io";
|
||||||
import db from "../db.js";
|
import db from "../db.js";
|
||||||
import { broadcastToCampaign } from "../socket.js";
|
import { broadcastToCampaign } from "../socket.js";
|
||||||
|
import { parseJson } from "../utils/parseJson.js";
|
||||||
|
|
||||||
type CampaignParams = ParamsDictionary & { campaignId: string };
|
type CampaignParams = ParamsDictionary & { campaignId: string };
|
||||||
|
|
||||||
|
|
@ -15,18 +17,7 @@ function generateCharacterColor(): string {
|
||||||
return `hsl(${hue}, 60%, 65%)`;
|
return `hsl(${hue}, 60%, 65%)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseJson(val: unknown): Record<string, unknown> {
|
function parseGear(rows: RowDataPacket[]) {
|
||||||
if (typeof val === "string") {
|
|
||||||
try {
|
|
||||||
return JSON.parse(val);
|
|
||||||
} catch {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (val as Record<string, unknown>) ?? {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseGear(rows: Array<Record<string, unknown>>) {
|
|
||||||
return rows.map((r) => ({
|
return rows.map((r) => ({
|
||||||
...r,
|
...r,
|
||||||
properties: parseJson(r.properties),
|
properties: parseJson(r.properties),
|
||||||
|
|
@ -34,360 +25,379 @@ function parseGear(rows: Array<Record<string, unknown>>) {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTalents(rows: Array<Record<string, unknown>>) {
|
function parseTalents(rows: RowDataPacket[]) {
|
||||||
return rows.map((r) => ({ ...r, effect: parseJson(r.effect) }));
|
return rows.map((r) => ({ ...r, effect: parseJson(r.effect) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/campaigns/:campaignId/characters — list characters in a campaign
|
async function enrichCharacters(characters: RowDataPacket[]) {
|
||||||
router.get<CampaignParams>("/", (req, res) => {
|
return Promise.all(
|
||||||
const { campaignId } = req.params;
|
characters.map(async (char) => {
|
||||||
const characters = db
|
const [stats] = await db.execute<RowDataPacket[]>(
|
||||||
.prepare("SELECT * FROM characters WHERE campaign_id = ? ORDER BY name")
|
"SELECT stat_name, value FROM character_stats WHERE character_id = ?",
|
||||||
.all(campaignId) as Array<Record<string, unknown>>;
|
[char.id]
|
||||||
|
|
||||||
const stmtStats = db.prepare(
|
|
||||||
"SELECT stat_name, value FROM character_stats WHERE character_id = ?",
|
|
||||||
);
|
|
||||||
const stmtGear = db.prepare(
|
|
||||||
"SELECT * FROM character_gear WHERE character_id = ?",
|
|
||||||
);
|
|
||||||
const stmtTalents = db.prepare(
|
|
||||||
"SELECT * FROM character_talents WHERE character_id = ?",
|
|
||||||
);
|
|
||||||
|
|
||||||
const enriched = characters.map((char) => ({
|
|
||||||
...char,
|
|
||||||
overrides: parseJson(char.overrides),
|
|
||||||
stats: stmtStats.all(char.id),
|
|
||||||
gear: parseGear(stmtGear.all(char.id) as Array<Record<string, unknown>>),
|
|
||||||
talents: parseTalents(
|
|
||||||
stmtTalents.all(char.id) as Array<Record<string, unknown>>,
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
res.json(enriched);
|
|
||||||
});
|
|
||||||
|
|
||||||
// POST /api/campaigns/:campaignId/characters — create a character
|
|
||||||
router.post<CampaignParams>("/", (req, res) => {
|
|
||||||
const { campaignId } = req.params;
|
|
||||||
const { name, class: charClass, ancestry, hp_max } = req.body;
|
|
||||||
|
|
||||||
if (!name || !name.trim()) {
|
|
||||||
res.status(400).json({ error: "Character name is required" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertChar = db.prepare(`
|
|
||||||
INSERT INTO characters (campaign_id, name, class, ancestry, hp_current, hp_max, color)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
||||||
`);
|
|
||||||
|
|
||||||
const insertStat = db.prepare(
|
|
||||||
"INSERT INTO character_stats (character_id, stat_name, value) VALUES (?, ?, 10)",
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = insertChar.run(
|
|
||||||
campaignId,
|
|
||||||
name.trim(),
|
|
||||||
charClass || "Fighter",
|
|
||||||
ancestry || "Human",
|
|
||||||
hp_max || 0,
|
|
||||||
hp_max || 0,
|
|
||||||
generateCharacterColor(),
|
|
||||||
);
|
|
||||||
const characterId = result.lastInsertRowid;
|
|
||||||
|
|
||||||
for (const stat of DEFAULT_STATS) {
|
|
||||||
insertStat.run(characterId, stat);
|
|
||||||
}
|
|
||||||
|
|
||||||
const character = db
|
|
||||||
.prepare("SELECT * FROM characters WHERE id = ?")
|
|
||||||
.get(characterId);
|
|
||||||
const stats = db
|
|
||||||
.prepare(
|
|
||||||
"SELECT stat_name, value FROM character_stats WHERE character_id = ?",
|
|
||||||
)
|
|
||||||
.all(characterId);
|
|
||||||
|
|
||||||
const enriched = {
|
|
||||||
...(character as Record<string, unknown>),
|
|
||||||
overrides: {},
|
|
||||||
stats,
|
|
||||||
gear: [],
|
|
||||||
talents: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const io: Server = req.app.get("io");
|
|
||||||
broadcastToCampaign(io, Number(campaignId), "character:created", enriched);
|
|
||||||
|
|
||||||
res.status(201).json(enriched);
|
|
||||||
});
|
|
||||||
|
|
||||||
// PATCH /api/characters/:id — update character fields
|
|
||||||
router.patch("/:id", (req, res) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
const allowedFields = [
|
|
||||||
"name",
|
|
||||||
"class",
|
|
||||||
"ancestry",
|
|
||||||
"level",
|
|
||||||
"xp",
|
|
||||||
"hp_current",
|
|
||||||
"hp_max",
|
|
||||||
"ac",
|
|
||||||
"alignment",
|
|
||||||
"title",
|
|
||||||
"notes",
|
|
||||||
"background",
|
|
||||||
"deity",
|
|
||||||
"languages",
|
|
||||||
"gp",
|
|
||||||
"sp",
|
|
||||||
"cp",
|
|
||||||
"gear_slots_max",
|
|
||||||
"overrides",
|
|
||||||
"color",
|
|
||||||
"luck_token",
|
|
||||||
"torch_lit_at",
|
|
||||||
];
|
|
||||||
|
|
||||||
const updates: string[] = [];
|
|
||||||
const values: unknown[] = [];
|
|
||||||
|
|
||||||
for (const field of allowedFields) {
|
|
||||||
if (req.body[field] !== undefined) {
|
|
||||||
updates.push(`${field} = ?`);
|
|
||||||
const val = req.body[field];
|
|
||||||
// JSON-stringify object fields for SQLite TEXT storage
|
|
||||||
values.push(
|
|
||||||
typeof val === "object" && val !== null ? JSON.stringify(val) : val,
|
|
||||||
);
|
);
|
||||||
}
|
const [gear] = await db.execute<RowDataPacket[]>(
|
||||||
}
|
"SELECT * FROM character_gear WHERE character_id = ?",
|
||||||
|
[char.id]
|
||||||
if (updates.length === 0) {
|
);
|
||||||
res.status(400).json({ error: "No valid fields to update" });
|
const [talents] = await db.execute<RowDataPacket[]>(
|
||||||
return;
|
"SELECT * FROM character_talents WHERE character_id = ?",
|
||||||
}
|
[char.id]
|
||||||
|
);
|
||||||
values.push(id);
|
return {
|
||||||
db.prepare(`UPDATE characters SET ${updates.join(", ")} WHERE id = ?`).run(
|
...char,
|
||||||
...values,
|
overrides: parseJson(char.overrides),
|
||||||
|
stats,
|
||||||
|
gear: parseGear(gear),
|
||||||
|
talents: parseTalents(talents),
|
||||||
|
};
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const character = db
|
// GET /api/campaigns/:campaignId/characters
|
||||||
.prepare("SELECT * FROM characters WHERE id = ?")
|
router.get<CampaignParams>("/", async (req, res) => {
|
||||||
.get(id) as Record<string, unknown>;
|
try {
|
||||||
if (!character) {
|
const { campaignId } = req.params;
|
||||||
res.status(404).json({ error: "Character not found" });
|
const [characters] = await db.execute<RowDataPacket[]>(
|
||||||
return;
|
"SELECT * FROM characters WHERE campaign_id = ? ORDER BY name",
|
||||||
|
[campaignId]
|
||||||
|
);
|
||||||
|
const enriched = await enrichCharacters(characters);
|
||||||
|
res.json(enriched);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: "Internal server error" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const io: Server = req.app.get("io");
|
|
||||||
broadcastToCampaign(io, Number(character.campaign_id), "character:updated", {
|
|
||||||
id: Number(id),
|
|
||||||
...req.body,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json(character);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// DELETE /api/characters/:id — delete a character
|
// POST /api/campaigns/:campaignId/characters
|
||||||
router.delete("/:id", (req, res) => {
|
router.post<CampaignParams>("/", async (req, res) => {
|
||||||
const character = db
|
try {
|
||||||
.prepare("SELECT * FROM characters WHERE id = ?")
|
const { campaignId } = req.params;
|
||||||
.get(req.params.id) as Record<string, unknown> | undefined;
|
const { name, class: charClass, ancestry, hp_max } = req.body;
|
||||||
if (!character) {
|
|
||||||
res.status(404).json({ error: "Character not found" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
db.prepare("DELETE FROM characters WHERE id = ?").run(req.params.id);
|
if (!name?.trim()) {
|
||||||
|
res.status(400).json({ error: "Character name is required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const io: Server = req.app.get("io");
|
const [result] = await db.execute<ResultSetHeader>(
|
||||||
broadcastToCampaign(io, Number(character.campaign_id), "character:deleted", {
|
`INSERT INTO characters
|
||||||
id: Number(req.params.id),
|
(campaign_id, name, class, ancestry, hp_current, hp_max, color)
|
||||||
});
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
[
|
||||||
|
campaignId,
|
||||||
|
name.trim(),
|
||||||
|
charClass ?? "Fighter",
|
||||||
|
ancestry ?? "Human",
|
||||||
|
hp_max ?? 0,
|
||||||
|
hp_max ?? 0,
|
||||||
|
generateCharacterColor(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
const characterId = result.insertId;
|
||||||
|
|
||||||
res.status(204).end();
|
await Promise.all(
|
||||||
});
|
DEFAULT_STATS.map((stat) =>
|
||||||
|
db.execute(
|
||||||
// PATCH /api/characters/:id/stats/:statName — update a single stat
|
"INSERT INTO character_stats (character_id, stat_name, value) VALUES (?, ?, 10)",
|
||||||
router.patch("/:id/stats/:statName", (req, res) => {
|
[characterId, stat]
|
||||||
const { id, statName } = req.params;
|
)
|
||||||
const { value } = req.body;
|
)
|
||||||
const upper = statName.toUpperCase();
|
|
||||||
|
|
||||||
if (!DEFAULT_STATS.includes(upper)) {
|
|
||||||
res.status(400).json({ error: "Invalid stat name" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
db.prepare(
|
|
||||||
"UPDATE character_stats SET value = ? WHERE character_id = ? AND stat_name = ?",
|
|
||||||
).run(value, id, upper);
|
|
||||||
|
|
||||||
const character = db
|
|
||||||
.prepare("SELECT campaign_id FROM characters WHERE id = ?")
|
|
||||||
.get(id) as Record<string, unknown>;
|
|
||||||
if (!character) {
|
|
||||||
res.status(404).json({ error: "Character not found" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const io: Server = req.app.get("io");
|
|
||||||
broadcastToCampaign(io, Number(character.campaign_id), "stat:updated", {
|
|
||||||
characterId: Number(id),
|
|
||||||
statName: upper,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({ characterId: Number(id), statName: upper, value });
|
|
||||||
});
|
|
||||||
|
|
||||||
// POST /api/characters/:id/gear — add gear
|
|
||||||
router.post("/:id/gear", (req, res) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { name, type, slot_count, properties, effects, game_item_id } =
|
|
||||||
req.body;
|
|
||||||
|
|
||||||
if (!name || !name.trim()) {
|
|
||||||
res.status(400).json({ error: "Gear name is required" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = db
|
|
||||||
.prepare(
|
|
||||||
"INSERT INTO character_gear (character_id, name, type, slot_count, properties, effects, game_item_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
||||||
)
|
|
||||||
.run(
|
|
||||||
id,
|
|
||||||
name.trim(),
|
|
||||||
type || "gear",
|
|
||||||
slot_count ?? 1,
|
|
||||||
JSON.stringify(properties || {}),
|
|
||||||
JSON.stringify(effects || {}),
|
|
||||||
game_item_id ?? null,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const gearRow = db
|
const [charRows] = await db.execute<RowDataPacket[]>(
|
||||||
.prepare("SELECT * FROM character_gear WHERE id = ?")
|
"SELECT * FROM characters WHERE id = ?",
|
||||||
.get(result.lastInsertRowid) as Record<string, unknown>;
|
[characterId]
|
||||||
const gear = {
|
);
|
||||||
...gearRow,
|
const enriched = {
|
||||||
properties: parseJson(gearRow.properties),
|
...charRows[0],
|
||||||
effects: parseJson(gearRow.effects),
|
overrides: {},
|
||||||
};
|
stats: DEFAULT_STATS.map((s) => ({ stat_name: s, value: 10 })),
|
||||||
|
gear: [],
|
||||||
|
talents: [],
|
||||||
|
};
|
||||||
|
|
||||||
const character = db
|
const io: Server = req.app.get("io");
|
||||||
.prepare("SELECT campaign_id FROM characters WHERE id = ?")
|
broadcastToCampaign(io, Number(campaignId), "character:created", enriched);
|
||||||
.get(id) as Record<string, unknown>;
|
res.status(201).json(enriched);
|
||||||
|
} catch (err) {
|
||||||
const io: Server = req.app.get("io");
|
console.error(err);
|
||||||
broadcastToCampaign(io, Number(character.campaign_id), "gear:added", {
|
res.status(500).json({ error: "Internal server error" });
|
||||||
characterId: Number(id),
|
}
|
||||||
gear,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(201).json(gear);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// DELETE /api/characters/:id/gear/:gearId — remove gear
|
// PATCH /api/characters/:id
|
||||||
router.delete("/:id/gear/:gearId", (req, res) => {
|
router.patch("/:id", async (req, res) => {
|
||||||
const { id, gearId } = req.params;
|
try {
|
||||||
const character = db
|
const { id } = req.params;
|
||||||
.prepare("SELECT campaign_id FROM characters WHERE id = ?")
|
const allowedFields = [
|
||||||
.get(id) as Record<string, unknown>;
|
"name", "class", "ancestry", "level", "xp", "hp_current", "hp_max",
|
||||||
if (!character) {
|
"ac", "alignment", "title", "notes", "background", "deity", "languages",
|
||||||
res.status(404).json({ error: "Character not found" });
|
"gp", "sp", "cp", "gear_slots_max", "overrides", "color", "luck_token",
|
||||||
return;
|
"torch_lit_at",
|
||||||
|
];
|
||||||
|
|
||||||
|
const updates: string[] = [];
|
||||||
|
const values: unknown[] = [];
|
||||||
|
|
||||||
|
for (const field of allowedFields) {
|
||||||
|
if (req.body[field] !== undefined) {
|
||||||
|
updates.push(`${field} = ?`);
|
||||||
|
const val = req.body[field];
|
||||||
|
values.push(typeof val === "object" && val !== null ? JSON.stringify(val) : val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.length === 0) {
|
||||||
|
res.status(400).json({ error: "No valid fields to update" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
values.push(id);
|
||||||
|
await db.execute(`UPDATE characters SET ${updates.join(", ")} WHERE id = ?`, values as import("mysql2").ExecuteValues);
|
||||||
|
|
||||||
|
const [rows] = await db.execute<RowDataPacket[]>(
|
||||||
|
"SELECT * FROM characters WHERE id = ?",
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
if (rows.length === 0) {
|
||||||
|
res.status(404).json({ error: "Character not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const io: Server = req.app.get("io");
|
||||||
|
broadcastToCampaign(io, Number(rows[0].campaign_id), "character:updated", {
|
||||||
|
id: Number(id),
|
||||||
|
...req.body,
|
||||||
|
});
|
||||||
|
res.json(rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: "Internal server error" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = db
|
|
||||||
.prepare("DELETE FROM character_gear WHERE id = ? AND character_id = ?")
|
|
||||||
.run(gearId, id);
|
|
||||||
if (result.changes === 0) {
|
|
||||||
res.status(404).json({ error: "Gear not found" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const io: Server = req.app.get("io");
|
|
||||||
broadcastToCampaign(io, Number(character.campaign_id), "gear:removed", {
|
|
||||||
characterId: Number(id),
|
|
||||||
gearId: Number(gearId),
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(204).end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /api/characters/:id/talents — add talent
|
// DELETE /api/characters/:id
|
||||||
router.post("/:id/talents", (req, res) => {
|
router.delete("/:id", async (req, res) => {
|
||||||
const { id } = req.params;
|
try {
|
||||||
const { name, description, effect, game_talent_id } = req.body;
|
const [rows] = await db.execute<RowDataPacket[]>(
|
||||||
|
"SELECT * FROM characters WHERE id = ?",
|
||||||
|
[req.params.id]
|
||||||
|
);
|
||||||
|
if (rows.length === 0) {
|
||||||
|
res.status(404).json({ error: "Character not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!name || !name.trim()) {
|
await db.execute("DELETE FROM characters WHERE id = ?", [req.params.id]);
|
||||||
res.status(400).json({ error: "Talent name is required" });
|
|
||||||
return;
|
const io: Server = req.app.get("io");
|
||||||
|
broadcastToCampaign(io, Number(rows[0].campaign_id), "character:deleted", {
|
||||||
|
id: Number(req.params.id),
|
||||||
|
});
|
||||||
|
res.status(204).end();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: "Internal server error" });
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const result = db
|
// PATCH /api/characters/:id/stats/:statName
|
||||||
.prepare(
|
router.patch("/:id/stats/:statName", async (req, res) => {
|
||||||
"INSERT INTO character_talents (character_id, name, description, effect, game_talent_id) VALUES (?, ?, ?, ?, ?)",
|
try {
|
||||||
)
|
const { id, statName } = req.params;
|
||||||
.run(
|
const { value } = req.body;
|
||||||
id,
|
const upper = statName.toUpperCase();
|
||||||
name.trim(),
|
|
||||||
description || "",
|
if (!DEFAULT_STATS.includes(upper)) {
|
||||||
JSON.stringify(effect || {}),
|
res.status(400).json({ error: "Invalid stat name" });
|
||||||
game_talent_id ?? null,
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
"UPDATE character_stats SET value = ? WHERE character_id = ? AND stat_name = ?",
|
||||||
|
[value, id, upper]
|
||||||
);
|
);
|
||||||
|
|
||||||
const talentRow = db
|
const [rows] = await db.execute<RowDataPacket[]>(
|
||||||
.prepare("SELECT * FROM character_talents WHERE id = ?")
|
"SELECT campaign_id FROM characters WHERE id = ?",
|
||||||
.get(result.lastInsertRowid) as Record<string, unknown>;
|
[id]
|
||||||
const talent = { ...talentRow, effect: parseJson(talentRow.effect) };
|
);
|
||||||
|
if (rows.length === 0) {
|
||||||
|
res.status(404).json({ error: "Character not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const character = db
|
const io: Server = req.app.get("io");
|
||||||
.prepare("SELECT campaign_id FROM characters WHERE id = ?")
|
broadcastToCampaign(io, Number(rows[0].campaign_id), "stat:updated", {
|
||||||
.get(id) as Record<string, unknown>;
|
characterId: Number(id),
|
||||||
|
statName: upper,
|
||||||
const io: Server = req.app.get("io");
|
value,
|
||||||
broadcastToCampaign(io, Number(character.campaign_id), "talent:added", {
|
});
|
||||||
characterId: Number(id),
|
res.json({ characterId: Number(id), statName: upper, value });
|
||||||
talent,
|
} catch (err) {
|
||||||
});
|
console.error(err);
|
||||||
|
res.status(500).json({ error: "Internal server error" });
|
||||||
res.status(201).json(talent);
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// DELETE /api/characters/:id/talents/:talentId — remove talent
|
// POST /api/characters/:id/gear
|
||||||
router.delete("/:id/talents/:talentId", (req, res) => {
|
router.post("/:id/gear", async (req, res) => {
|
||||||
const { id, talentId } = req.params;
|
try {
|
||||||
const character = db
|
const { id } = req.params;
|
||||||
.prepare("SELECT campaign_id FROM characters WHERE id = ?")
|
const { name, type, slot_count, properties, effects, game_item_id } = req.body;
|
||||||
.get(id) as Record<string, unknown>;
|
|
||||||
if (!character) {
|
if (!name?.trim()) {
|
||||||
res.status(404).json({ error: "Character not found" });
|
res.status(400).json({ error: "Gear name is required" });
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [result] = await db.execute<ResultSetHeader>(
|
||||||
|
`INSERT INTO character_gear
|
||||||
|
(character_id, name, type, slot_count, properties, effects, game_item_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
[
|
||||||
|
id, name.trim(), type ?? "gear", slot_count ?? 1,
|
||||||
|
JSON.stringify(properties ?? {}),
|
||||||
|
JSON.stringify(effects ?? {}),
|
||||||
|
game_item_id ?? null,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [gearRows] = await db.execute<RowDataPacket[]>(
|
||||||
|
"SELECT * FROM character_gear WHERE id = ?",
|
||||||
|
[result.insertId]
|
||||||
|
);
|
||||||
|
const gear = {
|
||||||
|
...gearRows[0],
|
||||||
|
properties: parseJson(gearRows[0].properties),
|
||||||
|
effects: parseJson(gearRows[0].effects),
|
||||||
|
};
|
||||||
|
|
||||||
|
const [charRows] = await db.execute<RowDataPacket[]>(
|
||||||
|
"SELECT campaign_id FROM characters WHERE id = ?",
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
const io: Server = req.app.get("io");
|
||||||
|
broadcastToCampaign(io, Number(charRows[0].campaign_id), "gear:added", {
|
||||||
|
characterId: Number(id),
|
||||||
|
gear,
|
||||||
|
});
|
||||||
|
res.status(201).json(gear);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: "Internal server error" });
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const result = db
|
// DELETE /api/characters/:id/gear/:gearId
|
||||||
.prepare("DELETE FROM character_talents WHERE id = ? AND character_id = ?")
|
router.delete("/:id/gear/:gearId", async (req, res) => {
|
||||||
.run(talentId, id);
|
try {
|
||||||
if (result.changes === 0) {
|
const { id, gearId } = req.params;
|
||||||
res.status(404).json({ error: "Talent not found" });
|
const [charRows] = await db.execute<RowDataPacket[]>(
|
||||||
return;
|
"SELECT campaign_id FROM characters WHERE id = ?",
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
if (charRows.length === 0) {
|
||||||
|
res.status(404).json({ error: "Character not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [result] = await db.execute<ResultSetHeader>(
|
||||||
|
"DELETE FROM character_gear WHERE id = ? AND character_id = ?",
|
||||||
|
[gearId, id]
|
||||||
|
);
|
||||||
|
if (result.affectedRows === 0) {
|
||||||
|
res.status(404).json({ error: "Gear not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const io: Server = req.app.get("io");
|
||||||
|
broadcastToCampaign(io, Number(charRows[0].campaign_id), "gear:removed", {
|
||||||
|
characterId: Number(id),
|
||||||
|
gearId: Number(gearId),
|
||||||
|
});
|
||||||
|
res.status(204).end();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: "Internal server error" });
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const io: Server = req.app.get("io");
|
// POST /api/characters/:id/talents
|
||||||
broadcastToCampaign(io, Number(character.campaign_id), "talent:removed", {
|
router.post("/:id/talents", async (req, res) => {
|
||||||
characterId: Number(id),
|
try {
|
||||||
talentId: Number(talentId),
|
const { id } = req.params;
|
||||||
});
|
const { name, description, effect, game_talent_id } = req.body;
|
||||||
|
|
||||||
res.status(204).end();
|
if (!name?.trim()) {
|
||||||
|
res.status(400).json({ error: "Talent name is required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [result] = await db.execute<ResultSetHeader>(
|
||||||
|
`INSERT INTO character_talents
|
||||||
|
(character_id, name, description, effect, game_talent_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?)`,
|
||||||
|
[id, name.trim(), description ?? "", JSON.stringify(effect ?? {}), game_talent_id ?? null]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [talentRows] = await db.execute<RowDataPacket[]>(
|
||||||
|
"SELECT * FROM character_talents WHERE id = ?",
|
||||||
|
[result.insertId]
|
||||||
|
);
|
||||||
|
const talent = { ...talentRows[0], effect: parseJson(talentRows[0].effect) };
|
||||||
|
|
||||||
|
const [charRows] = await db.execute<RowDataPacket[]>(
|
||||||
|
"SELECT campaign_id FROM characters WHERE id = ?",
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
const io: Server = req.app.get("io");
|
||||||
|
broadcastToCampaign(io, Number(charRows[0].campaign_id), "talent:added", {
|
||||||
|
characterId: Number(id),
|
||||||
|
talent,
|
||||||
|
});
|
||||||
|
res.status(201).json(talent);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: "Internal server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /api/characters/:id/talents/:talentId
|
||||||
|
router.delete("/:id/talents/:talentId", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id, talentId } = req.params;
|
||||||
|
const [charRows] = await db.execute<RowDataPacket[]>(
|
||||||
|
"SELECT campaign_id FROM characters WHERE id = ?",
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
if (charRows.length === 0) {
|
||||||
|
res.status(404).json({ error: "Character not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [result] = await db.execute<ResultSetHeader>(
|
||||||
|
"DELETE FROM character_talents WHERE id = ? AND character_id = ?",
|
||||||
|
[talentId, id]
|
||||||
|
);
|
||||||
|
if (result.affectedRows === 0) {
|
||||||
|
res.status(404).json({ error: "Talent not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const io: Server = req.app.get("io");
|
||||||
|
broadcastToCampaign(io, Number(charRows[0].campaign_id), "talent:removed", {
|
||||||
|
characterId: Number(id),
|
||||||
|
talentId: Number(talentId),
|
||||||
|
});
|
||||||
|
res.status(204).end();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: "Internal server error" });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue