@setzkasten-cms/core 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,126 @@
1
+ import { z } from 'zod'
2
+ import type {
3
+ AnyFieldDef,
4
+ ArrayFieldDef,
5
+ FieldDefinition,
6
+ FieldRecord,
7
+ ObjectFieldDef,
8
+ OverrideFieldDef,
9
+ } from '../fields/field-definition'
10
+
11
+ /**
12
+ * Generates a Zod schema from a FieldDefinition.
13
+ * This is the single source of truth for validation – Zod schemas are derived
14
+ * from field definitions, never written by hand.
15
+ */
16
+ export function fieldToZod(field: FieldDefinition): z.ZodType {
17
+ switch (field.type) {
18
+ case 'text':
19
+ return textToZod(field as AnyFieldDef & { type: 'text' })
20
+ case 'number':
21
+ return numberToZod(field as AnyFieldDef & { type: 'number' })
22
+ case 'boolean':
23
+ return z.boolean()
24
+ case 'select':
25
+ return selectToZod(field as AnyFieldDef & { type: 'select' })
26
+ case 'icon':
27
+ return field.required ? z.string().min(1, `${field.label} ist erforderlich`) : z.string()
28
+ case 'image':
29
+ return imageToZod(field as AnyFieldDef & { type: 'image' })
30
+ case 'array':
31
+ return arrayToZod(field as ArrayFieldDef)
32
+ case 'object':
33
+ return objectToZod(field as ObjectFieldDef)
34
+ case 'color':
35
+ return z.string().regex(/^#[0-9a-fA-F]{6}$/, 'Invalid hex color')
36
+ case 'override':
37
+ return overrideToZod(field as OverrideFieldDef)
38
+ default:
39
+ return z.unknown()
40
+ }
41
+ }
42
+
43
+ function textToZod(field: AnyFieldDef & { type: 'text' }): z.ZodType {
44
+ let schema = z.string()
45
+
46
+ if (field.required) {
47
+ schema = schema.min(1, `${field.label} ist erforderlich`)
48
+ }
49
+ if (field.maxLength) {
50
+ schema = schema.max(field.maxLength, `${field.label} darf max. ${field.maxLength} Zeichen haben`)
51
+ }
52
+ if (field.pattern) {
53
+ schema = schema.regex(field.pattern, `${field.label} hat ein ungültiges Format`)
54
+ }
55
+
56
+ return schema
57
+ }
58
+
59
+ function numberToZod(field: AnyFieldDef & { type: 'number' }): z.ZodType {
60
+ let schema = z.number()
61
+
62
+ if (field.min !== undefined) {
63
+ schema = schema.min(field.min)
64
+ }
65
+ if (field.max !== undefined) {
66
+ schema = schema.max(field.max)
67
+ }
68
+
69
+ return schema
70
+ }
71
+
72
+ function selectToZod(field: AnyFieldDef & { type: 'select' }): z.ZodType {
73
+ const values = field.options.map((o) => o.value)
74
+ if (values.length === 0) return z.string()
75
+
76
+ const [first, ...rest] = values
77
+ return z.enum([first!, ...rest])
78
+ }
79
+
80
+ function imageToZod(field: AnyFieldDef & { type: 'image' }): z.ZodType {
81
+ const schema = z.object({
82
+ path: z.string(),
83
+ alt: z.string().optional(),
84
+ })
85
+
86
+ return field.required ? schema.refine((v) => v.path.length > 0, `${field.label} ist erforderlich`) : schema
87
+ }
88
+
89
+ function arrayToZod(field: ArrayFieldDef): z.ZodType {
90
+ const itemSchema = fieldToZod(field.itemField)
91
+ let schema = z.array(itemSchema)
92
+
93
+ if (field.minItems !== undefined) {
94
+ schema = schema.min(field.minItems)
95
+ }
96
+ if (field.maxItems !== undefined) {
97
+ schema = schema.max(field.maxItems)
98
+ }
99
+
100
+ return schema
101
+ }
102
+
103
+ function objectToZod(field: ObjectFieldDef): z.ZodType {
104
+ return schemaToZod(field.fields)
105
+ }
106
+
107
+ function overrideToZod(field: OverrideFieldDef): z.ZodType {
108
+ const innerSchema = schemaToZod(field.fields)
109
+ return z.object({
110
+ active: z.boolean(),
111
+ }).and(innerSchema.partial())
112
+ }
113
+
114
+ /**
115
+ * Convert an entire field record (schema) to a Zod object schema.
116
+ */
117
+ export function schemaToZod(fields: FieldRecord): z.ZodObject<Record<string, z.ZodType>> {
118
+ const shape: Record<string, z.ZodType> = {}
119
+
120
+ for (const [key, field] of Object.entries(fields)) {
121
+ const zodField = fieldToZod(field)
122
+ shape[key] = field.required ? zodField : zodField.optional()
123
+ }
124
+
125
+ return z.object(shape)
126
+ }