modernice/goes

How to use `SoftRestorer`?

Opened this issue · 3 comments

Problem

When the event stream of an aggregate contains a SoftDeleter event, the aggregate can neither be queried nor fetched from the aggregate repository. How can an aggregate be restored if it cannot be fetched to raise the SoftRestorer event?

Example

package example

type RestoredEvent struct {}

func (RestoredEvent) SoftRestore() bool { return true }

func example(repo aggregate.Repository) {
  var foo aggregate.Aggregate // soft-deleted aggregate

  if err := repo.Fetch(context.TODO(), foo); err != nil {
    // fails with repository.ErrDeleted
  }

  // we want to do this
  aggregate.Next(foo, "restored", RestoredEvent{})
  repo.Save(context.TODO(), foo)
}

Proposal – context.Context API

The repository package could provide a "hidden" API using context.Context.WithValue() to disable soft-deletion checks:

package example

func example(repo aggregate.Repository) {
  var foo aggregate.Aggregate

  ctx := repository.WithSoftDeleted(context.TODO())

  repo.Fetch(ctx, foo)
  aggregate.Next(foo, "restored", RestoredEvent{})
  repo.Save(context.TODO(), foo)
}

Drawbacks

  • hiding options behind a Context is considered bad design

Proposal – "Prepare" method

The repository.Repository type could provide a Prepare() method that "prepares" the next query/fetch.

package example

func example(repo *repository.Repository) {
  var foo aggregate.Aggregate

  repo.Prepare(repository.WithSoftDeleted())
  repo.Fetch(ctx, foo)
  aggregate.Next(foo, "restored", RestoredEvent{})
  repo.Save(context.TODO(), foo)
}

Drawbacks

  • clunky to use
  • not concurrency-safe
  • requires users to depend on *repository.Repository instead of aggregate.Repository

Proposal – Add specialized methods

The repository.Repository type could provide a FetchDeleted() and a QueryDeleted() method.

package example
func example(repo *repository.Repository) {
  var foo aggregate.Aggregate

  repo.FetchDeleted(ctx, foo)
  aggregate.Next(foo, "restored", RestoredEvent{})
  repo.Save(context.TODO(), foo)
}

Drawbacks

  • requires users to depend on *repository.Repository instead of aggregate.Repository

Could variadic options not be added to the repository.Fetch method? It shouldn't be backwards incompatible since all previous calls would not include any extra arguments. This would bring it in line with aggregate stream implementation.

Proposal - Use variadic options

The repository.Fetch method could provide a repository.WithSoftDeleted(true) option

package example
func example(repo *repository.Repository) {
  var foo aggregate.Aggregate // aggregate whose latest event was a SoftDelete => true

  repo.Fetch(ctx, foo, repository.WithSoftDeleted(true))
  aggregate.Next(foo, "restored", RestoredEvent{})
  repo.Save(context.TODO(), foo)
}