UNPKG

fortify-schema

Version:

TypeScript interface-like schema validation system that's easier to use than Zod

356 lines (256 loc) 13.4 kB
# Fortify Schema - Conditional Validation Operators Reference This document provides a comprehensive reference for all conditional validation operators available in Fortify Schema. ## Overview Fortify Schema supports multiple conditional validation syntaxes: 1. **Syntax** (Recommended): `"when condition *? thenSchema : elseSchema"` 2. **Parentheses Syntax**: `"when(condition) then(thenSchema) else(elseSchema)"` 3. **When.field() API**: Programmatic conditional validation 4. **Legacy Syntax**: `"when:condition:thenSchema:elseSchema"` (backward compatibility) ## Supported Operators ### Comparison Operators | Operator | Description | Example | IDE Support | | -------- | --------------------- | ---------------------------------------------- | -------------------------------------- | | `=` | Equals | `"when role=admin *? string[] : string[]?"` | ✅ Full TypeScript inference | | `!=` | Not equals | `"when accountType!=free *? string : string?"` | ⚠️ **Works at runtime, no IDE errors** | | `>` | Greater than | `"when age>18 *? string : string?"` | ✅ Full TypeScript inference | | `<` | Less than | `"when age<65 *? string : string?"` | ✅ Full TypeScript inference | | `>=` | Greater than or equal | `"when age>=18 *? string : string?"` | ✅ Full TypeScript inference | | `<=` | Less than or equal | `"when age<=65 *? string : string?"` | ✅ Full TypeScript inference | ### Pattern Operators | Operator | Description | Example | IDE Support | | -------- | -------------------- | --------------------------------------------- | ---------------------------- | | `~` | Regex match | `"when email~^admin *? string[] : string[]?"` | ✅ Full TypeScript inference | | `!~` | Negative regex match | `"when email!~@temp *? string : string?"` | ✅ Full TypeScript inference | ### Existence Operators | Operator | Description | Example | IDE Support | | ---------- | --------------------------------- | -------------------------------------------------- | ---------------------------- | | `.exists` | Field exists (not null/undefined) | `"when paymentMethod.exists *? string : string?"` | ✅ Full TypeScript inference | | `.!exists` | Field does not exist | `"when paymentMethod.!exists *? string? : string"` | ✅ Full TypeScript inference | ### State Operators | Operator | Description | Example | IDE Support | | --------- | ------------------------------------ | ------------------------------------------------ | ---------------------------- | | `.empty` | Field is empty (string/array/object) | `"when description.empty *? =required : string"` | ✅ Full TypeScript inference | | `.!empty` | Field is not empty | `"when tags.!empty *? string[] : string[]?"` | ✅ Full TypeScript inference | | `.null` | Field is exactly null | `"when optionalField.null *? string? : string"` | ✅ Full TypeScript inference | | `.!null` | Field is not null | `"when requiredField.!null *? string : string?"` | ✅ Full TypeScript inference | ### Array Operators | Operator | Description | Example | IDE Support | | -------- | ------------------ | ------------------------------------------------------- | ---------------------------- | | `.in()` | Value in array | `"when role.in(admin,teacher) *? string[] : string[]?"` | ✅ Full TypeScript inference | | `.!in()` | Value not in array | `"when role.!in(guest,user) *? string[] : string[]?"` | ✅ Full TypeScript inference | ### String Operators | Operator | Description | Example | IDE Support | | --------------- | ----------------------------- | ------------------------------------------------------- | ---------------------------- | | `.startsWith()` | String starts with value | `"when filename.startsWith(temp_) *? =delete : string"` | ✅ Full TypeScript inference | | `.endsWith()` | String ends with value | `"when filename.endsWith(.tmp) *? =delete : string"` | ✅ Full TypeScript inference | | `.contains()` | String contains value | `"when path.contains(/temp/) *? =cleanup : string"` | ✅ Full TypeScript inference | | `.!contains()` | String does not contain value | `"when path.!contains(/secure/) *? =public : =private"` | ✅ Full TypeScript inference | ## Syntax Examples ### 1. Clean Syntax (Recommended) The cleanest and most intuitive syntax: ```typescript const UserSchema = Interface({ role: "admin|user|guest", accountType: "free|premium|enterprise", age: "int(13,120)", userType: "student|teacher|admin", email: "email", description: "string?", tags: "string[]?", // Basic equality permissions: "when role=admin *? string[] : string[]?", // Inequality (works at runtime, no IDE errors) paymentMethod: "when accountType!=free *? string : string?", // Numeric comparisons accessLevel: "when age>=18 *? string : string?", discountRate: "when age<25 *? number(0,0.5) : number(0,0.1)", // Pattern matching adminAccess: "when email~^admin *? =true : =false", publicEmail: "when email!~@temp *? =public : =private", // Existence checks billingAddress: "when paymentMethod.exists *? string : string?", guestFeatures: "when paymentMethod.!exists *? string[] : string[]?", // State checks descriptionRequired: "when description.empty *? =required : string", tagValidation: "when tags.!empty *? string[] : string[]?", optionalCheck: "when description.null *? string? : string", requiredCheck: "when email.!null *? string : string?", // Array operations maxProjects: "when userType.in(admin,teacher) *? int(1,) : int(1,10)", restrictions: "when role.!in(guest) *? string[] : string[]?", // String operations tempFileCleanup: "when filename.startsWith(temp_) *? =delete : =keep", backupRequired: "when filename.endsWith(.important) *? =true : =false", securityLevel: "when path.contains(/secure/) *? =high : =normal", publicAccess: "when path.!contains(/private/) *? =true : =false", }); ``` ### 2. Parentheses Syntax More explicit syntax with proper nesting support: ```typescript const OrderSchema = Interface({ orderType: "pickup|delivery", customerType: "regular|premium|vip", // Basic conditions deliveryFee: "when(orderType=delivery) then(number(5,50)) else(number?)", // Inequality rushFee: "when(customerType!=vip) then(number(10,25)) else(number?)", // Numeric comparisons discount: "when(orderValue>=100) then(number(0,0.2)) else(number(0,0.05))", // Special operators insurance: "when(deliveryAddress.exists) then(boolean) else(boolean?)", priority: "when(customerType.in(premium,vip)) then(high|urgent) else(normal|low)", }); ``` ### 3. When.field() API (Programmatic) For complex logic and better IDE support: ```typescript const EventSchema = Interface({ eventType: "conference|workshop|webinar", duration: "int(30,480)", // minutes attendeeCount: "int(1,1000)", // Programmatic conditional validation venue: When.field("eventType") .is("webinar") .then("string?") .isNot("webinar") .then("string") .default("string"), catering: When.field("attendeeCount") .when((count) => count >= 50) .then("boolean") .default("boolean?"), equipment: When.field("eventType") .in(["conference", "workshop"]) .then("string[]") .default("string[]?"), }); ``` ## IDE Support Analysis ### ✅ **Working Operators (Full TypeScript Support)** These operators work perfectly with TypeScript IDE inference: **Comparison Operators:** - `=` (equals) - `>` (greater than) - `<` (less than) - `>=` (greater than or equal) - `<=` (less than or equal) **Pattern Operators:** - `~` (regex match) - `!~` (negative regex match) **Existence Operators:** - `.exists` (field exists) - `.!exists` (field does not exist) **State Operators:** - `.empty` (field is empty) - `.!empty` (field is not empty) - `.null` (field is exactly null) - `.!null` (field is not null) **Array Operators:** - `.in()` (value in array) - `.!in()` (value not in array) **String Operators:** - `.startsWith()` (string starts with value) - `.endsWith()` (string ends with value) - `.contains()` (string contains value) - `.!contains()` (string does not contain value) ### ⚠️ **Runtime-Only Operators** These operators work at runtime but don't show IDE errors: - `!=` (not equals) - **This is the issue you discovered!** ## The `!=` Operator Issue You correctly identified that `!=` doesn't throw IDE errors but works at runtime: ```typescript // This compiles without errors but validates correctly at runtime permissions: "when role!=admin *? string[]? : string[]"; // ^^ // No IDE error, but runtime validation works! ``` **Why this happens:** - TypeScript's conditional type inference doesn't recognize `!=` as a valid type guard - The runtime parser correctly handles `!=` operator - Other operators like `=`, `>=`, etc. work with TypeScript's type system ## Workarounds for `!=` IDE Support ### Option 1: Use `.!in()` for single values ```typescript // Instead of: "when role!=admin *? ..." permissions: "when role.!in(admin) *? string[]? : string[]"; ``` ### Option 2: Use When.field() API ```typescript permissions: When.field("role") .isNot("admin") .then("string[]?") .else("string[]"); ``` ### Option 3: Use positive logic ```typescript // Instead of: "when accountType!=free *? ..." paymentMethod: "when accountType.in(premium,enterprise) *? string : string?"; ``` ## Complete Working Example ```typescript import { Interface, When } from "fortify-schema"; const UserSchema = Interface({ // Basic fields role: "admin|user|guest", accountType: "free|premium|enterprise", age: "int(13,120)", userType: "student|teacher|admin", // ✅ WORKING: Equality (full IDE support) permissions: "when role=admin *? string[] : string[]?", // ⚠️ RUNTIME ONLY: Inequality (no IDE errors, but validates correctly) paymentMethod: "when accountType!=free *? string : string?", // ✅ WORKING: Numeric comparisons (full IDE support) accessLevel: "when age>=18 *? string : string?", discountRate: "when userType=student *? number(0,0.5) : number(0,0.1)", // ✅ WORKING: Special operators (full IDE support) maxProjects: "when userType.in(admin,teacher) *? int(1,) : int(1,10)", billingAddress: "when paymentMethod.exists *? string : string?", // ✅ WORKING: Programmatic API (full IDE support) notifications: When.field("accountType") .isNot("free") .then("string[]") .else("string[]?"), }); // Test the schema const result = UserSchema.safeParse({ role: "=user", accountType: "free", age: 25, userType: "student", // paymentMethod: undefined, // This will pass validation correctly }); console.log(result.success); // true console.log(result.data); // Fully typed result ``` ## Operator Precedence When parsing conditions, operators are checked in this order (highest to lowest precedence): **Existence Operators (Highest Precedence):** 1. `.!exists` (field does not exist) 2. `.exists` (field exists) **State Operators:** 3. `.!empty` (field is not empty) 4. `.empty` (field is empty) 5. `.!null` (field is not null) 6. `.null` (field is exactly null) **Array Operators:** 7. `.!in()` (value not in array) 8. `.in()` (value in array) **Pattern Operators:** 9. `!~` (negative regex match) 10. `~` (regex match) **String Operators:** 11. `.!contains()` (string does not contain value) 12. `.contains()` (string contains value) 13. `.endsWith()` (string ends with value) 14. `.startsWith()` (string starts with value) **Comparison Operators (Lowest Precedence):** 15. `!=` (not equals) 16. `>=` (greater than or equal) 17. `<=` (less than or equal) 18. `>` (greater than) 19. `<` (less than) 20. `=` (equals) ## Best Practices 1. **Use `=` instead of `!=` when possible** for better IDE support 2. **Use `.!in()` for multiple exclusions** instead of multiple `!=` conditions 3. **Use When.field() API for complex logic** that needs full TypeScript support 4. **Test conditional validation thoroughly** since some operators work at runtime only 5. **Prefer positive logic over negative logic** for better readability ## Migration Guide If you're using `!=` and want full IDE support: ```typescript // Before (runtime only) permissions: "when role!=admin *? string[]? : string[]"; // After (full IDE support) permissions: When.field("role") .isNot("admin") .then("string[]?") .else("string[]"); // Or use positive logic permissions: "when role.in(user,guest) *? string[]? : string[]"; ``` This comprehensive reference should help you understand why `!=` works at runtime but doesn't show IDE errors, and provides alternatives for full TypeScript support.