import { mkdirSync } from 'node:fs'; import { homedir } from 'node:os'; import { join } from 'node:path'; import { Global, Inject, Logger, Module, type OnApplicationShutdown, type OnModuleInit, } from '@nestjs/common'; import { createDb, createPgliteDb, runPgliteMigrations, type Db, type DbHandle, } from '@mosaicstack/db'; import { createStorageAdapter, type StorageAdapter } from '@mosaicstack/storage'; import type { MosaicConfig } from '@mosaicstack/config'; import { MOSAIC_CONFIG } from '../config/config.module.js'; export const DB_HANDLE = 'DB_HANDLE'; export const DB = 'DB'; export const STORAGE_ADAPTER = 'STORAGE_ADAPTER'; @Global() @Module({ providers: [ { provide: DB_HANDLE, useFactory: (config: MosaicConfig): DbHandle => { if (config.tier === 'local') { const dataDir = join(homedir(), '.config', 'mosaic', 'gateway', 'pglite'); mkdirSync(dataDir, { recursive: true }); return createPgliteDb(dataDir); } return createDb(config.storage.type === 'postgres' ? config.storage.url : undefined); }, inject: [MOSAIC_CONFIG], }, { provide: DB, useFactory: (handle: DbHandle): Db => handle.db, inject: [DB_HANDLE], }, { provide: STORAGE_ADAPTER, useFactory: (config: MosaicConfig): StorageAdapter => createStorageAdapter(config.storage), inject: [MOSAIC_CONFIG], }, ], exports: [DB, STORAGE_ADAPTER], }) export class DatabaseModule implements OnApplicationShutdown, OnModuleInit { private readonly logger = new Logger(DatabaseModule.name); constructor( @Inject(DB_HANDLE) private readonly handle: DbHandle, @Inject(STORAGE_ADAPTER) private readonly storageAdapter: StorageAdapter, @Inject(MOSAIC_CONFIG) private readonly config: MosaicConfig, ) {} // Migrations must complete before any module that injects DB starts serving // requests. NestJS awaits onModuleInit before app.listen(), and modules that // inject DB are initialized after this one — so all DB-dependent code sees a // populated schema before the first HTTP request lands. // // Local (PGlite) tier: we run gateway-DB migrations explicitly here. The // storage adapter writes to a separate PGlite directory and only manages its // own KV tables, so we still call its migrate() afterwards. // // Postgres tier: PostgresAdapter.migrate() already calls runMigrations() on // the same DATABASE_URL, so a single call covers both the gateway DB and // the storage tables. We deliberately do NOT call runMigrations() here to // avoid opening a second short-lived connection and doubling startup cost. async onModuleInit(): Promise { if (this.config.tier === 'local') { this.logger.log('Applying PGlite schema migrations...'); await runPgliteMigrations(this.handle); } this.logger.log(`Initializing storage adapter (${this.storageAdapter.name})...`); await this.storageAdapter.migrate(); } async onApplicationShutdown(): Promise { await Promise.all([this.handle.close(), this.storageAdapter.close()]); } }