ESC
Type to search...
S
Soli Docs

Validations & Callbacks

Ensure data integrity with model validations and hook into the lifecycle of your records with callbacks.

Defining Validations

Use validates inside your model class to declare rules for each field. Validations run automatically before create and save operations.

class User < Model
    validates("email", { "presence": true, "uniqueness": true })
    validates("name", { "presence": true, "min_length": 2 })
    validates("age", { "numericality": true, "min": 0, "max": 150 })
    validates("password", { "min_length": 8 })
end

Multiple rules per field: You can combine any number of validation options in a single validates call. All rules for the field are checked together, and every failing rule produces its own error message.

Validation Options

Each option controls a specific check on the field value:

Option Description
presence: true Field must be present and not empty
uniqueness: true Value must be unique in collection
min_length: n String must be at least n characters
max_length: n String must be at most n characters
format: "regex" String must match regex pattern
numericality: true Value must be a number
min: n Number must be >= n
max: n Number must be <= n
custom: "method" Call custom validation method

Validation Results

Operations like Model.create() return a result hash indicating success or failure. When validation fails, the "errors" key contains an array of error objects with "field" and "message" keys.

let result = User.create({ "email": "" })
if result["valid"]
    let user = result["record"]
    print("Created user: " + user._key)
else
    for error in result["errors"]
        print(error["field"] + ": " + error["message"])
    end
end

Tip: Always check result["valid"] before accessing the record. When validations fail, the record is not persisted to the database.

Custom Methods on Models

Define instance methods directly in your model class to encapsulate business logic. Use this to access the current instance's fields.

class User < Model
    def is_admin -> Bool
        this.role == "admin"
    end

    def full_name -> String
        this.first_name + " " + this.last_name
    end
end

let user = User.find("user123")
if user.is_admin
    print("Welcome, admin " + user.full_name())
end

Zero-arg methods: Methods like is_admin that take no arguments can be called without parentheses (e.g. user.is_admin). Methods that return a value from a computation should use parentheses when called (e.g. user.full_name()).

Callbacks

Callbacks let you hook into the lifecycle of a model record. Declare them at the class level with the name of the instance method to call.

class User < Model
    before_save("normalize_email")
    after_create("send_welcome_email")
    before_update("log_changes")
    after_delete("cleanup_related")

    def normalize_email
        this.email = this.email.downcase()
    end

    def send_welcome_email
        # Send email logic
    end
end

Execution order: before_save runs before both create and update operations, making it ideal for data normalization. Specific callbacks like before_create or before_update run only for their respective operation.

Available Callbacks

before_save

Before create or update

after_save

After create or update

before_create

Before inserting new record

after_create

After inserting new record

before_update

Before updating record

after_update

After updating record

before_delete

Before deleting record

after_delete

After deleting record

Complete Example

class User < Model
    validates("email", { "presence": true, "uniqueness": true, "format": "^[^@]+@[^@]+$" })
    validates("name", { "presence": true, "min_length": 2, "max_length": 100 })
    validates("age", { "numericality": true, "min": 0, "max": 150 })
    validates("password", { "min_length": 8 })
    validates("role", { "custom": "validate_role" })

    before_save("normalize_email")
    after_create("send_welcome_email")
    before_delete("cleanup_related")

    def normalize_email
        this.email = this.email.downcase()
    end

    def send_welcome_email
        # Send welcome email to new users
    end

    def cleanup_related
        # Remove associated data before deletion
    end

    def validate_role
        let valid_roles = ["admin", "user", "moderator"]
        if !valid_roles.includes?(this.role)
            return "must be one of: admin, user, moderator"
        end
    end

    def is_admin -> Bool
        this.role == "admin"
    end

    def full_name -> String
        this.first_name + " " + this.last_name
    end
end

# Creating a user with validation
let result = User.create({
    "email": "[email protected]",
    "name": "Alice",
    "age": 30,
    "password": "securepass",
    "role": "admin"
})

if result["valid"]
    let user = result["record"]
    print(user.email)       # => "[email protected]" (normalized by before_save)
    print(user.is_admin)    # => true
    print(user.full_name()) # => "Alice "
end

Best Practices

  • Validate early - Add validations to catch bad data before it reaches the database
  • Use presence checks - Required fields should always have presence: true
  • Keep callbacks focused - Each callback method should do one thing well
  • Avoid heavy logic in callbacks - Don't put slow operations in before_save or before_create
  • Use custom validations - For complex rules that can't be expressed with built-in options
  • Check validation results - Always inspect result["valid"] after create

Next Steps