Changes not being called in embedded resource in Ash 2

Sorry that this is not an Ash 3 issue… I just didn’t had the time yet to port this project to it.

I have this embedded resource:

defmodule CoreWeb.Trash.FeedbackLive.UnderwritingFeedback.FirstStep.Resource do
  @moduledoc false

  alias CoreWeb.Trash.FeedbackLive.UnderwritingFeedback.FirstStep.Resource

  use Ash.Resource,
    data_layer: :embedded

  attributes do
    alias Core.Ash.Types.Currency

    attribute :flip_arv, Currency, allow_nil?: false
    attribute :flip_repair_estimate, Currency, allow_nil?: false
    attribute :rental_repair_estimate, Currency, allow_nil?: false
    attribute :monthly_rent, Currency, allow_nil?: false

    attribute :annual_rent, Currency, allow_nil?: false
    attribute :mao, Currency, allow_nil?: false
  end

  actions do
    alias Resource.Changes

    create :create do
      primary? true

      accept [:flip_arv, :flip_repair_estimate, :rental_repair_estimate, :monthly_rent]

      argument :config, :map, allow_nil?: false

      change Changes.CalculateMAO
    end
  end
end

And this is the change:

defmodule CoreWeb.Trash.FeedbackLive.UnderwritingFeedback.FirstStep.Resource.Changes.CalculateMAO do
  @moduledoc false

  use Ash.Resource.Change

  @impl true
  def change(changeset, _, _), do: Ash.Changeset.before_action(changeset, &do_change/1, append?: true)

  defp do_change(changeset) do
    dbg(changeset)

    config = Ash.Changeset.get_argument(changeset, :config)

    flip_arv = Ash.Changeset.get_attribute(changeset, :flip_arv)
    yearly_rent = Ash.Changeset.get_attribute(changeset, :yearly_rent) |> dbg()

    Ash.Changeset.change_attribute(changeset, :mao, Money.new(12312))
  end
end

I use this resource as an “intermediary” step in a multi-step form.

When I try to run AshPhoenix.Form.validate on it, it will run the action but not the changes. Why is that? Is this by design? I would expect that it would run all changes during a validate call.

:thinking: it should be running action changes. Can you see if you can reproduce in an example project?

Here is a minimal example reproducing the issue:

Mix.install([{:ash, "~> 2.20.3"}, {:ash_phoenix, "~> 1.2.26"}])

defmodule Resource.Changes.PrintMessage do
  use Ash.Resource.Change

  @impl true
  def change(changeset, _, _),
    do: Ash.Changeset.before_action(changeset, &do_change/1, append?: true)

  defp do_change(changeset) do
    IO.puts("GOT HERE!!!")
    
    changeset
  end
end

defmodule Resource do
  use Ash.Resource,
    data_layer: :embedded

  attributes do
    attribute :attribute, :string
  end

  actions do
    create :create do
      primary? true
      accept [:attribute]
      change Resource.Changes.PrintMessage
    end
  end
end

defmodule Api do
  use Ash.Api, validate_config_inclusion?: false
end

form = AshPhoenix.Form.for_create(Resource, :create, api: Api)

params = %{attribute: ""}

# This call will not run the change
form = AshPhoenix.Form.validate(form, params)

# This call will run the change
AshPhoenix.Form.submit(form)

If you don’t mind, I have another question. I need to set the , api: Api so the AshPhoenix.Form.submit/1 will work, but, as I mentioned before, the idea of this resource is only to be used in a form of a liveview for validation, it is not used anywhere else (the same way you can do with Ecto schemas).

This means that I’m not sure which api/domain I should use for it, personally I think it would make sense to not have any api/domain in this case, but that clearly is not supported (AFAIK), so, what would be a good workaround in this case? Maybe have a “global” api/domain that is only used in this cases?

Btw, I also tested it with Ash 3 and the behavior is the same as 2:

Mix.install([:ash, :ash_phoenix])

defmodule Resource.Changes.PrintMessage do
  use Ash.Resource.Change

  @impl true
  def change(changeset, _, _),
    do: Ash.Changeset.before_action(changeset, &do_change/1, append?: true)

  defp do_change(changeset) do
    IO.puts("GOT HERE!!!")
    
    changeset
  end
end

defmodule Resource do
  use Ash.Resource,
    data_layer: :embedded

  attributes do
    attribute :attribute, :string
  end

  actions do
    defaults []
    
    create :create do
      primary? true
      accept [:attribute]
      change Resource.Changes.PrintMessage
    end
  end
end

defmodule Domain do
  use Ash.Domain, validate_config_inclusion?: false
end

form = AshPhoenix.Form.for_create(Resource, :create, domain: Domain)

params = %{attribute: ""}

# This call will not run the change
form = AshPhoenix.Form.validate(form, params)

# This call will run the change
AshPhoenix.Form.submit(form)

Ah, okay, I think this is just a misunderstanding. before_action hooks do not run on validate. Their purpose is to add a hook that will be called on submit. If you want to do something on every call to validate, you put it in the body of the change/3 function, not inside of a before_action hook.