sequent 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a5a7fbe148700d4a9dcc94486ae59c237043bd824cac58cc40d65ca61f6c86e
4
- data.tar.gz: da13d594f5145355f85a9e02877b9c9137b5fc66757c4b1a1c318801f87065eb
3
+ metadata.gz: 11621c1f62780395fc4954a6a5180aa027eb8a26a88d42da6419b20f9e312122
4
+ data.tar.gz: 855d52912a637416cfdaf66fad0bdf15d9ea4f4474e8896e7441dc8d56afa98b
5
5
  SHA512:
6
- metadata.gz: effaa48077d9f375b804f35dfd1b51165cf919fdafeb2677a03173d8678228b8e6b3d08664ad23189cf88cbe2b3e54c8c62676cc9f70498c7a01f4c303434ba9
7
- data.tar.gz: 456a50e34447a5c1dd429c470b4ba4e75cb7f1b6cf7bb3392da5577057097de79b1b2331d09dd896640aaba4424d347cc3af7bf8aac660195fd4c5dec562d6c3
6
+ metadata.gz: c45c07e3e9ac5a4d52bb82dfb7bd76c7096115eb97f0f87477ee99b95eec513a68e67f422a2fb76d23626596c206a5c881f14afce281129e071b3c383ba36979
7
+ data.tar.gz: de31f973652d7bcc75f8dc6964039d139b464c59e2bd0ace6c4dbf000b017d708dd667fcf6cce90c925c7582361f0dd6a9d53f9abc18ed46c8ccae2c1461fcde
@@ -28,6 +28,8 @@ module Sequent
28
28
 
29
29
  DEFAULT_STRICT_CHECK_ATTRIBUTES_ON_APPLY_EVENTS = false
30
30
 
31
+ DEFAULT_ERROR_LOCALE_RESOLVER = -> { I18n.locale || :en }
32
+
31
33
  attr_accessor :aggregate_repository
32
34
 
33
35
  attr_accessor :event_store,
@@ -51,6 +53,7 @@ module Sequent
51
53
 
52
54
  attr_accessor :logger
53
55
 
56
+ attr_accessor :error_locale_resolver
54
57
 
55
58
  attr_accessor :migration_sql_files_directory,
56
59
  :view_schema_name,
@@ -109,6 +112,7 @@ module Sequent
109
112
  self.strict_check_attributes_on_apply_events = DEFAULT_STRICT_CHECK_ATTRIBUTES_ON_APPLY_EVENTS
110
113
 
111
114
  self.logger = Logger.new(STDOUT).tap {|l| l.level = Logger::INFO }
115
+ self.error_locale_resolver = DEFAULT_ERROR_LOCALE_RESOLVER
112
116
  end
113
117
 
114
118
  def replayed_ids_table_name=(table_name)
@@ -105,6 +105,11 @@ module Sequent
105
105
 
106
106
  # Gets all uncommitted_events from the 'registered' aggregates
107
107
  # and stores them in the event store.
108
+ #
109
+ # The events given to the EventStore are ordered in loading order
110
+ # of the different AggregateRoot's. So Events are stored
111
+ # (and therefore published) in order in which they are `apply`-ed per AggregateRoot.
112
+ #
108
113
  # The command is 'attached' for traceability purpose so we can see
109
114
  # which command resulted in which events.
110
115
  #
@@ -7,7 +7,15 @@ require_relative 'helpers/mergable'
7
7
 
8
8
  module Sequent
9
9
  module Core
10
- # Base command
10
+ #
11
+ # Base class for all Command's.
12
+ #
13
+ # Commands form the API of your domain. They are
14
+ # simple data objects with descriptive names
15
+ # of what they want to achieve. E.g. `SendInvoice`.
16
+ #
17
+ # BaseCommand uses `ActiveModel::Validations` for
18
+ # validations
11
19
  class BaseCommand
12
20
  include ActiveModel::Validations,
13
21
  Sequent::Core::Helpers::Copyable,
@@ -40,6 +48,9 @@ module Sequent
40
48
  end
41
49
  end
42
50
 
51
+ #
52
+ # Utility class containing all subclasses of BaseCommand
53
+ #
43
54
  class Commands
44
55
  class << self
45
56
  def commands
@@ -60,7 +71,7 @@ module Sequent
60
71
  end
61
72
  end
62
73
 
63
- # Most commonly used command
74
+ # Most commonly used Command
64
75
  # Command can be instantiated just by using:
65
76
  #
66
77
  # Command.new(aggregate_id: "1", user_id: "joe")
@@ -4,23 +4,29 @@ require_relative 'current_event'
4
4
  module Sequent
5
5
  module Core
6
6
  #
7
- # Single point in the application where subclasses of Sequent::Core::BaseCommand
8
- # are executed. This will initiate the entire flow of:
7
+ # Single point in the application to get something done in Sequent.
8
+ # The CommandService handles all subclasses Sequent::Core::BaseCommand. Most common
9
+ # use is to subclass `Sequent::Command`.
9
10
  #
10
- # * Validate command
11
- # * Call correct Sequent::Core::BaseCommandHandler
12
- # * CommandHandler decides which Sequent::Core::AggregateRoot (s) to call
13
- # * Events are stored in the Sequent::Core::EventStore
14
- # * Unit of Work is cleared
11
+ # The CommandService is available via the shortcut method `Sequent.command_service`
12
+ #
13
+ # To use the CommandService please use:
14
+ #
15
+ # Sequent.command_service.execute_commands(...)
15
16
  #
16
17
  class CommandService
18
+ #
17
19
  # Executes the given commands in a single transactional block as implemented by the +transaction_provider+
18
20
  #
19
- # For each command:
21
+ # For each Command:
22
+ #
23
+ # * Validate command
24
+ # * Call Sequent::CommandHandler's listening to the given Command
25
+ # * Store and publish Events
26
+ # * Any new Command's (from e.g. workflows) are queued for processing in the same transaction
27
+ #
28
+ # At the end the transaction is committed and the AggregateRepository's Unit of Work is cleared.
20
29
  #
21
- # * All filters are executed. Any exception raised will rollback the transaction and propagate up
22
- # * If the command is valid all +command_handlers+ that +handles_message?+ is invoked
23
- # * The +repository+ commits the command and all uncommitted_events resulting from the command
24
30
  def execute_commands(*commands)
25
31
  commands.each do |command|
26
32
  if command.respond_to?(:event_aggregate_id) && CurrentEvent.current
@@ -33,6 +39,7 @@ module Sequent
33
39
  end
34
40
 
35
41
  def remove_event_handler(clazz)
42
+ warn "[DEPRECATION] `remove_event_handler` is deprecated"
36
43
  event_store.remove_event_handler(clazz)
37
44
  end
38
45
 
@@ -54,9 +61,16 @@ module Sequent
54
61
  end
55
62
 
56
63
  def process_command(command)
64
+ fail ArgumentError, 'command is required' if command.nil?
65
+
66
+ Sequent.logger.debug("[CommandService] Processing command #{command.class}")
67
+
57
68
  filters.each { |filter| filter.execute(command) }
58
69
 
59
- raise CommandNotValid.new(command) unless command.valid?
70
+ I18n.with_locale(Sequent.configuration.error_locale_resolver.call) do
71
+ raise CommandNotValid.new(command) unless command.valid?
72
+ end
73
+
60
74
  parsed_command = command.parse_attrs_to_correct_types
61
75
  command_handlers.select { |h| h.class.handles_message?(parsed_command) }.each { |h| h.handle_message parsed_command }
62
76
  repository.commit(parsed_command)
@@ -46,6 +46,10 @@ module Sequent
46
46
  end
47
47
 
48
48
  def process_event(event)
49
+ fail ArgumentError, 'event is required' if event.nil?
50
+
51
+ Sequent.logger.debug("[EventPublisher] Publishing event #{event.class}")
52
+
49
53
  configuration.event_handlers.each do |handler|
50
54
  begin
51
55
  handler.handle_message event
@@ -33,10 +33,18 @@ module Sequent
33
33
  # Stores the events in the EventStore and publishes the events
34
34
  # to the registered event_handlers.
35
35
  #
36
- # Streams_with_Events is an enumerable of pairs from
37
- # `StreamRecord` to arrays of uncommitted `Event`s.
36
+ # The events are published according to the order in
37
+ # the tail of the given `streams_with_events` array pair.
38
+ #
39
+ # @param command The command that caused the Events
40
+ # @param streams_with_events is an enumerable of pairs from
41
+ # `StreamRecord` to arrays ordered uncommitted `Event`s.
38
42
  #
39
43
  def commit_events(command, streams_with_events)
44
+ fail ArgumentError, "command is required" if command.nil?
45
+
46
+ Sequent.logger.debug("[EventStore] Committing events for command #{command.class}")
47
+
40
48
  store_events(command, streams_with_events)
41
49
  publish_events(streams_with_events.flat_map { |_, events| events })
42
50
  end
@@ -9,16 +9,30 @@ require_relative 'association_validator'
9
9
  module Sequent
10
10
  module Core
11
11
  module Helpers
12
- # Provides functionality for defining attributes with their types
12
+ # Provides functionality for defining attributes with their types.
13
13
  #
14
- # Since our Commands and ValueObjects are not backed by a database like e.g. rails
14
+ # Since our Commands and ValueObjects are not backed by a database like e.g. Rails
15
15
  # we can not infer their types. We need the types to be able to parse from and to json.
16
- # We could have stored te type information in the json, but we didn't.
17
- #
18
16
  # You typically do not need to include this module in your classes. If you extend from
19
- # Sequent::Core::ValueObject, Sequent::Core::Event or Sequent::Core::BaseCommand you will
17
+ # Sequent::ValueObject, Sequent::Event or Sequent::Command you will
20
18
  # get this functionality for free.
21
19
  #
20
+ # Example:
21
+ #
22
+ # attrs name: String, age: Integer, born: Date
23
+ #
24
+ # Currently Sequent supports the following types:
25
+ #
26
+ # - String
27
+ # - Integer
28
+ # - Boolean
29
+ # - Date
30
+ # - DateTime
31
+ # - Subclasses of Sequent::ValueObject
32
+ # - Lists defined as `array(String)`
33
+ # - BigDecimal
34
+ # - Sequent::Secret
35
+ #
22
36
  module AttributeSupport
23
37
  class UnknownAttributeError < StandardError; end
24
38
 
@@ -8,6 +8,7 @@ module Sequent
8
8
  module Mergable
9
9
 
10
10
  def merge!(attrs = {})
11
+ warn "[DEPRECATION] `merge!` is deprecated. Please use `copy` instead. This method will no longer be included in the next version of Sequent. You can still use it but you will have to include the module `Sequent::Core::Helpers::Mergable` yourself."
11
12
  attrs.each do |name, value|
12
13
  self.send("#{name}=", value)
13
14
  end
@@ -64,6 +64,10 @@ module Sequent
64
64
  configuration.aggregate_repository
65
65
  end
66
66
 
67
+ def self.dry_run(*commands)
68
+ Sequent::Util::DryRun.these_commands(commands)
69
+ end
70
+
67
71
  # Shortcut classes for easy usage
68
72
  Event = Sequent::Core::Event
69
73
  Command = Sequent::Core::Command
@@ -0,0 +1,191 @@
1
+ require_relative '../test/command_handler_helpers'
2
+
3
+ module Sequent
4
+ module Util
5
+ ##
6
+ # Dry run provides the ability to inspect what would
7
+ # happen if the given commands would be executed
8
+ # without actually committing the results.
9
+ # You can inspect which commands are executed
10
+ # and what the resulting events would be
11
+ # with theSequent::Projector's and Sequent::Workflow's
12
+ # that would be invoked (without actually invoking them).
13
+ #
14
+ # Since the Workflow's are not actually invoked new commands
15
+ # resulting from this Workflow will of course not be in the result.
16
+ #
17
+ # Caution: Since the Sequent Configuration is shared between threads this method
18
+ # is not Thread safe.
19
+ #
20
+ # Example usage:
21
+ #
22
+ # result = Sequent.dry_run(create_foo_command, ping_foo_command)
23
+ #
24
+ # result.print(STDOUT)
25
+ #
26
+ module DryRun
27
+ EventInvokedHandler = Struct.new(:event, :handler)
28
+
29
+ ##
30
+ # Proxies the given EventStore implements commit_events
31
+ # that instead of publish and store just publishes the events.
32
+ class EventStoreProxy
33
+ attr_reader :command_with_events, :event_store
34
+
35
+ delegate :load_events_for_aggregates,
36
+ :load_events,
37
+ :publish_events,
38
+ :stream_exists?,
39
+ to: :event_store
40
+
41
+ def initialize(result)
42
+ @event_store = Sequent::Test::CommandHandlerHelpers::FakeEventStore.new
43
+ @command_with_events = {}
44
+ @result = result
45
+ end
46
+
47
+ def commit_events(command, streams_with_events)
48
+ event_store.commit_events(command, streams_with_events)
49
+
50
+ new_events = streams_with_events.flat_map { |_, events| events }
51
+ @result.published_command_with_events(command, new_events)
52
+ end
53
+ end
54
+
55
+ ##
56
+ # Records which Projector's and Workflow's are executed
57
+ #
58
+ class RecordingEventPublisher < Sequent::Core::EventPublisher
59
+ attr_reader :projectors, :workflows
60
+
61
+ def initialize(result)
62
+ @result = result
63
+ end
64
+
65
+ def process_event(event)
66
+ Sequent.configuration.event_handlers.each do |handler|
67
+ next unless handler.class.handles_message?(event)
68
+
69
+ if handler.is_a?(Sequent::Workflow)
70
+ @result.invoked_workflow(EventInvokedHandler.new(event, handler.class))
71
+ elsif handler.is_a?(Sequent::Projector)
72
+ @result.invoked_projector(EventInvokedHandler.new(event, handler.class))
73
+ else
74
+ fail "Unrecognized event_handler #{handler.class} called for event #{event.class}"
75
+ end
76
+ rescue
77
+ raise PublishEventError.new(handler.class, event)
78
+ end
79
+ end
80
+ end
81
+
82
+ ##
83
+ # Contains the result of a dry run.
84
+ #
85
+ # @see #tree
86
+ # @see #print
87
+ #
88
+ class Result
89
+ EventCalledHandlers = Struct.new(:event, :projectors, :workflows)
90
+
91
+ def initialize
92
+ @command_with_events = {}
93
+ @event_invoked_projectors = []
94
+ @event_invoked_workflows = []
95
+ end
96
+
97
+ def invoked_projector(event_invoked_handler)
98
+ event_invoked_projectors << event_invoked_handler
99
+ end
100
+
101
+ def invoked_workflow(event_invoked_handler)
102
+ event_invoked_workflows << event_invoked_handler
103
+ end
104
+
105
+ def published_command_with_events(command, events)
106
+ command_with_events[command] = events
107
+ end
108
+
109
+ ##
110
+ # Returns the command with events as a tree structure.
111
+ #
112
+ # {
113
+ # command => [
114
+ # EventCalledHandlers,
115
+ # EventCalledHandlers,
116
+ # EventCalledHandlers,
117
+ # ]
118
+ # }
119
+ #
120
+ # The EventCalledHandlers contains an Event with the
121
+ # lists of `Sequent::Projector`s and `Sequent::Workflow`s
122
+ # that were called.
123
+ #
124
+ def tree
125
+ command_with_events.reduce({}) do |memo, (command, events)|
126
+ events_to_handlers = events.map do |event|
127
+ for_current_event = ->(pair) { pair.event == event }
128
+ EventCalledHandlers.new(
129
+ event,
130
+ event_invoked_projectors.select(&for_current_event).map(&:handler),
131
+ event_invoked_workflows.select(&for_current_event).map(&:handler),
132
+ )
133
+ end
134
+ memo[command] = events_to_handlers
135
+ memo
136
+ end
137
+ end
138
+
139
+ ##
140
+ # Prints the output from #tree to the given `io`
141
+ #
142
+ def print(io)
143
+ tree.each_with_index do |(command, event_called_handlerss), index|
144
+ io.puts "+++++++++++++++++++++++++++++++++++" if index == 0
145
+ io.puts "Command: #{command.class} resulted in #{event_called_handlerss.length} events"
146
+ event_called_handlerss.each_with_index do |event_called_handlers, i|
147
+ io.puts "" if i > 0
148
+ io.puts "-- Event #{event_called_handlers.event.class} was handled by:"
149
+ io.puts "-- Projectors: [#{event_called_handlers.projectors.join(', ')}]"
150
+ io.puts "-- Workflows: [#{event_called_handlers.workflows.join(', ')}]"
151
+ end
152
+
153
+ io.puts "+++++++++++++++++++++++++++++++++++"
154
+ end
155
+ end
156
+
157
+ private
158
+
159
+ attr_reader :command_with_events, :event_invoked_projectors, :event_invoked_workflows
160
+ end
161
+
162
+ ##
163
+ # Main method of the DryRun.
164
+ #
165
+ # Caution: Since the Sequent Configuration is changed and is shared between threads this method
166
+ # is not Thread safe.
167
+ #
168
+ # After invocation the sequent configuration is reset to the state it was before
169
+ # invoking this method.
170
+ #
171
+ # @param commands - the commands to dry run
172
+ # @return Result - the Result of the dry run. See Result.
173
+ #
174
+ def self.these_commands(commands)
175
+ current_event_store = Sequent.configuration.event_store
176
+ current_event_publisher = Sequent.configuration.event_publisher
177
+ result = Result.new
178
+
179
+ Sequent.configuration.event_store = EventStoreProxy.new(result)
180
+ Sequent.configuration.event_publisher = RecordingEventPublisher.new(result)
181
+
182
+ Sequent.command_service.execute_commands(*commands)
183
+
184
+ result
185
+ ensure
186
+ Sequent.configuration.event_store = current_event_store
187
+ Sequent.configuration.event_publisher = current_event_publisher
188
+ end
189
+ end
190
+ end
191
+ end
@@ -1,3 +1,4 @@
1
1
  require_relative 'skip_if_already_processing'
2
2
  require_relative 'timer'
3
3
  require_relative 'printer'
4
+ require_relative 'dry_run'
@@ -1,3 +1,3 @@
1
1
  module Sequent
2
- VERSION = '3.4.0'
2
+ VERSION = '3.5.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequent
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.0
4
+ version: 3.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lars Vonk
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2019-12-09 00:00:00.000000000 Z
15
+ date: 2020-02-10 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: activerecord
@@ -152,6 +152,20 @@ dependencies:
152
152
  - - "~>"
153
153
  - !ruby/object:Gem::Version
154
154
  version: 2.6.5
155
+ - !ruby/object:Gem::Dependency
156
+ name: i18n
157
+ requirement: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ type: :runtime
163
+ prerelease: false
164
+ version_requirements: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
155
169
  - !ruby/object:Gem::Dependency
156
170
  name: rspec
157
171
  requirement: !ruby/object:Gem::Requirement
@@ -366,6 +380,7 @@ files:
366
380
  - lib/sequent/test/event_handler_helpers.rb
367
381
  - lib/sequent/test/event_stream_helpers.rb
368
382
  - lib/sequent/test/time_comparison.rb
383
+ - lib/sequent/util/dry_run.rb
369
384
  - lib/sequent/util/printer.rb
370
385
  - lib/sequent/util/skip_if_already_processing.rb
371
386
  - lib/sequent/util/timer.rb