feat: Datenmodell für Katalog, Tracking und Universen
Prisma-Schema in vier Schichten: Konten und Zugang, gemeinsamer Katalog (über die TMDB-ID dedupliziert), persönliches Tracking je Nutzer sowie Universen mit mehreren Reihenfolgen. Dazu Benachrichtigungskanäle und Protokoll der Hintergrundläufe. Prisma 7 verlangt einen Treiber-Adapter statt der URL im Schema; die Konfiguration liegt deshalb in prisma.config.mjs — bewusst als .mjs, damit die CLI auch im schlanken Produktionsimage ohne TypeScript-Loader läuft.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import "server-only";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { PrismaClient } from "@/generated/prisma/client";
|
||||
import { env } from "./env";
|
||||
|
||||
/**
|
||||
* Prisma-Client als Singleton.
|
||||
*
|
||||
* Im Entwicklungsmodus lädt Next Module bei jedem Hot-Reload neu; ohne das
|
||||
* globale Zwischenspeichern entstünde pro Reload ein neuer Verbindungspool.
|
||||
*/
|
||||
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
|
||||
|
||||
function createClient() {
|
||||
const adapter = new PrismaPg({ connectionString: env.databaseUrl });
|
||||
return new PrismaClient({
|
||||
adapter,
|
||||
log: process.env.NODE_ENV === "development" ? ["warn", "error"] : ["error"],
|
||||
});
|
||||
}
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? createClient();
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import "server-only";
|
||||
|
||||
/**
|
||||
* Zentraler, geprüfter Zugriff auf die Umgebungsvariablen.
|
||||
*
|
||||
* Absicht: Tippfehler und fehlende Pflichtwerte fallen beim Start auf, nicht
|
||||
* erst irgendwo tief in einem Request. Optionale Bausteine (TMDB, SMTP, OIDC)
|
||||
* melden sich stattdessen über `isConfigured`-Flags ab.
|
||||
*/
|
||||
|
||||
function required(name: string, fallback?: string): string {
|
||||
const value = process.env[name] ?? fallback;
|
||||
if (!value) {
|
||||
throw new Error(
|
||||
`Umgebungsvariable ${name} fehlt. Bitte in der .env setzen — siehe .env.example.`,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function optional(name: string, fallback = ""): string {
|
||||
return process.env[name]?.trim() ?? fallback;
|
||||
}
|
||||
|
||||
function bool(name: string, fallback: boolean): boolean {
|
||||
const raw = process.env[name]?.trim().toLowerCase();
|
||||
if (raw === undefined || raw === "") return fallback;
|
||||
return raw === "1" || raw === "true" || raw === "yes" || raw === "ja";
|
||||
}
|
||||
|
||||
function int(name: string, fallback: number): number {
|
||||
const raw = Number.parseInt(process.env[name] ?? "", 10);
|
||||
return Number.isFinite(raw) ? raw : fallback;
|
||||
}
|
||||
|
||||
const isBuildPhase = process.env.NEXT_PHASE === "phase-production-build";
|
||||
|
||||
export const env = {
|
||||
appName: optional("APP_NAME", "Flimmer"),
|
||||
appUrl: optional("APP_URL", "http://localhost:3000").replace(/\/+$/, ""),
|
||||
databaseUrl: isBuildPhase
|
||||
? optional("DATABASE_URL", "postgresql://build:build@localhost:5432/build")
|
||||
: required("DATABASE_URL"),
|
||||
authSecret: isBuildPhase ? optional("AUTH_SECRET", "build-time-placeholder") : required("AUTH_SECRET"),
|
||||
dataDir: optional("DATA_DIR", "./data"),
|
||||
timezone: optional("TZ", "Europe/Berlin"),
|
||||
|
||||
allowRegistrationDefault: bool("ALLOW_REGISTRATION", true),
|
||||
|
||||
tmdb: {
|
||||
apiKey: optional("TMDB_API_KEY"),
|
||||
accessToken: optional("TMDB_ACCESS_TOKEN"),
|
||||
language: optional("TMDB_LANGUAGE", "de-DE"),
|
||||
region: optional("TMDB_REGION", "DE"),
|
||||
get isConfigured() {
|
||||
return Boolean(this.apiKey || this.accessToken);
|
||||
},
|
||||
},
|
||||
|
||||
scheduler: {
|
||||
enabled: bool("ENABLE_SCHEDULER", false),
|
||||
hour: Math.min(23, Math.max(0, int("SYNC_HOUR", 4))),
|
||||
cronSecret: optional("CRON_SECRET"),
|
||||
},
|
||||
|
||||
smtp: {
|
||||
host: optional("SMTP_HOST"),
|
||||
port: int("SMTP_PORT", 587),
|
||||
user: optional("SMTP_USER"),
|
||||
password: optional("SMTP_PASSWORD"),
|
||||
secure: bool("SMTP_SECURE", false),
|
||||
from: optional("SMTP_FROM", "Flimmer <flimmer@localhost>"),
|
||||
get isConfigured() {
|
||||
return Boolean(this.host);
|
||||
},
|
||||
},
|
||||
|
||||
oidc: {
|
||||
issuer: optional("OIDC_ISSUER"),
|
||||
clientId: optional("OIDC_CLIENT_ID"),
|
||||
clientSecret: optional("OIDC_CLIENT_SECRET"),
|
||||
scopes: optional("OIDC_SCOPES", "openid profile email"),
|
||||
buttonLabel: optional("OIDC_BUTTON_LABEL", "Mit SSO anmelden"),
|
||||
autoRegister: bool("OIDC_AUTO_REGISTER", true),
|
||||
get isConfigured() {
|
||||
return Boolean(this.issuer && this.clientId && this.clientSecret);
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const isProduction = process.env.NODE_ENV === "production";
|
||||
Reference in New Issue
Block a user