Files
AfiS-Aktensystem/web/lib/templates/schema.ts
T
Harlekin e0b03ea336
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Gedankenstriche durch Bindestriche ersetzt
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
2026-09-04 08:54:52 +02:00

95 lines
3.1 KiB
TypeScript

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<string>()
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[]) : []
}