import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { DataSource } from 'typeorm'; @Injectable() export class DatabaseInitService implements OnApplicationBootstrap { private readonly logger = new Logger(DatabaseInitService.name); private initialized = false; constructor( private readonly dataSource: DataSource, private readonly config: ConfigService, ) {} /** * Explicit, awaitable database initialization. * * - Initializes the DataSource (idempotent — TypeORM returns the existing * connection if already initialized). * - Runs all pending migrations in order. * - Verifies that the expected schema and seeded data are present. * * `main.ts` calls this BEFORE `app.listen()` so the HTTP server never * accepts traffic until the DB is fully migrated and seeded. */ async init(): Promise { if (this.initialized) return; if (!this.dataSource.isInitialized) { await this.dataSource.initialize(); } // Enforce WAL journal mode on every startup. The InitSchema migration // also sets it for fresh DBs, but warm DBs (where migrations are // skipped) need it applied here so PRAGMA journal_mode is always 'wal'. await this.ensureWalMode(); const pending = await this.dataSource.showMigrations(); const pendingList: string[] = Array.isArray(pending) ? pending : []; this.logger.log(`Pending migrations: ${pendingList.length}`); if (pendingList.length > 0 || !(await this.hasCategoryTable())) { const ran = await this.dataSource.runMigrations({ transaction: 'each' }); this.logger.log(`Ran ${ran.length} migration(s): ${ran.map((m) => m.name).join(', ')}`); } else { this.logger.log('Database schema is up to date'); } try { await this.verifySeed(); } catch (e) { this.logger.warn(`verifySeed skipped: ${(e as Error).message}`); } this.initialized = true; } async onApplicationBootstrap(): Promise { await this.init(); } /** * Read PRAGMA journal_mode and switch to WAL if not already enabled. * Persisted for file-backed databases; idempotent and safe on every * startup. */ private async ensureWalMode(): Promise { try { const rows: any[] = await this.dataSource.query('PRAGMA journal_mode'); const current = String(rows?.[0]?.journal_mode ?? '').toLowerCase(); if (current === 'wal') { this.logger.log('PRAGMA journal_mode=wal (already set)'); return; } const setRows: any[] = await this.dataSource.query('PRAGMA journal_mode = WAL'); const actual = String(setRows?.[0]?.journal_mode ?? '').toLowerCase(); this.logger.log(`PRAGMA journal_mode set (was='${current}', now='${actual}')`); } catch (e) { this.logger.warn(`ensureWalMode skipped: ${(e as Error).message}`); } } private async verifySeed(): Promise { const categoryCount = await this.dataSource.query('SELECT COUNT(*) AS c FROM "category"').then((rows: any[]) => Number(rows[0]?.c ?? 0)); const settingsCount = await this.dataSource.query('SELECT COUNT(*) AS c FROM "setting"').then((rows: any[]) => Number(rows[0]?.c ?? 0)); this.logger.log(`Seed verified: ${categoryCount} categories, ${settingsCount} settings`); if (categoryCount < 6) { this.logger.warn(`Expected at least 6 seeded categories, found ${categoryCount}`); } } private async hasCategoryTable(): Promise { try { const rows: any[] = await this.dataSource.query("SELECT name FROM sqlite_master WHERE type='table' AND name='category'"); return rows.length > 0; } catch { return false; } } getDbPath(): string { return this.config.get('DATABASE_PATH', '/data/hipctf/db.sqlite'); } }