AI Implementation feature(820): Project Scaffold and Platform Foundations (#1)
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user