fable
fable copied to clipboard
Allow opting out of included functions from use Fable.Events
I have a situation where I'd like to have my own implementation of emit that enforces some meta-data (such as the current user performing a command).
In order to enforce this throughout the application I don't want Fable's default implementation of emit, but rather have my own which delegates to Fable.Events.emit(__fable_config__(), ...).
This adds the possibility to exclude functions you don't want when doing use Fable.Events.
Not sure how the behaviour should be handled if we allow this.
@frekw Interesting! How is this metadata stored, on the event itself? Or as part of the event?
@benwilson512 each handler (gRPC in our case) extracts a viewer from the authorization token for each request. This viewer is then threaded down into all calls into our domain modules, which in turn threads it down into the Events.emit call, which looks roughly like this:
defmodule App.Events do
use Fable.Events, repo: App.Repo, event_schema: App.Event, except: [:emit]
def emit(
%{
aggregate: aggregate,
fun: fun,
viewer: viewer
} = opts
) do
options = Map.get(opts, :options, [])
Fable.Events.emit(__fable_config__(), aggregate, fun, event_metadata(options, viewer))
end
defp event_metadata(options, %{id: id}) do
Keyword.update(options, :meta, %{viewer: id}, &Map.put_new(&1, :viewer, id))
end
end
This more or less forces anyone who wants to write into the application to provide some set of identification together with each write. :)
Gotcha. I think there are sort of two questions here.
- How do we let people customize the various functions
One option would be to make emit and friends defoverrideable. Then you can just do:
def emit(aggregate, fun, opts \\ []) do
viewer = opts[:viewer] || raise "viewer required!"
opts = Keyword.update(options, :meta, %{viewer: id}, &Map.put_new(&1, :viewer, id))
super(aggregate, fun, opts)
end
This would probably be my preference because it keeps a consistent API.
- Is this a good use for meta?
I had originally envisioned meta as simply for correlation / causation ids once I implemented those. Things like "who did this event" I imagined as being part of the event itself, which you could enforce via changeset functions on the event schema. That is to say, meta would be used by fable to track programmatic or library oriented metadata about an event, whereas domain related data for an event would belong in the event itself.
Still, I see the value here, some further discussion I think would be good.
Ah, yes, I think defoverridable is absolutely a valid approach. I opted for the other way because it allowed me to customize the API further should I want to, but I absolutely agree with you and the advantage of keeping an consistent API in regards to docs.
(2) is interesting. Now that I think about it, I think you're probably right that it belongs in the event itself (which will make enforcing more cumbersome for my use case :) ).
But I suppose that should mean that meta shouldn't really be exposed to any pieces outside of Fable? Otherwise I think users (like me) will find creative ways to abuse it.
It could be a naming thing. meta felt like something I could use from user-space, but if it was called internal I would have stayed away from it.
Ok @benwilson512 I finally had time to get back to this. I've changed it to only make emit overridable because I think that makes sense, but that's not the solution I ended up using.
Since I want to validate the contents of the event itself, and that isn't available until emit calls the function provided, I ended up with a middleware solution in the even router/handler instead that throws errors if :viewer isn't set on the event, thus rolling back the transaction.
defmodule App.Events do
use Fable.Events, repo: App.Repo, event_schema: App.Event
def handlers() do
App.Events.Router.handlers()
|> Enum.map(fn {k, handler} ->
{k, with_validation(handler)}
end)
|> Enum.into(%{})
end
def with_validation(handler) do
fn agg, event ->
if Map.get(event, :viewer) == nil do
raise "missing required field :viewer"
end
handler.(agg, event)
end
end
end
This was straight forward enough to bolt on given the building blocks, but perhaps the router could support middlewares out of the box. What do you think?
This was straight forward enough to bolt on given the building blocks, but perhaps the router could support middlewares out of the box. What do you think?
Currently Fable.Router is just a simple behaviour. There's not even the need for another module like you have here. As long as the module set as router in use Fable.Events (defaults to __MODULE__) somehow implements the behaviour it's fine. There's no prescribed structure to how the router gathers its result, so I'm not sure if it should have middlewares.