Hey hey,
I’ve just been trying to implement something that I had thought reasonable but am getting errors back and figured I’d see if it’s expected behavior or if something’s amiss.
I have a few resources as part of a ledger that have the following relevant actions:
# transaction.ex
create :create_with_entries do
argument :entries, {:array, :map} do
allow_nil? false
constraints min_length: 1
end
change set_attribute(:id, &Ash.UUIDv7.generate/0)
change set_attribute(:time, &DateTime.utc_now/0)
change fn changeset, _ ->
entries_arg = Ash.Changeset.get_argument(changeset, :entries)
transaction_id = Ash.Changeset.get_attribute(changeset, :id)
transaction_time = Ash.Changeset.get_attribute(changeset, :time)
entries =
entries_arg
|> Enum.map(fn entry ->
entry
|> Map.put(:transaction_id, transaction_id)
|> Map.put(:time, transaction_time)
end)
changeset
|> Ash.Changeset.manage_relationship(:entries, entries,
on_no_match: {:create, :create_and_update_account_balance}
)
end
# This for some reason wraps the action in a transaction that it otherwise wouldn't
change after_action(fn _changeset, result, _context ->
{:ok, result}
end)
end
# entry.ex
create :create_and_update_account_balance do
accept [:transaction_id, :account_id, :time, :amount]
change fn changeset, _context ->
changeset
|> Ash.Changeset.manage_relationship(
:account,
%{
id: Ash.Changeset.get_attribute(changeset, :account_id),
amount: Ash.Changeset.get_attribute(changeset, :amount)
},
on_match: {:update, :update_balance},
on_no_match: :error
)
end
end
# account.ex
update :update_balance do
argument :amount, :decimal do
allow_nil? false
end
change atomic_update(
:balance,
expr(
if ^arg(:amount) > 0 do
balance + ^arg(:amount)
else
if balance >= -(^arg(:amount)) do
balance + ^arg(:amount)
else
error(
Ash.Error.Changes.InvalidArgument,
%{
field: :amount,
message: "Insufficient balance"
}
)
end
end
)
)
end
On running the following command I get an error:
# iex
Transaction
|> Ash.Changeset.for_create(:create_with_entries, %{entries: entries})
|> Ash.create!()
* Invalid value provided for account: changes would create a new related record.
I can bypass this issue by instead using an after_action:
# entry.ex
create :create_and_update_account_balance do
accept [:transaction_id, :account_id, :time, :amount]
change after_action(fn _changeset, result, _context ->
account =
%Account{id: result.account_id}
|> Ash.Changeset.for_update(:update_balance, %{amount: result.amount})
|> Ash.update!()
result = Map.put(result, :account, account)
{:ok, result}
end)
end
# iex
Transaction
|> Ash.Changeset.for_create(:create_with_entries, %{entries: entries})
|> Ash.create!()
[debug] QUERY OK source="transactions" db=1.7ms
[debug] QUERY OK source="entries" db=4.5ms
[debug] QUERY OK source="accounts" db=12.9ms
Also, in doing some digging around I was able to get the manage_relationship to work via an update action but this led to additional queries and doesn’t solve the issue:
# entry
update :update_and_update_account_balance do
require_atomic? false
argument :account, :map, allow_nil?: false
change manage_relationship(:account, :account,
on_match: {:update, :update_balance},
on_no_match: :error
)
end
# iex
entry
|> Ash.Changeset.for_update(:update_and_update_account_balance, %{account: %{id: entry.account_id, amount: 1}})
|> Ash.update!
[debug] QUERY OK source="entries" db=8.0ms
[debug] QUERY OK source="accounts" db=3.3ms
[debug] QUERY OK source="entries" db=1.5ms
[debug] QUERY OK source="accounts" db=0.5ms
[debug] QUERY OK source="accounts" db=1.4ms
Few questions from this:
- Is it at all possible to update a managed relationship via a create action?
- Even if possible, is there a different way of doing it that’s preferred, e.g. via an after_action?
- [side] why is the :create_with_entries action not being run in a transaction unless providing an after_action
Note: transactions and entries are timescaledb hypertables and entries has no primary key but I added one and it didn’t fix anything (except for enabling the update action)