Proxying actions to other actions

I've found it a useful pattern to do some transformations that then proxy the remainder of the work to another action—often the primary action of its type. The way I'm doing that is roughly:
actions do
create :upstream do
change fn cs, ctx ->
# do some upstream stuff
Session
|> Changeset.new
|> Changeset.for_action(:create, %{param: value}, Map.to_list ctx)
end
end
end
actions do
create :upstream do
change fn cs, ctx ->
# do some upstream stuff
Session
|> Changeset.new
|> Changeset.for_action(:create, %{param: value}, Map.to_list ctx)
end
end
end
I've started getting a lot of the following:
warning: Changeset has already been validated for action :upstream.

In the future, this will become an error.

For safety, we prevent any changes after that point because they will bypass validations or other action logic.. To proceed anyway,
you can use `set_argument/3`. However, you should prefer a pattern like the below, which makes
any custom changes *before* calling the action.

Resource
|> Ash.Changeset.new()
|> Ash.Changeset.set_argument(...)
|> Ash.Changeset.for_create(...)
warning: Changeset has already been validated for action :upstream.

In the future, this will become an error.

For safety, we prevent any changes after that point because they will bypass validations or other action logic.. To proceed anyway,
you can use `set_argument/3`. However, you should prefer a pattern like the below, which makes
any custom changes *before* calling the action.

Resource
|> Ash.Changeset.new()
|> Ash.Changeset.set_argument(...)
|> Ash.Changeset.for_create(...)
Is there a preferred way to execute this pattern? It does seem like I'm doing something quite like what the message recommends.
37 Replies
ZachDaniel
ZachDaniel3y ago
So the problem is that you're returning a changeset for the wrong action What you want is a manual action
manual fn changeset, _ ->
__MODULE__
|> Changeset.for_action(:create, changeset.attributes)
|> Api.create()

end
manual fn changeset, _ ->
__MODULE__
|> Changeset.for_action(:create, changeset.attributes)
|> Api.create()

end
would be one way to go about it There is generally no need to use Changeset.new
\ ឵឵឵
\ ឵឵឵OP3y ago
Tbh I just added that to check if it would silence the warning 😂 Ok, so it would be better to use a manual for this, that's fair. I do recall thinking it seemed a little suspect to return a changeset for a different action 🙂 Would this still need Map.to_list ctx to thread through the calling context, or is that happening somewhere else?
ZachDaniel
ZachDaniel3y ago
Yeah, you would still need that kind of thing
\ ឵឵឵
\ ឵឵឵OP3y ago
Right on, cheers!
ZachDaniel
ZachDaniel3y ago
Might need to make a helper for doing that kind of thing because if we ever add a key to the context that isn't a valid option to new changesets, it would break that code
\ ឵឵឵
\ ឵឵឵OP3y ago
Thanks for the warning, I'll start wrapping them up from now on.
ZachDaniel
ZachDaniel3y ago
Yeah, maybe do something like Map.take(...) |> Map.to_list() and take the keys you know about
\ ឵឵឵
\ ឵឵឵OP3y ago
Actually I was experimenting with several options for this at one point; one of the reasons I ended up on this was because if I do it the other way, I'm getting:
[warning] Missed 1 notifications in action App.Post.create_special.

This happens when the resources are in a transaction, and you did not pass
`return_notifications?: true`. If you are in a changeset hook, you can
return the notifications. If not, you can send the notifications using
`Ash.Notifier.notify/1` once your resources are out of a transaction.
[warning] Missed 1 notifications in action App.Post.create_special.

This happens when the resources are in a transaction, and you did not pass
`return_notifications?: true`. If you are in a changeset hook, you can
return the notifications. If not, you can send the notifications using
`Ash.Notifier.notify/1` once your resources are out of a transaction.
ZachDaniel
ZachDaniel3y ago
Yep, so if you add return_notifications?: true option, it will return them alongside the actual result lemme check something real quick Yeah so you can return {:ok, result, %{notifications: notifications}} from manual create So you'd do something like:
case Api.create(changeset, Keyword.put(..., :return_notifications?, true)) do
{:ok, result, notifications} -> {:ok, result, %{notifications: notifications}}
{:error, error} -> {:error, error}
end
case Api.create(changeset, Keyword.put(..., :return_notifications?, true)) do
{:ok, result, notifications} -> {:ok, result, %{notifications: notifications}}
{:error, error} -> {:error, error}
end
I'm going to fix the need for that before too long, we can automatically gather up notifications Just need to do it 🙂 Might be a 3.0 thing
\ ឵឵឵
\ ឵឵឵OP3y ago
Nice! That would be great. I'm still getting the same warning, though, wondering if it might be somewhere else:
def nested_ctx(ctx) do
ctx
|> Map.take([:actor, :context])
|> Map.put(:return_notifications?, true)
|> Map.to_list
end
def nested_ctx(ctx) do
ctx
|> Map.take([:actor, :context])
|> Map.put(:return_notifications?, true)
|> Map.to_list
end
case Post.create(%{...}, nested_ctx ctx) do
{:ok, result, notes} ->
{:ok, result, %{notifications: notes}}

err ->
err
end
case Post.create(%{...}, nested_ctx ctx) do
{:ok, result, notes} ->
{:ok, result, %{notifications: notes}}

err ->
err
end
Wait a sec Thought it might be coming from the error branch, but seems ok:
{:error, Ash.Error.to_ash_error("unable to create post")}
{:error, Ash.Error.to_ash_error("unable to create post")}
ZachDaniel
ZachDaniel3y ago
🤔 Does it give you a stacktrace in the warning?
\ ឵឵឵
\ ឵឵឵OP3y ago
Yep
(elixir 1.14.4) lib/process.ex:773: Process.info/2
(ash 2.6.31) lib/ash/actions/helpers.ex:220: Ash.Actions.Helpers.warn_missed!/3
(ash 2.6.31) lib/ash/actions/update.ex:175: Ash.Actions.Update.add_notifications/6
(ash 2.6.31) lib/ash/actions/update.ex:38: Ash.Actions.Update.run/4
(app 0.1.3) lib/app/api.ex:1: App.Api.update/2
(app 0.1.3) lib/app/resources/auth/session.ex:83: App.Session.manual_0_generated_247EFA27A05C93763A80C351587532F0/2
(ash 2.6.31) lib/ash/actions/create.ex:354: anonymous fn/8 in Ash.Actions.Create.as_requests/5
(ash 2.6.31) lib/ash/changeset/changeset.ex:1865: Ash.Changeset.run_around_actions/2
(ash 2.6.31) lib/ash/changeset/changeset.ex:1575: anonymous fn/2 in Ash.Changeset.with_hooks/3
(ecto_sql 3.10.1) lib/ecto/adapters/sql.ex:1203: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
(db_connection 2.5.0) lib/db_connection.ex:1630: DBConnection.run_transaction/4
(ash 2.6.31) lib/ash/changeset/changeset.ex:1573: anonymous fn/3 in Ash.Changeset.with_hooks/3
(ash 2.6.31) lib/ash/changeset/changeset.ex:1689: Ash.Changeset.transaction_hooks/2
(ash 2.6.31) lib/ash/actions/create.ex:319: anonymous fn/10 in Ash.Actions.Create.as_requests/5
(ash 2.6.31) lib/ash/engine/request.ex:1048: Ash.Engine.Request.do_try_resolve_local/4
(ash 2.6.31) lib/ash/engine/request.ex:282: Ash.Engine.Request.do_next/1
(ash 2.6.31) lib/ash/engine/request.ex:211: Ash.Engine.Request.next/1
(ash 2.6.31) lib/ash/engine/engine.ex:683: Ash.Engine.advance_request/2
(ash 2.6.31) lib/ash/engine/engine.ex:589: Ash.Engine.fully_advance_request/2
(ash 2.6.31) lib/ash/engine/engine.ex:530: Ash.Engine.do_run_iteration/2
(elixir 1.14.4) lib/process.ex:773: Process.info/2
(ash 2.6.31) lib/ash/actions/helpers.ex:220: Ash.Actions.Helpers.warn_missed!/3
(ash 2.6.31) lib/ash/actions/update.ex:175: Ash.Actions.Update.add_notifications/6
(ash 2.6.31) lib/ash/actions/update.ex:38: Ash.Actions.Update.run/4
(app 0.1.3) lib/app/api.ex:1: App.Api.update/2
(app 0.1.3) lib/app/resources/auth/session.ex:83: App.Session.manual_0_generated_247EFA27A05C93763A80C351587532F0/2
(ash 2.6.31) lib/ash/actions/create.ex:354: anonymous fn/8 in Ash.Actions.Create.as_requests/5
(ash 2.6.31) lib/ash/changeset/changeset.ex:1865: Ash.Changeset.run_around_actions/2
(ash 2.6.31) lib/ash/changeset/changeset.ex:1575: anonymous fn/2 in Ash.Changeset.with_hooks/3
(ecto_sql 3.10.1) lib/ecto/adapters/sql.ex:1203: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
(db_connection 2.5.0) lib/db_connection.ex:1630: DBConnection.run_transaction/4
(ash 2.6.31) lib/ash/changeset/changeset.ex:1573: anonymous fn/3 in Ash.Changeset.with_hooks/3
(ash 2.6.31) lib/ash/changeset/changeset.ex:1689: Ash.Changeset.transaction_hooks/2
(ash 2.6.31) lib/ash/actions/create.ex:319: anonymous fn/10 in Ash.Actions.Create.as_requests/5
(ash 2.6.31) lib/ash/engine/request.ex:1048: Ash.Engine.Request.do_try_resolve_local/4
(ash 2.6.31) lib/ash/engine/request.ex:282: Ash.Engine.Request.do_next/1
(ash 2.6.31) lib/ash/engine/request.ex:211: Ash.Engine.Request.next/1
(ash 2.6.31) lib/ash/engine/engine.ex:683: Ash.Engine.advance_request/2
(ash 2.6.31) lib/ash/engine/engine.ex:589: Ash.Engine.fully_advance_request/2
(ash 2.6.31) lib/ash/engine/engine.ex:530: Ash.Engine.do_run_iteration/2
ZachDaniel
ZachDaniel3y ago
I'll tell you hwat I'll just make it all unnecessary and make the internals gather up notifications magically 🙂
\ ឵឵឵
\ ឵឵឵OP3y ago
Works for me 😁 Thanks mate
ZachDaniel
ZachDaniel3y ago
can you try main? It should handle the notifications automatically now
\ ឵឵឵
\ ឵឵឵OP3y ago
Yep, just a minute
ZachDaniel
ZachDaniel3y ago
Don't forget to remove your code that does return_notifications?
\ ឵឵឵
\ ឵឵឵OP3y ago
Already gone Now I'm getting the warning twice :thinkies: (for different actions, though)
ZachDaniel
ZachDaniel3y ago
🤔 but you weren't getting it before for those actions?
\ ឵឵឵
\ ឵឵឵OP3y ago
The second warning? Nope. But it looks like everything is working otherwise.
ZachDaniel
ZachDaniel3y ago
Sorry, still a bit unclear. Did you used to get warnings from those actions at all? So was it 1 and now its 2? Or was it 0 and now its 2?
\ ឵឵឵
\ ឵឵឵OP3y ago
I wasn't getting notification warnings lately, since I had them as
Resource
|> Changeset.for_action(...)
Resource
|> Changeset.for_action(...)
During the thread I've switched one path over to using code interface rather than that, but before it was producing only one warning. The two warnings it creates are for the actions I'd expect the path to take, though, so good possibility the one warning was worse.
ZachDaniel
ZachDaniel3y ago
can you lay it all out? like what actions you're talking about, how you're invoking them? are you using return_notifications? anywhere?
\ ឵឵឵
\ ឵឵឵OP3y ago
I had only added it to the one path as well, and removed it.
def nested_ctx(ctx) do
ctx
|> Map.take([:authorize?, :actor, :context])
|> Map.to_list
end
def nested_ctx(ctx) do
ctx
|> Map.take([:authorize?, :actor, :context])
|> Map.to_list
end
This is the util for threading context. The manual actions are returning from the code interface, like:
Post.create(%{...}, nested_ctx ctx)
Post.create(%{...}, nested_ctx ctx)
Error conditions are being returned so:
{:error, Ash.Error.to_ash_error("unable to create post")}
{:error, Ash.Error.to_ash_error("unable to create post")}
The vast majority of the actions are still create, update, etc. and not manual as we spoke about above. (where possible)
ZachDaniel
ZachDaniel3y ago
I just pushed up another thing to main mix deps.update ash try updating it and see if that helps
\ ឵឵឵
\ ឵឵឵OP3y ago
Sure, 2 new commits?
ZachDaniel
ZachDaniel3y ago
yep
\ ឵឵឵
\ ឵឵឵OP3y ago
Same 2 The new warning is coming from this action:
manual fn cs, _ ->
a = Changeset.get_argument(cs, :a)
b = Changeset.get_attribute(cs, :b)
valid = verify_attributes(a, b)
case valid do
true -> Changeset.apply_attributes(cs)
false -> {:error, Ash.Error.to_error_class("unable to verify attributes", changeset: cs)}
end
end
manual fn cs, _ ->
a = Changeset.get_argument(cs, :a)
b = Changeset.get_attribute(cs, :b)
valid = verify_attributes(a, b)
case valid do
true -> Changeset.apply_attributes(cs)
false -> {:error, Ash.Error.to_error_class("unable to verify attributes", changeset: cs)}
end
end
(Which is called by the first action)
ZachDaniel
ZachDaniel3y ago
that doesn't make any sense 😆 that action isn't even doing anything its just returning a struct can I see how the first action is calling it?
\ ឵឵឵
\ ឵឵឵OP3y ago
Just a sec
ZachDaniel
ZachDaniel3y ago
I have an example in my tests that does the same thing essentially (calls a nested action from within an action) and its not showing that warning
\ ឵឵឵
\ ឵឵឵OP3y ago
with {:tag, {:ok, %Tag{category: %Category{} = category}}} <-
{:a, Tag.get([tag: tag], authorize?: false, load: [:category])},
{:cat, {:ok, %Category{parent: %Parent{} = parent}}} <-
{:b, Category.proxy(category, parent, authorize?: false)}
do
Post.create(%{}, nested_ctx ctx)
else
{:tag, _} ->
{:error, Ash.Error.to_ash_error("invalid tag")}

{:cat, _} ->
{:error, Ash.Error.to_ash_error("invalid category")}
end
with {:tag, {:ok, %Tag{category: %Category{} = category}}} <-
{:a, Tag.get([tag: tag], authorize?: false, load: [:category])},
{:cat, {:ok, %Category{parent: %Parent{} = parent}}} <-
{:b, Category.proxy(category, parent, authorize?: false)}
do
Post.create(%{}, nested_ctx ctx)
else
{:tag, _} ->
{:error, Ash.Error.to_ash_error("invalid tag")}

{:cat, _} ->
{:error, Ash.Error.to_ash_error("invalid category")}
end
That's the shape of it. The get is unsurprisingly fine, the action call issuing a warning is the second action in the with. This action is being called via code interface, and warnings are coming from both Post.create and Category.proxy. This action is not generating a warning Gonna try something No dice
ZachDaniel
ZachDaniel3y ago
Are you creating a transaction manually above that? like Repo.transaction?
\ ឵឵឵
\ ឵឵឵OP3y ago
Nope, just calling that last action via code interface.
ZachDaniel
ZachDaniel3y ago
And the warning points at that Post.create I'll have to take a look later then
\ ឵឵឵
\ ឵឵឵OP3y ago
Yep, Post.create and Category.proxy No worries 🙂
ZachDaniel
ZachDaniel3y ago
Found the problem 🙂 will fix it later tonight or tomorrow 🙂 Fixed in main 🙂

Did you find this page helpful?