import "server-only"; import { Pool, types } from "pg"; // ═══ Wie Werte aus der Datenbank ankommen ════════════════════════ // // Der Wechsel des Zugriffswegs hat hier eine Falle hinterlassen, die kein // Typprüfer und keiner der Tests fangen konnte. // // Die alte API-Schicht lieferte JSON: ein `date` kam als "2026-08-03" an, // ein `numeric` als Zahl. Genau so steht es in lib/supabase/types.ts, und // darauf baut die gesamte Anwendung — Sortierungen mit localeCompare, // Vergleiche wie `entry_date <= stichtag`, das Ableiten des Status. // // Der `pg`-Treiber macht es anders herum: aus `date` wird ein Date-Objekt, // aus `numeric` eine Zeichenkette. Die Deklarationen blieben dabei // unverändert gültig — sie beschreiben ja nur, was der Code *glaubt*. Der // Fehler zeigt sich erst zur Laufzeit, und im günstigen Fall als Absturz // („a.date.localeCompare is not a function"). Im ungünstigen Fall gar // nicht: ein Datumsvergleich zwischen Date und Zeichenkette wirft nicht, er // liefert bloss das falsche Ergebnis. // // Deshalb wird der Treiber hier auf die Form zurückgestellt, die die Typen // beschreiben. Das ist die kleinere und ehrlichere Änderung, als 49 // Abfragestellen umzuschreiben. // // Nebenbei löst es ein zweites Problem: `date` ist ein Kalendertag ohne // Zeitzone. Als Date-Objekt bekäme er eine — Mitternacht in der Zone des // Servers —, und ein Geburtsdatum verschöbe sich beim Formatieren um einen // Tag. Dieselbe Klasse von Fehler wie im Seed. types.setTypeParser(1082, (v) => v); // date → "YYYY-MM-DD", unverändert types.setTypeParser(1184, (v) => new Date(v).toISOString()); // timestamptz → ISO-8601 mit Z types.setTypeParser(1114, (v) => new Date(v + "Z").toISOString()); // timestamp ohne Zone types.setTypeParser(1700, (v) => Number(v)); // numeric → Zahl // Bewusst *nicht* umgestellt: int8 (bigint). Es kommt nur aus count() und // wird überall mit Number() gelesen; als Zahl geparst verlöre es jenseits // von 2^53 stillschweigend an Genauigkeit. // Die einzige Stelle im Projekt, die `pg` importieren darf. // // Der Grund steht in lib/db/index.ts: eine Abfrage ausserhalb von withUser() // läuft ohne Sitzungskontext und damit — je nachdem, was die vorherige // Anfrage auf derselben gepoolten Verbindung hinterlassen hat — im Namen // einer fremden Person. Deshalb wird der Pool nicht exportiert, sondern nur // die Kysely-Instanz, die ihn benutzt, und eine ESLint-Regel verbietet den // Import von `pg` und von dieser Datei überall sonst. let instance: Pool | undefined; /** * Der Verbindungspool — erst beim ersten Zugriff angelegt, nicht beim Import. * * Der Unterschied ist nicht kosmetisch: `next build` importiert jedes Route- * Modul, um die Seitendaten einzusammeln. Entstünde der Pool dabei, bräuchte * schon der Bau Zugangsdaten zur Datenbank — ein Container-Abbild liesse sich * in einer Baustrecke ohne Produktionsgeheimnisse nicht mehr erzeugen. */ export function getPool(): Pool { if (instance) return instance; const connectionString = process.env.DATABASE_URL; if (!connectionString) { throw new Error( "DATABASE_URL fehlt. Erwartet wird ein PostgreSQL-Verbindungsstring — " + "die Anwendung spricht direkt mit der Datenbank, nicht über eine API-Schicht." ); } instance = new Pool({ connectionString, // Der Standard sind 10; bei serverseitigem Rendering hängt an jeder // Anfrage genau eine Transaktion, und mehr Verbindungen als die Datenbank // zulässt bringen nur Wartezeit an einer anderen Stelle. max: Number(process.env.DATABASE_POOL_MAX ?? 10), // Eine Anfrage, die länger braucht, ist kaputt und soll das melden statt // eine Verbindung zu belegen. statement_timeout: 20_000, idle_in_transaction_session_timeout: 20_000, connectionTimeoutMillis: 10_000, // Verwaltete Anbieter (Azure, RDS, Supabase) verlangen TLS; lokal nicht. ssl: process.env.DATABASE_SSL === "false" ? undefined : { rejectUnauthorized: false }, }); // Ein Fehler auf einer Leerlaufverbindung beendet sonst den Prozess. instance.on("error", (err) => { console.error("Unerwarteter Fehler auf einer Leerlaufverbindung:", err); }); return instance; }