Barazo AppView backend barazo.forum
at main 134 lines 4.5 kB view raw
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}