Merge branch 'main' into feat/7-domain-check
# Conflicts: # src/components/domain-admin.tsx
This commit is contained in:
+33
-14
@@ -1,23 +1,42 @@
|
||||
/** Kleine Hilfen für Aufrufe der eigenen API aus Client-Komponenten. */
|
||||
|
||||
async function readError(response: Response): Promise<string> {
|
||||
async function readError(response: Response): Promise<ApiError> {
|
||||
let body: unknown = null;
|
||||
try {
|
||||
const body: unknown = await response.json();
|
||||
if (
|
||||
body !== null &&
|
||||
typeof body === "object" &&
|
||||
"error" in body &&
|
||||
typeof (body as { error: unknown }).error === "string"
|
||||
) {
|
||||
return (body as { error: string }).error;
|
||||
}
|
||||
body = await response.json();
|
||||
} catch {
|
||||
// Antwort ohne JSON-Körper — unten kommt die allgemeine Meldung.
|
||||
}
|
||||
return `Die Anfrage ist fehlgeschlagen (HTTP ${response.status}).`;
|
||||
|
||||
const message =
|
||||
body !== null &&
|
||||
typeof body === "object" &&
|
||||
"error" in body &&
|
||||
typeof (body as { error: unknown }).error === "string"
|
||||
? (body as { error: string }).error
|
||||
: `Die Anfrage ist fehlgeschlagen (HTTP ${response.status}).`;
|
||||
|
||||
return new ApiError(message, response.status, body);
|
||||
}
|
||||
|
||||
export class ApiError extends Error {}
|
||||
/**
|
||||
* Fehler einer eigenen API-Antwort.
|
||||
*
|
||||
* Trägt neben der Meldung auch den Status und den Antwortkörper: Manche
|
||||
* Fehler sind keine Sackgasse, sondern eine Rückfrage — etwa die gemeldete
|
||||
* Dublette beim Ablegen, zu der die Oberfläche den vorhandenen Eintrag
|
||||
* anzeigen will.
|
||||
*/
|
||||
export class ApiError extends Error {
|
||||
readonly status: number;
|
||||
readonly details: unknown;
|
||||
|
||||
constructor(message: string, status = 0, details: unknown = null) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiJson<T>(
|
||||
url: string,
|
||||
@@ -34,7 +53,7 @@ export async function apiJson<T>(
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ApiError(await readError(response));
|
||||
throw await readError(response);
|
||||
}
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
@@ -55,7 +74,7 @@ export async function downloadFile(
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ApiError(await readError(response));
|
||||
throw await readError(response);
|
||||
}
|
||||
|
||||
const disposition = response.headers.get("Content-Disposition") ?? "";
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import "server-only";
|
||||
|
||||
import { toJsonInput } from "@/lib/codes";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
/**
|
||||
* Dublettenprüfung für statische Codes.
|
||||
*
|
||||
* Ein statischer Code trägt seinen Inhalt selbst — zweimal derselbe Inhalt
|
||||
* ergibt zweimal exakt dasselbe Bild. Solche Einträge sind nicht falsch, aber
|
||||
* sie fluten die Liste, und beim Wiederfinden weiß niemand mehr, welcher der
|
||||
* beiden gemeint war.
|
||||
*
|
||||
* Bewusst **nur** für statische Codes: Zwei dynamische Codes mit demselben
|
||||
* Ziel sind ein üblicher Fall — zwei Kampagnen, zwei Kurz-Links, getrennte
|
||||
* Zähler. Da wäre eine Warnung nur lästig.
|
||||
*
|
||||
* Bewusst **ohne** die Gestaltung: Verglichen wird der Inhalt. Derselbe Inhalt
|
||||
* einmal in Schwarz-Weiß für den Druck und einmal in Hausfarben ist zwar
|
||||
* bewusst zweimal angelegt — aber eben auch zweimal derselbe Code. Die
|
||||
* Warnung sagt das und lässt sich übergehen.
|
||||
*/
|
||||
export type StaticDuplicate = {
|
||||
id: string;
|
||||
label: string | null;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sucht einen bereits abgelegten statischen Code mit gleichem Typ und
|
||||
* gleichem Inhalt in derselben Gruppe — den zuletzt angelegten.
|
||||
*
|
||||
* Die Gruppe grenzt bewusst ein: Ein Treffer aus einer fremden Gruppe wäre
|
||||
* nicht bloß nutzlos, er verriete auch deren Bestand.
|
||||
*/
|
||||
export async function findStaticDuplicate(
|
||||
groupId: string,
|
||||
type: string,
|
||||
data: Record<string, unknown>,
|
||||
): Promise<StaticDuplicate | null> {
|
||||
const treffer = await prisma.qrCode.findFirst({
|
||||
where: {
|
||||
mode: "static",
|
||||
type,
|
||||
groupId,
|
||||
// Postgres vergleicht jsonb nach Struktur, nicht nach Schreibweise —
|
||||
// Schlüsselreihenfolge und Leerzeichen spielen keine Rolle.
|
||||
data: { equals: toJsonInput(data) },
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: { id: true, label: true, createdAt: true },
|
||||
});
|
||||
|
||||
if (!treffer) return null;
|
||||
|
||||
return {
|
||||
id: treffer.id,
|
||||
label: treffer.label,
|
||||
createdAt: treffer.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user