118 lines
4.0 KiB
TypeScript
118 lines
4.0 KiB
TypeScript
import { existsSync, statSync } from "node:fs";
|
|
import { type Client, createClient } from "@libsql/client";
|
|
import dotenv from "dotenv";
|
|
import { drizzle } from "drizzle-orm/libsql";
|
|
import { log } from "../utils/logger.js";
|
|
// Import utilities from db-utils (side-effect-free)
|
|
import {
|
|
ensureDataDirectory,
|
|
ensureDefaultUser,
|
|
getDbPaths,
|
|
repairOrphanedDoseIds,
|
|
repairTrailingHyphenDoseIds,
|
|
runAlterMigrations,
|
|
runDrizzleMigrations,
|
|
} from "./db-utils.js";
|
|
|
|
// Re-export all utilities so existing imports from client.ts keep working
|
|
export {
|
|
buildDbUrl,
|
|
ensureDataDirectory,
|
|
ensureDefaultUser,
|
|
getDataDir,
|
|
getDbPaths,
|
|
repairOrphanedDoseIds,
|
|
repairTrailingHyphenDoseIds,
|
|
runAlterMigrations,
|
|
runDrizzleMigrations,
|
|
} from "./db-utils.js";
|
|
|
|
// Load .env: try cwd first, then parent dir (for local dev running from backend/)
|
|
const envPath = process.env.DOTENV_PATH || (existsSync(".env") ? ".env" : "../.env");
|
|
dotenv.config({ path: envPath });
|
|
|
|
// =============================================================================
|
|
// Database initialization (runs on import)
|
|
// =============================================================================
|
|
|
|
// Use absolute path to ensure it works in Docker
|
|
const { dataDir, dbPath, url } = getDbPaths();
|
|
|
|
log.debug(`[DB] Data directory: ${dataDir}`);
|
|
log.debug(`[DB] Database path: ${dbPath}`);
|
|
log.debug(`[DB] Database URL: ${url}`);
|
|
|
|
// Ensure data directory exists and is writable
|
|
const dirResult = ensureDataDirectory(dataDir);
|
|
if (!dirResult.success) {
|
|
log.error(`[DB] ERROR: Cannot access data directory: ${dirResult.error}`);
|
|
log.error(`[DB] Please ensure the volume mount has correct permissions.`);
|
|
log.error(`[DB] Try running on host: sudo chown -R 1000:1000 ${dataDir}`);
|
|
process.exit(1);
|
|
} else {
|
|
log.debug(`[DB] Data directory is writable`);
|
|
|
|
// Log directory stats
|
|
const stats = statSync(dataDir);
|
|
log.debug(`[DB] Directory permissions: ${stats.mode.toString(8)}`);
|
|
log.debug(`[DB] Directory UID: ${stats.uid}, GID: ${stats.gid}`);
|
|
log.debug(`[DB] Write test successful`);
|
|
}
|
|
|
|
let client: Client;
|
|
try {
|
|
client = createClient({ url });
|
|
log.debug(`[DB] Database client created successfully`);
|
|
} catch (err: unknown) {
|
|
log.error(`[DB] ERROR: Failed to create database client: ${(err as Error).message}`);
|
|
log.error(`[DB] Database path: ${dbPath}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
export const db = drizzle(client);
|
|
|
|
// Auto-run migrations (self-healing database)
|
|
async function runMigrations() {
|
|
// Run drizzle-kit generated migrations
|
|
log.info(`[DB] Running migrations...`);
|
|
const migrateResult = await runDrizzleMigrations(db);
|
|
if (!migrateResult.success) {
|
|
log.error(`[DB] Migration error: ${migrateResult.error}`);
|
|
}
|
|
|
|
// Run ALTER TABLE migrations for backward compatibility
|
|
const alterResult = await runAlterMigrations(client);
|
|
if (alterResult.errors.length > 0) {
|
|
alterResult.errors.forEach((err) => log.error(`[DB] ALTER migration error: ${err}`));
|
|
}
|
|
log.debug(`[DB] Tables verified/created`);
|
|
|
|
// Repair dose IDs with trailing hyphens (from frontend takenBy bug)
|
|
const trailingResult = await repairTrailingHyphenDoseIds(client);
|
|
if (trailingResult.repaired > 0) {
|
|
log.info(`[DB] Repaired ${trailingResult.repaired} dose IDs with trailing hyphens`);
|
|
}
|
|
if (trailingResult.errors.length > 0) {
|
|
trailingResult.errors.forEach((err) => log.error(`[DB] Trailing-hyphen repair error: ${err}`));
|
|
}
|
|
|
|
// Repair orphaned dose tracking IDs from past schedule changes
|
|
const repairResult = await repairOrphanedDoseIds(client);
|
|
if (repairResult.repaired > 0) {
|
|
log.info(`[DB] Repaired ${repairResult.repaired} orphaned dose tracking IDs`);
|
|
}
|
|
if (repairResult.errors.length > 0) {
|
|
repairResult.errors.forEach((err) => log.error(`[DB] Dose repair error: ${err}`));
|
|
}
|
|
|
|
// If auth is disabled, ensure a default user exists (ID=1)
|
|
const authEnabled = process.env.AUTH_ENABLED === "true";
|
|
const created = await ensureDefaultUser(client, authEnabled);
|
|
if (created) {
|
|
log.info(`[DB] Created default user for auth-disabled mode`);
|
|
}
|
|
}
|
|
|
|
// Export promise so server can await it before starting
|
|
export const migrationsReady = runMigrations();
|