sequent 0.1.4 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/db/sequent_schema.rb +53 -0
- data/lib/sequent/configuration.rb +49 -0
- data/lib/sequent/core/aggregate_repository.rb +30 -19
- data/lib/sequent/core/aggregate_root.rb +59 -15
- data/lib/sequent/core/aggregate_snapshotter.rb +34 -0
- data/lib/sequent/core/base_command_handler.rb +4 -6
- data/lib/sequent/core/command_service.rb +29 -45
- data/lib/sequent/core/core.rb +2 -1
- data/lib/sequent/core/event.rb +4 -2
- data/lib/sequent/core/event_record.rb +4 -9
- data/lib/sequent/core/event_store.rb +80 -62
- data/lib/sequent/core/ext/ext.rb +9 -0
- data/lib/sequent/core/helpers/attribute_support.rb +0 -2
- data/lib/sequent/core/helpers/param_support.rb +17 -15
- data/lib/sequent/core/helpers/self_applier.rb +2 -4
- data/lib/sequent/core/snapshots.rb +23 -0
- data/lib/sequent/core/stream_record.rb +36 -0
- data/lib/sequent/core/tenant_event_store.rb +11 -5
- data/lib/sequent/migrations/migrate_events.rb +7 -4
- data/lib/sequent/sequent.rb +26 -0
- data/lib/sequent/test/command_handler_helpers.rb +48 -11
- data/lib/version.rb +1 -1
- metadata +35 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 814f54e9a769f6f73fc7c0cab26eebc4b9a191e1
|
4
|
+
data.tar.gz: c346224df919223cba73db9fe74097c235bacd32
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4e39adc404ed14163bd5b54b4ebdc558a12d4d56be8b60ad54140ee1cbc9102d89e17359a08d7a37eeeb299b99788359777d9f6cf778362969cb45b3182d493a
|
7
|
+
data.tar.gz: 667ab8ca0e0b5255184f162210d33a6775b26e952e8868a42a6a8a8ef408b24cec46f118c6fa9400b71a01632a0fb32388e2e23a6ab9362301f2247c2730726a
|
@@ -0,0 +1,53 @@
|
|
1
|
+
ActiveRecord::Schema.define do
|
2
|
+
|
3
|
+
create_table "event_records", :force => true do |t|
|
4
|
+
t.string "aggregate_id", :null => false
|
5
|
+
t.string "organization_id"
|
6
|
+
t.integer "sequence_number", :null => false
|
7
|
+
t.datetime "created_at", :null => false
|
8
|
+
t.string "event_type", :null => false
|
9
|
+
t.text "event_json", :null => false
|
10
|
+
t.integer "command_record_id", :null => false
|
11
|
+
t.integer "stream_record_id", :null => false
|
12
|
+
end
|
13
|
+
|
14
|
+
create_table "command_records", :force => true do |t|
|
15
|
+
t.string "organization_id"
|
16
|
+
t.string "user_id"
|
17
|
+
t.string "aggregate_id"
|
18
|
+
t.string "command_type", :null => false
|
19
|
+
t.text "command_json", :null => false
|
20
|
+
t.datetime "created_at", :null => false
|
21
|
+
end
|
22
|
+
|
23
|
+
execute %Q{
|
24
|
+
CREATE UNIQUE INDEX unique_event_per_aggregate ON event_records (
|
25
|
+
aggregate_id,
|
26
|
+
sequence_number,
|
27
|
+
(CASE event_type WHEN 'Sequent::Core::SnapshotEvent' THEN 0 ELSE 1 END)
|
28
|
+
)
|
29
|
+
}
|
30
|
+
execute %Q{
|
31
|
+
CREATE INDEX snapshot_events ON event_records (aggregate_id, sequence_number DESC) WHERE event_type = 'Sequent::Core::SnapshotEvent'
|
32
|
+
}
|
33
|
+
add_index "event_records", ["command_record_id"], :name => "index_event_records_on_command_record_id"
|
34
|
+
add_index "event_records", ["organization_id"], :name => "index_event_records_on_organization_id"
|
35
|
+
add_index "event_records", ["event_type"], :name => "index_event_records_on_event_type"
|
36
|
+
add_index "event_records", ["created_at"], :name => "index_event_records_on_created_at"
|
37
|
+
|
38
|
+
create_table "stream_records", :force => true do |t|
|
39
|
+
t.datetime "created_at", :null => false
|
40
|
+
t.string "aggregate_type", :null => false
|
41
|
+
t.string "aggregate_id", :null => false
|
42
|
+
t.integer "snapshot_threshold"
|
43
|
+
end
|
44
|
+
|
45
|
+
add_index "stream_records", ["aggregate_id"], :name => "index_stream_records_on_aggregate_id", :unique => true
|
46
|
+
execute %q{
|
47
|
+
ALTER TABLE event_records ADD CONSTRAINT command_fkey FOREIGN KEY (command_record_id) REFERENCES command_records (id)
|
48
|
+
}
|
49
|
+
execute %q{
|
50
|
+
ALTER TABLE event_records ADD CONSTRAINT stream_fkey FOREIGN KEY (stream_record_id) REFERENCES stream_records (id)
|
51
|
+
}
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require_relative 'core/event_store'
|
2
|
+
require_relative 'core/command_service'
|
3
|
+
require_relative 'core/transactions/no_transactions'
|
4
|
+
require_relative 'core/aggregate_repository'
|
5
|
+
|
6
|
+
module Sequent
|
7
|
+
class Configuration
|
8
|
+
attr_reader :aggregate_repository
|
9
|
+
|
10
|
+
attr_accessor :event_store,
|
11
|
+
:command_service,
|
12
|
+
:event_record_class,
|
13
|
+
:stream_record_class,
|
14
|
+
:snapshot_event_class,
|
15
|
+
:transaction_provider
|
16
|
+
|
17
|
+
attr_accessor :command_handlers,
|
18
|
+
:command_filters
|
19
|
+
|
20
|
+
attr_accessor :event_handlers
|
21
|
+
|
22
|
+
def self.instance
|
23
|
+
@instance ||= new
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.reset
|
27
|
+
@instance = new
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
self.command_handlers = []
|
32
|
+
self.command_filters = []
|
33
|
+
self.event_handlers = []
|
34
|
+
|
35
|
+
self.event_store = Sequent::Core::EventStore.new(self)
|
36
|
+
self.command_service = Sequent::Core::CommandService.new(self)
|
37
|
+
self.event_record_class = Sequent::Core::EventRecord
|
38
|
+
self.stream_record_class = Sequent::Core::StreamRecord
|
39
|
+
self.snapshot_event_class = Sequent::Core::SnapshotEvent
|
40
|
+
self.transaction_provider = Sequent::Core::Transactions::NoTransactions.new
|
41
|
+
end
|
42
|
+
|
43
|
+
def event_store=(event_store)
|
44
|
+
@event_store = event_store
|
45
|
+
@aggregate_repository = Sequent::Core::AggregateRepository.new(event_store)
|
46
|
+
self.command_handlers.each { |c| c.repository = @aggregate_repository }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -16,12 +16,20 @@ 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
|
+
|
19
21
|
class NonUniqueAggregateId < Exception
|
20
22
|
def initialize(existing, new)
|
21
23
|
super "Duplicate aggregate #{new} with same key as existing #{existing}"
|
22
24
|
end
|
23
25
|
end
|
24
26
|
|
27
|
+
class AggregateNotFound < Exception
|
28
|
+
def initialize(id)
|
29
|
+
super "Aggregate with id #{id} not found"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
25
33
|
def initialize(event_store)
|
26
34
|
@event_store = event_store
|
27
35
|
clear
|
@@ -33,8 +41,9 @@ module Sequent
|
|
33
41
|
# and all uncammited_events are stored in the +event_store+
|
34
42
|
#
|
35
43
|
def add_aggregate(aggregate)
|
36
|
-
|
37
|
-
|
44
|
+
existing = aggregates[aggregate.id]
|
45
|
+
if existing && !existing.equal?(aggregate)
|
46
|
+
raise NonUniqueAggregateId.new(aggregate, aggregates[aggregate.id])
|
38
47
|
else
|
39
48
|
aggregates[aggregate.id] = aggregate
|
40
49
|
end
|
@@ -47,17 +56,17 @@ module Sequent
|
|
47
56
|
|
48
57
|
# Loads aggregate by given id and class
|
49
58
|
# Returns the one in the current Unit Of Work otherwise loads it from history.
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
result
|
57
|
-
else
|
58
|
-
events = @event_store.load_events(aggregate_id)
|
59
|
-
aggregates[aggregate_id] = clazz.load_from_history(events)
|
59
|
+
def load_aggregate(aggregate_id, clazz = nil)
|
60
|
+
result = aggregates.fetch(aggregate_id) do |aggregate_id|
|
61
|
+
stream, events = @event_store.load_events(aggregate_id)
|
62
|
+
raise AggregateNotFound.new(aggregate_id) unless stream
|
63
|
+
aggregate_class = Class.const_get(stream.aggregate_type)
|
64
|
+
aggregates[aggregate_id] = aggregate_class.load_from_history(stream, events)
|
60
65
|
end
|
66
|
+
|
67
|
+
raise TypeError, "#{result.class} is not a #{clazz}" if result && clazz && !(result.class <= clazz)
|
68
|
+
|
69
|
+
result
|
61
70
|
end
|
62
71
|
|
63
72
|
# Gets all uncommitted_events from the 'registered' aggregates
|
@@ -68,11 +77,13 @@ module Sequent
|
|
68
77
|
# This is all abstracted away if you use the Sequent::Core::CommandService
|
69
78
|
#
|
70
79
|
def commit(command)
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
80
|
+
updated_aggregates = aggregates.values.reject {|x| x.uncommitted_events.empty?}
|
81
|
+
return if updated_aggregates.empty?
|
82
|
+
streams_with_events = updated_aggregates.map do |aggregate|
|
83
|
+
[ aggregate.event_stream, aggregate.uncommitted_events ]
|
84
|
+
end
|
85
|
+
updated_aggregates.each(&:clear_events)
|
86
|
+
store_events command, streams_with_events
|
76
87
|
end
|
77
88
|
|
78
89
|
# Clears the Unit of Work.
|
@@ -86,8 +97,8 @@ module Sequent
|
|
86
97
|
Thread.current[AGGREGATES_KEY]
|
87
98
|
end
|
88
99
|
|
89
|
-
def store_events(command,
|
90
|
-
@event_store.commit_events(command,
|
100
|
+
def store_events(command, streams_with_events)
|
101
|
+
@event_store.commit_events(command, streams_with_events)
|
91
102
|
end
|
92
103
|
end
|
93
104
|
end
|
@@ -1,19 +1,51 @@
|
|
1
|
+
require 'base64'
|
1
2
|
require_relative 'helpers/self_applier'
|
3
|
+
require_relative 'stream_record'
|
2
4
|
|
3
5
|
module Sequent
|
4
6
|
module Core
|
7
|
+
module SnapshotConfiguration
|
8
|
+
module ClassMethods
|
9
|
+
##
|
10
|
+
# Enable snapshots for this aggregate. The aggregate instance
|
11
|
+
# must define the *load_from_snapshot* and *save_to_snapshot*
|
12
|
+
# methods.
|
13
|
+
#
|
14
|
+
def enable_snapshots(default_threshold: 20)
|
15
|
+
@snapshot_default_threshold = default_threshold
|
16
|
+
end
|
17
|
+
|
18
|
+
def snapshots_enabled?
|
19
|
+
!snapshot_default_threshold.nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :snapshot_default_threshold
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.included(host_class)
|
26
|
+
host_class.extend(ClassMethods)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
5
30
|
# Base class for all your domain classes.
|
6
31
|
#
|
7
32
|
# +load_from_history+ functionality to be loaded_from_history, meaning a stream of events.
|
8
33
|
#
|
9
34
|
class AggregateRoot
|
10
35
|
include Helpers::SelfApplier
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
36
|
+
include SnapshotConfiguration
|
37
|
+
|
38
|
+
attr_reader :id, :uncommitted_events, :sequence_number, :event_stream
|
39
|
+
|
40
|
+
def self.load_from_history(stream, events)
|
41
|
+
first, *rest = events
|
42
|
+
if first.is_a? SnapshotEvent
|
43
|
+
aggregate_root = Marshal.load(Base64.decode64(first.data))
|
44
|
+
rest.each { |x| aggregate_root.apply_event(x) }
|
45
|
+
else
|
46
|
+
aggregate_root = allocate() # allocate without calling new
|
47
|
+
aggregate_root.load_from_history(stream, events)
|
48
|
+
end
|
17
49
|
aggregate_root
|
18
50
|
end
|
19
51
|
|
@@ -21,23 +53,36 @@ module Sequent
|
|
21
53
|
@id = id
|
22
54
|
@uncommitted_events = []
|
23
55
|
@sequence_number = 1
|
56
|
+
@event_stream = EventStream.new aggregate_type: self.class.name,
|
57
|
+
aggregate_id: id,
|
58
|
+
snapshot_threshold: self.class.snapshot_default_threshold
|
24
59
|
end
|
25
60
|
|
26
|
-
def load_from_history(events)
|
61
|
+
def load_from_history(stream, events)
|
27
62
|
raise "Empty history" if events.empty?
|
28
63
|
@id = events.first.aggregate_id
|
29
64
|
@uncommitted_events = []
|
30
|
-
@sequence_number =
|
31
|
-
|
65
|
+
@sequence_number = 1
|
66
|
+
@event_stream = stream
|
67
|
+
events.each { |event| apply_event(event) }
|
32
68
|
end
|
33
69
|
|
34
70
|
def to_s
|
35
71
|
"#{self.class.name}: #{@id}"
|
36
72
|
end
|
37
73
|
|
38
|
-
|
39
74
|
def clear_events
|
40
|
-
uncommitted_events
|
75
|
+
@uncommitted_events = []
|
76
|
+
end
|
77
|
+
|
78
|
+
def take_snapshot!
|
79
|
+
snapshot = build_event SnapshotEvent, data: Base64.encode64(Marshal.dump(self))
|
80
|
+
@uncommitted_events << snapshot
|
81
|
+
end
|
82
|
+
|
83
|
+
def apply_event(event)
|
84
|
+
handle_message(event)
|
85
|
+
@sequence_number = event.sequence_number + 1
|
41
86
|
end
|
42
87
|
|
43
88
|
protected
|
@@ -54,9 +99,8 @@ module Sequent
|
|
54
99
|
#
|
55
100
|
def apply(event, params={})
|
56
101
|
event = build_event(event, params) if event.is_a?(Class)
|
57
|
-
|
102
|
+
apply_event(event)
|
58
103
|
@uncommitted_events << event
|
59
|
-
@sequence_number += 1
|
60
104
|
end
|
61
105
|
end
|
62
106
|
|
@@ -71,10 +115,10 @@ module Sequent
|
|
71
115
|
@organization_id = organization_id
|
72
116
|
end
|
73
117
|
|
74
|
-
def load_from_history(events)
|
118
|
+
def load_from_history(stream, events)
|
75
119
|
raise "Empty history" if events.empty?
|
76
120
|
@organization_id = events.first.organization_id
|
77
|
-
super
|
121
|
+
super
|
78
122
|
end
|
79
123
|
|
80
124
|
protected
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Sequent
|
2
|
+
module Core
|
3
|
+
class SnapshotCommand < Sequent::Core::BaseCommand
|
4
|
+
attrs limit: Integer
|
5
|
+
end
|
6
|
+
|
7
|
+
class AggregateSnapshotter < BaseCommandHandler
|
8
|
+
|
9
|
+
def handles_message?(message)
|
10
|
+
message.is_a? SnapshotCommand
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# Take up to `limit` snapshots when needed. Throws `:done` when done.
|
15
|
+
#
|
16
|
+
on SnapshotCommand do |command|
|
17
|
+
aggregate_ids = repository.event_store.aggregates_that_need_snapshots(@last_aggregate_id, command.limit)
|
18
|
+
aggregate_ids.each do |aggregate_id|
|
19
|
+
take_snapshot!(aggregate_id)
|
20
|
+
end
|
21
|
+
@last_aggregate_id = aggregate_ids.last
|
22
|
+
throw :done if @last_aggregate_id.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
def take_snapshot!(aggregate_id)
|
26
|
+
aggregate = @repository.load_aggregate(aggregate_id)
|
27
|
+
Sequent.logger.info "Taking snapshot for aggregate #{aggregate}"
|
28
|
+
aggregate.take_snapshot!
|
29
|
+
rescue => e
|
30
|
+
Sequent.logger.warn "Failed to take snapshot for aggregate #{aggregate_id}: #{e}", e.inspect
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -12,7 +12,7 @@ module Sequent
|
|
12
12
|
# repository.add_aggregate Invoice.new(command.aggregate_id)
|
13
13
|
# end
|
14
14
|
#
|
15
|
-
# on
|
15
|
+
# on PayInvoiceCommand do |command|
|
16
16
|
# do_with_aggregate(command, Invoice) {|invoice|invoice.pay(command.pay_date)}
|
17
17
|
# end
|
18
18
|
# end
|
@@ -20,7 +20,9 @@ module Sequent
|
|
20
20
|
include Sequent::Core::Helpers::SelfApplier,
|
21
21
|
Sequent::Core::Helpers::UuidHelper
|
22
22
|
|
23
|
-
|
23
|
+
attr_accessor :repository
|
24
|
+
|
25
|
+
def initialize(repository = Sequent.configuration.aggregate_repository)
|
24
26
|
@repository = repository
|
25
27
|
end
|
26
28
|
|
@@ -30,10 +32,6 @@ module Sequent
|
|
30
32
|
yield aggregate if block_given?
|
31
33
|
end
|
32
34
|
|
33
|
-
protected
|
34
|
-
def repository
|
35
|
-
@repository
|
36
|
-
end
|
37
35
|
end
|
38
36
|
end
|
39
37
|
end
|
@@ -2,21 +2,6 @@ require_relative 'transactions/no_transactions'
|
|
2
2
|
|
3
3
|
module Sequent
|
4
4
|
module Core
|
5
|
-
|
6
|
-
class CommandServiceConfiguration
|
7
|
-
attr_accessor :event_store,
|
8
|
-
:command_handler_classes,
|
9
|
-
:transaction_provider,
|
10
|
-
:filters
|
11
|
-
|
12
|
-
def initialize
|
13
|
-
@command_handler_classes = []
|
14
|
-
@transaction_provider = Sequent::Core::Transactions::NoTransactions.new
|
15
|
-
@filters = []
|
16
|
-
end
|
17
|
-
|
18
|
-
end
|
19
|
-
|
20
5
|
#
|
21
6
|
# Single point in the application where subclasses of Sequent::Core::BaseCommand
|
22
7
|
# are executed. This will initiate the entire flow of:
|
@@ -28,21 +13,7 @@ module Sequent
|
|
28
13
|
# * Unit of Work is cleared
|
29
14
|
#
|
30
15
|
class CommandService
|
31
|
-
|
32
|
-
class << self
|
33
|
-
attr_accessor :configuration,
|
34
|
-
:instance
|
35
|
-
end
|
36
|
-
|
37
|
-
# Creates a new CommandService and overwrites all existing config.
|
38
|
-
# The new CommandService can be retrieved via the +CommandService.instance+ method.
|
39
|
-
#
|
40
|
-
# If you don't want a singleton you can always instantiate it yourself using the +CommandService.new+.
|
41
|
-
def self.configure
|
42
|
-
self.configuration = CommandServiceConfiguration.new
|
43
|
-
yield(configuration) if block_given?
|
44
|
-
self.instance = CommandService.new(configuration)
|
45
|
-
end
|
16
|
+
attr_accessor :configuration
|
46
17
|
|
47
18
|
# +DefaultCommandServiceConfiguration+ Configuration class for the CommandService containing:
|
48
19
|
#
|
@@ -51,11 +22,7 @@ module Sequent
|
|
51
22
|
# +transaction_provider+ How to do transaction management. Defaults to Sequent::Core::Transactions::NoTransactions
|
52
23
|
# +filters+ List of filter that respond_to :execute(command). Can be useful to do extra checks (security and such).
|
53
24
|
def initialize(configuration = CommandServiceConfiguration.new)
|
54
|
-
|
55
|
-
@repository = AggregateRepository.new(configuration.event_store)
|
56
|
-
@filters = configuration.filters
|
57
|
-
@transaction_provider = configuration.transaction_provider
|
58
|
-
@command_handlers = configuration.command_handler_classes.map { |handler| handler.new(@repository) }
|
25
|
+
self.configuration = configuration
|
59
26
|
end
|
60
27
|
|
61
28
|
# Executes the given commands in a single transactional block as implemented by the +transaction_provider+
|
@@ -67,26 +34,47 @@ module Sequent
|
|
67
34
|
# * The +repository+ commits the command and all uncommitted_events resulting from the command
|
68
35
|
def execute_commands(*commands)
|
69
36
|
begin
|
70
|
-
|
37
|
+
transaction_provider.transactional do
|
71
38
|
commands.each do |command|
|
72
|
-
|
39
|
+
filters.each { |filter| filter.execute(command) }
|
73
40
|
|
74
41
|
raise CommandNotValid.new(command) unless command.valid?
|
75
42
|
parsed_command = command.parse_attrs_to_correct_types
|
76
|
-
|
77
|
-
|
43
|
+
command_handlers.select { |h| h.handles_message?(parsed_command) }.each { |h| h.handle_message parsed_command }
|
44
|
+
repository.commit(parsed_command)
|
78
45
|
end
|
79
46
|
end
|
80
47
|
ensure
|
81
|
-
|
48
|
+
repository.clear
|
82
49
|
end
|
83
50
|
|
84
51
|
end
|
85
52
|
|
86
53
|
def remove_event_handler(clazz)
|
87
|
-
|
54
|
+
event_store.remove_event_handler(clazz)
|
88
55
|
end
|
89
56
|
|
57
|
+
private
|
58
|
+
|
59
|
+
def event_store
|
60
|
+
configuration.event_store
|
61
|
+
end
|
62
|
+
|
63
|
+
def repository
|
64
|
+
configuration.aggregate_repository
|
65
|
+
end
|
66
|
+
|
67
|
+
def filters
|
68
|
+
configuration.command_filters
|
69
|
+
end
|
70
|
+
|
71
|
+
def transaction_provider
|
72
|
+
configuration.transaction_provider
|
73
|
+
end
|
74
|
+
|
75
|
+
def command_handlers
|
76
|
+
configuration.command_handlers
|
77
|
+
end
|
90
78
|
end
|
91
79
|
|
92
80
|
# Raised when BaseCommand.valid? returns false
|
@@ -106,9 +94,5 @@ module Sequent
|
|
106
94
|
errors(@command.class.to_s.underscore)
|
107
95
|
end
|
108
96
|
end
|
109
|
-
|
110
97
|
end
|
111
98
|
end
|
112
|
-
|
113
|
-
|
114
|
-
|
data/lib/sequent/core/core.rb
CHANGED
@@ -2,15 +2,16 @@ require_relative 'sequent_oj'
|
|
2
2
|
require_relative 'helpers/helpers'
|
3
3
|
require_relative 'record_sessions/record_sessions'
|
4
4
|
require_relative 'transactions/transactions'
|
5
|
+
require_relative 'event'
|
5
6
|
require_relative 'aggregate_repository'
|
6
7
|
require_relative 'aggregate_root'
|
7
8
|
require_relative 'base_command_handler'
|
8
9
|
require_relative 'command'
|
9
10
|
require_relative 'command_service'
|
10
|
-
require_relative 'event'
|
11
11
|
require_relative 'value_object'
|
12
12
|
require_relative 'base_event_handler'
|
13
13
|
require_relative 'event_store'
|
14
14
|
require_relative 'tenant_event_store'
|
15
15
|
require_relative 'event_record'
|
16
16
|
require_relative 'command_record'
|
17
|
+
require_relative 'aggregate_snapshotter'
|
data/lib/sequent/core/event.rb
CHANGED
@@ -5,10 +5,9 @@ module Sequent
|
|
5
5
|
module Core
|
6
6
|
|
7
7
|
module SerializesEvent
|
8
|
-
|
9
8
|
def event
|
10
9
|
payload = Sequent::Core::Oj.strict_load(self.event_json)
|
11
|
-
Class.const_get(self.event_type
|
10
|
+
Class.const_get(self.event_type).deserialize_from_json(payload)
|
12
11
|
end
|
13
12
|
|
14
13
|
def event=(event)
|
@@ -19,7 +18,6 @@ module Sequent
|
|
19
18
|
self.created_at = event.created_at
|
20
19
|
self.event_json = Sequent::Core::Oj.dump(event.attributes)
|
21
20
|
end
|
22
|
-
|
23
21
|
end
|
24
22
|
|
25
23
|
class EventRecord < ActiveRecord::Base
|
@@ -27,15 +25,12 @@ module Sequent
|
|
27
25
|
|
28
26
|
self.table_name = "event_records"
|
29
27
|
|
28
|
+
belongs_to :stream_record
|
30
29
|
belongs_to :command_record
|
31
30
|
|
32
|
-
validates_presence_of :aggregate_id, :sequence_number, :event_type, :event_json
|
33
|
-
validates_numericality_of :sequence_number
|
34
|
-
|
35
|
-
|
36
|
-
|
31
|
+
validates_presence_of :aggregate_id, :sequence_number, :event_type, :event_json, :stream_record, :command_record
|
32
|
+
validates_numericality_of :sequence_number, only_integer: true, greater_than: 0
|
37
33
|
end
|
38
34
|
|
39
35
|
end
|
40
36
|
end
|
41
|
-
|
@@ -1,90 +1,105 @@
|
|
1
|
+
require 'forwardable'
|
1
2
|
require_relative 'event_record'
|
2
3
|
require_relative 'sequent_oj'
|
3
4
|
|
4
5
|
module Sequent
|
5
6
|
module Core
|
6
|
-
class EventStoreConfiguration
|
7
|
-
attr_accessor :record_class, :event_handlers
|
8
|
-
|
9
|
-
def initialize(record_class = Sequent::Core::EventRecord, event_handlers = [])
|
10
|
-
@record_class = record_class
|
11
|
-
@event_handlers = event_handlers
|
12
|
-
end
|
13
|
-
end
|
14
7
|
|
15
8
|
class EventStore
|
9
|
+
include ActiveRecord::ConnectionAdapters::Quoting
|
10
|
+
extend Forwardable
|
16
11
|
|
17
|
-
|
18
|
-
|
19
|
-
:instance
|
20
|
-
end
|
21
|
-
|
22
|
-
# Creates a new EventStore and overwrites all existing config.
|
23
|
-
# The new EventStore can be retrieved via the +EventStore.instance+ method.
|
24
|
-
#
|
25
|
-
# If you don't want a singleton you can always instantiate it yourself using the +EventStore.new+.
|
26
|
-
def self.configure
|
27
|
-
self.configuration = EventStoreConfiguration.new
|
28
|
-
yield(configuration) if block_given?
|
29
|
-
EventStore.instance = EventStore.new(configuration)
|
30
|
-
end
|
12
|
+
attr_accessor :configuration
|
13
|
+
def_delegators :@configuration, :stream_record_class, :event_record_class, :snapshot_event_class, :event_handlers
|
31
14
|
|
32
|
-
def initialize(configuration =
|
33
|
-
|
34
|
-
@
|
15
|
+
def initialize(configuration = Sequent.configuration)
|
16
|
+
self.configuration = configuration
|
17
|
+
@event_types = ThreadSafe::Cache.new
|
35
18
|
end
|
36
19
|
|
37
20
|
##
|
38
21
|
# Stores the events in the EventStore and publishes the events
|
39
22
|
# to the registered event_handlers.
|
40
23
|
#
|
41
|
-
|
42
|
-
|
43
|
-
|
24
|
+
# Streams_with_Events is an enumerable of pairs from
|
25
|
+
# `StreamRecord` to arrays of uncommitted `Event`s.
|
26
|
+
#
|
27
|
+
def commit_events(command, streams_with_events)
|
28
|
+
store_events(command, streams_with_events)
|
29
|
+
publish_events(streams_with_events.flat_map {|_, events| events}, event_handlers)
|
44
30
|
end
|
45
31
|
|
46
32
|
##
|
47
33
|
# Returns all events for the aggregate ordered by sequence_number
|
48
34
|
#
|
49
35
|
def load_events(aggregate_id)
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
36
|
+
stream = stream_record_class.where(aggregate_id: aggregate_id).first!
|
37
|
+
events = event_record_class.connection.select_all(%Q{
|
38
|
+
SELECT event_type, event_json
|
39
|
+
FROM #{quote_table_name event_record_class.table_name}
|
40
|
+
WHERE aggregate_id = #{quote aggregate_id}
|
41
|
+
AND sequence_number >= COALESCE((SELECT MAX(sequence_number)
|
42
|
+
FROM #{quote_table_name event_record_class.table_name}
|
43
|
+
WHERE event_type = #{quote snapshot_event_class.name}
|
44
|
+
AND aggregate_id = #{quote aggregate_id}), 0)
|
45
|
+
ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote snapshot_event_class.name} THEN 0 ELSE 1 END) ASC
|
46
|
+
}).map! do |event_hash|
|
47
|
+
deserialize_event(event_hash)
|
58
48
|
end
|
49
|
+
[stream.event_stream, events]
|
59
50
|
end
|
60
51
|
|
61
52
|
##
|
62
53
|
# Replays all events in the event store to the registered event_handlers.
|
63
54
|
#
|
64
|
-
# @param block that returns the
|
55
|
+
# @param block that returns the events.
|
65
56
|
def replay_events
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
57
|
+
events = yield.map {|event_hash| deserialize_event(event_hash)}
|
58
|
+
publish_events(events, event_handlers)
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# Returns the ids of aggregates that need a new snapshot.
|
63
|
+
#
|
64
|
+
def aggregates_that_need_snapshots(last_aggregate_id, limit = 10)
|
65
|
+
stream_table = quote_table_name stream_record_class.table_name
|
66
|
+
event_table = quote_table_name event_record_class.table_name
|
67
|
+
query = %Q{
|
68
|
+
SELECT aggregate_id
|
69
|
+
FROM #{stream_table} stream
|
70
|
+
WHERE aggregate_id > COALESCE(#{quote last_aggregate_id}, '')
|
71
|
+
AND snapshot_threshold IS NOT NULL
|
72
|
+
AND snapshot_threshold <= (
|
73
|
+
(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) -
|
74
|
+
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))
|
75
|
+
ORDER BY aggregate_id
|
76
|
+
LIMIT #{quote limit}
|
77
|
+
FOR UPDATE
|
78
|
+
}
|
79
|
+
event_record_class.connection.select_all(query).map {|x| x['aggregate_id']}
|
79
80
|
end
|
80
81
|
|
81
|
-
|
82
|
-
|
83
|
-
|
82
|
+
def find_event_stream(aggregate_id)
|
83
|
+
record = stream_record_class.where(aggregate_id: aggregate_id).first
|
84
|
+
if record
|
85
|
+
record.event_stream
|
86
|
+
else
|
87
|
+
nil
|
88
|
+
end
|
84
89
|
end
|
85
90
|
|
86
91
|
private
|
87
92
|
|
93
|
+
def deserialize_event(event_hash)
|
94
|
+
event_type = event_hash.fetch("event_type")
|
95
|
+
event_json = Sequent::Core::Oj.strict_load(event_hash.fetch("event_json"))
|
96
|
+
resolve_event_type(event_type).deserialize_from_json(event_json)
|
97
|
+
end
|
98
|
+
|
99
|
+
def resolve_event_type(event_type)
|
100
|
+
@event_types.fetch_or_store(event_type) { |k| Class.const_get(k) }
|
101
|
+
end
|
102
|
+
|
88
103
|
def publish_events(events, event_handlers)
|
89
104
|
events.each do |event|
|
90
105
|
event_handlers.each do |handler|
|
@@ -93,17 +108,20 @@ module Sequent
|
|
93
108
|
end
|
94
109
|
end
|
95
110
|
|
96
|
-
def
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
111
|
+
def store_events(command, streams_with_events = [])
|
112
|
+
command_record = CommandRecord.create!(command: command)
|
113
|
+
streams_with_events.each do |event_stream, uncommitted_events|
|
114
|
+
unless event_stream.stream_record_id
|
115
|
+
stream_record = stream_record_class.new
|
116
|
+
stream_record.event_stream = event_stream
|
117
|
+
stream_record.save!
|
118
|
+
event_stream.stream_record_id = stream_record.id
|
119
|
+
end
|
120
|
+
uncommitted_events.each do |event|
|
121
|
+
event_record_class.create!(command_record: command_record, stream_record_id: event_stream.stream_record_id, event: event)
|
122
|
+
end
|
104
123
|
end
|
105
124
|
end
|
106
|
-
|
107
125
|
end
|
108
126
|
|
109
127
|
end
|
data/lib/sequent/core/ext/ext.rb
CHANGED
@@ -12,28 +12,30 @@ module Sequent
|
|
12
12
|
module ParamSupport
|
13
13
|
module ClassMethods
|
14
14
|
def from_params(params = {})
|
15
|
-
|
16
|
-
params = HashWithIndifferentAccess.new(params)
|
17
|
-
result.class.types.each do |attribute, type|
|
18
|
-
value = params[attribute]
|
19
|
-
|
20
|
-
next if value.blank?
|
21
|
-
if type.respond_to? :from_params
|
22
|
-
value = type.from_params(value)
|
23
|
-
elsif type.is_a? Sequent::Core::Helpers::ArrayWithType
|
24
|
-
value = value.map { |v| type.item_type.from_params(v) }
|
25
|
-
end
|
26
|
-
result.instance_variable_set(:"@#{attribute}", value)
|
27
|
-
end
|
28
|
-
result
|
15
|
+
allocate.tap { |x| x.from_params(params) }
|
29
16
|
end
|
30
|
-
|
31
17
|
end
|
18
|
+
|
32
19
|
# extend host class with class methods when we're included
|
33
20
|
def self.included(host_class)
|
34
21
|
host_class.extend(ClassMethods)
|
35
22
|
end
|
36
23
|
|
24
|
+
def from_params(params)
|
25
|
+
params = HashWithIndifferentAccess.new(params)
|
26
|
+
self.class.types.each do |attribute, type|
|
27
|
+
value = params[attribute]
|
28
|
+
|
29
|
+
next if value.blank?
|
30
|
+
if type.respond_to? :from_params
|
31
|
+
value = type.from_params(value)
|
32
|
+
elsif type.is_a? Sequent::Core::Helpers::ArrayWithType
|
33
|
+
value = value.map { |v| type.item_type.from_params(v) }
|
34
|
+
end
|
35
|
+
instance_variable_set(:"@#{attribute}", value)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
37
39
|
def to_params(root)
|
38
40
|
make_params root, as_params
|
39
41
|
end
|
@@ -19,12 +19,11 @@ module Sequent
|
|
19
19
|
module ClassMethods
|
20
20
|
|
21
21
|
def on(*message_classes, &block)
|
22
|
-
|
23
|
-
message_classes.each { |message_class| @message_mapping[message_class] = block }
|
22
|
+
message_classes.each { |message_class| message_mapping[message_class] = block }
|
24
23
|
end
|
25
24
|
|
26
25
|
def message_mapping
|
27
|
-
@message_mapping
|
26
|
+
@message_mapping ||= {}
|
28
27
|
end
|
29
28
|
end
|
30
29
|
|
@@ -42,4 +41,3 @@ module Sequent
|
|
42
41
|
end
|
43
42
|
end
|
44
43
|
end
|
45
|
-
|
@@ -0,0 +1,23 @@
|
|
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
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Sequent
|
4
|
+
module Core
|
5
|
+
class EventStream
|
6
|
+
attr_accessor :aggregate_type, :aggregate_id, :snapshot_threshold, :stream_record_id
|
7
|
+
|
8
|
+
def initialize(aggregate_type:, aggregate_id:, snapshot_threshold: nil, stream_record_id: nil)
|
9
|
+
@aggregate_type = aggregate_type
|
10
|
+
@aggregate_id = aggregate_id
|
11
|
+
@snapshot_threshold = snapshot_threshold
|
12
|
+
@stream_record_id = stream_record_id
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class StreamRecord < ActiveRecord::Base
|
17
|
+
|
18
|
+
self.table_name = "stream_records"
|
19
|
+
|
20
|
+
validates_presence_of :aggregate_type, :aggregate_id
|
21
|
+
validates_numericality_of :snapshot_threshold, only_integer: true, greater_than: 0, allow_nil: true
|
22
|
+
|
23
|
+
has_many :events
|
24
|
+
|
25
|
+
def event_stream
|
26
|
+
EventStream.new(aggregate_type: aggregate_type, aggregate_id: aggregate_id, snapshot_threshold: snapshot_threshold, stream_record_id: id)
|
27
|
+
end
|
28
|
+
|
29
|
+
def event_stream=(data)
|
30
|
+
self.aggregate_type = data.aggregate_type
|
31
|
+
self.aggregate_id = data.aggregate_id
|
32
|
+
self.snapshot_threshold = data.snapshot_threshold
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -4,15 +4,21 @@
|
|
4
4
|
module Sequent
|
5
5
|
module Core
|
6
6
|
class TenantEventStore < EventStore
|
7
|
-
|
8
7
|
def replay_events_for(organization_id)
|
9
8
|
replay_events do
|
10
|
-
|
11
|
-
|
9
|
+
event_record_class.connection.select_all(%Q{
|
10
|
+
SELECT events.event_type, events.event_json
|
11
|
+
FROM #{quote_table_name event_record_class.table_name} events
|
12
|
+
WHERE events.aggregate_id IN (SELECT aggregates.aggregate_id
|
13
|
+
FROM #{quote_table_name event_record_class.table_name} aggregates
|
14
|
+
WHERE aggregates.organization_id = #{quote organization_id}
|
15
|
+
AND aggregates.sequence_number = 1
|
16
|
+
AND aggregates.event_type <> #{quote snapshot_event_class.name})
|
17
|
+
AND events.event_type <> #{quote snapshot_event_class.name}
|
18
|
+
ORDER BY events.id
|
19
|
+
})
|
12
20
|
end
|
13
21
|
end
|
14
|
-
|
15
22
|
end
|
16
|
-
|
17
23
|
end
|
18
24
|
end
|
@@ -36,10 +36,14 @@ module Sequent
|
|
36
36
|
def execute_migrations(current_version, new_version, &after_migration_block)
|
37
37
|
if current_version != new_version and current_version > 0
|
38
38
|
((current_version + 1)..new_version).each do |upgrade_to_version|
|
39
|
-
migration_class =
|
40
|
-
|
39
|
+
migration_class = begin
|
40
|
+
Class.const_get("Database::MigrateToVersion#{upgrade_to_version}")
|
41
|
+
rescue NameError
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
if migration_class
|
41
45
|
begin
|
42
|
-
|
46
|
+
migration_class.new(@env).migrate
|
43
47
|
ensure
|
44
48
|
after_migration_block.call if after_migration_block
|
45
49
|
end
|
@@ -47,7 +51,6 @@ module Sequent
|
|
47
51
|
end
|
48
52
|
end
|
49
53
|
end
|
50
|
-
|
51
54
|
end
|
52
55
|
end
|
53
56
|
end
|
data/lib/sequent/sequent.rb
CHANGED
@@ -1,3 +1,29 @@
|
|
1
1
|
require_relative 'core/core'
|
2
2
|
require_relative 'migrations/migrations'
|
3
3
|
require_relative 'test/test'
|
4
|
+
require_relative 'configuration'
|
5
|
+
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
module Sequent
|
9
|
+
def self.logger
|
10
|
+
@logger ||= Logger.new(STDOUT).tap {|l| l.level = Logger::INFO }
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.logger=(logger)
|
14
|
+
@logger = logger
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.configure
|
18
|
+
yield Configuration.instance
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.configuration
|
22
|
+
Configuration.instance
|
23
|
+
end
|
24
|
+
|
25
|
+
# Short hand for Sequent.configuration.command_service
|
26
|
+
def self.command_service
|
27
|
+
configuration.command_service
|
28
|
+
end
|
29
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'thread_safe'
|
2
|
+
|
1
3
|
module Sequent
|
2
4
|
module Test
|
3
5
|
##
|
@@ -36,32 +38,68 @@ module Sequent
|
|
36
38
|
|
37
39
|
class FakeEventStore
|
38
40
|
def initialize
|
39
|
-
@
|
41
|
+
@event_streams = {}
|
42
|
+
@all_events = {}
|
40
43
|
@stored_events = []
|
41
44
|
end
|
42
45
|
|
43
46
|
def load_events(aggregate_id)
|
44
|
-
|
47
|
+
event_stream = @event_streams[aggregate_id]
|
48
|
+
return nil unless event_stream
|
49
|
+
[event_stream, deserialize_events(@all_events[aggregate_id])]
|
50
|
+
end
|
51
|
+
|
52
|
+
def find_event_stream(aggregate_id)
|
53
|
+
@event_streams[aggregate_id]
|
45
54
|
end
|
46
55
|
|
47
56
|
def stored_events
|
48
57
|
deserialize_events(@stored_events)
|
49
58
|
end
|
50
59
|
|
51
|
-
def commit_events(_,
|
52
|
-
|
53
|
-
|
54
|
-
|
60
|
+
def commit_events(_, streams_with_events)
|
61
|
+
streams_with_events.each do |event_stream, events|
|
62
|
+
serialized = serialize_events(events)
|
63
|
+
@event_streams[event_stream.aggregate_id] = event_stream
|
64
|
+
@all_events[event_stream.aggregate_id] ||= []
|
65
|
+
@all_events[event_stream.aggregate_id] += serialized
|
66
|
+
@stored_events += serialized
|
67
|
+
end
|
55
68
|
end
|
56
69
|
|
57
70
|
def given_events(events)
|
58
|
-
|
71
|
+
commit_events(nil, to_event_streams(events))
|
59
72
|
@stored_events = []
|
60
73
|
end
|
61
74
|
|
62
75
|
private
|
76
|
+
|
77
|
+
def to_event_streams(events)
|
78
|
+
# Specs use a simple list of given events. We need a mapping from StreamRecord to the associated events for the event store.
|
79
|
+
streams_by_aggregate_id = {}
|
80
|
+
events.map do |event|
|
81
|
+
event_stream = streams_by_aggregate_id.fetch(event.aggregate_id) do |aggregate_id|
|
82
|
+
streams_by_aggregate_id[aggregate_id] =
|
83
|
+
find_event_stream(aggregate_id) ||
|
84
|
+
begin
|
85
|
+
aggregate_type = FakeEventStore.aggregate_type_for_event(event)
|
86
|
+
raise "cannot find aggregate type associated with creation event #{event}, did you include an event handler in your aggregate for this event?" unless aggregate_type
|
87
|
+
Sequent::Core::EventStream.new(aggregate_type: aggregate_type.name, aggregate_id: aggregate_id)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
[event_stream, [event]]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.aggregate_type_for_event(event)
|
95
|
+
@event_to_aggregate_type ||= ThreadSafe::Cache.new
|
96
|
+
@event_to_aggregate_type.fetch_or_store(event.class) do |klass|
|
97
|
+
Sequent::Core::AggregateRoot.descendants.find { |x| x.message_mapping.has_key?(klass) }
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
63
101
|
def serialize_events(events)
|
64
|
-
events.map { |event| [event.class.name
|
102
|
+
events.map { |event| [event.class.name, Sequent::Core::Oj.dump(event)] }
|
65
103
|
end
|
66
104
|
|
67
105
|
def deserialize_events(events)
|
@@ -73,7 +111,6 @@ module Sequent
|
|
73
111
|
end
|
74
112
|
|
75
113
|
def given_events *events
|
76
|
-
raise ArgumentError.new("events can not be nil") if events.compact.empty?
|
77
114
|
@event_store.given_events(events)
|
78
115
|
end
|
79
116
|
|
@@ -85,9 +122,9 @@ module Sequent
|
|
85
122
|
end
|
86
123
|
|
87
124
|
def then_events *events
|
88
|
-
@event_store.stored_events.map(&:class).
|
125
|
+
expect(@event_store.stored_events.map(&:class)).to eq(events.map(&:class))
|
89
126
|
@event_store.stored_events.zip(events).each do |actual, expected|
|
90
|
-
Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(actual.payload)).
|
127
|
+
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
|
91
128
|
end
|
92
129
|
end
|
93
130
|
|
data/lib/version.rb
CHANGED
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: 0.1.
|
4
|
+
version: 0.1.5
|
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: 2015-
|
13
|
+
date: 2015-06-25 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activerecord
|
@@ -68,6 +68,20 @@ dependencies:
|
|
68
68
|
- - "~>"
|
69
69
|
- !ruby/object:Gem::Version
|
70
70
|
version: '2.10'
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: thread_safe
|
73
|
+
requirement: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - "~>"
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 0.3.5
|
78
|
+
type: :runtime
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - "~>"
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: 0.3.5
|
71
85
|
- !ruby/object:Gem::Dependency
|
72
86
|
name: rspec
|
73
87
|
requirement: !ruby/object:Gem::Requirement
|
@@ -124,6 +138,20 @@ dependencies:
|
|
124
138
|
- - "~>"
|
125
139
|
- !ruby/object:Gem::Version
|
126
140
|
version: '10.4'
|
141
|
+
- !ruby/object:Gem::Dependency
|
142
|
+
name: pry
|
143
|
+
requirement: !ruby/object:Gem::Requirement
|
144
|
+
requirements:
|
145
|
+
- - "~>"
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
version: '0.10'
|
148
|
+
type: :development
|
149
|
+
prerelease: false
|
150
|
+
version_requirements: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - "~>"
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0.10'
|
127
155
|
description: Sequent is a CQRS and event sourcing framework for Ruby.
|
128
156
|
email:
|
129
157
|
- lars.vonk@gmail.com
|
@@ -133,9 +161,12 @@ executables: []
|
|
133
161
|
extensions: []
|
134
162
|
extra_rdoc_files: []
|
135
163
|
files:
|
164
|
+
- db/sequent_schema.rb
|
136
165
|
- lib/sequent.rb
|
166
|
+
- lib/sequent/configuration.rb
|
137
167
|
- lib/sequent/core/aggregate_repository.rb
|
138
168
|
- lib/sequent/core/aggregate_root.rb
|
169
|
+
- lib/sequent/core/aggregate_snapshotter.rb
|
139
170
|
- lib/sequent/core/base_command_handler.rb
|
140
171
|
- lib/sequent/core/base_event_handler.rb
|
141
172
|
- lib/sequent/core/command.rb
|
@@ -167,6 +198,8 @@ files:
|
|
167
198
|
- lib/sequent/core/record_sessions/record_sessions.rb
|
168
199
|
- lib/sequent/core/record_sessions/replay_events_session.rb
|
169
200
|
- lib/sequent/core/sequent_oj.rb
|
201
|
+
- lib/sequent/core/snapshots.rb
|
202
|
+
- lib/sequent/core/stream_record.rb
|
170
203
|
- lib/sequent/core/tenant_event_store.rb
|
171
204
|
- lib/sequent/core/transactions/active_record_transaction_provider.rb
|
172
205
|
- lib/sequent/core/transactions/no_transactions.rb
|