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
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
95 lines
3.1 KiB
TypeScript
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[]) : []
|
|
}
|