sequent 3.3.1 → 4.1.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 +31 -25
- data/lib/notices.rb +6 -0
- data/lib/sequent/application_record.rb +2 -0
- data/lib/sequent/configuration.rb +29 -29
- data/lib/sequent/core/aggregate_repository.rb +24 -14
- data/lib/sequent/core/aggregate_root.rb +16 -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 +30 -11
- data/lib/sequent/core/command_record.rb +12 -4
- data/lib/sequent/core/command_service.rb +41 -25
- 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 +20 -15
- data/lib/sequent/core/event_record.rb +7 -7
- data/lib/sequent/core/event_store.rb +75 -49
- 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 +64 -33
- 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 -4
- 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 +25 -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 +7 -9
- data/lib/sequent/core/transactions/no_transactions.rb +2 -1
- data/lib/sequent/core/transactions/transactions.rb +2 -0
- data/lib/sequent/core/value_object.rb +8 -10
- data/lib/sequent/core/workflow.rb +7 -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 +6 -0
- data/lib/sequent/generator/project.rb +3 -1
- data/lib/sequent/generator/template_project/Gemfile +1 -1
- data/lib/sequent/generator/template_project/spec/app/projectors/post_projector_spec.rb +1 -1
- data/lib/sequent/generator/template_project/spec/lib/post/post_command_handler_spec.rb +1 -1
- data/lib/sequent/generator.rb +3 -4
- data/lib/sequent/migrations/executor.rb +30 -9
- 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 +93 -44
- data/lib/sequent/rake/migration_tasks.rb +59 -23
- data/lib/sequent/rake/tasks.rb +5 -2
- data/lib/sequent/sequent.rb +6 -1
- data/lib/sequent/support/database.rb +39 -17
- 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 +39 -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 +194 -0
- data/lib/sequent/util/printer.rb +6 -5
- data/lib/sequent/util/skip_if_already_processing.rb +21 -5
- data/lib/sequent/util/timer.rb +2 -0
- data/lib/sequent/util/util.rb +3 -0
- data/lib/sequent.rb +4 -0
- data/lib/version.rb +3 -1
- metadata +110 -59
@@ -1,26 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'transactions/no_transactions'
|
2
4
|
require_relative 'current_event'
|
3
5
|
|
4
6
|
module Sequent
|
5
7
|
module Core
|
6
8
|
#
|
7
|
-
# Single point in the application
|
8
|
-
#
|
9
|
+
# Single point in the application to get something done in Sequent.
|
10
|
+
# The CommandService handles all subclasses Sequent::Core::BaseCommand. Most common
|
11
|
+
# use is to subclass `Sequent::Command`.
|
12
|
+
#
|
13
|
+
# The CommandService is available via the shortcut method `Sequent.command_service`
|
14
|
+
#
|
15
|
+
# To use the CommandService please use:
|
9
16
|
#
|
10
|
-
#
|
11
|
-
# * Call correct Sequent::Core::BaseCommandHandler
|
12
|
-
# * CommandHandler decides which Sequent::Core::AggregateRoot (s) to call
|
13
|
-
# * Events are stored in the Sequent::Core::EventStore
|
14
|
-
# * Unit of Work is cleared
|
17
|
+
# Sequent.command_service.execute_commands(...)
|
15
18
|
#
|
16
19
|
class CommandService
|
20
|
+
#
|
17
21
|
# Executes the given commands in a single transactional block as implemented by the +transaction_provider+
|
18
22
|
#
|
19
|
-
# For each
|
23
|
+
# For each Command:
|
24
|
+
#
|
25
|
+
# * Validate command
|
26
|
+
# * Call Sequent::CommandHandler's listening to the given Command
|
27
|
+
# * Store and publish Events
|
28
|
+
# * Any new Command's (from e.g. workflows) are queued for processing in the same transaction
|
29
|
+
#
|
30
|
+
# At the end the transaction is committed and the AggregateRepository's Unit of Work is cleared.
|
20
31
|
#
|
21
|
-
# * All filters are executed. Any exception raised will rollback the transaction and propagate up
|
22
|
-
# * If the command is valid all +command_handlers+ that +handles_message?+ is invoked
|
23
|
-
# * The +repository+ commits the command and all uncommitted_events resulting from the command
|
24
32
|
def execute_commands(*commands)
|
25
33
|
commands.each do |command|
|
26
34
|
if command.respond_to?(:event_aggregate_id) && CurrentEvent.current
|
@@ -33,6 +41,7 @@ module Sequent
|
|
33
41
|
end
|
34
42
|
|
35
43
|
def remove_event_handler(clazz)
|
44
|
+
warn '[DEPRECATION] `remove_event_handler` is deprecated'
|
36
45
|
event_store.remove_event_handler(clazz)
|
37
46
|
end
|
38
47
|
|
@@ -40,25 +49,29 @@ module Sequent
|
|
40
49
|
|
41
50
|
def process_commands
|
42
51
|
Sequent::Util.skip_if_already_processing(:command_service_process_commands) do
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
process_command(command_queue.pop)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
ensure
|
50
|
-
command_queue.clear
|
51
|
-
repository.clear
|
52
|
+
transaction_provider.transactional do
|
53
|
+
process_command(command_queue.pop) until command_queue.empty?
|
54
|
+
Sequent::Util.done_processing(:command_service_process_commands)
|
52
55
|
end
|
56
|
+
ensure
|
57
|
+
command_queue.clear
|
58
|
+
repository.clear
|
53
59
|
end
|
54
60
|
end
|
55
61
|
|
56
62
|
def process_command(command)
|
63
|
+
fail ArgumentError, 'command is required' if command.nil?
|
64
|
+
|
65
|
+
Sequent.logger.debug("[CommandService] Processing command #{command.class}")
|
66
|
+
|
57
67
|
filters.each { |filter| filter.execute(command) }
|
58
68
|
|
59
|
-
|
69
|
+
fail CommandNotValid, command unless command.valid?
|
70
|
+
|
60
71
|
parsed_command = command.parse_attrs_to_correct_types
|
61
|
-
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 }
|
62
75
|
repository.commit(parsed_command)
|
63
76
|
end
|
64
77
|
|
@@ -89,15 +102,18 @@ module Sequent
|
|
89
102
|
|
90
103
|
# Raised when BaseCommand.valid? returns false
|
91
104
|
class CommandNotValid < ArgumentError
|
105
|
+
attr_reader :command
|
92
106
|
|
93
107
|
def initialize(command)
|
94
108
|
@command = command
|
95
|
-
msg = @command.respond_to?(:aggregate_id) ? " #{@command.aggregate_id}" :
|
96
|
-
super "Invalid command #{@command.class
|
109
|
+
msg = @command.respond_to?(:aggregate_id) ? " #{@command.aggregate_id}" : ''
|
110
|
+
super "Invalid command #{@command.class}#{msg}, errors: #{@command.validation_errors}"
|
97
111
|
end
|
98
112
|
|
99
113
|
def errors(prefix = nil)
|
100
|
-
|
114
|
+
I18n.with_locale(Sequent.configuration.error_locale_resolver.call) do
|
115
|
+
@command.validation_errors(prefix)
|
116
|
+
end
|
101
117
|
end
|
102
118
|
|
103
119
|
def errors_with_command_prefix
|
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,23 +42,21 @@ module Sequent
|
|
35
42
|
|
36
43
|
def process_events
|
37
44
|
Sequent::Util.skip_if_already_processing(:events_queue_lock) do
|
38
|
-
|
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
|
|
48
51
|
def process_event(event)
|
52
|
+
fail ArgumentError, 'event is required' if event.nil?
|
53
|
+
|
54
|
+
Sequent.logger.debug("[EventPublisher] Publishing event #{event.class}")
|
55
|
+
|
49
56
|
configuration.event_handlers.each do |handler|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
raise PublishEventError.new(handler.class, event)
|
54
|
-
end
|
57
|
+
handler.handle_message event
|
58
|
+
rescue StandardError
|
59
|
+
raise PublishEventError.new(handler.class, event)
|
55
60
|
end
|
56
61
|
end
|
57
62
|
|
@@ -1,10 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_record'
|
2
4
|
require_relative 'sequent_oj'
|
3
|
-
require_relative '../application_record
|
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
|
@@ -33,10 +34,18 @@ module Sequent
|
|
33
34
|
# Stores the events in the EventStore and publishes the events
|
34
35
|
# to the registered event_handlers.
|
35
36
|
#
|
36
|
-
#
|
37
|
-
#
|
37
|
+
# The events are published according to the order in
|
38
|
+
# the tail of the given `streams_with_events` array pair.
|
39
|
+
#
|
40
|
+
# @param command The command that caused the Events
|
41
|
+
# @param streams_with_events is an enumerable of pairs from
|
42
|
+
# `StreamRecord` to arrays ordered uncommitted `Event`s.
|
38
43
|
#
|
39
44
|
def commit_events(command, streams_with_events)
|
45
|
+
fail ArgumentError, 'command is required' if command.nil?
|
46
|
+
|
47
|
+
Sequent.logger.debug("[EventStore] Committing events for command #{command.class}")
|
48
|
+
|
40
49
|
store_events(command, streams_with_events)
|
41
50
|
publish_events(streams_with_events.flat_map { |_, events| events })
|
42
51
|
end
|
@@ -53,40 +62,52 @@ module Sequent
|
|
53
62
|
|
54
63
|
streams = Sequent.configuration.stream_record_class.where(aggregate_id: aggregate_ids)
|
55
64
|
|
56
|
-
query = aggregate_ids.uniq.map { |aggregate_id| aggregate_query(aggregate_id) }.join(
|
57
|
-
events = Sequent.configuration.event_record_class.connection.select_all(query).map
|
65
|
+
query = aggregate_ids.uniq.map { |aggregate_id| aggregate_query(aggregate_id) }.join(' UNION ALL ')
|
66
|
+
events = Sequent.configuration.event_record_class.connection.select_all(query).map do |event_hash|
|
58
67
|
deserialize_event(event_hash)
|
59
68
|
end
|
60
69
|
|
61
70
|
events
|
62
|
-
.group_by
|
63
|
-
.map
|
71
|
+
.group_by(&:aggregate_id)
|
72
|
+
.map do |aggregate_id, es|
|
73
|
+
[
|
74
|
+
streams.find do |stream_record|
|
75
|
+
stream_record.aggregate_id == aggregate_id
|
76
|
+
end.event_stream,
|
77
|
+
es,
|
78
|
+
]
|
79
|
+
end
|
64
80
|
end
|
65
81
|
|
66
82
|
def aggregate_query(aggregate_id)
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
)
|
83
|
+
<<~SQL.chomp
|
84
|
+
(
|
85
|
+
SELECT event_type, event_json
|
86
|
+
FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS o
|
87
|
+
WHERE aggregate_id = #{quote(aggregate_id)}
|
88
|
+
AND sequence_number >= COALESCE((SELECT MAX(sequence_number)
|
89
|
+
FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS i
|
90
|
+
WHERE event_type = #{quote Sequent.configuration.snapshot_event_class.name}
|
91
|
+
AND i.aggregate_id = #{quote(aggregate_id)}), 0)
|
92
|
+
ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuration.snapshot_event_class.name} THEN 0 ELSE 1 END) ASC
|
93
|
+
)
|
94
|
+
SQL
|
77
95
|
end
|
78
96
|
|
79
97
|
def stream_exists?(aggregate_id)
|
80
98
|
Sequent.configuration.stream_record_class.exists?(aggregate_id: aggregate_id)
|
81
99
|
end
|
82
100
|
|
101
|
+
def events_exists?(aggregate_id)
|
102
|
+
Sequent.configuration.event_record_class.exists?(aggregate_id: aggregate_id)
|
103
|
+
end
|
83
104
|
##
|
84
105
|
# Replays all events in the event store to the registered event_handlers.
|
85
106
|
#
|
86
107
|
# @param block that returns the events.
|
87
108
|
# <b>DEPRECATED:</b> use <tt>replay_events_from_cursor</tt> instead.
|
88
109
|
def replay_events
|
89
|
-
warn
|
110
|
+
warn '[DEPRECATION] `replay_events` is deprecated in favor of `replay_events_from_cursor`'
|
90
111
|
events = yield.map { |event_hash| deserialize_event(event_hash) }
|
91
112
|
publish_events(events)
|
92
113
|
end
|
@@ -98,8 +119,7 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
|
|
98
119
|
#
|
99
120
|
# @param get_events lambda that returns the events cursor
|
100
121
|
# @param on_progress lambda that gets called on substantial progress
|
101
|
-
def replay_events_from_cursor(block_size: 2000,
|
102
|
-
get_events:,
|
122
|
+
def replay_events_from_cursor(get_events:, block_size: 2000,
|
103
123
|
on_progress: PRINT_PROGRESS)
|
104
124
|
progress = 0
|
105
125
|
cursor = get_events.call
|
@@ -117,7 +137,7 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
|
|
117
137
|
on_progress[progress, true, ids_replayed]
|
118
138
|
end
|
119
139
|
|
120
|
-
PRINT_PROGRESS =
|
140
|
+
PRINT_PROGRESS = ->(progress, done, _) do
|
121
141
|
if done
|
122
142
|
Sequent.logger.debug "Done replaying #{progress} events"
|
123
143
|
else
|
@@ -131,42 +151,46 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
|
|
131
151
|
def aggregates_that_need_snapshots(last_aggregate_id, limit = 10)
|
132
152
|
stream_table = quote_table_name Sequent.configuration.stream_record_class.table_name
|
133
153
|
event_table = quote_table_name Sequent.configuration.event_record_class.table_name
|
134
|
-
query =
|
135
|
-
SELECT aggregate_id
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
154
|
+
query = <<~SQL.chomp
|
155
|
+
SELECT aggregate_id
|
156
|
+
FROM #{stream_table} stream
|
157
|
+
WHERE aggregate_id::varchar > COALESCE(#{quote last_aggregate_id}, '')
|
158
|
+
AND snapshot_threshold IS NOT NULL
|
159
|
+
AND snapshot_threshold <= (
|
160
|
+
(SELECT MAX(events.sequence_number) FROM #{event_table} events WHERE events.event_type <> #{quote Sequent.configuration.snapshot_event_class.name} AND stream.aggregate_id = events.aggregate_id) -
|
161
|
+
COALESCE((SELECT MAX(snapshots.sequence_number) FROM #{event_table} snapshots WHERE snapshots.event_type = #{quote Sequent.configuration.snapshot_event_class.name} AND stream.aggregate_id = snapshots.aggregate_id), 0))
|
162
|
+
ORDER BY aggregate_id
|
163
|
+
LIMIT #{quote limit}
|
164
|
+
FOR UPDATE
|
165
|
+
SQL
|
146
166
|
Sequent.configuration.event_record_class.connection.select_all(query).map { |x| x['aggregate_id'] }
|
147
167
|
end
|
148
168
|
|
149
169
|
def find_event_stream(aggregate_id)
|
150
170
|
record = Sequent.configuration.stream_record_class.where(aggregate_id: aggregate_id).first
|
151
|
-
|
152
|
-
record.event_stream
|
153
|
-
else
|
154
|
-
nil
|
155
|
-
end
|
171
|
+
record&.event_stream
|
156
172
|
end
|
157
173
|
|
158
174
|
private
|
159
175
|
|
160
176
|
def column_names
|
161
|
-
@column_names ||= Sequent
|
177
|
+
@column_names ||= Sequent
|
178
|
+
.configuration
|
179
|
+
.event_record_class
|
180
|
+
.column_names
|
181
|
+
.reject { |c| c == primary_key_event_records }
|
182
|
+
end
|
183
|
+
|
184
|
+
def primary_key_event_records
|
185
|
+
@primary_key_event_records ||= Sequent.configuration.event_record_class.primary_key
|
162
186
|
end
|
163
187
|
|
164
188
|
def deserialize_event(event_hash)
|
165
|
-
event_type = event_hash.fetch(
|
166
|
-
event_json = Sequent::Core::Oj.strict_load(event_hash.fetch(
|
189
|
+
event_type = event_hash.fetch('event_type')
|
190
|
+
event_json = Sequent::Core::Oj.strict_load(event_hash.fetch('event_json'))
|
167
191
|
resolve_event_type(event_type).deserialize_from_json(event_json)
|
168
|
-
rescue
|
169
|
-
raise DeserializeEventError
|
192
|
+
rescue StandardError
|
193
|
+
raise DeserializeEventError, event_hash
|
170
194
|
end
|
171
195
|
|
172
196
|
def resolve_event_type(event_type)
|
@@ -196,13 +220,15 @@ SELECT aggregate_id
|
|
196
220
|
end
|
197
221
|
connection = Sequent.configuration.event_record_class.connection
|
198
222
|
values = event_records
|
199
|
-
|
200
|
-
|
223
|
+
.map { |r| "(#{column_names.map { |c| connection.quote(r[c.to_sym]) }.join(',')})" }
|
224
|
+
.join(',')
|
201
225
|
columns = column_names.map { |c| connection.quote_column_name(c) }.join(',')
|
202
|
-
sql =
|
203
|
-
|
226
|
+
sql = <<~SQL.chomp
|
227
|
+
insert into #{connection.quote_table_name(Sequent.configuration.event_record_class.table_name)} (#{columns}) values #{values}
|
228
|
+
SQL
|
229
|
+
Sequent.configuration.event_record_class.connection.insert(sql, nil, primary_key_event_records)
|
204
230
|
rescue ActiveRecord::RecordNotUnique
|
205
|
-
|
231
|
+
raise OptimisticLockingError
|
206
232
|
end
|
207
233
|
end
|
208
234
|
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)
|