sequent 4.0.0 → 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 +2 -0
- data/lib/sequent/application_record.rb +2 -0
- data/lib/sequent/configuration.rb +24 -31
- data/lib/sequent/core/aggregate_repository.rb +17 -13
- 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 +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 +57 -50
- 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 +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 +5 -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 +2 -0
- data/lib/sequent/generator/project.rb +2 -0
- data/lib/sequent/generator/template_project/Gemfile +1 -1
- 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 +11 -8
- 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 +81 -66
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
|
|
@@ -61,27 +62,36 @@ module Sequent
|
|
61
62
|
|
62
63
|
streams = Sequent.configuration.stream_record_class.where(aggregate_id: aggregate_ids)
|
63
64
|
|
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
|
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|
|
66
67
|
deserialize_event(event_hash)
|
67
68
|
end
|
68
69
|
|
69
70
|
events
|
70
|
-
.group_by
|
71
|
-
.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
|
72
80
|
end
|
73
81
|
|
74
82
|
def aggregate_query(aggregate_id)
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
)
|
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
|
85
95
|
end
|
86
96
|
|
87
97
|
def stream_exists?(aggregate_id)
|
@@ -97,7 +107,7 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
|
|
97
107
|
# @param block that returns the events.
|
98
108
|
# <b>DEPRECATED:</b> use <tt>replay_events_from_cursor</tt> instead.
|
99
109
|
def replay_events
|
100
|
-
warn
|
110
|
+
warn '[DEPRECATION] `replay_events` is deprecated in favor of `replay_events_from_cursor`'
|
101
111
|
events = yield.map { |event_hash| deserialize_event(event_hash) }
|
102
112
|
publish_events(events)
|
103
113
|
end
|
@@ -109,8 +119,7 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
|
|
109
119
|
#
|
110
120
|
# @param get_events lambda that returns the events cursor
|
111
121
|
# @param on_progress lambda that gets called on substantial progress
|
112
|
-
def replay_events_from_cursor(block_size: 2000,
|
113
|
-
get_events:,
|
122
|
+
def replay_events_from_cursor(get_events:, block_size: 2000,
|
114
123
|
on_progress: PRINT_PROGRESS)
|
115
124
|
progress = 0
|
116
125
|
cursor = get_events.call
|
@@ -128,7 +137,7 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
|
|
128
137
|
on_progress[progress, true, ids_replayed]
|
129
138
|
end
|
130
139
|
|
131
|
-
PRINT_PROGRESS =
|
140
|
+
PRINT_PROGRESS = ->(progress, done, _) do
|
132
141
|
if done
|
133
142
|
Sequent.logger.debug "Done replaying #{progress} events"
|
134
143
|
else
|
@@ -142,38 +151,34 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
|
|
142
151
|
def aggregates_that_need_snapshots(last_aggregate_id, limit = 10)
|
143
152
|
stream_table = quote_table_name Sequent.configuration.stream_record_class.table_name
|
144
153
|
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
|
-
|
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
|
157
166
|
Sequent.configuration.event_record_class.connection.select_all(query).map { |x| x['aggregate_id'] }
|
158
167
|
end
|
159
168
|
|
160
169
|
def find_event_stream(aggregate_id)
|
161
170
|
record = Sequent.configuration.stream_record_class.where(aggregate_id: aggregate_id).first
|
162
|
-
|
163
|
-
record.event_stream
|
164
|
-
else
|
165
|
-
nil
|
166
|
-
end
|
171
|
+
record&.event_stream
|
167
172
|
end
|
168
173
|
|
169
174
|
private
|
170
175
|
|
171
176
|
def column_names
|
172
177
|
@column_names ||= Sequent
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
178
|
+
.configuration
|
179
|
+
.event_record_class
|
180
|
+
.column_names
|
181
|
+
.reject { |c| c == primary_key_event_records }
|
177
182
|
end
|
178
183
|
|
179
184
|
def primary_key_event_records
|
@@ -181,11 +186,11 @@ SELECT aggregate_id
|
|
181
186
|
end
|
182
187
|
|
183
188
|
def deserialize_event(event_hash)
|
184
|
-
event_type = event_hash.fetch(
|
185
|
-
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'))
|
186
191
|
resolve_event_type(event_type).deserialize_from_json(event_json)
|
187
|
-
rescue
|
188
|
-
raise DeserializeEventError
|
192
|
+
rescue StandardError
|
193
|
+
raise DeserializeEventError, event_hash
|
189
194
|
end
|
190
195
|
|
191
196
|
def resolve_event_type(event_type)
|
@@ -215,13 +220,15 @@ SELECT aggregate_id
|
|
215
220
|
end
|
216
221
|
connection = Sequent.configuration.event_record_class.connection
|
217
222
|
values = event_records
|
218
|
-
|
219
|
-
|
223
|
+
.map { |r| "(#{column_names.map { |c| connection.quote(r[c.to_sym]) }.join(',')})" }
|
224
|
+
.join(',')
|
220
225
|
columns = column_names.map { |c| connection.quote_column_name(c) }.join(',')
|
221
|
-
sql =
|
226
|
+
sql = <<~SQL.chomp
|
227
|
+
insert into #{connection.quote_table_name(Sequent.configuration.event_record_class.table_name)} (#{columns}) values #{values}
|
228
|
+
SQL
|
222
229
|
Sequent.configuration.event_record_class.connection.insert(sql, nil, primary_key_event_records)
|
223
230
|
rescue ActiveRecord::RecordNotUnique
|
224
|
-
|
231
|
+
raise OptimisticLockingError
|
225
232
|
end
|
226
233
|
end
|
227
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)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support'
|
2
4
|
require_relative '../ext/ext'
|
3
5
|
require_relative 'array_with_type'
|
@@ -38,18 +40,15 @@ module Sequent
|
|
38
40
|
|
39
41
|
# module containing class methods to be added
|
40
42
|
module ClassMethods
|
41
|
-
|
42
43
|
def types
|
43
44
|
@types ||= {}
|
44
|
-
if @merged_types
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
@merged_types.merge!(mod.types)
|
50
|
-
end
|
51
|
-
@merged_types
|
45
|
+
return @merged_types if @merged_types
|
46
|
+
|
47
|
+
@merged_types = is_a?(Class) && superclass.respond_to?(:types) ? @types.merge(superclass.types) : @types
|
48
|
+
included_modules.select { |m| m.include? Sequent::Core::Helpers::AttributeSupport }.each do |mod|
|
49
|
+
@merged_types.merge!(mod.types)
|
52
50
|
end
|
51
|
+
@merged_types
|
53
52
|
end
|
54
53
|
|
55
54
|
def attrs(args)
|
@@ -58,14 +57,15 @@ module Sequent
|
|
58
57
|
associations = []
|
59
58
|
args.each do |attribute, type|
|
60
59
|
attr_accessor attribute
|
60
|
+
|
61
61
|
if included_modules.include?(Sequent::Core::Helpers::TypeConversionSupport)
|
62
62
|
Sequent::Core::Helpers::DefaultValidators.for(type).add_validations_for(self, attribute)
|
63
63
|
end
|
64
64
|
|
65
|
-
if type.
|
65
|
+
if type.instance_of?(Sequent::Core::Helpers::ArrayWithType)
|
66
66
|
associations << attribute
|
67
67
|
elsif included_modules.include?(ActiveModel::Validations) &&
|
68
|
-
|
68
|
+
type.included_modules.include?(Sequent::Core::Helpers::AttributeSupport)
|
69
69
|
associations << attribute
|
70
70
|
end
|
71
71
|
end
|
@@ -77,9 +77,9 @@ module Sequent
|
|
77
77
|
def update_all_attributes(attrs)
|
78
78
|
super if defined?(super)
|
79
79
|
ensure_known_attributes(attrs)
|
80
|
-
#{@types.map
|
81
|
-
|
82
|
-
|
80
|
+
#{@types.map do |attribute, _|
|
81
|
+
"@#{attribute} = attrs[:#{attribute}]"
|
82
|
+
end.join("\n ")}
|
83
83
|
self
|
84
84
|
end
|
85
85
|
EOS
|
@@ -87,9 +87,9 @@ EOS
|
|
87
87
|
class_eval <<EOS
|
88
88
|
def update_all_attributes_from_json(attrs)
|
89
89
|
super if defined?(super)
|
90
|
-
#{@types.map
|
91
|
-
|
92
|
-
|
90
|
+
#{@types.map do |attribute, type|
|
91
|
+
"@#{attribute} = #{type}.deserialize_from_json(attrs['#{attribute}'])"
|
92
|
+
end.join("\n ")}
|
93
93
|
end
|
94
94
|
EOS
|
95
95
|
end
|
@@ -106,17 +106,33 @@ EOS
|
|
106
106
|
|
107
107
|
def deserialize_from_json(args)
|
108
108
|
unless args.nil?
|
109
|
-
obj = allocate
|
109
|
+
obj = allocate
|
110
|
+
|
111
|
+
upcast!(args)
|
112
|
+
|
110
113
|
obj.update_all_attributes_from_json(args)
|
111
114
|
obj
|
112
115
|
end
|
113
116
|
end
|
114
117
|
|
115
|
-
|
116
118
|
def numeric?(object)
|
117
|
-
true if Float(object)
|
119
|
+
true if Float(object)
|
120
|
+
rescue StandardError
|
121
|
+
false
|
118
122
|
end
|
119
123
|
|
124
|
+
def upcast(&block)
|
125
|
+
@upcasters ||= []
|
126
|
+
@upcasters.push(block)
|
127
|
+
end
|
128
|
+
|
129
|
+
def upcast!(hash)
|
130
|
+
return if @upcasters.nil?
|
131
|
+
|
132
|
+
@upcasters.each do |upcaster|
|
133
|
+
upcaster.call(hash)
|
134
|
+
end
|
135
|
+
end
|
120
136
|
end
|
121
137
|
|
122
138
|
# extend host class with class methods when we're included
|
@@ -124,11 +140,10 @@ EOS
|
|
124
140
|
host_class.extend(ClassMethods)
|
125
141
|
end
|
126
142
|
|
127
|
-
|
128
143
|
def attributes
|
129
144
|
hash = HashWithIndifferentAccess.new
|
130
145
|
self.class.types.each do |name, _|
|
131
|
-
value =
|
146
|
+
value = instance_variable_get("@#{name}")
|
132
147
|
hash[name] = if value.respond_to?(:attributes)
|
133
148
|
value.attributes
|
134
149
|
else
|
@@ -141,7 +156,7 @@ EOS
|
|
141
156
|
def as_json(opts = {})
|
142
157
|
hash = HashWithIndifferentAccess.new
|
143
158
|
self.class.types.each do |name, _|
|
144
|
-
value =
|
159
|
+
value = instance_variable_get("@#{name}")
|
145
160
|
hash[name] = if value.respond_to?(:as_json)
|
146
161
|
value.as_json(opts)
|
147
162
|
else
|
@@ -158,15 +173,15 @@ EOS
|
|
158
173
|
def validation_errors(prefix = nil)
|
159
174
|
result = errors.to_hash
|
160
175
|
self.class.types.each do |field|
|
161
|
-
value =
|
176
|
+
value = instance_variable_get("@#{field[0]}")
|
162
177
|
if value.respond_to? :validation_errors
|
163
|
-
value.validation_errors.each { |k, v| result["#{field[0]
|
164
|
-
elsif field[1].
|
178
|
+
value.validation_errors.each { |k, v| result["#{field[0]}_#{k}".to_sym] = v }
|
179
|
+
elsif field[1].instance_of?(ArrayWithType) && value.present?
|
165
180
|
value
|
166
181
|
.select { |val| val.respond_to?(:validation_errors) }
|
167
182
|
.each_with_index do |val, index|
|
168
183
|
val.validation_errors.each do |k, v|
|
169
|
-
result["#{field[0]
|
184
|
+
result["#{field[0]}_#{index}_#{k}".to_sym] = v
|
170
185
|
end
|
171
186
|
end
|
172
187
|
end
|
@@ -178,7 +193,9 @@ EOS
|
|
178
193
|
return unless Sequent.configuration.strict_check_attributes_on_apply_events
|
179
194
|
|
180
195
|
unknowns = attrs.keys.map(&:to_s) - self.class.types.keys.map(&:to_s)
|
181
|
-
|
196
|
+
if unknowns.any?
|
197
|
+
fail UnknownAttributeError, "#{self.class.name} does not specify attrs: #{unknowns.join(', ')}"
|
198
|
+
end
|
182
199
|
end
|
183
200
|
end
|
184
201
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Sequent
|
2
4
|
module Core
|
3
5
|
module Helpers
|
@@ -24,8 +26,7 @@ module Sequent
|
|
24
26
|
#
|
25
27
|
module AutosetAttributes
|
26
28
|
module ClassMethods
|
27
|
-
|
28
|
-
@@autoset_ignore_attributes = %w{aggregate_id sequence_number created_at}
|
29
|
+
@@autoset_ignore_attributes = %w[aggregate_id sequence_number created_at]
|
29
30
|
|
30
31
|
def set_autoset_ignore_attributes(attribute_names)
|
31
32
|
@@autoset_ignore_attributes = attribute_names
|
@@ -39,7 +40,7 @@ module Sequent
|
|
39
40
|
event_classes.each do |event_class|
|
40
41
|
on event_class do |event|
|
41
42
|
self.class.event_attribute_keys(event_class).each do |attribute_name|
|
42
|
-
instance_variable_set(:"@#{attribute_name
|
43
|
+
instance_variable_set(:"@#{attribute_name}", event.send(attribute_name.to_sym))
|
43
44
|
end
|
44
45
|
end
|
45
46
|
end
|
@@ -53,4 +54,3 @@ module Sequent
|
|
53
54
|
end
|
54
55
|
end
|
55
56
|
end
|
56
|
-
|