spacebat

spacebat OP

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?

0
Post #11
zachdaniel

zachdaniel

Creator of Ash

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.

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.

0
Post #12
zachdaniel

zachdaniel

Creator of Ash

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

0
Post #13
spacebat

spacebat OP

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
Post #14
— All posts loaded —