Skip to content
On this page

Changesets

Changesets validate and cast data before database operations, inspired by Ecto's changeset pattern.

Creating a Changeset

typescript
import { changeset, cast } from "hull"

// Create empty changeset
const cs = changeset(User, {})

// Cast data into changeset (only specified fields are allowed)
const validated = cast(cs, {
  email: "[email protected]",
  name: "Bob",
  admin: true  // Will be ignored - not in allowed fields
}, ["email", "name"])

Validation

Add validations to changesets:

typescript
import { validateRequired, validateLength, validateFormat } from "hull"

let cs = changeset(User, {})
cs = cast(cs, data, ["email", "name", "password"])
cs = validateRequired(cs, ["email", "password"])
cs = validateLength(cs, "password", { min: 8, max: 100 })
cs = validateFormat(cs, "email", /^[^\s@]+@[^\s@]+\.[^\s@]+$/)

Checking Validity

typescript
if (cs.valid) {
  const user = await insert(repo, cs)
} else {
  console.log(cs.errors)
  // { email: ["is required"], password: ["must be at least 8 characters"] }
}

Updating Records

Use changesets with existing data:

typescript
// Fetch existing user
const user = await one(repo, whereEq(from(User), "id", userId))

// Create changeset from existing data
const cs = cast(
  changeset(User, user),
  { name: "New Name" },
  ["name"]
)

// Update
const updated = await update(repo, cs, "id", userId)

Custom Validation

typescript
const validatePasswordMatch = (cs, field1, field2) => {
  if (cs.changes[field1] !== cs.changes[field2]) {
    return {
      ...cs,
      valid: false,
      errors: { ...cs.errors, [field2]: ["does not match"] }
    }
  }
  return cs
}

cs = validatePasswordMatch(cs, "password", "password_confirmation")

Changeset Structure

typescript
type Changeset<S> = {
  schema: S
  data: Record<string, unknown>      // Original data
  changes: Record<string, unknown>   // New/modified values
  errors: Record<string, string[]>   // Validation errors
  valid: boolean                     // Overall validity
}

Released under the MIT License.