# Frontend Schemas Module ## Overview The Schemas module provides **Zod validation schemas** for form validation across the frontend application. These schemas define data validation rules, generate TypeScript types, and provide initial form values for entity forms. **Location:** `frontend/src/schemas/` **Total Files:** 6 files (~257 LOC) **Library:** [Zod](https://zod.dev/) - TypeScript-first schema validation --- ## Architecture ``` frontend/src/schemas/ ├── index.ts (9 LOC) # Central exports ├── userSchema.ts (80 LOC) # User validation (create/update) ├── assetSchema.ts (85 LOC) # Asset validation ├── projectSchema.ts (31 LOC) # Project validation ├── tourPageSchema.ts (35 LOC) # Tour page validation └── roleSchema.ts (17 LOC) # Role validation ``` **Related Schema:** `frontend/src/lib/offlineDb/schema.ts` - Dexie IndexedDB schema (separate concern) --- ## Schema Pattern Each schema file follows a consistent pattern: ```typescript /** * 1. Zod schema definition */ export const entitySchema = z.object({ fieldName: z.string().min(1, 'Error message'), // ... }); /** * 2. TypeScript type inference */ export type EntityFormData = z.infer; /** * 3. Initial form values */ export const entityInitialValues: EntityFormData = { fieldName: '', // ... }; ``` --- ## Schema Files ### 1. User Schema (`userSchema.ts`) **Purpose:** Validation for user creation and update forms. **Lines:** 80 LOC #### Schemas ```typescript // User creation - requires email, optional password export const userCreateSchema = z.object({ firstName: z.string().max(255, 'First name too long').optional().or(z.literal('')), lastName: z.string().max(255, 'Last name too long').optional().or(z.literal('')), phoneNumber: z.string().max(50, 'Phone number too long').optional().or(z.literal('')), email: z.string().email('Invalid email address').min(1, 'Email is required'), password: z.string().min(6, 'Password must be at least 6 characters').optional().or(z.literal('')), disabled: z.boolean().default(false), avatar: z.array(z.unknown()).optional(), app_role: z.unknown().optional().nullable(), custom_permissions: z.array(z.unknown()).optional(), }); // User update - same fields, password optional export const userUpdateSchema = z.object({ // Same structure as userCreateSchema }); ``` #### Exports | Export | Type | Description | |--------|------|-------------| | `userCreateSchema` | ZodObject | Create user validation | | `userUpdateSchema` | ZodObject | Update user validation | | `UserCreateFormData` | Type | Inferred create form type | | `UserUpdateFormData` | Type | Inferred update form type | | `userInitialValues` | Object | Default form values | #### Field Validations | Field | Validation | Error Message | |-------|------------|---------------| | `firstName` | max(255), optional | "First name too long" | | `lastName` | max(255), optional | "Last name too long" | | `phoneNumber` | max(50), optional | "Phone number too long" | | `email` | email(), min(1) | "Invalid email address", "Email is required" | | `password` | min(6), optional | "Password must be at least 6 characters" | | `disabled` | boolean, default(false) | - | | `avatar` | array, optional | - | | `app_role` | unknown, optional, nullable | - | | `custom_permissions` | array, optional | - | --- ### 2. Asset Schema (`assetSchema.ts`) **Purpose:** Validation for asset upload and edit forms. **Lines:** 85 LOC #### Schema ```typescript export const assetSchema = z.object({ project: z.unknown().optional().nullable(), name: z.string().min(1, 'Name is required').max(255, 'Name too long'), asset_type: z.enum(['image', 'video', 'audio', 'file']), type: z.enum([ 'general', 'icon', 'background_image', 'audio', 'video', 'transition', 'logo', 'favicon', 'document', ]).default('general'), cdn_url: z.string().url('Invalid URL').optional().or(z.literal('')), storage_key: z.string().max(500, 'Storage key too long').optional().or(z.literal('')), mime_type: z.string().max(100, 'MIME type too long').optional().or(z.literal('')), size_mb: z.coerce.number().min(0, 'Size cannot be negative').optional().or(z.literal('')), width_px: z.coerce.number().int().min(0, 'Width cannot be negative').optional().or(z.literal('')), height_px: z.coerce.number().int().min(0, 'Height cannot be negative').optional().or(z.literal('')), duration_sec: z.coerce.number().min(0, 'Duration cannot be negative').optional().or(z.literal('')), checksum: z.string().max(255, 'Checksum too long').optional().or(z.literal('')), is_public: z.boolean().default(false), is_deleted: z.boolean().default(false), deleted_at_time: z.date().optional().nullable(), }); ``` #### Exports | Export | Type | Description | |--------|------|-------------| | `assetSchema` | ZodObject | Asset validation schema | | `AssetFormData` | Type | Inferred form type | | `assetInitialValues` | Object | Default form values | #### Enum Values **asset_type:** - `image` - Image files (PNG, JPG, WebP, etc.) - `video` - Video files (MP4, WebM, etc.) - `audio` - Audio files (MP3, WAV, etc.) - `file` - Generic files (documents, etc.) **type (usage category):** - `general` - Default category - `icon` - UI icons - `background_image` - Page backgrounds - `audio` - Background audio - `video` - Video content - `transition` - Transition videos - `logo` - Brand logos - `favicon` - Site favicons - `document` - Documents/PDFs #### Coercion Pattern Number fields use `z.coerce` for form input handling: ```typescript // Converts string input to number, validates, allows empty string size_mb: z.coerce .number() .min(0, 'Size cannot be negative') .optional() .or(z.literal('')) ``` --- ### 3. Project Schema (`projectSchema.ts`) **Purpose:** Validation for project creation and edit forms. **Lines:** 31 LOC #### Schema ```typescript export const projectSchema = z.object({ name: z.string().min(1, 'Name is required').max(255, 'Name too long'), slug: z.string() .max(255, 'Slug too long') .regex( /^[a-z0-9_-]*$/i, 'Slug can only contain letters, numbers, dashes, underscores', ) .optional() .or(z.literal('')), description: z.string().max(5000, 'Description too long').optional().or(z.literal('')), }); ``` #### Exports | Export | Type | Description | |--------|------|-------------| | `projectSchema` | ZodObject | Project validation schema | | `ProjectFormData` | Type | Inferred form type | | `projectInitialValues` | Object | Default form values | #### Slug Validation The slug field uses regex validation for URL-safe strings: ```typescript .regex(/^[a-z0-9_-]*$/i, 'Slug can only contain letters, numbers, dashes, underscores') ``` **Valid:** `my-project`, `project_123`, `MyProject` **Invalid:** `my project`, `project@123`, `project/name` --- ### 4. Tour Page Schema (`tourPageSchema.ts`) **Purpose:** Validation for tour page creation and edit forms. **Lines:** 35 LOC #### Schema ```typescript export const tourPageSchema = z.object({ project: z.unknown().optional().nullable(), title: z.string().max(255, 'Title too long').optional().or(z.literal('')), slug: z.string() .max(255, 'Slug too long') .regex( /^[a-z0-9_-]*$/i, 'Slug can only contain letters, numbers, dashes, underscores', ) .optional() .or(z.literal('')), background_asset: z.unknown().optional().nullable(), audio_asset: z.unknown().optional().nullable(), is_start_page: z.boolean().default(false), sort_order: z.coerce.number().int().min(0).optional().or(z.literal('')), }); ``` #### Exports | Export | Type | Description | |--------|------|-------------| | `tourPageSchema` | ZodObject | Tour page validation schema | | `TourPageFormData` | Type | Inferred form type | | `tourPageInitialValues` | Object | Default form values | #### Initial Values ```typescript export const tourPageInitialValues: TourPageFormData = { project: null, title: '', slug: '', background_asset: null, audio_asset: null, is_start_page: false, sort_order: '', }; ``` --- ### 5. Role Schema (`roleSchema.ts`) **Purpose:** Validation for role creation and edit forms. **Lines:** 17 LOC #### Schema ```typescript export const roleSchema = z.object({ name: z.string().min(1, 'Name is required').max(255, 'Name too long'), permissions: z.array(z.unknown()).optional(), }); ``` #### Exports | Export | Type | Description | |--------|------|-------------| | `roleSchema` | ZodObject | Role validation schema | | `RoleFormData` | Type | Inferred form type | | `roleInitialValues` | Object | Default form values | #### Initial Values ```typescript export const roleInitialValues: RoleFormData = { name: '', permissions: [], }; ``` --- ### 6. Index File (`index.ts`) **Purpose:** Central re-export for all schemas. **Lines:** 9 LOC ```typescript export * from './userSchema'; export * from './projectSchema'; export * from './assetSchema'; export * from './roleSchema'; export * from './tourPageSchema'; ``` --- ## Zod Validation Patterns ### 1. Optional with Empty String Allow field to be empty or have valid value: ```typescript // Accepts: undefined, '', or valid string fieldName: z.string().optional().or(z.literal('')) ``` ### 2. Required String Field must have non-empty value: ```typescript // Fails on empty string name: z.string().min(1, 'Name is required') ``` ### 3. Email Validation Built-in email format validation: ```typescript email: z.string().email('Invalid email address').min(1, 'Email is required') ``` ### 4. Enum Validation Restrict to specific values: ```typescript asset_type: z.enum(['image', 'video', 'audio', 'file']) ``` ### 5. Number Coercion Convert string input to number: ```typescript // Input: "123" → number 123 // Input: "" → literal '' size_mb: z.coerce.number().min(0).optional().or(z.literal('')) ``` ### 6. Regex Pattern Validate string format: ```typescript slug: z.string().regex(/^[a-z0-9_-]*$/i, 'Error message') ``` ### 7. Nullable Relation Optional reference to another entity: ```typescript project: z.unknown().optional().nullable() ``` ### 8. Boolean with Default Boolean field with default value: ```typescript disabled: z.boolean().default(false) ``` --- ## Type Inference Zod schemas generate TypeScript types automatically: ```typescript // Define schema const projectSchema = z.object({ name: z.string().min(1), slug: z.string().optional(), }); // Infer type type ProjectFormData = z.infer; // Equivalent to: // type ProjectFormData = { // name: string; // slug?: string; // } ``` --- ## Usage with Formik Schemas can be integrated with Formik using adapters: ### Option 1: Manual Validation ```typescript import { projectSchema, projectInitialValues } from '@/schemas'; const validate = (values) => { const result = projectSchema.safeParse(values); if (!result.success) { const errors = {}; result.error.issues.forEach((issue) => { errors[issue.path[0]] = issue.message; }); return errors; } return {}; }; {/* form fields */} ``` ### Option 2: Zod-Formik Adapter ```typescript import { toFormikValidationSchema } from 'zod-formik-adapter'; import { projectSchema, projectInitialValues } from '@/schemas'; {/* form fields */} ``` --- ## Related: IndexedDB Schema The `lib/offlineDb/schema.ts` file defines the **Dexie.js** IndexedDB schema for offline storage (separate from form validation): ```typescript class OfflineDatabase extends Dexie { assets!: EntityTable; projects!: EntityTable; downloadQueue!: EntityTable; constructor() { super(OFFLINE_CONFIG.dbName); this.version(OFFLINE_CONFIG.dbVersion).stores({ assets: 'id, projectId, url, variantType, assetType, downloadedAt', projects: 'id, slug, status, lastSyncedAt', downloadQueue: 'id, projectId, status, priority, addedAt', }); } } export const offlineDb = new OfflineDatabase(); ``` **Key Differences:** | Aspect | Zod Schemas | Dexie Schema | |--------|-------------|--------------| | Purpose | Form validation | Database structure | | Library | Zod | Dexie.js | | Location | `schemas/` | `lib/offlineDb/` | | Runtime | Client-side validation | IndexedDB storage | --- ## Schema Inventory | File | LOC | Entity | Fields | Exports | |------|-----|--------|--------|---------| | `userSchema.ts` | 80 | User | 9 fields | 5 (2 schemas, 2 types, 1 initial) | | `assetSchema.ts` | 85 | Asset | 14 fields | 3 (1 schema, 1 type, 1 initial) | | `tourPageSchema.ts` | 35 | TourPage | 7 fields | 3 (1 schema, 1 type, 1 initial) | | `projectSchema.ts` | 31 | Project | 3 fields | 3 (1 schema, 1 type, 1 initial) | | `roleSchema.ts` | 17 | Role | 2 fields | 3 (1 schema, 1 type, 1 initial) | | `index.ts` | 9 | - | - | Re-exports all | | **Total** | **257** | **5** | **35** | **17** | --- ## Best Practices ### 1. Consistent Error Messages Use descriptive, user-friendly error messages: ```typescript // Good name: z.string().min(1, 'Name is required').max(255, 'Name too long') // Avoid name: z.string().min(1).max(255) // Generic messages ``` ### 2. Type-Safe Initial Values Use inferred type for initial values: ```typescript export const projectInitialValues: ProjectFormData = { name: '', // TypeScript enforces this matches schema slug: '', description: '', }; ``` ### 3. Empty String Handling For optional fields that may receive empty strings from forms: ```typescript // Allows both undefined and empty string fieldName: z.string().optional().or(z.literal('')) ``` ### 4. Relation Fields Use `z.unknown()` for relation fields that hold entity references: ```typescript // Accepts any value - actual validation happens on backend project: z.unknown().optional().nullable() ``` ### 5. Number Input Handling Use `z.coerce` for number fields from text inputs: ```typescript // Converts "123" to 123, validates as number amount: z.coerce.number().min(0) ``` --- ## Related Documentation - [types-module.md](./types-module.md) - Entity type definitions - [factories-module.md](./factories-module.md) - Form page factory (uses schemas) - [lib-module.md](./lib-module.md) - Offline storage schema - [hooks-module.md](./hooks-module.md) - Form hooks --- ## Adding New Schemas To add validation for a new entity: ### Step 1: Create Schema File ```typescript // schemas/widgetSchema.ts import { z } from 'zod'; export const widgetSchema = z.object({ name: z.string().min(1, 'Name is required').max(255, 'Name too long'), type: z.enum(['basic', 'advanced']).default('basic'), config: z.record(z.unknown()).optional(), }); export type WidgetFormData = z.infer; export const widgetInitialValues: WidgetFormData = { name: '', type: 'basic', config: {}, }; ``` ### Step 2: Export from Index ```typescript // schemas/index.ts export * from './widgetSchema'; ``` ### Step 3: Use in Form ```typescript import { widgetSchema, widgetInitialValues, type WidgetFormData } from '@/schemas'; // With validation const validate = (values: WidgetFormData) => { const result = widgetSchema.safeParse(values); // ... handle errors }; ```