# Changesets Changesets are the heart of data validation in repodb. They track changes to your data and validate them before persisting to the database. ## Creating Changesets Start with a record value and create a changeset: ```ocaml open Repodb type user = { id : int; name : string; email : string; age : int } let empty_user = { id = 0; name = ""; email = ""; age = 0 } (* Create a changeset from a record *) let cs = Changeset.create empty_user ``` ### For Insert vs Update Use explicit action markers for clarity: ```ocaml (* For new records *) let cs = Changeset.for_insert empty_user (* For existing records *) let cs = Changeset.for_update existing_user ``` ## Casting Parameters Cast external input (e.g., from HTTP requests) to your changeset: ```ocaml let params = [("name", "Alice"); ("email", "alice@example.com"); ("age", "25")] let cs = Changeset.create empty_user |> Changeset.cast params ~fields:[name_field; email_field; age_field] ``` Only fields listed in `~fields` are accepted. Other parameters are ignored, protecting against mass assignment. ### Working with Changes ```ocaml (* Add a change programmatically *) let cs = Changeset.put_change name_field "Bob" cs (* Remove a change *) let cs = Changeset.delete_change name_field cs (* Get a specific change *) let name_change = Changeset.get_change cs name_field (* string option *) (* Get all changes as list *) let all_changes = Changeset.changes cs (* (string * change) list *) ``` ## Validations ### Required Fields ```ocaml let cs = Changeset.create empty_user |> Changeset.cast params ~fields:[name_field; email_field] |> Changeset.validate_required [name_field; email_field] ``` Adds error "can't be blank" if field is missing or empty string. ### Format Validation ```ocaml let cs = cs |> Changeset.validate_format email_field ~pattern:"^[^@]+@[^@]+$" ``` Uses PCRE regular expressions. Adds error "has invalid format" on mismatch. ### Length Validation ```ocaml let cs = cs |> Changeset.validate_length name_field ~min:2 ~max:100 |> Changeset.validate_length password_field ~min:8 |> Changeset.validate_length pin_field ~is:4 (* exact length *) ``` Error messages: - "should be at least N character(s)" - "should be at most N character(s)" - "should be N character(s)" ### Number Validation ```ocaml let cs = cs |> Changeset.validate_number age_field ~greater_than_or_equal:0 |> Changeset.validate_number age_field ~less_than:150 |> Changeset.validate_number price_field ~greater_than:0 ``` Options: - `~greater_than:n` - `~less_than:n` - `~greater_than_or_equal:n` - `~less_than_or_equal:n` ### Inclusion/Exclusion ```ocaml (* Value must be one of these *) let cs = cs |> Changeset.validate_inclusion status_field ~values:["active"; "inactive"] (* Value must NOT be any of these *) let cs = cs |> Changeset.validate_exclusion username_field ~values:["admin"; "root"] ``` ### Acceptance (for checkboxes) ```ocaml let cs = cs |> Changeset.validate_acceptance terms_field ``` Validates that value is `true`, `"true"`, or `"1"`. ### Confirmation (password confirmation) ```ocaml let cs = cs |> Changeset.validate_confirmation password_field ~confirmation_field:password_confirmation_field ``` Adds error "does not match" on the confirmation field if values differ. ### Custom Validation ```ocaml let cs = cs |> Changeset.validate_change email_field (fun email -> if String.contains email '+' then Error "plus signs not allowed in email" else Ok () ) ``` ### Generic Validation Function For complex multi-field validation: ```ocaml let cs = cs |> Changeset.validate (fun cs -> match Changeset.get_change cs start_date_field, Changeset.get_change cs end_date_field with | Some start_date, Some end_date when end_date < start_date -> Changeset.add_error cs ~field:"end_date" ~message:"must be after start date" ~validation:"date_range" | _ -> cs ) ``` ## Checking Validity ```ocaml if Changeset.is_valid cs then (* proceed with insert/update *) let user = Changeset.data cs in ... else (* handle errors *) let errors = Changeset.errors cs in ... ``` ### Error Handling ```ocaml (* Get all errors *) let errors = Changeset.errors cs (* Error.validation_error list *) (* Get error messages as strings *) let messages = Changeset.error_messages cs (* ["name can't be blank"; ...] *) (* Check if specific field has error *) let has_name_error = Changeset.has_error cs name_field (* Get error for specific field *) let name_error = Changeset.get_error cs name_field (* validation_error option *) (* Iterate over errors *) Changeset.traverse_errors cs (fun field message -> Printf.printf "%s: %s\n" field message ) ``` ### Extract Data ```ocaml (* Get the underlying data (with changes applied - not automatic!) *) let user = Changeset.data cs (* Apply action - returns Ok data or Error errors *) let result = Changeset.apply_action cs (* result : (user, Error.validation_error list) result *) ``` ## Database Constraints Mark fields for database-level constraint checking: ```ocaml let cs = Changeset.create empty_user |> Changeset.cast params ~fields:[name_field; email_field] |> Changeset.unique_constraint email_field |> Changeset.foreign_key_constraint author_id_field ~references:("users", "id") |> Changeset.check_constraint ~name:"positive_age" age_field ~expression:"age >= 0" ``` These don't validate in OCaml but help map database constraint violations back to changeset errors. ## Nested Changesets ### Single Association ```ocaml let cs = Changeset.create empty_post |> Changeset.cast params ~fields:[title_field; body_field] |> Changeset.cast_assoc_one ~assoc_name:"author" ~params:author_params ~cast_fn:create_author_changeset ``` Errors are prefixed: `"author.name can't be blank"` ### Multiple Associations ```ocaml let cs = Changeset.create empty_post |> Changeset.cast_assoc_many ~assoc_name:"comments" ~params_list:[comment1_params; comment2_params] ~cast_fn:create_comment_changeset ``` Errors are indexed: `"comments[0].body can't be blank"` ### Embedded Data For JSON/embedded documents: ```ocaml let cs = Changeset.create empty_user |> Changeset.cast_embed ~embed_name:"settings" ~params:settings_params ~parse:parse_settings ``` ## Complete Example ```ocaml let create_user_changeset params = let empty_user = { id = 0; name = ""; email = ""; age = 0 } in Changeset.create empty_user |> Changeset.cast params ~fields:[name_field; email_field; age_field] |> Changeset.validate_required [name_field; email_field] |> Changeset.validate_format email_field ~pattern:"^[^@]+@[^@]+\\.[^@]+$" |> Changeset.validate_length name_field ~min:2 ~max:100 |> Changeset.validate_number age_field ~greater_than_or_equal:0 ~less_than:150 |> Changeset.unique_constraint email_field let insert_user conn params = let cs = create_user_changeset params in if Changeset.is_valid cs then let user = Changeset.data cs in Repo.insert conn ~table:users_table ~columns:["name"; "email"; "age"] ~values:[ Driver.Value.text user.name; Driver.Value.text user.email; Driver.Value.int user.age; ] else Error (Error.Validation_failed (Changeset.error_messages cs)) ``` ## Next Steps - [Queries](queries.md) - Build and execute queries - [Repo](repo.md) - Database operations - [Associations](associations.md) - Define relationships