sequent 3.2.2 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/notices.rb +4 -0
- data/lib/sequent.rb +3 -0
- data/lib/sequent/application_record.rb +7 -0
- data/lib/sequent/configuration.rb +13 -0
- data/lib/sequent/core/aggregate_repository.rb +7 -1
- data/lib/sequent/core/command.rb +13 -2
- data/lib/sequent/core/command_record.rb +5 -2
- data/lib/sequent/core/command_service.rb +28 -12
- data/lib/sequent/core/event_publisher.rb +4 -0
- data/lib/sequent/core/event_record.rb +2 -1
- data/lib/sequent/core/event_store.rb +23 -4
- data/lib/sequent/core/helpers/attribute_support.rb +28 -7
- data/lib/sequent/core/helpers/mergable.rb +1 -0
- data/lib/sequent/core/persistors/active_record_persistor.rb +1 -1
- data/lib/sequent/core/persistors/replay_optimized_postgres_persistor.rb +2 -2
- data/lib/sequent/core/projector.rb +23 -1
- data/lib/sequent/core/stream_record.rb +1 -1
- data/lib/sequent/core/transactions/active_record_transaction_provider.rb +6 -4
- data/lib/sequent/generator.rb +1 -4
- data/lib/sequent/generator/generator.rb +4 -0
- data/lib/sequent/generator/project.rb +1 -1
- data/lib/sequent/generator/template_project/Gemfile +1 -1
- data/lib/sequent/generator/template_project/app/records/post_record.rb +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/migrations/executor.rb +78 -0
- data/lib/sequent/migrations/functions.rb +76 -0
- data/lib/sequent/migrations/migrations.rb +1 -0
- data/lib/sequent/migrations/planner.rb +118 -0
- data/lib/sequent/migrations/projectors.rb +6 -5
- data/lib/sequent/migrations/sql.rb +17 -0
- data/lib/sequent/migrations/view_schema.rb +74 -73
- data/lib/sequent/rake/migration_tasks.rb +2 -2
- data/lib/sequent/rake/tasks.rb +1 -1
- data/lib/sequent/sequent.rb +5 -1
- data/lib/sequent/support/database.rb +11 -6
- data/lib/sequent/test/command_handler_helpers.rb +4 -0
- data/lib/sequent/util/dry_run.rb +191 -0
- data/lib/sequent/util/skip_if_already_processing.rb +19 -5
- data/lib/sequent/util/util.rb +1 -0
- data/lib/version.rb +1 -1
- metadata +77 -36
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3322a4b950e847a5818555b2718df70e2b81a8e398074309a0b58c2a8d332ce5
|
4
|
+
data.tar.gz: 6136697a8a9596e999fabf3814509428b06188a0ac577fa3443a80ec190cbdd7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f8ce675ae0a16274630066192086fb8c46bb0306e9280fd4b8901500af4a7af56844c5d6c40cd0f45a87342c33a4faeb2eee48f0dc0fb05a0a09046a2dea7414
|
7
|
+
data.tar.gz: e750e54d72de1c8641a513839c675adeb32fbeaa3435528a8aca755a951a7572d696e9348b7594e104f044978a0e9780f8e080ff1ad5e4ea9b7cc203fac6fee2
|
data/lib/notices.rb
ADDED
data/lib/sequent.rb
CHANGED
@@ -13,6 +13,7 @@ module Sequent
|
|
13
13
|
|
14
14
|
DEFAULT_MIGRATION_SQL_FILES_DIRECTORY = 'db/tables'
|
15
15
|
DEFAULT_DATABASE_CONFIG_DIRECTORY = 'db'
|
16
|
+
DEFAULT_DATABASE_SCHEMA_DIRECTORY = 'db'
|
16
17
|
|
17
18
|
DEFAULT_VIEW_SCHEMA_NAME = 'view_schema'
|
18
19
|
DEFAULT_EVENT_STORE_SCHEMA_NAME= 'sequent_schema'
|
@@ -26,6 +27,10 @@ module Sequent
|
|
26
27
|
|
27
28
|
DEFAULT_EVENT_RECORD_HOOKS_CLASS = Sequent::Core::EventRecordHooks
|
28
29
|
|
30
|
+
DEFAULT_STRICT_CHECK_ATTRIBUTES_ON_APPLY_EVENTS = false
|
31
|
+
|
32
|
+
DEFAULT_ERROR_LOCALE_RESOLVER = -> { I18n.locale || :en }
|
33
|
+
|
29
34
|
attr_accessor :aggregate_repository
|
30
35
|
|
31
36
|
attr_accessor :event_store,
|
@@ -49,14 +54,19 @@ module Sequent
|
|
49
54
|
|
50
55
|
attr_accessor :logger
|
51
56
|
|
57
|
+
attr_accessor :error_locale_resolver
|
58
|
+
|
52
59
|
attr_accessor :migration_sql_files_directory,
|
53
60
|
:view_schema_name,
|
54
61
|
:offline_replay_persistor_class,
|
55
62
|
:online_replay_persistor_class,
|
56
63
|
:number_of_replay_processes,
|
57
64
|
:database_config_directory,
|
65
|
+
:database_schema_directory,
|
58
66
|
:event_store_schema_name
|
59
67
|
|
68
|
+
attr_accessor :strict_check_attributes_on_apply_events
|
69
|
+
|
60
70
|
attr_reader :migrations_class_name,
|
61
71
|
:versions_table_name,
|
62
72
|
:replayed_ids_table_name
|
@@ -101,8 +111,11 @@ module Sequent
|
|
101
111
|
self.offline_replay_persistor_class = DEFAULT_OFFLINE_REPLAY_PERSISTOR_CLASS
|
102
112
|
self.online_replay_persistor_class = DEFAULT_ONLINE_REPLAY_PERSISTOR_CLASS
|
103
113
|
self.database_config_directory = DEFAULT_DATABASE_CONFIG_DIRECTORY
|
114
|
+
self.database_schema_directory = DEFAULT_DATABASE_SCHEMA_DIRECTORY
|
115
|
+
self.strict_check_attributes_on_apply_events = DEFAULT_STRICT_CHECK_ATTRIBUTES_ON_APPLY_EVENTS
|
104
116
|
|
105
117
|
self.logger = Logger.new(STDOUT).tap {|l| l.level = Logger::INFO }
|
118
|
+
self.error_locale_resolver = DEFAULT_ERROR_LOCALE_RESOLVER
|
106
119
|
end
|
107
120
|
|
108
121
|
def replayed_ids_table_name=(table_name)
|
@@ -100,11 +100,17 @@ module Sequent
|
|
100
100
|
##
|
101
101
|
# Returns whether the event store has an aggregate with the given id
|
102
102
|
def contains_aggregate?(aggregate_id)
|
103
|
-
Sequent.configuration.event_store.stream_exists?(aggregate_id)
|
103
|
+
Sequent.configuration.event_store.stream_exists?(aggregate_id) &&
|
104
|
+
Sequent.configuration.event_store.events_exists?(aggregate_id)
|
104
105
|
end
|
105
106
|
|
106
107
|
# Gets all uncommitted_events from the 'registered' aggregates
|
107
108
|
# and stores them in the event store.
|
109
|
+
#
|
110
|
+
# The events given to the EventStore are ordered in loading order
|
111
|
+
# of the different AggregateRoot's. So Events are stored
|
112
|
+
# (and therefore published) in order in which they are `apply`-ed per AggregateRoot.
|
113
|
+
#
|
108
114
|
# The command is 'attached' for traceability purpose so we can see
|
109
115
|
# which command resulted in which events.
|
110
116
|
#
|
data/lib/sequent/core/command.rb
CHANGED
@@ -7,7 +7,15 @@ require_relative 'helpers/mergable'
|
|
7
7
|
|
8
8
|
module Sequent
|
9
9
|
module Core
|
10
|
-
#
|
10
|
+
#
|
11
|
+
# Base class for all Command's.
|
12
|
+
#
|
13
|
+
# Commands form the API of your domain. They are
|
14
|
+
# simple data objects with descriptive names
|
15
|
+
# of what they want to achieve. E.g. `SendInvoice`.
|
16
|
+
#
|
17
|
+
# BaseCommand uses `ActiveModel::Validations` for
|
18
|
+
# validations
|
11
19
|
class BaseCommand
|
12
20
|
include ActiveModel::Validations,
|
13
21
|
Sequent::Core::Helpers::Copyable,
|
@@ -40,6 +48,9 @@ module Sequent
|
|
40
48
|
end
|
41
49
|
end
|
42
50
|
|
51
|
+
#
|
52
|
+
# Utility class containing all subclasses of BaseCommand
|
53
|
+
#
|
43
54
|
class Commands
|
44
55
|
class << self
|
45
56
|
def commands
|
@@ -60,7 +71,7 @@ module Sequent
|
|
60
71
|
end
|
61
72
|
end
|
62
73
|
|
63
|
-
# Most commonly used
|
74
|
+
# Most commonly used Command
|
64
75
|
# Command can be instantiated just by using:
|
65
76
|
#
|
66
77
|
# Command.new(aggregate_id: "1", user_id: "joe")
|
@@ -32,7 +32,7 @@ module Sequent
|
|
32
32
|
end
|
33
33
|
|
34
34
|
# For storing Sequent::Core::Command in the database using active_record
|
35
|
-
class CommandRecord <
|
35
|
+
class CommandRecord < Sequent::ApplicationRecord
|
36
36
|
include SerializesCommand
|
37
37
|
|
38
38
|
self.table_name = "command_records"
|
@@ -42,7 +42,10 @@ module Sequent
|
|
42
42
|
validates_presence_of :command_type, :command_json
|
43
43
|
|
44
44
|
def parent
|
45
|
-
EventRecord
|
45
|
+
EventRecord
|
46
|
+
.where(aggregate_id: event_aggregate_id, sequence_number: event_sequence_number)
|
47
|
+
.where('event_type != ?', Sequent::Core::SnapshotEvent.name)
|
48
|
+
.first
|
46
49
|
end
|
47
50
|
|
48
51
|
def children
|
@@ -4,23 +4,29 @@ require_relative 'current_event'
|
|
4
4
|
module Sequent
|
5
5
|
module Core
|
6
6
|
#
|
7
|
-
# Single point in the application
|
8
|
-
#
|
7
|
+
# Single point in the application to get something done in Sequent.
|
8
|
+
# The CommandService handles all subclasses Sequent::Core::BaseCommand. Most common
|
9
|
+
# use is to subclass `Sequent::Command`.
|
9
10
|
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
11
|
+
# The CommandService is available via the shortcut method `Sequent.command_service`
|
12
|
+
#
|
13
|
+
# To use the CommandService please use:
|
14
|
+
#
|
15
|
+
# Sequent.command_service.execute_commands(...)
|
15
16
|
#
|
16
17
|
class CommandService
|
18
|
+
#
|
17
19
|
# Executes the given commands in a single transactional block as implemented by the +transaction_provider+
|
18
20
|
#
|
19
|
-
# For each
|
21
|
+
# For each Command:
|
22
|
+
#
|
23
|
+
# * Validate command
|
24
|
+
# * Call Sequent::CommandHandler's listening to the given Command
|
25
|
+
# * Store and publish Events
|
26
|
+
# * Any new Command's (from e.g. workflows) are queued for processing in the same transaction
|
27
|
+
#
|
28
|
+
# At the end the transaction is committed and the AggregateRepository's Unit of Work is cleared.
|
20
29
|
#
|
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
30
|
def execute_commands(*commands)
|
25
31
|
commands.each do |command|
|
26
32
|
if command.respond_to?(:event_aggregate_id) && CurrentEvent.current
|
@@ -33,6 +39,7 @@ module Sequent
|
|
33
39
|
end
|
34
40
|
|
35
41
|
def remove_event_handler(clazz)
|
42
|
+
warn "[DEPRECATION] `remove_event_handler` is deprecated"
|
36
43
|
event_store.remove_event_handler(clazz)
|
37
44
|
end
|
38
45
|
|
@@ -45,6 +52,7 @@ module Sequent
|
|
45
52
|
while(!command_queue.empty?) do
|
46
53
|
process_command(command_queue.pop)
|
47
54
|
end
|
55
|
+
Sequent::Util.done_processing(:command_service_process_commands)
|
48
56
|
end
|
49
57
|
ensure
|
50
58
|
command_queue.clear
|
@@ -54,9 +62,16 @@ module Sequent
|
|
54
62
|
end
|
55
63
|
|
56
64
|
def process_command(command)
|
65
|
+
fail ArgumentError, 'command is required' if command.nil?
|
66
|
+
|
67
|
+
Sequent.logger.debug("[CommandService] Processing command #{command.class}")
|
68
|
+
|
57
69
|
filters.each { |filter| filter.execute(command) }
|
58
70
|
|
59
|
-
|
71
|
+
I18n.with_locale(Sequent.configuration.error_locale_resolver.call) do
|
72
|
+
raise CommandNotValid.new(command) unless command.valid?
|
73
|
+
end
|
74
|
+
|
60
75
|
parsed_command = command.parse_attrs_to_correct_types
|
61
76
|
command_handlers.select { |h| h.class.handles_message?(parsed_command) }.each { |h| h.handle_message parsed_command }
|
62
77
|
repository.commit(parsed_command)
|
@@ -89,6 +104,7 @@ module Sequent
|
|
89
104
|
|
90
105
|
# Raised when BaseCommand.valid? returns false
|
91
106
|
class CommandNotValid < ArgumentError
|
107
|
+
attr_reader :command
|
92
108
|
|
93
109
|
def initialize(command)
|
94
110
|
@command = command
|
@@ -46,6 +46,10 @@ module Sequent
|
|
46
46
|
end
|
47
47
|
|
48
48
|
def process_event(event)
|
49
|
+
fail ArgumentError, 'event is required' if event.nil?
|
50
|
+
|
51
|
+
Sequent.logger.debug("[EventPublisher] Publishing event #{event.class}")
|
52
|
+
|
49
53
|
configuration.event_handlers.each do |handler|
|
50
54
|
begin
|
51
55
|
handler.handle_message event
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'active_record'
|
2
2
|
require_relative 'sequent_oj'
|
3
|
+
require_relative '../application_record.rb'
|
3
4
|
|
4
5
|
module Sequent
|
5
6
|
module Core
|
@@ -72,7 +73,7 @@ module Sequent
|
|
72
73
|
end
|
73
74
|
end
|
74
75
|
|
75
|
-
class EventRecord <
|
76
|
+
class EventRecord < Sequent::ApplicationRecord
|
76
77
|
include SerializesEvent
|
77
78
|
|
78
79
|
self.table_name = "event_records"
|
@@ -33,10 +33,18 @@ module Sequent
|
|
33
33
|
# Stores the events in the EventStore and publishes the events
|
34
34
|
# to the registered event_handlers.
|
35
35
|
#
|
36
|
-
#
|
37
|
-
#
|
36
|
+
# The events are published according to the order in
|
37
|
+
# the tail of the given `streams_with_events` array pair.
|
38
|
+
#
|
39
|
+
# @param command The command that caused the Events
|
40
|
+
# @param streams_with_events is an enumerable of pairs from
|
41
|
+
# `StreamRecord` to arrays ordered uncommitted `Event`s.
|
38
42
|
#
|
39
43
|
def commit_events(command, streams_with_events)
|
44
|
+
fail ArgumentError, "command is required" if command.nil?
|
45
|
+
|
46
|
+
Sequent.logger.debug("[EventStore] Committing events for command #{command.class}")
|
47
|
+
|
40
48
|
store_events(command, streams_with_events)
|
41
49
|
publish_events(streams_with_events.flat_map { |_, events| events })
|
42
50
|
end
|
@@ -80,6 +88,9 @@ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuratio
|
|
80
88
|
Sequent.configuration.stream_record_class.exists?(aggregate_id: aggregate_id)
|
81
89
|
end
|
82
90
|
|
91
|
+
def events_exists?(aggregate_id)
|
92
|
+
Sequent.configuration.event_record_class.exists?(aggregate_id: aggregate_id)
|
93
|
+
end
|
83
94
|
##
|
84
95
|
# Replays all events in the event store to the registered event_handlers.
|
85
96
|
#
|
@@ -158,7 +169,15 @@ SELECT aggregate_id
|
|
158
169
|
private
|
159
170
|
|
160
171
|
def column_names
|
161
|
-
@column_names ||= Sequent
|
172
|
+
@column_names ||= Sequent
|
173
|
+
.configuration
|
174
|
+
.event_record_class
|
175
|
+
.column_names
|
176
|
+
.reject { |c| c == primary_key_event_records }
|
177
|
+
end
|
178
|
+
|
179
|
+
def primary_key_event_records
|
180
|
+
@primary_key_event_records ||= Sequent.configuration.event_record_class.primary_key
|
162
181
|
end
|
163
182
|
|
164
183
|
def deserialize_event(event_hash)
|
@@ -200,7 +219,7 @@ SELECT aggregate_id
|
|
200
219
|
.join(',')
|
201
220
|
columns = column_names.map { |c| connection.quote_column_name(c) }.join(',')
|
202
221
|
sql = %Q{insert into #{connection.quote_table_name(Sequent.configuration.event_record_class.table_name)} (#{columns}) values #{values}}
|
203
|
-
Sequent.configuration.event_record_class.connection.insert(sql)
|
222
|
+
Sequent.configuration.event_record_class.connection.insert(sql, nil, primary_key_event_records)
|
204
223
|
rescue ActiveRecord::RecordNotUnique
|
205
224
|
fail OptimisticLockingError.new
|
206
225
|
end
|
@@ -9,17 +9,33 @@ require_relative 'association_validator'
|
|
9
9
|
module Sequent
|
10
10
|
module Core
|
11
11
|
module Helpers
|
12
|
-
# Provides functionality for defining attributes with their types
|
12
|
+
# Provides functionality for defining attributes with their types.
|
13
13
|
#
|
14
|
-
# Since our Commands and ValueObjects are not backed by a database like e.g.
|
14
|
+
# Since our Commands and ValueObjects are not backed by a database like e.g. Rails
|
15
15
|
# we can not infer their types. We need the types to be able to parse from and to json.
|
16
|
-
# We could have stored te type information in the json, but we didn't.
|
17
|
-
#
|
18
16
|
# You typically do not need to include this module in your classes. If you extend from
|
19
|
-
# Sequent::
|
17
|
+
# Sequent::ValueObject, Sequent::Event or Sequent::Command you will
|
20
18
|
# get this functionality for free.
|
21
19
|
#
|
20
|
+
# Example:
|
21
|
+
#
|
22
|
+
# attrs name: String, age: Integer, born: Date
|
23
|
+
#
|
24
|
+
# Currently Sequent supports the following types:
|
25
|
+
#
|
26
|
+
# - String
|
27
|
+
# - Integer
|
28
|
+
# - Boolean
|
29
|
+
# - Date
|
30
|
+
# - DateTime
|
31
|
+
# - Subclasses of Sequent::ValueObject
|
32
|
+
# - Lists defined as `array(String)`
|
33
|
+
# - BigDecimal
|
34
|
+
# - Sequent::Secret
|
35
|
+
#
|
22
36
|
module AttributeSupport
|
37
|
+
class UnknownAttributeError < StandardError; end
|
38
|
+
|
23
39
|
# module containing class methods to be added
|
24
40
|
module ClassMethods
|
25
41
|
|
@@ -60,6 +76,7 @@ module Sequent
|
|
60
76
|
class_eval <<EOS
|
61
77
|
def update_all_attributes(attrs)
|
62
78
|
super if defined?(super)
|
79
|
+
ensure_known_attributes(attrs)
|
63
80
|
#{@types.map { |attribute, _|
|
64
81
|
"@#{attribute} = attrs[:#{attribute}]"
|
65
82
|
}.join("\n ")}
|
@@ -157,9 +174,13 @@ EOS
|
|
157
174
|
prefix ? HashWithIndifferentAccess[result.map { |k, v| ["#{prefix}_#{k}", v] }] : result
|
158
175
|
end
|
159
176
|
|
160
|
-
|
161
|
-
|
177
|
+
def ensure_known_attributes(attrs)
|
178
|
+
return unless Sequent.configuration.strict_check_attributes_on_apply_events
|
162
179
|
|
180
|
+
unknowns = attrs.keys.map(&:to_s) - self.class.types.keys.map(&:to_s)
|
181
|
+
raise UnknownAttributeError.new("#{self.class.name} does not specify attrs: #{unknowns.join(", ")}") if unknowns.any?
|
182
|
+
end
|
183
|
+
end
|
163
184
|
end
|
164
185
|
end
|
165
186
|
end
|
@@ -8,6 +8,7 @@ module Sequent
|
|
8
8
|
module Mergable
|
9
9
|
|
10
10
|
def merge!(attrs = {})
|
11
|
+
warn "[DEPRECATION] `merge!` is deprecated. Please use `copy` instead. This method will no longer be included in the next version of Sequent. You can still use it but you will have to include the module `Sequent::Core::Helpers::Mergable` yourself."
|
11
12
|
attrs.each do |name, value|
|
12
13
|
self.send("#{name}=", value)
|
13
14
|
end
|
@@ -307,7 +307,7 @@ module Sequent
|
|
307
307
|
end
|
308
308
|
|
309
309
|
buf = ''
|
310
|
-
conn =
|
310
|
+
conn = Sequent::ApplicationRecord.connection.raw_connection
|
311
311
|
copy_data = StringIO.new csv.string
|
312
312
|
conn.transaction do
|
313
313
|
conn.copy_data("COPY #{clazz.table_name} (#{column_names.join(",")}) FROM STDIN WITH csv") do
|
@@ -346,7 +346,7 @@ module Sequent
|
|
346
346
|
private
|
347
347
|
|
348
348
|
def cast_value_to_column_type(clazz, column_name, record)
|
349
|
-
|
349
|
+
Sequent::ApplicationRecord.connection.type_cast(record[column_name.to_sym], @column_cache[clazz.name][column_name])
|
350
350
|
end
|
351
351
|
end
|
352
352
|
end
|
@@ -11,7 +11,26 @@ module Sequent
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def managed_tables
|
14
|
-
@managed_tables
|
14
|
+
@managed_tables || managed_tables_from_superclass
|
15
|
+
end
|
16
|
+
|
17
|
+
def manages_no_tables
|
18
|
+
@manages_no_tables = true
|
19
|
+
manages_tables *[]
|
20
|
+
end
|
21
|
+
|
22
|
+
def manages_no_tables?
|
23
|
+
!!@manages_no_tables || manages_no_tables_from_superclass?
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def managed_tables_from_superclass
|
29
|
+
self.superclass.managed_tables if self.superclass.respond_to?(:managed_tables)
|
30
|
+
end
|
31
|
+
|
32
|
+
def manages_no_tables_from_superclass?
|
33
|
+
self.superclass.manages_no_tables? if self.superclass.respond_to?(:manages_no_tables?)
|
15
34
|
end
|
16
35
|
end
|
17
36
|
|
@@ -96,7 +115,10 @@ module Sequent
|
|
96
115
|
:commit
|
97
116
|
|
98
117
|
private
|
118
|
+
|
99
119
|
def ensure_valid!
|
120
|
+
return if self.class.manages_no_tables?
|
121
|
+
|
100
122
|
fail "A Projector must manage at least one table. Did you forget to add `managed_tables` to #{self.class.name}?" if self.class.managed_tables.nil? || self.class.managed_tables.empty?
|
101
123
|
end
|
102
124
|
end
|