Eventful
Eventful is a library for anyone who needs a trackable state machine. With transitions and triggers and guards.
Installation
If available in Hex, the package can be installed
by adding eventful to your list of dependencies in mix.exs:
def deps do
[
{:eventful, "~> 2.0.0"}
]
end
Current State
You will need to create a :current_state field in your model
schema "posts" do
field :current_state, :string, default: "draft"
end
# migration
alter table(:posts) do
field :current_state, :string, default: "draft", null: false
end
Event Schema
Generally your events table will be used to track events for a specific model you have. Let's assume that in this case we have MyApp.Post and MyApp.User as the authenticated user in our app.
We may create something like this.
defmodule MyApp.Post.Event do
alias MyApp.{
Post,
User
}
use Eventful,
parent: {:post, Post},
actor: {:user, User}
end
Migration
Generate a migration file for your events like this.
defmodule MyApp.Repo.Migrations.CreatePostEvents do
use Ecto.Migration
def change do
create table(:post_events) do
add(:name, :string, null: false)
add(:domain, :string, null: false)
add(:metadata, :map, default: "{}")
add(
:post_id,
references(:posts, on_delete: :restrict),
null: false
)
add(
:user_id,
references(:users, on_delete: :restrict),
null: false
)
timestamps()
end
create(index(:post_events, [:post_id]))
create(index(:post_events, [:user_id]))
end
end
Transitions
The next thing is defining a Transitions module
defmodule MyApp.Post.Transitions do
alias MyApp.Post
@behaviour Eventful.Handler
use Eventful.Transition, repo: MyApp.Repo
Post
|> transition(
[from: "draft", to: "reviewing", via: "review"],
fn changes -> transit(changes) end)
)
Post
|> transition(
[from: "reviewing", to: "published", via: "publish"],
fn changes -> transit(changes) end)
)
end
and specify the transition module for the schema
defmodule MyApp.Post do
use Ecto.Schema
use Eventful.Transitable, transitions_module: __MODULE__.Transitions
alias Post.Transitions
alias Post.Event
Transitions
|> governs(:current_state, on: Event)
schema "posts" do
field :current_state, :string, default: "draft"
end
end
Event Handler
You will now need to add the Transitions Handler to your Event module
defmodule MyApp.Post.Event do
alias MyApp.{
Post,
User
}
use Eventful,
parent: {:post, Post},
actor: {:user, User}
handle(:transitions, using: Post.Transitions)
end
Transitioning from State to State
MyApp.Post.Event.handle(post, user, %{domain: "transitions", name: "review"})
This will now transition and track your model and also track who did it.
Triggers
Triggers allow to run code whenever a transition has been made to a specific state
defmodule MyApp.Post.Triggers do
alias MyApp.Post
use Eventful.Trigger
Post
|> trigger([currently: "reviewing"], fn event, post ->
# user code
end)
end
In order for the triggers to execute, you need to pass your custom Triggers module to the transit/2 function
defmodule MyApp.Post.Transitions do
alias MyApp.Post
@behaviour Eventful.Handler
use Eventful.Transition, repo: MyApp.Repo
Post
|> transition(
[from: "draft", to: "reviewing", via: "review"],
fn changes -> transit(changes, Post.Triggers) end)
)
end
Guards
You may add guards allowing to validate an event for a particular resource and an actor (user or system originating the event). Guards are added into the transitions module.
The guards should either return {:ok, :passed} if the validation passed. Anything else will be considered as an error.
defmodule MyApp.Post.Transitions do
alias MyApp.Post
@behaviour Eventful.Handler
use Eventful.Transition, repo: MyApp.Repo
Post
|> transition(
[from: "draft", to: "reviewing", via: "review"],
fn changes -> transit(changes, Post.Triggers) end)
)
defp guard_transition(%Post{current_state: _current_state} = post, _originator, "review") do
if MyApp.check_something(post),
do: {:ok, :passed},
else: {:error, :failed}
end
defp guard_transition(_activity, _originator, _), do: {:ok, :passed}
end
Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/eventful.