mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-15 07:36:27 -05:00
129 lines
3.5 KiB
TypeScript
129 lines
3.5 KiB
TypeScript
import { execSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const ROOT_DIR = path.join(__dirname, "..");
|
|
|
|
const MIGRATIONS_DIR = path.join(ROOT_DIR, "migrations");
|
|
const TEST_DB_PATH = path.join(ROOT_DIR, "db-test.sqlite3");
|
|
|
|
export function setup() {
|
|
ensureMigratedDb(TEST_DB_PATH);
|
|
|
|
// test workers only read this file to copy its schema; out of WAL there are no
|
|
// -wal/-shm sidecars, so their concurrent opens need no write access
|
|
const db = new DatabaseSync(TEST_DB_PATH);
|
|
db.exec("PRAGMA journal_mode = DELETE");
|
|
db.close();
|
|
}
|
|
|
|
/**
|
|
* Creates `dbPath` if missing, applies pending migrations, and rebuilds it from scratch if an
|
|
* applied migration disappeared or changed contents: a branch edits the migration it added
|
|
* rather than stacking a new one, so the file name kysely tracks stays the same while the schema does not.
|
|
*/
|
|
export function ensureMigratedDb(dbPath: string) {
|
|
const resolvedPath = path.resolve(ROOT_DIR, dbPath);
|
|
|
|
if (!fs.existsSync(resolvedPath)) {
|
|
migrateUp(resolvedPath);
|
|
return;
|
|
}
|
|
|
|
const applied = appliedMigrations(resolvedPath);
|
|
const onDisk = migrationFilesOnDisk();
|
|
|
|
const hasDrift =
|
|
applied === null ||
|
|
applied.some((name) => !onDisk.has(name)) ||
|
|
readContentsMarker(resolvedPath) !== migrationContentsHash();
|
|
if (hasDrift) {
|
|
deleteDbFiles(resolvedPath);
|
|
migrateUp(resolvedPath);
|
|
return;
|
|
}
|
|
|
|
const appliedSet = new Set(applied);
|
|
const hasPending = [...onDisk].some((name) => !appliedSet.has(name));
|
|
if (hasPending) {
|
|
migrateUp(resolvedPath);
|
|
}
|
|
}
|
|
|
|
/** Fingerprint of every migration's name and contents. */
|
|
function migrationContentsHash() {
|
|
const hash = createHash("sha256");
|
|
for (const file of fs.readdirSync(MIGRATIONS_DIR).sort()) {
|
|
hash.update(file);
|
|
hash.update(fs.readFileSync(path.join(MIGRATIONS_DIR, file)));
|
|
}
|
|
return hash.digest("hex");
|
|
}
|
|
|
|
/** Sidecar recording the migration contents the database was built from. */
|
|
function contentsMarkerPath(dbPath: string) {
|
|
return `${dbPath}.migrations`;
|
|
}
|
|
|
|
function readContentsMarker(dbPath: string) {
|
|
try {
|
|
return fs.readFileSync(contentsMarkerPath(dbPath), "utf8");
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function migrationFilesOnDisk() {
|
|
return new Set(
|
|
fs
|
|
.readdirSync(MIGRATIONS_DIR)
|
|
.filter((file) => file.endsWith(".ts"))
|
|
// kysely tracks migrations by file name without the extension
|
|
.map((file) => file.slice(0, -".ts".length)),
|
|
);
|
|
}
|
|
|
|
function appliedMigrations(dbPath: string) {
|
|
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
try {
|
|
const hasMigrationsTable = db
|
|
.prepare(
|
|
"select 1 from sqlite_master where type = 'table' and name = 'kysely_migration'",
|
|
)
|
|
.get();
|
|
if (!hasMigrationsTable) return null;
|
|
|
|
const rows = db
|
|
.prepare("select name from kysely_migration")
|
|
.all() as Array<{
|
|
name: string;
|
|
}>;
|
|
return rows.map((row) => row.name);
|
|
} catch {
|
|
return null;
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
function deleteDbFiles(dbPath: string) {
|
|
for (const suffix of ["", "-shm", "-wal"]) {
|
|
fs.rmSync(`${dbPath}${suffix}`, { force: true });
|
|
}
|
|
fs.rmSync(contentsMarkerPath(dbPath), { force: true });
|
|
}
|
|
|
|
function migrateUp(dbPath: string) {
|
|
execSync("pnpm run migrate up", {
|
|
cwd: ROOT_DIR,
|
|
stdio: "inherit",
|
|
env: { ...process.env, DB_PATH: dbPath },
|
|
});
|
|
fs.writeFileSync(contentsMarkerPath(dbPath), migrationContentsHash());
|
|
}
|