ci/woodpecker/push/publish Pipeline failed
Co-authored-by: ops-deploy-01 <[email protected]>
114 lines
4.3 KiB
TypeScript
114 lines
4.3 KiB
TypeScript
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,
|
|
getMigrationStatus,
|
|
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.
|
|
//
|
|
// #1392: we DO verify afterwards (getMigrationStatus opens one short-lived
|
|
// connection) and refuse to start on an incomplete schema. A gateway that
|
|
// boots "healthy" on an empty or partial database is precisely the failure
|
|
// that shipped in the T63 batch: silent at startup, catastrophic later.
|
|
async onModuleInit(): Promise<void> {
|
|
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();
|
|
|
|
if (this.config.storage.type === 'postgres') {
|
|
const status = await getMigrationStatus(this.config.storage.url);
|
|
if (!status.complete) {
|
|
this.logger.error(
|
|
`Database schema incomplete: ${status.appliedCount.toString()}/${status.expectedCount.toString()} migrations applied ` +
|
|
`(last expected: ${status.expectedLastTag}). ` +
|
|
'Refusing to start on a partial schema — see issues #1392/#1402. ' +
|
|
"Remediation: re-run 'mosaic gateway install' (it now verifies), or apply migrations manually.",
|
|
);
|
|
throw new Error(
|
|
`Database schema incomplete: ${status.appliedCount.toString()}/${status.expectedCount.toString()} migrations applied`,
|
|
);
|
|
}
|
|
this.logger.log(
|
|
`Database schema verified: ${status.appliedCount.toString()}/${status.expectedCount.toString()} migrations applied.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
async onApplicationShutdown(): Promise<void> {
|
|
await Promise.all([this.handle.close(), this.storageAdapter.close()]);
|
|
}
|
|
}
|