AF
Ash Framework•5w ago
dmc

Ash Update Many with Different Values

Hi there! I'm a bit new to Ash so apologies if I am missing something, but I couldn't find this use case in Discord or Elixir Forum. Let's say I have a Todo List with many Items. The items are ordered 1..n and I have to maintain the order when any items are updated or deleted and the user can control the order. For example,
%Item{id: "A", order: 1}
%Item{id: "B", order: 2}
%Item{id: "C", order: 3}
%Item{id: "A", order: 1}
%Item{id: "B", order: 2}
%Item{id: "C", order: 3}
and the user deletes the item with id: "B", the result should be
%Item{id: "A", order: 1}
%Item{id: "C", order: 2}
%Item{id: "A", order: 1}
%Item{id: "C", order: 2}
1. Can I make the update part of it atomic and is it worth it? 2. How do folks typically handle auth in nested cases? This works, but at what cost??!
defmodule Item do
actions do
defaults [:read, :destroy]

update :update_order do
accept [:order]
end
end
end

defmodule List do
actions do
update :destroy_item_and_update_item_orders do
require_atomic? false

argument :item, :map, allow_nil?: false, description: "The item to destroy"
argument :items, {:array, :map}, allow_nil?: false

change before_action(fn changeset, context ->
Ash.Changeset.get_argument(changeset, :item)
|> Ash.Changeset.for_destroy(:destroy)
|> Ash.destroy!(authorize?: false)

changeset
end)

change before_action(fn changeset, context ->
destroyed_item = Ash.Changeset.get_argument(changeset, :item)

updated_items =
Ash.Changeset.get_argument(changeset, :items)
|> Enum.reject(&(&1.id == destroyed_item.id))
|> Enum.sort_by(& &1.order)
|> Enum.with_index(1)
|> Enum.map(fn {item, index} -> %{item | order: index} end)

Ash.Changeset.manage_relationship(changeset, :items, updated_items,
on_match: {:update, :update_order}
)
end)
end
end
end
defmodule Item do
actions do
defaults [:read, :destroy]

update :update_order do
accept [:order]
end
end
end

defmodule List do
actions do
update :destroy_item_and_update_item_orders do
require_atomic? false

argument :item, :map, allow_nil?: false, description: "The item to destroy"
argument :items, {:array, :map}, allow_nil?: false

change before_action(fn changeset, context ->
Ash.Changeset.get_argument(changeset, :item)
|> Ash.Changeset.for_destroy(:destroy)
|> Ash.destroy!(authorize?: false)

changeset
end)

change before_action(fn changeset, context ->
destroyed_item = Ash.Changeset.get_argument(changeset, :item)

updated_items =
Ash.Changeset.get_argument(changeset, :items)
|> Enum.reject(&(&1.id == destroyed_item.id))
|> Enum.sort_by(& &1.order)
|> Enum.with_index(1)
|> Enum.map(fn {item, index} -> %{item | order: index} end)

Ash.Changeset.manage_relationship(changeset, :items, updated_items,
on_match: {:update, :update_order}
)
end)
end
end
end
Thanks in advance!
12 Replies
franckstifler
franckstifler•5w ago
you can use https://hexdocs.pm/ash/Ash.Changeset.html#manage_relationship/4 and add the order_is_key option which is found in the documentation. Your action should look something like:
update :update_todo do
require_atomic? false
argument :items, {:array, :map}, allow_nil?: false

change manage_relationship(:items, :items, type: :direct_control, order_is_key: :order)
end
update :update_todo do
require_atomic? false
argument :items, {:array, :map}, allow_nil?: false

change manage_relationship(:items, :items, type: :direct_control, order_is_key: :order)
end
This should replace all the boilerplate you have. The type is important, and you can read more about it here: https://hexdocs.pm/ash/Ash.Changeset.html#manage_relationship/4-options How for your questions: 1. It cannot be atomic when you manage relationships with Ash. I don't think you'll have any issues with it not being atomic. 2. I'm not sure of the question here, but you should have the actor and tenant(if any) in the context and the changeset of your callbacks (before_action, after_action, before_transaction, after_transaction...)
dmc
dmcOP•5w ago
oooh wow, that's nice, I just need to make sure to pass ALL the items (or ALL sans the one I want to delete) when using direct_control OK. The question about the nested auth stuff is more around convention. If I pass both actor and authorize? from the context then will i still be able to test these actions in IEx with authorize?: false? thank you so much for your help!
barnabasj
barnabasj•5w ago
best practice I'd say is to pass the context as scope to nested actions https://hexdocs.pm/ash/3.5.20/Ash.Scope.html that forwards things like actor, tenant, authorize? and more
franckstifler
franckstifler•5w ago
I sometimes regret why the scope did not exist before. Passing actor and tenant was a laborious task! socket.assigns.tenant, socket.assigns.current_user 😞 @barnabasj
dmc
dmcOP•4w ago
hmm, not sure why this isn't working. It deletes the expected resource, but does not update the number attribute, as specified by order_is_key. The changeset looks correct, that is, I see the updated values for number, but the update isn't run in the database. I'm using Postgres and AshPostgres. I don't have a relationship on number, only an identity. Example below:
def change(changeset, _opts, context) do
design_id = Ash.Changeset.get_attribute(changeset, :id)
model_id = Ash.Changeset.get_argument(changeset, :model_id)

models =
Studies.Model
|> Ash.Query.filter(design_id: design_id)
|> Ash.Query.filter(id != ^model_id)
|> Ash.read!(actor: context.actor, authorize?: context.authorize?)

Ash.Changeset.manage_relationship(
changeset,
:models,
models,
order_is_key: :number,
on_match: {:update, :update_number},
on_missing: :destroy
)
end
def change(changeset, _opts, context) do
design_id = Ash.Changeset.get_attribute(changeset, :id)
model_id = Ash.Changeset.get_argument(changeset, :model_id)

models =
Studies.Model
|> Ash.Query.filter(design_id: design_id)
|> Ash.Query.filter(id != ^model_id)
|> Ash.read!(actor: context.actor, authorize?: context.authorize?)

Ash.Changeset.manage_relationship(
changeset,
:models,
models,
order_is_key: :number,
on_match: {:update, :update_number},
on_missing: :destroy
)
end
where the following action exists on the destination resource Model
update :update_number do
accept [:number]
end
update :update_number do
accept [:number]
end
I tried adding
on_no_match: :error,
on_lookup: :error,
authorize?: false
on_no_match: :error,
on_lookup: :error,
authorize?: false
to manage_relationship to see if it changes anything, but it does not. @franckstifler any thoughts as to why the update doesn't happen?
franckstifler
franckstifler•4w ago
Hi @dmc is your current question related to the one on your first post? I have difficulties understanding what's your current challenge. What does your log show as error?
barnabasj
barnabasj•4w ago
There is a debug option for manage_relationship, and there is a not about using order_by_key with direct_control, but I don't think that explains your problems. but if you set debug and can let us know exactly what it's doing we should be able to help out/fix it
dmc
dmcOP•4w ago
Sorry for being confusing, just the most recent code snippet is relevant. I’ll add ‘debug’ to see what I can learn. I don’t get an error, the update just doesn’t happen, the delete does.
franckstifler
franckstifler•4w ago
Is your record updated? what does the code of your action looks like (where you call the change module)?
dmc
dmcOP•3w ago
hmm, i've refactored a bit and it still doesn't happen. The data that is returned from executing the action includes the remaining items of the relationship with the value from the order_is_key attribute updated correctly, but when I load them from the database this is not the case. I can see from the logs that the DELETE occurs, but there is no UPDATE. I have tried the action with and without the relationship loaded on the source resource and that does not make a difference. Since the DELETE occurs, it must be comparing the input to the relationship as it exists in the database, and since it doesn't delete the other items it must be finding them correctly. The update action accepts the order_is_key attribute so that shouldn't be an issue. Does it matter that the logic is defined as a before_action ? I'm not sure this matters in this case. Does it matter if no attributes are changed on the source resource? Full example included below
update :destroy_model do
require_atomic? false

argument :id, :string, allow_nil?: false, description: "The id of model to destroy"

change before_action(fn changeset, context ->
models =
changeset
|> Ash.Changeset.get_attribute(:id)
|> Studies.Model.read_for_design!(
actor: context.actor,
authorize?: context.authorize?
)
|> Enum.reject(&(&1.id == changeset.arguments.id))

Ash.Changeset.manage_relationship(changeset, :models, models,
order_is_key: :number,
on_match: {:update, :update_number},
on_missing: :destroy
)
end)
end
update :destroy_model do
require_atomic? false

argument :id, :string, allow_nil?: false, description: "The id of model to destroy"

change before_action(fn changeset, context ->
models =
changeset
|> Ash.Changeset.get_attribute(:id)
|> Studies.Model.read_for_design!(
actor: context.actor,
authorize?: context.authorize?
)
|> Enum.reject(&(&1.id == changeset.arguments.id))

Ash.Changeset.manage_relationship(changeset, :models, models,
order_is_key: :number,
on_match: {:update, :update_number},
on_missing: :destroy
)
end)
end
update :update_number do
accept [:number]
end
update :update_number do
accept [:number]
end
debug doesn't really tell me anything I already don't know. here is the full log, been getting these warnings since updating last week.
dmc
dmcOP•3w ago
dmc
dmcOP•3w ago
thank you so much for your help

Did you find this page helpful?