spacebat

spacebat

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.

Showing Posts 14 to 5

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:

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

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.

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?

zachdaniel

zachdaniel

Creator of Ash

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
spacebat

spacebat OP

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?

zachdaniel

zachdaniel

Creator of Ash

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
spacebat

spacebat OP

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
zachdaniel

zachdaniel

Creator of Ash

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

spacebat

spacebat OP

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.29.3

Where Next? Top

Trending in Questions Top

water
I’m aware of 2 such projects, are there others? git: GitHub - team-alembic/realworld: A fullstack Phoenix LiveView application with back...
New
katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
peck
Hi everyone. My team and I have been working on a fairly modest app based around video streaming and chat, but we’ve landed a customer t...
New
bradley
I really like the adapter patterns that ecto, nebulex, waffle, etc. use and would love find something similar for a key management servic...
New
unaware8150
Hello folks! So at work, we are seeing some situations where we have to define some “fixed” strings that are used across the codebase in...
New
Alvinkariuki
How Can I Optimise Compile Time Dependencies I have been building an elixir application for about 2 years now. Many modules and files ha...
New
leoschweizer
Hi, we run different LLM models across a cluster, with each node loading a configurable subset. Model-specific queues let nodes consume ...
New

Other Trending Topics Top

budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
budgie
I love Elixir. It’s one of 2 programming languages I’ve ever fallen in love with. But I don’t use it anymore. Serverless was the promis...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews