{"id":19731365,"url":"https://github.com/cargosense/fable","last_synced_at":"2025-10-04T19:16:44.757Z","repository":{"id":38419359,"uuid":"164134108","full_name":"CargoSense/fable","owner":"CargoSense","description":"Your events have a story to tell","archived":false,"fork":false,"pushed_at":"2024-12-11T00:00:11.000Z","size":85,"stargazers_count":78,"open_issues_count":7,"forks_count":9,"subscribers_count":15,"default_branch":"main","last_synced_at":"2025-04-20T14:43:56.854Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/CargoSense.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-01-04T17:07:43.000Z","updated_at":"2025-02-25T05:16:16.000Z","dependencies_parsed_at":"2022-09-01T06:30:42.164Z","dependency_job_id":"7c9fc1e6-b1d7-459e-b3bb-3430bb9c6671","html_url":"https://github.com/CargoSense/fable","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CargoSense%2Ffable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CargoSense%2Ffable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CargoSense%2Ffable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CargoSense%2Ffable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/CargoSense","download_url":"https://codeload.github.com/CargoSense/fable/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251629069,"owners_count":21618100,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-12T00:20:35.993Z","updated_at":"2025-10-04T19:16:44.650Z","avatar_url":"https://github.com/CargoSense.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Fable\n\nYour events have a story to tell\n\n## Design Philosophy\n\n- Easy to retrofit\n- Drop in compatible with Ecto tests (async included)\n- No footguns. Event log should always be consistent.\n- Events are serialized around important \"aggregate\" database records.\n\n\n## Gettings started\n\nInclude `fable` in your `mix.exs`:\n\n```elixir\ndefp deps do\n  [\n    {:fable, \"~\u003e 0.0.1-alpha.0\", github: \"CargoSense/fable\", branch: \"master\"}\n  ]\nend\n```\n\nAdd the fable migration to `priv/repo/migrations` and migrate it.\n\nAs an example we're going to wrap the following function already existing in\na system:\n\n```elixir\ndefmodule MyApp.Blog do\n  alias MyApp.Blog.Post\n\n  def create_post(params, user) do\n    with :ok \u003c- is_admin(user) do\n      %Post{}\n      |\u003e Post.changeset(params)\n      |\u003e MyApp.Repo.insert()\n    end\n  end\n\n  def update_post(post, params, user) do\n    with :ok \u003c- is_admin(user) do\n      post\n      |\u003e Post.changeset(params)\n      |\u003e MyApp.Repo.update()\n    end\n  end\nend\n```\n\n### Create `MyApp.Events` module\n\nFirst we need to create an events module, which will be dealing with all the events\nbeing added to the system.\n\n```elixir\ndefmodule MyApp.Events do\n  use Fable.Events\n\n  @impl Fable.Router\n  def handlers do\n    %{}\n  end\nend\n```\n\nBy default the module will implement `Fable.Router.handlers/0`, but this can\nbe moved to a different module by doing `use Fable.Events, router: MyApp.EventsRouter`.\n\n### Creating events\n\nThe two actions of the system are creating a blog post and updating a blog post.\nIn the past tense this means we'll need a `PostCreated` and a `PostUpdated` event.\n\n```elixir\ndefmodule MyApp.Blog.Events.PostCreated do\n  use Fable.Event\n\n  embedded_schema do\n    field :title, :string\n    field :body, :string\n  end\nend\n\ndefmodule MyApp.Blog.Events.PostUpdated do\n  use Fable.Event\n\n  embedded_schema do\n    field :title, :string\n    field :body, :string\n  end\nend\n```\n\nThe data of those embedded schemas will be stored in fables event table besides\nsome metadata. The schemas will need to hold whatever data you need to actually\nrun the task it deals with. So in our case all the data passed to the changeset\nfunction for posts to be persisted.\n\n### Wrapping existing code\n\nWith the events being created we can move to wrapping the existing code to make\nuse of `:fable`.\n\nThere are few things to do: \n\n#### Move logic to separate function\n\nFirst move what shall happen as result of the event to a separate function.\n\n```elixir\ndefmodule MyApp.Blog do\n  alias MyApp.Blog.Post\n  alias MyApp.Blog.Events\n\n  def create_post(params, user) do\n    with :ok \u003c- is_admin(user) do\n      # moved\n    end\n  end\n\n  defp post_created(post, %Events.PostCreated{} = event) do\n    post\n    |\u003e Post.changeset(Map.from_struct(event))\n    |\u003e MyApp.Repo.insert()\n  end\n\n  def update_post(post, params, user) do\n    with :ok \u003c- is_admin(user) do\n      # moved\n    end\n  end\n\n  defp post_updated(post, %Events.PostUpdated{} = event) do\n    post\n    |\u003e Post.changeset(Map.from_struct(event))\n    |\u003e MyApp.Repo.update()\n  end\nend\n```\n\n#### Event handler registration\n\nThe new functions are private, because they should never be called by someone else.\nTo make them be callable by fable though they need to be registered as handlers for\ntheir respective events. One way to do that is having `MyApp.Blog` implement \n`Fable.Router` as well and letting `MyApp.Events` call `handlers/0` on `MyApp.Blog`.\nThis is just an example on how to handle event registration. Feel free to customize\nhow registration of event handlers works.\n\n```elixir\ndefmodule MyApp.Blog do\n  …\n\n  @behaviour Fable.Router\n  @impl Fable.Router\n  def handlers do\n    %{\n      Events.PostCreated =\u003e \u0026post_created/2,\n      Events.PostUpdated =\u003e \u0026post_updated/2\n    }\n  end\n\n  …\nend\n\ndefmodule MyApp.Events do\n  use Fable.Events\n\n  @impl Fable.Router\n  def handlers do\n    %{}\n    |\u003e Map.merge(MyApp.Blog.handlers(), \u0026merge/3)\n  end\n\n  # If multiple merged maps want to handle the same event\n  defp merge(_event, handlers_a, handlers_b) do\n    List.wrap(handlers_a) ++ List.wrap(handlers_b)\n  end\nend\n```\n\n#### Emit events\n\nThe last thing to do is emit the events, so the code in the handlers is called\nagain.\n\n```elixir\ndefmodule MyApp.Blog do\n  …\n\n  def create_post(params, user) do\n    %Post{id: Ecto.UUID.generate()}\n    |\u003e MyApp.Events.emit(fn _, _, _ -\u003e \n      with :ok \u003c- is_admin(user) do\n        %Events.PostCreated{\n          title: params[\"title\"],\n          body: params[\"body\"]\n        }\n      end\n    end)\n    |\u003e MyApp.Repo.transaction()\n  end\n\n  …\n\n  def update_post(post, params, user) do\n    post\n    |\u003e MyApp.Events.emit(fn _, _, _ -\u003e \n      with :ok \u003c- is_admin(user) do\n        %Events.PostUpdated{\n          title: params[\"title\"],\n          body: params[\"body\"]\n        }\n      end\n    end)\n    |\u003e MyApp.Repo.transaction()\n  end\n\n  …\nend\n\ndefmodule MyApp.Blog.Post do\n  use Ecto.Schema\n\n  schema \"posts\" do\n    …\n\n    # needs a migration as well\n    field :last_event_id, :integer\n  end\n\nend\n```\n\nThe important thing to note here is that the aggregate (the post) will need the\n`:last_event_id` field being added on the schema and in the db. But also it needs\nto have an id, before events can be applied to it. This is simple for uuid based\nids as shown. Using integer based ids is supported, but initial creation cannot be \nhandled by a fable event; consider some combination with `Ecto.Multi`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcargosense%2Ffable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcargosense%2Ffable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcargosense%2Ffable/lists"}