sequent 3.2.2 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/notices.rb +4 -0
  3. data/lib/sequent.rb +3 -0
  4. data/lib/sequent/application_record.rb +7 -0
  5. data/lib/sequent/configuration.rb +13 -0
  6. data/lib/sequent/core/aggregate_repository.rb +7 -1
  7. data/lib/sequent/core/command.rb +13 -2
  8. data/lib/sequent/core/command_record.rb +5 -2
  9. data/lib/sequent/core/command_service.rb +28 -12
  10. data/lib/sequent/core/event_publisher.rb +4 -0
  11. data/lib/sequent/core/event_record.rb +2 -1
  12. data/lib/sequent/core/event_store.rb +23 -4
  13. data/lib/sequent/core/helpers/attribute_support.rb +28 -7
  14. data/lib/sequent/core/helpers/mergable.rb +1 -0
  15. data/lib/sequent/core/persistors/active_record_persistor.rb +1 -1
  16. data/lib/sequent/core/persistors/replay_optimized_postgres_persistor.rb +2 -2
  17. data/lib/sequent/core/projector.rb +23 -1
  18. data/lib/sequent/core/stream_record.rb +1 -1
  19. data/lib/sequent/core/transactions/active_record_transaction_provider.rb +6 -4
  20. data/lib/sequent/generator.rb +1 -4
  21. data/lib/sequent/generator/generator.rb +4 -0
  22. data/lib/sequent/generator/project.rb +1 -1
  23. data/lib/sequent/generator/template_project/Gemfile +1 -1
  24. data/lib/sequent/generator/template_project/app/records/post_record.rb +1 -1
  25. data/lib/sequent/generator/template_project/spec/app/projectors/post_projector_spec.rb +1 -1
  26. data/lib/sequent/generator/template_project/spec/lib/post/post_command_handler_spec.rb +1 -1
  27. data/lib/sequent/migrations/executor.rb +78 -0
  28. data/lib/sequent/migrations/functions.rb +76 -0
  29. data/lib/sequent/migrations/migrations.rb +1 -0
  30. data/lib/sequent/migrations/planner.rb +118 -0
  31. data/lib/sequent/migrations/projectors.rb +6 -5
  32. data/lib/sequent/migrations/sql.rb +17 -0
  33. data/lib/sequent/migrations/view_schema.rb +74 -73
  34. data/lib/sequent/rake/migration_tasks.rb +2 -2
  35. data/lib/sequent/rake/tasks.rb +1 -1
  36. data/lib/sequent/sequent.rb +5 -1
  37. data/lib/sequent/support/database.rb +11 -6
  38. data/lib/sequent/test/command_handler_helpers.rb +4 -0
  39. data/lib/sequent/util/dry_run.rb +191 -0
  40. data/lib/sequent/util/skip_if_already_processing.rb +19 -5
  41. data/lib/sequent/util/util.rb +1 -0
  42. data/lib/version.rb +1 -1
  43. metadata +77 -36
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10b5839af491a1fc854e512051185f6141b6bff465d4aea09fe7c6f20940ba4e
4
- data.tar.gz: 32bccd0cc5b44a760e544a48b3be9578abcc9f9de7fbd083ca9ed6f38c5e5932
3
+ metadata.gz: 3322a4b950e847a5818555b2718df70e2b81a8e398074309a0b58c2a8d332ce5
4
+ data.tar.gz: 6136697a8a9596e999fabf3814509428b06188a0ac577fa3443a80ec190cbdd7
5
5
  SHA512:
6
- metadata.gz: 6c45431a49ad4100a7ad0ebe812aecb75144a9bf847823268a98b85c96689948e83fcfa76fd1d0c66dea5dabe923d41ab820ceefeb07cf2605d2b98296ebe7d6
7
- data.tar.gz: 3198cc99cf9bfa8ba955aecc4fd85fd7559f848a83c64c77d7a7092fafeb7c2aadfec4e31f587c929304537e306e2f5cebfcbbd780c14700c9b384daeaede68e
6
+ metadata.gz: f8ce675ae0a16274630066192086fb8c46bb0306e9280fd4b8901500af4a7af56844c5d6c40cd0f45a87342c33a4faeb2eee48f0dc0fb05a0a09046a2dea7414
7
+ data.tar.gz: e750e54d72de1c8641a513839c675adeb32fbeaa3435528a8aca755a951a7572d696e9348b7594e104f044978a0e9780f8e080ff1ad5e4ea9b7cc203fac6fee2
data/lib/notices.rb ADDED
@@ -0,0 +1,4 @@
1
+ # This file is for any notices such as deprecation warnings, which should appear
2
+ # in the logs during app boot. Adding such warnings in other places causes
3
+ # lots of noise with duplicated messages, whereas this file is only
4
+ # run once.
data/lib/sequent.rb CHANGED
@@ -1,4 +1,7 @@
1
+ require_relative 'sequent/application_record'
1
2
  require_relative 'sequent/sequent'
2
3
  require_relative 'sequent/core/core'
3
4
  require_relative 'sequent/util/util'
4
5
  require_relative 'sequent/migrations/migrations'
6
+
7
+ require_relative 'notices'
@@ -0,0 +1,7 @@
1
+ require 'active_record'
2
+
3
+ module Sequent
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -13,6 +13,7 @@ module Sequent
13
13
 
14
14
  DEFAULT_MIGRATION_SQL_FILES_DIRECTORY = 'db/tables'
15
15
  DEFAULT_DATABASE_CONFIG_DIRECTORY = 'db'
16
+ DEFAULT_DATABASE_SCHEMA_DIRECTORY = 'db'
16
17
 
17
18
  DEFAULT_VIEW_SCHEMA_NAME = 'view_schema'
18
19
  DEFAULT_EVENT_STORE_SCHEMA_NAME= 'sequent_schema'
@@ -26,6 +27,10 @@ module Sequent
26
27
 
27
28
  DEFAULT_EVENT_RECORD_HOOKS_CLASS = Sequent::Core::EventRecordHooks
28
29
 
30
+ DEFAULT_STRICT_CHECK_ATTRIBUTES_ON_APPLY_EVENTS = false
31
+
32
+ DEFAULT_ERROR_LOCALE_RESOLVER = -> { I18n.locale || :en }
33
+
29
34
  attr_accessor :aggregate_repository
30
35
 
31
36
  attr_accessor :event_store,
@@ -49,14 +54,19 @@ module Sequent
49
54
 
50
55
  attr_accessor :logger
51
56
 
57
+ attr_accessor :error_locale_resolver
58
+
52
59
  attr_accessor :migration_sql_files_directory,
53
60
  :view_schema_name,
54
61
  :offline_replay_persistor_class,
55
62
  :online_replay_persistor_class,
56
63
  :number_of_replay_processes,
57
64
  :database_config_directory,
65
+ :database_schema_directory,
58
66
  :event_store_schema_name
59
67
 
68
+ attr_accessor :strict_check_attributes_on_apply_events
69
+
60
70
  attr_reader :migrations_class_name,
61
71
  :versions_table_name,
62
72
  :replayed_ids_table_name
@@ -101,8 +111,11 @@ module Sequent
101
111
  self.offline_replay_persistor_class = DEFAULT_OFFLINE_REPLAY_PERSISTOR_CLASS
102
112
  self.online_replay_persistor_class = DEFAULT_ONLINE_REPLAY_PERSISTOR_CLASS
103
113
  self.database_config_directory = DEFAULT_DATABASE_CONFIG_DIRECTORY
114
+ self.database_schema_directory = DEFAULT_DATABASE_SCHEMA_DIRECTORY
115
+ self.strict_check_attributes_on_apply_events = DEFAULT_STRICT_CHECK_ATTRIBUTES_ON_APPLY_EVENTS
104
116
 
105
117
  self.logger = Logger.new(STDOUT).tap {|l| l.level = Logger::INFO }
118
+ self.error_locale_resolver = DEFAULT_ERROR_LOCALE_RESOLVER
106
119
  end
107
120
 
108
121
  def replayed_ids_table_name=(table_name)
@@ -100,11 +100,17 @@ module Sequent
100
100
  ##
101
101
  # Returns whether the event store has an aggregate with the given id
102
102
  def contains_aggregate?(aggregate_id)
103
- Sequent.configuration.event_store.stream_exists?(aggregate_id)
103
+ Sequent.configuration.event_store.stream_exists?(aggregate_id) &&
104
+ Sequent.configuration.event_store.events_exists?(aggregate_id)
104
105
  end
105
106
 
106
107
  # Gets all uncommitted_events from the 'registered' aggregates
107
108
  # and stores them in the event store.
109
+ #
110
+ # The events given to the EventStore are ordered in loading order
111
+ # of the different AggregateRoot's. So Events are stored
112
+ # (and therefore published) in order in which they are `apply`-ed per AggregateRoot.
113
+ #
108
114
  # The command is 'attached' for traceability purpose so we can see
109
115
  # which command resulted in which events.
110
116
  #
@@ -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")
@@ -32,7 +32,7 @@ module Sequent
32
32
  end
33
33
 
34
34
  # For storing Sequent::Core::Command in the database using active_record
35
- class CommandRecord < ActiveRecord::Base
35
+ class CommandRecord < Sequent::ApplicationRecord
36
36
  include SerializesCommand
37
37
 
38
38
  self.table_name = "command_records"
@@ -42,7 +42,10 @@ module Sequent
42
42
  validates_presence_of :command_type, :command_json
43
43
 
44
44
  def parent
45
- EventRecord.find_by(aggregate_id: event_aggregate_id, sequence_number: event_sequence_number)
45
+ EventRecord
46
+ .where(aggregate_id: event_aggregate_id, sequence_number: event_sequence_number)
47
+ .where('event_type != ?', Sequent::Core::SnapshotEvent.name)
48
+ .first
46
49
  end
47
50
 
48
51
  def children
@@ -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
 
@@ -45,6 +52,7 @@ module Sequent
45
52
  while(!command_queue.empty?) do
46
53
  process_command(command_queue.pop)
47
54
  end
55
+ Sequent::Util.done_processing(:command_service_process_commands)
48
56
  end
49
57
  ensure
50
58
  command_queue.clear
@@ -54,9 +62,16 @@ module Sequent
54
62
  end
55
63
 
56
64
  def process_command(command)
65
+ fail ArgumentError, 'command is required' if command.nil?
66
+
67
+ Sequent.logger.debug("[CommandService] Processing command #{command.class}")
68
+
57
69
  filters.each { |filter| filter.execute(command) }
58
70
 
59
- raise CommandNotValid.new(command) unless command.valid?
71
+ I18n.with_locale(Sequent.configuration.error_locale_resolver.call) do
72
+ raise CommandNotValid.new(command) unless command.valid?
73
+ end
74
+
60
75
  parsed_command = command.parse_attrs_to_correct_types
61
76
  command_handlers.select { |h| h.class.handles_message?(parsed_command) }.each { |h| h.handle_message parsed_command }
62
77
  repository.commit(parsed_command)
@@ -89,6 +104,7 @@ module Sequent
89
104
 
90
105
  # Raised when BaseCommand.valid? returns false
91
106
  class CommandNotValid < ArgumentError
107
+ attr_reader :command
92
108
 
93
109
  def initialize(command)
94
110
  @command = 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
@@ -1,5 +1,6 @@
1
1
  require 'active_record'
2
2
  require_relative 'sequent_oj'
3
+ require_relative '../application_record.rb'
3
4
 
4
5
  module Sequent
5
6
  module Core
@@ -72,7 +73,7 @@ module Sequent
72
73
  end
73
74
  end
74
75
 
75
- class EventRecord < ActiveRecord::Base
76
+ class EventRecord < Sequent::ApplicationRecord
76
77
  include SerializesEvent
77
78
 
78
79
  self.table_name = "event_records"
@@ -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
@@ -80,6 +88,9 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
80
88
  Sequent.configuration.stream_record_class.exists?(aggregate_id: aggregate_id)
81
89
  end
82
90
 
91
+ def events_exists?(aggregate_id)
92
+ Sequent.configuration.event_record_class.exists?(aggregate_id: aggregate_id)
93
+ end
83
94
  ##
84
95
  # Replays all events in the event store to the registered event_handlers.
85
96
  #
@@ -158,7 +169,15 @@ SELECT aggregate_id
158
169
  private
159
170
 
160
171
  def column_names
161
- @column_names ||= Sequent.configuration.event_record_class.column_names.reject { |c| c == 'id' }
172
+ @column_names ||= Sequent
173
+ .configuration
174
+ .event_record_class
175
+ .column_names
176
+ .reject { |c| c == primary_key_event_records }
177
+ end
178
+
179
+ def primary_key_event_records
180
+ @primary_key_event_records ||= Sequent.configuration.event_record_class.primary_key
162
181
  end
163
182
 
164
183
  def deserialize_event(event_hash)
@@ -200,7 +219,7 @@ SELECT aggregate_id
200
219
  .join(',')
201
220
  columns = column_names.map { |c| connection.quote_column_name(c) }.join(',')
202
221
  sql = %Q{insert into #{connection.quote_table_name(Sequent.configuration.event_record_class.table_name)} (#{columns}) values #{values}}
203
- Sequent.configuration.event_record_class.connection.insert(sql)
222
+ Sequent.configuration.event_record_class.connection.insert(sql, nil, primary_key_event_records)
204
223
  rescue ActiveRecord::RecordNotUnique
205
224
  fail OptimisticLockingError.new
206
225
  end
@@ -9,17 +9,33 @@ 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
37
+ class UnknownAttributeError < StandardError; end
38
+
23
39
  # module containing class methods to be added
24
40
  module ClassMethods
25
41
 
@@ -60,6 +76,7 @@ module Sequent
60
76
  class_eval <<EOS
61
77
  def update_all_attributes(attrs)
62
78
  super if defined?(super)
79
+ ensure_known_attributes(attrs)
63
80
  #{@types.map { |attribute, _|
64
81
  "@#{attribute} = attrs[:#{attribute}]"
65
82
  }.join("\n ")}
@@ -157,9 +174,13 @@ EOS
157
174
  prefix ? HashWithIndifferentAccess[result.map { |k, v| ["#{prefix}_#{k}", v] }] : result
158
175
  end
159
176
 
160
- end
161
-
177
+ def ensure_known_attributes(attrs)
178
+ return unless Sequent.configuration.strict_check_attributes_on_apply_events
162
179
 
180
+ unknowns = attrs.keys.map(&:to_s) - self.class.types.keys.map(&:to_s)
181
+ raise UnknownAttributeError.new("#{self.class.name} does not specify attrs: #{unknowns.join(", ")}") if unknowns.any?
182
+ end
183
+ end
163
184
  end
164
185
  end
165
186
  end
@@ -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
@@ -104,7 +104,7 @@ module Sequent
104
104
  end
105
105
 
106
106
  def execute_sql(statement)
107
- ActiveRecord::Base.connection.execute(statement)
107
+ Sequent::ApplicationRecord.connection.execute(statement)
108
108
  end
109
109
 
110
110
  def commit
@@ -307,7 +307,7 @@ module Sequent
307
307
  end
308
308
 
309
309
  buf = ''
310
- conn = ActiveRecord::Base.connection.raw_connection
310
+ conn = Sequent::ApplicationRecord.connection.raw_connection
311
311
  copy_data = StringIO.new csv.string
312
312
  conn.transaction do
313
313
  conn.copy_data("COPY #{clazz.table_name} (#{column_names.join(",")}) FROM STDIN WITH csv") do
@@ -346,7 +346,7 @@ module Sequent
346
346
  private
347
347
 
348
348
  def cast_value_to_column_type(clazz, column_name, record)
349
- ActiveRecord::Base.connection.type_cast(record[column_name.to_sym], @column_cache[clazz.name][column_name])
349
+ Sequent::ApplicationRecord.connection.type_cast(record[column_name.to_sym], @column_cache[clazz.name][column_name])
350
350
  end
351
351
  end
352
352
  end
@@ -11,7 +11,26 @@ module Sequent
11
11
  end
12
12
 
13
13
  def managed_tables
14
- @managed_tables
14
+ @managed_tables || managed_tables_from_superclass
15
+ end
16
+
17
+ def manages_no_tables
18
+ @manages_no_tables = true
19
+ manages_tables *[]
20
+ end
21
+
22
+ def manages_no_tables?
23
+ !!@manages_no_tables || manages_no_tables_from_superclass?
24
+ end
25
+
26
+ private
27
+
28
+ def managed_tables_from_superclass
29
+ self.superclass.managed_tables if self.superclass.respond_to?(:managed_tables)
30
+ end
31
+
32
+ def manages_no_tables_from_superclass?
33
+ self.superclass.manages_no_tables? if self.superclass.respond_to?(:manages_no_tables?)
15
34
  end
16
35
  end
17
36
 
@@ -96,7 +115,10 @@ module Sequent
96
115
  :commit
97
116
 
98
117
  private
118
+
99
119
  def ensure_valid!
120
+ return if self.class.manages_no_tables?
121
+
100
122
  fail "A Projector must manage at least one table. Did you forget to add `managed_tables` to #{self.class.name}?" if self.class.managed_tables.nil? || self.class.managed_tables.empty?
101
123
  end
102
124
  end