GuardedStruct.AshResource.Change (GuardedStruct v0.1.0-beta.1)

Copy Markdown View Source

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
end

Auto-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

after_action?()

after_batch?()

around_action?()

atomic(changeset, opts, context)

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 + atomics literals and return {:atomic, sanitized_map} for Ash to substitute into the single-statement UPDATE.

atomic?()

batch_change(changesets, opts, context)

Bulk-action entry. Maps change/3 over each changeset.

batch_change?()

before_action?()

before_batch?()

change(changeset, opts, context)

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.

has_after_action?()

has_after_batch?()

has_around_action?()

has_atomic?()

has_batch_change?()

has_before_action?()

has_before_batch?()

has_change?()

has_init?()

has_validate?()

validate?()