meteor-space / event-sourcing

CQRS and Event Sourcing Infrastructure for Meteor.
MIT License
63 stars 9 forks source link

Formalise concept of state-change rules within aggregate #60

Open rhyslbw opened 8 years ago

rhyslbw commented 8 years ago

A common business rule is to only allow a state change if the current state is equal to or is not equal to a single or group of values. It's currently tedious to protect against this, and as such I suspect may be just left out of testing or forgotten in the command handler.

Now

Donations.domain.test(Donations.Appeal)
  .given(closedAppeal.call(this))
  .when(
    new Donations.MakeAppeal({ targetId: this.appealId })
  )
  .expect([
    new Space.domain.Exception({
      thrower: 'Donations.Appeal',
      error: new Donations.InvalidAppealState('MakeAppeal', 'closed')
    })
  ]);

I suggest we take the focus of the message type ( commandMap and eventMap), and work on a pipeline for external state-change messages stateChangeMap? Mapping to a handler would be part of this, but only after enforcing state change rules as defined. It could throw a generic Space.Error like InvalidState.

As part of this we will need to abstract the mapping of internal state-change events into fields rather than just mapping an event type to a handler. This will be easier to test, and ensure dependencies are not being used to manipulate data once saved

qejk commented 8 years ago

We discussed with @rhyslbw few examples, however got nowhere atm:

(last been the first idea):

stateChangeMap: {
  'Businesses.UpdateBusienss': 
    denyWhen: [@STATES.closed, @STATES.canceled,@STATES.removed]
    handler: this._updateBusiness
    changeTo:  @STATES.newState
}
stateChangeMap: {
  'Businesses.UpdateBusienss': 
    allowWhen: (state) -> state is not in [@STATES.closed, @STATES.canceled,@STATES.removed]
    changeTo: (state) -> state
}
stateChangeMap: {
  'Businesses.UpdateBusienss': 
    denyWhen: [@STATES.closed, @STATES.canceled,@STATES.removed]
    changeTo:  @STATES.newState
}
stateChangeMap: {
  'Donations.CancelAppeal': 
    when: (state) -> state == Donations.Appeal.STATES.draft
    changeTo: Donations.Appeal.STATES.cancelled
}

stateChangeMap: {
  'Donations.UpdateAppealDraft': 
    when: (state) -> state != Donations.Appeal.STATES.draft
    changeTo: Donations.Appeal.STATES.cancelled
}
stateChangeMap: -> {
  'Donations.CancelAppeal': 
    denied/unallowed/blacklisted: []
    allowed/whitelisted: [Donations.Appeal.STATES.draft]
    changeTo/to: Donations.Appeal.STATES.cancelled
}

there’s maybe still potential to reduce boilerplate and confusion by providing a more expressive API, but I doubt we can achieve much past the state change validation