Assessor_2.0/server/repository-factory.ts

98 lines
3.8 KiB
TypeScript

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<void>;
}
export interface RepositoryEnvironment {
ASSESSOR_DATABASE_URL?: string;
ASSESSOR_DATABASE_SSL?: string;
ASSESSOR_DATABASE_POOL_SIZE?: string;
ASSESSOR_POSTGRES_DB?: 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;
}
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 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_USER: process.env.ASSESSOR_POSTGRES_USER,
ASSESSOR_POSTGRES_PASSWORD: process.env.ASSESSOR_POSTGRES_PASSWORD,
ASSESSOR_POSTGRES_PORT: process.env.ASSESSOR_POSTGRES_PORT
}): Promise<RepositoryRuntime> {
const explicitConnectionString = environment.ASSESSOR_DATABASE_URL?.trim();
const localPassword = environment.ASSESSOR_POSTGRES_PASSWORD?.trim();
const connectionString = explicitConnectionString || (localPassword
? `postgresql://${encodeURIComponent(environment.ASSESSOR_POSTGRES_USER?.trim() || 'assessor')}:${encodeURIComponent(localPassword)}@127.0.0.1:${environment.ASSESSOR_POSTGRES_PORT?.trim() || '5432'}/${encodeURIComponent(environment.ASSESSOR_POSTGRES_DB?.trim() || 'assessor')}`
: undefined);
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;
}
}