jdreaver / eventful

Event Sourcing library for Haskell
MIT License
106 stars 22 forks source link

Have some notion of validation of commands in Aggregate? #19

Open arianvp opened 7 years ago

arianvp commented 7 years ago

The current definition of Aggregate is as follows:

data Aggregate state event command =
  Aggregate
  { aggregateCommandHandler :: state -> command -> [event]
  , aggregateProjection :: Projection state event
  }

But people tell me I should never trust user input. What if the incoming command is invalid? How would I handle this? Usually I have something like:

data Validation errors v = Error errors | Validated v
instance Semigroup errors => Applicative (Validation errors)

data EmailError = IsNotAnEmail | NotAnEducationalInstitution | AlreadyExists
validateSubmitEmail :: EmailCommand -> EmailState -> Validation [EmailError] EmailEvent

Would I have to map errors to "events" ? Such that, an invalid command inserts an ErrorOccured <reason> event?

But that would give you superflous events that probably should not affect the state in the Projection

jdreaver commented 7 years ago

I went down this path a few months ago and decided not to modify the definition of Aggregate. The reason is because there are way too many subtle variations with how users of this library would want to handle errors, and committing to one way as the "blessed" way seems folly.

In my opinion there are a few options here:

  1. You can create "error" or "invalid" events, so event :: Either EmailError EmailEvent. Then in your code that actually uses the Aggregate, you can do whatever you want when you encounter any Left EmailError values. If you encounter these you can either not write these events to the event store, or not write any events.
  2. You can have some function against your state like state -> [EmailError] (this could even be a field if your state is a record type) that you can use to decide to fail your command handler. More specifically, if someone gives you an invalid event, then when you project it onto your state you can flip some bit that says it is invalid. Then you can read your state and the command handler function can use that information how it wishes.
  3. You can ignore the eventful Aggregate and indeed use your own! Yours is a perfectly fine abstraction. The real question of "what happens when a command is invalid" is handled in your handler code anyway, so whatever mechanism you use to determine a command is invalid is mainly a question of ergonomics.

The goal of eventful is to be a toolbox, not a framework, so my hope is to make the tools general enough to be used a la carte and not be too interdependent.

Also by the way, in the latest version of eventful Aggregate was renamed to CommandHandler. I didn't realize I haven't made a release in a few months, I should get on that...


As an aside, after talking with a few users I'm thinking of moving some of the opinionated abstractions like CommandHandler, ProcessManager, ProjectionCache, etc either to the docs or to some library that indicates they aren't the only way to do things (maybe call it eventful-simple?). Other eventful libraries would then deal principally with handling events, event storage, reading events, that type of thing.

I don't want users to think these are the only ways of doing things, or that they are even the most general ways of doing things. In your case, I would much rather you use the command handling abstraction that you like using best instead of having to do either of the suggestions I mentioned above.