107 lines
3.4 KiB
TypeScript
107 lines
3.4 KiB
TypeScript
import { createHash, randomUUID } from "node:crypto";
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
} from "node:fs";
|
|
import path from "node:path";
|
|
import Database from "better-sqlite3";
|
|
|
|
export interface DisposableDatabase {
|
|
databaseUrl: string;
|
|
databasePath: string;
|
|
directory: string;
|
|
}
|
|
|
|
function pathFromDatabaseUrl(databaseUrl: string, cwd = process.cwd()) {
|
|
if (!databaseUrl.startsWith("file:")) {
|
|
throw new Error("Tests require a file: SQLite DATABASE_URL");
|
|
}
|
|
const withoutScheme = databaseUrl.slice("file:".length).split("?")[0];
|
|
return path.resolve(cwd, withoutScheme);
|
|
}
|
|
|
|
export function assertDisposableTestDatabase(databaseUrl: string) {
|
|
const databasePath = pathFromDatabaseUrl(databaseUrl);
|
|
const normalized = databasePath.replaceAll("\\", "/").toLowerCase();
|
|
if (
|
|
normalized.endsWith("/dev.db") ||
|
|
normalized === "/app/data/study.db" ||
|
|
existsSync(databasePath) ||
|
|
!path.basename(databasePath).includes(".test.")
|
|
) {
|
|
throw new Error(`Refusing to use non-disposable database: ${databasePath}`);
|
|
}
|
|
return databasePath;
|
|
}
|
|
|
|
export function createDisposableDatabase(): DisposableDatabase {
|
|
const directory = path.join(
|
|
process.cwd(),
|
|
".test-databases",
|
|
`study-desk-tests-${randomUUID()}`
|
|
);
|
|
mkdirSync(directory, { recursive: true });
|
|
const databasePath = path.join(directory, `${randomUUID()}.test.db`);
|
|
const relativePath = path.relative(process.cwd(), databasePath).replaceAll("\\", "/");
|
|
const databaseUrl = `file:./${relativePath}`;
|
|
assertDisposableTestDatabase(databaseUrl);
|
|
return { databaseUrl, databasePath, directory };
|
|
}
|
|
|
|
export function applyCommittedMigrations(
|
|
database: DisposableDatabase,
|
|
options: { exclude?: string[]; through?: string } = {}
|
|
) {
|
|
assertDisposableTestDatabase(database.databaseUrl);
|
|
const migrationRoot = path.join(process.cwd(), "prisma", "migrations");
|
|
const migrationNames = readdirSync(migrationRoot, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name)
|
|
.filter((name) => !(options.exclude ?? []).includes(name))
|
|
.filter((name) => !options.through || name <= options.through)
|
|
.sort();
|
|
const client = new Database(database.databasePath);
|
|
try {
|
|
client.pragma("foreign_keys = ON");
|
|
client.exec(`
|
|
CREATE TABLE "_prisma_migrations" (
|
|
"id" TEXT NOT NULL PRIMARY KEY,
|
|
"checksum" TEXT NOT NULL,
|
|
"finished_at" DATETIME,
|
|
"migration_name" TEXT NOT NULL,
|
|
"logs" TEXT,
|
|
"rolled_back_at" DATETIME,
|
|
"started_at" DATETIME NOT NULL DEFAULT current_timestamp,
|
|
"applied_steps_count" INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
`);
|
|
const recordMigration = client.prepare(`
|
|
INSERT INTO "_prisma_migrations"
|
|
("id", "checksum", "finished_at", "migration_name", "applied_steps_count")
|
|
VALUES (?, ?, current_timestamp, ?, 1)
|
|
`);
|
|
for (const migrationName of migrationNames) {
|
|
const sql = readFileSync(
|
|
path.join(migrationRoot, migrationName, "migration.sql"),
|
|
"utf8"
|
|
);
|
|
client.transaction(() => {
|
|
client.exec(sql);
|
|
recordMigration.run(
|
|
randomUUID(),
|
|
createHash("sha256").update(sql).digest("hex"),
|
|
migrationName
|
|
);
|
|
})();
|
|
}
|
|
} finally {
|
|
client.close();
|
|
}
|
|
}
|
|
|
|
export function disposeDisposableDatabase(database: DisposableDatabase) {
|
|
rmSync(database.directory, { recursive: true, force: true });
|
|
}
|