Changeset-pipeline combinators for action validate/2 callbacks.
These compose into ordinary Ecto.Changeset pipelines — they are
never replacements for validate_* functions. The dividing line for
where a rule lives: payload-intrinsic rules (format, bounds, inclusion)
belong in the input module's own validations; operation- and actor-aware
rules belong in the action's validate/2.
Summary
Functions
Cheap gates expensive: no-ops when the changeset is already invalid,
otherwise applies fun. All DB-backed checks go through it, so garbage
input never triggers queries
Scoped uniqueness for input changesets. Skips when the field has no change (an untouched value on PATCH is already persisted).
Functions
@spec check(Ecto.Changeset.t(), (Ecto.Changeset.t() -> Ecto.Changeset.t())) :: Ecto.Changeset.t()
Cheap gates expensive: no-ops when the changeset is already invalid,
otherwise applies fun. All DB-backed checks go through it, so garbage
input never triggers queries:
def validate(changeset, ctx) do
changeset
|> validate_length(:note, max: 500)
|> check(&unique(&1, :slug, query: Project, repo: ctx.repo, scope: [org_id: ctx.actor.org_id], except: ctx.subject))
end
@spec unique(Ecto.Changeset.t(), atom(), keyword()) :: Ecto.Changeset.t()
Scoped uniqueness for input changesets. Skips when the field has no change (an untouched value on PATCH is already persisted).
Options:
:query(required) — the persistence queryable to check against:repo(required) — usuallyctx.repo:scope— keyword of extra column/value pairs to filter by (tenancy scoping):except— exclude the current row on PATCH. A struct (uses its primary key), an id (integer/binary, compared toid), or a keyword of column/value pairs. Soft-deletes stay on:query(e.g.from(s in Schema, where: is_nil(s.deleted_at))).
Adds "has already been taken" on the field, tagged
validation: :unique. Like Ecto's unsafe_validate_unique/4 this is
best-effort pre-flight UX — keep the DB unique index and declared
constraint as the source of truth; the runner promotes constraint-error
changesets from execute to :invalid.