sandthorn_sequel_projection 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +3 -0
  3. data/.gitignore +23 -0
  4. data/.rspec +2 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +9 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +130 -0
  11. data/Rakefile +16 -0
  12. data/lib/sandthorn_sequel_projection.rb +57 -0
  13. data/lib/sandthorn_sequel_projection/cursor.rb +39 -0
  14. data/lib/sandthorn_sequel_projection/errors.rb +14 -0
  15. data/lib/sandthorn_sequel_projection/event_handler.rb +49 -0
  16. data/lib/sandthorn_sequel_projection/event_handler_collection.rb +27 -0
  17. data/lib/sandthorn_sequel_projection/lock.rb +83 -0
  18. data/lib/sandthorn_sequel_projection/manifest.rb +10 -0
  19. data/lib/sandthorn_sequel_projection/processed_events_tracker.rb +94 -0
  20. data/lib/sandthorn_sequel_projection/projection.rb +65 -0
  21. data/lib/sandthorn_sequel_projection/runner.rb +62 -0
  22. data/lib/sandthorn_sequel_projection/tasks.rb +14 -0
  23. data/lib/sandthorn_sequel_projection/utilities.rb +5 -0
  24. data/lib/sandthorn_sequel_projection/utilities/core_extensions/array_wrap.rb +13 -0
  25. data/lib/sandthorn_sequel_projection/utilities/null_proc.rb +9 -0
  26. data/lib/sandthorn_sequel_projection/version.rb +3 -0
  27. data/sandthorn_sequel_projection.gemspec +37 -0
  28. data/spec/cursor_spec.rb +44 -0
  29. data/spec/event_handler_collection_spec.rb +39 -0
  30. data/spec/event_handler_spec.rb +82 -0
  31. data/spec/integration/projection_spec.rb +126 -0
  32. data/spec/lock_spec.rb +141 -0
  33. data/spec/processed_events_tracker_spec.rb +79 -0
  34. data/spec/projection_spec.rb +91 -0
  35. data/spec/runner_spec.rb +32 -0
  36. data/spec/spec_helper.rb +23 -0
  37. data/spec/support/mock_event_store.rb +39 -0
  38. data/spec/test_data/event_data.json +1876 -0
  39. data/spec/utilities/null_proc_spec.rb +14 -0
  40. 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
@@ -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