Write simple, event-driven applications.
- Easy to retrofit.
- Drop-in compatible with Ecto tests (async included).
- Event log should always be consistent.
- Events are serialized around important "aggregate" database records.
Add Fable in your project's mix.exs file:
defp deps do
[
{:fable, "~> 0.0.1-alpha.1"}
]
endAdd the Fable migration to priv/repo/migrations and migrate it.
As an example, we're going to wrap the following existing function:
defmodule MyApp.Blog do
alias MyApp.Blog.Post
def create_post(params, user) do
with :ok <- is_admin(user) do
%Post{}
|> Post.changeset(params)
|> MyApp.Repo.insert()
end
end
def update_post(post, params, user) do
with :ok <- is_admin(user) do
post
|> Post.changeset(params)
|> MyApp.Repo.update()
end
end
endFirst, create an events module that will handle all the events added to the system.
defmodule MyApp.Events do
use Fable.Events
@impl Fable.Router
def handlers do
%{}
end
endBy default, the module will implement Fable.Router.handlers/0. This can be changed by using use Fable.Events, router: MyApp.EventsRouter.
The example system consists of two actions: creating a blog post and updating a blog post. Using the past tense, define PostCreated and PostUpdated events.
defmodule MyApp.Blog.Events.PostCreated do
use Fable.Event
embedded_schema do
field :title, :string
field :body, :string
end
end
defmodule MyApp.Blog.Events.PostUpdated do
use Fable.Event
embedded_schema do
field :title, :string
field :body, :string
end
endThe data of these embedded schemas will be stored in Fable's event table along with some metadata. The schemas will hold the data your application needs to run the task it deals with. In this example case, all the data passed to the changeset function for posts to be persisted.
With the events being created, wrap the existing code to make use of :fable.
First, move what shall happen as result of the event to a separate function.
defmodule MyApp.Blog do
alias MyApp.Blog.Post
alias MyApp.Blog.Events
def create_post(params, user) do
with :ok <- is_admin(user) do
# moved
end
end
defp post_created(post, %Events.PostCreated{} = event) do
post
|> Post.changeset(Map.from_struct(event))
|> MyApp.Repo.insert()
end
def update_post(post, params, user) do
with :ok <- is_admin(user) do
# moved
end
end
defp post_updated(post, %Events.PostUpdated{} = event) do
post
|> Post.changeset(Map.from_struct(event))
|> MyApp.Repo.update()
end
endNote that the new functions are private and should never be called externally. To make them be callable by Fable, register handlers for their respective events. One way to do this is by having MyApp.Blog implement Fable.Router and letting MyApp.Events call handlers/0 on MyApp.Blog. This is only example of how to handle event registration.
defmodule MyApp.Blog do
# …
@behaviour Fable.Router
@impl Fable.Router
def handlers do
%{
Events.PostCreated => &post_created/2,
Events.PostUpdated => &post_updated/2
}
end
# …
end
defmodule MyApp.Events do
use Fable.Events
@impl Fable.Router
def handlers do
%{}
|> Map.merge(MyApp.Blog.handlers(), &merge/3)
end
# If multiple merged maps want to handle the same event
defp merge(_event, handlers_a, handlers_b) do
List.wrap(handlers_a) ++ List.wrap(handlers_b)
end
endThe last thing to do is emit the events so that the code in the hevent andlers is called.
defmodule MyApp.Blog do
# …
def create_post(params, user) do
%Post{id: Ecto.UUID.generate()}
|> MyApp.Events.emit(fn _, _, _ ->
with :ok <- is_admin(user) do
%Events.PostCreated{
title: params["title"],
body: params["body"]
}
end
end)
|> MyApp.Repo.transaction()
end
# …
def update_post(post, params, user) do
post
|> MyApp.Events.emit(fn _, _, _ ->
with :ok <- is_admin(user) do
%Events.PostUpdated{
title: params["title"],
body: params["body"]
}
end
end)
|> MyApp.Repo.transaction()
end
# …
end
defmodule MyApp.Blog.Post do
use Ecto.Schema
schema "posts" do
…
# needs a migration as well
field :last_event_id, :integer
end
endImportant
Note that the aggregate (the post) will need the :last_event_id field added on the schema and in the database. The aggregate also needs an id before events can be applied to it. This is simple for UUID-based IDs as shown. Using integer-based IDs is supported, but initial creation cannot be handled by a Fable event. Consider using in combination with Ecto.Multi.