a database layer insipred by caqti and ecto
1# Changesets 2 3Changesets are the heart of data validation in repodb. They track changes to your data and validate them before persisting to the database. 4 5## Creating Changesets 6 7Start with a record value and create a changeset: 8 9```ocaml 10open Repodb 11 12type user = { id : int; name : string; email : string; age : int } 13 14let empty_user = { id = 0; name = ""; email = ""; age = 0 } 15 16(* Create a changeset from a record *) 17let cs = Changeset.create empty_user 18``` 19 20### For Insert vs Update 21 22Use explicit action markers for clarity: 23 24```ocaml 25(* For new records *) 26let cs = Changeset.for_insert empty_user 27 28(* For existing records *) 29let cs = Changeset.for_update existing_user 30``` 31 32## Casting Parameters 33 34Cast external input (e.g., from HTTP requests) to your changeset: 35 36```ocaml 37let params = [("name", "Alice"); ("email", "alice@example.com"); ("age", "25")] 38 39let cs = 40 Changeset.create empty_user 41 |> Changeset.cast params ~fields:[name_field; email_field; age_field] 42``` 43 44Only fields listed in `~fields` are accepted. Other parameters are ignored, protecting against mass assignment. 45 46### Working with Changes 47 48```ocaml 49(* Add a change programmatically *) 50let cs = Changeset.put_change name_field "Bob" cs 51 52(* Remove a change *) 53let cs = Changeset.delete_change name_field cs 54 55(* Get a specific change *) 56let name_change = Changeset.get_change cs name_field (* string option *) 57 58(* Get all changes as list *) 59let all_changes = Changeset.changes cs (* (string * change) list *) 60``` 61 62## Validations 63 64### Required Fields 65 66```ocaml 67let cs = 68 Changeset.create empty_user 69 |> Changeset.cast params ~fields:[name_field; email_field] 70 |> Changeset.validate_required [name_field; email_field] 71``` 72 73Adds error "can't be blank" if field is missing or empty string. 74 75### Format Validation 76 77```ocaml 78let cs = 79 cs |> Changeset.validate_format email_field ~pattern:"^[^@]+@[^@]+$" 80``` 81 82Uses PCRE regular expressions. Adds error "has invalid format" on mismatch. 83 84### Length Validation 85 86```ocaml 87let cs = 88 cs 89 |> Changeset.validate_length name_field ~min:2 ~max:100 90 |> Changeset.validate_length password_field ~min:8 91 |> Changeset.validate_length pin_field ~is:4 (* exact length *) 92``` 93 94Error messages: 95- "should be at least N character(s)" 96- "should be at most N character(s)" 97- "should be N character(s)" 98 99### Number Validation 100 101```ocaml 102let cs = 103 cs 104 |> Changeset.validate_number age_field ~greater_than_or_equal:0 105 |> Changeset.validate_number age_field ~less_than:150 106 |> Changeset.validate_number price_field ~greater_than:0 107``` 108 109Options: 110- `~greater_than:n` 111- `~less_than:n` 112- `~greater_than_or_equal:n` 113- `~less_than_or_equal:n` 114 115### Inclusion/Exclusion 116 117```ocaml 118(* Value must be one of these *) 119let cs = 120 cs |> Changeset.validate_inclusion status_field ~values:["active"; "inactive"] 121 122(* Value must NOT be any of these *) 123let cs = 124 cs |> Changeset.validate_exclusion username_field ~values:["admin"; "root"] 125``` 126 127### Acceptance (for checkboxes) 128 129```ocaml 130let cs = cs |> Changeset.validate_acceptance terms_field 131``` 132 133Validates that value is `true`, `"true"`, or `"1"`. 134 135### Confirmation (password confirmation) 136 137```ocaml 138let cs = 139 cs |> Changeset.validate_confirmation password_field 140 ~confirmation_field:password_confirmation_field 141``` 142 143Adds error "does not match" on the confirmation field if values differ. 144 145### Custom Validation 146 147```ocaml 148let cs = 149 cs |> Changeset.validate_change email_field (fun email -> 150 if String.contains email '+' then 151 Error "plus signs not allowed in email" 152 else 153 Ok () 154 ) 155``` 156 157### Generic Validation Function 158 159For complex multi-field validation: 160 161```ocaml 162let cs = 163 cs |> Changeset.validate (fun cs -> 164 match Changeset.get_change cs start_date_field, 165 Changeset.get_change cs end_date_field with 166 | Some start_date, Some end_date when end_date < start_date -> 167 Changeset.add_error cs 168 ~field:"end_date" 169 ~message:"must be after start date" 170 ~validation:"date_range" 171 | _ -> cs 172 ) 173``` 174 175## Checking Validity 176 177```ocaml 178if Changeset.is_valid cs then 179 (* proceed with insert/update *) 180 let user = Changeset.data cs in 181 ... 182else 183 (* handle errors *) 184 let errors = Changeset.errors cs in 185 ... 186``` 187 188### Error Handling 189 190```ocaml 191(* Get all errors *) 192let errors = Changeset.errors cs (* Error.validation_error list *) 193 194(* Get error messages as strings *) 195let messages = Changeset.error_messages cs (* ["name can't be blank"; ...] *) 196 197(* Check if specific field has error *) 198let has_name_error = Changeset.has_error cs name_field 199 200(* Get error for specific field *) 201let name_error = Changeset.get_error cs name_field (* validation_error option *) 202 203(* Iterate over errors *) 204Changeset.traverse_errors cs (fun field message -> 205 Printf.printf "%s: %s\n" field message 206) 207``` 208 209### Extract Data 210 211```ocaml 212(* Get the underlying data (with changes applied - not automatic!) *) 213let user = Changeset.data cs 214 215(* Apply action - returns Ok data or Error errors *) 216let result = Changeset.apply_action cs 217(* result : (user, Error.validation_error list) result *) 218``` 219 220## Database Constraints 221 222Mark fields for database-level constraint checking: 223 224```ocaml 225let cs = 226 Changeset.create empty_user 227 |> Changeset.cast params ~fields:[name_field; email_field] 228 |> Changeset.unique_constraint email_field 229 |> Changeset.foreign_key_constraint author_id_field 230 ~references:("users", "id") 231 |> Changeset.check_constraint ~name:"positive_age" age_field 232 ~expression:"age >= 0" 233``` 234 235These don't validate in OCaml but help map database constraint violations back to changeset errors. 236 237## Nested Changesets 238 239### Single Association 240 241```ocaml 242let cs = 243 Changeset.create empty_post 244 |> Changeset.cast params ~fields:[title_field; body_field] 245 |> Changeset.cast_assoc_one ~assoc_name:"author" 246 ~params:author_params 247 ~cast_fn:create_author_changeset 248``` 249 250Errors are prefixed: `"author.name can't be blank"` 251 252### Multiple Associations 253 254```ocaml 255let cs = 256 Changeset.create empty_post 257 |> Changeset.cast_assoc_many ~assoc_name:"comments" 258 ~params_list:[comment1_params; comment2_params] 259 ~cast_fn:create_comment_changeset 260``` 261 262Errors are indexed: `"comments[0].body can't be blank"` 263 264### Embedded Data 265 266For JSON/embedded documents: 267 268```ocaml 269let cs = 270 Changeset.create empty_user 271 |> Changeset.cast_embed ~embed_name:"settings" 272 ~params:settings_params 273 ~parse:parse_settings 274``` 275 276## Complete Example 277 278```ocaml 279let create_user_changeset params = 280 let empty_user = { id = 0; name = ""; email = ""; age = 0 } in 281 Changeset.create empty_user 282 |> Changeset.cast params ~fields:[name_field; email_field; age_field] 283 |> Changeset.validate_required [name_field; email_field] 284 |> Changeset.validate_format email_field ~pattern:"^[^@]+@[^@]+\\.[^@]+$" 285 |> Changeset.validate_length name_field ~min:2 ~max:100 286 |> Changeset.validate_number age_field ~greater_than_or_equal:0 ~less_than:150 287 |> Changeset.unique_constraint email_field 288 289let insert_user conn params = 290 let cs = create_user_changeset params in 291 if Changeset.is_valid cs then 292 let user = Changeset.data cs in 293 Repo.insert conn 294 ~table:users_table 295 ~columns:["name"; "email"; "age"] 296 ~values:[ 297 Driver.Value.text user.name; 298 Driver.Value.text user.email; 299 Driver.Value.int user.age; 300 ] 301 else 302 Error (Error.Validation_failed (Changeset.error_messages cs)) 303``` 304 305## Next Steps 306 307- [Queries](queries.md) - Build and execute queries 308- [Repo](repo.md) - Database operations 309- [Associations](associations.md) - Define relationships