Class: Funes::Projection

Inherits:
Object
  • Object
show all
Defined in:
app/projections/funes/projection.rb

Overview

Projections perform the necessary pattern-matching to compute and aggregate the interpretations that the system gives to a particular collection of events. A projection consists of a series of interpretations defined by the programmer and an aggregated representation of these interpretations (therefore, a representation of transient and final states) which is referenced as the *materialized representation*.

A projection can be virtual or persistent. In practical terms, what defines this characteristic is the type of *materialized representation* configured in the projection.

  • **Virtual projection:** has an ActiveModel instance as its materialized representation. An instance like this

is not persistent, but can be calculated at runtime and has characteristics like validation that are quite valuable in the context of a projection.

  • **Persistent projection:** has an ActiveRecord instance as its materialized representation. A persistent

projection has the same validation capabilities but can be persisted and serve the search patterns needed for the application (read model or even [eager read derivation](martinfowler.com/bliki/EagerReadDerivation.html)).

## Bitemporal Queries

Projections support two temporal dimensions:

  • **Record history** (‘as_of`): Determines which events are loaded from the database (by `created_at`). This is handled at the EventStream level before events reach the projection.

  • **Actual history** (‘at`): The temporal reference for the projection. Passed to all interpretation blocks as the third argument.

All interpretation blocks (‘initial_state`, `interpretation_for`, and `final_state`) receive `at` as their temporal reference, representing when events actually occurred rather than when the system recorded them.

Class Method Summary collapse

Class Method Details

.final_state {|transient_state, at| ... } ⇒ void

This method returns an undefined value.

Register a final interpretation of the state.

*Default behavior:* when this is not defined the projection does nothing after the interpretations

Examples:

class YourProjection < Funes::Projection
  final_state do |transient_state, _at|
    # TODO...
  end
end

Yields:

  • (transient_state, at)

    Block invoked to produce the final state.

Yield Parameters:

  • transient_state (ActiveModel::Model, ActiveRecord::Base)

    The current transient state after all interpretations.

  • at (Time)

    The temporal reference point for the projection.

Yield Returns:

  • (ActiveModel::Model, ActiveRecord::Base)

    the final transient state instance



103
104
105
106
# File 'app/projections/funes/projection.rb', line 103

def final_state(&block)
  @interpretations ||= {}
  @interpretations[:final] = block
end

.initial_state {|materialization_model, at| ... } ⇒ void

This method returns an undefined value.

Registers the initial transient state that will be used for the current projection’s interpretations.

*Default behavior:* When no block is provided the initial state defaults to a new instance of the configured materialization model.

Examples:

class YourProjection < Funes::Projection
  initial_state do |materialization_model, _at|
    materialization_model.new(some: :specific, value: 42)
  end
end

Yields:

Yield Parameters:

  • materialization_model (Class<ActiveRecord::Base>, Class<ActiveModel::Model>)

    The materialization model constant.

  • at (Time)

    The temporal reference point for the projection.

Yield Returns:

  • (ActiveModel::Model, ActiveRecord::Base)

    the new transient state



82
83
84
85
# File 'app/projections/funes/projection.rb', line 82

def initial_state(&block)
  @interpretations ||= {}
  @interpretations[:init] = block
end

.interpretation_for(event_type) {|state, event, at| ... } ⇒ void

This method returns an undefined value.

Registers an interpretation block for a given event type.

Inside interpretation blocks, you can reject events by calling event.errors.add(…) on the event. When used as a **consistency projection**, these errors will be transferred to event.interpretation_errors and the event will not be persisted. In transactional or async projections, errors added to the event have no rejection effect and will be logged as a warning.

Examples:

Rejecting an event in a consistency projection

class YourProjection < Funes::Projection
  interpretation_for Order::Placed do |transient_state, current_event, _at|
    current_event.errors.add(:base, "Order total too high") if current_event.amount > 10_000
    transient_state.assign_attributes(total: (transient_state.total || 0) + current_event.amount)
    transient_state
  end
end

Parameters:

  • event_type (Class<Funes::Event>)

    The event class constant that will be interpreted.

Yields:

  • (state, event, at)

    Block invoked with the current state, the event and the temporal reference. It should return a new version of the transient state.

Yield Parameters:

  • transient_state (ActiveModel::Model, ActiveRecord::Base)

    The current transient state

  • event (Funes::Event)

    Event instance.

  • at (Time)

    The temporal reference point for the projection.

Yield Returns:

  • (ActiveModel::Model, ActiveRecord::Base)

    the new transient state



60
61
62
63
# File 'app/projections/funes/projection.rb', line 60

def interpretation_for(event_type, &block)
  @interpretations ||= {}
  @interpretations[event_type] = block
end

.materialization_model(active_record_or_model) ⇒ void

This method returns an undefined value.

Register the projection’s materialization model

Examples:

Virtual projection (non-persistent, ActiveModel)

class YourProjection < Funes::Projection
  materialization_model YourActiveModelClass
end

Persistent projection (persisted read model, ActiveRecord)

class YourProjection < Funes::Projection
  materialization_model YourActiveRecordClass
end

Parameters:

  • active_record_or_model (Class<ActiveRecord::Base>, Class<ActiveModel::Model>)

    The materialization model class.



122
123
124
# File 'app/projections/funes/projection.rb', line 122

def materialization_model(active_record_or_model)
  @materialization_model = active_record_or_model
end

.persist_materialization_model_with(method_name) ⇒ void

This method returns an undefined value.

Registers an instance method on the materialization model that owns the persistence step, replacing the framework’s default upsert.

When set, the named method is invoked on the in-memory state after all interpretations have run and idx has been assigned. The method takes no arguments and is expected to raise on failure. The framework still calls state.valid? and raises Funes::InvalidMaterializationState before delegating, so the custom method only runs against valid state.

Declaring persist_materialization_model_with also lifts the requirement that the materialization model be an ActiveRecord::Base subclass: a plain ActiveModel class is enough as long as it exposes assign_attributes, attributes, valid? and errors.

Examples:

Persisting a projection to a JSON file

class YourProjection < Funes::Projection
  materialization_model YourMaterializationModel
  persist_materialization_model_with :save_to_object_storage!
end

Parameters:

  • method_name (Symbol)

    The instance method on the materialization model that performs the write.



147
148
149
# File 'app/projections/funes/projection.rb', line 147

def persist_materialization_model_with(method_name)
  @persist_method = method_name
end

.raise_on_unknown_eventsvoid

This method returns an undefined value.

It changes the sensibility of the projection about events that it doesn’t know how to interpret

By default, a projection ignores events that it doesn’t have interpretations for. This method informs the projection that in cases like that an Funes::UnknownEvent should be raised and the DB transaction should be rolled back.

Examples:

class YourProjection < Funes::Projection
  raise_on_unknown_events
end


163
164
165
# File 'app/projections/funes/projection.rb', line 163

def raise_on_unknown_events
  @throws_on_unknown_events = true
end