sequent 3.3.1 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/bin/sequent +31 -25
  3. data/lib/notices.rb +6 -0
  4. data/lib/sequent/application_record.rb +2 -0
  5. data/lib/sequent/configuration.rb +29 -29
  6. data/lib/sequent/core/aggregate_repository.rb +24 -14
  7. data/lib/sequent/core/aggregate_root.rb +16 -7
  8. data/lib/sequent/core/aggregate_roots.rb +24 -0
  9. data/lib/sequent/core/aggregate_snapshotter.rb +8 -5
  10. data/lib/sequent/core/base_command_handler.rb +4 -2
  11. data/lib/sequent/core/command.rb +30 -11
  12. data/lib/sequent/core/command_record.rb +12 -4
  13. data/lib/sequent/core/command_service.rb +41 -25
  14. data/lib/sequent/core/core.rb +2 -0
  15. data/lib/sequent/core/current_event.rb +2 -0
  16. data/lib/sequent/core/event.rb +16 -11
  17. data/lib/sequent/core/event_publisher.rb +20 -15
  18. data/lib/sequent/core/event_record.rb +7 -7
  19. data/lib/sequent/core/event_store.rb +75 -49
  20. data/lib/sequent/core/ext/ext.rb +9 -1
  21. data/lib/sequent/core/helpers/array_with_type.rb +4 -1
  22. data/lib/sequent/core/helpers/association_validator.rb +9 -7
  23. data/lib/sequent/core/helpers/attribute_support.rb +64 -33
  24. data/lib/sequent/core/helpers/autoset_attributes.rb +4 -4
  25. data/lib/sequent/core/helpers/boolean_validator.rb +6 -1
  26. data/lib/sequent/core/helpers/copyable.rb +2 -2
  27. data/lib/sequent/core/helpers/date_time_validator.rb +4 -1
  28. data/lib/sequent/core/helpers/date_validator.rb +6 -1
  29. data/lib/sequent/core/helpers/default_validators.rb +12 -10
  30. data/lib/sequent/core/helpers/equal_support.rb +8 -6
  31. data/lib/sequent/core/helpers/helpers.rb +2 -0
  32. data/lib/sequent/core/helpers/mergable.rb +6 -4
  33. data/lib/sequent/core/helpers/message_handler.rb +3 -1
  34. data/lib/sequent/core/helpers/param_support.rb +19 -15
  35. data/lib/sequent/core/helpers/secret.rb +14 -12
  36. data/lib/sequent/core/helpers/string_support.rb +5 -4
  37. data/lib/sequent/core/helpers/string_to_value_parsers.rb +7 -2
  38. data/lib/sequent/core/helpers/string_validator.rb +6 -1
  39. data/lib/sequent/core/helpers/type_conversion_support.rb +5 -3
  40. data/lib/sequent/core/helpers/uuid_helper.rb +5 -2
  41. data/lib/sequent/core/helpers/value_validators.rb +23 -9
  42. data/lib/sequent/core/persistors/active_record_persistor.rb +19 -9
  43. data/lib/sequent/core/persistors/persistor.rb +16 -14
  44. data/lib/sequent/core/persistors/persistors.rb +2 -0
  45. data/lib/sequent/core/persistors/replay_optimized_postgres_persistor.rb +70 -47
  46. data/lib/sequent/core/projector.rb +25 -22
  47. data/lib/sequent/core/random_uuid_generator.rb +2 -0
  48. data/lib/sequent/core/sequent_oj.rb +2 -0
  49. data/lib/sequent/core/stream_record.rb +9 -3
  50. data/lib/sequent/core/transactions/active_record_transaction_provider.rb +7 -9
  51. data/lib/sequent/core/transactions/no_transactions.rb +2 -1
  52. data/lib/sequent/core/transactions/transactions.rb +2 -0
  53. data/lib/sequent/core/value_object.rb +8 -10
  54. data/lib/sequent/core/workflow.rb +7 -5
  55. data/lib/sequent/generator/aggregate.rb +16 -10
  56. data/lib/sequent/generator/command.rb +26 -19
  57. data/lib/sequent/generator/event.rb +19 -17
  58. data/lib/sequent/generator/generator.rb +6 -0
  59. data/lib/sequent/generator/project.rb +3 -1
  60. data/lib/sequent/generator/template_project/Gemfile +1 -1
  61. data/lib/sequent/generator/template_project/spec/app/projectors/post_projector_spec.rb +1 -1
  62. data/lib/sequent/generator/template_project/spec/lib/post/post_command_handler_spec.rb +1 -1
  63. data/lib/sequent/generator.rb +3 -4
  64. data/lib/sequent/migrations/executor.rb +30 -9
  65. data/lib/sequent/migrations/functions.rb +5 -6
  66. data/lib/sequent/migrations/migrate_events.rb +12 -9
  67. data/lib/sequent/migrations/migrations.rb +2 -1
  68. data/lib/sequent/migrations/planner.rb +33 -23
  69. data/lib/sequent/migrations/projectors.rb +4 -3
  70. data/lib/sequent/migrations/sql.rb +2 -0
  71. data/lib/sequent/migrations/view_schema.rb +93 -44
  72. data/lib/sequent/rake/migration_tasks.rb +59 -23
  73. data/lib/sequent/rake/tasks.rb +5 -2
  74. data/lib/sequent/sequent.rb +6 -1
  75. data/lib/sequent/support/database.rb +39 -17
  76. data/lib/sequent/support/view_projection.rb +6 -3
  77. data/lib/sequent/support/view_schema.rb +2 -0
  78. data/lib/sequent/support.rb +2 -0
  79. data/lib/sequent/test/command_handler_helpers.rb +39 -17
  80. data/lib/sequent/test/event_handler_helpers.rb +10 -4
  81. data/lib/sequent/test/event_stream_helpers.rb +7 -3
  82. data/lib/sequent/test/time_comparison.rb +12 -5
  83. data/lib/sequent/test.rb +2 -0
  84. data/lib/sequent/util/dry_run.rb +194 -0
  85. data/lib/sequent/util/printer.rb +6 -5
  86. data/lib/sequent/util/skip_if_already_processing.rb +21 -5
  87. data/lib/sequent/util/timer.rb +2 -0
  88. data/lib/sequent/util/util.rb +3 -0
  89. data/lib/sequent.rb +4 -0
  90. data/lib/version.rb +3 -1
  91. metadata +110 -59
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/hash_with_indifferent_access'
2
4
 
3
5
  module Sequent
@@ -16,20 +18,25 @@ module Sequent
16
18
  end
17
19
 
18
20
  def self.read_config(env)
19
- fail ArgumentError.new("env is mandatory") unless env
21
+ fail ArgumentError, 'env is mandatory' unless env
20
22
 
21
23
  database_yml = File.join(Sequent.configuration.database_config_directory, 'database.yml')
22
- YAML.load(ERB.new(File.read(database_yml)).result)[env]
24
+ config = YAML.safe_load(ERB.new(File.read(database_yml)).result, aliases: true)[env]
25
+ if Gem.loaded_specs['activerecord'].version >= Gem::Version.create('6.1.0')
26
+ ActiveRecord::Base.configurations.resolve(config).configuration_hash.with_indifferent_access
27
+ else
28
+ ActiveRecord::Base.resolve_config_for_connection(config)
29
+ end
23
30
  end
24
31
 
25
32
  def self.create!(db_config)
26
- ActiveRecord::Base.establish_connection(db_config.merge('database' => 'postgres'))
27
- ActiveRecord::Base.connection.create_database(db_config['database'])
33
+ ActiveRecord::Base.establish_connection(db_config.merge(database: 'postgres'))
34
+ ActiveRecord::Base.connection.create_database(db_config[:database])
28
35
  end
29
36
 
30
37
  def self.drop!(db_config)
31
- ActiveRecord::Base.establish_connection(db_config.merge('database' => 'postgres'))
32
- ActiveRecord::Base.connection.drop_database(db_config['database'])
38
+ ActiveRecord::Base.establish_connection(db_config.merge(database: 'postgres'))
39
+ ActiveRecord::Base.connection.drop_database(db_config[:database])
33
40
  end
34
41
 
35
42
  def self.establish_connection(db_config)
@@ -46,9 +53,8 @@ module Sequent
46
53
 
47
54
  def self.create_schema(schema)
48
55
  sql = "CREATE SCHEMA IF NOT EXISTS #{schema}"
49
- if user = ActiveRecord::Base.connection_config[:username]
50
- sql += " AUTHORIZATION #{user}"
51
- end
56
+ user = configuration_hash[:username]
57
+ sql += %( AUTHORIZATION "#{user}") if user
52
58
  execute_sql(sql)
53
59
  end
54
60
 
@@ -57,27 +63,41 @@ module Sequent
57
63
  end
58
64
 
59
65
  def self.with_schema_search_path(search_path, db_config, env = ENV['RACK_ENV'])
60
- fail ArgumentError.new("env is required") unless env
66
+ fail ArgumentError, 'env is required' unless env
61
67
 
62
68
  disconnect!
63
- original_search_paths = db_config['schema_search_path'].dup
64
- ActiveRecord::Base.configurations[env.to_s] = ActiveSupport::HashWithIndifferentAccess.new(db_config).stringify_keys
65
- db_config['schema_search_path'] = search_path
69
+ original_search_paths = db_config[:schema_search_path].dup
70
+
71
+ if ActiveRecord::VERSION::MAJOR < 6
72
+ ActiveRecord::Base.configurations[env.to_s] =
73
+ ActiveSupport::HashWithIndifferentAccess.new(db_config).stringify_keys
74
+ end
75
+
76
+ db_config[:schema_search_path] = search_path
77
+
66
78
  ActiveRecord::Base.establish_connection db_config
67
79
 
68
80
  yield
69
81
  ensure
70
82
  disconnect!
71
- db_config['schema_search_path'] = original_search_paths
83
+ db_config[:schema_search_path] = original_search_paths
72
84
  establish_connection(db_config)
73
85
  end
74
86
 
75
87
  def self.schema_exists?(schema)
76
88
  ActiveRecord::Base.connection.execute(
77
- "SELECT schema_name FROM information_schema.schemata WHERE schema_name like '#{schema}'"
89
+ "SELECT schema_name FROM information_schema.schemata WHERE schema_name like '#{schema}'",
78
90
  ).count == 1
79
91
  end
80
92
 
93
+ def self.configuration_hash
94
+ if Gem.loaded_specs['activesupport'].version >= Gem::Version.create('6.1.0')
95
+ ActiveRecord::Base.connection_db_config.configuration_hash
96
+ else
97
+ ActiveRecord::Base.connection_config
98
+ end
99
+ end
100
+
81
101
  def schema_exists?(schema)
82
102
  self.class.schema_exists?(schema)
83
103
  end
@@ -94,9 +114,11 @@ module Sequent
94
114
  self.class.execute_sql(sql)
95
115
  end
96
116
 
97
- def migrate(migrations_path, verbose: true)
117
+ def migrate(migrations_path, schema_migration: ActiveRecord::SchemaMigration, verbose: true)
98
118
  ActiveRecord::Migration.verbose = verbose
99
- if ActiveRecord::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR >= 2
119
+ if ActiveRecord::VERSION::MAJOR >= 6
120
+ ActiveRecord::MigrationContext.new([migrations_path], schema_migration).up
121
+ elsif ActiveRecord::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR >= 2
100
122
  ActiveRecord::MigrationContext.new([migrations_path]).up
101
123
  else
102
124
  ActiveRecord::Migrator.migrate(migrations_path)
@@ -1,9 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'postgresql_cursor'
2
4
 
3
5
  module Sequent
4
6
  module Support
5
7
  class ViewProjection
6
8
  attr_reader :name, :version, :schema_definition
9
+
7
10
  def initialize(options)
8
11
  @name = options.fetch(:name)
9
12
  @version = options.fetch(:version)
@@ -20,7 +23,7 @@ module Sequent
20
23
  ordering = Events::ORDERED_BY_STREAM
21
24
  event_store.replay_events_from_cursor(
22
25
  block_size: 10_000,
23
- get_events: ->() { ordering[event_store] }
26
+ get_events: -> { ordering[event_store] },
24
27
  )
25
28
  end
26
29
  end
@@ -42,13 +45,13 @@ module Sequent
42
45
  module Events
43
46
  extend ActiveRecord::ConnectionAdapters::Quoting
44
47
 
45
- ORDERED_BY_STREAM = lambda do |event_store|
48
+ ORDERED_BY_STREAM = ->(_event_store) do
46
49
  event_records = quote_table_name(Sequent.configuration.event_record_class.table_name)
47
50
  stream_records = quote_table_name(Sequent.configuration.stream_record_class.table_name)
48
51
  snapshot_event_type = quote(Sequent.configuration.snapshot_event_class)
49
52
 
50
53
  Sequent.configuration.event_record_class
51
- .select("event_type, event_json")
54
+ .select('event_type, event_json')
52
55
  .joins("INNER JOIN #{stream_records} ON #{event_records}.stream_record_id = #{stream_records}.id")
53
56
  .where("event_type <> #{snapshot_event_type}")
54
57
  .order!("#{stream_records}.id, #{event_records}.sequence_number")
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Support
3
5
  class ViewSchema < ActiveRecord::Schema
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'support/database'
2
4
  require_relative 'support/view_projection'
3
5
  require_relative 'support/view_schema'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thread_safe'
2
4
  require 'sequent/core/event_store'
3
5
 
@@ -36,7 +38,6 @@ module Sequent
36
38
  #
37
39
  # end
38
40
  module CommandHandlerHelpers
39
-
40
41
  class FakeEventStore
41
42
  extend Forwardable
42
43
 
@@ -89,21 +90,31 @@ module Sequent
89
90
  end
90
91
 
91
92
  def stream_exists?(aggregate_id)
92
- @event_streams.has_key?(aggregate_id)
93
+ @event_streams.key?(aggregate_id)
94
+ end
95
+
96
+ def events_exists?(aggregate_id)
97
+ @event_streams[aggregate_id].present?
93
98
  end
94
99
 
95
100
  private
96
101
 
97
102
  def to_event_streams(events)
98
- # Specs use a simple list of given events. We need a mapping from StreamRecord to the associated events for the event store.
103
+ # Specs use a simple list of given events.
104
+ # We need a mapping from StreamRecord to the associated events for the event store.
99
105
  streams_by_aggregate_id = {}
100
106
  events.map do |event|
101
107
  event_stream = streams_by_aggregate_id.fetch(event.aggregate_id) do |aggregate_id|
102
108
  streams_by_aggregate_id[aggregate_id] =
103
109
  find_event_stream(aggregate_id) ||
104
110
  begin
105
- aggregate_type = FakeEventStore.aggregate_type_for_event(event)
106
- raise "cannot find aggregate type associated with creation event #{event}, did you include an event handler in your aggregate for this event?" unless aggregate_type
111
+ aggregate_type = aggregate_type_for_event(event)
112
+ unless aggregate_type
113
+ fail <<~EOS
114
+ Cannot find aggregate type associated with creation event #{event}, did you include an event handler in your aggregate for this event?
115
+ EOS
116
+ end
117
+
107
118
  Sequent::Core::EventStream.new(aggregate_type: aggregate_type.name, aggregate_id: aggregate_id)
108
119
  end
109
120
  end
@@ -111,10 +122,10 @@ module Sequent
111
122
  end
112
123
  end
113
124
 
114
- def self.aggregate_type_for_event(event)
125
+ def aggregate_type_for_event(event)
115
126
  @event_to_aggregate_type ||= ThreadSafe::Cache.new
116
127
  @event_to_aggregate_type.fetch_or_store(event.class) do |klass|
117
- Sequent::Core::AggregateRoot.descendants.find { |x| x.message_mapping.has_key?(klass) }
128
+ Sequent::Core::AggregateRoots.all.find { |x| x.message_mapping.key?(klass) }
118
129
  end
119
130
  end
120
131
 
@@ -129,30 +140,41 @@ module Sequent
129
140
  end
130
141
  end
131
142
 
132
- def given_events *events
143
+ def given_events(*events)
133
144
  Sequent.configuration.event_store.given_events(events.flatten(1))
134
145
  end
135
146
 
136
- def when_command command
147
+ def when_command(command)
137
148
  Sequent.configuration.command_service.execute_commands command
138
149
  end
139
150
 
140
151
  def then_events(*expected_events)
141
- expected_classes = expected_events.flatten(1).map { |event| event.class == Class ? event : event.class }
152
+ expected_classes = expected_events.flatten(1).map { |event| event.instance_of?(Class) ? event : event.class }
142
153
  expect(Sequent.configuration.event_store.stored_events.map(&:class)).to eq(expected_classes)
143
154
 
144
- Sequent.configuration.event_store.stored_events.zip(expected_events.flatten(1)).each_with_index do |(actual, expected), index|
145
- next if expected.class == Class
146
- _actual = Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(actual.payload))
147
- _expected = Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(expected.payload))
148
- expect(_actual).to eq(_expected), "#{index+1}th Event of type #{actual.class} not equal\nexpected: #{_expected.inspect}\n got: #{_actual.inspect}" if expected
149
- end
155
+ Sequent
156
+ .configuration
157
+ .event_store
158
+ .stored_events
159
+ .zip(expected_events.flatten(1))
160
+ .each_with_index do |(actual, expected), index|
161
+ next if expected.instance_of?(Class)
162
+
163
+ actual_hash = Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(actual.payload))
164
+ expected_hash = Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(expected.payload))
165
+ next unless expected
166
+
167
+ # rubocop:disable Layout/LineLength
168
+ expect(actual_hash)
169
+ .to eq(expected_hash),
170
+ "#{index + 1}th Event of type #{actual.class} not equal\nexpected: #{expected_hash.inspect}\n got: #{actual_hash.inspect}"
171
+ # rubocop:enable Layout/LineLength
172
+ end
150
173
  end
151
174
 
152
175
  def then_no_events
153
176
  then_events
154
177
  end
155
-
156
178
  end
157
179
  end
158
180
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Test
3
5
  ##
@@ -26,7 +28,6 @@ module Sequent
26
28
  # end
27
29
  # end
28
30
  module WorkflowHelpers
29
-
30
31
  class FakeTransactionProvider
31
32
  def initialize
32
33
  @after_commit_blocks = []
@@ -55,12 +56,17 @@ module Sequent
55
56
  end
56
57
 
57
58
  def then_events(*expected_events)
58
- expected_classes = expected_events.flatten(1).map { |event| event.class == Class ? event : event.class }
59
+ expected_classes = expected_events.flatten(1).map { |event| event.instance_of?(Class) ? event : event.class }
59
60
  expect(Sequent.configuration.event_store.stored_events.map(&:class)).to eq(expected_classes)
60
61
 
61
62
  Sequent.configuration.event_store.stored_events.zip(expected_events.flatten(1)).each do |actual, expected|
62
- next if expected.class == Class
63
- expect(Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(actual.payload))).to eq(Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(expected.payload))) if expected
63
+ next if expected.instance_of?(Class)
64
+
65
+ next unless expected
66
+
67
+ expect(
68
+ Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(actual.payload)),
69
+ ).to eq(Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(expected.payload)))
64
70
  end
65
71
  end
66
72
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Test
3
5
  ##
@@ -41,12 +43,14 @@ module Sequent
41
43
  @events = []
42
44
  end
43
45
 
46
+ # rubocop:disable Style/MissingRespondToMissing
44
47
  def method_missing(name, *args, &block)
45
48
  args = prepare_arguments(args)
46
49
  @events << FactoryBot.build(name, *args, &block)
47
50
  end
51
+ # rubocop:enable Style/MissingRespondToMissing
48
52
 
49
- private
53
+ private
50
54
 
51
55
  def prepare_arguments(args)
52
56
  options = args.last.is_a?(Hash) ? args.pop : {}
@@ -68,10 +72,10 @@ module Sequent
68
72
  given_events(*event_stream(aggregate_id: aggregate_id, &block))
69
73
  end
70
74
 
71
- def self.included(spec)
75
+ def self.included(_spec)
72
76
  require 'factory_bot'
73
77
  rescue LoadError
74
- raise ArgumentError, "Factory bot is required to use the event stream helpers"
78
+ raise ArgumentError, 'Factory bot is required to use the event stream helpers'
75
79
  end
76
80
  end
77
81
  end
@@ -1,20 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Test
3
5
  module DateTimePatches
4
6
  module Normalize
5
7
  def normalize
6
- in_time_zone("UTC")
8
+ in_time_zone('UTC')
7
9
  end
8
10
  end
9
11
 
10
12
  module Compare
11
- alias_method :'___<=>', :<=>
13
+ # rubocop:disable Style/Alias
14
+ alias :'___<=>' :'<=>'
15
+ # rubocop:enable Style/Alias
12
16
 
13
17
  # omit nsec in datetime comparisons
14
18
  def <=>(other)
15
- if other && other.is_a?(DateTimePatches::Normalize)
19
+ if other&.is_a?(DateTimePatches::Normalize)
16
20
  result = normalize.iso8601 <=> other.normalize.iso8601
17
21
  return result unless result == 0
22
+
18
23
  # use usec here, which *truncates* the nsec (ie. like Postgres)
19
24
  return normalize.usec <=> other.normalize.usec
20
25
  end
@@ -35,6 +40,8 @@ class DateTime
35
40
  prepend Sequent::Test::DateTimePatches::Compare
36
41
  end
37
42
 
38
- class ActiveSupport::TimeWithZone
39
- prepend Sequent::Test::DateTimePatches::Normalize
43
+ module ActiveSupport
44
+ class TimeWithZone
45
+ prepend Sequent::Test::DateTimePatches::Normalize
46
+ end
40
47
  end
data/lib/sequent/test.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'sequent'
2
4
  require_relative 'test/command_handler_helpers'
3
5
  require_relative 'test/event_stream_helpers'
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test/command_handler_helpers'
4
+
5
+ module Sequent
6
+ module Util
7
+ ##
8
+ # Dry run provides the ability to inspect what would
9
+ # happen if the given commands would be executed
10
+ # without actually committing the results.
11
+ # You can inspect which commands are executed
12
+ # and what the resulting events would be
13
+ # with theSequent::Projector's and Sequent::Workflow's
14
+ # that would be invoked (without actually invoking them).
15
+ #
16
+ # Since the Workflow's are not actually invoked new commands
17
+ # resulting from this Workflow will of course not be in the result.
18
+ #
19
+ # Caution: Since the Sequent Configuration is shared between threads this method
20
+ # is not Thread safe.
21
+ #
22
+ # Example usage:
23
+ #
24
+ # result = Sequent.dry_run(create_foo_command, ping_foo_command)
25
+ #
26
+ # result.print(STDOUT)
27
+ #
28
+ module DryRun
29
+ EventInvokedHandler = Struct.new(:event, :handler)
30
+
31
+ ##
32
+ # Proxies the given EventStore implements commit_events
33
+ # that instead of publish and store just publishes the events.
34
+ class EventStoreProxy
35
+ attr_reader :command_with_events, :event_store
36
+
37
+ delegate :load_events_for_aggregates,
38
+ :load_events,
39
+ :publish_events,
40
+ :stream_exists?,
41
+ to: :event_store
42
+
43
+ def initialize(result)
44
+ @event_store = Sequent::Test::CommandHandlerHelpers::FakeEventStore.new
45
+ @command_with_events = {}
46
+ @result = result
47
+ end
48
+
49
+ def commit_events(command, streams_with_events)
50
+ event_store.commit_events(command, streams_with_events)
51
+
52
+ new_events = streams_with_events.flat_map { |_, events| events }
53
+ @result.published_command_with_events(command, new_events)
54
+ end
55
+ end
56
+
57
+ ##
58
+ # Records which Projector's and Workflow's are executed
59
+ #
60
+ class RecordingEventPublisher < Sequent::Core::EventPublisher
61
+ attr_reader :projectors, :workflows
62
+
63
+ def initialize(result)
64
+ super()
65
+ @result = result
66
+ end
67
+
68
+ def process_event(event)
69
+ Sequent.configuration.event_handlers.each do |handler|
70
+ next unless handler.class.handles_message?(event)
71
+
72
+ if handler.is_a?(Sequent::Workflow)
73
+ @result.invoked_workflow(EventInvokedHandler.new(event, handler.class))
74
+ elsif handler.is_a?(Sequent::Projector)
75
+ @result.invoked_projector(EventInvokedHandler.new(event, handler.class))
76
+ else
77
+ fail "Unrecognized event_handler #{handler.class} called for event #{event.class}"
78
+ end
79
+ rescue StandardError
80
+ raise PublishEventError.new(handler.class, event)
81
+ end
82
+ end
83
+ end
84
+
85
+ ##
86
+ # Contains the result of a dry run.
87
+ #
88
+ # @see #tree
89
+ # @see #print
90
+ #
91
+ class Result
92
+ EventCalledHandlers = Struct.new(:event, :projectors, :workflows)
93
+
94
+ def initialize
95
+ @command_with_events = {}
96
+ @event_invoked_projectors = []
97
+ @event_invoked_workflows = []
98
+ end
99
+
100
+ def invoked_projector(event_invoked_handler)
101
+ event_invoked_projectors << event_invoked_handler
102
+ end
103
+
104
+ def invoked_workflow(event_invoked_handler)
105
+ event_invoked_workflows << event_invoked_handler
106
+ end
107
+
108
+ def published_command_with_events(command, events)
109
+ command_with_events[command] = events
110
+ end
111
+
112
+ ##
113
+ # Returns the command with events as a tree structure.
114
+ #
115
+ # {
116
+ # command => [
117
+ # EventCalledHandlers,
118
+ # EventCalledHandlers,
119
+ # EventCalledHandlers,
120
+ # ]
121
+ # }
122
+ #
123
+ # The EventCalledHandlers contains an Event with the
124
+ # lists of `Sequent::Projector`s and `Sequent::Workflow`s
125
+ # that were called.
126
+ #
127
+ def tree
128
+ command_with_events.reduce({}) do |memo, (command, events)|
129
+ events_to_handlers = events.map do |event|
130
+ for_current_event = ->(pair) { pair.event == event }
131
+ EventCalledHandlers.new(
132
+ event,
133
+ event_invoked_projectors.select(&for_current_event).map(&:handler),
134
+ event_invoked_workflows.select(&for_current_event).map(&:handler),
135
+ )
136
+ end
137
+ memo[command] = events_to_handlers
138
+ memo
139
+ end
140
+ end
141
+
142
+ ##
143
+ # Prints the output from #tree to the given `io`
144
+ #
145
+ def print(io)
146
+ tree.each_with_index do |(command, event_called_handlerss), index|
147
+ io.puts '+++++++++++++++++++++++++++++++++++' if index == 0
148
+ io.puts "Command: #{command.class} resulted in #{event_called_handlerss.length} events"
149
+ event_called_handlerss.each_with_index do |event_called_handlers, i|
150
+ io.puts '' if i > 0
151
+ io.puts "-- Event #{event_called_handlers.event.class} was handled by:"
152
+ io.puts "-- Projectors: [#{event_called_handlers.projectors.join(', ')}]"
153
+ io.puts "-- Workflows: [#{event_called_handlers.workflows.join(', ')}]"
154
+ end
155
+
156
+ io.puts '+++++++++++++++++++++++++++++++++++'
157
+ end
158
+ end
159
+
160
+ private
161
+
162
+ attr_reader :command_with_events, :event_invoked_projectors, :event_invoked_workflows
163
+ end
164
+
165
+ ##
166
+ # Main method of the DryRun.
167
+ #
168
+ # Caution: Since the Sequent Configuration is changed and is shared between threads this method
169
+ # is not Thread safe.
170
+ #
171
+ # After invocation the sequent configuration is reset to the state it was before
172
+ # invoking this method.
173
+ #
174
+ # @param commands - the commands to dry run
175
+ # @return Result - the Result of the dry run. See Result.
176
+ #
177
+ def self.these_commands(commands)
178
+ current_event_store = Sequent.configuration.event_store
179
+ current_event_publisher = Sequent.configuration.event_publisher
180
+ result = Result.new
181
+
182
+ Sequent.configuration.event_store = EventStoreProxy.new(result)
183
+ Sequent.configuration.event_publisher = RecordingEventPublisher.new(result)
184
+
185
+ Sequent.command_service.execute_commands(*commands)
186
+
187
+ result
188
+ ensure
189
+ Sequent.configuration.event_store = current_event_store
190
+ Sequent.configuration.event_publisher = current_event_publisher
191
+ end
192
+ end
193
+ end
194
+ end
@@ -1,16 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Util
3
5
  module Printer
4
6
  def recursively_print(e)
5
- logger.error "#{e.to_s}\n\n#{e.backtrace.join("\n")}"
7
+ logger.error "#{e}\n\n#{e.backtrace.join("\n")}"
6
8
 
7
- while e.cause do
8
- logger.error "+++++++++++++++ CAUSE +++++++++++++++"
9
- logger.error "#{e.cause.to_s}\n\n#{e.cause.backtrace.join("\n")}"
9
+ while e.cause
10
+ logger.error '+++++++++++++++ CAUSE +++++++++++++++'
11
+ logger.error "#{e.cause}\n\n#{e.cause.backtrace.join("\n")}"
10
12
  e = e.cause
11
13
  end
12
14
  end
13
15
  end
14
16
  end
15
17
  end
16
-
@@ -1,15 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Util
3
- def self.skip_if_already_processing(already_processing_key, &block)
4
- return if Thread.current[already_processing_key]
5
+ ##
6
+ # Returns if the current Thread is already processing work
7
+ # given the +processing_key+ otherwise
8
+ # it yields the given +&block+.
9
+ #
10
+ # Useful in a Queue and Processing strategy
11
+ def self.skip_if_already_processing(processing_key)
12
+ return if Thread.current[processing_key]
5
13
 
6
14
  begin
7
- Thread.current[already_processing_key] = true
15
+ Thread.current[processing_key] = true
8
16
 
9
- block.yield
17
+ yield
10
18
  ensure
11
- Thread.current[already_processing_key] = nil
19
+ Thread.current[processing_key] = nil
12
20
  end
13
21
  end
22
+
23
+ ##
24
+ # Reset the given +processing_key+ for the current Thread.
25
+ #
26
+ # Usefull to make a block protected by +skip_if_already_processing+ reentrant.
27
+ def self.done_processing(processing_key)
28
+ Thread.current[processing_key] = nil
29
+ end
14
30
  end
15
31
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Util
3
5
  module Timer
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'skip_if_already_processing'
2
4
  require_relative 'timer'
3
5
  require_relative 'printer'
6
+ require_relative 'dry_run'
data/lib/sequent.rb CHANGED
@@ -1,5 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'sequent/application_record'
2
4
  require_relative 'sequent/sequent'
3
5
  require_relative 'sequent/core/core'
4
6
  require_relative 'sequent/util/util'
5
7
  require_relative 'sequent/migrations/migrations'
8
+
9
+ require_relative 'notices'