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