import { Pool, type PoolConfig } from 'pg'; import type { AssessorRepository } from '../src/persistence/assessor-repository'; import { LocalAssessorRepository } from '../src/persistence/local-assessor-repository'; import { runDatabaseMigrations } from './postgres/migration-runner'; import { PostgresAssessorRepository } from './postgres/postgres-assessor-repository'; export type PersistenceKind = 'memory' | 'postgresql'; export interface RepositoryRuntime { repository: AssessorRepository; persistence: PersistenceKind; close(): Promise; } export interface RepositoryEnvironment { ASSESSOR_DATABASE_URL?: string; ASSESSOR_DATABASE_SSL?: string; ASSESSOR_DATABASE_POOL_SIZE?: string; ASSESSOR_POSTGRES_DB?: string; ASSESSOR_POSTGRES_HOST?: string; ASSESSOR_POSTGRES_USER?: string; ASSESSOR_POSTGRES_PASSWORD?: string; ASSESSOR_POSTGRES_PORT?: string; } function parsePoolSize(value: string | undefined): number { if (value === undefined || value.trim() === '') return 10; const parsed = Number(value); if (!Number.isInteger(parsed) || parsed < 1 || parsed > 20) { throw new Error('ASSESSOR_DATABASE_POOL_SIZE muss eine Ganzzahl zwischen 1 und 20 sein.'); } return parsed; } export function sslConfiguration(value: string | undefined): PoolConfig['ssl'] { const mode = value?.trim().toLowerCase() || 'disable'; if (mode === 'disable') return false; if (mode === 'require') return { rejectUnauthorized: true }; throw new Error('ASSESSOR_DATABASE_SSL muss disable oder require sein.'); } export function repositoryConnectionString(environment: RepositoryEnvironment): string | undefined { const explicitConnectionString = environment.ASSESSOR_DATABASE_URL?.trim(); if (explicitConnectionString) return explicitConnectionString; const password = environment.ASSESSOR_POSTGRES_PASSWORD?.trim(); if (!password) return undefined; const host = environment.ASSESSOR_POSTGRES_HOST?.trim() || '127.0.0.1'; return `postgresql://${encodeURIComponent(environment.ASSESSOR_POSTGRES_USER?.trim() || 'assessor')}:${encodeURIComponent(password)}@${host}:${environment.ASSESSOR_POSTGRES_PORT?.trim() || '5432'}/${encodeURIComponent(environment.ASSESSOR_POSTGRES_DB?.trim() || 'assessor')}`; } export async function createRepositoryRuntime(environment: RepositoryEnvironment = { ASSESSOR_DATABASE_URL: process.env.ASSESSOR_DATABASE_URL, ASSESSOR_DATABASE_SSL: process.env.ASSESSOR_DATABASE_SSL, ASSESSOR_DATABASE_POOL_SIZE: process.env.ASSESSOR_DATABASE_POOL_SIZE, ASSESSOR_POSTGRES_DB: process.env.ASSESSOR_POSTGRES_DB, ASSESSOR_POSTGRES_HOST: process.env.ASSESSOR_POSTGRES_HOST, ASSESSOR_POSTGRES_USER: process.env.ASSESSOR_POSTGRES_USER, ASSESSOR_POSTGRES_PASSWORD: process.env.ASSESSOR_POSTGRES_PASSWORD, ASSESSOR_POSTGRES_PORT: process.env.ASSESSOR_POSTGRES_PORT }): Promise { const connectionString = repositoryConnectionString(environment); if (!connectionString) { return { repository: new LocalAssessorRepository(), persistence: 'memory', async close() {} }; } let databaseUrl: URL; try { databaseUrl = new URL(connectionString); } catch { throw new Error('ASSESSOR_DATABASE_URL ist keine gültige PostgreSQL-URL.'); } if (!['postgres:', 'postgresql:'].includes(databaseUrl.protocol)) { throw new Error('ASSESSOR_DATABASE_URL muss postgres:// oder postgresql:// verwenden.'); } const loopbackHosts = new Set(['localhost', '127.0.0.1', '[::1]']); const sslMode = environment.ASSESSOR_DATABASE_SSL?.trim() || (loopbackHosts.has(databaseUrl.hostname) ? 'disable' : 'require'); const pool = new Pool({ connectionString, ssl: sslConfiguration(sslMode), max: parsePoolSize(environment.ASSESSOR_DATABASE_POOL_SIZE), connectionTimeoutMillis: 5000, idleTimeoutMillis: 30000, application_name: 'assessor' }); try { await pool.query('SELECT 1'); await runDatabaseMigrations(pool); const repository = await PostgresAssessorRepository.create(pool); return { repository, persistence: 'postgresql', close: () => pool.end() }; } catch (error) { await pool.end().catch(() => undefined); throw error; } }