sequent 4.0.0 → 4.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/sequent +33 -26
- data/lib/notices.rb +2 -0
- data/lib/sequent/application_record.rb +2 -0
- data/lib/sequent/configuration.rb +24 -31
- data/lib/sequent/core/aggregate_repository.rb +48 -13
- data/lib/sequent/core/aggregate_root.rb +36 -7
- data/lib/sequent/core/aggregate_roots.rb +24 -0
- data/lib/sequent/core/aggregate_snapshotter.rb +8 -5
- data/lib/sequent/core/base_command_handler.rb +4 -2
- data/lib/sequent/core/command.rb +17 -9
- data/lib/sequent/core/command_record.rb +8 -3
- data/lib/sequent/core/command_service.rb +18 -18
- data/lib/sequent/core/core.rb +2 -0
- data/lib/sequent/core/current_event.rb +2 -0
- data/lib/sequent/core/event.rb +16 -11
- data/lib/sequent/core/event_publisher.rb +16 -15
- data/lib/sequent/core/event_record.rb +7 -7
- data/lib/sequent/core/event_store.rb +89 -51
- data/lib/sequent/core/ext/ext.rb +9 -1
- data/lib/sequent/core/helpers/array_with_type.rb +4 -1
- data/lib/sequent/core/helpers/association_validator.rb +9 -7
- data/lib/sequent/core/helpers/attribute_support.rb +45 -28
- data/lib/sequent/core/helpers/autoset_attributes.rb +4 -4
- data/lib/sequent/core/helpers/boolean_validator.rb +6 -1
- data/lib/sequent/core/helpers/copyable.rb +2 -2
- data/lib/sequent/core/helpers/date_time_validator.rb +4 -1
- data/lib/sequent/core/helpers/date_validator.rb +6 -1
- data/lib/sequent/core/helpers/default_validators.rb +12 -10
- data/lib/sequent/core/helpers/equal_support.rb +8 -6
- data/lib/sequent/core/helpers/helpers.rb +2 -0
- data/lib/sequent/core/helpers/mergable.rb +6 -5
- data/lib/sequent/core/helpers/message_handler.rb +3 -1
- data/lib/sequent/core/helpers/param_support.rb +19 -15
- data/lib/sequent/core/helpers/secret.rb +14 -12
- data/lib/sequent/core/helpers/string_support.rb +5 -4
- data/lib/sequent/core/helpers/string_to_value_parsers.rb +7 -2
- data/lib/sequent/core/helpers/string_validator.rb +6 -1
- data/lib/sequent/core/helpers/type_conversion_support.rb +5 -3
- data/lib/sequent/core/helpers/uuid_helper.rb +5 -2
- data/lib/sequent/core/helpers/value_validators.rb +23 -9
- data/lib/sequent/core/persistors/active_record_persistor.rb +19 -9
- data/lib/sequent/core/persistors/persistor.rb +16 -14
- data/lib/sequent/core/persistors/persistors.rb +2 -0
- data/lib/sequent/core/persistors/replay_optimized_postgres_persistor.rb +70 -47
- data/lib/sequent/core/projector.rb +53 -22
- data/lib/sequent/core/random_uuid_generator.rb +2 -0
- data/lib/sequent/core/sequent_oj.rb +2 -0
- data/lib/sequent/core/stream_record.rb +9 -3
- data/lib/sequent/core/transactions/active_record_transaction_provider.rb +30 -9
- data/lib/sequent/core/transactions/no_transactions.rb +2 -1
- data/lib/sequent/core/transactions/read_only_active_record_transaction_provider.rb +46 -0
- data/lib/sequent/core/transactions/transactions.rb +3 -0
- data/lib/sequent/core/value_object.rb +8 -10
- data/lib/sequent/core/workflow.rb +35 -5
- data/lib/sequent/generator/aggregate.rb +16 -10
- data/lib/sequent/generator/command.rb +26 -19
- data/lib/sequent/generator/event.rb +19 -17
- data/lib/sequent/generator/generator.rb +2 -0
- data/lib/sequent/generator/project.rb +9 -0
- data/lib/sequent/generator/template_project/Gemfile +1 -1
- data/lib/sequent/generator/template_project/ruby-version +1 -0
- data/lib/sequent/generator.rb +2 -0
- data/lib/sequent/migrations/executor.rb +22 -13
- data/lib/sequent/migrations/functions.rb +5 -6
- data/lib/sequent/migrations/migrate_events.rb +12 -9
- data/lib/sequent/migrations/migrations.rb +2 -1
- data/lib/sequent/migrations/planner.rb +33 -23
- data/lib/sequent/migrations/projectors.rb +4 -3
- data/lib/sequent/migrations/sql.rb +2 -0
- data/lib/sequent/migrations/view_schema.rb +84 -45
- data/lib/sequent/rake/migration_tasks.rb +58 -22
- data/lib/sequent/rake/tasks.rb +5 -2
- data/lib/sequent/sequent.rb +2 -0
- data/lib/sequent/support/database.rb +30 -15
- data/lib/sequent/support/view_projection.rb +6 -3
- data/lib/sequent/support/view_schema.rb +2 -0
- data/lib/sequent/support.rb +2 -0
- data/lib/sequent/test/command_handler_helpers.rb +35 -17
- data/lib/sequent/test/event_handler_helpers.rb +10 -4
- data/lib/sequent/test/event_stream_helpers.rb +7 -3
- data/lib/sequent/test/time_comparison.rb +12 -5
- data/lib/sequent/test.rb +2 -0
- data/lib/sequent/util/dry_run.rb +28 -20
- data/lib/sequent/util/printer.rb +6 -5
- data/lib/sequent/util/skip_if_already_processing.rb +3 -1
- data/lib/sequent/util/timer.rb +2 -0
- data/lib/sequent/util/util.rb +2 -0
- data/lib/sequent.rb +2 -0
- data/lib/version.rb +3 -1
- 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
|
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
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
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
|
data/lib/sequent/core/core.rb
CHANGED
data/lib/sequent/core/event.rb
CHANGED
@@ -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::
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
19
|
-
|
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
|
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
|
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
|
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
|
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:
|
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
|
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
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
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
|
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(
|
51
|
-
Class.const_get(
|
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 =
|
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,
|
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
|
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(
|
65
|
-
events = Sequent.configuration.event_record_class.connection.select_all(query).map
|
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
|
71
|
-
.map
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
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 =
|
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 =
|
146
|
-
SELECT aggregate_id
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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
|
-
|
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
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
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(
|
185
|
-
event_json = Sequent::Core::Oj.strict_load(event_hash.fetch(
|
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
|
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
|
-
|
219
|
-
|
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 =
|
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
|
-
|
262
|
+
raise OptimisticLockingError
|
225
263
|
end
|
226
264
|
end
|
227
265
|
end
|
data/lib/sequent/core/ext/ext.rb
CHANGED
@@ -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?
|
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
|
-
|
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
|
-
|
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
|
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
|
38
|
+
elsif value&.is_a?(Array)
|
38
39
|
item_type = record.class.types.fetch(association).item_type
|
39
|
-
record.errors.add(association,
|
40
|
-
|
41
|
-
record.errors.add(association,
|
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)
|