{"id":25736155,"url":"https://github.com/zacksiri/eventful","last_synced_at":"2025-05-07T23:21:39.548Z","repository":{"id":57498113,"uuid":"216989915","full_name":"zacksiri/eventful","owner":"zacksiri","description":"🎟 Powerful event tracking library for Ecto","archived":false,"fork":false,"pushed_at":"2023-07-04T03:57:30.000Z","size":106,"stargazers_count":34,"open_issues_count":1,"forks_count":1,"subscribers_count":2,"default_branch":"develop","last_synced_at":"2025-04-18T11:26:40.934Z","etag":null,"topics":["ecto","elixir","event-driven","state-machine"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zacksiri.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-10-23T07:02:12.000Z","updated_at":"2025-04-10T10:34:22.000Z","dependencies_parsed_at":"2023-02-09T02:45:14.773Z","dependency_job_id":null,"html_url":"https://github.com/zacksiri/eventful","commit_stats":null,"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zacksiri%2Feventful","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zacksiri%2Feventful/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zacksiri%2Feventful/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zacksiri%2Feventful/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zacksiri","download_url":"https://codeload.github.com/zacksiri/eventful/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252969004,"owners_count":21833389,"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":["ecto","elixir","event-driven","state-machine"],"created_at":"2025-02-26T05:39:38.997Z","updated_at":"2025-05-07T23:21:39.524Z","avatar_url":"https://github.com/zacksiri.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Eventful\n\n![](https://github.com/zacksiri/eventful/workflows/Elixir%20CI/badge.svg) [![Hex.pm](https://img.shields.io/hexpm/v/eventful.svg)](https://hex.pm/packages/eventful) [![Documentation](https://img.shields.io/badge/documentation-gray)](https://hexdocs.pm/eventful)\n\nEventful is a library for anyone who needs a trackable state machine. With transitions and triggers and guards.\n\n## Installation\n\nYou can add `eventful` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:eventful, \"~\u003e 3.0.0\"}\n  ]\nend\n```\n\nEventful is a state machine library with an audit trail for your schemas. You can attach a state machine to any schema in your application.\n\nIn the following we will use a blogging app as an example. Let's imagine you had a schema like the following to store your blog post.\n\n```elixir\ndefmodule MyApp.Post do\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  schema \"posts\" do\n    field :title, :string\n    field :content, :string\n  end\n\n  def changeset(resource, attrs) do\n    resource\n    |\u003e cast(attrs, [:title, :content])\n  end\nend\n```\n\n## Event Schema\n\nLet's imagine you want the ability to track the `state` of this post. You may have a collaboration feature where posts can be put into `draft` or `published` state, moreover you also want to track who did the transition. Let's assume you have a `User` schema of some kind. You could define an `Event` module like the following:\n\n```elixir\ndefmodule MyApp.Post.UserEvent do\n  alias MyApp.{\n    Post,\n    User\n  }\n\n  use Eventful,\n    parent: {:post, Post},\n    actor: {:user, User},\n    table_name: \"post_user_events\"\nend\n```\n\n## Migration\n\nTo make this work you'll also need to add a migration.\n\n```elixir\ndefmodule MyApp.Repo.Migrations.CreatePostUserEvents do\n  use Ecto.Migration\n\n  def change do\n    create table(:post_user_events) do\n      add(:name, :string, null: false)\n      add(:domain, :string, null: false)\n      add(:metadata, :map, default: \"{}\")\n\n      add(\n        :post_id,\n        references(:posts, on_delete: :restrict),\n        null: false\n      )\n\n      add(\n        :user_id,\n        references(:users, on_delete: :restrict),\n        null: false\n      )\n\n      timestamps()\n    end\n\n    create(index(:post_events, [:post_id]))\n    create(index(:post_events, [:user_id]))\n  end\nend\n```\n\n## State Machine\nNext you'll need to define your `Transitions` this will allow you to define which states the post can transition to.\n\n```elixir\ndefmodule MyApp.Post.Transitions do\n  use Eventful.Transition, repo: MyApp.Repo\n\n  @behaviour Eventful.Handler\n\n  alias MyApp.Post\n\n  Post\n  |\u003e transition([from: \"draft\", to: \"published\", via: \"publish\", fn changes -\u003e\n    transit(changes)\n  end)\n\n  Post\n  |\u003e transition([from: \"published\", to: \"draft\", via: \"drafting\", fn changes -\u003e\n    transit(changes)\n  end)\nend\n```\n\nNext you'll need to add some field to your `Post` schema which will be used to track the transitions. In this case let's add `:current_state` as the field and also define how the field is governed.\n\n```elixir\ndefmodule MyApp.Post do\n  # ...\n\n  use Eventful.Transitable\n\n  alias __MODULE__.UserEvent\n  alias __MODULE__.Transitions\n\n  Transitions\n  |\u003e governs(:current_state, on: UserEvent)\n\n  schema \"posts\" do\n    field :title, :string\n    field :content, :string\n\n    field :current_state, :string, default: \"draft\"\n  end\n\n  # ...\nend\n```\n\nAlso be sure to define the handler in your `UserEvent` module\n\n```elixir\ndefmodule MyApp.Post.UserEvent do\n  alias MyApp.{\n    Post,\n    User\n  }\n\n  use Eventful,\n    parent: {:post, Post},\n    actor: {:user, User},\n    table_name: \"post_user_events\"\n\n  handle(:transitions, using: Post.Transitions)\nend\n\ndefimpl Eventful.Transit, for: MyApp.Post do\n  alias MyApp.Post.UserEvent\n\n  def create(resource, actor, event_name, options \\\\ []) do\n    comment = Keyword.get(options, :comment)\n    parameters = Keyword.get(options, :parameters)\n\n    UserEvent.handle(post, user, %{\n      domain: \"transitions\", \n      event_name: event_name,\n      comment: comment,\n      parameters: parameters\n    })\n  end\nend\n```\n\nYou'll also need to add a migration for the post. You can use `:string` or if you prefer `:citext` for your `:current_state` field.\n\n```elixir\ndefmodule MyApp.Repo.Migrations.AddCurrentStateToPosts do\n  use Ecto.Migration\n\n  def change do\n    alter table(:posts) do\n      add(:current_state, :citext, default: \"draft\", null: false)\n    end\n\n    create(index(:posts, [:current_state]))\n  end\nend\n```\n\n## Transitioning the State\n\nThat's it! That's how your set up your first auditable state machine on your schema. You can how transition the post from state to state.\n\n```elixir\n{:ok, transition} = Eventful.Transit.perform(post, user, \"publish\")\n```\n\n## Copyright and License\n\nCopyright (c) 2022, Zack Siri.\n\nEventful source code is licensed under the [MIT License](LICENSE.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzacksiri%2Feventful","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzacksiri%2Feventful","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzacksiri%2Feventful/lists"}