Docker-Stack, README und Korrekturen aus dem ersten Durchlauf
- Dockerfile (4 Stufen) und docker-compose.yml mit Postgres-Healthcheck, Volume und direktem Port-Mapping 3000:3000, ohne Reverse Proxy. - Entrypoint wendet beim Start "prisma migrate deploy" an; die Prisma-CLI bekommt eine eigene Build-Stufe, weil sie mehr als nur @prisma/* braucht. - prisma.config.ts durch prisma.config.js ersetzt: laedt ohne TypeScript und ohne dotenv, damit sie im Laufzeit-Image funktioniert. - Prisma-Client wird erst beim ersten Zugriff erzeugt. Vorher scheiterte "next build" beim Einsammeln der Routen-Konfiguration ohne DATABASE_URL. - Postgres-18-Volume auf /var/lib/postgresql gemountet (neues Datenlayout). - npm 11 blockiert Installationsskripte: erlaubte Pakete stehen jetzt im Feld "allowScripts" der package.json. - Lokale Variable "module" ueberall in "qrType" umbenannt (verdeckte sonst die CommonJS-Variable) und Vorschau-Effekt ohne synchrones setState. - README.md auf Deutsch: Setup, Konfiguration, Bedienung, Aufbau, API und Ausblick auf Version 2.
This commit is contained in:
+4
-4
@@ -28,14 +28,14 @@ function asRecord(value: unknown): Record<string, unknown> {
|
||||
|
||||
export function toQrCodeDto(code: QrCode): QrCodeDto {
|
||||
const data = asRecord(code.data);
|
||||
const module = getQrType(code.type);
|
||||
const qrType = getQrType(code.type);
|
||||
|
||||
let target = "";
|
||||
let summary = "";
|
||||
if (module) {
|
||||
if (qrType) {
|
||||
try {
|
||||
target = module.redirectTarget(data);
|
||||
summary = module.summary(data);
|
||||
target = qrType.redirectTarget(data);
|
||||
summary = qrType.summary(data);
|
||||
} catch {
|
||||
// Datensatz passt nicht mehr zum Typ-Schema (z. B. nach einem
|
||||
// Schema-Wechsel). Die Liste soll trotzdem anzeigbar bleiben.
|
||||
|
||||
+32
-12
@@ -5,20 +5,40 @@ import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { PrismaClient } from "@/generated/prisma/client";
|
||||
import { getDatabaseUrl } from "@/lib/env";
|
||||
|
||||
type Client = InstanceType<typeof PrismaClient>;
|
||||
|
||||
// Im Entwicklungsmodus wird das Modul bei jedem Hot-Reload neu ausgewertet.
|
||||
// Ohne diesen Cache entstünde pro Reload ein neuer Verbindungspool.
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma?: ReturnType<typeof createPrismaClient>;
|
||||
};
|
||||
const globalForPrisma = globalThis as unknown as { prisma?: Client };
|
||||
|
||||
function createPrismaClient() {
|
||||
return new PrismaClient({
|
||||
adapter: new PrismaPg({ connectionString: getDatabaseUrl() }),
|
||||
});
|
||||
let client: Client | undefined;
|
||||
|
||||
function getClient(): Client {
|
||||
if (!client) {
|
||||
client =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
adapter: new PrismaPg({ connectionString: getDatabaseUrl() }),
|
||||
});
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalForPrisma.prisma = client;
|
||||
}
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? createPrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
/**
|
||||
* Der Client wird erst beim ersten Zugriff erzeugt.
|
||||
*
|
||||
* Beim Produktionsbuild lädt Next.js die Routen-Module, um ihre Konfiguration
|
||||
* einzusammeln. Zu diesem Zeitpunkt gibt es weder eine Datenbank noch eine
|
||||
* DATABASE_URL — ein sofort verbindender Client würde den Build scheitern
|
||||
* lassen.
|
||||
*/
|
||||
export const prisma: Client = new Proxy({} as Client, {
|
||||
get(_target, property) {
|
||||
const instance = getClient() as unknown as Record<string | symbol, unknown>;
|
||||
const value = instance[property];
|
||||
return typeof value === "function" ? value.bind(instance) : value;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Scanner erkennen einen Code über den Helligkeitsunterschied zwischen
|
||||
* Modul- und Hintergrundfarbe. Zu geringer Kontrast — und ebenso ein heller
|
||||
* Vordergrund auf dunklem Hintergrund („invertierter" Code) — führt bei
|
||||
* Vordergrund auf dunklem Hintergrund („invertierter“ Code) — führt bei
|
||||
* vielen Kameras zu Lesefehlern.
|
||||
*/
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ const MODULES: readonly QrTypeModule[] = [
|
||||
];
|
||||
|
||||
export const qrTypes: Record<QrTypeId, QrTypeModule> = Object.fromEntries(
|
||||
MODULES.map((module) => [module.id, module]),
|
||||
MODULES.map((qrType) => [qrType.id, qrType]),
|
||||
) as Record<QrTypeId, QrTypeModule>;
|
||||
|
||||
const missing = QR_TYPE_IDS.filter((id) => !qrTypes[id]);
|
||||
@@ -122,8 +122,8 @@ export function getQrType(id: string): QrTypeModule | undefined {
|
||||
|
||||
/** Liefert den Typ nur, wenn er in dieser Version nutzbar ist. */
|
||||
export function getActiveQrType(id: string): QrTypeModule | undefined {
|
||||
const module = getQrType(id);
|
||||
return module?.active ? module : undefined;
|
||||
const qrType = getQrType(id);
|
||||
return qrType?.active ? qrType : undefined;
|
||||
}
|
||||
|
||||
export { QR_TYPE_IDS, isQrTypeId };
|
||||
|
||||
+11
-11
@@ -41,7 +41,7 @@ export class QrRequestError extends Error {
|
||||
}
|
||||
|
||||
export type ResolvedQrRequest = {
|
||||
module: QrTypeModule;
|
||||
qrType: QrTypeModule;
|
||||
data: Record<string, unknown>;
|
||||
/** Der Text, der tatsächlich in den QR-Code kodiert wird. */
|
||||
content: string;
|
||||
@@ -52,23 +52,23 @@ export type ResolvedQrRequest = {
|
||||
* bei statischen Codes die Nutzdaten selbst, bei dynamischen die Kurz-URL.
|
||||
*/
|
||||
export function resolveQrRequest(request: QrRequest): ResolvedQrRequest {
|
||||
const module = getActiveQrType(request.type);
|
||||
if (!module) {
|
||||
const qrType = getActiveQrType(request.type);
|
||||
if (!qrType) {
|
||||
throw new QrRequestError(
|
||||
"Dieser QR-Typ ist in dieser Version noch nicht verfügbar.",
|
||||
404,
|
||||
);
|
||||
}
|
||||
|
||||
const validated = module.validate(request.data);
|
||||
const validated = qrType.validate(request.data);
|
||||
if (!validated.ok) {
|
||||
throw new QrRequestError(validated.message);
|
||||
}
|
||||
|
||||
if (request.mode === "dynamic") {
|
||||
if (!module.supportsDynamic) {
|
||||
if (!qrType.supportsDynamic) {
|
||||
throw new QrRequestError(
|
||||
`Der QR-Typ „${module.title}" unterstützt keine dynamischen Codes.`,
|
||||
`Der QR-Typ „${qrType.title}“ unterstützt keine dynamischen Codes.`,
|
||||
);
|
||||
}
|
||||
const slug = request.slug;
|
||||
@@ -76,28 +76,28 @@ export function resolveQrRequest(request: QrRequest): ResolvedQrRequest {
|
||||
throw new QrRequestError("Ungültige Kurz-URL.");
|
||||
}
|
||||
return {
|
||||
module,
|
||||
qrType,
|
||||
data: validated.data,
|
||||
content: shortUrl(slug ?? PREVIEW_SLUG),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
module,
|
||||
qrType,
|
||||
data: validated.data,
|
||||
content: module.encode(validated.data),
|
||||
content: qrType.encode(validated.data),
|
||||
};
|
||||
}
|
||||
|
||||
/** Dateiname für den Download, abgeleitet aus dem Inhalt des Codes. */
|
||||
export function downloadFilename(
|
||||
module: QrTypeModule,
|
||||
qrType: QrTypeModule,
|
||||
data: unknown,
|
||||
extension: string,
|
||||
): string {
|
||||
let base = "qr-code";
|
||||
try {
|
||||
const summary = module
|
||||
const summary = qrType
|
||||
.summary(data)
|
||||
.toLowerCase()
|
||||
.replaceAll("ä", "ae")
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { IconName } from "@/components/icons";
|
||||
/**
|
||||
* Alle QR-Typen, die die Anwendung kennt. V1 implementiert davon nur
|
||||
* "website"; die übrigen Einträge sind als Platzhalter registriert und
|
||||
* erscheinen in der Oberfläche mit dem Badge „Bald verfügbar".
|
||||
* erscheinen in der Oberfläche mit dem Badge „Bald verfügbar“.
|
||||
*/
|
||||
export const QR_TYPE_IDS = [
|
||||
"website",
|
||||
@@ -148,7 +148,7 @@ export function defineComingSoonType(spec: {
|
||||
}): QrTypeModule {
|
||||
const notAvailable = (): never => {
|
||||
throw new QrTypeError(
|
||||
`Der QR-Typ „${spec.title}" ist in dieser Version noch nicht verfügbar.`,
|
||||
`Der QR-Typ „${spec.title}“ ist in dieser Version noch nicht verfügbar.`,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -162,7 +162,7 @@ export function defineComingSoonType(spec: {
|
||||
defaultData: {},
|
||||
validate: () => ({
|
||||
ok: false,
|
||||
message: `Der QR-Typ „${spec.title}" ist in dieser Version noch nicht verfügbar.`,
|
||||
message: `Der QR-Typ „${spec.title}“ ist in dieser Version noch nicht verfügbar.`,
|
||||
}),
|
||||
encode: notAvailable,
|
||||
redirectTarget: notAvailable,
|
||||
|
||||
Reference in New Issue
Block a user