sequent 3.4.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
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