sequent 3.3.1 → 4.1.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.
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,26 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'transactions/no_transactions'
2
4
  require_relative 'current_event'
3
5
 
4
6
  module Sequent
5
7
  module Core
6
8
  #
7
- # Single point in the application where subclasses of Sequent::Core::BaseCommand
8
- # are executed. This will initiate the entire flow of:
9
+ # Single point in the application to get something done in Sequent.
10
+ # The CommandService handles all subclasses Sequent::Core::BaseCommand. Most common
11
+ # use is to subclass `Sequent::Command`.
12
+ #
13
+ # The CommandService is available via the shortcut method `Sequent.command_service`
14
+ #
15
+ # To use the CommandService please use:
9
16
  #
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
17
+ # Sequent.command_service.execute_commands(...)
15
18
  #
16
19
  class CommandService
20
+ #
17
21
  # Executes the given commands in a single transactional block as implemented by the +transaction_provider+
18
22
  #
19
- # For each command:
23
+ # For each Command:
24
+ #
25
+ # * Validate command
26
+ # * Call Sequent::CommandHandler's listening to the given Command
27
+ # * Store and publish Events
28
+ # * Any new Command's (from e.g. workflows) are queued for processing in the same transaction
29
+ #
30
+ # At the end the transaction is committed and the AggregateRepository's Unit of Work is cleared.
20
31
  #
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
32
  def execute_commands(*commands)
25
33
  commands.each do |command|
26
34
  if command.respond_to?(:event_aggregate_id) && CurrentEvent.current
@@ -33,6 +41,7 @@ module Sequent
33
41
  end
34
42
 
35
43
  def remove_event_handler(clazz)
44
+ warn '[DEPRECATION] `remove_event_handler` is deprecated'
36
45
  event_store.remove_event_handler(clazz)
37
46
  end
38
47
 
@@ -40,25 +49,29 @@ module Sequent
40
49
 
41
50
  def process_commands
42
51
  Sequent::Util.skip_if_already_processing(:command_service_process_commands) do
43
- begin
44
- transaction_provider.transactional do
45
- while(!command_queue.empty?) do
46
- process_command(command_queue.pop)
47
- end
48
- end
49
- ensure
50
- command_queue.clear
51
- repository.clear
52
+ transaction_provider.transactional do
53
+ process_command(command_queue.pop) until command_queue.empty?
54
+ Sequent::Util.done_processing(:command_service_process_commands)
52
55
  end
56
+ ensure
57
+ command_queue.clear
58
+ repository.clear
53
59
  end
54
60
  end
55
61
 
56
62
  def process_command(command)
63
+ fail ArgumentError, 'command is required' if command.nil?
64
+
65
+ Sequent.logger.debug("[CommandService] Processing command #{command.class}")
66
+
57
67
  filters.each { |filter| filter.execute(command) }
58
68
 
59
- raise CommandNotValid.new(command) unless command.valid?
69
+ fail CommandNotValid, command unless command.valid?
70
+
60
71
  parsed_command = command.parse_attrs_to_correct_types
61
- command_handlers.select { |h| h.class.handles_message?(parsed_command) }.each { |h| h.handle_message parsed_command }
72
+ command_handlers.select do |h|
73
+ h.class.handles_message?(parsed_command)
74
+ end.each { |h| h.handle_message parsed_command }
62
75
  repository.commit(parsed_command)
63
76
  end
64
77
 
@@ -89,15 +102,18 @@ module Sequent
89
102
 
90
103
  # Raised when BaseCommand.valid? returns false
91
104
  class CommandNotValid < ArgumentError
105
+ attr_reader :command
92
106
 
93
107
  def initialize(command)
94
108
  @command = command
95
- msg = @command.respond_to?(:aggregate_id) ? " #{@command.aggregate_id}" : ""
96
- super "Invalid command #{@command.class.to_s}#{msg}, errors: #{@command.validation_errors}"
109
+ msg = @command.respond_to?(:aggregate_id) ? " #{@command.aggregate_id}" : ''
110
+ super "Invalid command #{@command.class}#{msg}, errors: #{@command.validation_errors}"
97
111
  end
98
112
 
99
113
  def errors(prefix = nil)
100
- @command.validation_errors(prefix)
114
+ I18n.with_locale(Sequent.configuration.error_locale_resolver.call) do
115
+ @command.validation_errors(prefix)
116
+ end
101
117
  end
102
118
 
103
119
  def errors_with_command_prefix
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'sequent_oj'
2
4
  require_relative 'helpers/helpers'
3
5
  require_relative 'persistors/persistors'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Core
3
5
  class CurrentEvent
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_model'
2
4
  require_relative 'helpers/string_support'
3
5
  require_relative 'helpers/equal_support'
@@ -7,16 +9,17 @@ require_relative 'helpers/copyable'
7
9
  module Sequent
8
10
  module Core
9
11
  class Event
10
- include Sequent::Core::Helpers::StringSupport,
11
- Sequent::Core::Helpers::EqualSupport,
12
- Sequent::Core::Helpers::AttributeSupport,
13
- Sequent::Core::Helpers::Copyable
12
+ include Sequent::Core::Helpers::Copyable
13
+ include Sequent::Core::Helpers::AttributeSupport
14
+ include Sequent::Core::Helpers::EqualSupport
15
+ include Sequent::Core::Helpers::StringSupport
14
16
  attrs aggregate_id: String, sequence_number: Integer, created_at: DateTime
15
17
 
16
18
  def initialize(args = {})
17
19
  update_all_attributes args
18
- raise "Missing aggregate_id" unless @aggregate_id
19
- raise "Missing sequence_number" unless @sequence_number
20
+ fail 'Missing aggregate_id' unless @aggregate_id
21
+ fail 'Missing sequence_number' unless @sequence_number
22
+
20
23
  @created_at ||= DateTime.now
21
24
  end
22
25
 
@@ -24,22 +27,24 @@ module Sequent
24
27
  result = {}
25
28
  instance_variables
26
29
  .reject { |k| payload_variables.include?(k) }
27
- .select { |k| self.class.types.keys.include?(to_attribute_name(k))}
30
+ .select { |k| self.class.types.keys.include?(to_attribute_name(k)) }
28
31
  .each do |k|
29
- result[k.to_s[1 .. -1].to_sym] = instance_variable_get(k)
32
+ result[k.to_s[1..-1].to_sym] = instance_variable_get(k)
30
33
  end
31
34
  result
32
35
  end
36
+
33
37
  protected
38
+
34
39
  def payload_variables
35
- %i{@aggregate_id @sequence_number @created_at}
40
+ %i[@aggregate_id @sequence_number @created_at]
36
41
  end
37
42
 
38
43
  private
44
+
39
45
  def to_attribute_name(instance_variable_name)
40
- instance_variable_name[1 .. -1].to_sym
46
+ instance_variable_name[1..-1].to_sym
41
47
  end
42
-
43
48
  end
44
49
 
45
50
  class SnapshotEvent < Event
@@ -1,17 +1,23 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Core
3
5
  #
4
- # EventPublisher ensures that, for every thread, events will be published in the order in which they are queued for publishing.
6
+ # EventPublisher ensures that, for every thread, events will be published
7
+ # in the order in which they are queued for publishing.
5
8
  #
6
- # This potentially introduces a wrinkle into your plans: You therefore should not split a "unit of work" across multiple threads.
9
+ # This potentially introduces a wrinkle into your plans:
10
+ # You therefore should not split a "unit of work" across multiple threads.
7
11
  #
8
- # If you want other behaviour, you are free to implement your own version of EventPublisher and configure Sequent to use it.
12
+ # If you want other behaviour, you are free to implement your own version of EventPublisher
13
+ # and configure Sequent to use it.
9
14
  #
10
15
  class EventPublisher
11
16
  class PublishEventError < RuntimeError
12
17
  attr_reader :event_handler_class, :event
13
18
 
14
19
  def initialize(event_handler_class, event)
20
+ super()
15
21
  @event_handler_class = event_handler_class
16
22
  @event = event
17
23
  end
@@ -23,6 +29,7 @@ module Sequent
23
29
 
24
30
  def publish_events(events)
25
31
  return if configuration.disable_event_handlers
32
+
26
33
  events.each { |event| events_queue.push(event) }
27
34
  process_events
28
35
  end
@@ -35,23 +42,21 @@ module Sequent
35
42
 
36
43
  def process_events
37
44
  Sequent::Util.skip_if_already_processing(:events_queue_lock) do
38
- begin
39
- while(!events_queue.empty?) do
40
- process_event(events_queue.pop)
41
- end
42
- ensure
43
- events_queue.clear
44
- end
45
+ process_event(events_queue.pop) until events_queue.empty?
46
+ ensure
47
+ events_queue.clear
45
48
  end
46
49
  end
47
50
 
48
51
  def process_event(event)
52
+ fail ArgumentError, 'event is required' if event.nil?
53
+
54
+ Sequent.logger.debug("[EventPublisher] Publishing event #{event.class}")
55
+
49
56
  configuration.event_handlers.each do |handler|
50
- begin
51
- handler.handle_message event
52
- rescue
53
- raise PublishEventError.new(handler.class, event)
54
- end
57
+ handler.handle_message event
58
+ rescue StandardError
59
+ raise PublishEventError.new(handler.class, event)
55
60
  end
56
61
  end
57
62
 
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
  require_relative 'sequent_oj'
3
- require_relative '../application_record.rb'
5
+ require_relative '../application_record'
4
6
 
5
7
  module Sequent
6
8
  module Core
7
-
8
9
  # == Event Record Hooks
9
10
  #
10
11
  # These hooks are called during the life cycle of
@@ -27,7 +28,6 @@ module Sequent
27
28
  # end
28
29
  # end
29
30
  class EventRecordHooks
30
-
31
31
  # Called after assigning Sequent's event attributes to the +event_record+.
32
32
  #
33
33
  # *Params*
@@ -42,13 +42,12 @@ module Sequent
42
42
  def self.after_serialization(event_record, event)
43
43
  # noop
44
44
  end
45
-
46
45
  end
47
46
 
48
47
  module SerializesEvent
49
48
  def event
50
- payload = Sequent::Core::Oj.strict_load(self.event_json)
51
- Class.const_get(self.event_type).deserialize_from_json(payload)
49
+ payload = Sequent::Core::Oj.strict_load(event_json)
50
+ Class.const_get(event_type).deserialize_from_json(payload)
52
51
  end
53
52
 
54
53
  def event=(event)
@@ -76,7 +75,7 @@ module Sequent
76
75
  class EventRecord < Sequent::ApplicationRecord
77
76
  include SerializesEvent
78
77
 
79
- self.table_name = "event_records"
78
+ self.table_name = 'event_records'
80
79
 
81
80
  belongs_to :stream_record
82
81
  belongs_to :command_record
@@ -98,6 +97,7 @@ module Sequent
98
97
 
99
98
  def find_origin(record)
100
99
  return find_origin(record.parent) if record.parent.present?
100
+
101
101
  record
102
102
  end
103
103
  end
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
  require_relative 'event_record'
3
5
  require_relative 'sequent_oj'
4
6
 
5
7
  module Sequent
6
8
  module Core
7
-
8
9
  class EventStore
9
10
  include ActiveRecord::ConnectionAdapters::Quoting
10
11
  extend Forwardable
@@ -16,13 +17,13 @@ module Sequent
16
17
  attr_reader :event_hash
17
18
 
18
19
  def initialize(event_hash)
20
+ super()
19
21
  @event_hash = event_hash
20
22
  end
21
23
 
22
24
  def message
23
25
  "Event hash: #{event_hash.inspect}\nCause: #{cause.inspect}"
24
26
  end
25
-
26
27
  end
27
28
 
28
29
  def initialize
@@ -33,10 +34,18 @@ module Sequent
33
34
  # Stores the events in the EventStore and publishes the events
34
35
  # to the registered event_handlers.
35
36
  #
36
- # Streams_with_Events is an enumerable of pairs from
37
- # `StreamRecord` to arrays of uncommitted `Event`s.
37
+ # The events are published according to the order in
38
+ # the tail of the given `streams_with_events` array pair.
39
+ #
40
+ # @param command The command that caused the Events
41
+ # @param streams_with_events is an enumerable of pairs from
42
+ # `StreamRecord` to arrays ordered uncommitted `Event`s.
38
43
  #
39
44
  def commit_events(command, streams_with_events)
45
+ fail ArgumentError, 'command is required' if command.nil?
46
+
47
+ Sequent.logger.debug("[EventStore] Committing events for command #{command.class}")
48
+
40
49
  store_events(command, streams_with_events)
41
50
  publish_events(streams_with_events.flat_map { |_, events| events })
42
51
  end
@@ -53,40 +62,52 @@ module Sequent
53
62
 
54
63
  streams = Sequent.configuration.stream_record_class.where(aggregate_id: aggregate_ids)
55
64
 
56
- query = aggregate_ids.uniq.map { |aggregate_id| aggregate_query(aggregate_id) }.join(" UNION ALL ")
57
- events = Sequent.configuration.event_record_class.connection.select_all(query).map! do |event_hash|
65
+ query = aggregate_ids.uniq.map { |aggregate_id| aggregate_query(aggregate_id) }.join(' UNION ALL ')
66
+ events = Sequent.configuration.event_record_class.connection.select_all(query).map do |event_hash|
58
67
  deserialize_event(event_hash)
59
68
  end
60
69
 
61
70
  events
62
- .group_by { |event| event.aggregate_id }
63
- .map { |aggregate_id, _events| [streams.find { |stream_record| stream_record.aggregate_id == aggregate_id }.event_stream, _events] }
71
+ .group_by(&:aggregate_id)
72
+ .map do |aggregate_id, es|
73
+ [
74
+ streams.find do |stream_record|
75
+ stream_record.aggregate_id == aggregate_id
76
+ end.event_stream,
77
+ es,
78
+ ]
79
+ end
64
80
  end
65
81
 
66
82
  def aggregate_query(aggregate_id)
67
- %Q{(
68
- SELECT event_type, event_json
69
- FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS o
70
- WHERE aggregate_id = #{quote(aggregate_id)}
71
- AND sequence_number >= COALESCE((SELECT MAX(sequence_number)
72
- FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS i
73
- WHERE event_type = #{quote Sequent.configuration.snapshot_event_class.name}
74
- AND i.aggregate_id = #{quote(aggregate_id)}), 0)
75
- ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuration.snapshot_event_class.name} THEN 0 ELSE 1 END) ASC
76
- )}
83
+ <<~SQL.chomp
84
+ (
85
+ SELECT event_type, event_json
86
+ FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS o
87
+ WHERE aggregate_id = #{quote(aggregate_id)}
88
+ AND sequence_number >= COALESCE((SELECT MAX(sequence_number)
89
+ FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS i
90
+ WHERE event_type = #{quote Sequent.configuration.snapshot_event_class.name}
91
+ AND i.aggregate_id = #{quote(aggregate_id)}), 0)
92
+ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuration.snapshot_event_class.name} THEN 0 ELSE 1 END) ASC
93
+ )
94
+ SQL
77
95
  end
78
96
 
79
97
  def stream_exists?(aggregate_id)
80
98
  Sequent.configuration.stream_record_class.exists?(aggregate_id: aggregate_id)
81
99
  end
82
100
 
101
+ def events_exists?(aggregate_id)
102
+ Sequent.configuration.event_record_class.exists?(aggregate_id: aggregate_id)
103
+ end
83
104
  ##
84
105
  # Replays all events in the event store to the registered event_handlers.
85
106
  #
86
107
  # @param block that returns the events.
87
108
  # <b>DEPRECATED:</b> use <tt>replay_events_from_cursor</tt> instead.
88
109
  def replay_events
89
- warn "[DEPRECATION] `replay_events` is deprecated in favor of `replay_events_from_cursor`"
110
+ warn '[DEPRECATION] `replay_events` is deprecated in favor of `replay_events_from_cursor`'
90
111
  events = yield.map { |event_hash| deserialize_event(event_hash) }
91
112
  publish_events(events)
92
113
  end
@@ -98,8 +119,7 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
98
119
  #
99
120
  # @param get_events lambda that returns the events cursor
100
121
  # @param on_progress lambda that gets called on substantial progress
101
- def replay_events_from_cursor(block_size: 2000,
102
- get_events:,
122
+ def replay_events_from_cursor(get_events:, block_size: 2000,
103
123
  on_progress: PRINT_PROGRESS)
104
124
  progress = 0
105
125
  cursor = get_events.call
@@ -117,7 +137,7 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
117
137
  on_progress[progress, true, ids_replayed]
118
138
  end
119
139
 
120
- PRINT_PROGRESS = lambda do |progress, done, _|
140
+ PRINT_PROGRESS = ->(progress, done, _) do
121
141
  if done
122
142
  Sequent.logger.debug "Done replaying #{progress} events"
123
143
  else
@@ -131,42 +151,46 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
131
151
  def aggregates_that_need_snapshots(last_aggregate_id, limit = 10)
132
152
  stream_table = quote_table_name Sequent.configuration.stream_record_class.table_name
133
153
  event_table = quote_table_name Sequent.configuration.event_record_class.table_name
134
- query = %Q{
135
- SELECT aggregate_id
136
- FROM #{stream_table} stream
137
- WHERE aggregate_id::varchar > COALESCE(#{quote last_aggregate_id}, '')
138
- AND snapshot_threshold IS NOT NULL
139
- AND snapshot_threshold <= (
140
- (SELECT MAX(events.sequence_number) FROM #{event_table} events WHERE events.event_type <> #{quote Sequent.configuration.snapshot_event_class.name} AND stream.aggregate_id = events.aggregate_id) -
141
- COALESCE((SELECT MAX(snapshots.sequence_number) FROM #{event_table} snapshots WHERE snapshots.event_type = #{quote Sequent.configuration.snapshot_event_class.name} AND stream.aggregate_id = snapshots.aggregate_id), 0))
142
- ORDER BY aggregate_id
143
- LIMIT #{quote limit}
144
- FOR UPDATE
145
- }
154
+ query = <<~SQL.chomp
155
+ SELECT aggregate_id
156
+ FROM #{stream_table} stream
157
+ WHERE aggregate_id::varchar > COALESCE(#{quote last_aggregate_id}, '')
158
+ AND snapshot_threshold IS NOT NULL
159
+ AND snapshot_threshold <= (
160
+ (SELECT MAX(events.sequence_number) FROM #{event_table} events WHERE events.event_type <> #{quote Sequent.configuration.snapshot_event_class.name} AND stream.aggregate_id = events.aggregate_id) -
161
+ COALESCE((SELECT MAX(snapshots.sequence_number) FROM #{event_table} snapshots WHERE snapshots.event_type = #{quote Sequent.configuration.snapshot_event_class.name} AND stream.aggregate_id = snapshots.aggregate_id), 0))
162
+ ORDER BY aggregate_id
163
+ LIMIT #{quote limit}
164
+ FOR UPDATE
165
+ SQL
146
166
  Sequent.configuration.event_record_class.connection.select_all(query).map { |x| x['aggregate_id'] }
147
167
  end
148
168
 
149
169
  def find_event_stream(aggregate_id)
150
170
  record = Sequent.configuration.stream_record_class.where(aggregate_id: aggregate_id).first
151
- if record
152
- record.event_stream
153
- else
154
- nil
155
- end
171
+ record&.event_stream
156
172
  end
157
173
 
158
174
  private
159
175
 
160
176
  def column_names
161
- @column_names ||= Sequent.configuration.event_record_class.column_names.reject { |c| c == 'id' }
177
+ @column_names ||= Sequent
178
+ .configuration
179
+ .event_record_class
180
+ .column_names
181
+ .reject { |c| c == primary_key_event_records }
182
+ end
183
+
184
+ def primary_key_event_records
185
+ @primary_key_event_records ||= Sequent.configuration.event_record_class.primary_key
162
186
  end
163
187
 
164
188
  def deserialize_event(event_hash)
165
- event_type = event_hash.fetch("event_type")
166
- event_json = Sequent::Core::Oj.strict_load(event_hash.fetch("event_json"))
189
+ event_type = event_hash.fetch('event_type')
190
+ event_json = Sequent::Core::Oj.strict_load(event_hash.fetch('event_json'))
167
191
  resolve_event_type(event_type).deserialize_from_json(event_json)
168
- rescue
169
- raise DeserializeEventError.new(event_hash)
192
+ rescue StandardError
193
+ raise DeserializeEventError, event_hash
170
194
  end
171
195
 
172
196
  def resolve_event_type(event_type)
@@ -196,13 +220,15 @@ SELECT aggregate_id
196
220
  end
197
221
  connection = Sequent.configuration.event_record_class.connection
198
222
  values = event_records
199
- .map { |r| "(#{column_names.map { |c| connection.quote(r[c.to_sym]) }.join(',')})" }
200
- .join(',')
223
+ .map { |r| "(#{column_names.map { |c| connection.quote(r[c.to_sym]) }.join(',')})" }
224
+ .join(',')
201
225
  columns = column_names.map { |c| connection.quote_column_name(c) }.join(',')
202
- 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)
226
+ sql = <<~SQL.chomp
227
+ insert into #{connection.quote_table_name(Sequent.configuration.event_record_class.table_name)} (#{columns}) values #{values}
228
+ SQL
229
+ Sequent.configuration.event_record_class.connection.insert(sql, nil, primary_key_event_records)
204
230
  rescue ActiveRecord::RecordNotUnique
205
- fail OptimisticLockingError.new
231
+ raise OptimisticLockingError
206
232
  end
207
233
  end
208
234
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Symbol
2
4
  def self.deserialize_from_json(value)
3
5
  value.blank? ? nil : value.try(:to_sym)
@@ -25,19 +27,25 @@ end
25
27
  class BigDecimal
26
28
  def self.deserialize_from_json(value)
27
29
  return nil if value.nil?
30
+
28
31
  BigDecimal(value)
29
32
  end
30
33
  end
31
34
 
32
35
  module Boolean
33
36
  def self.deserialize_from_json(value)
34
- value.nil? ? nil : (value.present? ? value : false)
37
+ if value.nil?
38
+ nil
39
+ else
40
+ (value.present? ? value : false)
41
+ end
35
42
  end
36
43
  end
37
44
 
38
45
  class Date
39
46
  def self.from_params(value)
40
47
  return value if value.is_a?(Date)
48
+
41
49
  value.blank? ? nil : Date.iso8601(value.dup)
42
50
  rescue ArgumentError
43
51
  value
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Core
3
5
  module Helpers
@@ -5,7 +7,8 @@ module Sequent
5
7
  attr_accessor :item_type
6
8
 
7
9
  def initialize(item_type)
8
- raise "needs a item_type" unless item_type
10
+ fail 'needs a item_type' unless item_type
11
+
9
12
  @item_type = item_type
10
13
  end
11
14
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_model/validator'
2
4
 
3
5
  module Sequent
@@ -21,24 +23,23 @@ module Sequent
21
23
  # validates_with Sequent::Core::AssociationValidator, associations: [:trainee]
22
24
  #
23
25
  class AssociationValidator < ActiveModel::Validator
24
-
25
26
  def initialize(options = {})
26
27
  super
27
- raise "Must provide ':associations' to validate" unless options[:associations].present?
28
+ fail "Must provide ':associations' to validate" unless options[:associations].present?
28
29
  end
29
30
 
30
31
  def validate(record)
31
32
  associations = options[:associations]
32
33
  associations = [associations] unless associations.instance_of?(Array)
33
34
  associations.each do |association|
34
- value = record.instance_variable_get("@#{association.to_s}")
35
+ value = record.instance_variable_get("@#{association}")
35
36
  if value && incorrect_type?(value, record, association)
36
37
  record.errors.add(association, "is not of type #{describe_type(record.class.types[association])}")
37
- elsif value && value.is_a?(Array)
38
+ elsif value&.is_a?(Array)
38
39
  item_type = record.class.types.fetch(association).item_type
39
- record.errors.add(association, "is invalid") unless validate_all(value, item_type).all?
40
- else
41
- record.errors.add(association, "is invalid") if value && value.invalid?
40
+ record.errors.add(association, 'is invalid') unless validate_all(value, item_type).all?
41
+ elsif value&.invalid?
42
+ record.errors.add(association, 'is invalid')
42
43
  end
43
44
  end
44
45
  end
@@ -47,6 +48,7 @@ module Sequent
47
48
 
48
49
  def incorrect_type?(value, record, association)
49
50
  return unless record.class.respond_to?(:types)
51
+
50
52
  type = record.class.types[association]
51
53
  if type.respond_to?(:candidate?)
52
54
  !type.candidate?(value)