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 +4 -4
- data/lib/sequent/configuration.rb +4 -0
- data/lib/sequent/core/aggregate_repository.rb +5 -0
- data/lib/sequent/core/command.rb +13 -2
- data/lib/sequent/core/command_service.rb +26 -12
- data/lib/sequent/core/event_publisher.rb +4 -0
- data/lib/sequent/core/event_store.rb +10 -2
- data/lib/sequent/core/helpers/attribute_support.rb +19 -5
- data/lib/sequent/core/helpers/mergable.rb +1 -0
- data/lib/sequent/sequent.rb +4 -0
- data/lib/sequent/util/dry_run.rb +191 -0
- data/lib/sequent/util/util.rb +1 -0
- data/lib/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 11621c1f62780395fc4954a6a5180aa027eb8a26a88d42da6419b20f9e312122
|
4
|
+
data.tar.gz: 855d52912a637416cfdaf66fad0bdf15d9ea4f4474e8896e7441dc8d56afa98b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
#
|
data/lib/sequent/core/command.rb
CHANGED
@@ -7,7 +7,15 @@ require_relative 'helpers/mergable'
|
|
7
7
|
|
8
8
|
module Sequent
|
9
9
|
module Core
|
10
|
-
#
|
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
|
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
|
8
|
-
#
|
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
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
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
|
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
|
-
|
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
|
-
#
|
37
|
-
#
|
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.
|
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::
|
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
|
data/lib/sequent/sequent.rb
CHANGED
@@ -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
|
data/lib/sequent/util/util.rb
CHANGED
data/lib/version.rb
CHANGED
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
|
+
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:
|
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
|