Validating With Profiles

Advanced constraint profiles for targeted validation scenarios

Constraint profiles (introduced in CBValidation 2.0) allow you to validate specific subsets of fields for different scenarios. This enables flexible validation workflows where different operations require different validation rules.

Overview

Instead of validating all constraints every time, profiles let you:

  • Validate only relevant fields for specific operations

  • Improve performance by skipping unnecessary validations

  • Create context-specific validation rules (registration vs. update vs. password change)

  • Support multi-step forms and wizards

Basic Profile Definition

Define profiles using the this.constraintProfiles struct in your object:

User.bx
class {
    property name="firstName" type="string";
    property name="lastName" type="string";
    property name="email" type="string";
    property name="password" type="string";
    property name="confirmPassword" type="string";

    this.constraints = {
        firstName: { required: true, size: "2..50" },
        lastName: { required: true, size: "2..50" },
        email: { required: true, type: "email" },
        password: { required: true, size: "8..50" },
        confirmPassword: { required: true, sameAs: "password" }
    };

    this.constraintProfiles = {
        registration: "firstName,lastName,email,password,confirmPassword",
        update: "firstName,lastName,email",
        passwordChange: "password,confirmPassword"
    };
}

Using Profiles

Every validation method accepts a profiles argument:

// Single profile
var result = validate(target: user, profiles: "update");

// Multiple profiles
var result = validate(target: user, profiles: "update,passwordChange");

// With validateOrFail for APIs
try {
    var validUser = validateOrFail(target: user, profiles: "registration");
    userService.create(validUser);
} catch(ValidationException e) {
    // Handle validation errors
}

Complex Profile Scenarios

Multi-Step Registration Wizard

For complex forms split across multiple steps:

UserRegistration.bx
class {
    // Step 1: Basic Info
    property name="firstName" type="string";
    property name="lastName" type="string";
    property name="email" type="string";

    // Step 2: Account Details
    property name="username" type="string";
    property name="password" type="string";
    property name="confirmPassword" type="string";

    // Step 3: Profile Info
    property name="dateOfBirth" type="date";
    property name="phone" type="string";
    property name="address" type="string";

    // Step 4: Preferences
    property name="newsletter" type="boolean";
    property name="marketingEmails" type="boolean";

    this.constraints = {
        firstName: { required: true, size: "2..50", alpha: true },
        lastName: { required: true, size: "2..50", alpha: true },
        email: { required: true, type: "email" },
        username: { required: true, size: "3..20", regex: "^[a-zA-Z0-9_]+$" },
        password: { required: true, size: "8..50", regex: "^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)" },
        confirmPassword: { required: true, sameAs: "password" },
        dateOfBirth: { required: true, type: "date" },
        phone: { type: "telephone" },
        address: { size: "0..200" }
    };

    this.constraintProfiles = {
        step1: "firstName,lastName,email",
        step2: "username,password,confirmPassword",
        step3: "dateOfBirth,phone,address",
        step4: "newsletter,marketingEmails",
        complete: "firstName,lastName,email,username,password,confirmPassword,dateOfBirth"
    };
}

API Endpoint Profiles

Different API endpoints often need different validation rules:

Product.bx
class {
    property name="id" type="numeric";
    property name="name" type="string";
    property name="description" type="string";
    property name="price" type="numeric";
    property name="category" type="string";
    property name="sku" type="string";
    property name="active" type="boolean";
    property name="tags" type="array";

    this.constraints = {
        name: { required: true, size: "1..100" },
        description: { size: "0..1000" },
        price: { required: true, type: "numeric", min: 0 },
        category: { required: true, inList: "electronics,books,clothing,home" },
        sku: { required: true, regex: "^[A-Z0-9-]+$" },
        active: { type: "boolean", defaultValue: true },
        tags: { arrayItem: { type: "string", size: "1..20" } }
    };

    this.constraintProfiles = {
        // Full product creation
        create: "name,description,price,category,sku,active,tags",

        // Quick product creation (minimal required fields)
        quickCreate: "name,price,category,sku",

        // Update existing product (ID required, others optional)
        update: "name,description,price,category,active,tags",

        // Bulk import validation (stricter rules)
        import: "name,price,category,sku",

        // Admin-only fields
        admin: "name,description,price,category,sku,active,tags",

        // Public API (limited fields)
        public: "name,description,price,category"
    };
}

API Integration Patterns

RESTful API Validation

Use profiles to match your API endpoints:

ProductHandler.bx
function create(event, rc, prc) {
    try {
        // Use 'create' profile for POST /products
        var validProduct = validateOrFail(
            target: rc,
            constraints: "Product",
            profiles: "create"
        );

        var product = productService.create(validProduct);
        return event.renderData(data: product, statusCode: 201);
    } catch(ValidationException e) {
        return event.renderData(
            statusCode: 422,
            data: { errors: deserializeJSON(e.extendedInfo) }
        );
    }
}

function update(event, rc, prc) {
    try {
        // Use 'update' profile for PUT /products/:id
        var validProduct = validateOrFail(
            target: rc,
            constraints: "Product",
            profiles: "update"
        );

        var product = productService.update(rc.id, validProduct);
        return event.renderData(data: product);
    } catch(ValidationException e) {
        return event.renderData(
            statusCode: 422,
            data: { errors: deserializeJSON(e.extendedInfo) }
        );
    }
}

function bulkImport(event, rc, prc) {
    var results = [];

    for (var item in rc.products) {
        try {
            // Stricter validation for bulk imports
            var validProduct = validateOrFail(
                target: item,
                constraints: "Product",
                profiles: "import"
            );

            results.append({
                status: "success",
                product: productService.create(validProduct)
            });
        } catch(ValidationException e) {
            results.append({
                status: "error",
                errors: deserializeJSON(e.extendedInfo),
                data: item
            });
        }
    }

    return event.renderData(data: results);
}

Role-Based Validation

Different user roles may have different validation requirements:

function validateByRole(user, data, role) {
    var profile = switch(role) {
        case "admin": "admin";
        case "manager": "update,category";
        case "user": "public";
        default: "public";
    };

    return validate(
        target: data,
        constraints: "Product",
        profiles: profile
    );
}

Performance Considerations

Profile Selection Strategy

Choose profiles wisely to optimize performance:

// ✅ GOOD - Validates only 3 fields
var result = validate(target=user, profiles="passwordChange");

// ❌ LESS EFFICIENT - Validates all 15+ fields
var result = validate(target=user);

// ✅ GOOD - Multiple targeted profiles
var result = validate(target=user, profiles="update,preferences");

// ❌ AVOID - Overlapping profiles create redundant validation
var result = validate(target=user, profiles="update,admin,complete");

Caching Constraint Definitions

For high-performance scenarios, cache constraint lookups:

// In your service layer
class ProductService {
    property name="validationManager" inject="ValidationManager@cbvalidation";
    property name="constraintCache" type="struct";

    function init() {
        this.constraintCache = {};
        return this;
    }

    function validateProduct(product, profile = "create") {
        var cacheKey = "Product_" & profile;

        if (!this.constraintCache.keyExists(cacheKey)) {
            // Cache the constraint definition for reuse
            this.constraintCache[cacheKey] = validationManager.getConstraints("Product");
        }

        return validationManager.validate(
            target: product,
            constraints: this.constraintCache[cacheKey],
            profiles: profile
        );
    }
}

Profile Size Optimization

Keep profiles focused and avoid overlap:

// ✅ FOCUSED PROFILES - Clear purpose, minimal overlap
this.constraintProfiles = {
    create: "name,email,password",           // New user signup
    login: "email,password",                 // Authentication
    profile: "name,bio,avatar",              // Profile updates
    security: "password,confirmPassword,currentPassword"  // Security changes
};

// ❌ OVERLAPPING PROFILES - Redundant validations
this.constraintProfiles = {
    basic: "name,email",
    full: "name,email,password,bio,avatar",  // Contains 'basic' fields
    admin: "name,email,password,bio,avatar,role,permissions"  // Contains 'full' fields
};

Best Practices

1. Use Descriptive Profile Names

// ✅ CLEAR - Purpose is obvious
registration: "firstName,lastName,email,password,confirmPassword"
passwordReset: "password,confirmPassword,token"
profileUpdate: "firstName,lastName,bio,avatar"

// ❌ UNCLEAR - Hard to understand purpose
p1: "firstName,lastName,email"
basic: "password,confirmPassword"
// ✅ LOGICAL GROUPING
this.constraintProfiles = {
    // Authentication flows
    registration: "email,password,confirmPassword",
    login: "email,password",
    passwordReset: "password,confirmPassword,resetToken",

    // Profile management
    profileBasic: "firstName,lastName,bio",
    profileComplete: "firstName,lastName,bio,avatar,phone,address",

    // Administrative
    adminCreate: "email,firstName,lastName,role,permissions",
    adminUpdate: "firstName,lastName,role,permissions"
};

3. Document Profile Usage

/**
 * User Constraint Profiles
 *
 * registration - New user signup (email + password)
 * login - User authentication (email + password only)
 * profileUpdate - Update user information (no sensitive fields)
 * passwordChange - Change password (requires current + new password)
 * adminEdit - Administrative user editing (includes role/permissions)
 */
this.constraintProfiles = {
    registration: "email,password,confirmPassword,firstName,lastName",
    login: "email,password",
    profileUpdate: "firstName,lastName,bio,phone,address",
    passwordChange: "currentPassword,password,confirmPassword",
    adminEdit: "firstName,lastName,email,role,permissions,active"
};

4. Validate Profile Efficiency

Monitor which profiles are used most frequently and optimize accordingly:

// High-frequency profiles should be as lean as possible
frequentUpdate: "firstName,lastName"  // 2 fields - very fast

// Infrequent but comprehensive profiles can be larger
completeRegistration: "firstName,lastName,email,password,confirmPassword,phone,address,preferences"  // 8 fields - acceptable for rare use

See Also

Last updated

Was this helpful?