sandthorn_sequel_projection 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.autotest +3 -0
- data/.gitignore +23 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +130 -0
- data/Rakefile +16 -0
- data/lib/sandthorn_sequel_projection.rb +57 -0
- data/lib/sandthorn_sequel_projection/cursor.rb +39 -0
- data/lib/sandthorn_sequel_projection/errors.rb +14 -0
- data/lib/sandthorn_sequel_projection/event_handler.rb +49 -0
- data/lib/sandthorn_sequel_projection/event_handler_collection.rb +27 -0
- data/lib/sandthorn_sequel_projection/lock.rb +83 -0
- data/lib/sandthorn_sequel_projection/manifest.rb +10 -0
- data/lib/sandthorn_sequel_projection/processed_events_tracker.rb +94 -0
- data/lib/sandthorn_sequel_projection/projection.rb +65 -0
- data/lib/sandthorn_sequel_projection/runner.rb +62 -0
- data/lib/sandthorn_sequel_projection/tasks.rb +14 -0
- data/lib/sandthorn_sequel_projection/utilities.rb +5 -0
- data/lib/sandthorn_sequel_projection/utilities/core_extensions/array_wrap.rb +13 -0
- data/lib/sandthorn_sequel_projection/utilities/null_proc.rb +9 -0
- data/lib/sandthorn_sequel_projection/version.rb +3 -0
- data/sandthorn_sequel_projection.gemspec +37 -0
- data/spec/cursor_spec.rb +44 -0
- data/spec/event_handler_collection_spec.rb +39 -0
- data/spec/event_handler_spec.rb +82 -0
- data/spec/integration/projection_spec.rb +126 -0
- data/spec/lock_spec.rb +141 -0
- data/spec/processed_events_tracker_spec.rb +79 -0
- data/spec/projection_spec.rb +91 -0
- data/spec/runner_spec.rb +32 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/support/mock_event_store.rb +39 -0
- data/spec/test_data/event_data.json +1876 -0
- data/spec/utilities/null_proc_spec.rb +14 -0
- metadata +262 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module SandthornSequelProjection
|
4
|
+
|
5
|
+
class MyProjection < Projection
|
6
|
+
|
7
|
+
migration("20150303-1") do |db_con|
|
8
|
+
db_con.create_table?(table_name) do
|
9
|
+
primary_key :id
|
10
|
+
String :aggregate_id
|
11
|
+
TrueClass :on_sale, default: false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
define_event_handlers do |handlers|
|
16
|
+
handlers.define product_added: {
|
17
|
+
aggregate_type: "SandthornProduct",
|
18
|
+
event_name: "new"
|
19
|
+
}
|
20
|
+
handlers.define on_sale: {
|
21
|
+
aggregate_type: "SandthornProduct",
|
22
|
+
event_name: "product_on_sale"
|
23
|
+
}
|
24
|
+
handlers.define removed_from_sale: {
|
25
|
+
aggregate_type: "SandthornProduct",
|
26
|
+
event_names: ["removed_from_sale", "destroyed"]
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def on_sale(event)
|
31
|
+
aggregate_id = event[:aggregate_id]
|
32
|
+
add_aggregate(aggregate_id)
|
33
|
+
table.where(aggregate_id: aggregate_id).update(on_sale: true)
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_aggregate(aggregate_id)
|
37
|
+
db_connection.transaction do
|
38
|
+
exists = table.where(aggregate_id: aggregate_id).any?
|
39
|
+
return exists || table.insert(aggregate_id: aggregate_id)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def product_added(event)
|
44
|
+
add_aggregate(event[:aggregate_id])
|
45
|
+
end
|
46
|
+
|
47
|
+
def removed_from_sale(event)
|
48
|
+
aggregate_id = event[:aggregate_id]
|
49
|
+
table.where(aggregate_id: aggregate_id).update(on_sale: false)
|
50
|
+
end
|
51
|
+
|
52
|
+
def table
|
53
|
+
db_connection[table_name]
|
54
|
+
end
|
55
|
+
|
56
|
+
def table_name
|
57
|
+
:products_on_sale
|
58
|
+
end
|
59
|
+
|
60
|
+
def aggregates_on_sale_ids
|
61
|
+
table.where(on_sale: true).select_map(:aggregate_id)
|
62
|
+
end
|
63
|
+
|
64
|
+
def aggregate_ids
|
65
|
+
table.select_map(:aggregate_id)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe MyProjection do
|
70
|
+
|
71
|
+
def db_connection
|
72
|
+
SandthornSequelProjection.configuration.db_connection
|
73
|
+
end
|
74
|
+
|
75
|
+
def table
|
76
|
+
db_connection[projection.table_name]
|
77
|
+
end
|
78
|
+
|
79
|
+
let(:projection) { MyProjection.new }
|
80
|
+
|
81
|
+
before do
|
82
|
+
Sandthorn.default_event_store = MockEventStore.with_data
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "#migrate!" do
|
86
|
+
it "creates the wanted table" do
|
87
|
+
projection.migrate!
|
88
|
+
expect(table.all).to eq([])
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "#update" do
|
93
|
+
before do
|
94
|
+
projection.migrate!
|
95
|
+
end
|
96
|
+
methods = [:product_added, :removed_from_sale]
|
97
|
+
methods.each do |method|
|
98
|
+
it "calls #{method}" do
|
99
|
+
expect(projection).to receive(method).at_least(:once).and_call_original
|
100
|
+
projection.update!
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "data" do
|
105
|
+
before { projection.update! }
|
106
|
+
it "sets the on_sale boolean correctly" do
|
107
|
+
expected = %w[
|
108
|
+
ac1be457-b6b9-4dad-900b-acb400f810df
|
109
|
+
]
|
110
|
+
expect(projection.aggregates_on_sale_ids).to eq(expected)
|
111
|
+
end
|
112
|
+
|
113
|
+
it "sets the correct last processed sequence number" do
|
114
|
+
expect(projection.last_processed_sequence_number).to eq(128)
|
115
|
+
end
|
116
|
+
|
117
|
+
let(:json_events) { JSON.parse(File.read("./spec/test_data/event_data.json"), symbolize_names: true) }
|
118
|
+
let(:aggregate_ids) { json_events.map{|event| event[:aggregate_id] }.uniq }
|
119
|
+
|
120
|
+
it "records all aggregate ids" do
|
121
|
+
expect(projection.aggregate_ids).to contain_exactly(*aggregate_ids)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
data/spec/lock_spec.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
module SandthornSequelProjection
|
2
|
+
|
3
|
+
describe Lock do
|
4
|
+
let(:db_connection) { Sequel.sqlite }
|
5
|
+
let(:table_name) { ProcessedEventsTracker::DEFAULT_TABLE_NAME }
|
6
|
+
let(:lock) { SandthornSequelProjection::Lock.new("foo", db_connection) }
|
7
|
+
|
8
|
+
before(:each) do
|
9
|
+
db_connection.create_table?(table_name) do
|
10
|
+
String :identifier
|
11
|
+
DateTime :locked_at, null: true
|
12
|
+
end
|
13
|
+
db_connection[table_name].insert(identifier: lock.identifier)
|
14
|
+
end
|
15
|
+
|
16
|
+
def null_lock
|
17
|
+
db_connection[table_name].where(identifier: lock.identifier).update(locked_at: nil)
|
18
|
+
end
|
19
|
+
|
20
|
+
def set_lock(time = Time.now)
|
21
|
+
db_connection[table_name].where(identifier: lock.identifier).update(locked_at: time)
|
22
|
+
end
|
23
|
+
|
24
|
+
def lock_row
|
25
|
+
db_connection[table_name].where(identifier: lock.identifier).first
|
26
|
+
end
|
27
|
+
|
28
|
+
def lock_column
|
29
|
+
lock_row[:locked_at]
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "#locked?" do
|
33
|
+
context "when the lock column is nulled" do
|
34
|
+
it "should return false" do
|
35
|
+
null_lock
|
36
|
+
expect(lock.locked?).to be_falsey
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "when the lock is locked" do
|
41
|
+
it "should return true" do
|
42
|
+
set_lock
|
43
|
+
expect(lock.locked?).to be_truthy
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "#unlocked" do
|
49
|
+
context "when the lock column is nulled" do
|
50
|
+
it "should return true" do
|
51
|
+
expect(lock.unlocked?).to be_truthy
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "#attempt_lock" do
|
57
|
+
context "when there is no previous lock" do
|
58
|
+
it "creates the lock" do
|
59
|
+
null_lock
|
60
|
+
expect(lock.attempt_lock).to be_truthy
|
61
|
+
expect(lock.locked?).to be_truthy
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context "when there is a previous lock" do
|
66
|
+
context "and it has not expired" do
|
67
|
+
it "returns false and doesn't set the lock" do
|
68
|
+
set_lock
|
69
|
+
locked_at = lock_column
|
70
|
+
expect(lock.attempt_lock).to be_falsey
|
71
|
+
locked_at_after = lock_column
|
72
|
+
expect(locked_at_after).to eq(locked_at)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context "and it has expired" do
|
77
|
+
it "returns true at sets a new lock" do
|
78
|
+
set_lock(Time.now - lock.timeout)
|
79
|
+
locked_at = lock_column
|
80
|
+
expect(lock.attempt_lock).to be_truthy
|
81
|
+
locked_at_after = lock_column
|
82
|
+
expect(locked_at_after).to_not eq(locked_at)
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "#release" do
|
90
|
+
it "releases the lock" do
|
91
|
+
set_lock
|
92
|
+
expect(lock.release).to be_truthy
|
93
|
+
expect(lock_column).to be_nil
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "#aqcuire" do
|
98
|
+
context "when the lock is unlocked" do
|
99
|
+
it "executes the block" do
|
100
|
+
null_lock
|
101
|
+
expect { |b| lock.acquire(&b) }.to yield_control
|
102
|
+
end
|
103
|
+
|
104
|
+
it "has the lock during execution, and releases the lock afterwards" do
|
105
|
+
null_lock
|
106
|
+
lock.acquire do
|
107
|
+
expect(lock.locked?).to be_truthy
|
108
|
+
end
|
109
|
+
expect(lock_column).to be_nil
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context "when the lock is locked" do
|
114
|
+
it "doesn't yield" do
|
115
|
+
set_lock
|
116
|
+
expect { |b| lock.acquire(&b) }.to_not yield_control
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context "when an exception is raised" do
|
121
|
+
MyMegaException = Class.new(StandardError)
|
122
|
+
|
123
|
+
it "releases the lock" do
|
124
|
+
begin
|
125
|
+
lock.acquire do
|
126
|
+
expect(lock.locked?).to be_truthy
|
127
|
+
raise MyMegaException
|
128
|
+
end
|
129
|
+
rescue
|
130
|
+
ensure
|
131
|
+
expect(lock.locked?).to be_falsey
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
it "reraises the exception" do
|
136
|
+
expect { lock.acquire { raise MyMegaException } }.to raise_exception(MyMegaException)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module SandthornSequelProjection
|
4
|
+
describe ProcessedEventsTracker do
|
5
|
+
|
6
|
+
describe "migrated specs" do
|
7
|
+
let(:event_store) { Sandthorn.default_event_store }
|
8
|
+
let(:db_connection) { SandthornSequelProjection.configuration.db_connection }
|
9
|
+
let(:tracker) { ProcessedEventsTracker.new(identifier: :foo, event_store: event_store) }
|
10
|
+
describe "::initialize" do
|
11
|
+
it "ensures that the tracker row is present" do
|
12
|
+
tracker_row = db_connection[tracker.table_name].where(identifier: tracker.identifier).first
|
13
|
+
expect(tracker_row).to_not be_nil
|
14
|
+
expect(tracker_row[:identifier]).to eq(tracker.identifier)
|
15
|
+
expect(tracker_row[:last_processed_sequence_number]).to eq(0)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#last_processed_sequence_number' do
|
20
|
+
it "returns the integer in the db" do
|
21
|
+
db_connection[tracker.table_name].
|
22
|
+
where(identifier: tracker.identifier).
|
23
|
+
update(last_processed_sequence_number: 12)
|
24
|
+
expect(tracker.last_processed_sequence_number).to eq(12)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#process_events" do
|
29
|
+
let(:events) { [{sequence_number: 1}, {sequence_number: 2}] }
|
30
|
+
around do |example|
|
31
|
+
old_batch_size = SandthornSequelProjection.batch_size
|
32
|
+
SandthornSequelProjection.configuration.batch_size = 1
|
33
|
+
example.run
|
34
|
+
SandthornSequelProjection.configuration.batch_size = old_batch_size
|
35
|
+
end
|
36
|
+
|
37
|
+
before do
|
38
|
+
tracker.reset
|
39
|
+
Sandthorn.default_event_store.reset
|
40
|
+
events.each { |e| event_store.add(e) }
|
41
|
+
end
|
42
|
+
|
43
|
+
it "yields events" do
|
44
|
+
expect { |b| tracker.process_events(&b) }.to yield_successive_args([events.first], [events.last])
|
45
|
+
end
|
46
|
+
|
47
|
+
it "sets the last processed number" do
|
48
|
+
tracker.process_events { |*| }
|
49
|
+
expect(tracker.last_processed_sequence_number).to eq(2)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "has the lock during the yield" do
|
53
|
+
tracker.process_events do |*|
|
54
|
+
expect(tracker.lock.locked?).to be_truthy
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
it "has released the lock afterwards" do
|
59
|
+
tracker.process_events { |*| }
|
60
|
+
expect(tracker.lock.locked?).to be_falsey
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "non-migrated specs" do
|
67
|
+
let(:db_connection) { Sequel.sqlite }
|
68
|
+
describe "::migrate!" do
|
69
|
+
it "creates the requisite database table" do
|
70
|
+
expect { ProcessedEventsTracker.migrate!(db_connection) }.to_not raise_error
|
71
|
+
expect(db_connection.table_exists?(ProcessedEventsTracker.table_name)).to be_truthy
|
72
|
+
expected_columns = :identifier, :last_processed_sequence_number, :locked_at
|
73
|
+
expect(db_connection[ProcessedEventsTracker.table_name].columns).to include(*expected_columns)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module SandthornSequelProjection
|
4
|
+
|
5
|
+
module MyModule
|
6
|
+
class TestProjection < Projection
|
7
|
+
|
8
|
+
def foo
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
def bar
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
class WithHandlers < TestProjection
|
19
|
+
|
20
|
+
define_event_handlers do |handlers|
|
21
|
+
handlers.define(:foo)
|
22
|
+
handlers.define(:bar)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe Projection do
|
29
|
+
let(:projection) { MyModule::WithHandlers.new }
|
30
|
+
|
31
|
+
describe "::initialize" do
|
32
|
+
it "sets the handlers on the instance" do
|
33
|
+
handlers = projection.event_handlers
|
34
|
+
expect(handlers.length).to eq(2)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '::identifier' do
|
39
|
+
it "snake cases the class identifier" do
|
40
|
+
expect(MyModule::TestProjection.identifier).to eq("sandthorn_sequel_projection_my_module_test_projection")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "#update!" do
|
45
|
+
|
46
|
+
before do
|
47
|
+
event_store = Sandthorn.default_event_store
|
48
|
+
event_store.reset
|
49
|
+
event_store.add_event({sequence_number: 1})
|
50
|
+
event_store.add_event({sequence_number: 2})
|
51
|
+
end
|
52
|
+
|
53
|
+
it "fetches events and passes them on to the handlers" do
|
54
|
+
projection = MyModule::WithHandlers.new
|
55
|
+
handlers = projection.event_handlers
|
56
|
+
handlers.each do |handler|
|
57
|
+
expect(handler).to receive(:handle).twice
|
58
|
+
end
|
59
|
+
projection.update!
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "::event_store" do
|
64
|
+
let(:klass) { Class.new(Projection) }
|
65
|
+
context "when given an event store name" do
|
66
|
+
it "sets the event store" do
|
67
|
+
klass.event_store(:foo)
|
68
|
+
expect(klass.event_store_name).to eq(:foo)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "when given no argument" do
|
73
|
+
context "when an event store has been configured" do
|
74
|
+
before do
|
75
|
+
klass.event_store(:foo)
|
76
|
+
end
|
77
|
+
it "fetches the event store" do
|
78
|
+
expect(SandthornSequelProjection).to receive(:find_event_store).with(:foo)
|
79
|
+
klass.event_store
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "::define_event_handlers" do
|
86
|
+
it "yields an EventHandlerCollection" do
|
87
|
+
expect { |b| MyModule::TestProjection.define_event_handlers(&b) }.to yield_with_args(EventHandlerCollection)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/spec/runner_spec.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module SandthornSequelProjection
|
2
|
+
describe Runner do
|
3
|
+
|
4
|
+
module FakeProjection
|
5
|
+
def initialize(*); end
|
6
|
+
def migrate!; end
|
7
|
+
def update!; end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Projection1; include FakeProjection; end
|
11
|
+
class Projection2; include FakeProjection; end
|
12
|
+
|
13
|
+
let(:manifest) { Manifest.new(Projection1, Projection2) }
|
14
|
+
|
15
|
+
let(:runner) { SandthornSequelProjection::Runner.new(manifest) }
|
16
|
+
|
17
|
+
describe "#run" do
|
18
|
+
it "migrates all projections" do
|
19
|
+
expect_any_instance_of(Projection1).to receive(:migrate!).once
|
20
|
+
expect_any_instance_of(Projection2).to receive(:migrate!).once
|
21
|
+
runner.run(false)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "updates all projections" do
|
25
|
+
expect_any_instance_of(Projection1).to receive(:update!).once
|
26
|
+
expect_any_instance_of(Projection2).to receive(:update!).once
|
27
|
+
runner.run(false)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|