zilverline/sequent

View on GitHub
lib/sequent/configuration.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# frozen_string_literal: true

require_relative 'core/event_store'
require_relative 'core/command_service'
require_relative 'core/transactions/no_transactions'
require_relative 'core/aggregate_repository'
require_relative 'core/persistors/active_record_persistor'
require 'logger'

module Sequent
  class Configuration
    DEFAULT_VERSIONS_TABLE_NAME = 'sequent_versions'

    DEFAULT_MIGRATION_SQL_FILES_DIRECTORY = 'db/tables'
    DEFAULT_DATABASE_CONFIG_DIRECTORY = 'db'
    DEFAULT_DATABASE_SCHEMA_DIRECTORY = 'db'

    DEFAULT_VIEW_SCHEMA_NAME = 'view_schema'
    DEFAULT_EVENT_STORE_SCHEMA_NAME = 'sequent_schema'

    MIGRATIONS_CLASS_NAME = 'Sequent::Migrations::Projectors'

    DEFAULT_NUMBER_OF_REPLAY_PROCESSES = 4

    DEFAULT_OFFLINE_REPLAY_PERSISTOR_CLASS = Sequent::Core::Persistors::ActiveRecordPersistor
    DEFAULT_ONLINE_REPLAY_PERSISTOR_CLASS = Sequent::Core::Persistors::ActiveRecordPersistor

    DEFAULT_EVENT_RECORD_HOOKS_CLASS = Sequent::Core::EventRecordHooks

    DEFAULT_STRICT_CHECK_ATTRIBUTES_ON_APPLY_EVENTS = false

    DEFAULT_ERROR_LOCALE_RESOLVER = -> { I18n.locale || :en }

    DEFAULT_TIME_PRECISION = ActiveSupport::JSON::Encoding.time_precision

    attr_accessor :aggregate_repository,
                  :event_store,
                  :event_store_cache_event_types,
                  :command_service,
                  :event_record_class,
                  :stream_record_class,
                  :snapshot_event_class,
                  :transaction_provider,
                  :event_publisher,
                  :event_record_hooks_class,
                  :command_handlers,
                  :command_filters,
                  :command_middleware,
                  :event_handlers,
                  :uuid_generator,
                  :disable_event_handlers,
                  :logger,
                  :error_locale_resolver,
                  :migration_sql_files_directory,
                  :view_schema_name,
                  :offline_replay_persistor_class,
                  :online_replay_persistor_class,
                  :number_of_replay_processes,
                  :database_config_directory,
                  :database_schema_directory,
                  :event_store_schema_name,
                  :strict_check_attributes_on_apply_events,
                  :enable_multiple_database_support,
                  :primary_database_role,
                  :primary_database_key,
                  :time_precision,
                  :enable_autoregistration

    attr_reader :migrations_class_name,
                :versions_table_name

    def self.instance
      @instance ||= new
    end

    # Create a new instance of Configuration
    def self.reset
      @instance = new
    end

    # Restore the given Configuration
    # @param configuration [Sequent::Configuration]
    def self.restore(configuration)
      @instance = configuration
    end

    def initialize
      self.command_handlers = []
      self.command_filters = []
      self.event_handlers = []
      self.command_middleware = Sequent::Core::Middleware::Chain.new

      self.aggregate_repository = Sequent::Core::AggregateRepository.new
      self.event_store_cache_event_types = true
      self.event_store = Sequent::Core::EventStore.new
      self.command_service = Sequent::Core::CommandService.new
      self.event_record_class = Sequent::Core::EventRecord
      self.stream_record_class = Sequent::Core::StreamRecord
      self.snapshot_event_class = Sequent::Core::SnapshotEvent
      self.transaction_provider = Sequent::Core::Transactions::ActiveRecordTransactionProvider.new
      self.uuid_generator = Sequent::Core::RandomUuidGenerator
      self.event_publisher = Sequent::Core::EventPublisher.new
      self.disable_event_handlers = false
      self.versions_table_name = DEFAULT_VERSIONS_TABLE_NAME
      self.migration_sql_files_directory = DEFAULT_MIGRATION_SQL_FILES_DIRECTORY
      self.view_schema_name = DEFAULT_VIEW_SCHEMA_NAME
      self.event_store_schema_name = DEFAULT_EVENT_STORE_SCHEMA_NAME
      self.migrations_class_name = MIGRATIONS_CLASS_NAME
      self.number_of_replay_processes = DEFAULT_NUMBER_OF_REPLAY_PROCESSES

      self.event_record_hooks_class = DEFAULT_EVENT_RECORD_HOOKS_CLASS

      self.offline_replay_persistor_class = DEFAULT_OFFLINE_REPLAY_PERSISTOR_CLASS
      self.online_replay_persistor_class = DEFAULT_ONLINE_REPLAY_PERSISTOR_CLASS
      self.database_config_directory = DEFAULT_DATABASE_CONFIG_DIRECTORY
      self.database_schema_directory = DEFAULT_DATABASE_SCHEMA_DIRECTORY
      self.strict_check_attributes_on_apply_events = DEFAULT_STRICT_CHECK_ATTRIBUTES_ON_APPLY_EVENTS

      self.logger = Logger.new(STDOUT).tap { |l| l.level = Logger::INFO }
      self.error_locale_resolver = DEFAULT_ERROR_LOCALE_RESOLVER

      self.enable_multiple_database_support = false
      self.primary_database_role = :writing
      self.primary_database_key = :primary

      self.time_precision = DEFAULT_TIME_PRECISION

      self.enable_autoregistration = false
    end

    def can_use_multiple_databases?
      enable_multiple_database_support && ActiveRecord.version > Gem::Version.new('6.1.0')
    end

    def versions_table_name=(table_name)
      fail ArgumentError, 'table_name can not be nil' unless table_name

      @versions_table_name = table_name
      Sequent::Migrations::Versions.table_name = table_name
    end

    def migrations_class_name=(class_name)
      migration_class = Class.const_get(class_name)
      unless migration_class <= Sequent::Migrations::Projectors
        fail ArgumentError, "#{migration_class} must extend Sequent::Migrations::Projectors"
      end

      @migrations_class_name = class_name
    end

    # @!visibility private
    def autoregister!
      return unless enable_autoregistration

      # Only autoregister the AggregateSnapshotter if the autoregistration is enabled
      Sequent::Core::AggregateSnapshotter.skip_autoregister = false

      autoload_if_in_rails

      self.class.instance.command_handlers ||= []
      for_each_autoregisterable_descenant_of(Sequent::CommandHandler) do |command_handler_class|
        Sequent.logger.debug("[Configuration] Autoregistering CommandHandler #{command_handler_class}")
        self.class.instance.command_handlers << command_handler_class.new
      end

      self.class.instance.event_handlers ||= []
      for_each_autoregisterable_descenant_of(Sequent::Projector) do |projector_class|
        Sequent.logger.debug("[Configuration] Autoregistering Projector #{projector_class}")
        self.class.instance.event_handlers << projector_class.new
      end

      for_each_autoregisterable_descenant_of(Sequent::Workflow) do |workflow_class|
        Sequent.logger.debug("[Configuration] Autoregistering Workflow #{workflow_class}")
        self.class.instance.event_handlers << workflow_class.new
      end

      self.class.instance.command_handlers.map(&:class).tally.each do |(clazz, count)|
        if count > 1
          fail "CommandHandler #{clazz} is registered #{count} times. A CommandHandler can only be registered once"
        end
      end

      self.class.instance.event_handlers.map(&:class).tally.each do |(clazz, count)|
        if count > 1
          fail "EventHandler #{clazz} is registered #{count} times. An EventHandler can only be registered once"
        end
      end
    end

    private

    def autoload_if_in_rails
      Rails.autoloaders.main.eager_load(force: true) if defined?(Rails) && Rails.respond_to?(:autoloaders)
    end

    def for_each_autoregisterable_descenant_of(clazz, &block)
      clazz
        .descendants
        .reject(&:abstract_class)
        .reject(&:skip_autoregister)
        .each(&block)
    end
  end
end