102 lines
3.8 KiB
TypeScript
102 lines
3.8 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<boolean> {
|
|
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<string>('DATABASE_PATH', '/data/hipctf/db.sqlite');
|
|
}
|
|
} |