feat(server): add SQLite connection and migration system

Adds better-sqlite3 with WAL mode, auto-migration runner that tracks applied
migrations, and initial schema for ships and weapons tables with constraints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bas van Rossem
2026-02-19 16:16:01 +01:00
parent 6d60d714d0
commit 4b4d105009
5 changed files with 94 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
import Database, { type Database as DatabaseType } from 'better-sqlite3';
import { mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
import { DB_PATH } from '../config.js';
// Ensure the data directory exists
mkdirSync(dirname(DB_PATH), { recursive: true });
const db: DatabaseType = new Database(DB_PATH);
// Enable WAL mode for better concurrent read performance
db.pragma('journal_mode = WAL');
// Enable foreign key enforcement
db.pragma('foreign_keys = ON');
export default db;

43
server/src/db/migrate.ts Normal file
View File

@@ -0,0 +1,43 @@
import { readdirSync, readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import db from './connection.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const MIGRATIONS_DIR = join(__dirname, 'migrations');
export function runMigrations(): void {
// Create the migrations tracking table if it doesn't exist
db.exec(`
CREATE TABLE IF NOT EXISTS _migrations (
name TEXT PRIMARY KEY,
applied_at INTEGER NOT NULL DEFAULT (unixepoch('now') * 1000)
)
`);
// Read all .sql files, sorted by name
const files = readdirSync(MIGRATIONS_DIR)
.filter((f) => f.endsWith('.sql'))
.sort();
const applied = new Set(
db
.prepare('SELECT name FROM _migrations')
.all()
.map((row: any) => row.name),
);
for (const file of files) {
if (applied.has(file)) continue;
const sql = readFileSync(join(MIGRATIONS_DIR, file), 'utf-8');
const migrate = db.transaction(() => {
db.exec(sql);
db.prepare('INSERT INTO _migrations (name) VALUES (?)').run(file);
});
migrate();
console.log(`Migration applied: ${file}`);
}
}

View File

@@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS ships (
id TEXT PRIMARY KEY,
name TEXT NOT NULL DEFAULT 'Unnamed Ship',
hull_current INTEGER NOT NULL DEFAULT 0,
hull_max INTEGER NOT NULL DEFAULT 0,
armor_current INTEGER NOT NULL DEFAULT 0,
armor_max INTEGER NOT NULL DEFAULT 0,
ac INTEGER NOT NULL DEFAULT 10,
con_save INTEGER,
speed INTEGER,
maneuver_class TEXT CHECK(maneuver_class IS NULL OR maneuver_class IN ('A','B','C','D','E','F','S')),
size_category TEXT,
notes TEXT,
updated_at INTEGER NOT NULL DEFAULT (unixepoch('now') * 1000)
);

View File

@@ -0,0 +1,17 @@
CREATE TABLE IF NOT EXISTS weapons (
id TEXT PRIMARY KEY,
ship_id TEXT NOT NULL REFERENCES ships(id) ON DELETE CASCADE,
name TEXT NOT NULL DEFAULT 'Unnamed Weapon',
type TEXT,
attack_mod INTEGER,
damage TEXT,
range TEXT,
ammo_current INTEGER,
ammo_max INTEGER,
status TEXT DEFAULT 'ok' CHECK(status IS NULL OR status IN ('ok','damaged','disabled')),
notes TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL DEFAULT (unixepoch('now') * 1000)
);
CREATE INDEX IF NOT EXISTS idx_weapons_ship_id ON weapons(ship_id);

View File

@@ -1,5 +1,6 @@
import Fastify from 'fastify'; import Fastify from 'fastify';
import { PORT } from './config.js'; import { PORT } from './config.js';
import { runMigrations } from './db/migrate.js';
const app = Fastify({ logger: true }); const app = Fastify({ logger: true });
@@ -9,6 +10,8 @@ app.get('/', async () => {
const start = async () => { const start = async () => {
try { try {
runMigrations();
await app.listen({ port: PORT, host: '0.0.0.0' }); await app.listen({ port: PORT, host: '0.0.0.0' });
console.log(`Server running on port ${PORT}`); console.log(`Server running on port ${PORT}`);
} catch (err) { } catch (err) {