I’ve been working on a hobby app as a way to learn Elixir and Phoenix (one of approaching Rails horde, I imagine), and the need arose for an activity feed describing actions of various users. I searched around a bit and it didn’t seem there was anything currently existing, so I rolled my own: Spur. It’s a partial extraction of a solution that’s very simple but it’s been good enough for my needs so far. I learned a lot writing it, so hopefully it can at least serve as a learning tool for others.
The functionality is loosely based on this pretty common Rails gem, and although it’s also very simple it relies on two features of Rails that Phoenix/Ecto very consciously omits: callbacks and association polymorphism. The first issue does not pose much of a challenge since Multi
provides a very nice and explicit (if a bit bulky to my Rails trained eyes) API for grouping related operations. The core of this lib is extracting the main actions that activities usually need to track: inserts, updates, and deletes:
%Context.Resource{}
|> Context.Resource.changeset(params)
|> Spur.update
Polymorphism is another story. Initially I tried to follow the recommendations in the Ecto docs, and that worked alright in my own app, but they proved to be pretty abstraction proof because they require the Activity model to be aware of context specific modules that would be tracked. I briefly considered trying to solve that through configuration, but that would involve an amount of Elixir meta-programming that’s still a bit daunting for me at the moment, and more importantly, I had the thought that activity tracking actually shouldn’t rely on associations, because by its very nature describes an ephemeral relationship. For exactly this reason I usually use some sort of revision tracking in conjunction with it in order to support things like undo, etc. So instead I kept the schema as simple as possible, basically just specifying some required and recommended fields, along with the usual catch all meta field. Along with wrapping the activity and the object change in a transaction, the main sugar it sprinkles is the ability to pass a function that it executes and save the result automatically on the activity, which is useful for things like storing changes.
%Context.Resource{}
|> Context.Resource.changeset(params)
|> Spur.update(fn resource, params -> add_resource_data_to_meta(params) end)
Anyway, it is a good example of how getting acquainted with the Elixir way has made me more thoughtful about how I approach domain modeling, instead of relying on Rails “cheats” like association polymorphism. Of course, I’d greatly appreciate any tips on how to improve my Elixir idioms and suggestions for directions to take this. One idea I’ve been playing with, inspired by revisionair, which I’m using for revisions, is abstracting the underlying storage logic, but I’m not sure how useful that would be since most of the value here is pretty Ecto specific.