import { z } from 'zod' import { INPUT_TYPES, STATIC_TYPES, type TemplateField } from '@/lib/templates/fields' /** * Verbindliche Prüfung einer Vorlage. Läuft ausschliesslich serverseitig: * eine Server Action ist ein Endpunkt, und was hier durchgeht, geht auch durch, * wenn jemand den Editor umgeht. */ const key = z .string() .min(1) .max(64) // Der Schlüssel ist stabil und wird nie geändert: ein Abschnitt findet über // ihn seinen Baustein wieder, auch über Vorlagenversionen hinweg. .regex(/^[a-z][a-z0-9_]*$/, 'Kleinbuchstaben, Ziffern und Unterstrich') const gemeinsam = { key, label: z.string().min(1).max(200), help: z.string().max(500).optional(), /** Feld erst ab dieser Stufe sichtbar, siehe docs/05-akten-und-vorlagen.md. */ minClearance: z.number().int().min(0).max(5).optional(), } /** * `.strict()` und nicht die Voreinstellung: zod wuerde unbekannte Schluessel * **stillschweigend verwerfen**. Fuer eine Vorlage, die von Hand als JSON * eingegeben wird, ist das die schlechtere Antwort - wer `typ` statt `type` * schreibt, bekaeme eine gespeicherte Vorlage, die nicht tut, was dasteht. * Und was nicht ankommt, kann auch nichts einschleusen. */ const inputField = z.strictObject({ ...gemeinsam, type: z.enum(INPUT_TYPES), required: z.boolean().optional(), requiredToClose: z.boolean().optional(), options: z.array(z.string().min(1)).optional(), columns: z.array(z.string().min(1)).optional(), span: z.number().int().min(1).max(5).optional(), rows: z.number().int().min(1).max(12).optional(), slot: z.enum(['classbar']).optional(), emphasis: z.boolean().optional(), dateKey: key.optional(), }) const staticBlock = z.strictObject({ ...gemeinsam, type: z.enum(STATIC_TYPES), text: z.string().max(20_000).optional(), span: z.number().int().min(1).max(5).optional(), }) export const templateFieldSchema = z.union([inputField, staticBlock]) export const templateFieldsSchema = z .array(templateFieldSchema) .max(200) .superRefine((fields, ctx) => { const seen = new Set() for (const [index, field] of fields.entries()) { if (seen.has(field.key)) { ctx.addIssue({ code: 'custom', path: [index, 'key'], message: `Schlüssel doppelt vergeben: ${field.key}`, }) } seen.add(field.key) if ( (field.type === 'select' || field.type === 'multiselect') && (!('options' in field) || !field.options?.length) ) { ctx.addIssue({ code: 'custom', path: [index, 'options'], message: 'Auswahlfelder brauchen mindestens eine Option', }) } if ('dateKey' in field && field.dateKey && field.type !== 'signature') { ctx.addIssue({ code: 'custom', path: [index, 'dateKey'], message: 'Ein Datumsverweis gehört nur an eine Unterschrift', }) } } }) export function parseTemplateFields(raw: unknown): TemplateField[] { const parsed = templateFieldsSchema.safeParse(raw) return parsed.success ? (parsed.data as TemplateField[]) : [] }