Files

204 lines
5.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const Database = require('better-sqlite3');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const MIGRATIONS_DIR = path.join(__dirname, '../migrations');
const MIGRATION_BASELINE_CUTOFF = '008_web_push_notifications.sql';
function hashPassword(password) {
return crypto.createHash('sha256').update(password).digest('hex');
}
function getSqliteDbPath() {
return process.env.SQLITE_DB_PATH || path.join(process.cwd(), '.data', 'moontv.db');
}
function ensureDataDir(dbPath) {
const dataDir = path.dirname(dbPath);
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
}
function configureDatabase(db) {
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.pragma('busy_timeout = 5000');
}
function getMigrationFiles() {
if (!fs.existsSync(MIGRATIONS_DIR)) {
throw new Error(`Migrations directory not found: ${MIGRATIONS_DIR}`);
}
return fs
.readdirSync(MIGRATIONS_DIR)
.filter((file) => file.endsWith('.sql'))
.sort((a, b) => a.localeCompare(b));
}
function isIgnorableMigrationError(error) {
const message = error instanceof Error ? error.message : String(error || '');
return (
message.includes('table') && message.includes('already exists') ||
message.includes('index') && message.includes('already exists') ||
message.includes('duplicate column name')
);
}
function splitSqlStatements(sql) {
const withoutLineComments = sql
.split('\n')
.filter((line) => !line.trim().startsWith('--'))
.join('\n');
return withoutLineComments
.split(';')
.map((statement) => statement.trim())
.filter((statement) => statement.length > 0);
}
function tableExists(db, tableName) {
const row = db
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
.get(tableName);
return Boolean(row);
}
function ensureMigrationTable(db) {
db.exec(`
CREATE TABLE IF NOT EXISTS schema_migrations (
filename TEXT PRIMARY KEY,
applied_at INTEGER NOT NULL
)
`);
}
function getAppliedMigrations(db) {
return new Set(
db.prepare('SELECT filename FROM schema_migrations').all().map((row) => row.filename)
);
}
function markMigrationApplied(db, filename) {
db.prepare(
'INSERT OR IGNORE INTO schema_migrations (filename, applied_at) VALUES (?, ?)'
).run(filename, Date.now());
}
function seedExistingMigrationBaseline(db, migrationFiles, hadExistingSchema) {
const applied = getAppliedMigrations(db);
if (!hadExistingSchema || applied.size > 0) return;
for (const file of migrationFiles) {
if (file.localeCompare(MIGRATION_BASELINE_CUTOFF) < 0) {
markMigrationApplied(db, file);
}
}
}
function runMigrations(db) {
const migrationFiles = getMigrationFiles();
const hadExistingSchema = tableExists(db, 'users');
ensureMigrationTable(db);
seedExistingMigrationBaseline(db, migrationFiles, hadExistingSchema);
for (const file of migrationFiles) {
const applied = getAppliedMigrations(db);
if (applied.has(file)) {
console.log(`⏭️ Migration already applied: ${file}`);
continue;
}
const migrationPath = path.join(MIGRATIONS_DIR, file);
const sql = fs.readFileSync(migrationPath, 'utf8');
const statements = splitSqlStatements(sql);
console.log(`▶️ Applying migration: ${file}`);
for (const statement of statements) {
try {
db.exec(statement);
} catch (error) {
if (isIgnorableMigrationError(error)) {
console.log(`⏭️ Statement skipped in ${file}: ${error.message}`);
continue;
}
throw error;
}
}
markMigrationApplied(db, file);
console.log(`✅ Migration applied: ${file}`);
}
}
function ensureDefaultAdmin(db) {
const username = process.env.USERNAME || 'admin';
const password = process.env.PASSWORD || '123456789';
const passwordHash = hashPassword(password);
const existingUser = db
.prepare('SELECT username FROM users WHERE username = ? LIMIT 1')
.get(username);
if (existingUser) {
console.log(`️ Admin user already exists: ${username}`);
return;
}
db.prepare(`
INSERT INTO users (
username, password_hash, role, created_at,
playrecord_migrated, favorite_migrated, skip_migrated
)
VALUES (?, ?, 'owner', ?, 1, 1, 1)
`).run(username, passwordHash, Date.now());
console.log(`✅ Default admin user created: ${username}`);
}
function initSQLiteDatabase() {
const dbPath = getSqliteDbPath();
ensureDataDir(dbPath);
let db;
try {
db = new Database(dbPath);
} catch (error) {
if (error && typeof error.message === 'string' && error.message.includes('Could not locate the bindings file')) {
console.error('❌ better-sqlite3 native binding is missing or incompatible with current Node.js runtime.');
console.error('💡 Please run: pnpm rebuild better-sqlite3');
console.error('💡 If you recently changed Node.js version, reinstall dependencies or rebuild native modules.');
}
throw error;
}
configureDatabase(db);
console.log('📦 Initializing SQLite database...');
console.log('📍 Database location:', dbPath);
try {
runMigrations(db);
ensureDefaultAdmin(db);
} finally {
db.close();
}
console.log('🎉 SQLite database is ready!');
}
module.exports = {
initSQLiteDatabase,
getSqliteDbPath,
};
if (require.main === module) {
try {
initSQLiteDatabase();
} catch (err) {
console.error('❌ SQLite initialization failed:', err);
process.exit(1);
}
}