CI / build + unit + e2e (pull_request) Successful in 1m14s
- DELETE /api/games/:id route + Db.delete() (404 when absent) - PresetPicker: per-saved-game trash button + confirm dialog (Cancel/Delete) - App: runDeleteGame; useSse onError -> goHome on 'deleted'/'not found' (a deleted/missing game now returns the live tab and any deep link home) - DeleteGameResponse in shared/protocol.ts; api.deleteGame() - unit test (Db.delete) + 2 e2e tests (home-screen delete; in-flight delete returns the active tab home and a fresh deep link lands on home) - version 0.7.0 -> 0.8.0 + changelog Fixes #20
58 lines
1.6 KiB
TypeScript
58 lines
1.6 KiB
TypeScript
// SQLite persistence for games (one row per game id; the full GameState as JSON).
|
|
import { mkdirSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import Database from 'better-sqlite3';
|
|
import type { GameState } from '../shared/types';
|
|
|
|
export interface Db {
|
|
save(state: GameState): void;
|
|
load(id: string): GameState | null;
|
|
listIds(): string[];
|
|
delete(id: string): boolean;
|
|
close(): void;
|
|
}
|
|
|
|
export function openDb(filePath: string): Db {
|
|
const dir = path.dirname(filePath);
|
|
if (dir) mkdirSync(dir, { recursive: true });
|
|
const db = new Database(filePath);
|
|
db.pragma('journal_mode = WAL');
|
|
db.exec(
|
|
`CREATE TABLE IF NOT EXISTS games (
|
|
id TEXT PRIMARY KEY,
|
|
data TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
)`,
|
|
);
|
|
const upsert = db.prepare(
|
|
`INSERT INTO games (id, data, updated_at) VALUES (?, ?, ?)
|
|
ON CONFLICT(id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`,
|
|
);
|
|
const byId = db.prepare('SELECT data FROM games WHERE id = ?');
|
|
const all = db.prepare('SELECT id FROM games ORDER BY updated_at DESC');
|
|
const del = db.prepare('DELETE FROM games WHERE id = ?');
|
|
return {
|
|
save(state) {
|
|
upsert.run(state.id, JSON.stringify(state), new Date().toISOString());
|
|
},
|
|
load(id) {
|
|
const row = byId.get(id) as { data: string } | undefined;
|
|
if (!row) return null;
|
|
try {
|
|
return JSON.parse(row.data) as GameState;
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
listIds() {
|
|
return (all.all() as { id: string }[]).map((r) => r.id);
|
|
},
|
|
delete(id) {
|
|
return del.run(id).changes > 0;
|
|
},
|
|
close() {
|
|
db.close();
|
|
},
|
|
};
|
|
}
|