import { z } from "zod"; /** * Bausteine, die alle Stammdatenlisten teilen. * * Jede Liste in FLORIAN kann dasselbe: sortieren, filtern, durchsuchen, * blättern. Damit sich das nicht in jedem Router wiederholt — und vor allem, * damit es sich überall gleich verhält — steht die Form der Abfrage hier. */ /** Höchstzahl je Seite. Mehr als das braucht keine Ansicht, und es schützt * davor, dass ein Aufruf versehentlich zehntausend Zeilen zieht. */ export const MAX_SEITENGROESSE = 200; export const sortierrichtung = z.enum(["asc", "desc"]); /** * Standardform einer Listenabfrage. * * @param sortierfelder erlaubte Sortierspalten — als Aufzählung, damit kein * beliebiger Feldname in die Abfrage wandern kann */ export function listenEingabe(sortierfelder: T) { return z.object({ /** Volltextsuche über die Felder, die der jeweilige Router festlegt. */ suche: z.string().trim().max(200).optional(), seite: z.number().int().min(1).default(1), seitengroesse: z.number().int().min(1).max(MAX_SEITENGROESSE).default(50), sortierung: z.enum(sortierfelder as unknown as [string, ...string[]]).optional(), richtung: sortierrichtung.default("asc"), /** Gelöschte mit anzeigen. Nur für die Wiederherstellung gedacht. */ mitGeloeschten: z.boolean().default(false), }); } export type ListenEingabe = z.infer>>; /** Antwortform jeder Liste. */ export interface ListenAntwort { eintraege: T[]; gesamt: number; seite: number; seitengroesse: number; seitenGesamt: number; } export function listenAntwort( eintraege: T[], gesamt: number, eingabe: { seite: number; seitengroesse: number }, ): ListenAntwort { return { eintraege, gesamt, seite: eingabe.seite, seitengroesse: eingabe.seitengroesse, seitenGesamt: Math.max(1, Math.ceil(gesamt / eingabe.seitengroesse)), }; } /** Übersetzt Seite und Seitengröße in skip/take. */ export function blaettern(eingabe: { seite: number; seitengroesse: number }) { return { skip: (eingabe.seite - 1) * eingabe.seitengroesse, take: eingabe.seitengroesse, }; } /** * Baut aus einem Suchbegriff eine ODER-Bedingung über mehrere Textfelder. * Leere Suche gibt undefined zurück — dann bleibt die Bedingung ganz weg. */ export function sucheUeber(felder: string[], begriff?: string) { const text = begriff?.trim(); if (!text) return undefined; return { OR: felder.map((feld) => ({ [feld]: { contains: text, mode: "insensitive" as const }, })), }; } /** Sortierung mit einem verlässlichen Standard. */ export function sortierenNach( feld: string | undefined, richtung: "asc" | "desc", standard: Record, ): Record { if (!feld) return standard; return { [feld]: richtung }; } /** Blendet Soft-Deletes aus, sofern nicht ausdrücklich gewünscht. */ export function ohneGeloeschte(mitGeloeschten: boolean) { return mitGeloeschten ? {} : { geloeschtAm: null }; } /** Eingabe für alles, was über eine Kennung angesprochen wird. */ export const idEingabe = z.object({ id: z.string().uuid() }); /** Eingabe für die Massenbearbeitung. */ export const idsEingabe = z.object({ ids: z.array(z.string().uuid()).min(1).max(500), });