import { getTableColumns } from "drizzle-orm" export type InputField = { schema: Record parse: (value: unknown, path: string) => any } const invalid = (path: string, expected: string): never => { throw new Error(`${path}: ${expected}`) } export const textField = (nonEmpty = false): InputField => ({ schema: { type: "string", ...(nonEmpty ? { minLength: 1 } : {}) }, parse: (value, path) => typeof value === "string" && (!nonEmpty || value.trim()) ? value.trim() : invalid(path, "Text" + (nonEmpty ? " darf nicht leer sein" : " erwartet")), }) export const numericField = (integer = false, minimum?: number, maximum?: number): InputField => ({ schema: { type: integer ? "integer" : "number", ...(minimum !== undefined ? { minimum } : {}), ...(maximum !== undefined ? { maximum } : {}) }, parse: (value, path) => { if (typeof value !== "number" || !Number.isFinite(value) || (integer && !Number.isSafeInteger(value)) || (minimum !== undefined && value < minimum) || (maximum !== undefined && value > maximum)) { return invalid(path, `gültige ${integer ? "ganze " : ""}Zahl im angegebenen Bereich erwartet`) } return value }, }) export const idField = numericField(true, 1) export const uuidField: InputField = { schema: { type: "string", format: "uuid" }, parse: (value, path) => typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value) ? value.toLowerCase() : invalid(path, "gültige UUID erwartet"), } export const booleanField: InputField = { schema: { type: "boolean" }, parse: (value, path) => typeof value === "boolean" ? value : invalid(path, "Boolean erwartet"), } export const dateField: InputField = { schema: { type: "string", format: "date", description: "Kalenderdatum YYYY-MM-DD." }, parse: (value, path) => { if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return invalid(path, "Datum YYYY-MM-DD erwartet") const date = new Date(`${value}T00:00:00Z`) if (!Number.isFinite(date.getTime()) || date.toISOString().slice(0, 10) !== value) return invalid(path, "ungültiges Kalenderdatum") return value }, } export const nullable = (field: InputField): InputField => ({ schema: { ...field.schema, type: [field.schema.type, "null"] }, parse: (value, path) => value === null ? null : field.parse(value, path), }) export const arrayField = (item: InputField): InputField => ({ schema: { type: "array", items: item.schema }, parse: (value, path) => Array.isArray(value) ? value.map((entry, index) => item.parse(entry, `${path}[${index}]`)) : invalid(path, "Array erwartet"), }) export const objectField = (fields: Record, additionalProperties = false): InputField => ({ schema: { type: "object", properties: Object.fromEntries(Object.entries(fields).map(([key, field]) => [key, field.schema])), additionalProperties }, parse: (value, path) => { if (!value || typeof value !== "object" || Array.isArray(value)) return invalid(path, "Objekt erwartet") const entries = Object.entries(value).map(([key, entry]) => { if (Object.prototype.hasOwnProperty.call(fields, key)) return [key, fields[key].parse(entry, `${path}.${key}`)] if (!additionalProperties || ["__proto__", "constructor", "prototype"].includes(key)) return invalid(`${path}.${key}`, "unbekanntes Feld") return [key, entry] }) return Object.fromEntries(entries) }, }) /** Explicit allowlist; scalar types and nullability follow the persisted columns. */ export function tableFields(table: any, names: string, overrides: Record = {}) { const columns = getTableColumns(table) as Record return Object.fromEntries(names.split(/\s+/).filter(Boolean).map((name) => { const column = columns[name] if (!column) throw new Error(`Unbekanntes Stammdatenfeld ${name}`) let field = overrides[name] if (!field) { if (column.columnType === "PgUUID") field = uuidField else if (column.columnType === "PgDateString") field = dateField else if (column.dataType === "string") field = textField(name === "name") else if (column.dataType === "boolean") field = booleanField else if (column.dataType === "number") { field = column.columnType === "PgSmallInt" ? numericField(true, -32768, 32767) : numericField(column.columnType !== "PgDoublePrecision") } else throw new Error(`Explizites Eingabeschema für ${name} erforderlich`) } return [name, column.notNull ? field : nullable(field)] })) }