Das Feld „Name (optional)" stand nur im Dynamisch-Block. Bei einem statischen Code gab es damit keine Moeglichkeit, ihm einen Namen zum Wiederfinden zu geben. Jetzt gilt es in beiden Betriebsarten und tut zweierlei: Es bestimmt den Dateinamen beim Herunterladen — aus „Plakat Eingang" wird qr-plakat-eingang.png statt qr-example-com-plakat.png — und es laesst sich damit ein statischer Code in „Meine Codes" ablegen. Ein abgelegter statischer Code ist ein Schnappschuss, kein steuerbares Ziel. Aenderbar ist nur der Name; Inhalt und Gestaltung bleiben, wie sie beim Ablegen waren, denn genau das steckt im gedruckten Bild. Er bekommt keinen Kurz-Link, ist unter /r nicht erreichbar und zaehlt keine Scans — in der Liste steht dort „statisch" und ein Strich. Wer nach dem Ablegen den Inhalt aendert, bekommt das Ablegen erneut angeboten; der vorherige Eintrag bleibt als eigener Schnappschuss. Dafuer traegt QrCode jetzt `mode`, und `slug` darf leer sein. In PostgreSQL schliessen sich mehrere NULL in einem eindeutigen Index nicht aus, die Bedingung „je Domain eindeutig" gilt fuer dynamische Codes also unveraendert. Refs #2
100 lines
3.2 KiB
TypeScript
100 lines
3.2 KiB
TypeScript
import "server-only";
|
|
|
|
import type { Prisma, QrCode } from "@/generated/prisma/client";
|
|
import type { QrCodeDto } from "@/lib/dto";
|
|
import { landingUrl, shortUrl } from "@/lib/env";
|
|
import type { EcLevel } from "@/lib/qr/design";
|
|
import { getQrType } from "@/lib/qr/registry";
|
|
import type { QrTypeId } from "@/lib/qr/type-module";
|
|
|
|
export type { QrCodeDto };
|
|
|
|
/**
|
|
* Die Typ-Module liefern validierte, JSON-serialisierbare Objekte. Prisma
|
|
* kann das aus `Record<string, unknown>` nicht ableiten, deshalb hier die
|
|
* einzige Stelle mit einer expliziten Umdeutung.
|
|
*/
|
|
export function toJsonInput(
|
|
data: Record<string, unknown>,
|
|
): Prisma.InputJsonValue {
|
|
return data as Prisma.InputJsonValue;
|
|
}
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
? (value as Record<string, unknown>)
|
|
: {};
|
|
}
|
|
|
|
/** Ein Code samt der Beziehungen, die für die Anzeige gebraucht werden. */
|
|
export type QrCodeWithRelations = QrCode & {
|
|
group?: { name: string } | null;
|
|
domain?: { hostname: string } | null;
|
|
createdBy?: { name: string | null; email: string } | null;
|
|
aliases?: { slug: string }[];
|
|
};
|
|
|
|
export function toQrCodeDto(code: QrCodeWithRelations): QrCodeDto {
|
|
const data = asRecord(code.data);
|
|
const qrType = getQrType(code.type);
|
|
|
|
const istStatisch = code.mode === "static";
|
|
|
|
let target = "";
|
|
let summary = "";
|
|
if (qrType) {
|
|
try {
|
|
// Ein statischer Code führt nirgendwohin — sein Inhalt steckt in ihm
|
|
// selbst. Als „Ziel“ bleibt deshalb nichts übrig; die Zusammenfassung
|
|
// sagt trotzdem, was drinsteht.
|
|
// Typen mit eigener Landingpage führen auf eine Seite dieser Anwendung.
|
|
// Die ist als „Ziel“ das Ehrlichste — und obendrein anklickbar, um sie
|
|
// sich anzusehen.
|
|
target =
|
|
istStatisch || !code.slug
|
|
? ""
|
|
: qrType.hasLandingPage
|
|
? landingUrl(code.slug, code.domain?.hostname)
|
|
: 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.
|
|
summary = "Ungültige Daten";
|
|
}
|
|
} else {
|
|
summary = "Unbekannter Typ";
|
|
}
|
|
|
|
return {
|
|
id: code.id,
|
|
mode: istStatisch ? "static" : "dynamic",
|
|
slug: code.slug,
|
|
aliases: (code.aliases ?? []).map((alias) => alias.slug),
|
|
type: code.type as QrTypeId,
|
|
label: code.label,
|
|
data,
|
|
shortUrl: code.slug ? shortUrl(code.slug, code.domain?.hostname) : null,
|
|
target,
|
|
summary,
|
|
design: {
|
|
fgColor: code.fgColor,
|
|
bgColor: code.bgColor,
|
|
size: code.size,
|
|
ecLevel: code.ecLevel as EcLevel,
|
|
margin: code.margin,
|
|
},
|
|
scans: code.scans,
|
|
lastScanAt: code.lastScanAt?.toISOString() ?? null,
|
|
createdAt: code.createdAt.toISOString(),
|
|
updatedAt: code.updatedAt.toISOString(),
|
|
groupId: code.groupId,
|
|
groupName: code.group?.name ?? null,
|
|
domainId: code.domainId,
|
|
domainHostname: code.domain?.hostname ?? null,
|
|
createdByName: code.createdBy
|
|
? (code.createdBy.name?.trim() || code.createdBy.email)
|
|
: null,
|
|
};
|
|
}
|