Files
qrcode-generator/src/lib/api-client.ts
T
Florian Wilde 60f46bcdb5 Warnung vor doppelt abgelegten statischen Codes
Ein statischer Code traegt seinen Inhalt selbst — zweimal derselbe Inhalt
ergibt zweimal exakt dasselbe Bild. Bisher entstand der zweite Eintrag
stillschweigend.

Das Anlegen antwortet nun mit 409 und nennt den vorhandenen Eintrag samt
Namen und Datum; die Oberflaeche zeigt das als Warnung mit dem Knopf
"Trotzdem ablegen". Eine Rueckfrage, keine Sperre — zwei Fassungen
desselben Codes koennen gewollt sein.

Verglichen wird Typ und Inhalt innerhalb der Gruppe, nicht die Gestaltung.
Dynamische Codes bleiben aussen vor: zwei Kurz-Links auf dasselbe Ziel
sind der Normalfall, sobald getrennt gezaehlt werden soll.

ApiError traegt dafuer jetzt Status und Antwortkoerper — eine gemeldete
Dublette ist keine Sackgasse, sondern eine Rueckfrage.

Refs #6
2026-08-21 02:53:23 +02:00

95 lines
2.6 KiB
TypeScript

/** Kleine Hilfen für Aufrufe der eigenen API aus Client-Komponenten. */
async function readError(response: Response): Promise<ApiError> {
let body: unknown = null;
try {
body = await response.json();
} catch {
// Antwort ohne JSON-Körper — unten kommt die allgemeine Meldung.
}
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);
}
/**
* 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,
init?: RequestInit & { json?: unknown },
): Promise<T> {
const { json, ...rest } = init ?? {};
const response = await fetch(url, {
...rest,
headers:
json === undefined
? rest.headers
: { "Content-Type": "application/json", ...rest.headers },
body: json === undefined ? rest.body : JSON.stringify(json),
});
if (!response.ok) {
throw await readError(response);
}
if (response.status === 204) {
return undefined as T;
}
return (await response.json()) as T;
}
/** Holt eine Datei von der API und stößt den Browser-Download an. */
export async function downloadFile(
url: string,
json: unknown,
fallbackName: string,
): Promise<void> {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(json),
});
if (!response.ok) {
throw await readError(response);
}
const disposition = response.headers.get("Content-Disposition") ?? "";
const match = /filename="([^"]+)"/.exec(disposition);
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = objectUrl;
anchor.download = match?.[1] ?? fallbackName;
document.body.append(anchor);
anchor.click();
anchor.remove();
// Der Browser braucht den Blob noch einen Moment für den Download.
setTimeout(() => URL.revokeObjectURL(objectUrl), 10_000);
}