UNPKG

fortify-schema

Version:

A modern TypeScript validation library designed around familiar interface syntax and powerful conditional validation. Experience schema validation that feels natural to TypeScript developers while unlocking advanced runtime validation capabilities.

638 lines (496 loc) 17.4 kB
# Conditional Validation Guide Complete guide to Fortify Schema's conditional validation - from basic V1 syntax to advanced V2 runtime methods. ## 📚 Table of Contents - [Introduction](#introduction) - [V1 Conditional Validation (Legacy)](#v1-conditional-validation-legacy) - [V2 Conditional Validation (Current)](#v2-conditional-validation-current) - [V2 Runtime Methods](#v2-runtime-methods) - [Advanced Patterns](#advanced-patterns) - [Migration from V1 to V2](#migration-from-v1-to-v2) - [Best Practices](#best-practices) ## Introduction Conditional validation allows you to create dynamic validation rules based on the values of other fields or runtime properties. Fortify Schema supports both V1 (legacy) and V2 (current) conditional validation syntax. ### Basic Concept ```typescript const Schema = Interface({ role: "admin|user|guest", // Conditional validation: "when condition *? then : else" permissions: "when role=admin *? string[] : string[]?", // ^^^^ ^^ ^ // keyword operator separator }); ``` ## V1 Conditional Validation (Legacy) V1 syntax is still supported for backward compatibility but V2 is recommended for new projects. But the same logic can be applied to V2, only the methods call syntax are different. ### Basic V1 Syntax ```typescript const V1Schema = Interface({ role: "admin|user|guest", accountType: "free|premium|enterprise", age: "number(13,120)", // Basic equality adminAccess: "when role=admin *? string[] : string[]?", // Inequality nonGuestAccess: "when role!=guest *? boolean : boolean?", // Numeric comparisons adultContent: "when age>=18 *? boolean : boolean?", seniorDiscount: "when age>65 *? number(0,50) : number(0,0)", youthProgram: "when age<25 *? boolean : boolean?", // Value inclusion premiumFeatures: "when accountType.$in(premium,enterprise) *? string[] : string[]?", }); ``` ### V1 Operators | Operator | Description | Example | | -------- | --------------------- | ------------------------------- | | `=` | Equals | `when role=admin` | | `!=` | Not equals | `when role!=guest` | | `>` | Greater than | `when age>18` | | `>=` | Greater than or equal | `when age>=21` | | `<` | Less than | `when age<65` | | `<=` | Less than or equal | `when age<=25` | | `.in()` | Value in list | `when role.in(admin,moderator)` | ### V1 Limitations - Will be deprecated in future versions - Limited to simple property comparisons - No runtime property existence checking - No complex method calls - Limited string operations ## V2 Conditional Validation (Current) V2 introduces powerful runtime property checking with the new `property.$method()` syntax. ### Basic V2 Syntax ```typescript const V2Schema = Interface({ // Runtime data objects config: "any?", user: "any?", features: "any?", // Basic user data id: "uuid", email: "email", role: "admin|user|guest", // V2 Runtime Methods - Enhanced property checking hasPermissions: "when config.permissions.$exists() *? boolean : =false", hasProfile: "when user.profile.$exists() *? boolean : =false", isListEmpty: "when config.items.$empty() *? boolean : =true", hasAdminRole: "when user.roles.$contains(admin) *? boolean : =false", }); ``` ### V2 Advantages - **Runtime property checking** - Check if properties exist at runtime - **Deep property access** - Navigate nested objects safely - **String operations** - Advanced string checking methods - **Numeric operations** - Range and value checking - **Complex defaults** - Object and array default values - **Special character support** - Handle properties with special characters ## V2 Runtime Methods ### Property Existence Methods #### `$exists()` - Check Property Existence ```typescript const ExistsSchema = Interface({ config: "any?", user: "any?", // Basic existence checking hasConfig: "when config.$exists() *? boolean : =false", hasUserProfile: "when user.profile.$exists() *? boolean : =false", // Deep nested checking hasAdvancedSettings: "when user.profile.settings.advanced.$exists() *? boolean : =false", // Special characters hasSpecialFeature: 'when config["admin-override"].$exists() *? boolean : =false', // Unicode support hasUnicodeFeature: "when features.feature_🚀.$exists() *? boolean : =false", }); ``` ### Empty/Null Checking Methods #### `$empty()` - Check if Empty ```typescript const EmptySchema = Interface({ data: "any?", // String empty checking hasContent: "when data.description.$empty() *? =no_content : =has_content", // Array empty checking hasItems: "when data.items.$empty() *? =no_items : =has_items", // Object empty checking hasMetadata: "when data.metadata.$empty() *? =no_metadata : =has_metadata", }); ``` #### `$null()` - Check if Null ```typescript const NullSchema = Interface({ data: "any?", // Null checking isDataNull: "when data.value.$null() *? =is_null : =not_null", // Combined with existence hasValidData: "when data.value.$exists() && !data.value.$null() *? boolean : =false", }); ``` ### String Methods #### `$contains(value)` - Check String Contains ```typescript const ContainsSchema = Interface({ data: "any?", // Basic contains hasImportantInfo: "when data.description.$contains(important) *? boolean : =none", // Multiple contains checks hasKeywords: "when data.content.$contains(urgent) || data.content.$contains(priority) *? boolean : =no_keywords", // Case-sensitive checking hasExactMatch: "when data.title.$contains(URGENT) *? boolean : =no_match", }); const data = { title: "Big title ..... ", content: "something more beautifull....urgent", description: `Lorem ipsum dolor sit amet consectetur, adipisicing elit. Harum neque architecto commodi ipsam excepturi repellat consequatur, provident asperiores. Excepturi earum vero amet enim consequatur quasi, fugit cupiditate! Corrupti, incidunt exercitationem. something ..... `, }; const result = ContainsSchema.safeParse({ data, hasExactMatch: "no_match", hasImportantInfo: "none", hasKeywords: "no_keywords", }); if (result.success) { console.log("✅ Expected success:", result.data); } else { console.log("❌ Expected errors:", result.errors); } ``` #### `$startsWith(prefix)` - Check String Prefix ```typescript const StartsWithSchema = Interface({ data: "any?", // Prefix checking isSystemMessage: "when data.message.$startsWith(SYSTEM:) *? boolean : =false", isErrorCode: "when data.code.$startsWith(ERR_) *? boolean : =false", // URL checking isSecureUrl: "when data.url.$startsWith(https://) *? boolean : =false", }); ``` #### `$endsWith(suffix)` - Check String Suffix ```typescript const EndsWithSchema = Interface({ data: "any?", // File extension checking isPdfFile: "when data.filename.$endsWith(.pdf) *? boolean : =no_pdf_file", isImageFile: "when data.filename.$endsWith(.jpg) || data.filename.$endsWith(.png) *? boolean : =no_img", // Domain checking isCorporateEmail: "when data.email.$endsWith(@company.com) *? boolean : =no_itsnot_corporated", }); const data = { filename: "super_file.ehezezpaozdj.pdf", //is pdf email: "mail@somethingelse.com", //false so should fail }; const result = EndsWithSchema.safeParse({ data, isPdfFile: true, isCorporateEmail: true, isImageFile: "no_img", }); if (result.success) { console.log("✅ Expected success:", result.data); } else { console.log("❌ Expected errors:", result.errors); } ``` ### Numeric Methods #### `$between(min,max)` - Check Numeric Range ```typescript const BetweenSchema = Interface({ data: "any?", // Age range checking isAdult: "when data.age.$between(18,65) *? boolean : =isnt_adult", // Score validation isPassingGrade: "when data.score.$between(60,100) *? boolean : =itsnt_passgrade", // Price range isAffordable: "when data.price.$between(0,100) *? boolean : =yes_itis", }); const data = { age: 17, score: 59, price: 40, }; const result = BetweenSchema.safeParse({ data, isAdult: true, //should fail because of age >= 18 isPassingGrade: true, //should also faild isAffordable: true, }); if (result.success) { console.log("✅ Expected success:", result.data); } else { console.log("❌ Expected errors:", result.errors); } ``` #### `$in(val1,val2,...)` - Check Value Inclusion ```typescript const InSchema = Interface({ data: "any?", // Role checking hasElevatedAccess: "when data.role.$in(admin,moderator,super_admin) *? boolean : =not_allowed", // Status checking isActiveStatus: "when data.status.$in(active,pending,processing) *? boolean : =not_active", // Category checking isPremiumCategory: "when data.category.$in(premium,enterprise,vip) *? boolean : =itsnot_premium", }); const data = { age: 17, role: "admin", status: "active", category: "premium", }; const result = InSchema.safeParse({ data, hasElevatedAccess: true, isActiveStatus: "not_active", //should faild because "status" contains one of the requirements isPremiumCategory: "itsnot_premium", // should faild also }); if (result.success) { console.log("✅ Expected success:", result.data); } else { console.log("❌ Expected errors:", result.errors); } ``` ## Advanced Patterns ### Complex Default Values ```typescript const ComplexDefaultsSchema = Interface({ config: "any?", // Object defaults // @fortify-ignore defaultSettings: 'when config.settings.$exists() *? any : ={"theme":"dark","lang":"en"}', // Array defaults // @fortify-ignore defaultTags: 'when config.tags.$exists() *? string[] : =["default","user"]', // Nested object defaults defaultProfile: 'when config.profile.$exists() *? any : ={"name":"Anonymous","avatar":null}', // Complex conditional defaults advancedConfig: 'when config.advanced.$exists() *? any : ={"features":[],"permissions":{}}', }); const config = { // settings: "yes", //not exist // advancedConfig: "yes", //not exist defaultProfile: "yes", defaultTags: "yes" }; const result = ComplexDefaultsSchema.safeParse({ config, defaultSettings: "", defaultTags: true, defaultProfile: true, advancedConfig: "" }); if (result.success) { console.log("✅ Expected success:", result.data); } else { console.log("❌ Expected errors:", result.errors); } ``` ### Method Combinations ```typescript const CombinedMethodsSchema = Interface({ user: "any?", config: "any?", // Logical AND isValidUser: "when user.email.$exists() && user.verified.$exists() *? boolean : =false", // Logical OR hasContact: "when user.email.$exists() || user.phone.$exists() *? boolean : =false", // Complex combinations canAccessFeature: "when user.role.$in(admin,premium) && config.features.$contains(advanced) *? boolean : =false", // Nested conditions accessLevel: "when user.role=admin *? when config.superAdmin.$exists() *? =super : =admin : =user", }); ``` ### Deep Property Access ```typescript const DeepAccessSchema = Interface({ data: "any?", // Multi-level property access hasDeepFeature: "when data.user.profile.settings.advanced.features.$exists() *? boolean : =false", // Array property access hasFirstItem: "when data.items[0].$exists() *? boolean : =false", // Mixed access patterns hasNestedValue: "when data.config.nested.deep.value.$exists() *? boolean : =false", }); ``` ### Special Characters and Unicode ```typescript const SpecialCharsSchema = Interface({ config: "any?", // Properties with hyphens hasAdminOverride: 'when config["admin-override"].$exists() *? boolean : =not_allowed', // Properties with spaces hasSpecialConfig: 'when config["special config"].$exists() *? boolean : =no_sp_config', }); const config = { "admin-override": true, "special config": true } const result = SpecialCharsSchema.safeParse({ config, hasAdminOverride: "not_allowed", // should throw err hasSpecialConfig: "no_sp_config", // should also throw err }); if (result.success) { console.log("✅ Expected success:", result.data); } else { console.log("❌ Expected errors:", result.errors); } ``` ## Migration from V1 to V2 ### Migration Examples #### Basic Property Checking ```typescript const Schema1 = Interface({ role: "admin|user|guest", permissions: "when role=admin *? string[] : string[]?", }); const Schema2 = Interface({ role: "admin|user|guest", config: "any?", permissions: "when config.hasPermissions.$exists() *? string[] : =[]", }); ``` #### Complex Business Logic ```typescript const V1BusinessSchema = Interface({ accountType: "free|premium|enterprise", userLevel: "basic|advanced|expert", maxProjects: "when accountType=free *? number(1,3) : number(1,100)", advancedFeatures: "when userLevel.in(advanced,expert) *? string[] : string[]?", }); const V2BusinessSchema = Interface({ accountType: "free|premium|enterprise", userLevel: "basic|advanced|expert", config: "any?", features: "any?", maxProjects: "when config.account.$in(free,trial) *? number(1,3) : number(1,100)", advancedFeatures: "when features.advanced.$exists() *? string[] : =[]", // New capabilities not possible in V1 dynamicLimits: "when config.limits.$exists() *? any : ={}", customFeatures: "when features.custom.$exists() *? string[] : =[]", }); ``` ### Migration Strategy 1. **Identify V1 patterns** in your existing schemas 2. **Add runtime data objects** (`config: "any?"`, `features: "any?"`) 3. **Replace simple comparisons** with runtime method calls 4. **Enhance with new capabilities** available in V2 5. **Test thoroughly** to ensure behavior matches expectations ## Best Practices ### 1. Use Descriptive Property Names ```typescript // ✅ Good const Schema = Interface({ config: "any?", hasPermissions: "when config.permissions.$exists() *? boolean : =false", canEditContent: "when config.editRights.$exists() *? boolean : =false", }); // ❌ Avoid const Schema = Interface({ config: "any?", p: "when config.permissions.$exists() *? boolean : =false", e: "when config.editRights.$exists() *? boolean : =false", }); ``` ### 2. Provide Meaningful Defaults ```typescript // ✅ Good - Clear default values const Schema = Interface({ config: "any?", userRole: "when config.role.$exists() *? string : =guest", permissions: "when config.permissions.$exists() *? string[] : =[]", settings: 'when config.settings.$exists() *? any : ={"theme":"light"}', }); // ❌ Avoid - Unclear defaults const Schema = Interface({ config: "any?", userRole: "when config.role.$exists() *? string : =unknown", permissions: "when config.permissions.$exists() *? string[] : =null", }); ``` ### 3. Use Appropriate Methods ```typescript // ✅ Good - Use specific methods for specific checks const Schema = Interface({ data: "any?", hasContent: "when data.description.$exists() && !data.description.$empty() *? boolean : =false", isValidEmail: "when data.email.$exists() && data.email.$contains(@) *? boolean : =false", isAdminUser: "when data.role.$in(admin,super_admin) *? boolean : =false", }); // ❌ Avoid - Generic existence checking when specific methods exist const Schema = Interface({ data: "any?", hasContent: "when data.description.$exists() *? boolean : =false", // Doesn't check if empty isValidEmail: "when data.email.$exists() *? boolean : =false", // Doesn't validate format isAdminUser: "when data.role.$exists() *? boolean : =false", // Doesn't check specific values }); ``` ### 4. Handle Edge Cases ```typescript const RobustSchema = Interface({ user: "any?", config: "any?", // Handle missing nested properties safely hasValidProfile: "when user.$exists() && user.profile.$exists() && !user.profile.$empty() *? boolean : =false", // Provide fallbacks for missing configuration maxRetries: "when config.retries.$exists() *? number : =3", timeout: "when config.timeout.$exists() *? number : =5000", // Handle array edge cases hasItems: "when config.items.$exists() && !config.items.$empty() *? boolean : =false", }); ``` ### 5. Performance Considerations ```typescript // ✅ Good - Efficient condition ordering const Schema = Interface({ config: "any?", // Check existence first (fastest) hasFeature: "when config.$exists() && config.features.$exists() *? boolean : =false", // Simple checks before complex ones isEnabled: "when config.enabled.$exists() && config.features.$contains(advanced) *? boolean : =false", }); // ❌ Avoid - Inefficient ordering const Schema = Interface({ config: "any?", // Complex check before existence check hasFeature: "when config.features.$contains(advanced) && config.$exists() *? boolean : =false", }); ``` ## 🔗 Related Documentation - **[Getting Started](./GETTING-STARTED.md)** - Basic Fortify Schema usage - **[Field Types Reference](./FIELD-TYPES.md)** - Complete type reference - **[Examples Collection](./EXAMPLES.md)** - Real-world usage patterns - **[Quick Reference](./QUICK-REFERENCE.md)** - Syntax cheat sheet