sequent 1.0.0 → 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f1c376e79b2afb9c653058f4cb38e7d24bb7d742
4
- data.tar.gz: 751c997c10407d7dd073b70b268ed69d16d1b459
3
+ metadata.gz: 2f3c785214713fa790ec8e000314e79730913df4
4
+ data.tar.gz: 0da86f6f53cacc4c007020e021cb849931154a67
5
5
  SHA512:
6
- metadata.gz: 042064fb131306a5197e035330dceb3e467c2031f73932777286ec72219545ef7fcc5bbd3b0322177801d994e87adf66735be07e4b03fbf2e725d209b11e8b09
7
- data.tar.gz: 777558493270ceeb8e80038e6b2336bfebbdbb103e74c341f676e53599971e8723862b3b6aa424d008f294ff78108a580652f31a90600fa90a275387696ba5f2
6
+ metadata.gz: a422b778b039fdb34d9b6782dc06861a6c17472c15773de773d79e5a902854d9792eec2d9463cc93d6ac2fec1bd4b2ec2b54efcb2a4f3adb0d1e2798dddb7ab0
7
+ data.tar.gz: 46c9c7acae7f5148fffbe715d49cf80f245569ac77e0824dfae22fb42949a8e14684f22a850519803b3be42f2364f50a39fb387e68ae7b7c13a0a10ed55b176f
@@ -5,14 +5,15 @@ require_relative 'core/aggregate_repository'
5
5
 
6
6
  module Sequent
7
7
  class Configuration
8
- attr_reader :aggregate_repository
8
+ attr_accessor :aggregate_repository
9
9
 
10
10
  attr_accessor :event_store,
11
11
  :command_service,
12
12
  :event_record_class,
13
13
  :stream_record_class,
14
14
  :snapshot_event_class,
15
- :transaction_provider
15
+ :transaction_provider,
16
+ :event_publisher
16
17
 
17
18
  attr_accessor :command_handlers,
18
19
  :command_filters
@@ -40,20 +41,16 @@ module Sequent
40
41
  self.command_filters = []
41
42
  self.event_handlers = []
42
43
 
43
- self.event_store = Sequent::Core::EventStore.new(self)
44
- self.command_service = Sequent::Core::CommandService.new(self)
44
+ self.aggregate_repository = Sequent::Core::AggregateRepository.new
45
+ self.event_store = Sequent::Core::EventStore.new
46
+ self.command_service = Sequent::Core::CommandService.new
45
47
  self.event_record_class = Sequent::Core::EventRecord
46
48
  self.stream_record_class = Sequent::Core::StreamRecord
47
49
  self.snapshot_event_class = Sequent::Core::SnapshotEvent
48
50
  self.transaction_provider = Sequent::Core::Transactions::NoTransactions.new
49
51
  self.uuid_generator = Sequent::Core::RandomUuidGenerator
52
+ self.event_publisher = Sequent::Core::EventPublisher.new
50
53
  self.disable_event_handlers = false
51
54
  end
52
-
53
- def event_store=(event_store)
54
- @event_store = event_store
55
- @aggregate_repository = Sequent::Core::AggregateRepository.new(event_store)
56
- self.command_handlers.each { |c| c.repository = @aggregate_repository }
57
- end
58
55
  end
59
56
  end
@@ -16,8 +16,6 @@ module Sequent
16
16
  # Key used in thread local
17
17
  AGGREGATES_KEY = 'Sequent::Core::AggregateRepository::aggregates'.to_sym
18
18
 
19
- attr_reader :event_store
20
-
21
19
  class NonUniqueAggregateId < StandardError
22
20
  def initialize(existing, new)
23
21
  super "Duplicate aggregate #{new} with same key as existing #{existing}"
@@ -30,10 +28,6 @@ module Sequent
30
28
  end
31
29
  end
32
30
 
33
- def initialize(event_store)
34
- @event_store = event_store
35
- end
36
-
37
31
  # Adds the given aggregate to the repository (or unit of work).
38
32
  #
39
33
  # Only when +commit+ is called all aggregates in the unit of work are 'processed'
@@ -80,7 +74,7 @@ module Sequent
80
74
  _aggregates = aggregates.values_at(*_aggregate_ids).compact
81
75
  _query_ids = _aggregate_ids - _aggregates.map(&:id)
82
76
 
83
- _aggregates += @event_store.load_events_for_aggregates(_query_ids).map do |stream, events|
77
+ _aggregates += Sequent.configuration.event_store.load_events_for_aggregates(_query_ids).map do |stream, events|
84
78
  aggregate_class = Class.const_get(stream.aggregate_type)
85
79
  aggregate_class.load_from_history(stream, events)
86
80
  end
@@ -104,7 +98,7 @@ module Sequent
104
98
  ##
105
99
  # Returns whether the event store has an aggregate with the given id
106
100
  def contains_aggregate?(aggregate_id)
107
- @event_store.stream_exists?(aggregate_id)
101
+ Sequent.configuration.event_store.stream_exists?(aggregate_id)
108
102
  end
109
103
 
110
104
  # Gets all uncommitted_events from the 'registered' aggregates
@@ -136,7 +130,7 @@ module Sequent
136
130
  end
137
131
 
138
132
  def store_events(command, streams_with_events)
139
- @event_store.commit_events(command, streams_with_events)
133
+ Sequent.configuration.event_store.commit_events(command, streams_with_events)
140
134
  end
141
135
  end
142
136
  end
@@ -1,18 +1,20 @@
1
1
  module Sequent
2
2
  module Core
3
+
4
+ ##
5
+ # Take up to `limit` snapshots when needed. Throws `:done` when done.
6
+ #
3
7
  class SnapshotCommand < Sequent::Core::BaseCommand
4
8
  attrs limit: Integer
5
9
  end
6
10
 
7
- class AggregateSnapshotter < BaseCommandHandler
11
+ ##
12
+ # Take snapshot of given aggregate
13
+ class TakeSnapshot < Sequent::Core::Command
14
+ end
8
15
 
9
- def self.handles_message?(message)
10
- message.is_a? SnapshotCommand
11
- end
16
+ class AggregateSnapshotter < BaseCommandHandler
12
17
 
13
- ##
14
- # Take up to `limit` snapshots when needed. Throws `:done` when done.
15
- #
16
18
  on SnapshotCommand do |command|
17
19
  aggregate_ids = repository.event_store.aggregates_that_need_snapshots(@last_aggregate_id, command.limit)
18
20
  aggregate_ids.each do |aggregate_id|
@@ -22,8 +24,12 @@ module Sequent
22
24
  throw :done if @last_aggregate_id.nil?
23
25
  end
24
26
 
27
+ on TakeSnapshot do |command|
28
+ take_snapshot!(command.aggregate_id)
29
+ end
30
+
25
31
  def take_snapshot!(aggregate_id)
26
- aggregate = @repository.load_aggregate(aggregate_id)
32
+ aggregate = repository.load_aggregate(aggregate_id)
27
33
  Sequent.logger.info "Taking snapshot for aggregate #{aggregate}"
28
34
  aggregate.take_snapshot!
29
35
  rescue => e
@@ -20,16 +20,14 @@ module Sequent
20
20
  include Sequent::Core::Helpers::SelfApplier,
21
21
  Sequent::Core::Helpers::UuidHelper
22
22
 
23
- attr_accessor :repository
23
+ protected
24
24
 
25
- def initialize(repository = Sequent.configuration.aggregate_repository)
26
- @repository = repository
25
+ def repository
26
+ Sequent.configuration.aggregate_repository
27
27
  end
28
28
 
29
- protected
30
-
31
29
  def do_with_aggregate(command, clazz = nil, aggregate_id = nil)
32
- aggregate = @repository.load_aggregate(aggregate_id.nil? ? command.aggregate_id : aggregate_id, clazz)
30
+ aggregate = repository.load_aggregate(aggregate_id.nil? ? command.aggregate_id : aggregate_id, clazz)
33
31
  yield aggregate if block_given?
34
32
  end
35
33
  end
@@ -42,7 +42,7 @@ module Sequent
42
42
  @record_session = record_session
43
43
  end
44
44
 
45
- def_delegators :@record_session, :update_record, :create_record, :create_or_update_record, :get_record!, :get_record,
45
+ def_delegators :@record_session, :update_record, :create_record, :create_records, :create_or_update_record, :get_record!, :get_record,
46
46
  :delete_all_records, :update_all_records, :do_with_records, :do_with_record, :delete_record,
47
47
  :find_records, :last_record, :execute
48
48
 
@@ -13,19 +13,6 @@ module Sequent
13
13
  # * Unit of Work is cleared
14
14
  #
15
15
  class CommandService
16
- attr_accessor :configuration
17
-
18
- # Create a command service with the given configuration.
19
- #
20
- # +event_store+ The Sequent::Core::EventStore
21
- # +aggregate_repository+ The Sequent::Core::AggregateRepository
22
- # +transaction_provider+ How to do transaction management.
23
- # +command_handlers+ List of command handlers that need to handle commands
24
- # +command_filters+ List of filter that respond_to :execute(command). Can be useful to do extra checks (security and such).
25
- def initialize(configuration)
26
- self.configuration = configuration
27
- end
28
-
29
16
  # Executes the given commands in a single transactional block as implemented by the +transaction_provider+
30
17
  #
31
18
  # For each command:
@@ -34,21 +21,8 @@ module Sequent
34
21
  # * If the command is valid all +command_handlers+ that +handles_message?+ is invoked
35
22
  # * The +repository+ commits the command and all uncommitted_events resulting from the command
36
23
  def execute_commands(*commands)
37
- begin
38
- transaction_provider.transactional do
39
- commands.each do |command|
40
- filters.each { |filter| filter.execute(command) }
41
-
42
- raise CommandNotValid.new(command) unless command.valid?
43
- parsed_command = command.parse_attrs_to_correct_types
44
- command_handlers.select { |h| h.class.handles_message?(parsed_command) }.each { |h| h.handle_message parsed_command }
45
- repository.commit(parsed_command)
46
- end
47
- end
48
- ensure
49
- repository.clear
50
- end
51
-
24
+ commands.each { |command| command_queue.push(command) }
25
+ process_commands
52
26
  end
53
27
 
54
28
  def remove_event_handler(clazz)
@@ -57,24 +31,52 @@ module Sequent
57
31
 
58
32
  private
59
33
 
34
+ def process_commands
35
+ Sequent::Util.skip_if_already_processing(:command_service_process_commands) do
36
+ begin
37
+ transaction_provider.transactional do
38
+ while(!command_queue.empty?) do
39
+ process_command(command_queue.pop)
40
+ end
41
+ end
42
+ ensure
43
+ command_queue.clear
44
+ repository.clear
45
+ end
46
+ end
47
+ end
48
+
49
+ def process_command(command)
50
+ filters.each { |filter| filter.execute(command) }
51
+
52
+ raise CommandNotValid.new(command) unless command.valid?
53
+ parsed_command = command.parse_attrs_to_correct_types
54
+ command_handlers.select { |h| h.class.handles_message?(parsed_command) }.each { |h| h.handle_message parsed_command }
55
+ repository.commit(parsed_command)
56
+ end
57
+
58
+ def command_queue
59
+ Thread.current[:command_service_commands] ||= Queue.new
60
+ end
61
+
60
62
  def event_store
61
- configuration.event_store
63
+ Sequent.configuration.event_store
62
64
  end
63
65
 
64
66
  def repository
65
- configuration.aggregate_repository
67
+ Sequent.configuration.aggregate_repository
66
68
  end
67
69
 
68
70
  def filters
69
- configuration.command_filters
71
+ Sequent.configuration.command_filters
70
72
  end
71
73
 
72
74
  def transaction_provider
73
- configuration.transaction_provider
75
+ Sequent.configuration.transaction_provider
74
76
  end
75
77
 
76
78
  def command_handlers
77
- configuration.command_handlers
79
+ Sequent.configuration.command_handlers
78
80
  end
79
81
  end
80
82
 
@@ -16,3 +16,4 @@ require_relative 'command_record'
16
16
  require_relative 'aggregate_snapshotter'
17
17
  require_relative 'workflow'
18
18
  require_relative 'random_uuid_generator'
19
+ require_relative 'event_publisher'
@@ -0,0 +1,63 @@
1
+ module Sequent
2
+ module Core
3
+ #
4
+ # EventPublisher ensures that, for every thread, events will be published in the order in which they are queued for publishing.
5
+ #
6
+ # This potentially introduces a wrinkle into your plans: You therefore should not split a "unit of work" across multiple threads.
7
+ #
8
+ # If you want other behaviour, you are free to implement your own version of EventPublisher and configure Sequent to use it.
9
+ #
10
+ class EventPublisher
11
+ class PublishEventError < RuntimeError
12
+ attr_reader :event_handler_class, :event
13
+
14
+ def initialize(event_handler_class, event)
15
+ @event_handler_class = event_handler_class
16
+ @event = event
17
+ end
18
+
19
+ def message
20
+ "Event Handler: #{@event_handler_class.inspect}\nEvent: #{@event.inspect}\nCause: #{cause.inspect}"
21
+ end
22
+ end
23
+
24
+ def publish_events(events)
25
+ return if configuration.disable_event_handlers
26
+ events.each { |event| events_queue.push(event) }
27
+ process_events
28
+ end
29
+
30
+ private
31
+
32
+ def events_queue
33
+ Thread.current[:events_queue] ||= Queue.new
34
+ end
35
+
36
+ def process_events
37
+ Sequent::Util.skip_if_already_processing(:events_queue_lock) do
38
+ begin
39
+ while(!events_queue.empty?) do
40
+ process_event(events_queue.pop)
41
+ end
42
+ ensure
43
+ events_queue.clear
44
+ end
45
+ end
46
+ end
47
+
48
+ def process_event(event)
49
+ configuration.event_handlers.each do |handler|
50
+ begin
51
+ handler.handle_message event
52
+ rescue
53
+ raise PublishEventError.new(handler.class, event)
54
+ end
55
+ end
56
+ end
57
+
58
+ def configuration
59
+ Sequent.configuration
60
+ end
61
+ end
62
+ end
63
+ end
@@ -9,19 +9,6 @@ module Sequent
9
9
  include ActiveRecord::ConnectionAdapters::Quoting
10
10
  extend Forwardable
11
11
 
12
- class PublishEventError < RuntimeError
13
- attr_reader :event_handler_class, :event
14
-
15
- def initialize(event_handler_class, event)
16
- @event_handler_class = event_handler_class
17
- @event = event
18
- end
19
-
20
- def message
21
- "Event Handler: #{@event_handler_class.inspect}\nEvent: #{@event.inspect}\nCause: #{cause.inspect}"
22
- end
23
- end
24
-
25
12
  class OptimisticLockingError < RuntimeError
26
13
  end
27
14
 
@@ -38,11 +25,7 @@ module Sequent
38
25
 
39
26
  end
40
27
 
41
- attr_accessor :configuration
42
- def_delegators :@configuration, :stream_record_class, :event_record_class, :snapshot_event_class, :event_handlers
43
-
44
- def initialize(configuration = Sequent.configuration)
45
- self.configuration = configuration
28
+ def initialize
46
29
  @event_types = ThreadSafe::Cache.new
47
30
  end
48
31
 
@@ -55,7 +38,7 @@ module Sequent
55
38
  #
56
39
  def commit_events(command, streams_with_events)
57
40
  store_events(command, streams_with_events)
58
- publish_events(streams_with_events.flat_map { |_, events| events }, event_handlers)
41
+ publish_events(streams_with_events.flat_map { |_, events| events })
59
42
  end
60
43
 
61
44
  ##
@@ -68,18 +51,10 @@ module Sequent
68
51
  def load_events_for_aggregates(aggregate_ids)
69
52
  return [] if aggregate_ids.none?
70
53
 
71
- streams = stream_record_class.where(aggregate_id: aggregate_ids)
54
+ streams = Sequent.configuration.stream_record_class.where(aggregate_id: aggregate_ids)
72
55
 
73
- events = event_record_class.connection.select_all(%Q{
74
- SELECT event_type, event_json
75
- FROM #{quote_table_name event_record_class.table_name}
76
- WHERE aggregate_id in (#{aggregate_ids.map{ |aggregate_id| quote(aggregate_id)}.join(",")})
77
- AND sequence_number >= COALESCE((SELECT MAX(sequence_number)
78
- FROM #{quote_table_name event_record_class.table_name}
79
- WHERE event_type = #{quote snapshot_event_class.name}
80
- AND aggregate_id in (#{aggregate_ids.map{ |aggregate_id| quote(aggregate_id)}.join(",")})), 0)
81
- ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote snapshot_event_class.name} THEN 0 ELSE 1 END) ASC
82
- }).map! do |event_hash|
56
+ query = aggregate_ids.uniq.map { |aggregate_id| aggregate_query(aggregate_id) }.join(" UNION ALL ")
57
+ events = Sequent.configuration.event_record_class.connection.select_all(query).map! do |event_hash|
83
58
  deserialize_event(event_hash)
84
59
  end
85
60
 
@@ -88,8 +63,21 @@ WHERE aggregate_id in (#{aggregate_ids.map{ |aggregate_id| quote(aggregate_id)}.
88
63
  .map { |aggregate_id, _events| [streams.find { |stream_record| stream_record.aggregate_id == aggregate_id }.event_stream, _events] }
89
64
  end
90
65
 
66
+ def aggregate_query(aggregate_id)
67
+ %Q{(
68
+ SELECT event_type, event_json
69
+ FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS o
70
+ WHERE aggregate_id = #{quote(aggregate_id)}
71
+ AND sequence_number >= COALESCE((SELECT MAX(sequence_number)
72
+ FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS i
73
+ WHERE event_type = #{quote Sequent.configuration.snapshot_event_class.name}
74
+ AND i.aggregate_id = #{quote(aggregate_id)}), 0)
75
+ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuration.snapshot_event_class.name} THEN 0 ELSE 1 END) ASC
76
+ )}
77
+ end
78
+
91
79
  def stream_exists?(aggregate_id)
92
- stream_record_class.exists?(aggregate_id: aggregate_id)
80
+ Sequent.configuration.stream_record_class.exists?(aggregate_id: aggregate_id)
93
81
  end
94
82
 
95
83
  ##
@@ -100,7 +88,7 @@ WHERE aggregate_id in (#{aggregate_ids.map{ |aggregate_id| quote(aggregate_id)}.
100
88
  def replay_events
101
89
  warn "[DEPRECATION] `replay_events` is deprecated in favor of `replay_events_from_cursor`"
102
90
  events = yield.map { |event_hash| deserialize_event(event_hash) }
103
- publish_events(events, event_handlers)
91
+ publish_events(events)
104
92
  end
105
93
 
106
94
  ##
@@ -118,7 +106,7 @@ WHERE aggregate_id in (#{aggregate_ids.map{ |aggregate_id| quote(aggregate_id)}.
118
106
  ids_replayed = []
119
107
  cursor.each_row(block_size: block_size).each do |record|
120
108
  event = deserialize_event(record)
121
- publish_events([event], event_handlers)
109
+ publish_events([event])
122
110
  progress += 1
123
111
  ids_replayed << record['id']
124
112
  if progress % block_size == 0
@@ -141,25 +129,25 @@ WHERE aggregate_id in (#{aggregate_ids.map{ |aggregate_id| quote(aggregate_id)}.
141
129
  # Returns the ids of aggregates that need a new snapshot.
142
130
  #
143
131
  def aggregates_that_need_snapshots(last_aggregate_id, limit = 10)
144
- stream_table = quote_table_name stream_record_class.table_name
145
- event_table = quote_table_name event_record_class.table_name
132
+ stream_table = quote_table_name Sequent.configuration.stream_record_class.table_name
133
+ event_table = quote_table_name Sequent.configuration.event_record_class.table_name
146
134
  query = %Q{
147
135
  SELECT aggregate_id
148
136
  FROM #{stream_table} stream
149
137
  WHERE aggregate_id::varchar > COALESCE(#{quote last_aggregate_id}, '')
150
138
  AND snapshot_threshold IS NOT NULL
151
139
  AND snapshot_threshold <= (
152
- (SELECT MAX(events.sequence_number) FROM #{event_table} events WHERE events.event_type <> #{quote snapshot_event_class.name} AND stream.aggregate_id = events.aggregate_id) -
153
- COALESCE((SELECT MAX(snapshots.sequence_number) FROM #{event_table} snapshots WHERE snapshots.event_type = #{quote snapshot_event_class.name} AND stream.aggregate_id = snapshots.aggregate_id), 0))
140
+ (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) -
141
+ 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))
154
142
  ORDER BY aggregate_id
155
143
  LIMIT #{quote limit}
156
144
  FOR UPDATE
157
145
  }
158
- event_record_class.connection.select_all(query).map { |x| x['aggregate_id'] }
146
+ Sequent.configuration.event_record_class.connection.select_all(query).map { |x| x['aggregate_id'] }
159
147
  end
160
148
 
161
149
  def find_event_stream(aggregate_id)
162
- record = stream_record_class.where(aggregate_id: aggregate_id).first
150
+ record = Sequent.configuration.stream_record_class.where(aggregate_id: aggregate_id).first
163
151
  if record
164
152
  record.event_stream
165
153
  else
@@ -170,7 +158,7 @@ SELECT aggregate_id
170
158
  private
171
159
 
172
160
  def column_names
173
- @column_names ||= event_record_class.column_names.reject { |c| c == 'id' }
161
+ @column_names ||= Sequent.configuration.event_record_class.column_names.reject { |c| c == 'id' }
174
162
  end
175
163
 
176
164
  def deserialize_event(event_hash)
@@ -185,24 +173,15 @@ SELECT aggregate_id
185
173
  @event_types.fetch_or_store(event_type) { |k| Class.const_get(k) }
186
174
  end
187
175
 
188
- def publish_events(events, event_handlers)
189
- return if configuration.disable_event_handlers
190
- event_handlers.each do |handler|
191
- events.each do |event|
192
- begin
193
- handler.handle_message event
194
- rescue
195
- raise PublishEventError.new(handler.class, event)
196
- end
197
- end
198
- end
176
+ def publish_events(events)
177
+ Sequent.configuration.event_publisher.publish_events(events)
199
178
  end
200
179
 
201
180
  def store_events(command, streams_with_events = [])
202
181
  command_record = CommandRecord.create!(command: command)
203
182
  event_records = streams_with_events.flat_map do |event_stream, uncommitted_events|
204
183
  unless event_stream.stream_record_id
205
- stream_record = stream_record_class.new
184
+ stream_record = Sequent.configuration.stream_record_class.new
206
185
  stream_record.event_stream = event_stream
207
186
  stream_record.save!
208
187
  event_stream.stream_record_id = stream_record.id
@@ -214,21 +193,21 @@ SELECT aggregate_id
214
193
  aggregate_id: event.aggregate_id,
215
194
  sequence_number: event.sequence_number,
216
195
  event_type: event.class.name,
217
- event_json: event_record_class.serialize_to_json(event),
196
+ event_json: Sequent.configuration.event_record_class.serialize_to_json(event),
218
197
  created_at: event.created_at
219
198
  }
220
199
  values = values.merge(organization_id: event.organization_id) if event.respond_to?(:organization_id)
221
200
 
222
- event_record_class.new(values)
201
+ Sequent.configuration.event_record_class.new(values)
223
202
  end
224
203
  end
225
- connection = event_record_class.connection
204
+ connection = Sequent.configuration.event_record_class.connection
226
205
  values = event_records
227
206
  .map { |r| "(#{column_names.map { |c| connection.quote(r[c.to_sym]) }.join(',')})" }
228
207
  .join(',')
229
208
  columns = column_names.map { |c| connection.quote_column_name(c) }.join(',')
230
- sql = %Q{insert into #{connection.quote_table_name(event_record_class.table_name)} (#{columns}) values #{values}}
231
- event_record_class.connection.insert(sql)
209
+ sql = %Q{insert into #{connection.quote_table_name(Sequent.configuration.event_record_class.table_name)} (#{columns}) values #{values}}
210
+ Sequent.configuration.event_record_class.connection.insert(sql)
232
211
  rescue ActiveRecord::RecordNotUnique
233
212
  fail OptimisticLockingError.new
234
213
  end
@@ -3,6 +3,7 @@ require 'active_record'
3
3
  module Sequent
4
4
  module Core
5
5
  module RecordSessions
6
+
6
7
  #
7
8
  # Session objects are used to update view state
8
9
  #
@@ -33,6 +34,21 @@ module Sequent
33
34
  record
34
35
  end
35
36
 
37
+ def create_records(record_class, array_of_value_hashes)
38
+ table = record_class.arel_table
39
+
40
+ query = array_of_value_hashes.map do |values|
41
+ insert_manager = new_insert_manager
42
+ insert_manager.into(table)
43
+ insert_manager.insert(values.map do |key, value|
44
+ convert_to_values(key, table, value)
45
+ end)
46
+ insert_manager.to_sql
47
+ end.join(";")
48
+
49
+ execute(query)
50
+ end
51
+
36
52
  def create_or_update_record(record_class, values, created_at = Time.now)
37
53
  record = get_record(record_class, values)
38
54
  unless record
@@ -91,8 +107,22 @@ module Sequent
91
107
  record_class.unscoped.new(values)
92
108
  end
93
109
 
94
- end
110
+ def new_insert_manager
111
+ if ActiveRecord::VERSION::MAJOR <= 4
112
+ Arel::InsertManager.new(ActiveRecord::Base)
113
+ else
114
+ Arel::InsertManager.new
115
+ end
116
+ end
95
117
 
118
+ def convert_to_values(key, table, value)
119
+ if ActiveRecord::VERSION::MAJOR <= 4
120
+ [table[key], value]
121
+ else
122
+ [table[key], table.type_cast_for_database(key, value)]
123
+ end
124
+ end
125
+ end
96
126
  end
97
127
  end
98
128
  end
@@ -1,5 +1,6 @@
1
1
  require 'set'
2
2
  require 'active_record'
3
+ require 'csv'
3
4
 
4
5
  module Sequent
5
6
  module Core
@@ -198,6 +199,10 @@ module Sequent
198
199
  record
199
200
  end
200
201
 
202
+ def create_records(record_class, array_of_value_hashes)
203
+ array_of_value_hashes.each { |values| create_record(record_class, values) }
204
+ end
205
+
201
206
  def create_or_update_record(record_class, values, created_at = Time.now)
202
207
  record = get_record(record_class, values)
203
208
  unless record
@@ -287,10 +292,8 @@ module Sequent
287
292
  csv = CSV.new("")
288
293
  column_names = clazz.column_names.reject { |name| name == "id" }
289
294
  records.each do |obj|
290
- begin
291
- csv << column_names.map do |column_name|
292
- @column_cache[clazz.name][column_name].type_cast_for_database(obj[column_name])
293
- end
295
+ csv << column_names.map do |column_name|
296
+ ActiveRecord::Base.connection.type_cast(obj[column_name], @column_cache[clazz.name][column_name])
294
297
  end
295
298
  end
296
299
 
@@ -311,7 +314,7 @@ module Sequent
311
314
  prepared_values = (1..column_names.size).map { |i| "$#{i}" }.join(",")
312
315
  records.each do |r|
313
316
  values = column_names.map do |column_name|
314
- @column_cache[clazz.name][column_name].type_cast_for_database(r[column_name.to_sym])
317
+ ActiveRecord::Base.connection.type_cast(r[column_name.to_sym], @column_cache[clazz.name][column_name])
315
318
  end
316
319
  inserts << values
317
320
  end
@@ -6,7 +6,7 @@ module Sequent
6
6
  class Oj
7
7
  ::Oj.default_options = {
8
8
  bigdecimal_as_decimal: false,
9
- mode: :compat,
9
+ mode: :rails,
10
10
  }
11
11
 
12
12
  def self.strict_load(json)
@@ -1,4 +1,5 @@
1
1
  require_relative 'core/core'
2
+ require_relative 'util/util'
2
3
  require_relative 'migrations/migrations'
3
4
  require_relative 'configuration'
4
5
 
@@ -43,11 +43,11 @@ module Sequent
43
43
  extend ActiveRecord::ConnectionAdapters::Quoting
44
44
 
45
45
  ORDERED_BY_STREAM = lambda do |event_store|
46
- event_records = quote_table_name(event_store.event_record_class.table_name)
47
- stream_records = quote_table_name(event_store.stream_record_class.table_name)
48
- snapshot_event_type = quote(event_store.snapshot_event_class)
46
+ event_records = quote_table_name(Sequent.configuration.event_record_class.table_name)
47
+ stream_records = quote_table_name(Sequent.configuration.stream_record_class.table_name)
48
+ snapshot_event_type = quote(Sequent.configuration.snapshot_event_class)
49
49
 
50
- event_store.event_record_class
50
+ Sequent.configuration.event_record_class
51
51
  .select("event_type, event_json")
52
52
  .joins("INNER JOIN #{stream_records} ON #{event_records}.stream_record_id = #{stream_records}.id")
53
53
  .where("event_type <> #{snapshot_event_type}")
@@ -1,4 +1,5 @@
1
1
  require 'thread_safe'
2
+ require 'sequent/core/event_store'
2
3
 
3
4
  module Sequent
4
5
  module Test
@@ -22,9 +23,9 @@ module Sequent
22
23
  # describe InvoiceCommandHandler do
23
24
  #
24
25
  # before :each do
25
- # @event_store = Sequent::Test::CommandHandlerHelpers::FakeEventStore.new
26
- # @repository = Sequent::Core::AggregateRepository.new(@event_store)
27
- # @command_handler = InvoiceCommandHandler.new(@repository)
26
+ # Sequent.configuration.event_store = Sequent::Test::CommandHandlerHelpers::FakeEventStore.new
27
+ # Sequent.configuration.command_handlers = [] # add your command handlers here
28
+ # Sequent.configuration.event_handlers = [] # add you event handlers (eg, workflows) here
28
29
  # end
29
30
  #
30
31
  # it "marks an invoice as paid" do
@@ -37,6 +38,8 @@ module Sequent
37
38
  module CommandHandlerHelpers
38
39
 
39
40
  class FakeEventStore
41
+ extend Forwardable
42
+
40
43
  def initialize
41
44
  @event_streams = {}
42
45
  @all_events = {}
@@ -73,6 +76,11 @@ module Sequent
73
76
  @all_events[event_stream.aggregate_id] += serialized
74
77
  @stored_events += serialized
75
78
  end
79
+ publish_events(streams_with_events.flat_map { |_, events| events })
80
+ end
81
+
82
+ def publish_events(events)
83
+ Sequent.configuration.event_publisher.publish_events(events)
76
84
  end
77
85
 
78
86
  def given_events(events)
@@ -122,22 +130,18 @@ module Sequent
122
130
  end
123
131
 
124
132
  def given_events *events
125
- @event_store.given_events(events.flatten(1))
133
+ Sequent.configuration.event_store.given_events(events.flatten(1))
126
134
  end
127
135
 
128
136
  def when_command command
129
- raise "@command_handler is mandatory when using the #{self.class}" unless @command_handler
130
- raise "Command handler #{@command_handler} cannot handle command #{command}, please configure the command type (forgot an include in the command class?)" unless @command_handler.class.handles_message?(command)
131
- @command_handler.handle_message(command)
132
- @repository.commit(command)
133
- @repository.clear
137
+ Sequent.configuration.command_service.execute_commands command
134
138
  end
135
139
 
136
140
  def then_events(*expected_events)
137
141
  expected_classes = expected_events.flatten(1).map { |event| event.class == Class ? event : event.class }
138
- expect(@event_store.stored_events.map(&:class)).to eq(expected_classes)
142
+ expect(Sequent.configuration.event_store.stored_events.map(&:class)).to eq(expected_classes)
139
143
 
140
- @event_store.stored_events.zip(expected_events.flatten(1)).each_with_index do |(actual, expected), index|
144
+ Sequent.configuration.event_store.stored_events.zip(expected_events.flatten(1)).each_with_index do |(actual, expected), index|
141
145
  next if expected.class == Class
142
146
  _actual = Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(actual.payload))
143
147
  _expected = Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(expected.payload))
@@ -41,9 +41,9 @@ module Sequent
41
41
 
42
42
  def then_events(*expected_events)
43
43
  expected_classes = expected_events.flatten(1).map { |event| event.class == Class ? event : event.class }
44
- expect(@event_store.stored_events.map(&:class)).to eq(expected_classes)
44
+ expect(Sequent.configuration.event_store.stored_events.map(&:class)).to eq(expected_classes)
45
45
 
46
- @event_store.stored_events.zip(expected_events.flatten(1)).each do |actual, expected|
46
+ Sequent.configuration.event_store.stored_events.zip(expected_events.flatten(1)).each do |actual, expected|
47
47
  next if expected.class == Class
48
48
  expect(Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(actual.payload))).to eq(Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(expected.payload))) if expected
49
49
  end
@@ -0,0 +1,15 @@
1
+ module Sequent
2
+ module Util
3
+ def self.skip_if_already_processing(already_processing_key, &block)
4
+ return if Thread.current[already_processing_key]
5
+
6
+ begin
7
+ Thread.current[already_processing_key] = true
8
+
9
+ block.yield
10
+ ensure
11
+ Thread.current[already_processing_key] = nil
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1 @@
1
+ require_relative 'skip_if_already_processing'
@@ -1,3 +1,3 @@
1
1
  module Sequent
2
- VERSION = '1.0.0'
2
+ VERSION = '2.0.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequent
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lars Vonk
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2017-06-30 00:00:00.000000000 Z
13
+ date: 2017-11-24 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 2.17.5
89
+ version: 3.3.9
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: 2.17.5
96
+ version: 3.3.9
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: thread_safe
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -214,6 +214,7 @@ files:
214
214
  - lib/sequent/core/command_service.rb
215
215
  - lib/sequent/core/core.rb
216
216
  - lib/sequent/core/event.rb
217
+ - lib/sequent/core/event_publisher.rb
217
218
  - lib/sequent/core/event_record.rb
218
219
  - lib/sequent/core/event_store.rb
219
220
  - lib/sequent/core/ext/ext.rb
@@ -239,7 +240,6 @@ files:
239
240
  - lib/sequent/core/record_sessions/record_sessions.rb
240
241
  - lib/sequent/core/record_sessions/replay_events_session.rb
241
242
  - lib/sequent/core/sequent_oj.rb
242
- - lib/sequent/core/snapshots.rb
243
243
  - lib/sequent/core/stream_record.rb
244
244
  - lib/sequent/core/transactions/active_record_transaction_provider.rb
245
245
  - lib/sequent/core/transactions/no_transactions.rb
@@ -259,6 +259,8 @@ files:
259
259
  - lib/sequent/test/event_handler_helpers.rb
260
260
  - lib/sequent/test/event_stream_helpers.rb
261
261
  - lib/sequent/test/time_comparison.rb
262
+ - lib/sequent/util/skip_if_already_processing.rb
263
+ - lib/sequent/util/util.rb
262
264
  - lib/version.rb
263
265
  homepage: https://github.com/zilverline/sequent
264
266
  licenses:
@@ -280,7 +282,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
280
282
  version: '0'
281
283
  requirements: []
282
284
  rubyforge_project:
283
- rubygems_version: 2.4.5
285
+ rubygems_version: 2.5.2
284
286
  signing_key:
285
287
  specification_version: 4
286
288
  summary: Event sourcing framework for Ruby
@@ -1,23 +0,0 @@
1
- module Sequent
2
- module Core
3
- class Snapshots
4
-
5
- def aggregates_that_need_snapshots(events_since_last_snapshot: 20, limit: 10, last_aggregate_id: nil)
6
- query = %Q{
7
- SELECT aggregate_id
8
- FROM event_records events
9
- WHERE aggregate_id > '#{last_aggregate_id}'
10
- GROUP BY aggregate_id
11
- HAVING MAX(sequence_number) - (COALESCE((SELECT MAX(sequence_number)
12
- FROM event_records snapshots
13
- WHERE event_type = 'Sequent::Core::SnapshotEvent'
14
- AND snapshots.aggregate_id = events.aggregate_id), 0)) > #{events_since_last_snapshot}
15
- ORDER BY aggregate_id
16
- LIMIT #{limit};
17
- }
18
- @record_class.connection.select_all(query).to_a
19
- end
20
-
21
- end
22
- end
23
- end