sandthorn_sequel_projection 0.0.1
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 +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
|