Initial commit: Kalendartool (Next.js, Prisma, Docker)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Kühn
2026-10-08 14:38:04 +02:00
commit bc49c3074e
94 changed files with 12812 additions and 0 deletions

23
lib/db/client.ts Normal file
View File

@@ -0,0 +1,23 @@
import { PrismaClient } from '@prisma/client';
/**
* Globaler Prisma-Client.
*
* In der Entwicklung startet Next.js bei Hot Reload mehrfach Modul-Instanzen;
* ohne Caching wuerde jede Instanz eigene Verbindungen aufbauen und die
* Datenbank mit Verbindungen ueberfluten. Daher wird die Instanz auf
* `globalThis` gecacht (offizielles Prisma-Rezept fuer Next.js).
*/
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma: PrismaClient =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['warn', 'error'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}

43
lib/db/tx.ts Normal file
View File

@@ -0,0 +1,43 @@
/**
* Transaktions-Helper: SERIALIZABLE mit Retry bei Serialisierungs-
* konflikten (Prisma-Fehlercode P2034).
*
* Postgres bricht bei Parallelitaets-Anomalien unter SERIALIZABLE eine
* der beteiligten Transaktionen ab. Wiederholbare Operationen (z. B.
* Registrierung mit Invite-Verbrauch, Rollen-Aenderung) werden dann
* einfach erneut ausgefuehrt - der Konflikt loest sich auf, weil die
* andere Transaktion inzwischen committet hat.
*/
import { Prisma } from '@prisma/client';
import { prisma } from '@/lib/db/client';
/** Ist es ein wiederholbarer Serialisierungs-Konflikt? */
function isSerializationError(error: unknown): boolean {
return (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2034'
);
}
/**
* Fuehrt die Operation in einer SERIALIZABLE-Transaktion aus und
* wiederholt sie bei Serialisierungs-Konflikten (max. 3 Nachlaeufe).
*/
export async function withSerializableTx<T>(
operation: (tx: Prisma.TransactionClient) => Promise<T>,
maxRetries = 3,
): Promise<T> {
for (let attempt = 0; ; attempt++) {
try {
return await prisma.$transaction(operation, {
isolationLevel: 'Serializable',
});
} catch (error) {
if (attempt >= maxRetries || !isSerializationError(error)) {
throw error;
}
// Naechster Versuch: Die abgebrochene Transaktion hat nichts
// committet, ein erneuter Lauf sieht den aktuellen Stand.
}
}
}