Best fix for "Changeset has already been validated for action" warning

I recently updated from an early Ash 3.x to the most recent, and find at runtime a lot of warnings like:

Changeset has already been validated for action :create.

For safety, we prevent any changes after that point because they will bypass validations or other action logic.. To proceed anyway, you can use `force_change_attribute/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.change_attribute(...)
  |> Ash.Changeset.for_create(...)

So far in my application I have a lot of actions that use Ash.Changeset.change_attribute/3 within change before_action(fn ....) blocks. This is usually to support computed defaults that depend on other attributes/arguments so that the call sites for the actions are nice and clean.

The simplest thing is to change all these occurrences to Ash.Changeset.force_change_attribute/3 but the warning advises against it.

I could write wrapper functions for all these actions that compute the defaults before calling the action, as advised by the warning, but then the actions themselves become far less convenient to call directly, and the functions will be located elsewhere in the module, not right next to the action they are related to.

Maybe wrapper actions would be better, so have a create :raw_create that defaults nothing, and create :create would accept no attributes, only arguments, compute the defaults and call the raw_create action, but that feels verbose and undesirable.

Validations could be removed from the attributes and applied explicitly within each action, but that would lose a lot of the attribute metadata and be more repetitive and error prone.

I don’t see a change before_validation hook but if it existed that would be a simple fix.

It feels like I’ve been using Ash wrong but it’s unclear what the best approach is, perhaps I’m missing something obvious that I didn’t notice in the docs.

Do the before action hooks you’re using have side effects/do anything expensive? If not your best bet is to to just move the code into the body of the cnange.

Alternatively for actions that have to set values in before_action hooks you can set the validations to be done themselves in before action hooks with before_action?: true

Then they will happen after your logic to change attributes despite that logic being in a before_action hook :slight_smile:

No, at worst they might lazy load something, for the most part it’s things like the run_mode attribute depends on the type of another argument.

Right, so in that case there is likely no need for a before_action hook at all.

i.e

def change(changeset, _, _) do
  Ash.Changeset.before_action(changeset, fn changeset, _ -> 
    if changeset.arguments.whatever == "foo" do
      Ash.Changeset.change_attribute(changeset, :is_foo, true)
    else
      changeset
    end
  end)
end

can be

def change(changeset, _, _) do
  if changeset.arguments[:whatever] == "foo" do
    Ash.Changeset.change_attribute(changeset, :is_foo, true)
  else
    changeset
  end
end

The major difference you may notice is that the body of changes runs on both valid and invalid change sets, so you may want to add change YourChange, only_when_valid?: true, or an if changeset.valid? in your change implementation, or simply handle the fact that the changes may be invalid (so don’t rely on something like changeset.arguments.whatever, instead use Ash.Changeset.get_argument(changeset, :whatever), and be aware that it can be invalid.

This is good though as it allows knowing up front an action will fail and producing multiple errors at a time.

I guess that only works with custom actions each in their own module? At the moment they are all just create and update actions within the resource modules.

I noticed that the example for a custom action uses force_change_attribute anyway: Changes — ash v3.4.52

I’m not sure what you mean. Do you mean that the changes are all in anonymous functions? You can put them in change modules that use Ash.Resource.Change

All my actions are of the form:

actions do
  create :create do
    accept [:foo]
    argument :whatever, :integer
    change before_action(fn changeset, _ ->
      if Ash.Changeset.get_argument(changeset, :whatever) > 0 do
        Ash.Changeset.change_attribute(changeset, :foo, "positive")
      else
        changeset  
      end  
    end)
  end
end

To keep that form

actions do
  create :create do
    accept [:foo]
    argument :whatever, :integer
    change fn changeset, _ ->
      if Ash.Changeset.get_argument(changeset, :whatever) > 0 do
        Ash.Changeset.change_attribute(changeset, :foo, "positive")
      else
        changeset  
      end  
    end
  end
end

That’s a bit confusing - the warning is about calling change_attribute in a change before_action, which is post validation. Isn’t change even more post validation?

Nope. Change happens first. It’s the very first thing that happens. It happens on every valid

The general formula looks like this:

defmodule AChange do
  use Ash.Resource.Change


  def change(changeset, opts, context) do
    changeset
    |> Ash.Changeset.before_action(fn changeset ->
      # does something before the action is invoked
    end)
    |> Ash.Changeset.after_action(fn changeset, result ->
      # does something after
    end)
    |> …other hooks or modifications
  end
end

Anything that you do in the change function happens at the very beginning after basic input validation, when you run functions like Ash.Changeset.for_create(…).

change before_action(function) is just a built in shorthand that looks like this

defmodule BeforeAction do
  use Ash.Resource.Change

  def change(changeset, opts, context) do
    Ash.Changeset.before_action(changeset, fn changeset ->
      opts[:function].(changeset, context)
    end)
  end
end

OK, so change is early on but still post “basic input validation”. Is that different from the validation that the warning was about? Is there somewhere the full sequence of events is documented?

We should enhance the docs here to talk about the way that changes/validations are executed. It’s documented in the changeset functions that run that logic. We should probably but it in module docs or a guide somewhere.

https://hexdocs.pm/ash/Ash.Changeset.html#for_create/4-what-does-this-function-do

When I said “basic input validation” I mean that we handle the types of each attribute/argument and validate them before setting them. So in the below example, you won’t ever see foo as 10. But the changes still run on an invalid changeset.

Each change and validation is run in the order they appear. When we say that “ypass validations or other action logic”, we’re referring to validations in the action.

Here is an example:

create :create do
  argument :foo, :string, allow_nil?: false

  change fn changeset, _ -> 
    if changeset.arguments[:foo] == "fred" do
      Ash.Changeset.change_attribute(changeset, :is_fred, true)
    else
      changeset
    end
  end

  validate attribute_does_not_equal(:is_fred, true)
end

With the above construction, if you passed in %{foo: "fred"} to the function, it would set is_fred to true, and the validation would fail, because the validation comes after the change.

create :create do
  argument :foo, :string, allow_nil?: false

  change fn changeset, _ ->
    Ash.Changeset.before_action(changeset, fn changeset -> 
      if changeset.arguments[:foo] == "fred" do
        Ash.Changeset.change_attribute(changeset, :is_fred, true)
      else
        changeset
      end
    end)
  end

  validate attribute_does_not_equal(:is_fred, true)
end

With this construction, the order of events looks like this:

  1. validate arguments
  2. run the change, which adds a hook (to be executed later)
  3. run the validation, which doesn’t see the attribute change (because its in a hook to be executed later)
  4. return the changeset

So all of the above happens when you do

Ash.Changeset.for_action(Resource, :create, %{foo: "foo"})

In the second example you get back a changeset with a hook on it.

Then it can be passed to Ash.create to actually run the action, which runs any associated hooks.

When using things like AshGraphql or AshJsonApi or code interfaces etc. this is kind of transparent to the caller because it both builds a changeset and runs it on the spot. But Ash actions are actually two phase things, with the idea that phase 1 is cheap and repeatable. If using AshPhoenix.Form for example, each change runs on every keypress because we build the changeset each time.

There are also docs in each action showing what the action does w/ the changeset given to it: Create Actions — ash v3.4.52

That’s a lot to chew on, thanks I’m sure re-reading this in the morning will be fruitful. I can’t wait for the Ash book :slight_smile:

1 Like