RodolfoSilva
How can I compute an attribute’s value based on an argument without set `require_atomic?` to false?
For instance, certain attributes are stored in the database as utc_datetime, and I need to assign their values conditionally based on a boolean argument.
defmodule MyApp.AshDomains.Accounts.Preferences do
use Ash.Resource,
domain: MyApp.AshDomains.Accounts,
data_layer: AshPostgres.DataLayer,
extensions: [AshArchival.Resource]
alias Ash.Changeset
postgres do
table "account_preferences"
repo MyApp.Repo
end
archive do
attribute :deleted_at
end
changes do
change fn changeset, _context ->
notification_enabled_at = Changeset.get_attribute(changeset, :notification_enabled_at)
notification_enabled = Changeset.get_argument(changeset, :notification_enabled)
cond do
is_nil(notification_enabled_at) and notification_enabled == true ->
Changeset.force_change_attribute(changeset, :notification_enabled_at, expr(now()))
not is_nil(notification_enabled_at) and notification_enabled == false ->
Changeset.force_change_attribute(changeset, :notification_enabled_at, nil)
true ->
changeset
end
end
end
actions do
defaults [:read, :destroy]
create :create do
argument :notification_enabled, :boolean, allow_nil?: false
end
update :update do
argument :notification_enabled, :boolean, allow_nil?: false
end
end
attributes do
uuid_v7_primary_key :id
attribute :notification_enabled_at, :utc_datetime
end
calculations do
calculate :notification_enabled, :boolean, expr(not is_nil(notification_enabled_at))
end
end
When I try to set the attribute value, I get an error about implementing atomic/3, but I don’t think it should be atomic, it’s just a derived value.
[warning] Unhandled error in form submission for Salao365.AshDomains.Appointments.Product.update
This error was unhandled because Ash.Error.Framework.MustBeAtomic does not implement the
AshPhoenix.FormData.Errorprotocol.** (Ash.Error.Framework.MustBeAtomic) Salao365.AshDomains.Appointments.Product.update must be performed atomically, but it could not be
Reason: Ash.Resource.Change.Function does not implement
atomic/3See Update Actions — ash v3.29.3 for more on atomics.
@zachdaniel do you have any suggestions?
I was expecting something like this:
change set_new_attribute(:notification_enabled_at, expr(if arg(:notification_enabled) == true, do: now(), else: nil))
Marked As Solved
zachdaniel
Three things:
- a misunderstanding to clear up
- Generally “how to make a change that works atomically”
- How I’d actually do this one
Misunderstanding: When can you use expressions to change attributes
There is only one place you can change attributes using expressions, and that is in atomic_update statements via Ash.Changeset.atomic_update or change atomic_update. This is only supported for update actions.
For example:
change atomic_update(:notification_enabled_at, expr(now())
Making changes safe to do atomically
The reason function changes like that can’t be done atomically is because it has to be explicitly annotated as supporting atomic behavior by defining the atomic/3 callback. You can actually implement a change as a module, and define the atomic/3 callback. If you want this logic to support both create and update actions, and be abstracted into a single module, this is the approach to use.
For example:
defmodule YourApp.YourDomain.YourResource.Changes.SetEnabledAt do
use Ash.Resource.Change
def change(%{action_type: :create} = changeset, _, _) do
Ash.Changeset.force_change_attribute(changeset, :notification_enabled_at, DateTime.utc_now())
end
def change(changeset, _, _) do
Ash.Changeset.atomic_update(changeset, :notification_enabled_at,
expr(fragment("LEAST(?, ?)", ^atomic_ref(:notification_enabled_at), now()))
)
end
# define the `atomic/3` callback. The behavior is the same as `change`, so no special logic needed.
def atomic(changeset, opts, context) do
{:ok, change(changeset, opts, context)}
end
end
How I’d probably do it
changes do
change set_attribute(:notification_enabled_at, &DateTime.utc_now/0),
where: argument_equals(:notification_enabled, true),
on: :create
change atomic_update(:notification_enabled_at, expr(
fragment("LEAST(?, ?)", ^atomic_ref(:notification_enabled_at), now())
) ,
where: argument_equals(:notification_enabled, true),
on: :update
end
Also Liked
zachdaniel
It’s for all changes and validations. You can use validations in the where clause to make other changes/validations conditional.
Last Post!
FlyingNoodle
Popular in Questions
Other popular topics
Latest Ash Threads
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #hex
- #security









