feat: adopt bchen-sqlite-migrate package; replace inline SCHEMA_DDL

Phase 3 of the cross-project sqlite-migrate adoption — port nanodrop to
consume bchen-sqlite-migrate@v0.1.0. Replaces the inline db.exec(...)
block in src/db/schema.ts with applyMigrations(db, MIGRATIONS_DIR,
{ genesisProbeTable: 'users' }).

The genesis-probe (table 'users' exists) handles pre-existing prod DBs
automatically — first deploy after merge stamps 0001_init as applied
without re-executing, subsequent boots are no-ops.

Adds three npm scripts (db:migrate, db:status, db:stamp) and a
byte-stability test pinning sha256(0001_init.sql) so the migration is
treated as immutable history.
This commit is contained in:
2026-05-12 08:02:36 -07:00
parent 3f8da8c12c
commit 436f7417be
9 changed files with 994 additions and 536 deletions

View File

@@ -1,38 +1,27 @@
import path from 'node:path';
import Database from 'better-sqlite3';
import { applyMigrations } from 'bchen-sqlite-migrate';
export const MIGRATIONS_DIR = path.resolve(import.meta.dirname, 'migrations');
export function applySchema(db: Database.Database): void {
applyMigrations(db, MIGRATIONS_DIR, { genesisProbeTable: 'users' });
}
export function initDb(dbPath: string): Database.Database {
const db = new Database(dbPath);
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS files (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
original_name TEXT NOT NULL,
mime_type TEXT NOT NULL,
size INTEGER NOT NULL,
stored_name TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS login_attempts (
username TEXT PRIMARY KEY,
failed_count INTEGER NOT NULL DEFAULT 0,
last_failed_at TEXT,
locked_until TEXT
);
CREATE INDEX IF NOT EXISTS idx_login_attempts_locked_until
ON login_attempts(locked_until);
`);
const stampGenesis = process.env.DB_MIGRATIONS_STAMP_GENESIS === '1';
const summary = applyMigrations(db, MIGRATIONS_DIR, {
stampGenesis,
genesisProbeTable: 'users',
logger: (msg) => process.stdout.write(`${msg}\n`),
});
process.stdout.write(
`migrations: ${summary.applied + summary.alreadyApplied} applied, ${summary.pending} pending\n`,
);
return db;
}