Refactor Study Desk application structure

This commit is contained in:
Elijah 2026-08-07 19:31:23 -07:00
parent faaccf8a7e
commit 089439ed90
145 changed files with 8087 additions and 3412 deletions

View file

@ -0,0 +1,197 @@
import { randomUUID } from "node:crypto";
import {
copyFileSync,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
rmSync,
statSync,
} from "node:fs";
import path from "node:path";
import Database from "better-sqlite3";
import { backupDatabase } from "./backup-database.mjs";
const root = path.join(process.cwd(), ".test-databases");
const directory = path.join(root, `backup-restore-${randomUUID()}`);
if (!path.resolve(directory).startsWith(`${path.resolve(root)}${path.sep}`)) {
throw new Error("Unsafe disposable backup verification path");
}
mkdirSync(directory, { recursive: true });
const sourcePath = path.join(directory, "source.test.db");
const backupPath = path.join(directory, "backup.test.db");
const restoredPath = path.join(directory, "restored.test.db");
let source;
function rowCounts(database) {
const tables = database
.prepare(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
)
.all()
.map((row) => row.name);
return Object.fromEntries(
tables.map((table) => [
table,
database.prepare(`SELECT COUNT(*) AS count FROM ${JSON.stringify(table)}`).get().count,
])
);
}
try {
source = new Database(sourcePath);
source.pragma("foreign_keys = ON");
source.pragma("journal_mode = WAL");
source.pragma("wal_autocheckpoint = 0");
const migrations = readdirSync(path.join(process.cwd(), "prisma", "migrations"), {
withFileTypes: true,
})
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort();
for (const migration of migrations) {
source.exec(
readFileSync(
path.join(process.cwd(), "prisma", "migrations", migration, "migration.sql"),
"utf8"
)
);
}
source.transaction(() => {
source
.prepare('INSERT INTO "Class" ("id", "name", "slug", "sortOrder") VALUES (?, ?, ?, ?)')
.run("backup-class", "Representative content", "representative-content", 0);
source
.prepare(
'INSERT INTO "MaterialGroup" ("id", "classId", "name", "type", "sortOrder") VALUES (?, ?, ?, ?, ?)'
)
.run("backup-group", "backup-class", "Backup group", "DECK", 0);
source
.prepare(
'INSERT INTO "Deck" ("id", "classId", "groupId", "name", "description", "sortOrder") VALUES (?, ?, ?, ?, ?, ?)'
)
.run(
"backup-deck",
"backup-class",
"backup-group",
"Backup deck",
"Restore drill content",
0
);
source
.prepare(
'INSERT INTO "Flashcard" ("id", "deckId", "front", "back", "sortOrder") VALUES (?, ?, ?, ?, ?)'
)
.run("backup-card", "backup-deck", "Representative front", "Representative back", 0);
source
.prepare(
'INSERT INTO "QuizSet" ("id", "classId", "name", "description", "sortOrder") VALUES (?, ?, ?, ?, ?)'
)
.run("backup-quiz", "backup-class", "Backup quiz", "Quiz relationship", 0);
source
.prepare(
'INSERT INTO "Question" ("id", "quizSetId", "type", "prompt", "rationale", "category", "sortOrder") VALUES (?, ?, ?, ?, ?, ?, ?)'
)
.run(
"backup-question",
"backup-quiz",
"MULTIPLE_CHOICE",
"Representative prompt",
"Representative rationale",
"backup",
0
);
source
.prepare(
'INSERT INTO "AnswerOption" ("id", "questionId", "text", "isCorrect", "sortOrder") VALUES (?, ?, ?, ?, ?)'
)
.run("backup-option", "backup-question", "Correct option", 1, 0);
source
.prepare(
'INSERT INTO "SpacedRepetitionSet" ("id", "classId", "name", "sortOrder", "updatedAt") VALUES (?, ?, ?, ?, ?)'
)
.run("backup-srs", "backup-class", "Backup SRS", 0, Date.now());
source
.prepare(
'INSERT INTO "SpacedRepetitionSetDeck" ("setId", "deckId", "sortOrder") VALUES (?, ?, ?)'
)
.run("backup-srs", "backup-deck", 0);
source
.prepare(
'INSERT INTO "ShareLink" ("id", "targetType", "groupId") VALUES (?, ?, ?)'
)
.run("backup-share", "GROUP", "backup-group");
})();
const walPath = `${sourcePath}-wal`;
if (!existsSync(walPath) || statSync(walPath).size === 0) {
throw new Error("Backup drill did not create an active WAL database");
}
const sourceWalBytes = statSync(walPath).size;
const sourceCounts = rowCounts(source);
await backupDatabase(sourcePath, backupPath);
copyFileSync(backupPath, restoredPath);
source.close();
source = undefined;
const restored = new Database(restoredPath, { readonly: true, fileMustExist: true });
try {
const integrity = restored.pragma("integrity_check", { simple: true });
const foreignKeyErrors = restored.pragma("foreign_key_check");
const restoredCounts = rowCounts(restored);
const representative = restored
.prepare(`
SELECT c.name AS className, g.name AS groupName, d.description,
f.front, s.name AS setName, sl.targetType
FROM "Class" c
JOIN "MaterialGroup" g ON g.classId = c.id
JOIN "Deck" d ON d.groupId = g.id
JOIN "Flashcard" f ON f.deckId = d.id
JOIN "SpacedRepetitionSetDeck" sd ON sd.deckId = d.id
JOIN "SpacedRepetitionSet" s ON s.id = sd.setId
JOIN "ShareLink" sl ON sl.groupId = g.id
WHERE d.id = ?
`)
.get("backup-deck");
const quizRelationship = restored
.prepare(`
SELECT q.name AS quizName, question.prompt, option.text, option.isCorrect
FROM "QuizSet" q
JOIN "Question" question ON question.quizSetId = q.id
JOIN "AnswerOption" option ON option.questionId = question.id
WHERE q.id = ?
`)
.get("backup-quiz");
if (
integrity !== "ok" ||
foreignKeyErrors.length !== 0 ||
JSON.stringify(restoredCounts) !== JSON.stringify(sourceCounts) ||
representative?.description !== "Restore drill content" ||
representative?.setName !== "Backup SRS" ||
representative?.targetType !== "GROUP" ||
quizRelationship?.isCorrect !== 1
) {
throw new Error("Restored database did not preserve integrity, counts, content, and relationships");
}
console.info(
JSON.stringify(
{
method: "better-sqlite3 online backup while the WAL source remained open",
sourceWalBytes,
integrity,
foreignKeyErrors,
counts: restoredCounts,
representative,
quizRelationship,
},
null,
2
)
);
} finally {
restored.close();
}
} finally {
source?.close();
rmSync(directory, { recursive: true, force: true });
}