From 04404ffdae1dd3f2871113ffc05685f41a61a027 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 07:45:05 +0000 Subject: [PATCH] refactor(account): isRowMissing als gemeinsame Hilfsfunktion statt dreier Kopien Co-Authored-By: Claude Opus 5.5 --- web/src/account/errors.test.ts | 29 ++++++++++++++++++++++++++++- web/src/account/errors.ts | 9 +++++++++ web/src/account/profile.ts | 8 +------- web/src/community/gallery.ts | 8 +------- web/src/logos/logos.ts | 8 +------- 5 files changed, 40 insertions(+), 22 deletions(-) diff --git a/web/src/account/errors.test.ts b/web/src/account/errors.test.ts index cb954b9..28a4304 100644 --- a/web/src/account/errors.test.ts +++ b/web/src/account/errors.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { errorMessage, errorType, isInvalidLinkError, isUnauthorized, type ErrorContext } from './errors'; +import { errorMessage, errorType, isInvalidLinkError, isRowMissing, isUnauthorized, type ErrorContext } from './errors'; /** Fehler wie vom SDK (AppwriteException): Felder code, type, message. */ const fail = (code: number, type: string, message = '') => ({ code, type, message }); @@ -149,6 +149,33 @@ describe('errorType', () => { }); }); +describe('isRowMissing', () => { + it('404 mit row_not_found heißt: die Zeile gibt es nicht (oder nicht für mich)', () => { + expect(isRowMissing(fail(404, 'row_not_found'))).toBe(true); + }); + + it('404 ohne Typ (leer oder fehlend) zählt ebenso', () => { + expect(isRowMissing(fail(404, ''))).toBe(true); + expect(isRowMissing({ code: 404 })).toBe(true); + }); + + it('eine fehlende Tabelle, Datenbank oder Datei ist ein echter Fehler, kein „Zeile weg“', () => { + for (const type of ['table_not_found', 'database_not_found', 'storage_file_not_found']) { + expect(isRowMissing(fail(404, type)), type).toBe(false); + } + }); + + it('andere Status und Nicht-Fehler nicht', () => { + expect(isRowMissing(fail(401, 'row_not_found'))).toBe(false); + expect(isRowMissing(fail(500, 'row_not_found'))).toBe(false); + expect(isRowMissing({ code: '404', type: 'row_not_found' })).toBe(false); + expect(isRowMissing(new TypeError('Failed to fetch'))).toBe(false); + expect(isRowMissing('row_not_found')).toBe(false); + expect(isRowMissing(null)).toBe(false); + expect(isRowMissing(undefined)).toBe(false); + }); +}); + describe('isInvalidLinkError (Passwort-neu-Seite)', () => { it('Link-Fehler', () => { expect(isInvalidLinkError(fail(401, 'user_invalid_token'))).toBe(true); diff --git a/web/src/account/errors.ts b/web/src/account/errors.ts index 99307e9..7c6cc18 100644 --- a/web/src/account/errors.ts +++ b/web/src/account/errors.ts @@ -24,6 +24,15 @@ export function isUnauthorized(err: unknown): boolean { return field(err, 'code') === 401; } +/** + * „Die Zeile gibt es nicht (oder nicht für mich)“: 404 `row_not_found` (auch ein 404 ohne Typ). Eine fehlende Tabelle, Datenbank oder + * Datei ist dagegen ein echter Fehler. Die Einzel-Abfragen der Tabellen (`getGalleryCard`, `getLogo`, `getUsername`) werten damit 404 als „nicht da“. + */ +export function isRowMissing(err: unknown): boolean { + const type = field(err, 'type'); + return field(err, 'code') === 404 && (type === 'row_not_found' || type === undefined || type === ''); +} + /** Bei `general_argument_invalid` nennt die Nachricht den Parameter: „Invalid `email` param: …“. */ function invalidParam(err: unknown): string | null { const message = field(err, 'message'); diff --git a/web/src/account/profile.ts b/web/src/account/profile.ts index 652d062..f2474fa 100644 --- a/web/src/account/profile.ts +++ b/web/src/account/profile.ts @@ -9,6 +9,7 @@ import { API_MESSAGES } from '../i18n/api-messages'; import { msg, t } from '../i18n/i18n'; import { IDS, tables } from './appwrite'; import { currentUserId } from './auth'; +import { isRowMissing } from './errors'; /** Wie im Dienst (deploy/api/common.js): 3–20 Zeichen, das erste ein Buchstabe oder eine Zahl, danach Buchstaben, Zahlen, `.`, `_`, `-`. */ export const USERNAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{2,19}$/; @@ -112,13 +113,6 @@ export function myUsername(): string | null { // ---- Lesen -------------------------------------------------------------------------------------------------- -/** „Es gibt kein Profil (oder keines für mich)“; eine fehlende Tabelle oder Datenbank ist dagegen ein echter Fehler. */ -function isRowMissing(err: unknown): boolean { - if (typeof err !== 'object' || err === null) return false; - const { code, type } = err as { code?: unknown; type?: unknown }; - return code === 404 && (type === 'row_not_found' || type === undefined || type === ''); -} - /** * Der Name eines Kontos. Hat es kein Profil, ist das Ergebnis null (Appwrite antwortet mit 404); eine ID, die keine Appwrite-ID sein * kann, ergibt null ohne Anfrage. Andere Fehler (Netz, Server) gehen durch. diff --git a/web/src/community/gallery.ts b/web/src/community/gallery.ts index 05c5c29..b0ed5fb 100644 --- a/web/src/community/gallery.ts +++ b/web/src/community/gallery.ts @@ -3,6 +3,7 @@ import { AppwriteException, Query, type Models } from 'appwrite'; import { ID_PATTERN, fetchFile } from '../account/cards'; import { IDS, tables } from '../account/appwrite'; +import { isRowMissing } from '../account/errors'; import { msg } from '../i18n/i18n'; export interface GalleryCard { @@ -112,13 +113,6 @@ export async function listGallery(opts: GalleryOptions): Promise<{ cards: Galler return { cards, nextCursor: cards.length === PAGE_SIZE ? cards[cards.length - 1].id : null }; } -/** „Die Zeile gibt es nicht (oder nicht für mich)“; fehlende Tabelle oder Datenbank ist dagegen ein echter Fehler. */ -function isRowMissing(err: unknown): boolean { - if (typeof err !== 'object' || err === null) return false; - const { code, type } = err as { code?: unknown; type?: unknown }; - return code === 404 && (type === 'row_not_found' || type === undefined || type === ''); -} - /** * Eine Karte per ID. Gibt es sie nicht oder ist sie für mich ausgeblendet (Appwrite antwortet beides mit 404), ist das * Ergebnis null. Eine ID, die keine Appwrite-ID sein kann, ergibt null ohne Anfrage. diff --git a/web/src/logos/logos.ts b/web/src/logos/logos.ts index 8767257..597104a 100644 --- a/web/src/logos/logos.ts +++ b/web/src/logos/logos.ts @@ -4,6 +4,7 @@ import { AppwriteException, Query, type Models } from 'appwrite'; import { ID_PATTERN, fetchFile } from '../account/cards'; import { IDS, tables } from '../account/appwrite'; +import { isRowMissing } from '../account/errors'; export interface CommunityLogo { id: string; @@ -95,13 +96,6 @@ export async function listLogos(opts: LogoListOptions): Promise<{ logos: Communi return { logos, nextCursor: logos.length === LOGO_PAGE_SIZE ? logos[logos.length - 1].id : null }; } -/** „Die Zeile gibt es nicht (oder nicht für mich)“; fehlende Tabelle oder Datenbank ist dagegen ein echter Fehler. */ -function isRowMissing(err: unknown): boolean { - if (typeof err !== 'object' || err === null) return false; - const { code, type } = err as { code?: unknown; type?: unknown }; - return code === 404 && (type === 'row_not_found' || type === undefined || type === ''); -} - /** * Ein Logo per ID. Gibt es das Logo nicht oder ist es für mich ausgeblendet oder gelöscht (Appwrite antwortet beides mit 404), * ist das Ergebnis null. Eine ID, die keine Appwrite-ID sein kann, ergibt null ohne Anfrage.