Adds GET/POST/PATCH/DELETE /api/ships with constraint validation (current <= max), enum checks for maneuver_class, and cascade delete for weapons. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
118 lines
3.4 KiB
TypeScript
118 lines
3.4 KiB
TypeScript
import { v4 as uuid } from 'uuid';
|
|
import db from '../db/connection.js';
|
|
import type { CreateShipInput, UpdateShipInput } from '../validation/ship-schema.js';
|
|
|
|
export interface Ship {
|
|
id: string;
|
|
name: string;
|
|
hull_current: number;
|
|
hull_max: number;
|
|
armor_current: number;
|
|
armor_max: number;
|
|
ac: number;
|
|
con_save: number | null;
|
|
speed: number | null;
|
|
maneuver_class: string | null;
|
|
size_category: string | null;
|
|
notes: string | null;
|
|
updated_at: number;
|
|
}
|
|
|
|
export interface Weapon {
|
|
id: string;
|
|
ship_id: string;
|
|
name: string;
|
|
type: string | null;
|
|
attack_mod: number | null;
|
|
damage: string | null;
|
|
range: string | null;
|
|
ammo_current: number | null;
|
|
ammo_max: number | null;
|
|
status: string | null;
|
|
notes: string | null;
|
|
sort_order: number;
|
|
updated_at: number;
|
|
}
|
|
|
|
export function listShips(): Pick<Ship, 'id' | 'name' | 'updated_at'>[] {
|
|
return db
|
|
.prepare('SELECT id, name, updated_at FROM ships ORDER BY updated_at DESC')
|
|
.all() as Pick<Ship, 'id' | 'name' | 'updated_at'>[];
|
|
}
|
|
|
|
export function getShip(id: string): { ship: Ship; weapons: Weapon[] } | null {
|
|
const ship = db.prepare('SELECT * FROM ships WHERE id = ?').get(id) as Ship | undefined;
|
|
if (!ship) return null;
|
|
|
|
const weapons = db
|
|
.prepare('SELECT * FROM weapons WHERE ship_id = ? ORDER BY sort_order ASC, name ASC')
|
|
.all(id) as Weapon[];
|
|
|
|
return { ship, weapons };
|
|
}
|
|
|
|
export function createShip(input: CreateShipInput): Ship {
|
|
const id = uuid();
|
|
const now = Date.now();
|
|
|
|
db.prepare(`
|
|
INSERT INTO ships (id, name, hull_current, hull_max, armor_current, armor_max, ac, con_save, speed, maneuver_class, size_category, notes, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run(
|
|
id,
|
|
input.name,
|
|
input.hull_current,
|
|
input.hull_max,
|
|
input.armor_current,
|
|
input.armor_max,
|
|
input.ac,
|
|
input.con_save,
|
|
input.speed,
|
|
input.maneuver_class,
|
|
input.size_category,
|
|
input.notes,
|
|
now,
|
|
);
|
|
|
|
return db.prepare('SELECT * FROM ships WHERE id = ?').get(id) as Ship;
|
|
}
|
|
|
|
export function updateShip(id: string, patch: UpdateShipInput): Ship | null {
|
|
const existing = db.prepare('SELECT * FROM ships WHERE id = ?').get(id) as Ship | undefined;
|
|
if (!existing) return null;
|
|
|
|
const fields = Object.keys(patch).filter(
|
|
(k) => (patch as Record<string, unknown>)[k] !== undefined,
|
|
);
|
|
if (fields.length === 0) return existing;
|
|
|
|
// Validate current <= max against existing values when only one side is patched
|
|
const newHullCurrent = patch.hull_current ?? existing.hull_current;
|
|
const newHullMax = patch.hull_max ?? existing.hull_max;
|
|
const newArmorCurrent = patch.armor_current ?? existing.armor_current;
|
|
const newArmorMax = patch.armor_max ?? existing.armor_max;
|
|
|
|
if (newHullCurrent > newHullMax) {
|
|
throw new Error('hull_current cannot exceed hull_max');
|
|
}
|
|
if (newArmorCurrent > newArmorMax) {
|
|
throw new Error('armor_current cannot exceed armor_max');
|
|
}
|
|
|
|
const sets = fields.map((f) => `${f} = ?`);
|
|
sets.push('updated_at = ?');
|
|
|
|
const values = fields.map((f) => (patch as Record<string, unknown>)[f]);
|
|
values.push(Date.now());
|
|
values.push(id);
|
|
|
|
db.prepare(`UPDATE ships SET ${sets.join(', ')} WHERE id = ?`).run(...values);
|
|
|
|
return db.prepare('SELECT * FROM ships WHERE id = ?').get(id) as Ship;
|
|
}
|
|
|
|
export function deleteShip(id: string): boolean {
|
|
const result = db.prepare('DELETE FROM ships WHERE id = ?').run(id);
|
|
return result.changes > 0;
|
|
}
|