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
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a &gt; b) do {:ok, "a"} end if (a &lt; b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
Kurisu
For example for a current url like http://localhost:4000/cosmetic/products?_utf8=✓&amp;query=perfume&amp;page=2, I would like to get: ...
New
tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New

Other popular topics Top

hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a &gt; b) do {:ok, "a"} end if (a &lt; b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
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
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
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
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 127536 1222
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 54250 245
New

We're in Beta

About us Mission Statement