import fs from 'node:fs'; import path from 'node:path'; import Database from 'better-sqlite3'; import type { CharacterSheet, SystemDefinition } from '../shared/types.ts'; import { validateSystemDefinition } from '../shared/systemValidate.ts'; export class AppStore { private readonly db: Database.Database; constructor(dbPath: string) { fs.mkdirSync(path.dirname(dbPath), { recursive: true }); this.db = new Database(dbPath); this.db.exec(` CREATE TABLE IF NOT EXISTS systems ( id TEXT PRIMARY KEY, revision INTEGER NOT NULL, schema_version INTEGER NOT NULL, name TEXT NOT NULL, json TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS characters ( id TEXT PRIMARY KEY, system_id TEXT NOT NULL, name TEXT NOT NULL, json TEXT NOT NULL, updated_at TEXT NOT NULL ); `); } listSystems(): Array<{ id: string; name: string; revision: number; schemaVersion: number }> { return this.db .prepare('SELECT id, name, revision, schema_version AS schemaVersion FROM systems ORDER BY name') .all() as Array<{ id: string; name: string; revision: number; schemaVersion: number }>; } getSystem(id: string): SystemDefinition | null { const row = this.db.prepare('SELECT json FROM systems WHERE id = ?').get(id) as | { json: string } | undefined; return row ? (JSON.parse(row.json) as SystemDefinition) : null; } upsertSystem(system: SystemDefinition): SystemDefinition { const check = validateSystemDefinition(system); if (!check.ok) { throw new Error(check.errors.map((e) => e.message).join('; ')); } const existing = this.getSystem(system.id); const next: SystemDefinition = { ...system, revision: existing ? existing.revision + 1 : system.revision || 1, }; const now = new Date().toISOString(); this.db .prepare( `INSERT INTO systems (id, revision, schema_version, name, json, updated_at) VALUES (@id, @revision, @schemaVersion, @name, @json, @updatedAt) ON CONFLICT(id) DO UPDATE SET revision=excluded.revision, schema_version=excluded.schema_version, name=excluded.name, json=excluded.json, updated_at=excluded.updated_at`, ) .run({ id: next.id, revision: next.revision, schemaVersion: next.schemaVersion, name: next.name, json: JSON.stringify(next), updatedAt: now, }); return next; } deleteSystem(id: string): void { const tx = this.db.transaction(() => { this.db.prepare('DELETE FROM characters WHERE system_id = ?').run(id); this.db.prepare('DELETE FROM systems WHERE id = ?').run(id); }); tx(); } listCharacters(systemId?: string): Array<{ id: string; name: string; systemId: string }> { if (systemId) { return this.db .prepare( 'SELECT id, name, system_id AS systemId FROM characters WHERE system_id = ? ORDER BY name', ) .all(systemId) as Array<{ id: string; name: string; systemId: string }>; } return this.db .prepare('SELECT id, name, system_id AS systemId FROM characters ORDER BY name') .all() as Array<{ id: string; name: string; systemId: string }>; } getCharacter(id: string): CharacterSheet | null { const row = this.db.prepare('SELECT json FROM characters WHERE id = ?').get(id) as | { json: string } | undefined; return row ? (JSON.parse(row.json) as CharacterSheet) : null; } upsertCharacter(character: CharacterSheet): CharacterSheet { const now = new Date().toISOString(); this.db .prepare( `INSERT INTO characters (id, system_id, name, json, updated_at) VALUES (@id, @systemId, @name, @json, @updatedAt) ON CONFLICT(id) DO UPDATE SET system_id=excluded.system_id, name=excluded.name, json=excluded.json, updated_at=excluded.updated_at`, ) .run({ id: character.id, systemId: character.systemId, name: character.name, json: JSON.stringify(character), updatedAt: now, }); return character; } deleteCharacter(id: string): void { this.db.prepare('DELETE FROM characters WHERE id = ?').run(id); } close(): void { this.db.close(); } }