sequent 3.3.1 → 4.1.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 +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)
|