sequent 4.0.0 → 4.3.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 +33 -26
  3. data/lib/notices.rb +2 -0
  4. data/lib/sequent/application_record.rb +2 -0
  5. data/lib/sequent/configuration.rb +24 -31
  6. data/lib/sequent/core/aggregate_repository.rb +48 -13
  7. data/lib/sequent/core/aggregate_root.rb +36 -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 +17 -9
  12. data/lib/sequent/core/command_record.rb +8 -3
  13. data/lib/sequent/core/command_service.rb +18 -18
  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 +16 -15
  18. data/lib/sequent/core/event_record.rb +7 -7
  19. data/lib/sequent/core/event_store.rb +89 -51
  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 +45 -28
  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 -5
  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 +53 -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 +30 -9
  51. data/lib/sequent/core/transactions/no_transactions.rb +2 -1
  52. data/lib/sequent/core/transactions/read_only_active_record_transaction_provider.rb +46 -0
  53. data/lib/sequent/core/transactions/transactions.rb +3 -0
  54. data/lib/sequent/core/value_object.rb +8 -10
  55. data/lib/sequent/core/workflow.rb +35 -5
  56. data/lib/sequent/generator/aggregate.rb +16 -10
  57. data/lib/sequent/generator/command.rb +26 -19
  58. data/lib/sequent/generator/event.rb +19 -17
  59. data/lib/sequent/generator/generator.rb +2 -0
  60. data/lib/sequent/generator/project.rb +9 -0
  61. data/lib/sequent/generator/template_project/Gemfile +1 -1
  62. data/lib/sequent/generator/template_project/ruby-version +1 -0
  63. data/lib/sequent/generator.rb +2 -0
  64. data/lib/sequent/migrations/executor.rb +22 -13
  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 +84 -45
  72. data/lib/sequent/rake/migration_tasks.rb +58 -22
  73. data/lib/sequent/rake/tasks.rb +5 -2
  74. data/lib/sequent/sequent.rb +2 -0
  75. data/lib/sequent/support/database.rb +30 -15
  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 +35 -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 +28 -20
  85. data/lib/sequent/util/printer.rb +6 -5
  86. data/lib/sequent/util/skip_if_already_processing.rb +3 -1
  87. data/lib/sequent/util/timer.rb +2 -0
  88. data/lib/sequent/util/util.rb +2 -0
  89. data/lib/sequent.rb +2 -0
  90. data/lib/version.rb +3 -1
  91. metadata +84 -67
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'transactions/no_transactions'
2
4
  require_relative 'current_event'
3
5
 
@@ -39,7 +41,7 @@ module Sequent
39
41
  end
40
42
 
41
43
  def remove_event_handler(clazz)
42
- warn "[DEPRECATION] `remove_event_handler` is deprecated"
44
+ warn '[DEPRECATION] `remove_event_handler` is deprecated'
43
45
  event_store.remove_event_handler(clazz)
44
46
  end
45
47
 
@@ -47,17 +49,13 @@ module Sequent
47
49
 
48
50
  def process_commands
49
51
  Sequent::Util.skip_if_already_processing(:command_service_process_commands) do
50
- begin
51
- transaction_provider.transactional do
52
- while(!command_queue.empty?) do
53
- process_command(command_queue.pop)
54
- end
55
- Sequent::Util.done_processing(:command_service_process_commands)
56
- end
57
- ensure
58
- command_queue.clear
59
- 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)
60
55
  end
56
+ ensure
57
+ command_queue.clear
58
+ repository.clear
61
59
  end
62
60
  end
63
61
 
@@ -68,12 +66,12 @@ module Sequent
68
66
 
69
67
  filters.each { |filter| filter.execute(command) }
70
68
 
71
- I18n.with_locale(Sequent.configuration.error_locale_resolver.call) do
72
- raise CommandNotValid.new(command) unless command.valid?
73
- end
69
+ fail CommandNotValid, command unless command.valid?
74
70
 
75
71
  parsed_command = command.parse_attrs_to_correct_types
76
- 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 }
77
75
  repository.commit(parsed_command)
78
76
  end
79
77
 
@@ -108,12 +106,14 @@ module Sequent
108
106
 
109
107
  def initialize(command)
110
108
  @command = command
111
- msg = @command.respond_to?(:aggregate_id) ? " #{@command.aggregate_id}" : ""
112
- 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}"
113
111
  end
114
112
 
115
113
  def errors(prefix = nil)
116
- @command.validation_errors(prefix)
114
+ I18n.with_locale(Sequent.configuration.error_locale_resolver.call) do
115
+ @command.validation_errors(prefix)
116
+ end
117
117
  end
118
118
 
119
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,13 +42,9 @@ 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
 
@@ -51,11 +54,9 @@ module Sequent
51
54
  Sequent.logger.debug("[EventPublisher] Publishing event #{event.class}")
52
55
 
53
56
  configuration.event_handlers.each do |handler|
54
- begin
55
- handler.handle_message event
56
- rescue
57
- raise PublishEventError.new(handler.class, event)
58
- end
57
+ handler.handle_message event
58
+ rescue StandardError
59
+ raise PublishEventError.new(handler.class, event)
59
60
  end
60
61
  end
61
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
@@ -41,7 +42,7 @@ module Sequent
41
42
  # `StreamRecord` to arrays ordered uncommitted `Event`s.
42
43
  #
43
44
  def commit_events(command, streams_with_events)
44
- fail ArgumentError, "command is required" if command.nil?
45
+ fail ArgumentError, 'command is required' if command.nil?
45
46
 
46
47
  Sequent.logger.debug("[EventStore] Committing events for command #{command.class}")
47
48
 
@@ -50,7 +51,38 @@ module Sequent
50
51
  end
51
52
 
52
53
  ##
53
- # Returns all events for the aggregate ordered by sequence_number
54
+ # Returns all events for the AggregateRoot ordered by sequence_number, disregarding snapshot events.
55
+ #
56
+ # This streaming is done in batches to prevent loading many events in memory all at once. A usecase for ignoring
57
+ # the snapshots is when events of a nested AggregateRoot need to be loaded up until a certain moment in time.
58
+ #
59
+ # @param aggregate_id Aggregate id of the AggregateRoot
60
+ # @param load_until The timestamp up until which you want to built the aggregate. Optional.
61
+ # @param &block Block that should be passed to handle the batches returned from this method
62
+ def stream_events_for_aggregate(aggregate_id, load_until: nil, &block)
63
+ stream = find_event_stream(aggregate_id)
64
+ fail ArgumentError, 'no stream found for this aggregate' if stream.blank?
65
+
66
+ q = Sequent
67
+ .configuration
68
+ .event_record_class
69
+ .where(aggregate_id: aggregate_id)
70
+ .where.not(event_type: Sequent.configuration.snapshot_event_class.name)
71
+ .order(:sequence_number)
72
+ q = q.where('created_at < ?', load_until) if load_until.present?
73
+ has_events = false
74
+
75
+ q.select('event_type, event_json').each_row do |event_hash|
76
+ has_events = true
77
+ event = deserialize_event(event_hash)
78
+ block.call([stream, event])
79
+ end
80
+ fail ArgumentError, 'no events for this aggregate' unless has_events
81
+ end
82
+
83
+ ##
84
+ # Returns all events for the aggregate ordered by sequence_number, loading them from the latest snapshot
85
+ # event onwards, if a snapshot is present
54
86
  #
55
87
  def load_events(aggregate_id)
56
88
  load_events_for_aggregates([aggregate_id])[0]
@@ -61,27 +93,36 @@ module Sequent
61
93
 
62
94
  streams = Sequent.configuration.stream_record_class.where(aggregate_id: aggregate_ids)
63
95
 
64
- query = aggregate_ids.uniq.map { |aggregate_id| aggregate_query(aggregate_id) }.join(" UNION ALL ")
65
- events = Sequent.configuration.event_record_class.connection.select_all(query).map! do |event_hash|
96
+ query = aggregate_ids.uniq.map { |aggregate_id| aggregate_query(aggregate_id) }.join(' UNION ALL ')
97
+ events = Sequent.configuration.event_record_class.connection.select_all(query).map do |event_hash|
66
98
  deserialize_event(event_hash)
67
99
  end
68
100
 
69
101
  events
70
- .group_by { |event| event.aggregate_id }
71
- .map { |aggregate_id, _events| [streams.find { |stream_record| stream_record.aggregate_id == aggregate_id }.event_stream, _events] }
102
+ .group_by(&:aggregate_id)
103
+ .map do |aggregate_id, es|
104
+ [
105
+ streams.find do |stream_record|
106
+ stream_record.aggregate_id == aggregate_id
107
+ end.event_stream,
108
+ es,
109
+ ]
110
+ end
72
111
  end
73
112
 
74
113
  def aggregate_query(aggregate_id)
75
- %Q{(
76
- SELECT event_type, event_json
77
- FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS o
78
- WHERE aggregate_id = #{quote(aggregate_id)}
79
- AND sequence_number >= COALESCE((SELECT MAX(sequence_number)
80
- FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS i
81
- WHERE event_type = #{quote Sequent.configuration.snapshot_event_class.name}
82
- AND i.aggregate_id = #{quote(aggregate_id)}), 0)
83
- ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuration.snapshot_event_class.name} THEN 0 ELSE 1 END) ASC
84
- )}
114
+ <<~SQL.chomp
115
+ (
116
+ SELECT event_type, event_json
117
+ FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS o
118
+ WHERE aggregate_id = #{quote(aggregate_id)}
119
+ AND sequence_number >= COALESCE((SELECT MAX(sequence_number)
120
+ FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS i
121
+ WHERE event_type = #{quote Sequent.configuration.snapshot_event_class.name}
122
+ AND i.aggregate_id = #{quote(aggregate_id)}), 0)
123
+ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuration.snapshot_event_class.name} THEN 0 ELSE 1 END) ASC
124
+ )
125
+ SQL
85
126
  end
86
127
 
87
128
  def stream_exists?(aggregate_id)
@@ -97,7 +138,7 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
97
138
  # @param block that returns the events.
98
139
  # <b>DEPRECATED:</b> use <tt>replay_events_from_cursor</tt> instead.
99
140
  def replay_events
100
- warn "[DEPRECATION] `replay_events` is deprecated in favor of `replay_events_from_cursor`"
141
+ warn '[DEPRECATION] `replay_events` is deprecated in favor of `replay_events_from_cursor`'
101
142
  events = yield.map { |event_hash| deserialize_event(event_hash) }
102
143
  publish_events(events)
103
144
  end
@@ -109,8 +150,7 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
109
150
  #
110
151
  # @param get_events lambda that returns the events cursor
111
152
  # @param on_progress lambda that gets called on substantial progress
112
- def replay_events_from_cursor(block_size: 2000,
113
- get_events:,
153
+ def replay_events_from_cursor(get_events:, block_size: 2000,
114
154
  on_progress: PRINT_PROGRESS)
115
155
  progress = 0
116
156
  cursor = get_events.call
@@ -128,7 +168,7 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
128
168
  on_progress[progress, true, ids_replayed]
129
169
  end
130
170
 
131
- PRINT_PROGRESS = lambda do |progress, done, _|
171
+ PRINT_PROGRESS = ->(progress, done, _) do
132
172
  if done
133
173
  Sequent.logger.debug "Done replaying #{progress} events"
134
174
  else
@@ -142,38 +182,34 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
142
182
  def aggregates_that_need_snapshots(last_aggregate_id, limit = 10)
143
183
  stream_table = quote_table_name Sequent.configuration.stream_record_class.table_name
144
184
  event_table = quote_table_name Sequent.configuration.event_record_class.table_name
145
- query = %Q{
146
- SELECT aggregate_id
147
- FROM #{stream_table} stream
148
- WHERE aggregate_id::varchar > COALESCE(#{quote last_aggregate_id}, '')
149
- AND snapshot_threshold IS NOT NULL
150
- AND snapshot_threshold <= (
151
- (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) -
152
- 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))
153
- ORDER BY aggregate_id
154
- LIMIT #{quote limit}
155
- FOR UPDATE
156
- }
185
+ query = <<~SQL.chomp
186
+ SELECT aggregate_id
187
+ FROM #{stream_table} stream
188
+ WHERE aggregate_id::varchar > COALESCE(#{quote last_aggregate_id}, '')
189
+ AND snapshot_threshold IS NOT NULL
190
+ AND snapshot_threshold <= (
191
+ (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) -
192
+ 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))
193
+ ORDER BY aggregate_id
194
+ LIMIT #{quote limit}
195
+ FOR UPDATE
196
+ SQL
157
197
  Sequent.configuration.event_record_class.connection.select_all(query).map { |x| x['aggregate_id'] }
158
198
  end
159
199
 
160
200
  def find_event_stream(aggregate_id)
161
201
  record = Sequent.configuration.stream_record_class.where(aggregate_id: aggregate_id).first
162
- if record
163
- record.event_stream
164
- else
165
- nil
166
- end
202
+ record&.event_stream
167
203
  end
168
204
 
169
205
  private
170
206
 
171
207
  def column_names
172
208
  @column_names ||= Sequent
173
- .configuration
174
- .event_record_class
175
- .column_names
176
- .reject { |c| c == primary_key_event_records }
209
+ .configuration
210
+ .event_record_class
211
+ .column_names
212
+ .reject { |c| c == primary_key_event_records }
177
213
  end
178
214
 
179
215
  def primary_key_event_records
@@ -181,11 +217,11 @@ SELECT aggregate_id
181
217
  end
182
218
 
183
219
  def deserialize_event(event_hash)
184
- event_type = event_hash.fetch("event_type")
185
- event_json = Sequent::Core::Oj.strict_load(event_hash.fetch("event_json"))
220
+ event_type = event_hash.fetch('event_type')
221
+ event_json = Sequent::Core::Oj.strict_load(event_hash.fetch('event_json'))
186
222
  resolve_event_type(event_type).deserialize_from_json(event_json)
187
- rescue
188
- raise DeserializeEventError.new(event_hash)
223
+ rescue StandardError
224
+ raise DeserializeEventError, event_hash
189
225
  end
190
226
 
191
227
  def resolve_event_type(event_type)
@@ -215,13 +251,15 @@ SELECT aggregate_id
215
251
  end
216
252
  connection = Sequent.configuration.event_record_class.connection
217
253
  values = event_records
218
- .map { |r| "(#{column_names.map { |c| connection.quote(r[c.to_sym]) }.join(',')})" }
219
- .join(',')
254
+ .map { |r| "(#{column_names.map { |c| connection.quote(r[c.to_sym]) }.join(',')})" }
255
+ .join(',')
220
256
  columns = column_names.map { |c| connection.quote_column_name(c) }.join(',')
221
- sql = %Q{insert into #{connection.quote_table_name(Sequent.configuration.event_record_class.table_name)} (#{columns}) values #{values}}
257
+ sql = <<~SQL.chomp
258
+ insert into #{connection.quote_table_name(Sequent.configuration.event_record_class.table_name)} (#{columns}) values #{values}
259
+ SQL
222
260
  Sequent.configuration.event_record_class.connection.insert(sql, nil, primary_key_event_records)
223
261
  rescue ActiveRecord::RecordNotUnique
224
- fail OptimisticLockingError.new
262
+ raise OptimisticLockingError
225
263
  end
226
264
  end
227
265
  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)