When you’re building some kind of a CRUD type API, particularly when being quasi-RESTful, state changes typically happen with updating something like a status
or state
field. This ends up evoking other changes and/or defines what related changes can be made.
For instance, if I was to update to canceled
I might have a field like cancellation_reason
that would be required only on that state change. When handling things with dedicated functions or even actions, you have stuff like: “process” or “cancel” or whatever. This is place where something functional/RPC > REST/CRUD in my opinion.
How do I play this out in an Ash action(s)? Is there a way to have a generic update action that can then traffic control to the appropriate related action based on the updated value in status
or do I end up needing to cram all of that logic into a update function?
With plain old functions I typically check if/what the change to the status
field is then just route to different change sets accordingly. I might have something like Resource.cancellation_changeset/2
that checks:
- Can I even transition to cancelled from the current state?
- Are cancellation specific attributes included and correct?
- Are there any tools in AshStateMachine that would really help here? It appears that plugin is mainly just about making sure transitions follow a predictable flow.
There are definitely ways to “route” to other actions within an update action, typically using manual or generic actions. I typically suggest starting by putting the logic in changes, however. For example:
defmodule YourResource.Changes.TryCancel do
use Ash.Resource.Change
def change(changeset, _, _) do
# your logic in a change can be "good old fashioned functions" that you compose as normal
case try_cancel(changeset) do
{:ok, changeset} -> ...something
:error -> ...something else
end
end
end
update :cancel do
change __MODULE__.Changes.TryCancel
end
Changes can be written in an abstract way to allow each action to be a relatively simple declaration of: 1. its interface and 2: its constituent parts
update :cancel do
change {__MODULE__.Changes.TransitionState, to: :cancelled}
end
update :process do
change {__MODULE__.Changes.TransitionState, to: :cancelled}
end
This is often far simpler than having router actions IMO. The TransitionState
change proposed there would be able to “route” depending on conditions of the action/data, etc.
1 Like
The main reason for wanting to route through other actions is to take advantage of the DSLs and such.
For instance, let’s say I have a validation I want to run—as in something doing use Ash.Resource.Validation
. How do I apply that to changeset inside the change?
(Looking at the actual data I can see an :atomic_validations
key that’s just a list. Easy enough. But I don’t see any obvious official way to manipulate this outside of the validate
DSL in an action.
Most of my interest in routing through actions is just this. How do I use building blocks like existing changes, validations, etc. outside of an action? And, if I’m not really meant to, it seems as if the answer is routing through other actions.
(I’ll have a look at generics in a moment.)
Right, I feel you. We’ve talked about writing composers for validation modules and change modules. I think maybe some utilities could be all it takes to make this work well?
like
def change(changeset, _, _) do
if some_condition do
Ash.Changeset.apply_validation(changeset, {Validation, opts})
else
changeset
end
end
def atomic(changeset, _, _) do
if some_condition do
Ash.Changeset.apply_atomic_validation(changeset, {Validation, opts})
else
changeset
end
end
That would actually work just fine.
Want me to take a stab at it? Haha.
It wouldn’t hurt at all to have these functions Whether or not this is the route we go for general usage remains to be seen though. We’ve got some ideas.