Files
courtside/server/store.js
T
bkvargyasandClaude Fable 5.1 5ade592384 Courtside MVP: engine, web app, Docker + Cloudflare Tunnel deploy, docs
Tournament engine (pools, single/double elimination, standings with proper
tiebreaks, withdrawals, two-court queue with ETAs and alerts), a single-process
Node server with SQLite via node:sqlite and a WebSocket live board, organizer
desk, QR landing page that follows the tournament phase, Dockerfile and
compose with cloudflared, and documentation for deploying and running a day.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MB7nCCAscYsb3zzkT6LHZi
2026-09-03 20:31:46 +00:00

59 lines
2.6 KiB
JavaScript

// SQLite persistence using Node's built-in node:sqlite (Node 22.13+). No native modules.
// Each tournament is stored as two JSON documents (tournament state + court engine state),
// rewritten on every mutation. Data is tiny (a few hundred KB for a big day), so this is
// simpler and more robust than a normalized schema, and a backup is one file copy.
import { DatabaseSync } from 'node:sqlite';
import { mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
export class Store {
constructor(path) {
if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true });
this.db = new DatabaseSync(path);
this.db.exec(`
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS tournaments (
slug TEXT PRIMARY KEY,
tournament_json TEXT NOT NULL,
engine_json TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL,
ts INTEGER NOT NULL,
actor TEXT,
type TEXT NOT NULL,
json TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS events_slug ON events(slug, id);
`);
this.stmts = {
all: this.db.prepare('SELECT slug, tournament_json, engine_json FROM tournaments ORDER BY created_at DESC'),
upsert: this.db.prepare(`INSERT INTO tournaments (slug, tournament_json, engine_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(slug) DO UPDATE SET tournament_json = excluded.tournament_json, engine_json = excluded.engine_json, updated_at = excluded.updated_at`),
del: this.db.prepare('DELETE FROM tournaments WHERE slug = ?'),
event: this.db.prepare('INSERT INTO events (slug, ts, actor, type, json) VALUES (?, ?, ?, ?, ?)'),
events: this.db.prepare('SELECT ts, actor, type, json FROM events WHERE slug = ? ORDER BY id DESC LIMIT ?'),
};
}
loadAll() {
return this.stmts.all.all().map(r => ({ slug: r.slug, tournament: JSON.parse(r.tournament_json), engine: r.engine_json ? JSON.parse(r.engine_json) : null }));
}
save(slug, tournamentJson, engineJson) {
const now = Date.now();
this.stmts.upsert.run(slug, JSON.stringify(tournamentJson), engineJson ? JSON.stringify(engineJson) : null, tournamentJson.createdAt ?? now, now);
}
remove(slug) { this.stmts.del.run(slug); }
logEvent(slug, evt) { this.stmts.event.run(slug, Date.now(), evt.actor ?? null, evt.type, JSON.stringify(evt)); }
events(slug, limit = 200) { return this.stmts.events.all(slug, limit).map(r => ({ ts: r.ts, ...JSON.parse(r.json) })); }
close() { this.db.close(); }
}