Barazo AppView backend
barazo.forum
1import { z } from 'zod/v4'
2
3// ---------------------------------------------------------------------------
4// Field type enum
5// ---------------------------------------------------------------------------
6
7export const onboardingFieldTypeSchema = z.enum([
8 'age_confirmation',
9 'tos_acceptance',
10 'newsletter_email',
11 'custom_text',
12 'custom_select',
13 'custom_checkbox',
14])
15
16export type OnboardingFieldType = z.infer<typeof onboardingFieldTypeSchema>
17
18// ---------------------------------------------------------------------------
19// Config schemas per field type
20// ---------------------------------------------------------------------------
21
22const selectConfigSchema = z.object({
23 options: z.array(z.string().min(1).max(200)).min(2).max(20),
24})
25
26// ---------------------------------------------------------------------------
27// Admin CRUD schemas
28// ---------------------------------------------------------------------------
29
30export const createOnboardingFieldSchema = z.object({
31 fieldType: onboardingFieldTypeSchema,
32 label: z
33 .string()
34 .trim()
35 .min(1, 'Label is required')
36 .max(200, 'Label must be at most 200 characters'),
37 description: z.string().trim().max(500).nullable().optional(),
38 isMandatory: z.boolean().default(true),
39 sortOrder: z.number().int().min(0).default(0),
40 config: z.record(z.string(), z.unknown()).nullable().optional(),
41})
42
43export type CreateOnboardingFieldInput = z.infer<typeof createOnboardingFieldSchema>
44
45export const updateOnboardingFieldSchema = z.object({
46 label: z.string().trim().min(1).max(200).optional(),
47 description: z.string().trim().max(500).nullable().optional(),
48 isMandatory: z.boolean().optional(),
49 sortOrder: z.number().int().min(0).optional(),
50 config: z.record(z.string(), z.unknown()).nullable().optional(),
51})
52
53export type UpdateOnboardingFieldInput = z.infer<typeof updateOnboardingFieldSchema>
54
55export const reorderFieldsSchema = z
56 .array(
57 z.object({
58 id: z.string().min(1),
59 sortOrder: z.number().int().min(0),
60 })
61 )
62 .min(1)
63
64export type ReorderFieldsInput = z.infer<typeof reorderFieldsSchema>
65
66// ---------------------------------------------------------------------------
67// User submission schema
68// ---------------------------------------------------------------------------
69
70export const submitOnboardingSchema = z
71 .array(
72 z.object({
73 fieldId: z.string().min(1),
74 response: z.unknown(),
75 })
76 )
77 .min(1)
78
79export type SubmitOnboardingInput = z.infer<typeof submitOnboardingSchema>
80
81// ---------------------------------------------------------------------------
82// Validation helpers
83// ---------------------------------------------------------------------------
84
85/**
86 * Validate a user's response value against the field type and config.
87 * Returns an error message if invalid, or null if valid.
88 */
89export function validateFieldResponse(
90 fieldType: OnboardingFieldType,
91 response: unknown,
92 config: Record<string, unknown> | null | undefined
93): string | null {
94 switch (fieldType) {
95 case 'age_confirmation': {
96 if (typeof response !== 'number') return 'Age confirmation must be a number'
97 const validAges = [0, 13, 14, 15, 16, 18]
98 if (!validAges.includes(response)) return 'Invalid age value'
99 return null
100 }
101 case 'tos_acceptance': {
102 if (response !== true) return 'Terms of service must be accepted'
103 return null
104 }
105 case 'newsletter_email': {
106 if (typeof response !== 'string') return 'Email must be a string'
107 if (response.length === 0) return null // optional empty is fine
108 const emailResult = z.email().safeParse(response)
109 if (!emailResult.success) return 'Invalid email format'
110 return null
111 }
112 case 'custom_text': {
113 if (typeof response !== 'string') return 'Response must be a string'
114 if (response.length > 1000) return 'Response must be at most 1000 characters'
115 return null
116 }
117 case 'custom_select': {
118 if (typeof response !== 'string') return 'Selection must be a string'
119 if (config) {
120 const parsed = selectConfigSchema.safeParse(config)
121 if (parsed.success && !parsed.data.options.includes(response)) {
122 return 'Invalid selection'
123 }
124 }
125 return null
126 }
127 case 'custom_checkbox': {
128 if (typeof response !== 'boolean') return 'Checkbox must be true or false'
129 return null
130 }
131 default:
132 return 'Unknown field type'
133 }
134}