sequent 4.0.0 → 4.3.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 +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)