carlgleisner

carlgleisner

AshAdmin and catching illegal state transitions with after_transaction?

I’ve been trying to set up a resource with AshStateMachine and testing it out in AshAdmin.

If anyone has any idea about these questions I would be most grateful.

  • Is the after_transaction change in the basic state machine example in the documentation functional?
  • How is AshAdmin supposed to handle errors in the case of illegal state changes anyway?

Regarding admin I merely se the name of the resource shift a little as the empty error message tags are patched in place.

Steps to reproduce

The code, without any Phoenix or admin however, is available in this repository.

Get dependencies and fire up an IEX session.

mix deps.get
iex -S mix

Create a ticket.

iex(1)> ticket = Statepoc.Support.Ticket |> Ash.Changeset.for_create(:create) |> Statepoc.Support.create!()
#Statepoc.Support.Ticket<
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "61868f6d-1dc0-42d5-92c4-01eee81d6d29",
  error: nil,
  error_state: nil,
  state: :pending,
  aggregates: %{},
  calculations: %{},
  ...
>

Now the Ticket resource has a change that will hook in to after_transaction/1 and pass through any successful updates but set the attributes :errorand :error_state if receiving changeset, {:error, error}.

Both paths call IO.inspect/2.

changes do
  change after_transaction(fn
            changeset, {:ok, result} ->
              IO.inspect("Got {:ok, result}", label: "after_transaction")
              {:ok, result}

            changeset, {:error, error} ->
              message = Exception.message(error)
              IO.inspect(message, label: "after_transaction")

              changeset.data
              |> Ash.Changeset.for_update(:error, %{
                error: message,
                error_state: changeset.data.state
              })
              |> Statepoc.Support.update()
          end),
          on: [:update]
end

Now trigger a legal state change and see the IO.inspect/2 output.

iex(2)> ticket = ticket |> Ash.Changeset.for_update(:confirm) |> Statepoc.Support.update!()
after_transaction: "Got {:ok, result}"
#Statepoc.Support.Ticket<
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "61868f6d-1dc0-42d5-92c4-01eee81d6d29",
  error: nil,
  error_state: nil,
  state: :confirmed,
  aggregates: %{},
  calculations: %{},
  ...
>

Great, it says Got {:ok, result} and the state attribute is updated.

Let’s trigger an illegal state change to spice things up.

iex(3)> ticket |> Ash.Changeset.for_update(:package_arrived) |> Statepoc.Support.update()
{:error,
 %Ash.Error.Invalid{
   errors: [
     %AshStateMachine.Errors.NoMatchingTransition{
       action: :package_arrived,
       target: :arrived,
       old_state: :confirmed,
       changeset: nil,
       query: nil,
       error_context: [],
       vars: [],
       path: [],
       stacktrace: #Stacktrace<>,
       class: :invalid
     }
   ],
   stacktraces?: true,
   changeset: #Ash.Changeset<
     action_type: :update,
     action: :package_arrived,
     attributes: %{},
     relationships: %{},
     errors: [
       %AshStateMachine.Errors.NoMatchingTransition{
         action: :package_arrived,
         target: :arrived,
         old_state: :confirmed,
         changeset: nil,
         query: nil,
         error_context: [],
         vars: [],
         path: [],
         stacktrace: #Stacktrace<>,
         class: :invalid
       }
     ],
     data: #Statepoc.Support.Ticket<
       __meta__: #Ecto.Schema.Metadata<:loaded>,
       id: "61868f6d-1dc0-42d5-92c4-01eee81d6d29",
       error: nil,
       error_state: nil,
       state: :confirmed,
       aggregates: %{},
       calculations: %{},
       ...
     >,
     context: %{actor: nil, authorize?: false},
     valid?: false
   >,
   query: nil,
   error_context: [nil],
   vars: [],
   path: [],
   stacktrace: #Stacktrace<>,
   class: :invalid
 }}

Great, we correctly get an error. However, the after_transaction doesn’t appear to have been triggered since no message was logged to the terminal.

Querying all Tickets also reveals that the record hasn’t been updated with the error as intended.

iex(4)> require Ash.Query
Ash.Query
iex(5)> Statepoc.Support.Ticket |> Statepoc.Support.read()
{:ok,
 [
   #Statepoc.Support.Ticket<
     __meta__: #Ecto.Schema.Metadata<:loaded>,
     id: "61868f6d-1dc0-42d5-92c4-01eee81d6d29",
     error: nil,
     error_state: nil,
     state: :confirmed,
     aggregates: %{},
     calculations: %{},
     ...
   >
 ]}

The attributes error and error_state are nil and no logging was triggered.

Do note that I think that the message field passed in the update error changeset in the documentation example should be error to match the resource attributes.

Most Liked

zachdaniel

zachdaniel

Creator of Ash

The reason that the after_action isn’t getting triggered is actually because, in a way, the action is never really being called. We’re returning early from the action with the invalid changeset. However, this to me looks like a bug since after_transaction hooks are meant to be run on both success and failure. What this means is that we need to address our create/update/destroy code that is returning early for changesets w/ errors to properly invoke the after_transaction hook.

If you have a chance, please open a bug for this on ash.

Separately, the errors not showing in AshAdmin is also, in a way, a bug. What is happening there is that we only show errors (currently) in ash_admin that implement the AshPhoenix.Form.Error protocol. That protocol exists to avoid showing sensitive errors on the client. We implement the protocol by default for common/standard errors, and then for anything out of the usual we expect users to implement it themselves (i.e to show custom errors and/or surface other kinds of errors that don’t have an obvious “externally safe” message). However, AshAdmin is an admin tool, and so we should do the following:

  1. by default, if there is an exception that isn’t implemented, we show a message like “Got N messages that could not be rendered, please check the server logs for more” and then log the errors.

  2. allow a configuration that will show external messages that defaults to off.

While this could be seen as a bug because we don’t show an error you’d expect to see, it is probably more accurate to call it a feature request. If you have a chance, please also open this as an issue on the ash_admin repo.

Where Next?

Popular in Questions Top

nobody
How to bind a phoenix app to a specific ip address? could not find anything about that, nowhere, unfortunately, but for me this is quite...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New

Other popular topics Top

danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29703 241
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 43806 214
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36432 110
New
boundedvariable
I am going through the kafka architecture. All the features what the kafka is providing are already in Erlang. I would like hear your opi...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New

We're in Beta

About us Mission Statement