An Ash.Resource.Change module that plugs __guarded_change__/1 into the
Ash changeset pipeline.
Usage
Manual wiring
defmodule MyApp.User do
use Ash.Resource, extensions: [GuardedStruct.AshResource]
guardedstruct do
field :email, :string, derives: "sanitize(trim, downcase) validate(email_r)"
end
changes do
change GuardedStruct.AshResource.Change
end
endAuto-wiring
Set auto_wire: true on the guardedstruct section and the change is
injected automatically — no changes do ... end block needed.
Atomic mode
atomic/3 runs the GuardedStruct pipeline in Elixir on the plain
literal values Ash placed in changeset.attributes and
changeset.atomics, then returns {:atomic, sanitized_map} — the
shape Ash uses to substitute pre-computed values into the single SQL
statement. The action stays atomic (one UPDATE, no extra round-trip)
and the persisted value is the sanitized one.
This means sanitize / validate / derive / auto: MFAs / custom
Derive.Extension ops all work in atomic mode. The only blocker is
when the user explicitly provides an Ash.Expr for a field via
Ash.Changeset.atomic_update/3:
Ash.Changeset.atomic_update(record, :counter, expr(counter + 1))In that case we can't sanitize a value we won't know until the SQL
evaluates, so atomic/3 returns {:not_atomic, reason} and Ash
falls back to the imperative path. This is rare in practice — 99% of
changesets pass plain literals.
No require_atomic? false flag is needed on update / destroy actions.
Bulk operations
Ash.bulk_create(input_list, MyApp.User, :create,
return_records?: true,
return_errors?: true
)Ash.bulk_update/3 also works — strategy: :atomic (the default) uses
our atomic pattern; strategy: :stream uses the imperative change/3
path. Both produce identical results.
Summary
Functions
The atomic-mode callback.
Bulk-action entry. Maps change/3 over each changeset.
The Ash.Resource.Change callback for the non-atomic / regular-change
path. Runs the GuardedStruct pipeline on changeset.attributes and
applies the transformed values back, or adds errors.
Functions
The atomic-mode callback.
- If any field's atomic value is an
Ash.Expr, bail with{:not_atomic, reason}— we can't transform a value we won't know until the SQL evaluates. - Otherwise, run the GuardedStruct pipeline on the combined
attributes+atomicsliterals and return{:atomic, sanitized_map}for Ash to substitute into the single-statement UPDATE.
Bulk-action entry. Maps change/3 over each changeset.
The Ash.Resource.Change callback for the non-atomic / regular-change
path. Runs the GuardedStruct pipeline on changeset.attributes and
applies the transformed values back, or adds errors.