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,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)